mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Add resizable panes with drag handles, min widths, and persistence
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Pane layout state — the resizable three-pane shell (spec §01A, persisted per
|
||||
* §09D "Panel layout").
|
||||
*
|
||||
* Model: the two **side** panes (library, preview) carry explicit remembered
|
||||
* widths; the **center** editor flexes to fill the remainder. This is what makes
|
||||
* a drag "leave the rest of the layout unaffected" (§01A): dragging the left
|
||||
* handle trades width between library and editor, the right handle between
|
||||
* preview and editor — the opposite side pane never moves.
|
||||
*
|
||||
* Per-pane visibility / the toggle strip (the other half of §01A) is not here
|
||||
* yet; this slice is resize + persistence, pulled forward from M6 because it is
|
||||
* coupled to the Live Preview's container sizing.
|
||||
*
|
||||
* Pure clamp helpers live alongside the store so the resize math is unit-tested
|
||||
* without a DOM. Persistence is a startup subscriber (orchestration/panes), not
|
||||
* done here — the store stays browser-free.
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
|
||||
/** Which side pane a drag handle controls. */
|
||||
export type PaneSide = 'library' | 'preview';
|
||||
|
||||
/** Minimum widths (px) enforced while resizing, so no pane collapses (§01A). */
|
||||
export const PANE_MIN = { library: 180, preview: 240, editor: 320 } as const;
|
||||
|
||||
/** Initial side-pane widths (px) on first run, before any persisted layout. */
|
||||
export const PANE_DEFAULT = { library: 280, preview: 360 } as const;
|
||||
|
||||
/** Combined width (px) the resize handles occupy between the panes. */
|
||||
export const HANDLES_TOTAL = 12;
|
||||
|
||||
/**
|
||||
* Clamp a desired side-pane width so it stays at least its own minimum and
|
||||
* leaves the editor at least its minimum. Pure — the single place the resize
|
||||
* constraint lives. `containerWidth` is the full panes-row width; `otherWidth`
|
||||
* is the opposite side pane's current width.
|
||||
*/
|
||||
export function clampSideWidth(
|
||||
side: PaneSide,
|
||||
desired: number,
|
||||
containerWidth: number,
|
||||
otherWidth: number,
|
||||
): number {
|
||||
const min = PANE_MIN[side];
|
||||
// The widest this pane can be while the editor still meets its minimum.
|
||||
const max = containerWidth - otherWidth - HANDLES_TOTAL - PANE_MIN.editor;
|
||||
// If the container is too narrow for everyone, the min wins (never below it).
|
||||
return Math.max(min, Math.min(desired, Math.max(min, max)));
|
||||
}
|
||||
|
||||
export interface PanesState {
|
||||
libraryWidth: number;
|
||||
previewWidth: number;
|
||||
/** Set a side pane's width (already clamped by the caller). */
|
||||
setWidth: (side: PaneSide, width: number) => void;
|
||||
/** Restore persisted widths on startup; missing values keep their defaults. */
|
||||
hydrate: (layout: { libraryWidth?: number; previewWidth?: number }) => void;
|
||||
}
|
||||
|
||||
export const usePanesStore = create<PanesState>((set) => ({
|
||||
libraryWidth: PANE_DEFAULT.library,
|
||||
previewWidth: PANE_DEFAULT.preview,
|
||||
|
||||
setWidth: (side, width) =>
|
||||
set(side === 'library' ? { libraryWidth: width } : { previewWidth: width }),
|
||||
|
||||
hydrate: (layout) =>
|
||||
set({
|
||||
libraryWidth: layout.libraryWidth ?? PANE_DEFAULT.library,
|
||||
previewWidth: layout.previewWidth ?? PANE_DEFAULT.preview,
|
||||
}),
|
||||
}));
|
||||
Reference in New Issue
Block a user