import type { Rect, WindowMode } from './floatingGeometry'; /** * Persisted window geometry ( sub-task 2). * * GEOMETRY IS NOT PHI. x/y/w/h is layout, so persisting it is fine and follows the existing UI-preference * precedent (akashic_nav_collapsed, akashic_overview_messages_collapsed). The encounter NOTE DRAFT is a * different thing entirely and must stay in memory - do NOT "make them consistent". */ export interface StoredWindowState { mode: WindowMode; rect: Rect; restoreTo: 'fullscreen' | 'windowed'; /** * The viewport the `rect` was computed for. * * WITHOUT THIS, A POSITION FOR A SCREEN THAT NO LONGER EXISTS IS REPLAYED AS THOUGH IT WERE A * CHOICE. Measured 2026-08-22 on the encounter notes window: opened at 1032px it anchors to x=656, * which is right there. Reopened on a 2560px monitor that same x sits **on top of the collapsed * patient queue rail**, and the window's left resize grip swallows the *Expand queue* click - the * exact defect was raised for, arriving by a different route and surviving the fix for the * original one. * * CLAMPING CANNOT CATCH IT. The stale rect is entirely ON SCREEN at the wider size; what it has * lost is not visibility but ANCHORING, and nothing about a rect says which edge it was anchored to. * * Optional because a value stored before this field existed has no viewport to compare - and an * unknown viewport must be treated as a mismatch, so those self-heal on their first read. */ viewport?: { width: number; height: number }; } /** * `id` must be a STATIC surface name (`'meeting'`, `'encounter-notes'`), never a per-record identifier * (patient id, encounter id, etc.) - see the matching JSDoc on `FloatingWindowProps.id` in * `FloatingWindow.tsx`, which is the normative doc; this is the pointer to it from the storage side. * * Two concrete reasons: (1) `useFloatingGeometry` reads stored state for `id` exactly ONCE, at mount, so * changing `id` on a live instance does not "switch" which saved state is read - it silently overwrites * the NEW id's storage key with the state that was live under the OLD id; (2) a consumer-chosen * identifier baked into this key would survive logout on a shared clinic workstation, which the * "geometry is not PHI" note above does not cover - that note is about the persisted VALUE, not about * what a consumer chooses to name the KEY. */ export const windowStorageKey = (id: string): string => `akashic_floating_window_${id}`; const MODES: readonly WindowMode[] = ['fullscreen', 'windowed', 'minimised']; const isFiniteNumber = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v); const isRect = (v: unknown): v is Rect => { if (typeof v !== 'object' || v === null) return false; const r = v as Record; return isFiniteNumber(r.x) && isFiniteNumber(r.y) && isFiniteNumber(r.width) && isFiniteNumber(r.height); }; const isStoredWindowState = (v: unknown): v is StoredWindowState => { if (typeof v !== 'object' || v === null || Array.isArray(v)) return false; const s = v as Record; return MODES.includes(s.mode as WindowMode) && (s.restoreTo === 'fullscreen' || s.restoreTo === 'windowed') && isRect(s.rect); }; const sameViewport = ( stored: StoredWindowState['viewport'], current: { width: number; height: number } | undefined, ): boolean => Boolean( stored && current && isFiniteNumber(stored.width) && isFiniteNumber(stored.height) && stored.width === current.width && stored.height === current.height, ); /** * THE RECT IS DROPPED WHEN THE VIEWPORT HAS CHANGED; THE MODE IS NOT. * * *"I like this fullscreen"* is a preference about the surface and survives a monitor change. *"It was * at x=656"* is a fact about a screen the user is no longer looking at, and replaying it is what puts * the window over the patient queue. * * Callers that pass no viewport get the old behaviour, rect included - the check is opt-in so a * surface with no anchoring to lose is not forced to re-derive its geometry. */ export function readWindowState( id: string, currentViewport?: { width: number; height: number }, ): StoredWindowState | undefined { let raw: string | null = null; try { raw = localStorage.getItem(windowStorageKey(id)); } catch { return undefined; // private-mode Safari and similar } if (!raw) return undefined; try { const parsed: unknown = JSON.parse(raw); // Validate rather than trust: a stale or hand-edited value must not be able to render an // unreachable window, and a NaN would poison every later calculation silently. if (!isStoredWindowState(parsed)) return undefined; if (currentViewport && !sameViewport(parsed.viewport, currentViewport)) { // Keep what is still true, drop what is not. The caller falls back to its anchored default. return { mode: parsed.mode, restoreTo: parsed.restoreTo, rect: undefined as unknown as Rect }; } return parsed; } catch { return undefined; } } export function writeWindowState(id: string, state: StoredWindowState): void { try { localStorage.setItem(windowStorageKey(id), JSON.stringify(state)); } catch { // Quota exceeded or storage unavailable. A lost layout preference is not worth an exception. } }