feat(sound): 우하단 빠른 볼륨/3프리셋 및 더미 저장 복원 적용

This commit is contained in:
2026-03-04 15:20:56 +09:00
parent 8f64637b6f
commit 27a64d9d81
7 changed files with 368 additions and 112 deletions

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { SOUND_PRESETS } from '@/entities/session'; import { SOUND_PRESETS } from '@/entities/session';
const TRACK_KEYS = ['white', 'rain', 'cafe', 'wave', 'fan'] as const; const TRACK_KEYS = ['white', 'rain', 'cafe', 'wave', 'fan'] as const;
@@ -16,24 +16,93 @@ const DEFAULT_TRACK_LEVELS: Record<SoundTrackKey, number> = {
}; };
const DEFAULT_MASTER_VOLUME = 68; const DEFAULT_MASTER_VOLUME = 68;
const SOUND_PREF_STORAGE_KEY = 'viberoom:sound-pref:v1';
const clampVolume = (value: number) => {
return Math.min(100, Math.max(0, value));
};
interface StoredSoundPref {
selectedPresetId?: string;
masterVolume?: number;
isMuted?: boolean;
}
const readStoredSoundPref = (): StoredSoundPref => {
if (typeof window === 'undefined') {
return {};
}
const raw = window.localStorage.getItem(SOUND_PREF_STORAGE_KEY);
if (!raw) {
return {};
}
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') {
return {};
}
return parsed as StoredSoundPref;
} catch {
return {};
}
};
export const useSoundPresetSelection = (initialPresetId?: string) => { export const useSoundPresetSelection = (initialPresetId?: string) => {
const storedPref = useMemo(() => readStoredSoundPref(), []);
const safeInitialPresetId = useMemo(() => { const safeInitialPresetId = useMemo(() => {
const hasPreset = SOUND_PRESETS.some((preset) => preset.id === initialPresetId); const candidates = [initialPresetId, storedPref.selectedPresetId];
return hasPreset && initialPresetId ? initialPresetId : SOUND_PRESETS[0].id;
}, [initialPresetId]); for (const candidate of candidates) {
if (!candidate) {
continue;
}
if (SOUND_PRESETS.some((preset) => preset.id === candidate)) {
return candidate;
}
}
return SOUND_PRESETS[0].id;
}, [initialPresetId, storedPref.selectedPresetId]);
const [selectedPresetId, setSelectedPresetId] = useState(safeInitialPresetId); const [selectedPresetId, setSelectedPresetId] = useState(safeInitialPresetId);
const [isMixerOpen, setMixerOpen] = useState(false); const [isMixerOpen, setMixerOpen] = useState(false);
const [isMuted, setMuted] = useState(false); const [isMuted, setMuted] = useState(Boolean(storedPref.isMuted));
const [masterVolume, setMasterVolume] = useState(DEFAULT_MASTER_VOLUME); const [masterVolume, setMasterVolume] = useState(
clampVolume(storedPref.masterVolume ?? DEFAULT_MASTER_VOLUME),
);
const [trackLevels, setTrackLevels] = const [trackLevels, setTrackLevels] =
useState<Record<SoundTrackKey, number>>(DEFAULT_TRACK_LEVELS); useState<Record<SoundTrackKey, number>>(DEFAULT_TRACK_LEVELS);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
window.localStorage.setItem(
SOUND_PREF_STORAGE_KEY,
JSON.stringify({
selectedPresetId,
masterVolume: clampVolume(masterVolume),
isMuted,
}),
);
}, [selectedPresetId, masterVolume, isMuted]);
const setTrackLevel = (track: SoundTrackKey, level: number) => { const setTrackLevel = (track: SoundTrackKey, level: number) => {
setTrackLevels((current) => ({ ...current, [track]: level })); setTrackLevels((current) => ({ ...current, [track]: level }));
}; };
const setMasterVolumeSafe = (nextVolume: number) => {
setMasterVolume(clampVolume(nextVolume));
};
return { return {
selectedPresetId, selectedPresetId,
setSelectedPresetId, setSelectedPresetId,
@@ -42,7 +111,7 @@ export const useSoundPresetSelection = (initialPresetId?: string) => {
isMuted, isMuted,
setMuted, setMuted,
masterVolume, masterVolume,
setMasterVolume, setMasterVolume: setMasterVolumeSafe,
trackLevels, trackLevels,
setTrackLevel, setTrackLevel,
trackKeys: TRACK_KEYS, trackKeys: TRACK_KEYS,

View File

@@ -11,10 +11,8 @@ const isQuickAvailablePreset = (preset: SoundPreset | undefined): preset is Soun
return !PRO_LOCKED_SOUND_IDS.includes(preset.id); return !PRO_LOCKED_SOUND_IDS.includes(preset.id);
}; };
export const getQuickSoundPresets = (presets: SoundPreset[], selectedPresetId: string) => { export const getQuickSoundPresets = (presets: SoundPreset[]) => {
const uniquePresetIds = Array.from(new Set([selectedPresetId, ...QUICK_SOUND_FALLBACK_IDS])); return QUICK_SOUND_FALLBACK_IDS
return uniquePresetIds
.map((presetId) => presets.find((preset) => preset.id === presetId)) .map((presetId) => presets.find((preset) => preset.id === presetId))
.filter(isQuickAvailablePreset) .filter(isQuickAvailablePreset)
.slice(0, 3); .slice(0, 3);

View File

@@ -0,0 +1,53 @@
import { cn } from '@/shared/lib/cn';
import { formatThoughtCount, RAIL_ICON } from './constants';
interface FocusRightRailProps {
isIdle: boolean;
thoughtCount: number;
onOpenInbox: () => void;
onOpenControlCenter: () => void;
}
export const FocusRightRail = ({
isIdle,
thoughtCount,
onOpenInbox,
onOpenControlCenter,
}: FocusRightRailProps) => {
return (
<div
className={cn(
'fixed z-30 transition-opacity right-[calc(env(safe-area-inset-right,0px)+0.75rem)] top-1/2 -translate-y-1/2',
isIdle ? 'opacity-34' : 'opacity-78',
)}
>
<div className="rounded-2xl border border-white/14 bg-black/22 p-1.5 backdrop-blur-md">
<div className="flex flex-col gap-1">
<button
type="button"
aria-label="인박스 열기"
title="인박스"
onClick={onOpenInbox}
className="relative inline-flex h-8 w-8 items-center justify-center rounded-xl border border-white/12 bg-white/[0.03] text-white/82 transition-colors hover:bg-white/10"
>
{RAIL_ICON.inbox}
{thoughtCount > 0 ? (
<span className="absolute -right-1 -top-1 inline-flex min-w-[0.95rem] items-center justify-center rounded-full bg-sky-200/28 px-1 py-0.5 text-[8px] font-semibold text-sky-50">
{formatThoughtCount(thoughtCount)}
</span>
) : null}
</button>
<button
type="button"
aria-label="Quick Controls 열기"
title="Quick Controls"
onClick={onOpenControlCenter}
className="inline-flex h-8 w-8 items-center justify-center rounded-xl border border-white/12 bg-white/[0.03] text-white/82 transition-colors hover:bg-white/10"
>
{RAIL_ICON.controlCenter}
</button>
</div>
</div>
</div>
);
};

View File

@@ -1,5 +1,5 @@
'use client'; 'use client';
import { useEffect, useMemo, useState, type KeyboardEvent as ReactKeyboardEvent } from 'react'; import { useEffect, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from 'react';
import type { PlanTier } from '@/entities/plan'; import type { PlanTier } from '@/entities/plan';
import type { RoomTheme } from '@/entities/room'; import type { RoomTheme } from '@/entities/room';
import { SOUND_PRESETS, type RecentThought, type TimerPreset } from '@/entities/session'; import { SOUND_PRESETS, type RecentThought, type TimerPreset } from '@/entities/session';
@@ -13,7 +13,10 @@ import { SpaceSideSheet } from '@/widgets/space-sheet-shell';
import type { SpaceAnchorPopoverId, SpaceUtilityPanelId } from '../model/types'; import type { SpaceAnchorPopoverId, SpaceUtilityPanelId } from '../model/types';
import { applyQuickPack } from '../model/applyQuickPack'; import { applyQuickPack } from '../model/applyQuickPack';
import { getQuickSoundPresets } from '../model/getQuickSoundPresets'; import { getQuickSoundPresets } from '../model/getQuickSoundPresets';
import { ANCHOR_ICON, formatThoughtCount, RAIL_ICON, UTILITY_PANEL_TITLE } from './constants'; import { ANCHOR_ICON, formatThoughtCount, UTILITY_PANEL_TITLE } from './constants';
import { FocusRightRail } from './FocusRightRail';
import { QuickNotesPopover } from './popovers/QuickNotesPopover';
import { QuickSoundPopover } from './popovers/QuickSoundPopover';
import { InboxToolPanel } from './panels/InboxToolPanel'; import { InboxToolPanel } from './panels/InboxToolPanel';
interface SpaceToolsDockWidgetProps { interface SpaceToolsDockWidgetProps {
isFocusMode: boolean; isFocusMode: boolean;
@@ -22,6 +25,10 @@ interface SpaceToolsDockWidgetProps {
selectedTimerLabel: string; selectedTimerLabel: string;
timerPresets: TimerPreset[]; timerPresets: TimerPreset[];
selectedPresetId: string; selectedPresetId: string;
soundVolume: number;
onSetSoundVolume: (volume: number) => void;
isSoundMuted: boolean;
onSetSoundMuted: (nextMuted: boolean) => void;
thoughts: RecentThought[]; thoughts: RecentThought[];
thoughtCount: number; thoughtCount: number;
onRoomSelect: (roomId: string) => void; onRoomSelect: (roomId: string) => void;
@@ -43,6 +50,10 @@ export const SpaceToolsDockWidget = ({
selectedTimerLabel, selectedTimerLabel,
timerPresets, timerPresets,
selectedPresetId, selectedPresetId,
soundVolume,
onSetSoundVolume,
isSoundMuted,
onSetSoundMuted,
thoughts, thoughts,
thoughtCount, thoughtCount,
onRoomSelect, onRoomSelect,
@@ -60,8 +71,10 @@ export const SpaceToolsDockWidget = ({
const [openPopover, setOpenPopover] = useState<SpaceAnchorPopoverId | null>(null); const [openPopover, setOpenPopover] = useState<SpaceAnchorPopoverId | null>(null);
const [utilityPanel, setUtilityPanel] = useState<SpaceUtilityPanelId | null>(null); const [utilityPanel, setUtilityPanel] = useState<SpaceUtilityPanelId | null>(null);
const [noteDraft, setNoteDraft] = useState(''); const [noteDraft, setNoteDraft] = useState('');
const [volumeFeedback, setVolumeFeedback] = useState<string | null>(null);
const [plan, setPlan] = useState<PlanTier>('normal'); const [plan, setPlan] = useState<PlanTier>('normal');
const [isIdle, setIdle] = useState(false); const [isIdle, setIdle] = useState(false);
const volumeFeedbackTimerRef = useRef<number | null>(null);
const selectedSoundLabel = useMemo(() => { const selectedSoundLabel = useMemo(() => {
return ( return (
@@ -70,8 +83,17 @@ export const SpaceToolsDockWidget = ({
}, [selectedPresetId]); }, [selectedPresetId]);
const quickSoundPresets = useMemo(() => { const quickSoundPresets = useMemo(() => {
return getQuickSoundPresets(SOUND_PRESETS, selectedPresetId); return getQuickSoundPresets(SOUND_PRESETS);
}, [selectedPresetId]); }, []);
useEffect(() => {
return () => {
if (volumeFeedbackTimerRef.current) {
window.clearTimeout(volumeFeedbackTimerRef.current);
volumeFeedbackTimerRef.current = null;
}
};
}, []);
useEffect(() => { useEffect(() => {
if (!openPopover) { if (!openPopover) {
@@ -167,15 +189,6 @@ export const SpaceToolsDockWidget = ({
}); });
}; };
const handleNoteKeyDown = (event: ReactKeyboardEvent<HTMLInputElement>) => {
if (event.key !== 'Enter') {
return;
}
event.preventDefault();
handleNoteSubmit();
};
const handleInboxComplete = (thought: RecentThought) => { const handleInboxComplete = (thought: RecentThought) => {
const previousThought = onSetThoughtCompleted(thought.id, !thought.isCompleted); const previousThought = onSetThoughtCompleted(thought.id, !thought.isCompleted);
@@ -262,6 +275,42 @@ export const SpaceToolsDockWidget = ({
const handleApplyPack = (packId: 'balanced' | 'deep-work' | 'gentle') => const handleApplyPack = (packId: 'balanced' | 'deep-work' | 'gentle') =>
applyQuickPack({ packId, onTimerSelect, onSelectPreset, pushToast }); applyQuickPack({ packId, onTimerSelect, onSelectPreset, pushToast });
const showVolumeFeedback = (nextVolume: number) => {
setVolumeFeedback(`${nextVolume}%`);
if (volumeFeedbackTimerRef.current) {
window.clearTimeout(volumeFeedbackTimerRef.current);
}
volumeFeedbackTimerRef.current = window.setTimeout(() => {
setVolumeFeedback(null);
volumeFeedbackTimerRef.current = null;
}, 900);
};
const handleVolumeChange = (nextVolume: number) => {
const clamped = Math.min(100, Math.max(0, nextVolume));
onSetSoundVolume(clamped);
if (isSoundMuted && clamped > 0) {
onSetSoundMuted(false);
}
showVolumeFeedback(clamped);
};
const handleVolumeKeyDown = (event: ReactKeyboardEvent<HTMLInputElement>) => {
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
return;
}
event.preventDefault();
const step = event.shiftKey ? 10 : 5;
const delta = event.key === 'ArrowRight' ? step : -step;
handleVolumeChange(soundVolume + delta);
};
return ( return (
<> <>
{openPopover ? ( {openPopover ? (
@@ -287,40 +336,12 @@ export const SpaceToolsDockWidget = ({
{isFocusMode ? ( {isFocusMode ? (
<> <>
<div <FocusRightRail
className={cn( isIdle={isIdle}
'fixed z-30 transition-opacity right-[calc(env(safe-area-inset-right,0px)+0.75rem)] top-1/2 -translate-y-1/2', thoughtCount={thoughtCount}
isIdle ? 'opacity-34' : 'opacity-78', onOpenInbox={() => openUtilityPanel('inbox')}
)} onOpenControlCenter={() => openUtilityPanel('control-center')}
> />
<div className="rounded-2xl border border-white/14 bg-black/22 p-1.5 backdrop-blur-md">
<div className="flex flex-col gap-1">
<button
type="button"
aria-label="인박스 열기"
title="인박스"
onClick={() => openUtilityPanel('inbox')}
className="relative inline-flex h-8 w-8 items-center justify-center rounded-xl border border-white/12 bg-white/[0.03] text-white/82 transition-colors hover:bg-white/10"
>
{RAIL_ICON.inbox}
{thoughtCount > 0 ? (
<span className="absolute -right-1 -top-1 inline-flex min-w-[0.95rem] items-center justify-center rounded-full bg-sky-200/28 px-1 py-0.5 text-[8px] font-semibold text-sky-50">
{formatThoughtCount(thoughtCount)}
</span>
) : null}
</button>
<button
type="button"
aria-label="Quick Controls 열기"
title="Quick Controls"
onClick={() => openUtilityPanel('control-center')}
className="inline-flex h-8 w-8 items-center justify-center rounded-xl border border-white/12 bg-white/[0.03] text-white/82 transition-colors hover:bg-white/10"
>
{RAIL_ICON.controlCenter}
</button>
</div>
</div>
</div>
<div <div
className={cn( className={cn(
@@ -344,29 +365,12 @@ export const SpaceToolsDockWidget = ({
</button> </button>
{openPopover === 'notes' ? ( {openPopover === 'notes' ? (
<div <QuickNotesPopover
className="mb-2 w-[min(320px,calc(100vw-2rem))] rounded-2xl border border-white/14 bg-slate-950/74 p-3 shadow-[0_18px_44px_rgba(2,6,23,0.4)] backdrop-blur-xl animate-[popover-rise_220ms_ease-out] motion-reduce:animate-none" noteDraft={noteDraft}
style={{ position: 'absolute', bottom: 'calc(100% + 0.5rem)', left: 0 }} onDraftChange={setNoteDraft}
> onDraftEnter={handleNoteSubmit}
<p className="text-[11px] text-white/56"> </p> onSubmit={handleNoteSubmit}
<div className="mt-2 flex gap-1.5"> />
<input
value={noteDraft}
onChange={(event) => setNoteDraft(event.target.value)}
onKeyDown={handleNoteKeyDown}
placeholder="떠오른 생각을 잠깐 주차…"
className="h-8 min-w-0 flex-1 rounded-lg border border-white/14 bg-white/[0.04] px-2.5 text-xs text-white placeholder:text-white/38 focus:border-sky-200/42 focus:outline-none"
/>
<button
type="button"
onClick={handleNoteSubmit}
className="h-8 rounded-lg border border-sky-200/34 bg-sky-200/14 px-2.5 text-xs text-white/88"
>
</button>
</div>
<p className="mt-2 text-[11px] text-white/52"> .</p>
</div>
) : null} ) : null}
</div> </div>
</div> </div>
@@ -393,37 +397,26 @@ export const SpaceToolsDockWidget = ({
</button> </button>
{openPopover === 'sound' ? ( {openPopover === 'sound' ? (
<div <QuickSoundPopover
className="mb-2 w-[min(288px,calc(100vw-2rem))] rounded-2xl border border-white/14 bg-slate-950/74 p-3 shadow-[0_18px_44px_rgba(2,6,23,0.4)] backdrop-blur-xl animate-[popover-rise_220ms_ease-out] motion-reduce:animate-none" selectedSoundLabel={selectedSoundLabel}
style={{ position: 'absolute', bottom: 'calc(100% + 0.5rem)', right: 0 }} isSoundMuted={isSoundMuted}
> soundVolume={soundVolume}
<p className="text-[11px] text-white/56"> </p> volumeFeedback={volumeFeedback}
<div className="mt-2 flex flex-wrap gap-1.5"> quickSoundPresets={quickSoundPresets}
{quickSoundPresets.map((preset) => { selectedPresetId={selectedPresetId}
const selected = preset.id === selectedPresetId; onToggleMute={() => {
const nextMuted = !isSoundMuted;
return ( onSetSoundMuted(nextMuted);
<button showVolumeFeedback(nextMuted ? 0 : soundVolume);
key={preset.id} }}
type="button" onVolumeChange={handleVolumeChange}
onClick={() => { onVolumeKeyDown={handleVolumeKeyDown}
onSelectPreset(preset.id); onSelectPreset={(presetId) => {
pushToast({ title: `${preset.label}로 전환했어요.` }); onSelectPreset(presetId);
setOpenPopover(null); pushToast({ title: '사운드 변경(더미)' });
}} setOpenPopover(null);
className={cn( }}
'rounded-full border px-2.5 py-0.5 text-[10px] transition-colors', />
selected
? 'border-sky-200/34 bg-sky-200/14 text-white/90'
: 'border-white/12 bg-white/[0.03] text-white/66 hover:bg-white/8',
)}
>
{preset.label}
</button>
);
})}
</div>
</div>
) : null} ) : null}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,46 @@
interface QuickNotesPopoverProps {
noteDraft: string;
onDraftChange: (value: string) => void;
onDraftEnter: () => void;
onSubmit: () => void;
}
export const QuickNotesPopover = ({
noteDraft,
onDraftChange,
onDraftEnter,
onSubmit,
}: QuickNotesPopoverProps) => {
return (
<div
className="mb-2 w-[min(320px,calc(100vw-2rem))] rounded-2xl border border-white/14 bg-slate-950/74 p-3 shadow-[0_18px_44px_rgba(2,6,23,0.4)] backdrop-blur-xl animate-[popover-rise_220ms_ease-out] motion-reduce:animate-none"
style={{ position: 'absolute', bottom: 'calc(100% + 0.5rem)', left: 0 }}
>
<p className="text-[11px] text-white/56"> </p>
<div className="mt-2 flex gap-1.5">
<input
value={noteDraft}
onChange={(event) => onDraftChange(event.target.value)}
onKeyDown={(event) => {
if (event.key !== 'Enter') {
return;
}
event.preventDefault();
onDraftEnter();
}}
placeholder="떠오른 생각을 잠깐 주차…"
className="h-8 min-w-0 flex-1 rounded-lg border border-white/14 bg-white/[0.04] px-2.5 text-xs text-white placeholder:text-white/38 focus:border-sky-200/42 focus:outline-none"
/>
<button
type="button"
onClick={onSubmit}
className="h-8 rounded-lg border border-sky-200/34 bg-sky-200/14 px-2.5 text-xs text-white/88"
>
</button>
</div>
<p className="mt-2 text-[11px] text-white/52"> .</p>
</div>
);
};

View File

@@ -0,0 +1,89 @@
import { cn } from '@/shared/lib/cn';
import type { SoundPreset } from '@/entities/session';
import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
interface QuickSoundPopoverProps {
selectedSoundLabel: string;
isSoundMuted: boolean;
soundVolume: number;
volumeFeedback: string | null;
quickSoundPresets: SoundPreset[];
selectedPresetId: string;
onToggleMute: () => void;
onVolumeChange: (nextVolume: number) => void;
onVolumeKeyDown: (event: ReactKeyboardEvent<HTMLInputElement>) => void;
onSelectPreset: (presetId: string) => void;
}
export const QuickSoundPopover = ({
selectedSoundLabel,
isSoundMuted,
soundVolume,
volumeFeedback,
quickSoundPresets,
selectedPresetId,
onToggleMute,
onVolumeChange,
onVolumeKeyDown,
onSelectPreset,
}: QuickSoundPopoverProps) => {
return (
<div
className="mb-2 w-[min(288px,calc(100vw-2rem))] rounded-2xl border border-white/14 bg-slate-950/74 p-3 shadow-[0_18px_44px_rgba(2,6,23,0.4)] backdrop-blur-xl animate-[popover-rise_220ms_ease-out] motion-reduce:animate-none"
style={{ position: 'absolute', bottom: 'calc(100% + 0.5rem)', right: 0 }}
>
<p className="text-[11px] text-white/56"> </p>
<p className="mt-1 truncate text-sm font-medium text-white/88">{selectedSoundLabel}</p>
<div className="mt-3 rounded-xl border border-white/14 bg-white/[0.04] px-2.5 py-2">
<div className="flex items-center gap-2">
<button
type="button"
aria-label={isSoundMuted ? '음소거 해제' : '음소거'}
onClick={onToggleMute}
className="inline-flex h-7 w-7 items-center justify-center rounded-full border border-white/16 bg-white/[0.05] text-xs text-white/80 transition-colors hover:bg-white/[0.12]"
>
🔇
</button>
<input
type="range"
min={0}
max={100}
step={1}
value={soundVolume}
onChange={(event) => onVolumeChange(Number(event.target.value))}
onKeyDown={onVolumeKeyDown}
aria-label="사운드 볼륨"
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-white/18 accent-sky-200"
/>
<span className="w-9 text-right text-[11px] text-white/66">
{volumeFeedback ?? (isSoundMuted ? '0%' : `${soundVolume}%`)}
</span>
</div>
</div>
<p className="mt-3 text-[11px] text-white/56"> </p>
<div className="mt-2 flex flex-wrap gap-1.5">
{quickSoundPresets.map((preset) => {
const selected = preset.id === selectedPresetId;
return (
<button
key={preset.id}
type="button"
onClick={() => onSelectPreset(preset.id)}
className={cn(
'rounded-full border px-2.5 py-0.5 text-[10px] transition-colors',
selected
? 'border-sky-200/34 bg-sky-200/14 text-white/90'
: 'border-white/12 bg-white/[0.03] text-white/66 hover:bg-white/8',
)}
>
{preset.label}
</button>
);
})}
</div>
</div>
);
};

View File

@@ -80,6 +80,10 @@ export const SpaceWorkspaceWidget = () => {
const { const {
selectedPresetId, selectedPresetId,
setSelectedPresetId, setSelectedPresetId,
masterVolume,
setMasterVolume,
isMuted,
setMuted,
} = useSoundPresetSelection(initialSoundPresetId); } = useSoundPresetSelection(initialSoundPresetId);
const selectedRoom = useMemo(() => { const selectedRoom = useMemo(() => {
@@ -212,6 +216,10 @@ export const SpaceWorkspaceWidget = () => {
onRoomSelect={setSelectedRoomId} onRoomSelect={setSelectedRoomId}
onTimerSelect={setSelectedTimerLabel} onTimerSelect={setSelectedTimerLabel}
onSelectPreset={setSelectedPresetId} onSelectPreset={setSelectedPresetId}
soundVolume={masterVolume}
onSetSoundVolume={setMasterVolume}
isSoundMuted={isMuted}
onSetSoundMuted={setMuted}
onCaptureThought={(note) => addThought(note, selectedRoom.name)} onCaptureThought={(note) => addThought(note, selectedRoom.name)}
onDeleteThought={removeThought} onDeleteThought={removeThought}
onSetThoughtCompleted={setThoughtCompleted} onSetThoughtCompleted={setThoughtCompleted}