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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,11 +19,9 @@ import { useHudStatusLine } from "@/shared/lib/useHudStatusLine";
|
||||
import { copy } from "@/shared/i18n";
|
||||
import { SpaceFocusHudWidget } from "@/widgets/space-focus-hud";
|
||||
import { SpaceSetupDrawerWidget } from "@/widgets/space-setup-drawer";
|
||||
import { SpaceToolsDockWidget } from "@/widgets/space-tools-dock";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { SessionEntryPoint, WorkspaceMode } from "../model/types";
|
||||
import { useAwayReturnRecovery } from "../model/useAwayReturnRecovery";
|
||||
import { useSpaceWorkspaceSelection } from "../model/useSpaceWorkspaceSelection";
|
||||
import { useSpaceWorkspaceSessionControls } from "../model/useSpaceWorkspaceSessionControls";
|
||||
import {
|
||||
@@ -31,7 +29,6 @@ import {
|
||||
resolveInitialSceneId,
|
||||
resolveInitialSoundPreset,
|
||||
resolveInitialTimerLabel,
|
||||
resolveTimerLabelFromPresetId,
|
||||
TIMER_SELECTION_PRESETS,
|
||||
} from "../model/workspaceSelection";
|
||||
import { FocusTopToast } from "./FocusTopToast";
|
||||
@@ -49,14 +46,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
);
|
||||
|
||||
const {
|
||||
thoughts,
|
||||
thoughtCount,
|
||||
addThought,
|
||||
removeThought,
|
||||
clearThoughts,
|
||||
restoreThought,
|
||||
restoreThoughts,
|
||||
setThoughtCompleted,
|
||||
} = useThoughtInbox();
|
||||
|
||||
const {
|
||||
@@ -108,14 +98,11 @@ export const SpaceWorkspaceWidget = () => {
|
||||
selectedPresetId,
|
||||
setSelectedPresetId,
|
||||
masterVolume,
|
||||
setMasterVolume,
|
||||
isMuted,
|
||||
setMuted,
|
||||
} = useSoundPresetSelection(initialSoundPresetId);
|
||||
const {
|
||||
currentSession,
|
||||
isBootstrapping,
|
||||
isMutating: isSessionMutating,
|
||||
timeDisplay,
|
||||
phase,
|
||||
startSession,
|
||||
@@ -127,7 +114,6 @@ export const SpaceWorkspaceWidget = () => {
|
||||
completeSession,
|
||||
advanceGoal,
|
||||
abandonSession,
|
||||
syncCurrentSession,
|
||||
} = useFocusSessionEngine();
|
||||
|
||||
const isFocusMode = workspaceMode === "focus";
|
||||
@@ -208,13 +194,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
setSelectedGoalId: selection.setSelectedGoalId,
|
||||
setShowResumePrompt: selection.setShowResumePrompt,
|
||||
});
|
||||
const handleStartRequested = controls.handleStartRequested;
|
||||
|
||||
const awayReturnRecovery = useAwayReturnRecovery({
|
||||
currentSession,
|
||||
isBootstrapping,
|
||||
syncCurrentSession,
|
||||
});
|
||||
const hasEnoughWeeklyData =
|
||||
weeklySummary.last7Days.startedSessions >= 3 &&
|
||||
(weeklySummary.last7Days.completedSessions >= 2 ||
|
||||
@@ -349,22 +329,6 @@ export const SpaceWorkspaceWidget = () => {
|
||||
hasActiveSession={Boolean(currentSession)}
|
||||
playbackState={resolvedPlaybackState}
|
||||
sessionPhase={phase ?? 'focus'}
|
||||
isSessionActionPending={isSessionMutating}
|
||||
canStartSession={controls.canStartSession}
|
||||
canPauseSession={controls.canPauseSession}
|
||||
canRestartSession={controls.canRestartSession}
|
||||
returnPromptMode={awayReturnRecovery.returnPromptMode}
|
||||
onStartRequested={() => {
|
||||
void handleStartRequested();
|
||||
}}
|
||||
onPauseRequested={() => {
|
||||
void controls.handlePauseRequested();
|
||||
}}
|
||||
onRestartRequested={() => {
|
||||
void controls.handleRestartRequested();
|
||||
}}
|
||||
onDismissReturnPrompt={awayReturnRecovery.dismissReturnPrompt}
|
||||
onStatusMessage={pushStatusLine}
|
||||
onIntentUpdate={controls.handleIntentUpdate}
|
||||
onGoalFinish={async () => {
|
||||
const didFinish = await controls.handleGoalComplete();
|
||||
@@ -376,6 +340,9 @@ export const SpaceWorkspaceWidget = () => {
|
||||
return didFinish;
|
||||
}}
|
||||
onGoalUpdate={controls.handleGoalAdvance}
|
||||
onStatusMessage={pushStatusLine}
|
||||
onCaptureThought={(note) => addThought(note, selection.selectedScene.name)}
|
||||
onExitRequested={() => void controls.handleExitRequested()}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -385,47 +352,6 @@ export const SpaceWorkspaceWidget = () => {
|
||||
actionLabel={activeStatus?.action?.label}
|
||||
onAction={runActiveAction}
|
||||
/>
|
||||
|
||||
<SpaceToolsDockWidget
|
||||
isFocusMode={isFocusMode}
|
||||
scenes={selection.setupScenes}
|
||||
sceneAssetMap={sceneAssetMap}
|
||||
selectedSceneId={selection.selectedScene.id}
|
||||
selectedTimerLabel={selection.selectedTimerLabel}
|
||||
timerPresets={TIMER_SELECTION_PRESETS}
|
||||
thoughts={thoughts}
|
||||
thoughtCount={thoughtCount}
|
||||
selectedPresetId={selectedPresetId}
|
||||
onSceneSelect={selection.handleSelectScene}
|
||||
onTimerSelect={(timerLabel) =>
|
||||
selection.handleSelectTimer(timerLabel, true)
|
||||
}
|
||||
onQuickSoundSelect={(presetId) =>
|
||||
selection.handleSelectSound(presetId, true)
|
||||
}
|
||||
sceneRecommendedSoundLabel={selection.selectedScene.recommendedSound}
|
||||
sceneRecommendedTimerLabel={
|
||||
resolveTimerLabelFromPresetId(
|
||||
selection.selectedScene.recommendedTimerPresetId,
|
||||
) ?? selection.selectedTimerLabel
|
||||
}
|
||||
soundVolume={masterVolume}
|
||||
onSetSoundVolume={setMasterVolume}
|
||||
isSoundMuted={isMuted}
|
||||
onSetSoundMuted={setMuted}
|
||||
onCaptureThought={(note) =>
|
||||
addThought(note, selection.selectedScene.name)
|
||||
}
|
||||
onDeleteThought={removeThought}
|
||||
onSetThoughtCompleted={setThoughtCompleted}
|
||||
onRestoreThought={restoreThought}
|
||||
onRestoreThoughts={restoreThoughts}
|
||||
onClearInbox={clearThoughts}
|
||||
onStatusMessage={pushStatusLine}
|
||||
onExitRequested={() => {
|
||||
void controls.handleExitRequested();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user