fix(space): stale hud 참조 정리
This commit is contained in:
@@ -2,10 +2,10 @@ 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 { ExitHoldButton } from '@/features/exit-hold';
|
||||
import { GoalCompleteSheet } from './GoalCompleteSheet';
|
||||
import { InlineMicrostep } from './InlineMicrostep';
|
||||
import { ReturnPrompt } from './ReturnPrompt';
|
||||
import { ThoughtOrb } from './ThoughtOrb';
|
||||
|
||||
interface SpaceFocusHudWidgetProps {
|
||||
goal: string;
|
||||
@@ -14,21 +14,12 @@ interface SpaceFocusHudWidgetProps {
|
||||
hasActiveSession?: boolean;
|
||||
playbackState?: 'running' | 'paused';
|
||||
sessionPhase?: 'focus' | 'break' | null;
|
||||
isSessionActionPending?: boolean;
|
||||
canStartSession?: boolean;
|
||||
canPauseSession?: boolean;
|
||||
canRestartSession?: boolean;
|
||||
entryOverlayIntent?: 'resume-refocus' | null;
|
||||
returnPromptMode?: 'focus' | 'break' | null;
|
||||
onEntryOverlayIntentHandled?: () => void;
|
||||
onStartRequested?: () => void;
|
||||
onPauseRequested?: () => void;
|
||||
onRestartRequested?: () => void;
|
||||
onDismissReturnPrompt?: () => void;
|
||||
onIntentUpdate: (payload: { goal?: string; microStep?: string | null }) => boolean | Promise<boolean>;
|
||||
onGoalUpdate: (nextGoal: string) => boolean | Promise<boolean>;
|
||||
onGoalFinish: () => boolean | Promise<boolean>;
|
||||
onStatusMessage: (payload: HudStatusLinePayload) => void;
|
||||
onCaptureThought: (note: string) => void;
|
||||
onExitRequested: () => void;
|
||||
}
|
||||
|
||||
export const SpaceFocusHudWidget = ({
|
||||
@@ -38,42 +29,20 @@ export const SpaceFocusHudWidget = ({
|
||||
hasActiveSession = false,
|
||||
playbackState = 'paused',
|
||||
sessionPhase = 'focus',
|
||||
isSessionActionPending = false,
|
||||
canStartSession = false,
|
||||
canPauseSession = false,
|
||||
canRestartSession = false,
|
||||
entryOverlayIntent = null,
|
||||
returnPromptMode = null,
|
||||
onEntryOverlayIntentHandled,
|
||||
onStartRequested,
|
||||
onPauseRequested,
|
||||
onRestartRequested,
|
||||
onDismissReturnPrompt,
|
||||
onIntentUpdate,
|
||||
onGoalUpdate,
|
||||
onGoalFinish,
|
||||
onStatusMessage,
|
||||
onCaptureThought,
|
||||
onExitRequested,
|
||||
}: SpaceFocusHudWidgetProps) => {
|
||||
const [overlay, setOverlay] = useState<'none' | 'return' | 'complete'>('none');
|
||||
const [overlay, setOverlay] = useState<'none' | 'complete'>('none');
|
||||
const [completePreferredView, setCompletePreferredView] = useState<'choice' | 'next'>('choice');
|
||||
const [isSavingIntent, setSavingIntent] = useState(false);
|
||||
|
||||
const visibleRef = useRef(false);
|
||||
const resumePlaybackStateRef = useRef<'running' | 'paused'>(playbackState);
|
||||
const restReminderTimerRef = useRef<number | null>(null);
|
||||
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : copy.space.focusHud.goalFallback;
|
||||
const isReturnPromptOpen = overlay === 'return';
|
||||
const isCompleteOpen = overlay === 'complete';
|
||||
const isIntentOverlayOpen = overlay !== 'none';
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (restReminderTimerRef.current) {
|
||||
window.clearTimeout(restReminderTimerRef.current);
|
||||
restReminderTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasActiveSession) {
|
||||
@@ -83,21 +52,6 @@ export const SpaceFocusHudWidget = ({
|
||||
}
|
||||
}, [hasActiveSession]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!returnPromptMode) {
|
||||
if (overlay === 'return') {
|
||||
setOverlay('none');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (overlay === 'complete') {
|
||||
return;
|
||||
}
|
||||
|
||||
setOverlay('return');
|
||||
}, [overlay, returnPromptMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visibleRef.current && playbackState === 'running') {
|
||||
onStatusMessage({
|
||||
@@ -108,36 +62,11 @@ export const SpaceFocusHudWidget = ({
|
||||
visibleRef.current = true;
|
||||
}, [normalizedGoal, onStatusMessage, playbackState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resumePlaybackStateRef.current === 'paused' && playbackState === 'running') {
|
||||
onStatusMessage({
|
||||
message: copy.space.focusHud.goalToast(normalizedGoal),
|
||||
});
|
||||
}
|
||||
|
||||
resumePlaybackStateRef.current = playbackState;
|
||||
}, [normalizedGoal, onStatusMessage, playbackState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (entryOverlayIntent !== 'resume-refocus' || !hasActiveSession || overlay !== 'none') {
|
||||
return;
|
||||
}
|
||||
// With inline microsteps, we just handle the intent and let the user click if they want.
|
||||
onEntryOverlayIntentHandled?.();
|
||||
}, [entryOverlayIntent, hasActiveSession, onEntryOverlayIntentHandled, overlay]);
|
||||
|
||||
const handleOpenCompleteSheet = (preferredView: 'choice' | 'next' = 'choice') => {
|
||||
setCompletePreferredView(preferredView);
|
||||
setOverlay('complete');
|
||||
};
|
||||
|
||||
const handleDismissReturnPrompt = () => {
|
||||
onDismissReturnPrompt?.();
|
||||
if (overlay === 'return') {
|
||||
setOverlay('none');
|
||||
}
|
||||
};
|
||||
|
||||
const handleInlineMicrostepUpdate = async (nextStep: string | null) => {
|
||||
if (isSavingIntent) return false;
|
||||
|
||||
@@ -159,18 +88,19 @@ export const SpaceFocusHudWidget = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThoughtOrb isFocusMode={hasActiveSession} onCaptureThought={onCaptureThought} />
|
||||
|
||||
<div className="pointer-events-none fixed inset-0 z-20 flex flex-col items-center justify-center pt-10 pb-32">
|
||||
{/* The Monolith (Central Hub) */}
|
||||
<div className={cn(
|
||||
"pointer-events-auto flex flex-col items-center text-center max-w-4xl px-6 transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)]",
|
||||
isIntentOverlayOpen ? "opacity-0 scale-95 blur-md" : "opacity-100 scale-100 blur-0"
|
||||
isCompleteOpen ? "opacity-0 scale-95 blur-md" : "opacity-100 scale-100 blur-0"
|
||||
)}>
|
||||
{/* Massive Central Timer */}
|
||||
<div className="relative group cursor-pointer" onClick={() => playbackState === 'running' ? onPauseRequested?.() : onStartRequested?.()}>
|
||||
{/* Massive Unstoppable Timer */}
|
||||
<div className="relative group cursor-default">
|
||||
<p className={cn(
|
||||
"text-[8rem] md:text-[14rem] font-light tracking-tighter leading-none transition-colors duration-500",
|
||||
sessionPhase === 'break' ? "text-emerald-300 drop-shadow-[0_0_40px_rgba(16,185,129,0.3)]" : "text-white drop-shadow-[0_0_40px_rgba(255,255,255,0.15)]",
|
||||
playbackState === 'paused' && "opacity-60"
|
||||
sessionPhase === 'break' ? "text-emerald-300 drop-shadow-[0_0_40px_rgba(16,185,129,0.3)]" : "text-white drop-shadow-[0_0_40px_rgba(255,255,255,0.15)]"
|
||||
)}>
|
||||
{timeDisplay}
|
||||
</p>
|
||||
@@ -206,28 +136,6 @@ export const SpaceFocusHudWidget = ({
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-none fixed inset-0 z-30">
|
||||
<ReturnPrompt
|
||||
open={isReturnPromptOpen && Boolean(returnPromptMode)}
|
||||
mode={returnPromptMode === 'break' ? 'break' : 'focus'}
|
||||
isBusy={isSavingIntent}
|
||||
onContinue={() => {
|
||||
handleDismissReturnPrompt();
|
||||
}}
|
||||
onRefocus={() => {
|
||||
handleDismissReturnPrompt();
|
||||
}}
|
||||
onRest={() => {
|
||||
handleDismissReturnPrompt();
|
||||
}}
|
||||
onNextGoal={() => {
|
||||
handleDismissReturnPrompt();
|
||||
handleOpenCompleteSheet('next');
|
||||
}}
|
||||
onFinish={() => {
|
||||
handleDismissReturnPrompt();
|
||||
handleOpenCompleteSheet('choice');
|
||||
}}
|
||||
/>
|
||||
<GoalCompleteSheet
|
||||
open={isCompleteOpen}
|
||||
currentGoal={goal}
|
||||
@@ -236,16 +144,7 @@ export const SpaceFocusHudWidget = ({
|
||||
onFinish={() => Promise.resolve(onGoalFinish())}
|
||||
onRest={() => {
|
||||
setOverlay('none');
|
||||
onPauseRequested?.();
|
||||
|
||||
if (restReminderTimerRef.current) {
|
||||
window.clearTimeout(restReminderTimerRef.current);
|
||||
}
|
||||
|
||||
restReminderTimerRef.current = window.setTimeout(() => {
|
||||
onStatusMessage({ message: copy.space.focusHud.restReminder });
|
||||
restReminderTimerRef.current = null;
|
||||
}, 5 * 60 * 1000);
|
||||
// The timer doesn't pause, they just rest within the flow.
|
||||
}}
|
||||
onConfirm={(nextGoal) => {
|
||||
return Promise.resolve(onGoalUpdate(nextGoal));
|
||||
@@ -253,18 +152,16 @@ export const SpaceFocusHudWidget = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SpaceTimerHudWidget
|
||||
hasActiveSession={hasActiveSession}
|
||||
sessionPhase={sessionPhase}
|
||||
playbackState={playbackState}
|
||||
isControlsDisabled={isSessionActionPending}
|
||||
canStart={canStartSession}
|
||||
canPause={canPauseSession}
|
||||
canReset={canRestartSession}
|
||||
onStartClick={onStartRequested}
|
||||
onPauseClick={onPauseRequested}
|
||||
onResetClick={onRestartRequested}
|
||||
/>
|
||||
{/* Emergency Tether (Exit) */}
|
||||
<div className="fixed bottom-8 inset-x-0 z-40 flex justify-center pointer-events-none">
|
||||
<div className="pointer-events-auto opacity-10 hover:opacity-100 transition-opacity duration-500">
|
||||
<ExitHoldButton
|
||||
variant="bar"
|
||||
onConfirm={onExitRequested}
|
||||
className="bg-black/20 text-white/70 hover:bg-black/40 hover:text-white border border-white/5 backdrop-blur-md px-6 py-2 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user