feat(space/app): app 진입부 및 space 몰입 환경(HUD/Tools) 프리미엄 UI 리팩토링
맥락: - 기존 app 대시보드와 space 화면의 UI가 SaaS 툴처럼 딱딱하고 투박하여, 유저가 기꺼이 지갑을 열 만한 몰입감과 고급스러움(Premium feel)이 부족함. - 인지적 과부하를 줄이기 위해 제안된 '첫 5분 행동(Micro-step)'이 타이머 영역에 묻혀 있어 행동 유발 효과가 미미함. 변경사항: - app: 컨테이너 박스를 제거하고 전체 배경 화면(Immersive Background)과 Glassmorphism을 활용한 1.5 Step 진입 플로우로 전면 개편. - space/hud: 하단의 두꺼운 타이머 패널을 초박형(Slim) 글라스 알약 형태로 축소하여 배경 씬의 개방감 확보. - space/hud: 목표(Goal)와 첫 단계(Micro-step)를 분리하여 좌측 상단의 우아한 Floating UI로 재배치하고, 체크 완료 시 사라지는 도파민 인터랙션 추가. - space/tools: 흩어져 있던 노트, 사운드, 설정 도구들을 우측 레일(Right-Rail)로 통합하고 팝오버 디자인을 고급화함. - ui/contrast: 밝은 배경에서도 텍스트가 잘 보이도록 좌측 상단 비네팅(Vignette) 및 다중 텍스트 그림자(Multi-layered Shadow) 효과 적용. 검증: - npm run build 정상 통과 확인. - 브라우저 상에서 micro-step 완료 애니메이션 및 도구막대 팝오버 슬라이드 동작 확인. 세션-상태: app 진입부터 space 몰입까지의 코어 UX/UI 하이엔드 개편 완료. 세션-다음: 프로 요금제(PRO) 전환 유도(Paywall) 흐름 및 상세 분석 리포트(Analytics) 뷰 구현. 세션-리스크: 없음.
This commit is contained in:
@@ -28,6 +28,7 @@ import { useWorkspaceMediaDiagnostics } from './useWorkspaceMediaDiagnostics';
|
||||
interface UseSpaceWorkspaceSelectionParams {
|
||||
initialSceneId: string;
|
||||
initialGoal: string;
|
||||
initialFocusPlanItemId: string | null;
|
||||
initialTimerLabel: string;
|
||||
sceneQuery: string | null;
|
||||
goalQuery: string;
|
||||
@@ -64,6 +65,7 @@ const getVisibleSetupScenes = (selectedScene: SceneTheme) => {
|
||||
export const useSpaceWorkspaceSelection = ({
|
||||
initialSceneId,
|
||||
initialGoal,
|
||||
initialFocusPlanItemId,
|
||||
initialTimerLabel,
|
||||
sceneQuery,
|
||||
goalQuery,
|
||||
@@ -86,6 +88,7 @@ export const useSpaceWorkspaceSelection = ({
|
||||
const [selectedSceneId, setSelectedSceneId] = useState(initialSceneId);
|
||||
const [selectedTimerLabel, setSelectedTimerLabel] = useState(initialTimerLabel);
|
||||
const [goalInput, setGoalInput] = useState(initialGoal);
|
||||
const [linkedFocusPlanItemId, setLinkedFocusPlanItemId] = useState<string | null>(initialFocusPlanItemId);
|
||||
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
|
||||
const [resumeGoal, setResumeGoal] = useState('');
|
||||
const [showResumePrompt, setShowResumePrompt] = useState(false);
|
||||
@@ -262,6 +265,7 @@ export const useSpaceWorkspaceSelection = ({
|
||||
|
||||
const handleGoalChipSelect = useCallback((chip: GoalChip) => {
|
||||
setShowResumePrompt(false);
|
||||
setLinkedFocusPlanItemId(null);
|
||||
setSelectedGoalId(chip.id);
|
||||
setGoalInput(chip.label);
|
||||
}, []);
|
||||
@@ -271,6 +275,7 @@ export const useSpaceWorkspaceSelection = ({
|
||||
setShowResumePrompt(false);
|
||||
}
|
||||
|
||||
setLinkedFocusPlanItemId(null);
|
||||
setGoalInput(value);
|
||||
|
||||
if (value.trim().length === 0) {
|
||||
@@ -385,6 +390,7 @@ export const useSpaceWorkspaceSelection = ({
|
||||
setSelectedTimerLabel(nextTimerLabel);
|
||||
setSelectedPresetId(nextSoundPresetId);
|
||||
setGoalInput(currentSession.goal);
|
||||
setLinkedFocusPlanItemId(currentSession.focusPlanItemId ?? null);
|
||||
setSelectedGoalId(null);
|
||||
setShowResumePrompt(false);
|
||||
});
|
||||
@@ -418,6 +424,7 @@ export const useSpaceWorkspaceSelection = ({
|
||||
selectedSceneId,
|
||||
selectedTimerLabel,
|
||||
goalInput,
|
||||
linkedFocusPlanItemId,
|
||||
selectedGoalId,
|
||||
resumeGoal,
|
||||
showResumePrompt,
|
||||
@@ -428,6 +435,7 @@ export const useSpaceWorkspaceSelection = ({
|
||||
setupScenes,
|
||||
canStart,
|
||||
setGoalInput,
|
||||
setLinkedFocusPlanItemId,
|
||||
setSelectedGoalId,
|
||||
setShowResumePrompt,
|
||||
setResumeGoal,
|
||||
|
||||
@@ -17,6 +17,7 @@ interface UseSpaceWorkspaceSessionControlsParams {
|
||||
canStart: boolean;
|
||||
currentSession: FocusSession | null;
|
||||
goalInput: string;
|
||||
linkedFocusPlanItemId: string | null;
|
||||
selectedSceneId: string;
|
||||
selectedTimerLabel: string;
|
||||
selectedPresetId: string;
|
||||
@@ -28,18 +29,24 @@ interface UseSpaceWorkspaceSessionControlsParams {
|
||||
sceneId: string;
|
||||
goal: string;
|
||||
timerPresetId: string;
|
||||
soundPresetId: string;
|
||||
soundPresetId: string | null;
|
||||
focusPlanItemId?: string;
|
||||
entryPoint: SessionEntryPoint;
|
||||
}) => Promise<FocusSession | null>;
|
||||
pauseSession: () => Promise<FocusSession | null>;
|
||||
resumeSession: () => Promise<FocusSession | null>;
|
||||
restartCurrentPhase: () => Promise<FocusSession | null>;
|
||||
completeSession: (input: {
|
||||
completionType: 'goal-complete';
|
||||
advanceGoal: (input: {
|
||||
completedGoal: string;
|
||||
}) => Promise<FocusSession | null>;
|
||||
nextGoal: string;
|
||||
sceneId: string;
|
||||
timerPresetId: string;
|
||||
soundPresetId: string;
|
||||
focusPlanItemId?: string;
|
||||
}) => Promise<{ nextSession: FocusSession } | null>;
|
||||
abandonSession: () => Promise<boolean>;
|
||||
setGoalInput: (value: string) => void;
|
||||
setLinkedFocusPlanItemId: (value: string | null) => void;
|
||||
setSelectedGoalId: (value: string | null) => void;
|
||||
setShowResumePrompt: (value: boolean) => void;
|
||||
}
|
||||
@@ -54,6 +61,7 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
canStart,
|
||||
currentSession,
|
||||
goalInput,
|
||||
linkedFocusPlanItemId,
|
||||
selectedSceneId,
|
||||
selectedTimerLabel,
|
||||
selectedPresetId,
|
||||
@@ -65,9 +73,10 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
pauseSession,
|
||||
resumeSession,
|
||||
restartCurrentPhase,
|
||||
completeSession,
|
||||
advanceGoal,
|
||||
abandonSession,
|
||||
setGoalInput,
|
||||
setLinkedFocusPlanItemId,
|
||||
setSelectedGoalId,
|
||||
setShowResumePrompt,
|
||||
}: UseSpaceWorkspaceSessionControlsParams) => {
|
||||
@@ -110,6 +119,7 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
goal: trimmedGoal,
|
||||
timerPresetId,
|
||||
soundPresetId: selectedPresetId,
|
||||
focusPlanItemId: linkedFocusPlanItemId ?? undefined,
|
||||
entryPoint: pendingSessionEntryPoint,
|
||||
});
|
||||
|
||||
@@ -129,6 +139,7 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
selectedPresetId,
|
||||
selectedSceneId,
|
||||
selectedTimerLabel,
|
||||
linkedFocusPlanItemId,
|
||||
setPreviewPlaybackState,
|
||||
startSession,
|
||||
]);
|
||||
@@ -222,33 +233,61 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
|
||||
const handleGoalAdvance = useCallback(async (nextGoal: string) => {
|
||||
const trimmedNextGoal = nextGoal.trim();
|
||||
const trimmedCurrentGoal = goalInput.trim();
|
||||
const timerPresetId = resolveTimerPresetIdFromLabel(selectedTimerLabel);
|
||||
|
||||
if (!trimmedNextGoal) {
|
||||
return;
|
||||
if (!trimmedNextGoal || !trimmedCurrentGoal || !timerPresetId || !currentSession) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentSession) {
|
||||
const completedSession = await completeSession({
|
||||
completionType: 'goal-complete',
|
||||
completedGoal: goalInput.trim(),
|
||||
});
|
||||
await unlockPlayback(resolveSoundPlaybackUrl(selectedPresetId));
|
||||
|
||||
if (!completedSession) {
|
||||
pushStatusLine({
|
||||
message: copy.space.workspace.goalCompleteSyncFailed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const nextState = await advanceGoal({
|
||||
completedGoal: trimmedCurrentGoal,
|
||||
nextGoal: trimmedNextGoal,
|
||||
sceneId: selectedSceneId,
|
||||
timerPresetId,
|
||||
soundPresetId: selectedPresetId,
|
||||
focusPlanItemId: linkedFocusPlanItemId ?? undefined,
|
||||
});
|
||||
|
||||
if (!nextState) {
|
||||
pushStatusLine({
|
||||
message: copy.space.workspace.goalCompleteSyncFailed,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
setGoalInput(trimmedNextGoal);
|
||||
setLinkedFocusPlanItemId(nextState.nextSession.focusPlanItemId ?? null);
|
||||
setSelectedGoalId(null);
|
||||
setShowResumePrompt(false);
|
||||
setPendingSessionEntryPoint('goal-complete');
|
||||
setPreviewPlaybackState('paused');
|
||||
setPreviewPlaybackState('running');
|
||||
setWorkspaceMode('focus');
|
||||
pushStatusLine({
|
||||
message: copy.space.workspace.nextGoalReady,
|
||||
message: copy.space.workspace.nextGoalStarted,
|
||||
});
|
||||
}, [completeSession, currentSession, goalInput, pushStatusLine, setGoalInput, setPendingSessionEntryPoint, setPreviewPlaybackState, setSelectedGoalId]);
|
||||
return true;
|
||||
}, [
|
||||
advanceGoal,
|
||||
currentSession,
|
||||
goalInput,
|
||||
linkedFocusPlanItemId,
|
||||
pushStatusLine,
|
||||
resolveSoundPlaybackUrl,
|
||||
selectedPresetId,
|
||||
selectedSceneId,
|
||||
selectedTimerLabel,
|
||||
setGoalInput,
|
||||
setLinkedFocusPlanItemId,
|
||||
setPendingSessionEntryPoint,
|
||||
setPreviewPlaybackState,
|
||||
setSelectedGoalId,
|
||||
setShowResumePrompt,
|
||||
setWorkspaceMode,
|
||||
unlockPlayback,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const previousBodyOverflow = document.body.style.overflow;
|
||||
|
||||
@@ -1,45 +1,48 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { getSceneById, SCENE_THEMES } from '@/entities/scene';
|
||||
import {
|
||||
getSceneStageBackgroundStyle,
|
||||
getSceneStagePhotoUrl,
|
||||
preloadAssetImage,
|
||||
useMediaCatalog,
|
||||
} from '@/entities/media';
|
||||
} from "@/entities/media";
|
||||
import { getSceneById, SCENE_THEMES } from "@/entities/scene";
|
||||
import { GOAL_CHIPS, SOUND_PRESETS, useThoughtInbox } from "@/entities/session";
|
||||
import { useFocusSessionEngine } from "@/features/focus-session";
|
||||
import {
|
||||
GOAL_CHIPS,
|
||||
SOUND_PRESETS,
|
||||
useThoughtInbox,
|
||||
} from '@/entities/session';
|
||||
import { useFocusSessionEngine } from '@/features/focus-session';
|
||||
import { useSoundPlayback, useSoundPresetSelection } from '@/features/sound-preset';
|
||||
import { useHudStatusLine } from '@/shared/lib/useHudStatusLine';
|
||||
import { SpaceFocusHudWidget } from '@/widgets/space-focus-hud';
|
||||
import { SpaceSetupDrawerWidget } from '@/widgets/space-setup-drawer';
|
||||
import { SpaceToolsDockWidget } from '@/widgets/space-tools-dock';
|
||||
import type { SessionEntryPoint, WorkspaceMode } from '../model/types';
|
||||
import { useSpaceWorkspaceSelection } from '../model/useSpaceWorkspaceSelection';
|
||||
import { useSpaceWorkspaceSessionControls } from '../model/useSpaceWorkspaceSessionControls';
|
||||
useSoundPlayback,
|
||||
useSoundPresetSelection,
|
||||
} from "@/features/sound-preset";
|
||||
import { useHudStatusLine } from "@/shared/lib/useHudStatusLine";
|
||||
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, useState } from "react";
|
||||
import type { SessionEntryPoint, WorkspaceMode } from "../model/types";
|
||||
import { useSpaceWorkspaceSelection } from "../model/useSpaceWorkspaceSelection";
|
||||
import { useSpaceWorkspaceSessionControls } from "../model/useSpaceWorkspaceSessionControls";
|
||||
import {
|
||||
resolveFocusTimeDisplayFromTimerLabel,
|
||||
resolveInitialSceneId,
|
||||
resolveInitialSoundPreset,
|
||||
resolveInitialTimerLabel,
|
||||
resolveTimerLabelFromPresetId,
|
||||
TIMER_SELECTION_PRESETS,
|
||||
resolveFocusTimeDisplayFromTimerLabel,
|
||||
} from '../model/workspaceSelection';
|
||||
import { FocusTopToast } from './FocusTopToast';
|
||||
} from "../model/workspaceSelection";
|
||||
import { FocusTopToast } from "./FocusTopToast";
|
||||
|
||||
export const SpaceWorkspaceWidget = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const sceneQuery = searchParams.get('scene') ?? searchParams.get('room');
|
||||
const goalQuery = searchParams.get('goal')?.trim() ?? '';
|
||||
const soundQuery = searchParams.get('sound');
|
||||
const timerQuery = searchParams.get('timer');
|
||||
const hasQueryOverrides = Boolean(sceneQuery || goalQuery || soundQuery || timerQuery);
|
||||
const router = useRouter();
|
||||
const sceneQuery = searchParams.get("scene") ?? searchParams.get("room");
|
||||
const goalQuery = searchParams.get("goal")?.trim() ?? "";
|
||||
const focusPlanItemIdQuery = searchParams.get("planItemId");
|
||||
const soundQuery = searchParams.get("sound");
|
||||
const timerQuery = searchParams.get("timer");
|
||||
const hasQueryOverrides = Boolean(
|
||||
sceneQuery || goalQuery || focusPlanItemIdQuery || soundQuery || timerQuery,
|
||||
);
|
||||
|
||||
const {
|
||||
thoughts,
|
||||
@@ -60,22 +63,39 @@ export const SpaceWorkspaceWidget = () => {
|
||||
hasResolvedManifest,
|
||||
} = useMediaCatalog();
|
||||
|
||||
const initialSceneId = useMemo(() => resolveInitialSceneId(sceneQuery, undefined), [sceneQuery]);
|
||||
const initialScene = useMemo(() => getSceneById(initialSceneId) ?? SCENE_THEMES[0], [initialSceneId]);
|
||||
const initialSoundPresetId = useMemo(() => resolveInitialSoundPreset(
|
||||
soundQuery,
|
||||
undefined,
|
||||
initialScene.recommendedSoundPresetId,
|
||||
), [initialScene.recommendedSoundPresetId, soundQuery]);
|
||||
const initialTimerLabel = useMemo(() => resolveInitialTimerLabel(
|
||||
timerQuery,
|
||||
undefined,
|
||||
initialScene.recommendedTimerPresetId,
|
||||
), [initialScene.recommendedTimerPresetId, timerQuery]);
|
||||
const initialSceneId = useMemo(
|
||||
() => resolveInitialSceneId(sceneQuery, undefined),
|
||||
[sceneQuery],
|
||||
);
|
||||
const initialScene = useMemo(
|
||||
() => getSceneById(initialSceneId) ?? SCENE_THEMES[0],
|
||||
[initialSceneId],
|
||||
);
|
||||
const initialSoundPresetId = useMemo(
|
||||
() =>
|
||||
resolveInitialSoundPreset(
|
||||
soundQuery,
|
||||
undefined,
|
||||
initialScene.recommendedSoundPresetId,
|
||||
),
|
||||
[initialScene.recommendedSoundPresetId, soundQuery],
|
||||
);
|
||||
const initialTimerLabel = useMemo(
|
||||
() =>
|
||||
resolveInitialTimerLabel(
|
||||
timerQuery,
|
||||
undefined,
|
||||
initialScene.recommendedTimerPresetId,
|
||||
),
|
||||
[initialScene.recommendedTimerPresetId, timerQuery],
|
||||
);
|
||||
|
||||
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>('setup');
|
||||
const [previewPlaybackState, setPreviewPlaybackState] = useState<'running' | 'paused'>('paused');
|
||||
const [pendingSessionEntryPoint, setPendingSessionEntryPoint] = useState<SessionEntryPoint>('space-setup');
|
||||
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>("setup");
|
||||
const [previewPlaybackState, setPreviewPlaybackState] = useState<
|
||||
"running" | "paused"
|
||||
>("paused");
|
||||
const [pendingSessionEntryPoint, setPendingSessionEntryPoint] =
|
||||
useState<SessionEntryPoint>("space-setup");
|
||||
|
||||
const {
|
||||
selectedPresetId,
|
||||
@@ -85,9 +105,9 @@ export const SpaceWorkspaceWidget = () => {
|
||||
isMuted,
|
||||
setMuted,
|
||||
} = useSoundPresetSelection(initialSoundPresetId);
|
||||
|
||||
const {
|
||||
currentSession,
|
||||
isBootstrapping,
|
||||
isMutating: isSessionMutating,
|
||||
timeDisplay,
|
||||
playbackState,
|
||||
@@ -97,15 +117,16 @@ export const SpaceWorkspaceWidget = () => {
|
||||
resumeSession,
|
||||
restartCurrentPhase,
|
||||
updateCurrentSelection,
|
||||
completeSession,
|
||||
advanceGoal,
|
||||
abandonSession,
|
||||
} = useFocusSessionEngine();
|
||||
|
||||
const isFocusMode = workspaceMode === 'focus';
|
||||
const isFocusMode = workspaceMode === "focus";
|
||||
const resolvedPlaybackState = currentSession?.state ?? previewPlaybackState;
|
||||
const shouldPlaySound = isFocusMode && resolvedPlaybackState === 'running';
|
||||
const shouldPlaySound = isFocusMode && resolvedPlaybackState === "running";
|
||||
|
||||
const { activeStatus, pushStatusLine, runActiveAction } = useHudStatusLine(isFocusMode);
|
||||
const { activeStatus, pushStatusLine, runActiveAction } =
|
||||
useHudStatusLine(isFocusMode);
|
||||
|
||||
const { error: soundPlaybackError, unlockPlayback } = useSoundPlayback({
|
||||
selectedPresetId,
|
||||
@@ -116,7 +137,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
});
|
||||
|
||||
const resolveSoundPlaybackUrl = (presetId: string) => {
|
||||
if (presetId === 'silent') {
|
||||
if (presetId === "silent") {
|
||||
return null;
|
||||
}
|
||||
const asset = soundAssetMap[presetId];
|
||||
@@ -126,6 +147,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
const selection = useSpaceWorkspaceSelection({
|
||||
initialSceneId,
|
||||
initialGoal: goalQuery,
|
||||
initialFocusPlanItemId: focusPlanItemIdQuery,
|
||||
initialTimerLabel,
|
||||
sceneQuery,
|
||||
goalQuery,
|
||||
@@ -156,6 +178,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
canStart: selection.canStart,
|
||||
currentSession,
|
||||
goalInput: selection.goalInput,
|
||||
linkedFocusPlanItemId: selection.linkedFocusPlanItemId,
|
||||
selectedSceneId: selection.selectedSceneId,
|
||||
selectedTimerLabel: selection.selectedTimerLabel,
|
||||
selectedPresetId,
|
||||
@@ -167,27 +190,47 @@ export const SpaceWorkspaceWidget = () => {
|
||||
pauseSession,
|
||||
resumeSession,
|
||||
restartCurrentPhase,
|
||||
completeSession,
|
||||
advanceGoal,
|
||||
abandonSession,
|
||||
setGoalInput: selection.setGoalInput,
|
||||
setLinkedFocusPlanItemId: selection.setLinkedFocusPlanItemId,
|
||||
setSelectedGoalId: selection.setSelectedGoalId,
|
||||
setShowResumePrompt: selection.setShowResumePrompt,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBootstrapping && !currentSession && !hasQueryOverrides) {
|
||||
router.replace("/app");
|
||||
}
|
||||
}, [isBootstrapping, currentSession, hasQueryOverrides, router]);
|
||||
|
||||
useEffect(() => {
|
||||
const preferMobile =
|
||||
typeof window !== 'undefined' ? window.matchMedia('(max-width: 767px)').matches : false;
|
||||
preloadAssetImage(getSceneStagePhotoUrl(selection.selectedScene, selection.selectedSceneAsset, { preferMobile }));
|
||||
typeof window !== "undefined"
|
||||
? window.matchMedia("(max-width: 767px)").matches
|
||||
: false;
|
||||
preloadAssetImage(
|
||||
getSceneStagePhotoUrl(
|
||||
selection.selectedScene,
|
||||
selection.selectedSceneAsset,
|
||||
{ preferMobile },
|
||||
),
|
||||
);
|
||||
}, [selection.selectedScene, selection.selectedSceneAsset]);
|
||||
|
||||
const resolvedTimeDisplay = timeDisplay ?? resolveFocusTimeDisplayFromTimerLabel(selection.selectedTimerLabel);
|
||||
const resolvedTimeDisplay =
|
||||
timeDisplay ??
|
||||
resolveFocusTimeDisplayFromTimerLabel(selection.selectedTimerLabel);
|
||||
|
||||
return (
|
||||
<div className="relative h-dvh overflow-hidden text-white">
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute -inset-8 bg-cover bg-center will-change-transform animate-[space-stage-pan_42s_ease-in-out_infinite_alternate] motion-reduce:animate-none"
|
||||
style={getSceneStageBackgroundStyle(selection.selectedScene, selection.selectedSceneAsset)}
|
||||
style={getSceneStageBackgroundStyle(
|
||||
selection.selectedScene,
|
||||
selection.selectedSceneAsset,
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex h-full flex-col">
|
||||
@@ -208,39 +251,47 @@ export const SpaceWorkspaceWidget = () => {
|
||||
timerPresets={TIMER_SELECTION_PRESETS}
|
||||
canStart={selection.canStart}
|
||||
onSceneSelect={selection.handleSelectScene}
|
||||
onTimerSelect={(timerLabel) => selection.handleSelectTimer(timerLabel, true)}
|
||||
onSoundSelect={(presetId) => selection.handleSelectSound(presetId, true)}
|
||||
onTimerSelect={(timerLabel) =>
|
||||
selection.handleSelectTimer(timerLabel, true)
|
||||
}
|
||||
onSoundSelect={(presetId) =>
|
||||
selection.handleSelectSound(presetId, true)
|
||||
}
|
||||
onGoalChange={selection.handleGoalChange}
|
||||
onGoalChipSelect={selection.handleGoalChipSelect}
|
||||
onStart={controls.handleSetupFocusOpen}
|
||||
resumeHint={
|
||||
selection.showResumePrompt && selection.resumeGoal
|
||||
? {
|
||||
goal: selection.resumeGoal,
|
||||
onResume: () => {
|
||||
selection.setGoalInput(selection.resumeGoal);
|
||||
selection.setSelectedGoalId(null);
|
||||
selection.setShowResumePrompt(false);
|
||||
controls.openFocusMode(selection.resumeGoal, 'resume-restore');
|
||||
},
|
||||
onStartFresh: () => {
|
||||
selection.setGoalInput('');
|
||||
selection.setSelectedGoalId(null);
|
||||
selection.setShowResumePrompt(false);
|
||||
},
|
||||
}
|
||||
goal: selection.resumeGoal,
|
||||
onResume: () => {
|
||||
selection.setGoalInput(selection.resumeGoal);
|
||||
selection.setSelectedGoalId(null);
|
||||
selection.setShowResumePrompt(false);
|
||||
controls.openFocusMode(
|
||||
selection.resumeGoal,
|
||||
"resume-restore",
|
||||
);
|
||||
},
|
||||
onStartFresh: () => {
|
||||
selection.setGoalInput("");
|
||||
selection.setSelectedGoalId(null);
|
||||
selection.setShowResumePrompt(false);
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<SpaceFocusHudWidget
|
||||
goal={selection.goalInput.trim()}
|
||||
microStep={currentSession?.microStep ?? null}
|
||||
timerLabel={selection.selectedTimerLabel}
|
||||
timeDisplay={resolvedTimeDisplay}
|
||||
visible={isFocusMode}
|
||||
hasActiveSession={Boolean(currentSession)}
|
||||
playbackState={resolvedPlaybackState}
|
||||
sessionPhase={phase ?? 'focus'}
|
||||
sessionPhase={phase ?? "focus"}
|
||||
isSessionActionPending={isSessionMutating}
|
||||
canStartSession={controls.canStartSession}
|
||||
canPauseSession={controls.canPauseSession}
|
||||
@@ -260,7 +311,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
|
||||
<FocusTopToast
|
||||
visible={isFocusMode && Boolean(activeStatus)}
|
||||
message={activeStatus?.message ?? ''}
|
||||
message={activeStatus?.message ?? ""}
|
||||
actionLabel={activeStatus?.action?.label}
|
||||
onAction={runActiveAction}
|
||||
/>
|
||||
@@ -276,15 +327,25 @@ export const SpaceWorkspaceWidget = () => {
|
||||
thoughtCount={thoughtCount}
|
||||
selectedPresetId={selectedPresetId}
|
||||
onSceneSelect={selection.handleSelectScene}
|
||||
onTimerSelect={(timerLabel) => selection.handleSelectTimer(timerLabel, true)}
|
||||
onQuickSoundSelect={(presetId) => selection.handleSelectSound(presetId, true)}
|
||||
onTimerSelect={(timerLabel) =>
|
||||
selection.handleSelectTimer(timerLabel, true)
|
||||
}
|
||||
onQuickSoundSelect={(presetId) =>
|
||||
selection.handleSelectSound(presetId, true)
|
||||
}
|
||||
sceneRecommendedSoundLabel={selection.selectedScene.recommendedSound}
|
||||
sceneRecommendedTimerLabel={resolveTimerLabelFromPresetId(selection.selectedScene.recommendedTimerPresetId) ?? selection.selectedTimerLabel}
|
||||
sceneRecommendedTimerLabel={
|
||||
resolveTimerLabelFromPresetId(
|
||||
selection.selectedScene.recommendedTimerPresetId,
|
||||
) ?? selection.selectedTimerLabel
|
||||
}
|
||||
soundVolume={masterVolume}
|
||||
onSetSoundVolume={setMasterVolume}
|
||||
isSoundMuted={isMuted}
|
||||
onSetSoundMuted={setMuted}
|
||||
onCaptureThought={(note) => addThought(note, selection.selectedScene.name)}
|
||||
onCaptureThought={(note) =>
|
||||
addThought(note, selection.selectedScene.name)
|
||||
}
|
||||
onDeleteThought={removeThought}
|
||||
onSetThoughtCompleted={setThoughtCompleted}
|
||||
onRestoreThought={restoreThought}
|
||||
|
||||
Reference in New Issue
Block a user