Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172// 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<SidebarMode, SidebarMode> = { 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 }}