feat(core-loop): /app 진입과 /space 복구 흐름 구현
This commit is contained in:
@@ -4,11 +4,24 @@ import type { FormEvent } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { copy } from '@/shared/i18n';
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
import {
|
||||
HUD_FIELD,
|
||||
HUD_OPTION_CHEVRON,
|
||||
HUD_OPTION_ROW,
|
||||
HUD_OPTION_ROW_PRIMARY,
|
||||
HUD_TEXT_LINK,
|
||||
HUD_TEXT_LINK_STRONG,
|
||||
HUD_TRAY_HAIRLINE,
|
||||
HUD_TRAY_LAYER,
|
||||
HUD_TRAY_SHELL,
|
||||
} from './overlayStyles';
|
||||
|
||||
interface GoalCompleteSheetProps {
|
||||
open: boolean;
|
||||
currentGoal: string;
|
||||
preferredView?: 'choice' | 'next';
|
||||
onConfirm: (nextGoal: string) => Promise<boolean> | boolean;
|
||||
onFinish: () => Promise<boolean> | boolean;
|
||||
onRest: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
@@ -16,18 +29,22 @@ interface GoalCompleteSheetProps {
|
||||
export const GoalCompleteSheet = ({
|
||||
open,
|
||||
currentGoal,
|
||||
preferredView = 'choice',
|
||||
onConfirm,
|
||||
onFinish,
|
||||
onRest,
|
||||
onClose,
|
||||
}: GoalCompleteSheetProps) => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [draft, setDraft] = useState('');
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
const [submissionMode, setSubmissionMode] = useState<'next' | 'finish' | null>(null);
|
||||
const [view, setView] = useState<'choice' | 'next'>('choice');
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
setDraft('');
|
||||
setView(preferredView);
|
||||
}, 0);
|
||||
|
||||
return () => {
|
||||
@@ -35,6 +52,10 @@ export const GoalCompleteSheet = ({
|
||||
};
|
||||
}
|
||||
|
||||
if (view !== 'next') {
|
||||
return;
|
||||
}
|
||||
|
||||
const rafId = window.requestAnimationFrame(() => {
|
||||
inputRef.current?.focus();
|
||||
});
|
||||
@@ -42,7 +63,15 @@ export const GoalCompleteSheet = ({
|
||||
return () => {
|
||||
window.cancelAnimationFrame(rafId);
|
||||
};
|
||||
}, [open]);
|
||||
}, [open, preferredView, view]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
setView(preferredView);
|
||||
}, [open, preferredView]);
|
||||
|
||||
const placeholder = useMemo(() => {
|
||||
const trimmed = currentGoal.trim();
|
||||
@@ -55,6 +84,9 @@ export const GoalCompleteSheet = ({
|
||||
}, [currentGoal]);
|
||||
|
||||
const canConfirm = draft.trim().length > 0;
|
||||
const isSubmitting = submissionMode !== null;
|
||||
const trimmedCurrentGoal = currentGoal.trim();
|
||||
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -62,7 +94,7 @@ export const GoalCompleteSheet = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
setSubmissionMode('next');
|
||||
|
||||
try {
|
||||
const didAdvance = await onConfirm(draft.trim());
|
||||
@@ -71,7 +103,25 @@ export const GoalCompleteSheet = ({
|
||||
onClose();
|
||||
}
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
setSubmissionMode(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinish = async () => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmissionMode('finish');
|
||||
|
||||
try {
|
||||
const didFinish = await onFinish();
|
||||
|
||||
if (didFinish) {
|
||||
onClose();
|
||||
}
|
||||
} finally {
|
||||
setSubmissionMode(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -85,12 +135,9 @@ export const GoalCompleteSheet = ({
|
||||
)}
|
||||
aria-hidden={!open}
|
||||
>
|
||||
<section className="pointer-events-auto relative mt-3 w-full overflow-hidden rounded-[22px] border border-white/10 bg-[#0f1115]/28 px-5 py-4 text-white shadow-[0_12px_28px_rgba(2,6,23,0.14)] backdrop-blur-[8px] backdrop-saturate-125">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 rounded-[22px] bg-[linear-gradient(180deg,rgba(255,255,255,0.08)_0%,rgba(255,255,255,0.025)_24%,rgba(255,255,255,0.01)_100%)]"
|
||||
/>
|
||||
<div aria-hidden className="pointer-events-none absolute inset-x-0 top-0 h-px bg-white/16" />
|
||||
<section className={HUD_TRAY_SHELL}>
|
||||
<div aria-hidden className={HUD_TRAY_LAYER} />
|
||||
<div aria-hidden className={HUD_TRAY_HAIRLINE} />
|
||||
|
||||
<header className="relative flex items-start justify-between gap-2">
|
||||
<div>
|
||||
@@ -109,32 +156,115 @@ export const GoalCompleteSheet = ({
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<form className="relative mt-3 space-y-3" onSubmit={handleSubmit}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={draft}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="h-11 w-full rounded-[18px] border border-white/10 bg-black/14 px-3.5 text-[0.98rem] tracking-tight text-white placeholder:text-white/30 focus:border-white/20 focus:bg-black/20 focus:outline-none focus:ring-2 focus:ring-white/8"
|
||||
/>
|
||||
<footer className="mt-3 flex items-center justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRest}
|
||||
disabled={isSubmitting}
|
||||
className="inline-flex h-8 items-center justify-center rounded-full border border-white/10 bg-black/14 px-3 text-[11px] font-medium tracking-[0.14em] text-white/62 backdrop-blur-md transition-all hover:bg-black/20 hover:text-white/84 disabled:cursor-not-allowed disabled:border-white/6 disabled:bg-black/10 disabled:text-white/26"
|
||||
>
|
||||
{copy.space.goalComplete.restButton}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!canConfirm || isSubmitting}
|
||||
className="inline-flex h-8 items-center justify-center rounded-full border border-white/12 bg-black/18 px-3.5 text-[11px] font-semibold tracking-[0.16em] text-white/88 backdrop-blur-md transition-all hover:bg-black/24 hover:text-white disabled:cursor-not-allowed disabled:border-white/8 disabled:bg-black/10 disabled:text-white/30"
|
||||
>
|
||||
{isSubmitting ? copy.space.goalComplete.confirmPending : copy.space.goalComplete.confirmButton}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
{view === 'choice' ? (
|
||||
<div className="relative mt-3 space-y-3">
|
||||
{trimmedCurrentGoal ? (
|
||||
<div className="rounded-[18px] border border-white/8 bg-black/10 px-3.5 py-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/36">
|
||||
{copy.space.goalComplete.currentGoalLabel}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-[14px] text-white/86">{trimmedCurrentGoal}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<footer className="mt-4 space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFinish}
|
||||
disabled={isSubmitting}
|
||||
className={HUD_OPTION_ROW}
|
||||
>
|
||||
<div>
|
||||
<p className="text-[13px] font-medium tracking-[0.01em] text-white/78">
|
||||
{submissionMode === 'finish'
|
||||
? copy.space.goalComplete.finishPending
|
||||
: copy.space.goalComplete.finishButton}
|
||||
</p>
|
||||
<p className="mt-1 text-[12px] text-white/44">
|
||||
{copy.space.goalComplete.finishDescription}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRest}
|
||||
disabled={isSubmitting}
|
||||
className={HUD_OPTION_ROW}
|
||||
>
|
||||
<div>
|
||||
<p className="text-[13px] font-medium tracking-[0.01em] text-white/78">
|
||||
{copy.space.goalComplete.restButton}
|
||||
</p>
|
||||
<p className="mt-1 text-[12px] text-white/44">
|
||||
{copy.space.goalComplete.restDescription}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView('next')}
|
||||
disabled={isSubmitting}
|
||||
className={cn(HUD_OPTION_ROW, HUD_OPTION_ROW_PRIMARY)}
|
||||
>
|
||||
<div>
|
||||
<p className="text-[13px] font-semibold tracking-[0.01em] text-white/90">
|
||||
{copy.space.goalComplete.chooseNextButton}
|
||||
</p>
|
||||
<p className="mt-1 text-[12px] text-white/48">
|
||||
{copy.space.goalComplete.chooseNextDescription}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
) : (
|
||||
<form className="relative mt-3 space-y-3" onSubmit={handleSubmit}>
|
||||
{trimmedCurrentGoal ? (
|
||||
<div className="rounded-[18px] border border-white/8 bg-black/10 px-3.5 py-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/36">
|
||||
{copy.space.goalComplete.currentGoalLabel}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-[14px] text-white/86">{trimmedCurrentGoal}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-[11px] font-medium uppercase tracking-[0.16em] text-white/36">
|
||||
{copy.space.goalComplete.nextGoalLabel}
|
||||
</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={draft}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={HUD_FIELD}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<footer className="mt-3 flex items-center justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView('choice')}
|
||||
disabled={isSubmitting}
|
||||
className={HUD_TEXT_LINK}
|
||||
>
|
||||
{copy.space.goalComplete.backButton}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!canConfirm || isSubmitting}
|
||||
className={HUD_TEXT_LINK_STRONG}
|
||||
>
|
||||
{submissionMode === 'next'
|
||||
? copy.space.goalComplete.confirmPending
|
||||
: copy.space.goalComplete.confirmButton}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,18 @@
|
||||
|
||||
import { copy } from '@/shared/i18n';
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
import {
|
||||
HUD_OPTION_CHEVRON,
|
||||
HUD_OPTION_ROW,
|
||||
HUD_OPTION_ROW_PRIMARY,
|
||||
HUD_TRAY_HAIRLINE,
|
||||
HUD_TRAY_LAYER,
|
||||
HUD_TRAY_SHELL,
|
||||
} from './overlayStyles';
|
||||
|
||||
interface NextMicroStepPromptProps {
|
||||
open: boolean;
|
||||
goal: string;
|
||||
isSubmitting: boolean;
|
||||
error: string | null;
|
||||
onKeepGoalOnly: () => void;
|
||||
@@ -13,11 +22,14 @@ interface NextMicroStepPromptProps {
|
||||
|
||||
export const NextMicroStepPrompt = ({
|
||||
open,
|
||||
goal,
|
||||
isSubmitting,
|
||||
error,
|
||||
onKeepGoalOnly,
|
||||
onDefineNext,
|
||||
}: NextMicroStepPromptProps) => {
|
||||
const trimmedGoal = goal.trim();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -28,12 +40,9 @@ export const NextMicroStepPrompt = ({
|
||||
)}
|
||||
aria-hidden={!open}
|
||||
>
|
||||
<section className="pointer-events-auto relative mt-3 w-full overflow-hidden rounded-[22px] border border-white/10 bg-[#0f1115]/28 px-5 py-4 text-white shadow-[0_12px_28px_rgba(2,6,23,0.14)] backdrop-blur-[8px] backdrop-saturate-125">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 rounded-[22px] bg-[linear-gradient(180deg,rgba(255,255,255,0.08)_0%,rgba(255,255,255,0.025)_24%,rgba(255,255,255,0.01)_100%)]"
|
||||
/>
|
||||
<div aria-hidden className="pointer-events-none absolute inset-x-0 top-0 h-px bg-white/16" />
|
||||
<section className={HUD_TRAY_SHELL}>
|
||||
<div aria-hidden className={HUD_TRAY_LAYER} />
|
||||
<div aria-hidden className={HUD_TRAY_HAIRLINE} />
|
||||
|
||||
<div className="relative w-full">
|
||||
<p className="text-[11px] font-medium tracking-[0.08em] text-white/42">다음 한 조각</p>
|
||||
@@ -44,28 +53,53 @@ export const NextMicroStepPrompt = ({
|
||||
{copy.space.focusHud.microStepPromptDescription}
|
||||
</p>
|
||||
|
||||
{trimmedGoal ? (
|
||||
<div className="mt-3 rounded-[16px] border border-white/8 bg-black/10 px-3.5 py-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/36">
|
||||
{copy.space.focusHud.intentLabel}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-[14px] text-white/84">{trimmedGoal}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<p className="mt-3 text-[12px] text-rose-100/86">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="mt-3 flex flex-wrap items-center gap-4">
|
||||
<div className="mt-4 space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onKeepGoalOnly}
|
||||
disabled={isSubmitting}
|
||||
className="text-[12px] font-medium tracking-[0.08em] text-white/62 underline decoration-white/16 underline-offset-4 transition-all duration-200 hover:text-white/86 hover:decoration-white/28 disabled:cursor-default disabled:text-white/26 disabled:no-underline"
|
||||
className={HUD_OPTION_ROW}
|
||||
>
|
||||
{copy.space.focusHud.microStepPromptKeep}
|
||||
<div>
|
||||
<p className="text-[13px] font-medium tracking-[0.01em] text-white/78">
|
||||
{copy.space.focusHud.microStepPromptKeep}
|
||||
</p>
|
||||
<p className="mt-1 text-[12px] text-white/44">
|
||||
{copy.space.focusHud.microStepPromptKeepHint}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDefineNext}
|
||||
disabled={isSubmitting}
|
||||
className="text-[12px] font-semibold tracking-[0.08em] text-white/86 underline decoration-white/22 underline-offset-4 transition-all duration-200 hover:text-white hover:decoration-white/36 disabled:cursor-default disabled:text-white/30 disabled:no-underline"
|
||||
className={cn(HUD_OPTION_ROW, HUD_OPTION_ROW_PRIMARY)}
|
||||
>
|
||||
{copy.space.focusHud.microStepPromptDefine}
|
||||
<div>
|
||||
<p className="text-[13px] font-semibold tracking-[0.01em] text-white/90">
|
||||
{copy.space.focusHud.microStepPromptDefine}
|
||||
</p>
|
||||
<p className="mt-1 text-[12px] text-white/48">
|
||||
{copy.space.focusHud.microStepPromptDefineHint}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
93
src/widgets/space-focus-hud/ui/PauseRefocusPrompt.tsx
Normal file
93
src/widgets/space-focus-hud/ui/PauseRefocusPrompt.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import { copy } from '@/shared/i18n';
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
import {
|
||||
HUD_OPTION_CHEVRON,
|
||||
HUD_OPTION_ROW,
|
||||
HUD_OPTION_ROW_PRIMARY,
|
||||
HUD_PAUSE_BODY,
|
||||
HUD_PAUSE_EYEBROW,
|
||||
HUD_PAUSE_TITLE,
|
||||
HUD_TRAY_HAIRLINE,
|
||||
HUD_TRAY_LAYER,
|
||||
HUD_TRAY_SHELL,
|
||||
} from './overlayStyles';
|
||||
|
||||
interface PauseRefocusPromptProps {
|
||||
open: boolean;
|
||||
isBusy: boolean;
|
||||
onRefocus: () => void;
|
||||
onKeepCurrent: () => void;
|
||||
}
|
||||
|
||||
export const PauseRefocusPrompt = ({
|
||||
open,
|
||||
isBusy,
|
||||
onRefocus,
|
||||
onKeepCurrent,
|
||||
}: PauseRefocusPromptProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none w-full overflow-hidden transition-all duration-300 ease-out motion-reduce:duration-0',
|
||||
open
|
||||
? 'max-h-[24rem] translate-y-0 opacity-100'
|
||||
: 'pointer-events-none max-h-0 -translate-y-2 opacity-0',
|
||||
)}
|
||||
aria-hidden={!open}
|
||||
>
|
||||
<section className={HUD_TRAY_SHELL}>
|
||||
<div aria-hidden className={HUD_TRAY_LAYER} />
|
||||
<div aria-hidden className={HUD_TRAY_HAIRLINE} />
|
||||
|
||||
<div className="relative px-6 py-5 md:px-6 md:py-5">
|
||||
<p className={HUD_PAUSE_EYEBROW}>
|
||||
{copy.space.focusHud.pausePromptEyebrow}
|
||||
</p>
|
||||
<h3 className={HUD_PAUSE_TITLE}>
|
||||
{copy.space.focusHud.pausePromptTitle}
|
||||
</h3>
|
||||
<p className={HUD_PAUSE_BODY}>
|
||||
{copy.space.focusHud.pausePromptDescription}
|
||||
</p>
|
||||
|
||||
<div className="mt-5 space-y-2.5 border-t border-white/8 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRefocus}
|
||||
disabled={isBusy}
|
||||
className={cn(HUD_OPTION_ROW, HUD_OPTION_ROW_PRIMARY)}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[14px] font-semibold leading-[1.35] tracking-[-0.01em] text-white/92">
|
||||
{copy.space.focusHud.pausePromptRefocus}
|
||||
</p>
|
||||
<p className="mt-1.5 max-w-[20rem] text-[12px] leading-[1.5] text-white/50">
|
||||
{copy.space.focusHud.pausePromptRefocusHint}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden className={cn(HUD_OPTION_CHEVRON, 'mt-1')}>→</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onKeepCurrent}
|
||||
disabled={isBusy}
|
||||
className={HUD_OPTION_ROW}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[14px] font-medium leading-[1.35] tracking-[-0.01em] text-white/82">
|
||||
{copy.space.focusHud.pausePromptKeep}
|
||||
</p>
|
||||
<p className="mt-1.5 max-w-[20rem] text-[12px] leading-[1.5] text-white/46">
|
||||
{copy.space.focusHud.pausePromptKeepHint}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden className={cn(HUD_OPTION_CHEVRON, 'mt-1')}>→</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,12 +4,21 @@ import type { FormEvent } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { copy } from '@/shared/i18n';
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
import {
|
||||
HUD_FIELD,
|
||||
HUD_TEXT_LINK,
|
||||
HUD_TEXT_LINK_STRONG,
|
||||
HUD_TRAY_HAIRLINE,
|
||||
HUD_TRAY_LAYER,
|
||||
HUD_TRAY_SHELL,
|
||||
} from './overlayStyles';
|
||||
|
||||
interface RefocusSheetProps {
|
||||
open: boolean;
|
||||
goalDraft: string;
|
||||
microStepDraft: string;
|
||||
autoFocusField: 'goal' | 'microStep';
|
||||
submitLabel?: string;
|
||||
isSaving: boolean;
|
||||
error: string | null;
|
||||
onGoalChange: (value: string) => void;
|
||||
@@ -23,6 +32,7 @@ export const RefocusSheet = ({
|
||||
goalDraft,
|
||||
microStepDraft,
|
||||
autoFocusField,
|
||||
submitLabel,
|
||||
isSaving,
|
||||
error,
|
||||
onGoalChange,
|
||||
@@ -91,12 +101,9 @@ export const RefocusSheet = ({
|
||||
)}
|
||||
aria-hidden={!open}
|
||||
>
|
||||
<section className="pointer-events-auto relative mt-3 w-full overflow-hidden rounded-[22px] border border-white/10 bg-[#0f1115]/28 text-white shadow-[0_12px_28px_rgba(2,6,23,0.14)] backdrop-blur-[8px] backdrop-saturate-125">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 rounded-[22px] bg-[linear-gradient(180deg,rgba(255,255,255,0.08)_0%,rgba(255,255,255,0.025)_24%,rgba(255,255,255,0.01)_100%)]"
|
||||
/>
|
||||
<div aria-hidden className="pointer-events-none absolute inset-x-0 top-0 h-px bg-white/16" />
|
||||
<section className={HUD_TRAY_SHELL}>
|
||||
<div aria-hidden className={HUD_TRAY_LAYER} />
|
||||
<div aria-hidden className={HUD_TRAY_HAIRLINE} />
|
||||
|
||||
<header className="relative px-5 pt-4">
|
||||
<div className="min-w-0">
|
||||
@@ -118,7 +125,7 @@ export const RefocusSheet = ({
|
||||
value={goalDraft}
|
||||
onChange={(event) => onGoalChange(event.target.value)}
|
||||
placeholder={copy.space.sessionGoal.placeholder}
|
||||
className="h-11 w-full rounded-[18px] border border-white/10 bg-black/14 px-3.5 text-[1rem] tracking-tight text-white placeholder:text-white/28 focus:border-white/20 focus:bg-black/20 focus:outline-none focus:ring-2 focus:ring-white/8"
|
||||
className={HUD_FIELD}
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -131,7 +138,7 @@ export const RefocusSheet = ({
|
||||
value={microStepDraft}
|
||||
onChange={(event) => onMicroStepChange(event.target.value)}
|
||||
placeholder={copy.space.sessionGoal.hint}
|
||||
className="h-11 w-full rounded-[18px] border border-white/10 bg-black/12 px-3.5 text-[0.98rem] tracking-tight text-white placeholder:text-white/26 focus:border-white/20 focus:bg-black/18 focus:outline-none focus:ring-2 focus:ring-white/8"
|
||||
className={HUD_FIELD}
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -146,16 +153,16 @@ export const RefocusSheet = ({
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={isSaving}
|
||||
className="inline-flex h-8 items-center justify-center rounded-full border border-white/10 bg-black/14 px-3 text-[11px] font-medium tracking-[0.14em] text-white/62 backdrop-blur-md transition-all hover:bg-black/20 hover:text-white/84 disabled:cursor-default disabled:border-white/6 disabled:bg-black/10 disabled:text-white/26"
|
||||
className={HUD_TEXT_LINK}
|
||||
>
|
||||
{copy.common.cancel}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSaving || goalDraft.trim().length === 0}
|
||||
className="inline-flex h-8 items-center justify-center rounded-full border border-white/12 bg-black/18 px-3 text-[11px] font-semibold tracking-[0.16em] text-white/84 backdrop-blur-md transition-all hover:bg-black/24 hover:text-white disabled:cursor-not-allowed disabled:border-white/8 disabled:bg-black/10 disabled:text-white/30"
|
||||
className={HUD_TEXT_LINK_STRONG}
|
||||
>
|
||||
{isSaving ? copy.space.focusHud.refocusApplying : copy.space.focusHud.refocusApply}
|
||||
{isSaving ? copy.space.focusHud.refocusApplying : submitLabel ?? copy.space.focusHud.refocusApply}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
157
src/widgets/space-focus-hud/ui/ReturnPrompt.tsx
Normal file
157
src/widgets/space-focus-hud/ui/ReturnPrompt.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
'use client';
|
||||
|
||||
import { copy } from '@/shared/i18n';
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
import {
|
||||
HUD_OPTION_CHEVRON,
|
||||
HUD_OPTION_ROW,
|
||||
HUD_OPTION_ROW_PRIMARY,
|
||||
HUD_TRAY_HAIRLINE,
|
||||
HUD_TRAY_LAYER,
|
||||
HUD_TRAY_SHELL,
|
||||
} from './overlayStyles';
|
||||
|
||||
interface ReturnPromptProps {
|
||||
open: boolean;
|
||||
mode: 'focus' | 'break';
|
||||
isBusy: boolean;
|
||||
onContinue: () => void;
|
||||
onRefocus: () => void;
|
||||
onRest?: () => void;
|
||||
onNextGoal?: () => void;
|
||||
}
|
||||
|
||||
export const ReturnPrompt = ({
|
||||
open,
|
||||
mode,
|
||||
isBusy,
|
||||
onContinue,
|
||||
onRefocus,
|
||||
onRest,
|
||||
onNextGoal,
|
||||
}: ReturnPromptProps) => {
|
||||
const isBreakReturn = mode === 'break';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none w-full overflow-hidden transition-all duration-300 ease-out motion-reduce:duration-0',
|
||||
open
|
||||
? 'max-h-[22rem] translate-y-0 opacity-100'
|
||||
: 'pointer-events-none max-h-0 -translate-y-2 opacity-0',
|
||||
)}
|
||||
aria-hidden={!open}
|
||||
>
|
||||
<section className={HUD_TRAY_SHELL}>
|
||||
<div aria-hidden className={HUD_TRAY_LAYER} />
|
||||
<div aria-hidden className={HUD_TRAY_HAIRLINE} />
|
||||
|
||||
<div className="relative">
|
||||
<p className="text-[11px] font-medium tracking-[0.08em] text-white/42">
|
||||
{copy.space.focusHud.returnPromptEyebrow}
|
||||
</p>
|
||||
<h3 className="mt-1 text-[1rem] font-medium tracking-tight text-white/94">
|
||||
{isBreakReturn
|
||||
? copy.space.focusHud.returnPromptBreakTitle
|
||||
: copy.space.focusHud.returnPromptFocusTitle}
|
||||
</h3>
|
||||
<p className="mt-1 text-[13px] text-white/58">
|
||||
{isBreakReturn
|
||||
? copy.space.focusHud.returnPromptBreakDescription
|
||||
: copy.space.focusHud.returnPromptFocusDescription}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 space-y-2">
|
||||
{isBreakReturn ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRest}
|
||||
disabled={isBusy}
|
||||
className={cn(HUD_OPTION_ROW, HUD_OPTION_ROW_PRIMARY)}
|
||||
>
|
||||
<div>
|
||||
<p className="text-[13px] font-semibold tracking-[0.01em] text-white/90">
|
||||
{copy.space.focusHud.returnPromptRest}
|
||||
</p>
|
||||
<p className="mt-1 text-[12px] text-white/48">
|
||||
{copy.space.focusHud.returnPromptRestHint}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNextGoal}
|
||||
disabled={isBusy}
|
||||
className={HUD_OPTION_ROW}
|
||||
>
|
||||
<div>
|
||||
<p className="text-[13px] font-medium tracking-[0.01em] text-white/78">
|
||||
{copy.space.focusHud.returnPromptNext}
|
||||
</p>
|
||||
<p className="mt-1 text-[12px] text-white/44">
|
||||
{copy.space.focusHud.returnPromptNextHint}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRefocus}
|
||||
disabled={isBusy}
|
||||
className={HUD_OPTION_ROW}
|
||||
>
|
||||
<div>
|
||||
<p className="text-[13px] font-medium tracking-[0.01em] text-white/78">
|
||||
{copy.space.focusHud.returnPromptRefocus}
|
||||
</p>
|
||||
<p className="mt-1 text-[12px] text-white/44">
|
||||
{copy.space.focusHud.returnPromptRefocusHint}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onContinue}
|
||||
disabled={isBusy}
|
||||
className={cn(HUD_OPTION_ROW, HUD_OPTION_ROW_PRIMARY)}
|
||||
>
|
||||
<div>
|
||||
<p className="text-[13px] font-semibold tracking-[0.01em] text-white/90">
|
||||
{copy.space.focusHud.returnPromptContinue}
|
||||
</p>
|
||||
<p className="mt-1 text-[12px] text-white/48">
|
||||
{copy.space.focusHud.returnPromptContinueHint}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRefocus}
|
||||
disabled={isBusy}
|
||||
className={HUD_OPTION_ROW}
|
||||
>
|
||||
<div>
|
||||
<p className="text-[13px] font-medium tracking-[0.01em] text-white/78">
|
||||
{copy.space.focusHud.returnPromptRefocus}
|
||||
</p>
|
||||
<p className="mt-1 text-[12px] text-white/44">
|
||||
{copy.space.focusHud.returnPromptRefocusHint}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,9 @@ import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud';
|
||||
import { GoalCompleteSheet } from './GoalCompleteSheet';
|
||||
import { IntentCapsule } from './IntentCapsule';
|
||||
import { NextMicroStepPrompt } from './NextMicroStepPrompt';
|
||||
import { PauseRefocusPrompt } from './PauseRefocusPrompt';
|
||||
import { RefocusSheet } from './RefocusSheet';
|
||||
import { ReturnPrompt } from './ReturnPrompt';
|
||||
|
||||
interface SpaceFocusHudWidgetProps {
|
||||
goal: string;
|
||||
@@ -18,11 +20,14 @@ interface SpaceFocusHudWidgetProps {
|
||||
canStartSession?: boolean;
|
||||
canPauseSession?: boolean;
|
||||
canRestartSession?: boolean;
|
||||
returnPromptMode?: 'focus' | 'break' | null;
|
||||
onStartRequested?: () => void;
|
||||
onPauseRequested?: () => void;
|
||||
onRestartRequested?: () => void;
|
||||
onDismissReturnPrompt?: () => void;
|
||||
onIntentUpdate: (payload: { goal?: string; microStep?: string | null }) => boolean | Promise<boolean>;
|
||||
onGoalUpdate: (nextGoal: string) => boolean | Promise<boolean>;
|
||||
onGoalFinish: () => boolean | Promise<boolean>;
|
||||
onStatusMessage: (payload: HudStatusLinePayload) => void;
|
||||
}
|
||||
|
||||
@@ -37,16 +42,19 @@ export const SpaceFocusHudWidget = ({
|
||||
canStartSession = false,
|
||||
canPauseSession = false,
|
||||
canRestartSession = false,
|
||||
returnPromptMode = null,
|
||||
onStartRequested,
|
||||
onPauseRequested,
|
||||
onRestartRequested,
|
||||
onDismissReturnPrompt,
|
||||
onIntentUpdate,
|
||||
onGoalUpdate,
|
||||
onGoalFinish,
|
||||
onStatusMessage,
|
||||
}: SpaceFocusHudWidgetProps) => {
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
const [isRefocusOpen, setRefocusOpen] = useState(false);
|
||||
const [isMicroStepPromptOpen, setMicroStepPromptOpen] = useState(false);
|
||||
const [overlay, setOverlay] = useState<'none' | 'paused' | 'return' | 'refocus' | 'next-beat' | 'complete'>('none');
|
||||
const [refocusOrigin, setRefocusOrigin] = useState<'manual' | 'pause' | 'next-beat' | 'return'>('manual');
|
||||
const [completePreferredView, setCompletePreferredView] = useState<'choice' | 'next'>('choice');
|
||||
const [draftGoal, setDraftGoal] = useState('');
|
||||
const [draftMicroStep, setDraftMicroStep] = useState('');
|
||||
const [autoFocusField, setAutoFocusField] = useState<'goal' | 'microStep'>('goal');
|
||||
@@ -58,7 +66,12 @@ export const SpaceFocusHudWidget = ({
|
||||
const restReminderTimerRef = useRef<number | null>(null);
|
||||
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : copy.space.focusHud.goalFallback;
|
||||
const normalizedMicroStep = microStep?.trim() ? microStep.trim() : null;
|
||||
const isIntentOverlayOpen = isRefocusOpen || isMicroStepPromptOpen || sheetOpen;
|
||||
const isPausedPromptOpen = overlay === 'paused';
|
||||
const isReturnPromptOpen = overlay === 'return';
|
||||
const isRefocusOpen = overlay === 'refocus';
|
||||
const isMicroStepPromptOpen = overlay === 'next-beat';
|
||||
const isCompleteOpen = overlay === 'complete';
|
||||
const isIntentOverlayOpen = overlay !== 'none';
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -69,6 +82,32 @@ export const SpaceFocusHudWidget = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasActiveSession) {
|
||||
setOverlay('none');
|
||||
setIntentError(null);
|
||||
setSavingIntent(false);
|
||||
setRefocusOrigin('manual');
|
||||
setCompletePreferredView('choice');
|
||||
}
|
||||
}, [hasActiveSession]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!returnPromptMode) {
|
||||
if (overlay === 'return') {
|
||||
setOverlay('none');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (overlay === 'complete') {
|
||||
return;
|
||||
}
|
||||
|
||||
setIntentError(null);
|
||||
setOverlay('return');
|
||||
}, [overlay, returnPromptMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visibleRef.current && playbackState === 'running') {
|
||||
onStatusMessage({
|
||||
@@ -89,13 +128,16 @@ export const SpaceFocusHudWidget = ({
|
||||
resumePlaybackStateRef.current = playbackState;
|
||||
}, [normalizedGoal, onStatusMessage, playbackState]);
|
||||
|
||||
const openRefocus = useCallback((field: 'goal' | 'microStep' = 'goal') => {
|
||||
const openRefocus = useCallback((
|
||||
field: 'goal' | 'microStep' = 'goal',
|
||||
origin: 'manual' | 'pause' | 'next-beat' | 'return' = 'manual',
|
||||
) => {
|
||||
setDraftGoal(goal.trim());
|
||||
setDraftMicroStep(normalizedMicroStep ?? '');
|
||||
setAutoFocusField(field);
|
||||
setIntentError(null);
|
||||
setMicroStepPromptOpen(false);
|
||||
setRefocusOpen(true);
|
||||
setRefocusOrigin(origin);
|
||||
setOverlay('refocus');
|
||||
}, [goal, normalizedMicroStep]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -103,31 +145,42 @@ export const SpaceFocusHudWidget = ({
|
||||
pausePlaybackStateRef.current === 'running' &&
|
||||
playbackState === 'paused' &&
|
||||
hasActiveSession &&
|
||||
!isRefocusOpen &&
|
||||
!sheetOpen
|
||||
overlay === 'none'
|
||||
) {
|
||||
openRefocus('microStep');
|
||||
setIntentError(null);
|
||||
setOverlay('paused');
|
||||
onStatusMessage({
|
||||
message: copy.space.focusHud.refocusOpenOnPause,
|
||||
});
|
||||
}
|
||||
|
||||
pausePlaybackStateRef.current = playbackState;
|
||||
}, [hasActiveSession, isRefocusOpen, onStatusMessage, openRefocus, playbackState, sheetOpen]);
|
||||
}, [hasActiveSession, onStatusMessage, overlay, playbackState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (normalizedMicroStep) {
|
||||
return;
|
||||
if (playbackState === 'running' && overlay === 'paused') {
|
||||
setOverlay('none');
|
||||
}
|
||||
}, [overlay, playbackState]);
|
||||
|
||||
setMicroStepPromptOpen(false);
|
||||
}, [normalizedMicroStep]);
|
||||
useEffect(() => {
|
||||
if (!normalizedMicroStep && overlay === 'next-beat') {
|
||||
setOverlay('none');
|
||||
}
|
||||
}, [normalizedMicroStep, overlay]);
|
||||
|
||||
const handleOpenCompleteSheet = () => {
|
||||
const handleOpenCompleteSheet = (preferredView: 'choice' | 'next' = 'choice') => {
|
||||
setIntentError(null);
|
||||
setRefocusOpen(false);
|
||||
setMicroStepPromptOpen(false);
|
||||
setSheetOpen(true);
|
||||
setCompletePreferredView(preferredView);
|
||||
setOverlay('complete');
|
||||
};
|
||||
|
||||
const handleDismissReturnPrompt = () => {
|
||||
onDismissReturnPrompt?.();
|
||||
|
||||
if (overlay === 'return') {
|
||||
setOverlay('none');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefocusSubmit = async () => {
|
||||
@@ -151,10 +204,18 @@ export const SpaceFocusHudWidget = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setRefocusOpen(false);
|
||||
setOverlay('none');
|
||||
onStatusMessage({
|
||||
message: copy.space.focusHud.refocusSaved,
|
||||
});
|
||||
|
||||
if (refocusOrigin === 'return') {
|
||||
onDismissReturnPrompt?.();
|
||||
}
|
||||
|
||||
if (refocusOrigin === 'pause' && playbackState === 'paused') {
|
||||
onStartRequested?.();
|
||||
}
|
||||
} finally {
|
||||
setSavingIntent(false);
|
||||
}
|
||||
@@ -178,7 +239,7 @@ export const SpaceFocusHudWidget = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setMicroStepPromptOpen(false);
|
||||
setOverlay('none');
|
||||
onStatusMessage({
|
||||
message: copy.space.focusHud.microStepCleared,
|
||||
});
|
||||
@@ -192,37 +253,70 @@ export const SpaceFocusHudWidget = ({
|
||||
setDraftMicroStep('');
|
||||
setAutoFocusField('microStep');
|
||||
setIntentError(null);
|
||||
setMicroStepPromptOpen(false);
|
||||
setRefocusOpen(true);
|
||||
setRefocusOrigin('next-beat');
|
||||
setOverlay('refocus');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pointer-events-none fixed left-6 top-6 z-20 w-[min(26rem,calc(100vw-3rem))] md:left-10 md:top-9">
|
||||
<div className="pointer-events-none fixed left-6 top-6 z-20 w-[min(29rem,calc(100vw-3rem))] md:left-10 md:top-9">
|
||||
<IntentCapsule
|
||||
goal={normalizedGoal}
|
||||
microStep={microStep}
|
||||
canRefocus={Boolean(hasActiveSession)}
|
||||
canComplete={hasActiveSession && sessionPhase === 'focus'}
|
||||
showActions={!isIntentOverlayOpen}
|
||||
onOpenRefocus={() => openRefocus()}
|
||||
onOpenRefocus={() => openRefocus('goal', 'manual')}
|
||||
onMicroStepDone={() => {
|
||||
if (!normalizedMicroStep) {
|
||||
openRefocus('microStep');
|
||||
openRefocus('microStep', 'next-beat');
|
||||
return;
|
||||
}
|
||||
|
||||
setIntentError(null);
|
||||
setRefocusOpen(false);
|
||||
setMicroStepPromptOpen(true);
|
||||
setOverlay('next-beat');
|
||||
}}
|
||||
onGoalCompleteRequest={handleOpenCompleteSheet}
|
||||
/>
|
||||
<ReturnPrompt
|
||||
open={isReturnPromptOpen && Boolean(returnPromptMode)}
|
||||
mode={returnPromptMode === 'break' ? 'break' : 'focus'}
|
||||
isBusy={isSavingIntent}
|
||||
onContinue={() => {
|
||||
handleDismissReturnPrompt();
|
||||
}}
|
||||
onRefocus={() => {
|
||||
handleDismissReturnPrompt();
|
||||
openRefocus('microStep', 'return');
|
||||
}}
|
||||
onRest={() => {
|
||||
handleDismissReturnPrompt();
|
||||
onStatusMessage({ message: copy.space.focusHud.restReminder });
|
||||
}}
|
||||
onNextGoal={() => {
|
||||
handleDismissReturnPrompt();
|
||||
handleOpenCompleteSheet('next');
|
||||
}}
|
||||
/>
|
||||
<PauseRefocusPrompt
|
||||
open={isPausedPromptOpen}
|
||||
isBusy={isSavingIntent}
|
||||
onRefocus={() => openRefocus('microStep', 'pause')}
|
||||
onKeepCurrent={() => {
|
||||
setOverlay('none');
|
||||
onStartRequested?.();
|
||||
}}
|
||||
/>
|
||||
<RefocusSheet
|
||||
open={isRefocusOpen}
|
||||
goalDraft={draftGoal}
|
||||
microStepDraft={draftMicroStep}
|
||||
autoFocusField={autoFocusField}
|
||||
submitLabel={
|
||||
refocusOrigin === 'pause' && playbackState === 'paused'
|
||||
? copy.space.focusHud.refocusApplyAndResume
|
||||
: copy.space.focusHud.refocusApply
|
||||
}
|
||||
isSaving={isSavingIntent}
|
||||
error={intentError}
|
||||
onGoalChange={setDraftGoal}
|
||||
@@ -233,7 +327,7 @@ export const SpaceFocusHudWidget = ({
|
||||
}
|
||||
|
||||
setIntentError(null);
|
||||
setRefocusOpen(false);
|
||||
setOverlay('none');
|
||||
}}
|
||||
onSubmit={() => {
|
||||
void handleRefocusSubmit();
|
||||
@@ -241,6 +335,7 @@ export const SpaceFocusHudWidget = ({
|
||||
/>
|
||||
<NextMicroStepPrompt
|
||||
open={isMicroStepPromptOpen}
|
||||
goal={normalizedGoal}
|
||||
isSubmitting={isSavingIntent}
|
||||
error={intentError}
|
||||
onKeepGoalOnly={() => {
|
||||
@@ -249,11 +344,13 @@ export const SpaceFocusHudWidget = ({
|
||||
onDefineNext={handleDefineNextMicroStep}
|
||||
/>
|
||||
<GoalCompleteSheet
|
||||
open={sheetOpen}
|
||||
open={isCompleteOpen}
|
||||
currentGoal={goal}
|
||||
onClose={() => setSheetOpen(false)}
|
||||
preferredView={completePreferredView}
|
||||
onClose={() => setOverlay('none')}
|
||||
onFinish={() => Promise.resolve(onGoalFinish())}
|
||||
onRest={() => {
|
||||
setSheetOpen(false);
|
||||
setOverlay('none');
|
||||
|
||||
if (restReminderTimerRef.current) {
|
||||
window.clearTimeout(restReminderTimerRef.current);
|
||||
|
||||
34
src/widgets/space-focus-hud/ui/overlayStyles.ts
Normal file
34
src/widgets/space-focus-hud/ui/overlayStyles.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export const HUD_TRAY_SHELL =
|
||||
'pointer-events-auto relative mt-3 w-full overflow-hidden rounded-[22px] border border-white/10 bg-[#101318]/30 text-white shadow-[0_12px_28px_rgba(2,6,23,0.14)] backdrop-blur-[8px] backdrop-saturate-125';
|
||||
|
||||
export const HUD_TRAY_LAYER =
|
||||
'pointer-events-none absolute inset-0 rounded-[22px] bg-[linear-gradient(180deg,rgba(255,255,255,0.08)_0%,rgba(255,255,255,0.025)_24%,rgba(255,255,255,0.01)_100%)]';
|
||||
|
||||
export const HUD_TRAY_HAIRLINE = 'pointer-events-none absolute inset-x-0 top-0 h-px bg-white/16';
|
||||
|
||||
export const HUD_FIELD =
|
||||
'h-11 w-full rounded-[18px] border border-white/10 bg-black/14 px-3.5 text-[0.98rem] tracking-tight text-white placeholder:text-white/30 focus:border-white/20 focus:bg-black/20 focus:outline-none focus:ring-2 focus:ring-white/8';
|
||||
|
||||
export const HUD_OPTION_ROW =
|
||||
'group flex w-full items-start justify-between gap-4 rounded-[20px] border border-white/8 bg-black/10 px-4 py-3.5 text-left transition-all duration-200 hover:border-white/14 hover:bg-black/14 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/10 disabled:cursor-not-allowed disabled:border-white/6 disabled:bg-black/8 disabled:text-white/30';
|
||||
|
||||
export const HUD_OPTION_ROW_PRIMARY =
|
||||
'border-white/12 bg-black/14 hover:border-white/18 hover:bg-black/18';
|
||||
|
||||
export const HUD_OPTION_CHEVRON =
|
||||
'mt-0.5 shrink-0 text-[13px] text-white/28 transition-colors duration-200 group-hover:text-white/52';
|
||||
|
||||
export const HUD_PAUSE_EYEBROW =
|
||||
'text-[11px] font-medium tracking-[0.12em] text-white/42';
|
||||
|
||||
export const HUD_PAUSE_TITLE =
|
||||
'mt-2 max-w-[24rem] text-[1.18rem] font-medium leading-[1.34] tracking-[-0.02em] text-white/95 md:text-[1.28rem]';
|
||||
|
||||
export const HUD_PAUSE_BODY =
|
||||
'mt-2 max-w-[23rem] text-[13px] leading-[1.6] text-white/58 md:text-[13.5px]';
|
||||
|
||||
export const HUD_TEXT_LINK =
|
||||
'text-[12px] font-medium tracking-[0.08em] text-white/62 underline decoration-white/16 underline-offset-4 transition-all duration-200 hover:text-white/84 hover:decoration-white/28 disabled:cursor-default disabled:text-white/26 disabled:no-underline';
|
||||
|
||||
export const HUD_TEXT_LINK_STRONG =
|
||||
'text-[12px] font-semibold tracking-[0.08em] text-white/86 underline decoration-white/22 underline-offset-4 transition-all duration-200 hover:text-white hover:decoration-white/36 disabled:cursor-default disabled:text-white/30 disabled:no-underline';
|
||||
Reference in New Issue
Block a user