export type SidebarViewport = "mobile" | "tablet" | "desktop"; type EditableTarget = { readonly tagName?: unknown; readonly isContentEditable?: unknown; }; export type SidebarShortcutEvent = Pick< KeyboardEvent, "key" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey" > & { readonly target: EventTarget | EditableTarget | null; }; function isEditableTarget(target: SidebarShortcutEvent["target"]): target is EditableTarget { return target !== null && typeof target === "object"; } export function sidebarViewportForWidth(width: number): SidebarViewport { if (width < 768) return "mobile"; if (width < 1024) return "tablet"; return "desktop"; } export function defaultSidebarOpen(viewport: SidebarViewport): boolean { return viewport === "desktop"; } export function shouldHandleSidebarShortcut(event: SidebarShortcutEvent): boolean { const target = event.target; const editable = isEditableTarget(target) && (target.isContentEditable === true || /^(INPUT|TEXTAREA|SELECT)$/.test(String(target.tagName ?? ""))); return !editable && event.key.toLowerCase() === "b" && (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey; } /** * Where the desktop/tablet collapse state is remembered across pages. * * localStorage, not the shadcn `sidebar_state` cookie: `/` is a `○ Static` * route and `/chart` and `/ephemeris` are static too, so reading a cookie on * the server would opt all three out of static rendering. A cookie that only * the client may read buys nothing a localStorage key does not, so this is the * cheaper half of the D5 choice. The mobile drawer is deliberately excluded: * reopening a phone drawer on every navigation is not a preference anyone set. */ export const SIDEBAR_STATE_STORAGE_KEY = "sidebar_state"; type SidebarStateStorage = Pick; function sidebarStateStorage(): SidebarStateStorage | null { try { return globalThis.localStorage ?? null; } catch { /* Private mode and blocked site data both throw on access. */ return null; } } /** `null` when nothing was stored, or when storage is unavailable. */ export function readStoredSidebarOpen( viewport: SidebarViewport, storage: SidebarStateStorage | null = sidebarStateStorage(), ): boolean | null { if (viewport === "mobile" || storage === null) return null; try { const raw = storage.getItem(SIDEBAR_STATE_STORAGE_KEY); if (raw === "true") return true; if (raw === "false") return false; return null; } catch { return null; } } export function writeStoredSidebarOpen( viewport: SidebarViewport, open: boolean, storage: SidebarStateStorage | null = sidebarStateStorage(), ): void { if (viewport === "mobile" || storage === null) return; try { storage.setItem(SIDEBAR_STATE_STORAGE_KEY, open ? "true" : "false"); } catch { /* Quota and private mode: the sidebar still works, it just forgets. */ } }