// How wide the rail is. // // The width is furniture rather than protocol: it belongs to the browser profile, like the theme, and // is remembered there (`radial:rail-width`). It lives here rather than in `ui.svelte.ts` because it is // the one piece of window state with rules — a floor, a ceiling, and a pane that must survive the // ceiling — and rules are worth testing without a browser. // // 246px is the width the design was drawn at (DESIGN.md, Layout), so that is where a profile that has // never touched the grip starts and where a double-click puts it back. The floor is where a nav item's // label stops being readable before its count; the ceiling is where the rail stops being a rail. /** The narrowest rail that still reads as a list of labels rather than a column of glyphs. */ export const RAIL_MIN = 190 /** The widest the rail is ever allowed to get, however much window there is. */ export const RAIL_MAX = 460 /** The comp's width: the default, and what a double-click on the grip restores. */ export const RAIL_DEFAULT = 246 /** * What the pane keeps, whatever was dragged or remembered. Roughly the width at which a goal row * still has somewhere to put its tail — below it the sheet stops being a sheet, and a rail that wide * has taken the thing the reader came for. */ export const PANE_MIN = 560 const KEY = 'radial:rail-width' /** * A width the layout will accept: inside the fixed range, and — when the space the two columns share * is known — never so wide that the pane is left with less than `PANE_MIN`. A shell too narrow to * honour both loses to the floor, because a rail below `RAIL_MIN` is unreadable while a squeezed pane * is merely tight. * * `viewport` is the width of `.app` rather than of the window: under `?frame` the shell is * `min(1220px, 100%)` inside a padded body, so the window is up to 52px wider than the thing this is * dividing. `RailResizer` measures the element; the pre-paint script in `app.html`, which has no * layout to measure, computes the same number from the stylesheet's own — `test/rail-bounds.test.mjs` * keeps the two copies honest. */ export function clampRailWidth(px: number, viewport?: number): number { const ceiling = viewport === undefined ? RAIL_MAX : Math.max(RAIL_MIN, Math.min(RAIL_MAX, viewport - PANE_MIN)) if (!Number.isFinite(px)) return RAIL_DEFAULT return Math.min(ceiling, Math.max(RAIL_MIN, Math.round(px))) } /** The rail's geometry, and whether a drag is in progress (the app shell dims its own cursor for it). */ export const rail = $state({ width: RAIL_DEFAULT, resizing: false }) /** A browser that refuses storage still resizes; it just forgets. Same bargain as the theme toggle. */ function storage(): Storage | undefined { try { return globalThis.localStorage ?? undefined } catch { return undefined } } /** * What this profile last chose. Not narrowed to the window it is being read into: what is stored is a * decision, and a session spent in a small window must not quietly rewrite it. Fitting the width to * the window that is actually there is the drawing's job, one `clampRailWidth` later. */ export function savedRailWidth(): number | undefined { const raw = storage()?.getItem(KEY) if (raw === null || raw === undefined) return undefined const px = Number(raw) return Number.isFinite(px) && px > 0 ? clampRailWidth(px) : undefined } /** Adopt the remembered width. Called at module load so the first paint is already the chosen one. */ export function restoreRailWidth(): void { const saved = savedRailWidth() if (saved !== undefined) rail.width = saved } /** Move the edge. Clamped here rather than at each call site, so no caller can set an illegal width. */ export function setRailWidth(px: number, viewport?: number): void { rail.width = clampRailWidth(px, viewport) } /** The keyboard's version of a drag: the arrow keys move the edge by a step. */ export function nudgeRailWidth(delta: number, viewport?: number): void { setRailWidth(rail.width + delta, viewport) } /** Back to the width the app was designed at — the double-click, and Enter on the grip. */ export function resetRailWidth(): void { rail.width = RAIL_DEFAULT saveRailWidth() } /** * Remember the width. Only at the end of a gesture: a drag is a few hundred `pointermove`s, and none * of the intermediate widths is a decision anybody made. */ export function saveRailWidth(): void { try { storage()?.setItem(KEY, String(rail.width)) } catch { // Quota, private mode, a profile with storage off — the rail is still the width they dragged it to. } } // The pre-paint script in `app.html` applies the same value to `--rail-w` before this module exists, // which is what keeps a remembered 320px rail from being drawn at 246px first. Reading it again here // means the state the grip edits and the variable the grid reads start out agreeing. restoreRailWidth()