// src/layout/layout-state.ts /** * Editor display mode. */ export type EditorMode = 'source' | 'live-preview' /** * Whether the given editor mode is read-only. * Read mode was removed; all supported modes are editable. */ export function isReadOnly(_mode: EditorMode): boolean { return false } /** * Sidebar display mode. * - 'hidden': Neither ribbon nor sidebar visible * - 'ribbon': Only ribbon icon strip visible * - 'full': Both ribbon and full sidebar visible */ export type SidebarMode = 'hidden' | 'ribbon' | 'full' /** State machine for sidebar mode toggle: hidden -> ribbon -> full -> hidden */ const SIDEBAR_MODE_CYCLE: Record = { hidden: 'ribbon', ribbon: 'full', full: 'hidden', } /** * Layout state for the app shell. * Sidebar/right-panel chrome persists per device; editor mode comes from * the vault preference when mounted by the application host. * * Tab state is owned by the workspace (see Workspace.serializeLayout / * restoreLayout and `WORKSPACE_TABS_STORAGE_KEY`); LayoutState only owns * sidebar/right-panel chrome and the current editor-mode projection. */ export interface LayoutState { sidebarWidth: number sidebarMode: SidebarMode rightPanelWidth: number rightPanelCollapsed: boolean editorMode: EditorMode } /** Default sidebar width in pixels. */ export const DEFAULT_SIDEBAR_WIDTH = 240 /** Minimum sidebar width in pixels. */ export const MIN_SIDEBAR_WIDTH = 160 /** Maximum sidebar width in pixels. */ export const MAX_SIDEBAR_WIDTH = 400 /** Ribbon width in pixels. Must match `--ribbon-width` in src/layout/Ribbon.css. * Consumed by AppShell.tsx for toolbar offset math (#213). */ export const RIBBON_WIDTH = 44 /** Default right panel width in pixels. */ export const DEFAULT_RIGHT_PANEL_WIDTH = 280 /** Minimum right panel width in pixels. */ export const MIN_RIGHT_PANEL_WIDTH = 200 /** Maximum right panel width in pixels. */ export const MAX_RIGHT_PANEL_WIDTH = 500 /** * Compute the left offset (in pixels) for fixed-position elements that * need to clear the ribbon and sidebar on desktop. Used by the * FormattingToolbar via the --toolbar-left-offset CSS variable. #213 * * - sidebarMode === 'hidden': ribbon is hidden, no offset. * - sidebarMode === 'ribbon': ribbon visible, sidebar hidden → ribbon width. * - sidebarMode === 'full': ribbon + sidebar both visible → ribbon + sidebar widths. */ export function computeToolbarLeftOffset( sidebarMode: SidebarMode, sidebarWidth: number, ): number { if (sidebarMode === 'hidden') return 0 if (sidebarMode === 'ribbon') return RIBBON_WIDTH return RIBBON_WIDTH + sidebarWidth } /** * Compute the right offset (in pixels) for fixed-position elements that * need to clear the right panel on desktop. Used by the * FormattingToolbar via the --toolbar-right-offset CSS variable. #213 */ export function computeToolbarRightOffset( rightPanelCollapsed: boolean, rightPanelWidth: number, ): number { return rightPanelCollapsed ? 0 : rightPanelWidth } /** Mobile breakpoint in pixels. Below this width, sidebars overlay the editor. */ export const MOBILE_BREAKPOINT = 768 /** * Create initial layout state with sensible defaults. */ export function createInitialLayoutState(viewportWidth = typeof window === 'undefined' ? MOBILE_BREAKPOINT : window.innerWidth): LayoutState { return { sidebarWidth: DEFAULT_SIDEBAR_WIDTH, sidebarMode: viewportWidth < MOBILE_BREAKPOINT ? 'ribbon' : 'full', rightPanelWidth: DEFAULT_RIGHT_PANEL_WIDTH, rightPanelCollapsed: true, // Right panel closed by default editorMode: 'live-preview', } } /** * Cycle sidebar mode: hidden -> ribbon -> full -> hidden */ export function toggleSidebar(state: LayoutState): LayoutState { return { ...state, sidebarMode: SIDEBAR_MODE_CYCLE[state.sidebarMode] } } /** Valid sidebar modes for runtime validation */ const VALID_SIDEBAR_MODES: readonly SidebarMode[] = ['hidden', 'ribbon', 'full'] /** * Set the sidebar mode directly. * Invalid modes are ignored (returns state unchanged). */ export function setSidebarMode(state: LayoutState, mode: SidebarMode): LayoutState { // Runtime guard: ignore invalid modes (can happen if called from JS or with type assertions) if (!VALID_SIDEBAR_MODES.includes(mode)) { return state } if (state.sidebarMode === mode) return state return { ...state, sidebarMode: mode } } /** * Toggle right panel collapsed state. */ export function toggleRightPanel(state: LayoutState): LayoutState { return { ...state, rightPanelCollapsed: !state.rightPanelCollapsed } } /** * Set right panel collapsed state directly. */ export function setRightPanelCollapsed(state: LayoutState, collapsed: boolean): LayoutState { if (state.rightPanelCollapsed === collapsed) return state return { ...state, rightPanelCollapsed: collapsed } } /** * Set sidebar width, clamped to min/max constraints. */ export function setSidebarWidth(state: LayoutState, width: number): LayoutState { const clamped = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, width)) return { ...state, sidebarWidth: clamped } } /** * Set right panel width, clamped to min/max constraints. */ export function setRightPanelWidth(state: LayoutState, width: number): LayoutState { const clamped = Math.max(MIN_RIGHT_PANEL_WIDTH, Math.min(MAX_RIGHT_PANEL_WIDTH, width)) return { ...state, rightPanelWidth: clamped } } /** * Set the editor display mode. */ export function setEditorMode(state: LayoutState, mode: EditorMode): LayoutState { return { ...state, editorMode: mode } }