diff --git a/src/App.tsx b/src/App.tsx index 1c8db33..d1e18fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,7 @@ import PreferencesDialog, { type PreferencesDialogRef, } from "./components/PreferencesDialog"; import LinkPickerDialog from "./components/LinkPickerDialog"; -import Sidebar, { type SidebarRef } from "./components/sidebar"; +import Sidebar, { clampSidebarWidth, type SidebarRef } from "./components/sidebar"; import Statusbar from "./components/statusbar"; import Toolbar from "./components/toolbar"; import WasmGuest from "./components/WasmGuest"; @@ -27,6 +27,11 @@ import { autoLogService, createAutoLogSessionDraft, } from "./logging/AutoLogService"; +import { + loadStoredValue, + saveStoredValue, + type LocalStorageSchema, +} from "./persistence"; import { usePreferences } from "./stores/preferencesStore"; import { useRoomStore } from "./stores/roomStore"; import { useConnectionStore } from "./stores/connectionStore"; @@ -36,6 +41,19 @@ import { ensurePushSubscription } from "./webpush"; const WINDOW_TITLE = "Mongoose Client"; const DOUBLE_PRESS_WINDOW_MS = 500; +// User-chosen sidebar width in px; absent until the user first resizes. +const sidebarWidthSchema: LocalStorageSchema = { + key: "sidebarWidth", + version: 1, + migrate: (data) => + typeof data === "number" && Number.isFinite(data) ? data : undefined, +}; + +function loadSidebarWidth(): number | null { + const stored = loadStoredValue(sidebarWidthSchema, null); + return stored === null ? null : clampSidebarWidth(stored); +} + function setWindowSubtitle(subtitle?: string) { document.title = subtitle ? `${WINDOW_TITLE} - ${subtitle}` : WINDOW_TITLE; } @@ -92,6 +110,18 @@ function App() { const [client, setClient] = useState(null); const [showSidebar, setShowSidebar] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [sidebarWidth, setSidebarWidth] = useState(loadSidebarWidth); + + // Persist the user-chosen sidebar width, debounced so drags don't + // write localStorage on every pointer move + useEffect(() => { + if (sidebarWidth === null) return; + const timer = window.setTimeout( + () => saveStoredValue(sidebarWidthSchema, sidebarWidth), + 250, + ); + return () => window.clearTimeout(timer); + }, [sidebarWidth]); const [hostState, setHostState] = useState({ roomId: null, guestCount: 0, @@ -110,8 +140,8 @@ function App() { const clientInitialized = useRef(false); const hapticsRuntimeRef = useRef(null); - const midiEnabled = usePreferences((state) => state.midi.enabled); - const hapticsEnabled = usePreferences((state) => state.haptics.enabled); + const midiEnabled = usePreferences((state) => state.midi.enabled); + const hapticsEnabled = usePreferences((state) => state.haptics.enabled); const connected = useConnectionStore((state) => state.connected); const sessionReady = useConnectionStore((state) => state.sessionReady); useFileTransferNotifications(client); @@ -335,7 +365,7 @@ function App() { }, [handleAppKeyDown]); useEffect(() => { - if (!midiEnabled) return; + if (!midiEnabled) return; let cancelled = false; import("./VirtualMidiService") @@ -356,7 +386,7 @@ function App() { return () => { cancelled = true; }; - }, [midiEnabled]); + }, [midiEnabled]); // Window subtitle tracks the current room from the room store. On disconnect // the client resets the store, which clears roomInfo and so clears the subtitle. @@ -373,8 +403,8 @@ function App() { }, [client, roomInfo]); useEffect(() => { - hapticsRuntimeRef.current?.setEnabled(hapticsEnabled); - }, [hapticsEnabled]); + hapticsRuntimeRef.current?.setEnabled(hapticsEnabled); + }, [hapticsEnabled]); const handleCommand = useCallback( (text: string) => { @@ -435,6 +465,11 @@ function App() { {client && (
setSidebarCollapsed(!sidebarCollapsed)} + width={sidebarWidth} + onWidthChange={setSidebarWidth} />