// src/layout/layout-persistence.ts import { createInitialLayoutState, type LayoutState, type EditorMode, type SidebarMode, MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH, MIN_RIGHT_PANEL_WIDTH, MAX_RIGHT_PANEL_WIDTH, } from './layout-state' export const LAYOUT_STORAGE_KEY = 'reams:layout' export const WORKSPACE_TABS_STORAGE_KEY = 'reams:workspace-tabs' export const LAYOUT_VERSION = 5 interface PersistedLayoutState { version: number sidebarWidth?: number sidebarMode?: string rightPanelWidth?: number rightPanelCollapsed?: boolean editorMode?: string } /** * Save layout state to localStorage. * Silently fails if localStorage is unavailable. * * v5+ does not persist tab state — tabs are owned by the workspace and * persisted under WORKSPACE_TABS_STORAGE_KEY. */ export function saveLayoutState(state: LayoutState, options?: { persistEditorMode?: boolean }): void { try { const persisted: PersistedLayoutState = { version: LAYOUT_VERSION, sidebarWidth: state.sidebarWidth, sidebarMode: state.sidebarMode, rightPanelWidth: state.rightPanelWidth, rightPanelCollapsed: state.rightPanelCollapsed, ...(options?.persistEditorMode === false ? {} : { editorMode: state.editorMode }), } localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(persisted)) } catch { // localStorage may be unavailable (private browsing, storage full, etc.) // Silently fail — layout state is non-critical } } /** * Load layout state from localStorage. * Returns default state if localStorage is unavailable, empty, or invalid. * * Older layout schemas are ignored; the workspace owns tab state. */ export function loadLayoutState(): LayoutState { const defaults = createInitialLayoutState() try { const stored = localStorage.getItem(LAYOUT_STORAGE_KEY) if (!stored) { return defaults } const parsed: unknown = JSON.parse(stored) if (!isObject(parsed)) { return defaults } const persisted = parsed as unknown as PersistedLayoutState // Reject obsolete and unknown versions. if (persisted.version !== LAYOUT_VERSION) { return defaults } // Load v5 state. return { sidebarWidth: clampNumber(persisted.sidebarWidth, MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH, defaults.sidebarWidth), sidebarMode: validateSidebarMode(persisted.sidebarMode, defaults.sidebarMode), rightPanelWidth: clampNumber(persisted.rightPanelWidth, MIN_RIGHT_PANEL_WIDTH, MAX_RIGHT_PANEL_WIDTH, defaults.rightPanelWidth), rightPanelCollapsed: typeof persisted.rightPanelCollapsed === 'boolean' ? persisted.rightPanelCollapsed : defaults.rightPanelCollapsed, editorMode: validateEditorMode(persisted.editorMode), } } catch { // JSON parse error or localStorage access error return defaults } } function isObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function clampNumber( value: unknown, min: number, max: number, defaultValue: number ): number { if (typeof value !== 'number' || !Number.isFinite(value)) { return defaultValue } return Math.max(min, Math.min(max, value)) } function validateEditorMode(mode: unknown): EditorMode { if (mode === 'source' || mode === 'live-preview') { return mode } return 'live-preview' } function validateSidebarMode(mode: unknown, fallback: SidebarMode): SidebarMode { if (mode === 'hidden' || mode === 'ribbon' || mode === 'full') { return mode } return fallback } // ============================================================================ // Workspace tab persistence (v5+) // ============================================================================ export interface WorkspaceTabsState { tabs: Array<{ docUrl: string }> activeIndex: number } /** * Save workspace tab state to localStorage. * Silently fails if localStorage is unavailable. */ export function saveWorkspaceTabs(state: WorkspaceTabsState, storageKey = WORKSPACE_TABS_STORAGE_KEY): void { try { localStorage.setItem(storageKey, JSON.stringify(state)) } catch { // Silent fail } } /** * Load workspace tab state from localStorage. * Returns null if unavailable, empty, or invalid. */ export function loadWorkspaceTabs(storageKey = WORKSPACE_TABS_STORAGE_KEY): WorkspaceTabsState | null { try { const stored = localStorage.getItem(storageKey) if (!stored) return null const parsed = JSON.parse(stored) if (!Array.isArray(parsed.tabs)) return null // Validate shape — filter out invalid entries const validTabs = parsed.tabs.filter( (t: unknown) => typeof t === 'object' && t !== null && typeof (t as { docUrl?: unknown }).docUrl === 'string' ) return { tabs: validTabs, activeIndex: typeof parsed.activeIndex === 'number' ? parsed.activeIndex : 0, } } catch { return null } }