diff --git a/src/widgets/space-focus-hud/ui/GoalFlashOverlay.tsx b/src/widgets/space-focus-hud/ui/GoalFlashOverlay.tsx new file mode 100644 index 0000000..02e008f --- /dev/null +++ b/src/widgets/space-focus-hud/ui/GoalFlashOverlay.tsx @@ -0,0 +1,25 @@ +import { cn } from '@/shared/lib/cn'; + +interface GoalFlashOverlayProps { + goal: string; + visible: boolean; +} + +export const GoalFlashOverlay = ({ goal, visible }: GoalFlashOverlayProps) => { + const normalizedGoal = goal.trim().length > 0 ? goal.trim() : '이번 한 조각을 잊지 마세요.'; + + return ( +
+
+ 이번 한 조각: {normalizedGoal} +
+
+ ); +}; diff --git a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx index dba47be..0ea056c 100644 --- a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx +++ b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx @@ -1,4 +1,7 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useReducedMotion } from '@/shared/lib/useReducedMotion'; import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud'; +import { GoalFlashOverlay } from './GoalFlashOverlay'; interface SpaceFocusHudWidgetProps { goal: string; @@ -11,16 +14,85 @@ export const SpaceFocusHudWidget = ({ timerLabel, visible, }: SpaceFocusHudWidgetProps) => { + const reducedMotion = useReducedMotion(); + const [flashVisible, setFlashVisible] = useState(false); + const playbackStateRef = useRef<'running' | 'paused'>('running'); + const flashTimerRef = useRef(null); + + const triggerFlash = useCallback((durationMs: number) => { + if (reducedMotion || !visible) { + return; + } + + setFlashVisible(true); + + if (flashTimerRef.current) { + window.clearTimeout(flashTimerRef.current); + } + + flashTimerRef.current = window.setTimeout(() => { + setFlashVisible(false); + flashTimerRef.current = null; + }, durationMs); + }, [reducedMotion, visible]); + + useEffect(() => { + return () => { + if (flashTimerRef.current) { + window.clearTimeout(flashTimerRef.current); + flashTimerRef.current = null; + } + }; + }, []); + + useEffect(() => { + if (!visible || reducedMotion) { + setFlashVisible(false); + return; + } + + triggerFlash(2000); + }, [visible, reducedMotion, triggerFlash]); + + useEffect(() => { + if (!visible || reducedMotion) { + return; + } + + const intervalId = window.setInterval(() => { + triggerFlash(800); + }, 5 * 60 * 1000); + + return () => { + window.clearInterval(intervalId); + }; + }, [visible, reducedMotion, triggerFlash]); + if (!visible) { return null; } return ( - + <> + + { + if (reducedMotion) { + playbackStateRef.current = state; + return; + } + + if (playbackStateRef.current === 'paused' && state === 'running') { + triggerFlash(1000); + } + + playbackStateRef.current = state; + }} + /> + ); }; diff --git a/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx b/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx index 5a88c75..648fd82 100644 --- a/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx +++ b/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx @@ -1,6 +1,7 @@ 'use client'; import { cn } from '@/shared/lib/cn'; +import { useToast } from '@/shared/ui'; import { RECOVERY_30S_MODE_LABEL, Restart30sAction, @@ -12,6 +13,7 @@ interface SpaceTimerHudWidgetProps { goal: string; className?: string; isImmersionMode?: boolean; + onPlaybackStateChange?: (state: 'running' | 'paused') => void; } const HUD_ACTIONS = [ @@ -25,8 +27,11 @@ export const SpaceTimerHudWidget = ({ goal, className, isImmersionMode = false, + onPlaybackStateChange, }: SpaceTimerHudWidgetProps) => { + const { pushToast } = useToast(); const { isBreatheMode, hintMessage, triggerRestart } = useRestart30s(); + const normalizedGoal = goal.trim().length > 0 ? goal.trim() : '이번 한 조각을 설정해 주세요.'; return (
+
+
+

Goal

+ +
+

+ {normalizedGoal} +

+
{hintMessage ? ( -

+

{hintMessage}

- ) : ( -

- 목표: {goal} -

- )} + ) : null}
@@ -89,6 +109,15 @@ export const SpaceTimerHudWidget = ({ key={action.id} type="button" title={action.label} + onClick={() => { + if (action.id === 'start') { + onPlaybackStateChange?.('running'); + } + + if (action.id === 'pause') { + onPlaybackStateChange?.('paused'); + } + }} className={cn( 'inline-flex h-8 w-8 items-center justify-center rounded-full border text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80', isImmersionMode