Files
viberoom-web/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx
corpi e9e6006513 style(control-center): 모드 선택 pill로 교체하고 패널 바디로 재배치
맥락:
- Quick Controls 헤더의 모드 토글이 대시보드형 느낌을 만들어 감성 톤을 해치고 있었습니다.

변경사항:
- Control Center 헤더에서는 모드 조작 UI를 제거하고 Plan Pill + 닫기만 유지했습니다.
- 패널 바디 첫 섹션에 기본/몰입 segmented pill을 배치하고, 선택 상태에 따라 저자극 스타일을 적용했습니다.
- 모드 설명 1줄(기본: 모든 컨트롤 표시, 몰입: 필수만 남기고 숨김)을 추가했습니다.
- 모드 상태를 workspace -> tools-dock -> focus-hud 경로로 연결해 HUD 톤 반영을 유지했습니다.

검증:
- npx tsc --noEmit

세션-상태: Quick Controls 헤더가 깔끔해지고 모드 선택이 패널 바디에서 동작합니다.
세션-다음: Scene 추천 매핑 품질 점검과 override UX 검증을 진행합니다.
세션-리스크: 모드 설명 문구의 톤/길이는 실제 사용성 테스트에서 추가 미세조정이 필요할 수 있습니다.
2026-03-05 14:08:09 +09:00

155 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;
isImmersionMode: boolean;
onGoalUpdate: (nextGoal: string) => void;
statusLine: HudStatusLineItem | null;
onStatusAction: () => void;
onStatusMessage: (payload: HudStatusLinePayload) => void;
}
export const SpaceFocusHudWidget = ({
goal,
timerLabel,
visible,
isImmersionMode,
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={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);
}}
/>
</>
);
};