맥락: - 몰입모드 토글은 상태 인지 비용을 높여 집중 흐름을 끊고, Quick Controls 헤더를 대시보드형으로 보이게 만들고 있었습니다. 변경사항: - Quick Controls에서 기본/몰입 모드 토글 UI를 완전히 제거했습니다. - Focus 화면의 HUD 톤은 외부 모드 상태 없이 항상 몰입 톤으로 렌더링되도록 고정했습니다. - workspace/tools-dock/focus-hud 간 모드 토글 상태 전달 경로를 정리해, 컨트롤은 패널을 열었을 때만 보이는 Focus-First 흐름으로 단순화했습니다. 검증: - npx tsc --noEmit 세션-상태: 모드 토글 없이 패널 열림 상태만으로 컨트롤 노출이 정의됩니다. 세션-다음: (선택) 컨트롤 자동 숨김 표시 정책 옵션을 패널 내부에 추가합니다. 세션-리스크: 자동 숨김 정책이 아직 없어 패널을 열어둔 채 방치되는 경우 수동 닫기가 필요합니다.
153 lines
4.1 KiB
TypeScript
153 lines
4.1 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import type { HudStatusLineItem, HudStatusLinePayload } from '@/shared/lib/useHudStatusLine';
|
|
import { useReducedMotion } from '@/shared/lib/useReducedMotion';
|
|
import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud';
|
|
import { GoalCompleteSheet } from './GoalCompleteSheet';
|
|
import { GoalFlashOverlay } from './GoalFlashOverlay';
|
|
|
|
interface SpaceFocusHudWidgetProps {
|
|
goal: string;
|
|
timerLabel: string;
|
|
visible: boolean;
|
|
onGoalUpdate: (nextGoal: string) => void;
|
|
statusLine: HudStatusLineItem | null;
|
|
onStatusAction: () => void;
|
|
onStatusMessage: (payload: HudStatusLinePayload) => void;
|
|
}
|
|
|
|
export const SpaceFocusHudWidget = ({
|
|
goal,
|
|
timerLabel,
|
|
visible,
|
|
onGoalUpdate,
|
|
statusLine,
|
|
onStatusAction,
|
|
onStatusMessage,
|
|
}: SpaceFocusHudWidgetProps) => {
|
|
const reducedMotion = useReducedMotion();
|
|
const [flashVisible, setFlashVisible] = useState(false);
|
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
const playbackStateRef = useRef<'running' | 'paused'>('running');
|
|
const flashTimerRef = useRef<number | null>(null);
|
|
const restReminderTimerRef = 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;
|
|
}
|
|
if (restReminderTimerRef.current) {
|
|
window.clearTimeout(restReminderTimerRef.current);
|
|
restReminderTimerRef.current = null;
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!visible || reducedMotion) {
|
|
setFlashVisible(false);
|
|
return;
|
|
}
|
|
|
|
triggerFlash(2000);
|
|
}, [visible, reducedMotion, triggerFlash]);
|
|
|
|
useEffect(() => {
|
|
const ENABLE_PERIODIC_FLASH = false;
|
|
|
|
if (!visible || reducedMotion || !ENABLE_PERIODIC_FLASH) {
|
|
return;
|
|
}
|
|
|
|
const intervalId = window.setInterval(() => {
|
|
triggerFlash(800);
|
|
}, 10 * 60 * 1000);
|
|
|
|
return () => {
|
|
window.clearInterval(intervalId);
|
|
};
|
|
}, [visible, reducedMotion, triggerFlash]);
|
|
|
|
if (!visible) {
|
|
return null;
|
|
}
|
|
|
|
const handleOpenCompleteSheet = () => {
|
|
setSheetOpen(true);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<GoalFlashOverlay goal={goal} visible={flashVisible} />
|
|
<SpaceTimerHudWidget
|
|
timerLabel={timerLabel}
|
|
goal={goal}
|
|
statusLine={
|
|
statusLine
|
|
? {
|
|
message: statusLine.message,
|
|
actionLabel: statusLine.action?.label,
|
|
}
|
|
: null
|
|
}
|
|
isImmersionMode
|
|
className="pr-[4.2rem]"
|
|
onGoalCompleteRequest={handleOpenCompleteSheet}
|
|
onStatusAction={onStatusAction}
|
|
onPlaybackStateChange={(state) => {
|
|
if (reducedMotion) {
|
|
playbackStateRef.current = state;
|
|
return;
|
|
}
|
|
|
|
if (playbackStateRef.current === 'paused' && state === 'running') {
|
|
triggerFlash(1000);
|
|
}
|
|
|
|
playbackStateRef.current = state;
|
|
}}
|
|
/>
|
|
<GoalCompleteSheet
|
|
open={sheetOpen}
|
|
currentGoal={goal}
|
|
onClose={() => setSheetOpen(false)}
|
|
onRest={() => {
|
|
setSheetOpen(false);
|
|
|
|
if (restReminderTimerRef.current) {
|
|
window.clearTimeout(restReminderTimerRef.current);
|
|
}
|
|
|
|
restReminderTimerRef.current = window.setTimeout(() => {
|
|
onStatusMessage({ message: '5분이 지났어요. 다음 한 조각으로 돌아와요.' });
|
|
restReminderTimerRef.current = null;
|
|
}, 5 * 60 * 1000);
|
|
}}
|
|
onConfirm={(nextGoal) => {
|
|
onGoalUpdate(nextGoal);
|
|
setSheetOpen(false);
|
|
triggerFlash(1200);
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
};
|