feat(goal): Focus HUD 목표 가독성·Flash 상기·완료 액션 추가

This commit is contained in:
2026-03-04 15:49:42 +09:00
parent 96b6c0cb8f
commit b38455bf56
3 changed files with 139 additions and 13 deletions

View File

@@ -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 (
<div
className={cn(
'pointer-events-none fixed inset-x-0 z-20 flex justify-center px-4 transition-opacity duration-[240ms] ease-out motion-reduce:duration-0',
visible ? 'opacity-100' : 'opacity-0',
)}
style={{ bottom: 'calc(env(safe-area-inset-bottom, 0px) + 5.1rem)' }}
aria-hidden
>
<div className="rounded-full border border-white/12 bg-black/24 px-3 py-1.5 text-xs text-white/82 backdrop-blur-md">
: <span className="text-white/92">{normalizedGoal}</span>
</div>
</div>
);
};

View File

@@ -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<number | null>(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 (
<SpaceTimerHudWidget
timerLabel={timerLabel}
goal={goal}
isImmersionMode
className="pr-[4.2rem]"
/>
<>
<GoalFlashOverlay goal={goal} visible={flashVisible} />
<SpaceTimerHudWidget
timerLabel={timerLabel}
goal={goal}
isImmersionMode
className="pr-[4.2rem]"
onPlaybackStateChange={(state) => {
if (reducedMotion) {
playbackStateRef.current = state;
return;
}
if (playbackStateRef.current === 'paused' && state === 'running') {
triggerFlash(1000);
}
playbackStateRef.current = state;
}}
/>
</>
);
};