import { atomWithLocalStorage, getLocalStorageItem, setLocalStorageItem, } from './utils/atomWithLocalStorage'; import { sidebarWidthAtom } from './sidebarWidth'; export const MEDIA_SIDE_PANEL_WIDTH_KEY = 'vojo_media_side_panel_width'; // Lowest readable width for `MediaViewerBody`'s media card + action row. // Below ~360 the file-name truncates aggressively and the action row // starts wrapping. export const MEDIA_SIDE_PANEL_WIDTH_MIN = 360; // Pleasant starting width on a typical 1440-wide desktop after the // chat-list column at its default 416 and the chat column reserve. export const MEDIA_SIDE_PANEL_WIDTH_DEFAULT = 520; // Hard ceiling: even on ultra-wide displays the media pane shouldn't // dwarf the chat — matches the prior CSS `clamp(..., ..., 880px)` cap. export const MEDIA_SIDE_PANEL_WIDTH_HARD_MAX = 880; // Layout constants the smart-max math depends on. Kept here (not // imported from horseshoe.ts / Sidebar.css.ts) so the clamp helper // stays a pure function safe to call from anywhere — including a // re-mount race where the CSS-module hasn't bound yet. const SIDEBAR_RAIL_PX = 66; // global icon rail, fixed const VOID_GAP_PX = 12; // VOJO_HORSESHOE_GAP_PX (page-nav<->chat and chat<->media) // Minimum width to reserve for the chat column when media is open. // Below ~360 the timeline text gets line-broken to single words and // reading falls apart — the whole point of this clamp is to protect // that reading area, not the media pane. const CHAT_COLUMN_RESERVED_PX = 360; const readMediaSidePanelWidth = (key: string): number => { const raw = getLocalStorageItem(key, MEDIA_SIDE_PANEL_WIDTH_DEFAULT); const value = typeof raw === 'number' && Number.isFinite(raw) ? raw : MEDIA_SIDE_PANEL_WIDTH_DEFAULT; return Math.max(MEDIA_SIDE_PANEL_WIDTH_MIN, Math.round(value)); }; export const mediaSidePanelWidthAtom = atomWithLocalStorage( MEDIA_SIDE_PANEL_WIDTH_KEY, readMediaSidePanelWidth, setLocalStorageItem ); // Smart maximum: viewport minus (left rail + chat-list column + two // horseshoe void gaps + chat-column reservation). Both the chat-list // and the chat-column are protected — dragging media wider can only // eat the slack between them and the hard cap. If even the reservation // can't fit (tiny laptop / virtual viewport), the max collapses to // MIN so the panel still mounts at a sane size. export const computeMediaSidePanelMax = (viewportWidth: number, pageNavWidth: number): number => { const reserved = SIDEBAR_RAIL_PX + pageNavWidth + VOID_GAP_PX * 2 + CHAT_COLUMN_RESERVED_PX; const available = viewportWidth - reserved; return Math.max(MEDIA_SIDE_PANEL_WIDTH_MIN, Math.min(MEDIA_SIDE_PANEL_WIDTH_HARD_MAX, available)); }; export const clampMediaSidePanelWidth = ( px: number, viewportWidth: number, pageNavWidth: number ): number => { const max = computeMediaSidePanelMax(viewportWidth, pageNavWidth); return Math.max(MEDIA_SIDE_PANEL_WIDTH_MIN, Math.min(max, Math.round(px))); }; // Re-export so callers don't have to import `sidebarWidthAtom` from a // separate state file just to feed the clamp helper. export { sidebarWidthAtom };