From bdbcf3c3f17b4f247a9293154bc9c1149fafd7f0 Mon Sep 17 00:00:00 2001 From: corpi Date: Tue, 17 Mar 2026 20:50:28 +0900 Subject: [PATCH] refactor(space): refine end session modal motion --- .../ui/EndSessionConfirmModal.tsx | 272 ++++++++---------- .../space-focus-hud/ui/InlineMicrostep.tsx | 174 ++++++----- 2 files changed, 217 insertions(+), 229 deletions(-) diff --git a/src/widgets/space-focus-hud/ui/EndSessionConfirmModal.tsx b/src/widgets/space-focus-hud/ui/EndSessionConfirmModal.tsx index eab2ef4..46b7a0b 100644 --- a/src/widgets/space-focus-hud/ui/EndSessionConfirmModal.tsx +++ b/src/widgets/space-focus-hud/ui/EndSessionConfirmModal.tsx @@ -36,21 +36,27 @@ export const EndSessionConfirmModal = ({ const endSessionCopy = copy.space.endSession; const trimmedGoal = currentGoal.trim() || copy.space.focusHud.goalFallback; + const activeStage = completionResult ? completionResult.completionSource === 'manual-end' ? 'result-saved' : 'result-success' : stage; + const focusedMinutes = completionResult ? formatFocusedMinutes(completionResult.focusedSeconds) : 0; + const hasThoughts = Boolean(completionResult && completionResult.thoughts.length > 0); useEffect(() => { if (!open) { - setStage('decision'); - setIsSubmitting(false); - return; + // Add a slight delay before resetting state so the fade-out animation can finish smoothly + const timer = setTimeout(() => { + setStage('decision'); + setIsSubmitting(false); + }, 700); + return () => clearTimeout(timer); } const allowEscape = !isSubmitting && !completionResult; @@ -67,10 +73,7 @@ export const EndSessionConfirmModal = ({ }, [completionResult, isSubmitting, onClose, open]); const handleFinishHere = async () => { - if (isSubmitting) { - return; - } - + if (isSubmitting) return; setIsSubmitting(true); try { await onFinishHere(); @@ -80,10 +83,7 @@ export const EndSessionConfirmModal = ({ }; const handleSaveAndReturn = async () => { - if (isSubmitting) { - return; - } - + if (isSubmitting) return; setIsSubmitting(true); try { await onSaveAndReturn(); @@ -95,59 +95,70 @@ export const EndSessionConfirmModal = ({ return (
+ {/* Abyssal Backdrop: Direct filter animation to prevent WebKit blur pop-in */}
+ > +
+
+
{activeStage === 'decision' ? ( -
-

+

+

{endSessionCopy.decision.eyebrow}

{endSessionCopy.decision.title}

-
-

+ {/* Volumetric Card */} +

+

{endSessionCopy.decision.goalLabel}

-

{trimmedGoal}

+

{trimmedGoal}

+ {/* Primary CTA (The Pearl) */} + + {/* Secondary CTA (Ghost Button) */} @@ -157,7 +168,7 @@ export const EndSessionConfirmModal = ({ type="button" onClick={onClose} disabled={isSubmitting} - className="mt-8 text-[12px] font-medium text-white/30 underline decoration-transparent underline-offset-4 transition-colors hover:text-white/70 hover:decoration-white/30 disabled:opacity-50" + className="mt-10 text-[11px] font-light uppercase tracking-widest text-white/20 transition-all duration-300 hover:text-white/60 disabled:opacity-50" > {endSessionCopy.decision.cancelButton} @@ -165,26 +176,26 @@ export const EndSessionConfirmModal = ({ ) : null} {activeStage === 'unfinished-confirm' ? ( -
-
-

+

+
+

{endSessionCopy.unfinishedConfirm.eyebrow}

{endSessionCopy.unfinishedConfirm.title}

-

+

{endSessionCopy.unfinishedConfirm.description}

-
-

+

+

{endSessionCopy.decision.goalLabel}

-

{trimmedGoal}

+

{trimmedGoal}

@@ -192,7 +203,7 @@ export const EndSessionConfirmModal = ({ type="button" onClick={onClose} disabled={isSubmitting} - className="flex-1 rounded-full border border-white/20 bg-black/40 px-6 py-4 text-[15px] font-medium text-white backdrop-blur-md transition-all hover:border-white/40 hover:bg-white/10 active:scale-95 disabled:opacity-50" + className="flex-1 rounded-full border border-white/10 bg-transparent px-6 py-4 text-[14px] font-light text-white/60 transition-all duration-500 hover:border-white/20 hover:bg-white/[0.03] hover:text-white/90 active:scale-[0.98] disabled:opacity-50" > {endSessionCopy.unfinishedConfirm.keepFocusingButton} @@ -200,59 +211,85 @@ export const EndSessionConfirmModal = ({ type="button" onClick={handleSaveAndReturn} disabled={isSubmitting} - className="flex-1 rounded-full bg-white px-6 py-4 text-[15px] font-semibold text-black shadow-[0_0_30px_rgba(255,255,255,0.18)] transition-all hover:scale-105 active:scale-95 disabled:opacity-50" + className="group relative flex-1 overflow-hidden rounded-full bg-white px-6 py-4 text-[14px] font-medium text-black transition-all duration-500 hover:scale-[1.02] hover:shadow-[0_0_40px_rgba(255,255,255,0.2)] active:scale-[0.98] disabled:opacity-50" > - {isSubmitting - ? endSessionCopy.unfinishedConfirm.saveAndReturnPending - : endSessionCopy.unfinishedConfirm.saveAndReturnButton} +
+ + {isSubmitting + ? endSessionCopy.unfinishedConfirm.saveAndReturnPending + : endSessionCopy.unfinishedConfirm.saveAndReturnButton} +
) : null} - {activeStage === 'result-success' && completionResult ? ( -
-
-

- {endSessionCopy.resultSuccess.eyebrow} + {(activeStage === 'result-success' || activeStage === 'result-saved') && completionResult ? ( +

+ {/* Stage dependent subtle glow */} +
+ +

+ {activeStage === 'result-success' ? endSessionCopy.resultSuccess.eyebrow : endSessionCopy.resultSaved.eyebrow}

- {endSessionCopy.resultSuccess.title} + {activeStage === 'result-success' ? endSessionCopy.resultSuccess.title : endSessionCopy.resultSaved.title}

-

- {endSessionCopy.resultSuccess.description} +

+ {activeStage === 'result-success' ? endSessionCopy.resultSuccess.description : endSessionCopy.resultSaved.description}

-
-
-

+

+ {/* The Hero Number */} +
+
+

{endSessionCopy.resultSuccess.focusedLabel}

-

- {focusedMinutes} - m -

+
+
+

+ {focusedMinutes} + m +

+
-
-

+ {activeStage === 'result-saved' && ( +

+

+ {endSessionCopy.resultSaved.goalStatusLabel} +

+

+ {endSessionCopy.resultSaved.goalStatusValue} +

+
+ )} + +
+

{endSessionCopy.resultSuccess.goalLabel}

-

+

{completionResult.goalText}

{hasThoughts ? ( -
-
-

+

+
+

{endSessionCopy.resultSuccess.thoughtsLabel}

- + {completionResult.thoughts.length}
@@ -260,9 +297,9 @@ export const EndSessionConfirmModal = ({ {completionResult.thoughts.map((thought) => (
-

+

{thought.text}

@@ -272,94 +309,19 @@ export const EndSessionConfirmModal = ({ ) : null}
- -
- ) : null} - - {activeStage === 'result-saved' && completionResult ? ( -
-
-

- {endSessionCopy.resultSaved.eyebrow} -

-

- {endSessionCopy.resultSaved.title} -

-

- {endSessionCopy.resultSaved.description} -

- -
-
-

- {endSessionCopy.resultSaved.focusedLabel} -

-

- {focusedMinutes} - m -

-
- -
-

- {endSessionCopy.resultSaved.goalStatusLabel} -

-

- {endSessionCopy.resultSaved.goalStatusValue} -

-
- -
-

- {endSessionCopy.resultSaved.goalLabel} -

-

- {completionResult.goalText} -

-
- - {hasThoughts ? ( -
-
-

- {endSessionCopy.resultSaved.thoughtsLabel} -

- - {completionResult.thoughts.length} - -
-
- {completionResult.thoughts.map((thought) => ( -
-

- {thought.text} -

-
- ))} -
-
- ) : null} +
+
- - +
) : null}
diff --git a/src/widgets/space-focus-hud/ui/InlineMicrostep.tsx b/src/widgets/space-focus-hud/ui/InlineMicrostep.tsx index 5cbdde4..16722f4 100644 --- a/src/widgets/space-focus-hud/ui/InlineMicrostep.tsx +++ b/src/widgets/space-focus-hud/ui/InlineMicrostep.tsx @@ -13,6 +13,7 @@ export const InlineMicrostep = ({ microStep, isBusy, onUpdate }: InlineMicrostep const [isEditing, setIsEditing] = useState(false); const [draft, setDraft] = useState(''); const [isCompleting, setIsCompleting] = useState(false); + const [isFocused, setIsFocused] = useState(false); const inputRef = useRef(null); const normalizedStep = microStep?.trim() || null; @@ -31,6 +32,7 @@ export const InlineMicrostep = ({ microStep, isBusy, onUpdate }: InlineMicrostep const cancelEditing = () => { setIsEditing(false); + setIsFocused(false); setDraft(''); }; @@ -61,86 +63,110 @@ export const InlineMicrostep = ({ microStep, isBusy, onUpdate }: InlineMicrostep if (isBusy || isCompleting || !normalizedStep) return; setIsCompleting(true); - // Visual delay for the strikethrough animation before actually clearing it + // Visual delay for the evaporating strikethrough animation (Sensory Closure) setTimeout(async () => { await onUpdate(null); setIsCompleting(false); - }, 400); + }, 600); }; - if (isEditing) { - return ( -
- setDraft(e.target.value)} - onKeyDown={handleKeyDown} - onBlur={() => void submitDraft()} - disabled={isBusy} - placeholder="Enter next small step..." - className="flex-1 bg-transparent text-[15px] font-medium text-white outline-none placeholder:text-white/30 disabled:opacity-50" - /> - -
- ); - } - - if (normalizedStep) { - return ( -
- - -
- ); - } + // Determine the primary physical state for morphing + const state = isEditing ? 'editing' : (normalizedStep ? 'active' : 'idle'); return ( - +
+
+ + {/* Content Layer 1: Idle (Just one small step) */} +
+ + + Start with one small win +
+ + {/* Content Layer 2: Active (Has Step) */} +
+ +
+

+ {normalizedStep} +

+
+
+ + {/* Content Layer 3: Editing (The 'First Move') */} +
+ setDraft(e.target.value)} + onKeyDown={handleKeyDown} + onFocus={() => setIsFocused(true)} + onBlur={() => { + setIsFocused(false); + void submitDraft(); + }} + disabled={isBusy} + placeholder="What's the very first move?" + className="flex-1 bg-transparent text-[15.5px] font-light tracking-wide text-white outline-none placeholder:text-white/30 disabled:opacity-50 min-w-0" + /> + +
+ +
+
); };