vojo/src/app/state/mediaSidePanelWidth.ts

68 lines
3.1 KiB
TypeScript

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<unknown>(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<number>(
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 };