diff --git a/src/widgets/space-focus-hud/ui/InlineMicrostep.tsx b/src/widgets/space-focus-hud/ui/InlineMicrostep.tsx new file mode 100644 index 0000000..d919ad1 --- /dev/null +++ b/src/widgets/space-focus-hud/ui/InlineMicrostep.tsx @@ -0,0 +1,145 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { cn } from '@/shared/lib/cn'; + +interface InlineMicrostepProps { + microStep: string | null; + isBusy: boolean; + onUpdate: (nextStep: string | null) => Promise; +} + +export const InlineMicrostep = ({ microStep, isBusy, onUpdate }: InlineMicrostepProps) => { + const [isEditing, setIsEditing] = useState(false); + const [draft, setDraft] = useState(''); + const [isCompleting, setIsCompleting] = useState(false); + const inputRef = useRef(null); + + const normalizedStep = microStep?.trim() || null; + + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus(); + } + }, [isEditing]); + + const startEditing = () => { + if (isBusy || isCompleting) return; + setDraft(normalizedStep ?? ''); + setIsEditing(true); + }; + + const cancelEditing = () => { + setIsEditing(false); + setDraft(''); + }; + + const submitDraft = async () => { + const trimmed = draft.trim(); + if (trimmed === normalizedStep) { + cancelEditing(); + return; + } + + const success = await onUpdate(trimmed || null); + if (success) { + cancelEditing(); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + void submitDraft(); + } else if (e.key === 'Escape') { + e.preventDefault(); + cancelEditing(); + } + }; + + const handleComplete = async () => { + if (isBusy || isCompleting || !normalizedStep) return; + + setIsCompleting(true); + // Visual delay for the strikethrough animation before actually clearing it + setTimeout(async () => { + await onUpdate(null); + setIsCompleting(false); + }, 400); + }; + + 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 ( +
+ + +
+ ); + } + + return ( + + ); +}; diff --git a/src/widgets/space-focus-hud/ui/IntentCapsule.tsx b/src/widgets/space-focus-hud/ui/IntentCapsule.tsx deleted file mode 100644 index 1d49ee0..0000000 --- a/src/widgets/space-focus-hud/ui/IntentCapsule.tsx +++ /dev/null @@ -1,228 +0,0 @@ -'use client'; - -import { useEffect, useRef, useState } from 'react'; -import { copy } from '@/shared/i18n'; -import { cn } from '@/shared/lib/cn'; - -interface IntentCapsuleProps { - goal: string; - microStep?: string | null; - canRefocus: boolean; - canComplete?: boolean; - showActions?: boolean; - onOpenRefocus: () => void; - onMicroStepDone: () => void; - onGoalCompleteRequest?: () => void; -} - -export const IntentCapsule = ({ - goal, - microStep, - canRefocus, - canComplete = false, - showActions = true, - onOpenRefocus, - onMicroStepDone, - onGoalCompleteRequest, -}: IntentCapsuleProps) => { - const [isPinnedExpanded, setPinnedExpanded] = useState(false); - const [isHovered, setHovered] = useState(false); - const [isFocusWithin, setFocusWithin] = useState(false); - const sectionRef = useRef(null); - - const normalizedMicroStep = microStep?.trim() ? microStep.trim() : null; - const isExpanded = showActions && (isPinnedExpanded || isHovered || isFocusWithin); - const canInteract = showActions && canRefocus; - const microGlyphClass = - 'inline-flex h-5 w-5 items-center justify-center rounded-full border border-white/18 text-white/62 transition-all duration-200 hover:border-white/32 hover:text-white focus-visible:border-white/32 focus-visible:text-white disabled:cursor-default disabled:opacity-30'; - - useEffect(() => { - if (!isExpanded || !showActions) { - return; - } - - const handlePointerDown = (event: PointerEvent) => { - const target = event.target; - - if (!(target instanceof Node)) { - return; - } - - if (sectionRef.current?.contains(target)) { - return; - } - - setPinnedExpanded(false); - setHovered(false); - setFocusWithin(false); - }; - - document.addEventListener('pointerdown', handlePointerDown); - - return () => { - document.removeEventListener('pointerdown', handlePointerDown); - }; - }, [isExpanded, showActions]); - - const handleExpand = () => { - if (!showActions) { - return; - } - - setPinnedExpanded(true); - }; - - return ( -
-
{ - if (showActions) { - setHovered(true); - } - }} - onMouseLeave={() => setHovered(false)} - onFocusCapture={() => { - if (showActions) { - setFocusWithin(true); - } - }} - onBlurCapture={(event) => { - if (!event.currentTarget.contains(event.relatedTarget)) { - setFocusWithin(false); - } - }} - > -
-
- -
- {isExpanded ? ( -
-
-

- {goal} -

-
- - {canInteract ? ( - - ) : null} -
- ) : ( - - )} - -
- {normalizedMicroStep ? ( -
- -

- {normalizedMicroStep} -

-
- ) : ( -

- {copy.space.focusHud.refocusDescription} -

- )} - - {showActions && canComplete ? ( -
- -
- ) : null} -
-
-
-
- ); -}; diff --git a/src/widgets/space-focus-hud/ui/NextMicroStepPrompt.tsx b/src/widgets/space-focus-hud/ui/NextMicroStepPrompt.tsx deleted file mode 100644 index 3be9956..0000000 --- a/src/widgets/space-focus-hud/ui/NextMicroStepPrompt.tsx +++ /dev/null @@ -1,129 +0,0 @@ -'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_REVEAL_BASE, - HUD_REVEAL_HIDDEN, - HUD_REVEAL_NEXT_BEAT, - HUD_TRAY_CONTENT, - HUD_TRAY_HAIRLINE, - HUD_TRAY_LAYER, - HUD_TRAY_SHELL, -} from './overlayStyles'; - -interface NextMicroStepPromptProps { - open: boolean; - goal: string; - isSubmitting: boolean; - error: string | null; - onKeepGoalOnly: () => void; - onDefineNext: () => void; - onFinish: () => void; -} - -export const NextMicroStepPrompt = ({ - open, - goal, - isSubmitting, - error, - onKeepGoalOnly, - onDefineNext, - onFinish, -}: NextMicroStepPromptProps) => { - const trimmedGoal = goal.trim(); - - return ( -
-
-
-
- -
-

- {copy.space.focusHud.microStepPromptEyebrow} -

-

- {copy.space.focusHud.microStepPromptTitle} -

-

- {copy.space.focusHud.microStepPromptDescription} -

- - {trimmedGoal ? ( -
-

- {copy.space.focusHud.intentLabel} -

-

{trimmedGoal}

-
- ) : null} - - {error ? ( -

- {error} -

- ) : null} - -
- - -
- -
- -

- {copy.space.focusHud.microStepPromptFinishHint} -

-
-
-
-
- ); -}; diff --git a/src/widgets/space-focus-hud/ui/PauseRefocusPrompt.tsx b/src/widgets/space-focus-hud/ui/PauseRefocusPrompt.tsx deleted file mode 100644 index 2b9bc64..0000000 --- a/src/widgets/space-focus-hud/ui/PauseRefocusPrompt.tsx +++ /dev/null @@ -1,111 +0,0 @@ -'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_REVEAL_BASE, - HUD_REVEAL_HIDDEN, - HUD_REVEAL_PAUSE, - HUD_TRAY_CONTENT, - HUD_TRAY_HAIRLINE, - HUD_TRAY_LAYER, - HUD_TRAY_SHELL, -} from './overlayStyles'; - -interface PauseRefocusPromptProps { - open: boolean; - isBusy: boolean; - onRefocus: () => void; - onKeepCurrent: () => void; - onFinish: () => void; -} - -export const PauseRefocusPrompt = ({ - open, - isBusy, - onRefocus, - onKeepCurrent, - onFinish, -}: PauseRefocusPromptProps) => { - return ( -
-
-
-
- -
-

- {copy.space.focusHud.pausePromptEyebrow} -

-

- {copy.space.focusHud.pausePromptTitle} -

-

- {copy.space.focusHud.pausePromptDescription} -

- -
- - -
- -
- -

- {copy.space.focusHud.pausePromptFinishHint} -

-
-
-
-
- ); -}; diff --git a/src/widgets/space-focus-hud/ui/RefocusSheet.tsx b/src/widgets/space-focus-hud/ui/RefocusSheet.tsx deleted file mode 100644 index fe347b9..0000000 --- a/src/widgets/space-focus-hud/ui/RefocusSheet.tsx +++ /dev/null @@ -1,172 +0,0 @@ -'use client'; - -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; - onMicroStepChange: (value: string) => void; - onClose: () => void; - onSubmit: () => void; -} - -export const RefocusSheet = ({ - open, - goalDraft, - microStepDraft, - autoFocusField, - submitLabel, - isSaving, - error, - onGoalChange, - onMicroStepChange, - onClose, - onSubmit, -}: RefocusSheetProps) => { - const goalRef = useRef(null); - const microStepRef = useRef(null); - - useEffect(() => { - if (!open) { - return; - } - - const rafId = window.requestAnimationFrame(() => { - if (autoFocusField === 'microStep') { - microStepRef.current?.focus(); - microStepRef.current?.select(); - return; - } - - goalRef.current?.focus(); - goalRef.current?.select(); - }); - - return () => { - window.cancelAnimationFrame(rafId); - }; - }, [autoFocusField, open]); - - useEffect(() => { - if (!open) { - return; - } - - const handleEscape = (event: KeyboardEvent) => { - if (event.key === 'Escape' && !isSaving) { - onClose(); - } - }; - - window.addEventListener('keydown', handleEscape); - return () => { - window.removeEventListener('keydown', handleEscape); - }; - }, [isSaving, onClose, open]); - - const handleSubmit = (event: FormEvent) => { - event.preventDefault(); - - if (isSaving || goalDraft.trim().length === 0) { - return; - } - - onSubmit(); - }; - - return ( -
-
-
-
- -
-
-

다시 방향 잡기

-

- {copy.space.focusHud.refocusTitle} -

-

{copy.space.focusHud.refocusDescription}

-
-
- -
- - - - - {error ? ( -

- {error} -

- ) : null} - -
- - -
-
-
-
- ); -}; diff --git a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx index 3963b68..2652938 100644 --- a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx +++ b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx @@ -1,12 +1,10 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { copy } from '@/shared/i18n'; +import { cn } from '@/shared/lib/cn'; import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine'; 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 { InlineMicrostep } from './InlineMicrostep'; import { ReturnPrompt } from './ReturnPrompt'; interface SpaceFocusHudWidgetProps { @@ -56,25 +54,15 @@ export const SpaceFocusHudWidget = ({ onGoalFinish, onStatusMessage, }: SpaceFocusHudWidgetProps) => { - const [overlay, setOverlay] = useState<'none' | 'paused' | 'return' | 'refocus' | 'next-beat' | 'complete'>('none'); - const [refocusOrigin, setRefocusOrigin] = useState<'manual' | 'pause' | 'next-beat' | 'return'>('manual'); + const [overlay, setOverlay] = useState<'none' | 'return' | 'complete'>('none'); const [completePreferredView, setCompletePreferredView] = useState<'choice' | 'next'>('choice'); - const [draftGoal, setDraftGoal] = useState(''); - const [draftMicroStep, setDraftMicroStep] = useState(''); - const [autoFocusField, setAutoFocusField] = useState<'goal' | 'microStep'>('goal'); const [isSavingIntent, setSavingIntent] = useState(false); - const [intentError, setIntentError] = useState(null); + const visibleRef = useRef(false); const resumePlaybackStateRef = useRef<'running' | 'paused'>(playbackState); - const pausePlaybackStateRef = useRef<'running' | 'paused'>(playbackState); - const suppressNextPausePromptRef = useRef(false); const restReminderTimerRef = useRef(null); const normalizedGoal = goal.trim().length > 0 ? goal.trim() : copy.space.focusHud.goalFallback; - const normalizedMicroStep = microStep?.trim() ? microStep.trim() : null; - 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'; @@ -90,9 +78,7 @@ export const SpaceFocusHudWidget = ({ useEffect(() => { if (!hasActiveSession) { setOverlay('none'); - setIntentError(null); setSavingIntent(false); - setRefocusOrigin('manual'); setCompletePreferredView('choice'); } }, [hasActiveSession]); @@ -109,7 +95,6 @@ export const SpaceFocusHudWidget = ({ return; } - setIntentError(null); setOverlay('return'); }, [overlay, returnPromptMode]); @@ -133,172 +118,94 @@ export const SpaceFocusHudWidget = ({ resumePlaybackStateRef.current = playbackState; }, [normalizedGoal, onStatusMessage, playbackState]); - const openRefocus = useCallback(( - field: 'goal' | 'microStep' = 'goal', - origin: 'manual' | 'pause' | 'next-beat' | 'return' = 'manual', - ) => { - setDraftGoal(goal.trim()); - setDraftMicroStep(normalizedMicroStep ?? ''); - setAutoFocusField(field); - setIntentError(null); - setRefocusOrigin(origin); - setOverlay('refocus'); - }, [goal, normalizedMicroStep]); - - useEffect(() => { - if ( - pausePlaybackStateRef.current === 'running' && - playbackState === 'paused' && - hasActiveSession && - overlay === 'none' - ) { - if (suppressNextPausePromptRef.current) { - suppressNextPausePromptRef.current = false; - pausePlaybackStateRef.current = playbackState; - return; - } - - setIntentError(null); - setOverlay('paused'); - onStatusMessage({ - message: copy.space.focusHud.refocusOpenOnPause, - }); - } - - pausePlaybackStateRef.current = playbackState; - }, [hasActiveSession, onStatusMessage, overlay, playbackState]); - - useEffect(() => { - if (playbackState === 'running' && overlay === 'paused') { - setOverlay('none'); - } - }, [overlay, playbackState]); - - useEffect(() => { - if (!normalizedMicroStep && overlay === 'next-beat') { - setOverlay('none'); - } - }, [normalizedMicroStep, overlay]); - useEffect(() => { if (entryOverlayIntent !== 'resume-refocus' || !hasActiveSession || overlay !== 'none') { return; } - - openRefocus('microStep', 'manual'); + // With inline microsteps, we just handle the intent and let the user click if they want. onEntryOverlayIntentHandled?.(); - }, [entryOverlayIntent, hasActiveSession, onEntryOverlayIntentHandled, openRefocus, overlay]); + }, [entryOverlayIntent, hasActiveSession, onEntryOverlayIntentHandled, overlay]); const handleOpenCompleteSheet = (preferredView: 'choice' | 'next' = 'choice') => { - setIntentError(null); setCompletePreferredView(preferredView); setOverlay('complete'); }; const handleDismissReturnPrompt = () => { onDismissReturnPrompt?.(); - if (overlay === 'return') { setOverlay('none'); } }; - const handleRefocusSubmit = async () => { - const trimmedGoal = draftGoal.trim(); - - if (!trimmedGoal || isSavingIntent) { - return; - } - + const handleInlineMicrostepUpdate = async (nextStep: string | null) => { + if (isSavingIntent) return false; + setSavingIntent(true); - setIntentError(null); - try { - const didUpdate = await onIntentUpdate({ - goal: trimmedGoal, - microStep: draftMicroStep.trim() || null, - }); - - if (!didUpdate) { - setIntentError(copy.space.workspace.intentSyncFailed); - return; - } - - setOverlay('none'); - onStatusMessage({ - message: copy.space.focusHud.refocusSaved, - }); - - if (refocusOrigin === 'return') { - onDismissReturnPrompt?.(); - } - - if (refocusOrigin === 'pause' && playbackState === 'paused') { - onStartRequested?.(); + const didUpdate = await onIntentUpdate({ microStep: nextStep }); + if (didUpdate) { + if (nextStep) { + onStatusMessage({ message: copy.space.focusHud.refocusSaved }); + } else { + onStatusMessage({ message: copy.space.focusHud.microStepCleared }); + } } + return didUpdate; } finally { setSavingIntent(false); } }; - const handleKeepGoalOnly = async () => { - if (isSavingIntent) { - return; - } - - setSavingIntent(true); - setIntentError(null); - - try { - const didUpdate = await onIntentUpdate({ - microStep: null, - }); - - if (!didUpdate) { - setIntentError(copy.space.workspace.intentSyncFailed); - return; - } - - setOverlay('none'); - onStatusMessage({ - message: copy.space.focusHud.microStepCleared, - }); - } finally { - setSavingIntent(false); - } - }; - - const handleDefineNextMicroStep = () => { - setDraftGoal(goal.trim()); - setDraftMicroStep(''); - setAutoFocusField('microStep'); - setIntentError(null); - setRefocusOrigin('next-beat'); - setOverlay('refocus'); - }; - return ( <> -
- openRefocus('goal', 'manual')} - onMicroStepDone={() => { - if (!normalizedMicroStep) { - openRefocus('microStep', 'next-beat'); - return; - } +
+ {/* The Monolith (Central Hub) */} +
+ {/* Massive Central Timer */} +
playbackState === 'running' ? onPauseRequested?.() : onStartRequested?.()}> +

+ {timeDisplay} +

+
- setIntentError(null); - setOverlay('next-beat'); - }} - onGoalCompleteRequest={handleOpenCompleteSheet} - /> + {/* Core Intent */} +
+ {/* Immutable Goal */} +

+ {normalizedGoal} +

+ + {/* Kinetic Inline Microstep */} +
+ +
+ + {hasActiveSession && (sessionPhase === 'focus' || sessionPhase === 'break') && ( + + )} +
+
+
+ +
{ handleDismissReturnPrompt(); - openRefocus('microStep', 'return'); }} onRest={() => { handleDismissReturnPrompt(); @@ -322,59 +228,6 @@ export const SpaceFocusHudWidget = ({ handleOpenCompleteSheet('choice'); }} /> - openRefocus('microStep', 'pause')} - onKeepCurrent={() => { - setOverlay('none'); - onStartRequested?.(); - }} - onFinish={() => { - setOverlay('none'); - handleOpenCompleteSheet('choice'); - }} - /> - { - if (isSavingIntent) { - return; - } - - setIntentError(null); - setOverlay('none'); - }} - onSubmit={() => { - void handleRefocusSubmit(); - }} - /> - { - void handleKeepGoalOnly(); - }} - onDefineNext={handleDefineNextMicroStep} - onFinish={() => { - setIntentError(null); - handleOpenCompleteSheet('choice'); - }} - /> Promise.resolve(onGoalFinish())} onRest={() => { setOverlay('none'); - suppressNextPausePromptRef.current = true; onPauseRequested?.(); if (restReminderTimerRef.current) { @@ -400,9 +252,8 @@ export const SpaceFocusHudWidget = ({ }} />
+ -
+
-
+
{modeLabel} - - - {timeDisplay} -
-
+
{HUD_ACTIONS.map((action) => { const isStartAction = action.id === 'start'; const isPauseAction = action.id === 'pause'; @@ -117,20 +100,11 @@ export const SpaceTimerHudWidget = ({ if (isResetAction) onResetClick?.(); }} className={cn( - 'inline-flex h-8 w-8 items-center justify-center rounded-full text-sm transition-all duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 active:scale-95 disabled:cursor-not-allowed disabled:opacity-30', - isImmersionMode - ? isBreakPhase - ? 'text-white/74 hover:bg-emerald-100/10 hover:text-white' - : 'text-white/70 hover:bg-white/10 hover:text-white' - : isBreakPhase - ? 'text-white/82 hover:bg-emerald-100/12 hover:text-white' - : 'text-white/80 hover:bg-white/15 hover:text-white', - isStartAction && isHighlighted - ? isBreakPhase - ? 'bg-emerald-100/10 text-white shadow-sm' - : 'bg-white/10 text-white shadow-sm' - : '', - isPauseAction && isHighlighted + 'inline-flex h-9 w-9 items-center justify-center rounded-full text-sm transition-all duration-300 ease-out focus-visible:outline-none focus-visible:ring-2 active:scale-95 disabled:cursor-not-allowed disabled:opacity-20', + isBreakPhase + ? 'text-white/70 hover:bg-emerald-100/10 hover:text-white' + : 'text-white/60 hover:bg-white/10 hover:text-white', + isHighlighted ? isBreakPhase ? 'bg-emerald-100/10 text-white shadow-sm' : 'bg-white/10 text-white shadow-sm' @@ -144,7 +118,7 @@ export const SpaceTimerHudWidget = ({ })}
diff --git a/src/widgets/space-tools-dock/ui/FocusRightRail.tsx b/src/widgets/space-tools-dock/ui/FocusRightRail.tsx index b588586..2ffb380 100644 --- a/src/widgets/space-tools-dock/ui/FocusRightRail.tsx +++ b/src/widgets/space-tools-dock/ui/FocusRightRail.tsx @@ -59,41 +59,51 @@ export const FocusRightRail = ({ isIdle ? 'opacity-0 translate-x-4 pointer-events-none' : 'opacity-100 translate-x-0', )} > -
+
- {/* Notes Toggle */} -
+ {/* Thought Orb (Brain Dump) */} +
{/* Tooltip */}
- - {copy.space.toolsDock.notesButton} + + Brain Dump
{openPopover === 'notes' ? ( -
- +
+
+ +
) : null}
+ {/* Standard Tools */} +
+ {/* Sound Toggle */}
+
+
);