import { type JSX, For, children as resolveChildren } from 'solid-js'; import { cn } from '../utils/cn'; export interface PaneGridProps { /** The panes - arbitrary N. Each top-level child is one tile in the grid. */ children: JSX.Element; /** Minimum width of every pane, in px, before columns drop / the grid scrolls. * Defaults to `280`. */ minPaneWidth?: number; /** Minimum height of every pane, in px, before the grid scrolls vertically. * Defaults to `200`. */ minPaneHeight?: number; /** Column cap when the container is wide. Columns fill up to this many, then drop * to fewer as the container narrows. Defaults to `3`. */ maxColumns?: number; /** Gap between panes - any CSS length. Defaults to the kit gap * (`var(--kai-pane-grid-gap, 0.5rem)`). */ gap?: string; /** When set to a valid child index, render ONLY that pane full-bleed (a single * filling cell) - a simple maximize hook the consumer drives. `null`/`undefined` * (or an out-of-range index) shows the full tiled grid. */ maximizedIndex?: number | null; /** Extra classes for the grid container. */ class?: string; } /** * PaneGrid - an N-pane responsive tiling grid with a min-size + scroll floor. * * Lays an arbitrary number of panes out in a CSS grid that fills up to * `maxColumns` columns when wide, then DROPS columns (maxColumns -> ... -> 1) as * the container narrows - so panes never squish below their minimums. Once even one * column can't hold a pane at `minPaneWidth`, the grid SCROLLS instead of shrinking * the panes; rows are kept at least `minPaneHeight` tall the same way. Because new * rows stack downward, the overflow is preferentially VERTICAL - horizontal scroll * only appears when the container is narrower than a single pane's minimum. * * This generalizes the hand-rolled center grid from the Split Workspace demo, where * `kai-resizable` (capped at 3 panes) couldn't express an arbitrary-N tiled grid. * * Column track (computed from the props): * * repeat(auto-fit, minmax(max(px, (100% - ) / ), 1fr)) * * where `` is the total inter-column gap at the cap, `(maxColumns - 1) * gap`. * The inner `max()` is the floor: each track is at least `minPaneWidth`, and at most * `1fr`; the `(100% - gaps) / maxColumns` term is the per-column width WHEN the cap is * filled, so `auto-fit` never packs in more than `maxColumns` columns. As the * container shrinks past the point where a column can be `minPaneWidth` wide, a column * drops; below one pane's minimum, the `overflow:auto` + `min-w-0`/`min-h-0` container * scrolls. * * Maximize: when `maximizedIndex` points at a child, only that pane renders, in a * single `1fr` x `1fr` cell that fills the grid. Restore by clearing the prop. * * The gap default reads `--kai-pane-grid-gap` (fallback `0.5rem`), so a consumer can * retune spacing from outside without passing the prop. Everything is tokenized * (surface/border come from whatever the panes use) - no hardcoded colors, so it * reads correctly in light and dark. */ export function PaneGrid(props: PaneGridProps) { const minPaneWidth = () => props.minPaneWidth ?? 280; const minPaneHeight = () => props.minPaneHeight ?? 200; const maxColumns = () => Math.max(1, props.maxColumns ?? 3); const gap = () => props.gap ?? 'var(--kai-pane-grid-gap, 0.5rem)'; // Resolve children to their rendered nodes so a single pane can be picked out for // the maximize hook (mirrors how Resizable reads its panels). const resolved = resolveChildren(() => props.children); const items = (): JSX.Element[] => resolved.toArray() as JSX.Element[]; // A valid maximize target, or null for the full grid. const maxIndex = (): number | null => { const m = props.maximizedIndex; if (m == null) return null; return m >= 0 && m < items().length ? m : null; }; const shown = (): JSX.Element[] => { const m = maxIndex(); return m == null ? items() : [items()[m]]; }; const gridStyle = (): JSX.CSSProperties => { const g = gap(); if (maxIndex() != null) { // One filling cell. return { 'grid-template-columns': '1fr', 'grid-auto-rows': '1fr', gap: g, }; } const cols = maxColumns(); // The per-column width when the cap is filled: (container - all gaps) / cols. // Inside max() this is a math sub-expression, so the var/number arithmetic is valid. const capped = `(100% - ${cols - 1} * ${g}) / ${cols}`; return { 'grid-template-columns': `repeat(auto-fit, minmax(max(${minPaneWidth()}px, ${capped}), 1fr))`, 'grid-auto-rows': `minmax(${minPaneHeight()}px, 1fr)`, gap: g, }; }; return (
{(pane) => pane}
); }