@dimina-kit/electron-deck 0.1.0-dev.20260702175315 → 0.1.0-dev.20260703051110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -15
- package/dist/client/index.d.ts +8 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/dock-react/dock-view.d.ts +43 -19
- package/dist/dock-react/dock-view.d.ts.map +1 -1
- package/dist/dock-react/drag-redock.d.ts +18 -0
- package/dist/dock-react/drag-redock.d.ts.map +1 -1
- package/dist/dock-react/group-view.d.ts +18 -0
- package/dist/dock-react/group-view.d.ts.map +1 -0
- package/dist/dock-react/index.d.ts +3 -2
- package/dist/dock-react/index.d.ts.map +1 -1
- package/dist/dock-react/index.js +384 -289
- package/dist/dock-react/index.js.map +1 -1
- package/dist/dock-react/panel-body.d.ts +24 -0
- package/dist/dock-react/panel-body.d.ts.map +1 -0
- package/dist/dock-react/split-sizing.d.ts +109 -0
- package/dist/dock-react/split-sizing.d.ts.map +1 -0
- package/dist/dock-react/split-view.d.ts +15 -0
- package/dist/dock-react/split-view.d.ts.map +1 -0
- package/dist/{electron-deck-B9fun93z.js → electron-deck-Bi-u-Aze.js} +175 -79
- package/dist/electron-deck-Bi-u-Aze.js.map +1 -0
- package/dist/electron-deck.d.ts +5 -0
- package/dist/electron-deck.d.ts.map +1 -1
- package/dist/events.d.ts +1 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/host/index.d.ts +5 -0
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/deck-app-view-move.d.ts +17 -0
- package/dist/internal/deck-app-view-move.d.ts.map +1 -0
- package/dist/internal/deck-app.d.ts +58 -1
- package/dist/internal/deck-app.d.ts.map +1 -1
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +2 -2
- package/dist/layout/model.d.ts.map +1 -1
- package/dist/layout/mutations.d.ts.map +1 -1
- package/dist/layout/tree-query.d.ts +18 -0
- package/dist/layout/tree-query.d.ts.map +1 -0
- package/dist/layout/types.d.ts +11 -9
- package/dist/layout/types.d.ts.map +1 -1
- package/dist/{layout-ByOiOdnc.js → layout-D-LYJIpM.js} +57 -41
- package/dist/layout-D-LYJIpM.js.map +1 -0
- package/dist/main/compositor.d.ts.map +1 -1
- package/dist/main/index.js +1 -1
- package/dist/main/view-handle.d.ts.map +1 -1
- package/dist/preload/index.cjs +5 -0
- package/dist/preload/index.cjs.map +1 -1
- package/dist/preload/index.d.ts +5 -0
- package/dist/preload/index.d.ts.map +1 -1
- package/dist/preload/index.js +5 -0
- package/dist/preload/index.js.map +1 -1
- package/dist/types.d.ts +32 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/{view-handle-DDhpBuW-.js → view-handle-zav25k3-.js} +141 -86
- package/dist/view-handle-zav25k3-.js.map +1 -0
- package/package.json +11 -5
- package/dist/electron-deck-B9fun93z.js.map +0 -1
- package/dist/layout-ByOiOdnc.js.map +0 -1
- package/dist/view-handle-DDhpBuW-.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/dock-react/drag-redock.ts","../../src/dock-react/dock-view.tsx"],"sourcesContent":["/**\n * `drag-redock` — PURE geometry + descriptor layer for tab drag-to-redock.\n *\n * This module is intentionally REACT-FREE and ELECTRON-FREE so it can run under\n * the node `vitest.config.ts` suite (its spec is `drag-redock.test.ts`). It owns\n * two responsibilities, both pure functions of their inputs:\n *\n * 1. GEOMETRY — `computeDropZone(rect, point)` maps a pointer position over a\n * group's rectangle to one of five drop zones (the four edge bands plus the\n * interior `center`). This is the only place edge-band math lives; the React\n * layer feeds it real `getBoundingClientRect` numbers during dragover.\n *\n * 2. DESCRIPTOR — `dropZoneToMutation(zone, dragged, target)` translates a zone\n * into an engine-NEUTRAL `RedockMutation` intent (`move` | `split`). The\n * descriptor only NAMES the intent; the React layer realizes it against the\n * real tree (a split of an EXISTING panel needs extract-then-split, because\n * `splitPanel` throws if the new panel already exists — see the caller).\n *\n * Keeping this split (geometry/descriptor here, engine application in React)\n * means the geometry is exhaustively unit-testable without a DOM, and the React\n * layer stays a thin gesture binding.\n */\n\n/** The five drop zones: four edge bands + the tab-joining interior. */\nexport type DropZone = 'left' | 'right' | 'top' | 'bottom' | 'center'\n\n/**\n * Engine-neutral re-dock INTENT. `move` joins the dragged panel into a tab\n * group; `split` puts it adjacent to a target panel in a new split. This is a\n * pure descriptor — it does NOT itself touch a tree (the caller applies it,\n * extract-then-split'ing for an existing panel).\n */\nexport type RedockMutation =\n\t| { kind: 'move'; panelId: string; destGroupId: string }\n\t| {\n\t\tkind: 'split'\n\t\tatPanelId: string\n\t\tdir: 'row' | 'column'\n\t\tside: 'before' | 'after'\n\t\tnewPanelId: string\n\t}\n\n/**\n * Classify a pointer position (RELATIVE to the rect's top-left; `(0,0)` is the\n * corner) into a drop zone.\n *\n * Band thickness = `edgeFraction * min(width, height)` — a single symmetric band\n * width derived from the SHORTER side so a wide/short panel doesn't get an\n * absurdly fat horizontal band. A point in NO band is `center` (the tab-join\n * interior). A point in TWO bands (a corner) tie-breaks by NORMALIZED distance\n * to each active edge; on an exact tie HORIZONTAL wins. A point OUTSIDE the rect\n * clamps to the nearest edge zone (per-axis; diagonal corners pick the larger\n * overshoot, horizontal breaking ties).\n */\nexport function computeDropZone(\n\trect: { width: number; height: number },\n\tpoint: { x: number; y: number },\n\tedgeFraction = 0.25,\n): DropZone {\n\tconst { width, height } = rect\n\tconst { x, y } = point\n\n\t// ── DEGENERATE / NON-FINITE guard (N1) ─────────────────────────────────\n\t// A zero/negative/non-finite rect has no meaningful edge bands; a non-finite\n\t// point can't be classified. Treat either as the interior (tab-join). Note a\n\t// non-finite WIDTH/HEIGHT (e.g. Infinity) satisfies `> 0` and would otherwise\n\t// slip through — `band = ef * min(Infinity, h)` yields a finite band and the\n\t// point is misclassified as an EDGE zone — so reject finiteness EXPLICITLY.\n\tif (\n\t\t!(width > 0) || !(height > 0)\n\t\t|| !Number.isFinite(width) || !Number.isFinite(height)\n\t\t|| !Number.isFinite(x) || !Number.isFinite(y)\n\t) {\n\t\treturn 'center'\n\t}\n\t// Clamp the band fraction to [0, 0.5] (N1): a fraction > 0.5 makes the left\n\t// and right bands (and top/bottom) OVERLAP, so a point near the right edge of\n\t// a narrow rect would satisfy BOTH `inLeft` and `inRight` and be misread as\n\t// `left`. Capping at 0.5 keeps the two bands disjoint.\n\tconst ef = Number.isFinite(edgeFraction) ? Math.max(0, Math.min(0.5, edgeFraction)) : 0.25\n\n\t// ── OUT-OF-RECT clamp ──────────────────────────────────────────────────\n\t// A pointer dragged past the panel edge has no band membership; clamp it to\n\t// the nearest edge zone. Compute the signed per-axis overshoot: negative =\n\t// out the low side (left/top), positive = out the high side (right/bottom).\n\tconst outX = x < 0 ? x : x > width ? x - width : 0\n\tconst outY = y < 0 ? y : y > height ? y - height : 0\n\tif (outX !== 0 || outY !== 0) {\n\t\tconst magX = Math.abs(outX)\n\t\tconst magY = Math.abs(outY)\n\t\t// Both axes out (a diagonal corner): the axis with the LARGER overshoot\n\t\t// magnitude wins; an exact tie is broken by HORIZONTAL (the x axis).\n\t\tif (magX >= magY) {\n\t\t\tif (magX > 0) return outX < 0 ? 'left' : 'right'\n\t\t}\n\t\t// Only y out, or y overshoot strictly larger.\n\t\treturn outY < 0 ? 'top' : 'bottom'\n\t}\n\n\t// ── BAND membership (in-rect) ──────────────────────────────────────────\n\tconst band = ef * Math.min(width, height)\n\tconst inLeft = x < band\n\tconst inRight = x > width - band\n\tconst inTop = y < band\n\tconst inBottom = y > height - band\n\n\tconst activeHoriz = inLeft || inRight\n\tconst activeVert = inTop || inBottom\n\n\t// No band => interior => join the tab group.\n\tif (!activeHoriz && !activeVert) return 'center'\n\n\t// Single-axis band => that edge zone directly.\n\tif (activeHoriz && !activeVert) return inLeft ? 'left' : 'right'\n\tif (activeVert && !activeHoriz) return inTop ? 'top' : 'bottom'\n\n\t// ── CORNER tie-break (both axes active) ────────────────────────────────\n\t// Pick the edge with the SMALLER normalized distance. Only the ACTIVE\n\t// horizontal edge and ACTIVE vertical edge can compete (left vs right and\n\t// top vs bottom can't both be active for a sane rect). On an exact tie,\n\t// HORIZONTAL wins (`<=` on the horizontal distance).\n\tconst dHoriz = inLeft ? x / width : (width - x) / width\n\tconst dVert = inTop ? y / height : (height - y) / height\n\tif (dHoriz <= dVert) return inLeft ? 'left' : 'right'\n\treturn inTop ? 'top' : 'bottom'\n}\n\n/**\n * Translate a drop zone into an engine-neutral re-dock descriptor.\n *\n * center => move the dragged panel into the target's tab group.\n * left/right => split the target panel horizontally (row), dragged before/after.\n * top/bottom => split the target panel vertically (column), dragged before/after.\n */\nexport function dropZoneToMutation(\n\tzone: DropZone,\n\tdragged: string,\n\ttarget: { groupId: string; panelId: string },\n): RedockMutation {\n\tif (zone === 'center') {\n\t\treturn { kind: 'move', panelId: dragged, destGroupId: target.groupId }\n\t}\n\tconst dir: 'row' | 'column' = zone === 'left' || zone === 'right' ? 'row' : 'column'\n\tconst side: 'before' | 'after' = zone === 'left' || zone === 'top' ? 'before' : 'after'\n\treturn { kind: 'split', atPanelId: target.panelId, dir, side, newPanelId: dragged }\n}\n\n/**\n * A re-dock is a NO-OP when it would not change the tree:\n *\n * 1. The drop targets the dragged panel ITSELF (`dragged === target.panelId`),\n * in ANY zone — you can neither tab a panel onto its own tab nor split a\n * panel around itself. Crucially this also guards the SPLIT case: without\n * it, `extractPanel(dragged)` then `splitPanel(atPanelId = dragged)` removes\n * the very anchor it then splits at and THROWS (the self-collapse / single-\n * panel-self-split bug).\n * 2. A `center` drop into a group the dragged panel ALREADY lives in\n * (`draggedGroupId === target.groupId`) — `movePanel` would re-append it and\n * bump the revision for no visible change (churn).\n *\n * A SPLIT onto a DIFFERENT panel of the dragged panel's own group is still a real\n * re-dock (it splits the group around that sibling), so it is NOT a no-op.\n *\n * `draggedGroupId` is the id of the tab group currently holding `dragged`\n * (`undefined` if it is not in the tree).\n */\nexport function isNoopRedock(\n\tdragged: string,\n\tdraggedGroupId: string | undefined,\n\ttarget: { groupId: string; panelId: string },\n\tzone: DropZone,\n): boolean {\n\tif (dragged === target.panelId) return true\n\tif (zone === 'center' && draggedGroupId !== undefined && draggedGroupId === target.groupId) {\n\t\treturn true\n\t}\n\treturn false\n}\n\n/**\n * Map a pointer x-position over a horizontal tab strip to an insertion index for\n * a within-strip REORDER (the `dropPolicy:'reorder-only'` gesture). The strip is\n * the dragged tab's own group; `tabRects` are the tab buttons' rects in visual\n * order (each `left` is the rect's left edge, `width` its width). The index is\n * the count of tabs whose MIDPOINT the pointer has passed: the LEFT half of a tab\n * inserts BEFORE it, the RIGHT half (and the exact midpoint) inserts AFTER it. A\n * pointer left of the first tab → 0; past the last tab → `tabRects.length`. Pure\n * (no DOM): the caller measures the rects and passes the pointer x. An empty\n * strip or a non-finite pointer → 0.\n */\nexport function computeReorderIndex(\n\ttabRects: readonly { left: number; width: number }[],\n\tpointerX: number,\n): number {\n\tif (!Number.isFinite(pointerX)) return 0\n\tlet index = 0\n\tfor (const rect of tabRects) {\n\t\tconst midpoint = rect.left + rect.width / 2\n\t\tif (pointerX >= midpoint) index += 1\n\t\telse break\n\t}\n\treturn index\n}\n","/**\n * `<DockView>` — React renderer for a layout-as-data tree.\n *\n * Pure function of the current model snapshot (+ registry + the two host\n * callbacks). It owns NO layout state: it subscribes to the `LayoutModel`,\n * keeps the latest tree in component state, and re-renders on every emission.\n * All structural targeting is via STABLE `data-*` attributes — see the\n * contract doc-block in `dock-view.test.tsx`.\n *\n * This file lives under `src/dock-react/` (NOT `src/layout/`), so importing\n * react here does not violate the pure-TS layout boundary.\n */\nimport {\n\tcreateContext,\n\tFragment,\n\tuseCallback,\n\tuseContext,\n\tuseEffect,\n\tuseRef,\n\tuseState,\n\ttype DragEvent,\n\ttype ReactNode,\n} from 'react'\nimport { Group, Panel, Separator } from 'react-resizable-panels'\nimport type { GroupImperativeHandle, Layout } from 'react-resizable-panels'\nimport {\n\tclosePanelForUser,\n\textractPanel,\n\tmovePanel,\n\tsetActive,\n\tsetSizes,\n\tsplitPanel,\n} from '../layout/index.js'\nimport type {\n\tLayoutModel,\n\tLayoutNode,\n\tLayoutTree,\n\tPanelRegistry,\n\tSizeConstraint,\n\tSplitNode,\n\tTabGroupNode,\n} from '../layout/index.js'\nimport {\n\tcomputeDropZone,\n\tcomputeReorderIndex,\n\tdropZoneToMutation,\n\tisNoopRedock,\n\ttype DropZone,\n} from './drag-redock.js'\n\n/**\n * The dataTransfer MIME under which a drag carries the dragged panel id. A\n * custom type (vs `text/plain`) keeps deck drags from being confused with\n * arbitrary text drops; the panel id also stays recoverable from the source\n * tab's `data-deck-tab` attribute (the jsdom seam relies on the latter).\n */\nconst DRAG_PANEL_MIME = 'application/x-deck-panel'\n\n// The panel id of the in-flight tab drag, set on `dragstart` and cleared on\n// `dragend`/`drop`. The DataTransfer VALUE is unreadable during `dragover` (only\n// `types` is exposed, by spec), so the drop-indicator path can't recover the\n// dragged id from the event — it reads it from here instead to decide whether to\n// paint a highlight for THIS drag (e.g. a `reorder-only` panel shows none).\nlet activeDragPanelId: string | null = null\n\nexport interface DockViewProps {\n\tmodel: LayoutModel\n\tregistry: PanelRegistry\n\t/**\n\t * Render a DOM panel's body. `opts.active` is `true` iff `panelId` is its tab\n\t * group's currently-active panel. Under DOM-panel keepalive every DOM panel in\n\t * a group is rendered (the inactive ones hidden), so this callback is invoked\n\t * for ALL of them and `opts.active` is RE-EVALUATED on every activation change\n\t * WITHOUT remounting the kept-alive subtree — letting a host run on-activation\n\t * side effects (e.g. data refresh) off the false→true `active` edge.\n\t */\n\trenderDomPanel: (panelId: string, opts: { active: boolean }) => ReactNode\n\tbindNativeSlot: (panelId: string, el: HTMLElement | null) => void\n\t/**\n\t * Opt-in: while a `dropPolicy:'reorder-only'` panel is dragged, paint NO drop\n\t * indicator over a group body (its own edges or any other group) — those are\n\t * no-op targets, so the highlight is misleading. Default `false` keeps the\n\t * geometry-only indicator (it paints where the pointer is; the capability\n\t * gate still rejects the drop). Hosts that want the cleaner \"no misleading\n\t * highlight\" UX pass `true`.\n\t */\n\tsuppressReorderOnlyDropIndicator?: boolean\n\t/**\n\t * Fires when the user clicks a tab that is ALREADY active. The host can use\n\t * this to toggle the panel's visibility (e.g. collapse the debug group).\n\t * When absent, clicking an active tab is a no-op (the existing behaviour).\n\t */\n\tonActiveTabClick?: (panelId: string) => void\n}\n\n/**\n * The current layout EPOCH — the model's revision, bumped once per committed\n * mutation. `DockView` provides it; descendant panels read it via\n * `useDockLayoutEpoch`.\n *\n * Its reason to exist: a native-view overlay anchored inside a dock panel\n * (a `WebContentsView` tracking its slot's geometry) re-publishes its bounds\n * only on GEOMETRY events (ResizeObserver / window-resize / splitter-drag). A\n * layout mutation that REORDERS a slot without resizing it — flipping a\n * fixed-width simulator column left↔right, moving a region — produces NO such\n * event (a same-size flex reorder fires nothing), so the overlay would freeze\n * at its old position. The epoch is the layout layer's explicit \"something\n * moved\" signal: a panel hosting a native overlay re-measures (e.g. pulses its\n * view-anchor) when the epoch changes, catching the translate the browser never\n * reports. Default 0 (outside a provider): the consuming effect runs once on\n * mount, which is harmless.\n */\nconst LayoutEpochContext = createContext<number>(0)\n\n/**\n * Read the current dock layout epoch (the model revision). Re-renders the caller\n * whenever a layout mutation commits. Keyed into a panel's effect deps, it lets\n * a native-overlay host re-measure on a reorder that fires no geometry event.\n * Returns 0 when used outside a `<DockView>`.\n */\nexport function useDockLayoutEpoch(): number {\n\treturn useContext(LayoutEpochContext)\n}\n\n/** Normalize raw split weights to percentages summing ~100 for `defaultSize`. */\nfunction toPercentages(sizes: readonly number[]): number[] {\n\tconst total = sizes.reduce((a, b) => a + (b > 0 ? b : 0), 0)\n\tif (total <= 0) {\n\t\t// Degenerate weights → distribute evenly.\n\t\tconst even = sizes.length > 0 ? 100 / sizes.length : 100\n\t\treturn sizes.map(() => even)\n\t}\n\treturn sizes.map((s) => ((s > 0 ? s : 0) / total) * 100)\n}\n\n/**\n * Compute the `defaultSize` percentage for each FLEXIBLE child, keyed by its\n * ORIGINAL child index. Px-sized children (a non-null `constraint` — `fixedPx`\n * locked OR `minPx` floored) are EXCLUDED from the pool: their size is px, not a\n * weight, so it must never pollute the flexible siblings' normalization (FIX E1).\n * `constraints[i]` non-null ⇒ child i is px-sized and absent from the returned map.\n */\nexport function computeFlexiblePercentages(\n\tsizes: readonly number[],\n\tconstraints: readonly (SizeConstraint | null)[] | undefined,\n): Map<number, number> {\n\tconst flexibleIndices = sizes\n\t\t.map((_, i) => i)\n\t\t.filter((i) => (constraints?.[i] ?? null) === null)\n\tconst pct = toPercentages(flexibleIndices.map((i) => sizes[i] ?? 0))\n\tconst out = new Map<number, number>()\n\tflexibleIndices.forEach((origIndex, j) => {\n\t\tout.set(origIndex, pct[j]!)\n\t})\n\treturn out\n}\n\nexport function DockView(props: DockViewProps): ReactNode {\n\tconst { model, registry, renderDomPanel, bindNativeSlot, suppressReorderOnlyDropIndicator, onActiveTabClick } = props\n\n\t// Snapshot the canonical tree; re-render on every external emission. The\n\t// `epoch` mirrors the model revision (0 before the first apply) and is\n\t// provided to descendant panels via `LayoutEpochContext` so a native-overlay\n\t// host can re-measure on a reorder that fires no geometry event.\n\tconst [tree, setTree] = useState<LayoutTree>(() => model.get())\n\tconst [epoch, setEpoch] = useState<number>(0)\n\n\tuseEffect(() => {\n\t\t// Re-sync immediately in case the model changed between the initial\n\t\t// `useState` read and effect commit, then track future emissions.\n\t\tsetTree(model.get())\n\t\tconst unsubscribe = model.subscribe((snap) => {\n\t\t\tsetTree(snap.tree)\n\t\t\tsetEpoch(snap.revision)\n\t\t})\n\t\treturn unsubscribe\n\t}, [model])\n\n\tconst handleActivate = useCallback(\n\t\t(groupId: string, panelId: string) => {\n\t\t\tmodel.apply((t) => setActive(t, groupId, panelId))\n\t\t},\n\t\t[model],\n\t)\n\n\t// Close write-back: every tab close funnels through the capability-aware\n\t// user action. The generic `closePanel` mutation remains available to\n\t// programmatic layout transforms.\n\tconst handleClose = useCallback(\n\t\t(panelId: string) => {\n\t\t\tmodel.apply((t) => closePanelForUser(t, panelId, registry))\n\t\t},\n\t\t[model, registry],\n\t)\n\n\t// Total panels across the WHOLE tree. The close affordance is suppressed only\n\t// when the entire layout has a single panel left (closing it would no-op);\n\t// a multi-panel single group still shows closes. GroupView sees only its own\n\t// node, so we compute the global count here and thread `canClose` down.\n\tconst canClose = countPanels(tree.root) > 1\n\n\t// Panel-id membership of the CURRENT tree (M2). A drop's payload must be a\n\t// panel that actually lives in the tree, not merely one that is registered —\n\t// see `handleDrop`. Derived from the snapshot the component already holds.\n\tconst isPanelInTree = useCallback(\n\t\t(panelId: string) => findPanelGroupId(tree.root, panelId) !== undefined,\n\t\t[tree],\n\t)\n\n\t// Resize write-back: a drag commit (or the `__deckApplyLayout` seam) funnels\n\t// new per-split weights here, which apply `setSizes` to the canonical model.\n\tconst handleApplyLayout = useCallback(\n\t\t(splitId: string, weights: number[]) => {\n\t\t\tmodel.apply((t) => setSizes(t, splitId, weights))\n\t\t},\n\t\t[model],\n\t)\n\n\t// Drag-to-redock commit. A tab dragged onto `groupId`'s `zone` (the GROUP seam\n\t// or a real dragover) lands here. `activePanelId` is the group's natural split\n\t// anchor (the visible body the user dropped onto). We translate the zone to an\n\t// engine-neutral descriptor, skip true no-ops (drop onto own tab center), and\n\t// apply the descriptor:\n\t// - move => single `movePanel` (joins the target tab group).\n\t// - split => extract-then-split COMPOSED in ONE `model.apply` so the whole\n\t// re-dock is a single atomic emission (one subscriber notification, one\n\t// re-render): `splitPanel` throws if the dragged panel already exists, so\n\t// an EXISTING panel must be `extractPanel`'d first, then split against the\n\t// target. Doing both inside one `apply` keeps the tree from ever being\n\t// observed in the transient extracted state.\n\tconst handleRedock = useCallback(\n\t\t(\n\t\t\tgroupId: string,\n\t\t\tactivePanelId: string,\n\t\t\tdraggedPanelId: string,\n\t\t\tzone: DropZone,\n\t\t\treorderIndex?: number,\n\t\t) => {\n\t\t\tconst target = { groupId, panelId: activePanelId }\n\t\t\t// The dragged panel's CURRENT group — needed to skip a center-drop back\n\t\t\t// into its own group (M2) and (via `dragged === target.panelId`) a\n\t\t\t// self-split (M1).\n\t\t\tconst draggedGroupId = findPanelGroupId(model.get().root, draggedPanelId)\n\n\t\t\t// ── PanelCapabilities gate (GOAL A source): a `draggable:false` panel is\n\t\t\t// a locked STRUCTURAL panel — it can never be torn into another region.\n\t\t\t// UI drag-start already refuses to lift it, but the imperative drop seam\n\t\t\t// bypasses that, so reject a locked dragged source here too (defense in\n\t\t\t// depth) — its position in the tree is fixed. ──\n\t\t\tif (registry.get(draggedPanelId)?.draggable === false) return\n\n\t\t\t// ── PanelCapabilities gate (GOAL A target): a group whose ACTIVE panel is\n\t\t\t// `draggable:false` is a locked drop ANCHOR — nothing may join or split\n\t\t\t// against it, in any zone. Checked before the no-op/reorder logic so a\n\t\t\t// locked simulator/editor can never absorb another panel. ──\n\t\t\tif (registry.get(activePanelId)?.draggable === false) return\n\n\t\t\t// ── PanelCapabilities gate (GOAL B): a `reorder-only` dragged panel may\n\t\t\t// ONLY reorder WITHIN its own group. It never leaves the group (a center\n\t\t\t// drop into another group) and never edge-splits (any edge zone, own or\n\t\t\t// other group). The one motion it permits — a center drop into its OWN\n\t\t\t// group — must OVERRIDE `isNoopRedock` (which would swallow it as churn).\n\t\t\tif (registry.get(draggedPanelId)?.dropPolicy === 'reorder-only') {\n\t\t\t\tif (draggedGroupId === undefined || draggedGroupId !== groupId) return\n\t\t\t\tif (zone !== 'center') return\n\t\t\t\t// Anchor the reorder on the pointer-derived index when the gesture\n\t\t\t\t// supplies one (real drag), else on the active anchor's CURRENT index\n\t\t\t\t// (the imperative seam carries no pointer).\n\t\t\t\tconst group = findGroupById(model.get().root, groupId)\n\t\t\t\tconst index = reorderIndex ?? (group ? group.panels.indexOf(activePanelId) : undefined)\n\t\t\t\tmodel.apply((t) => movePanel(t, draggedPanelId, { groupId, index }))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (isNoopRedock(draggedPanelId, draggedGroupId, target, zone)) return\n\t\t\tconst mutation = dropZoneToMutation(zone, draggedPanelId, target)\n\t\t\tif (mutation.kind === 'move') {\n\t\t\t\tmodel.apply((t) => movePanel(t, mutation.panelId, { groupId: mutation.destGroupId }))\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Atomic extract-then-split for an existing dragged panel. Defensive\n\t\t\t// guard (M1): if extracting the dragged panel removed the split anchor,\n\t\t\t// abort instead of letting `splitPanel` throw on a missing anchor.\n\t\t\tmodel.apply((t) => {\n\t\t\t\tconst { tree } = extractPanel(t, mutation.newPanelId)\n\t\t\t\tif (findPanelGroupId(tree.root, mutation.atPanelId) === undefined) return t\n\t\t\t\treturn splitPanel(tree, mutation.atPanelId, mutation.dir, mutation.newPanelId, mutation.side)\n\t\t\t})\n\t\t},\n\t\t[model, registry],\n\t)\n\n\treturn (\n\t\t<LayoutEpochContext.Provider value={epoch}>\n\t\t\t{renderNode(tree.root, {\n\t\t\t\tregistry,\n\t\t\t\trenderDomPanel,\n\t\t\t\tbindNativeSlot,\n\t\t\t\tonActivate: handleActivate,\n\t\t\t\tonActiveTabClick,\n\t\t\t\tonApplyLayout: handleApplyLayout,\n\t\t\t\tonRedock: handleRedock,\n\t\t\t\tonClose: handleClose,\n\t\t\t\tcanClose,\n\t\t\t\tisPanelInTree,\n\t\t\t\tsuppressReorderOnlyDropIndicator: suppressReorderOnlyDropIndicator ?? false,\n\t\t\t})}\n\t\t</LayoutEpochContext.Provider>\n\t)\n}\n\n/** A split wrapper element augmented with the resize write-back seam plus the\n * rrp Group imperative handle (M1 model→view sync). Hosts/tests reach the live\n * split layout through `__deckGroupApi` the same way they reach the write-back\n * through `__deckApplyLayout`. */\ntype DeckSplitElement = HTMLDivElement & {\n\t__deckApplyLayout?: (weights: number[]) => void\n\t__deckGroupApi?: GroupImperativeHandle\n}\n\n/** A group wrapper element augmented with the drop-handling seam. Mirrors the\n * `__deckApplyLayout` discipline on split elements: an imperative hook the\n * gesture (or a unit test) calls to commit a re-dock against the model. */\ntype DeckGroupElement = HTMLDivElement & {\n\t__deckHandleDrop?: (draggedPanelId: string, zone: DropZone) => void\n}\n\ninterface RenderContext {\n\tregistry: PanelRegistry\n\trenderDomPanel: (panelId: string, opts: { active: boolean }) => ReactNode\n\tbindNativeSlot: (panelId: string, el: HTMLElement | null) => void\n\tonActivate: (groupId: string, panelId: string) => void\n\tonActiveTabClick: ((panelId: string) => void) | undefined\n\tonApplyLayout: (splitId: string, weights: number[]) => void\n\tonRedock: (\n\t\tgroupId: string,\n\t\tactivePanelId: string,\n\t\tdraggedPanelId: string,\n\t\tzone: DropZone,\n\t\t/** Pointer-derived insertion index for a within-group reorder\n\t\t * (`dropPolicy:'reorder-only'`). Omitted by the imperative seam, which\n\t\t * falls back to the active anchor's current index. */\n\t\treorderIndex?: number,\n\t) => void\n\tonClose: (panelId: string) => void\n\t/** False when the whole tree has a single panel — suppresses every close\n\t * button so the layout can't be emptied (closePanel would no-op anyway). */\n\tcanClose: boolean\n\t/** True iff `panelId` is present in the CURRENT layout tree (M2). Registry\n\t * membership is NOT enough: a panel can be registered but absent from the tree\n\t * (closed out / never docked), and driving a re-dock on it makes the mutation\n\t * layer throw `panel not found`. A drop carrying such an id must be a no-op. */\n\tisPanelInTree: (panelId: string) => boolean\n\t/** See {@link DockViewProps.suppressReorderOnlyDropIndicator}. */\n\tsuppressReorderOnlyDropIndicator: boolean\n}\n\n/** The id of the tab group currently holding `panelId`, or `undefined` if the\n * panel is not in the tree. Used by `handleRedock` to detect a drop back into\n * the dragged panel's own group (M2) and to guard a vanished split anchor (M1). */\nfunction findPanelGroupId(node: LayoutNode, panelId: string): string | undefined {\n\tif (node.kind === 'tabs') {\n\t\treturn node.panels.includes(panelId) ? node.id : undefined\n\t}\n\tfor (const child of node.children) {\n\t\tconst found = findPanelGroupId(child, panelId)\n\t\tif (found !== undefined) return found\n\t}\n\treturn undefined\n}\n\n/** The tab-group node with `groupId`, or `undefined`. Used by `handleRedock` to\n * read the live `panels` order of a reorder target (to anchor a same-group\n * reorder on the active panel's current index when the gesture carries no\n * pointer-derived index — e.g. the `__deckHandleDrop` test seam). */\nfunction findGroupById(node: LayoutNode, groupId: string): TabGroupNode | undefined {\n\tif (node.kind === 'tabs') return node.id === groupId ? node : undefined\n\tfor (const child of node.children) {\n\t\tconst found = findGroupById(child, groupId)\n\t\tif (found !== undefined) return found\n\t}\n\treturn undefined\n}\n\n/** Total panels anywhere in the tree. Drives the last-panel close suppression:\n * a GroupView only knows its own node, so DockView computes this global count\n * once and threads the resulting `canClose` boolean down. */\nfunction countPanels(node: LayoutNode): number {\n\tif (node.kind === 'tabs') return node.panels.length\n\treturn node.children.reduce((sum, child) => sum + countPanels(child), 0)\n}\n\nfunction renderNode(node: LayoutNode, ctx: RenderContext): ReactNode {\n\treturn node.kind === 'split'\n\t\t? renderSplit(node, ctx)\n\t\t: renderGroup(node, ctx)\n}\n\n/** A split is a stateful component (it holds the rrp Group imperative ref + runs\n * the M1 model→view sync effect), so render it via JSX with a STABLE key so\n * React preserves that ref / its kept-alive subtree across model emissions\n * rather than remounting on every snapshot. Mirrors `renderGroup`/`GroupView`. */\nfunction renderSplit(node: SplitNode, ctx: RenderContext): ReactNode {\n\treturn <SplitView key={node.id} node={node} ctx={ctx} />\n}\n\ninterface SplitViewProps {\n\tnode: SplitNode\n\tctx: RenderContext\n}\n\n/** Epsilon (in percentage points) within which two split layouts are treated as\n * equivalent. Guards BOTH sides of the model↔view loop: we skip pushing a\n * `setLayout` when the live layout already matches the target, and skip the\n * `onLayoutChanged` write-back when the incoming layout matches the model — so\n * `setLayout`→`onLayoutChanged`→write-back→re-sync cannot loop. */\nconst LAYOUT_EPSILON = 0.5\n\n/** TIGHT tolerance (percentage points) for the BASIS-NORMALIZED flexible-ratio\n * compare in `handleLayoutChanged`. We normalize the incoming layout's flexible\n * subset to ratios summing to 100 and compare them against the model's flexible\n * ratios (`computeFlexiblePercentages`, also summing to 100). If they match\n * within this tolerance the `onLayoutChanged` is either our own `setLayout` echo\n * OR a ratio-preserving spontaneous re-measure (mount / fixed-px re-pin /\n * container resize) — SKIP the write-back. If they differ it is a genuine user\n * resize (pointer OR keyboard) — WRITE BACK.\n *\n * Set to ~0.1pp: large enough to absorb rrp's ~3-decimal float noise on an echo,\n * yet FAR below a real drag's delta. R1's \"sub-0.5%\" drag moves a panel ~0.33pp\n * of the container; normalized over the two-flexible-child subset that is ~0.66pp\n * of ratio — comfortably above 0.1, so it is NOT mistaken for an echo and is\n * written back.\n *\n * CAVEAT: this is a flexible-RATIO tolerance (the flexible subset normalized to\n * sum 100), NOT a container-%. It is safe against rrp's 3-decimal echo noise.\n * In a pathologically WIDE split (~≥10 flexible children) a single arrow-key\n * nudge (±~5% of the container on one child) can, once normalized over many\n * flexible siblings, fall BELOW 0.1pp of ratio and be skipped — exotic and\n * self-healing (the next, larger resize writes back). If that ever matters,\n * scale the tolerance down by the flexible child count. */\nconst FLEX_RATIO_TOLERANCE = 0.1\n\n/**\n * The minimum weight a FLEXIBLE child may hold, given how many flexible children\n * share the split (Bug #1). A flexible `<Panel>` has no rrp `minSize` by default\n * (rrp floor is 0%), so a user can drag it to ~0 width; the resize write-back\n * would then persist a ~0 weight and the panel comes back invisible/stuck.\n *\n * The floor is `min(1, floor(90 / flexCount))` — i.e. ~1 weight unit (a flexible\n * split's weights are normalized to percentages downstream, so ~1 reads as ~1%\n * of the flexible pool). With any realistic flexible count this is exactly 1; the\n * `min(1, …)` only matters for a hypothetical 90+ flexible-child split. It is the\n * SAME value used for the rrp `minSize` (A) and the write-back clamp (B) so the\n * two defenses never disagree.\n */\nfunction flexibleFloor(flexCount: number): number {\n\t// `floor(90 / flexCount)` is ~1 for any realistic count, but goes to 0 once\n\t// flexCount > 90 — which would silently DEFEAT the floor (a 0 minSize / 0 clamp\n\t// is no floor at all). Clamp into [MIN_POSITIVE_FLOOR, 1] so the floor is always\n\t// a positive percentage even in a pathologically wide split.\n\tconst MIN_POSITIVE_FLOOR = 0.5\n\treturn Math.min(1, Math.max(MIN_POSITIVE_FLOOR, Math.floor(90 / Math.max(1, flexCount))))\n}\n\n/**\n * Clamp the FLEXIBLE entries of a full-length weights array up to `floor` (Bug #1\n * defense B). Px-sized children (a non-null constraint) are left untouched — their\n * `sizes[i]` is a preserved placeholder, not a live weight. Returns a new array;\n * a weight already ≥ floor is kept verbatim so a healthy ratio is undisturbed.\n */\nfunction clampFlexibleWeights(\n\tweights: readonly number[],\n\tconstraints: readonly (SizeConstraint | null)[] | undefined,\n): number[] {\n\tconst flexCount = weights.filter((_, i) => (constraints?.[i] ?? null) === null).length\n\tconst floor = flexibleFloor(flexCount)\n\treturn weights.map((w, i) => {\n\t\tif ((constraints?.[i] ?? null) !== null) return w // px child — leave as-is\n\t\treturn Number.isFinite(w) && w >= floor ? w : floor\n\t})\n}\n\n/** Are two panelId→percentage maps equivalent within `epsilon` percentage\n * points? Both maps must cover EXACTLY the `ids` key set — a missing key OR an\n * extra key (a key present in `a`/`b` but absent from `ids`) counts as NOT\n * equivalent so the sync is not falsely suppressed. A non-finite value\n * (NaN/±Infinity) is likewise NOT equivalent — `typeof NaN === 'number'` and\n * `Math.abs(NaN - x) > eps` is `false`, so without the `Number.isFinite` guard a\n * NaN would slip through as \"equivalent\" and wrongly suppress a legitimate\n * sync/write-back (N1). EXPORTED (pure) for direct unit coverage. */\nexport function layoutsEquivalent(\n\ta: Record<string, number>,\n\tb: Record<string, number>,\n\tids: readonly string[],\n\tepsilon: number = LAYOUT_EPSILON,\n): boolean {\n\t// Exact key-set match: any key in `a` or `b` that is not in `ids` (extra key)\n\t// makes the maps non-equivalent. `ids` is the authoritative compared set.\n\tconst idSet = new Set(ids)\n\tfor (const k of Object.keys(a)) if (!idSet.has(k)) return false\n\tfor (const k of Object.keys(b)) if (!idSet.has(k)) return false\n\tfor (const id of ids) {\n\t\tconst av = a[id]\n\t\tconst bv = b[id]\n\t\tif (!Number.isFinite(av) || !Number.isFinite(bv)) return false\n\t\tif (Math.abs((av as number) - (bv as number)) > epsilon) return false\n\t}\n\treturn true\n}\n\n/**\n * Build the panel-ID→PERCENTAGE map for an imperative `setLayout`, given the\n * model's full-length raw `sizes` (per child) and `constraints`:\n *\n * - FIXED (px-pinned) children keep their CURRENT measured percentage (read\n * from the live `getLayout()`); they are NOT derived from weights, so a\n * flexible-weights change never disturbs their pixel lock.\n * - The REMAINING percentage (100 − Σ fixed%) is distributed across the\n * FLEXIBLE children in proportion to their weights.\n *\n * Returns `null` when the map can't be built faithfully (e.g. `live` is empty —\n * jsdom's stub — or a fixed child's live % is missing), so the caller skips the\n * `setLayout` rather than pushing a corrupt total. The result always sums to\n * ~100 over all children.\n */\nfunction buildSetLayoutMap(\n\tchildIds: readonly string[],\n\tsizes: readonly number[],\n\tconstraints: readonly (SizeConstraint | null)[] | undefined,\n\tlive: Record<string, number>,\n): Record<string, number> | null {\n\tconst isFixedAt = (i: number): boolean => (constraints?.[i] ?? null) !== null\n\n\t// Sum the fixed children's CURRENT live percentages (preserve their px lock).\n\tlet fixedTotal = 0\n\tfor (let i = 0; i < childIds.length; i++) {\n\t\tif (!isFixedAt(i)) continue\n\t\tconst livePct = live[childIds[i]!]\n\t\t// Without a measured live % for a fixed child we cannot preserve its px\n\t\t// lock faithfully — bail so we don't corrupt the fixed child.\n\t\tif (typeof livePct !== 'number') return null\n\t\tfixedTotal += livePct\n\t}\n\n\tconst remaining = Math.max(0, 100 - fixedTotal)\n\n\t// Flexible weight pool.\n\tlet flexWeightTotal = 0\n\tfor (let i = 0; i < childIds.length; i++) {\n\t\tif (isFixedAt(i)) continue\n\t\tconst w = sizes[i] ?? 0\n\t\tflexWeightTotal += w > 0 ? w : 0\n\t}\n\tconst flexibleCount = childIds.length - childIds.filter((_, i) => isFixedAt(i)).length\n\n\tconst out: Record<string, number> = {}\n\tfor (let i = 0; i < childIds.length; i++) {\n\t\tconst id = childIds[i]!\n\t\tif (isFixedAt(i)) {\n\t\t\tout[id] = live[id]!\n\t\t\tcontinue\n\t\t}\n\t\tif (flexWeightTotal <= 0) {\n\t\t\t// Degenerate flexible weights → split the remaining space evenly.\n\t\t\tout[id] = flexibleCount > 0 ? remaining / flexibleCount : remaining\n\t\t\tcontinue\n\t\t}\n\t\tconst w = sizes[i] ?? 0\n\t\tout[id] = (remaining * (w > 0 ? w : 0)) / flexWeightTotal\n\t}\n\treturn out\n}\n\n/**\n * Extract an rrp `onLayoutChanged` map's FLEXIBLE subset (skipping fixed-px\n * children) in child order and NORMALIZE it by its own sum to ratios summing to\n * ~100 — the basis `computeFlexiblePercentages` already produces for the model,\n * so the two are directly comparable. Returns the original child `indices`\n * alongside the `ratios`. Null when the map is malformed (a flexible id is\n * missing / non-finite) or there is no flexible child — the caller then never\n * writes back from it.\n */\nfunction incomingFlexRatios(\n\tchildIds: readonly string[],\n\tconstraints: readonly (SizeConstraint | null)[] | undefined,\n\tlayout: Record<string, number>,\n): { indices: number[]; ratios: number[] } | null {\n\tconst indices: number[] = []\n\tconst raw: number[] = []\n\tfor (let i = 0; i < childIds.length; i++) {\n\t\tif ((constraints?.[i] ?? null) !== null) continue\n\t\tconst v = layout[childIds[i]!]\n\t\tif (typeof v !== 'number' || !Number.isFinite(v)) return null\n\t\tindices.push(i)\n\t\traw.push(v > 0 ? v : 0)\n\t}\n\tif (indices.length === 0) return null\n\treturn { indices, ratios: toPercentages(raw) }\n}\n\nfunction SplitView(props: SplitViewProps): ReactNode {\n\tconst { node, ctx } = props\n\tconst orientation = node.orientation === 'row' ? 'horizontal' : 'vertical'\n\n\t// A child is FIXED (px-pinned) iff it carries a non-null constraint. Fixed\n\t// children are excluded from the flexible-percentage pool: their px panel does\n\t// not participate in weight-based sizing, so polluting `toPercentages` with\n\t// their weight would skew the flexible siblings' defaultSize.\n\t// `computeFlexiblePercentages` therefore normalizes percentages over the\n\t// FLEXIBLE indices only and maps them back by index; fixed children keep their\n\t// px defaultSize/min/max. (Both write-back seams derive fixed-ness from\n\t// `nodeRef.current.constraints` directly — see `handleLayoutChanged` and\n\t// `__deckApplyLayout` — so no render-closure `isFixed` helper is needed here.)\n\tconst percentageByIndex = computeFlexiblePercentages(node.sizes, node.constraints)\n\n\t// Bug #1 defense A: the minimum % a flexible child may shrink to. rrp's default\n\t// flexible `minSize` is 0% (a panel can be dragged to nothing); this floors it\n\t// so the simulator/editor can never be pulled to 0 width. Unitless string =\n\t// percentage (same convention as `defaultSize` above; a px `minSize` next to a\n\t// %-`defaultSize` is misparsed by rrp). The floor is keyed on the COUNT of\n\t// flexible children in THIS split (the same value the write-back clamp uses).\n\tconst flexCount = node.children.filter((_, i) => (node.constraints?.[i] ?? null) === null).length\n\tconst flexMinSize = String(flexibleFloor(flexCount))\n\n\t// ── M1 model→view sync plumbing ────────────────────────────────────────\n\t// The rrp Group's imperative handle (getLayout/setLayout) — the seam through\n\t// which the model becomes the source of truth for the visible split ratio.\n\tconst groupRef = useRef<GroupImperativeHandle | null>(null)\n\t// Keep the latest node reachable from the imperative ref + drag callbacks\n\t// without re-binding the ref on every render. The sync effect reads the\n\t// freshest `node` directly (it re-runs when `node.sizes` change), so it does\n\t// not go stale; this ref is for the imperative seams that capture once.\n\tconst nodeRef = useRef(node)\n\tnodeRef.current = node\n\n\tconst items: ReactNode[] = []\n\tnode.children.forEach((child, i) => {\n\t\tif (i > 0) {\n\t\t\titems.push(\n\t\t\t\t<Separator\n\t\t\t\t\tkey={`handle-${i}`}\n\t\t\t\t\tdata-deck-resize-handle=\"\"\n\t\t\t\t/>,\n\t\t\t)\n\t\t}\n\t\t// PIXEL-sized children (`fixedPx`/`minPx`) are sized in px, NOT weights, so\n\t\t// they are excluded from the flexible % pool (`computeFlexiblePercentages`)\n\t\t// and never normalized against weight-sized siblings. rrp needs CONSISTENT\n\t\t// units per panel — a px `minSize` on a %-`defaultSize` panel is misparsed\n\t\t// (the panel grabs the whole region), so a px floor MUST pair with a px\n\t\t// `defaultSize`.\n\t\tconst constraint = node.constraints?.[i] ?? null\n\t\tif (constraint?.fixedPx != null) {\n\t\t\t// `fixedPx` LOCKS the child: min===max + `preserve-pixel-size` keeps it\n\t\t\t// fixed while flexible siblings absorb group resizes.\n\t\t\tconst px = `${constraint.fixedPx}px`\n\t\t\titems.push(\n\t\t\t\t<Panel\n\t\t\t\t\tkey={panelKey(child)}\n\t\t\t\t\tid={child.id}\n\t\t\t\t\tdefaultSize={px}\n\t\t\t\t\tminSize={px}\n\t\t\t\t\tmaxSize={px}\n\t\t\t\t\tgroupResizeBehavior=\"preserve-pixel-size\"\n\t\t\t\t>\n\t\t\t\t\t{renderNode(child, ctx)}\n\t\t\t\t</Panel>,\n\t\t\t)\n\t\t}\n\t\telse if (constraint?.minPx != null) {\n\t\t\t// `minPx` FLOORS the child at a pixel minimum but leaves it DRAGGABLE\n\t\t\t// above it: px `defaultSize`+`minSize` (starts AT the floor, can't shrink\n\t\t\t// past it), NO `maxSize` so the user can widen it. `preserve-pixel-size`\n\t\t\t// puts the panel in rrp's pixel mode — WITHOUT it rrp misparses a px\n\t\t\t// `defaultSize` next to %-sized siblings and the panel grabs ~80% of the\n\t\t\t// region. It only governs WINDOW-resize behavior (keep pixel width, don't\n\t\t\t// scale); the user can still drag the separator to resize it (≥ minSize).\n\t\t\tconst px = `${constraint.minPx}px`\n\t\t\titems.push(\n\t\t\t\t<Panel\n\t\t\t\t\tkey={panelKey(child)}\n\t\t\t\t\tid={child.id}\n\t\t\t\t\tdefaultSize={px}\n\t\t\t\t\tminSize={px}\n\t\t\t\t\tgroupResizeBehavior=\"preserve-pixel-size\"\n\t\t\t\t>\n\t\t\t\t\t{renderNode(child, ctx)}\n\t\t\t\t</Panel>,\n\t\t\t)\n\t\t}\n\t\telse {\n\t\t\t// `defaultSize` seeds the FLEXIBLE child at mount; post-mount the model\n\t\t\t// becomes the source of truth via the imperative `setLayout` driven from\n\t\t\t// the sync effect below (M1). The value is passed as a STRING percentage:\n\t\t\t// rrp treats a NUMBER `defaultSize` as PIXELS, so `defaultSize={70}` would\n\t\t\t// be 70px (not 70%) — which mis-proportions a flexible sibling next to a\n\t\t\t// px-sized (`fixedPx`/`minPx`) panel and the sync then preserves the wrong\n\t\t\t// size. A unitless string is parsed as a percentage.\n\t\t\tconst pct = percentageByIndex.get(i)\n\t\t\titems.push(\n\t\t\t\t<Panel\n\t\t\t\t\tkey={panelKey(child)}\n\t\t\t\t\tid={child.id}\n\t\t\t\t\tdefaultSize={pct != null ? String(pct) : undefined}\n\t\t\t\t\tminSize={flexMinSize}\n\t\t\t\t>\n\t\t\t\t\t{renderNode(child, ctx)}\n\t\t\t\t</Panel>,\n\t\t\t)\n\t\t}\n\t})\n\n\t// A real rrp resize (pointer OR keyboard) — or our own `setLayout` echo / a\n\t// spontaneous re-measure — commits new per-Panel percentages here. The SINGLE\n\t// discriminator is a BASIS-NORMALIZED flexible-ratio compare (no gate, no echo\n\t// token): write back IFF the incoming layout's FLEXIBLE-child ratios differ from\n\t// the model's. `incomingFlexRatios` normalizes rrp's CONTAINER-% over the\n\t// flexible subset; `computeFlexiblePercentages` already does the same for the\n\t// model — so both bases sum to 100 and are directly comparable.\n\tconst handleLayoutChanged = (layout: Layout): void => {\n\t\tconst cur = nodeRef.current\n\t\tconst curChildIds = cur.children.map((c) => c.id)\n\t\tconst isFixedAt = (i: number): boolean => (cur.constraints?.[i] ?? null) !== null\n\n\t\tconst incoming = incomingFlexRatios(curChildIds, cur.constraints, layout)\n\t\tif (!incoming) return // malformed / no flexible child → never write back\n\t\tconst modelPctByIndex = computeFlexiblePercentages(cur.sizes, cur.constraints)\n\t\tconst modelNorm = incoming.indices.map((i) => modelPctByIndex.get(i) ?? 0)\n\n\t\t// Equal within a TIGHT tolerance → our own `setLayout` echo OR a\n\t\t// ratio-preserving spontaneous re-measure (mount / fixed-px re-pin / container\n\t\t// resize). SKIP: no model churn, no loop, no R2 corruption. Otherwise it is a\n\t\t// genuine user resize → WRITE BACK.\n\t\tconst differs = incoming.ratios.some(\n\t\t\t(r, k) => Math.abs(r - modelNorm[k]!) > FLEX_RATIO_TOLERANCE,\n\t\t)\n\t\tif (!differs) return\n\n\t\t// FULL-LENGTH weights (setSizes requires length === children.length). FIXED\n\t\t// children keep the model's existing weight — rrp reports a container-derived %\n\t\t// for the pinned panel which, written back, would corrupt the stored weight and\n\t\t// lose it when the constraint is later cleared. FLEXIBLE children take rrp's.\n\t\tconst weights = curChildIds.map((id, i) =>\n\t\t\tisFixedAt(i) ? cur.sizes[i] ?? 1 : layout[id]!,\n\t\t)\n\t\t// Bug #1 defense B: a flexible child dragged to ~0 width reports a ~0 ratio;\n\t\t// floor it before persisting so a 0-width weight can never reach the model\n\t\t// (the rrp `minSize` from A is the first defense, this is the write-back\n\t\t// backstop). Px children are untouched (clampFlexibleWeights skips them).\n\t\tctx.onApplyLayout(cur.id, clampFlexibleWeights(weights, cur.constraints))\n\t}\n\n\t// Ref callback exposing the resize write-back seam + the rrp Group imperative\n\t// handle on the split element. A drag round-trip (e2e) and the unit seam both\n\t// land on the same engine path; the `__deckGroupApi` handle lets hosts/tests\n\t// read the LIVE split layout (M1 model→view sync seam).\n\tconst setSplitRef = (el: DeckSplitElement | null): void => {\n\t\tif (el) {\n\t\t\tel.__deckApplyLayout = (weights: number[]) => {\n\t\t\t\t// Mirror handleLayoutChanged: a fixed child's stored weight is never\n\t\t\t\t// overwritten by an incoming (container-derived) value — preserve\n\t\t\t\t// node.sizes[i] for fixed indices so clearing the constraint later\n\t\t\t\t// restores the original weight. Flexible indices take the supplied value.\n\t\t\t\t// Derive fixed-ness from `nodeRef.current.constraints` (the SAME fresh\n\t\t\t\t// node we read sizes from) so this seam has no fresh-sizes/stale-constraints\n\t\t\t\t// asymmetry. Mirrors `isFixedAt` in handleLayoutChanged.\n\t\t\t\tconst cur = nodeRef.current\n\t\t\t\tconst isFixedAt = (i: number): boolean => (cur.constraints?.[i] ?? null) !== null\n\t\t\t\tconst next = weights.map((w, i) => (isFixedAt(i) ? cur.sizes[i] ?? 1 : w))\n\t\t\t\t// Bug #1 defense B (same clamp as handleLayoutChanged): floor flexible\n\t\t\t\t// weights so a near-0 drag can never persist a 0-width panel.\n\t\t\t\tctx.onApplyLayout(cur.id, clampFlexibleWeights(next, cur.constraints))\n\t\t\t}\n\t\t\t// Expose the rrp Group imperative handle (getLayout/setLayout) so a host —\n\t\t\t// and the jsdom `[M1-seam-live]` test — can reach the live split layout.\n\t\t\t// May be null until the Group mounts its handle; the getter reads the\n\t\t\t// freshest ref each call.\n\t\t\tObject.defineProperty(el, '__deckGroupApi', {\n\t\t\t\tconfigurable: true,\n\t\t\t\tget: () => groupRef.current ?? undefined,\n\t\t\t})\n\t\t}\n\t}\n\n\t// ── M1 model→view sync ─────────────────────────────────────────────────\n\t// Push the model's FLEXIBLE weights into the live Group via `setLayout` so the\n\t// model is the source of truth for the visible split post-mount (without it rrp\n\t// freezes at the mount-time `defaultSize`). Reads the FRESHEST `node` from the\n\t// ref so an external `setSizes` syncs against the latest model. Returns true if\n\t// a `setLayout` was actually pushed.\n\tconst runSync = useCallback((): boolean => {\n\t\tconst api = groupRef.current\n\t\tif (!api) return false\n\t\tconst cur = nodeRef.current\n\t\tconst curChildIds = cur.children.map((c) => c.id)\n\n\t\tconst live = api.getLayout()\n\t\t// jsdom's stub returns `{}` (no measured geometry) — buildSetLayoutMap then\n\t\t// returns null for any fixed child, and for the all-flexible case the\n\t\t// equivalence check below sees missing live ids and proceeds; but `setLayout`\n\t\t// is a no-op under jsdom anyway. In a real renderer `live` is populated.\n\t\tconst targetMap = buildSetLayoutMap(curChildIds, cur.sizes, cur.constraints, live)\n\t\tif (!targetMap) return false\n\n\t\t// SET-side redundant-push skip: don't re-push a layout the live Group already\n\t\t// satisfies. This is the loop break on the SET side — pushing an identical\n\t\t// layout would re-emit `onLayoutChanged`, but its flexible ratios match the\n\t\t// model so the normalized compare in `handleLayoutChanged` skips the write-back\n\t\t// anyway; this skip just avoids the redundant work.\n\t\tif (layoutsEquivalent(live, targetMap, curChildIds)) return false\n\n\t\tapi.setLayout(targetMap)\n\t\treturn true\n\t}, [])\n\n\t// Re-run the sync whenever the model's raw weights change (every external\n\t// `setSizes`). The model is stable DURING a user drag — the write-back lands on\n\t// release — so this effect does not fire mid-drag and never fights the live\n\t// pointer snapshot. An external `setSizes` (incl. one after an away-and-back\n\t// drag) syncs immediately (R3): there is no drag flag to get stuck.\n\tconst sizesKey = node.sizes.join(',')\n\t// Child COUNT is part of the sync key (Bug #2 follow-up): the `key={children.\n\t// length}` on the Group remounts it on a cardinality change, and a remount\n\t// reseeds the rrp layout from each Panel's `defaultSize` — for a `minPx` column\n\t// that is the floor (the device-min), NOT the user's last-dragged width. Re-\n\t// running `runSync` after the remount re-pushes the model's stored weights into\n\t// the fresh Group so a surviving split's proportions are restored rather than\n\t// snapping back to the per-Panel defaults. A pure weight resize (same count)\n\t// already re-syncs via `sizesKey`; this only adds the count edge.\n\tconst childCountKey = node.children.length\n\tuseEffect(() => {\n\t\trunSync()\n\t\t// Keyed on `sizesKey` + `childCountKey`; `runSync` reads `node`/`childIds`/\n\t\t// `constraints` fresh from the ref, so they cannot go stale relative to either.\n\t}, [sizesKey, childCountKey, runSync])\n\n\t// The split-level `data-*` attributes ride on an outer wrapper rather than\n\t// the Group itself, so hosts/tests can target them regardless of how the\n\t// library forwards unknown props. `data-deck-sizes` mirrors the model's\n\t// CURRENT raw weights so the render is a function of model.sizes.\n\treturn (\n\t\t<div\n\t\t\tref={setSplitRef}\n\t\t\tdata-deck-split={node.id}\n\t\t\tdata-orientation={node.orientation}\n\t\t\tdata-deck-sizes={node.sizes.join(',')}\n\t\t\tstyle={{ width: '100%', height: '100%' }}\n\t\t>\n\t\t\t<Group\n\t\t\t\t// Bug #2 (white-screen crash guard): KEY the Group on its child COUNT.\n\t\t\t\t// rrp v4.10 caches the previous layout in a ref keyed by group id; on a\n\t\t\t\t// render where the child count CHANGES (a panel closed/split — the split\n\t\t\t\t// id stays 'root' so the Group instance is otherwise reused), rrp's\n\t\t\t\t// commit-phase layout effect synchronously validates the STALE\n\t\t\t\t// (old-length) cached layout against the NEW (different-length)\n\t\t\t\t// constraints and throws `Invalid N panel layout`. That throw escapes\n\t\t\t\t// SplitView during commit and unmounts the whole tree (white screen).\n\t\t\t\t// Keying on `node.children.length` remounts the Group with a fresh\n\t\t\t\t// internal layout sized to the new child count ONLY when the count\n\t\t\t\t// changes — an ordinary weight resize never changes the key, so this\n\t\t\t\t// adds NO remount on drags. The model→view sync (runSync) re-binds\n\t\t\t\t// `groupRef` on remount and re-pushes the model's weights via the\n\t\t\t\t// `sizesKey` effect; the simulator/console native overlay re-anchors via\n\t\t\t\t// its slot ref callback (same path as a tab switch). See\n\t\t\t\t// dock-view-robustness.test.tsx \"3 → 2\" for the regression.\n\t\t\t\tkey={node.children.length}\n\t\t\t\tgroupRef={groupRef}\n\t\t\t\torientation={orientation}\n\t\t\t\tonLayoutChanged={handleLayoutChanged}\n\t\t\t\tstyle={{ width: '100%', height: '100%' }}\n\t\t\t>\n\t\t\t\t{items}\n\t\t\t</Group>\n\t\t</div>\n\t)\n}\n\n/** Stable React key for a child node (split id or group id). */\nfunction panelKey(node: LayoutNode): string {\n\treturn node.id\n}\n\nfunction renderGroup(node: TabGroupNode, ctx: RenderContext): ReactNode {\n\t// A group is a stateful component (it tracks the live drop-hover zone during a\n\t// drag), so render it via JSX with a STABLE key so React preserves that hover\n\t// state across model emissions rather than remounting on every snapshot.\n\treturn <GroupView key={node.id} node={node} ctx={ctx} />\n}\n\ninterface GroupViewProps {\n\tnode: TabGroupNode\n\tctx: RenderContext\n}\n\n/**\n * One tab GROUP: tab strip (draggable tabs) + active body + the imperative\n * `__deckHandleDrop` seam + a geometry-driven drop indicator while a drag hovers.\n */\nfunction GroupView(props: GroupViewProps): ReactNode {\n\tconst { node, ctx } = props\n\n\t// The live drop zone under the pointer during a drag-over (null = no drag over\n\t// this group). Drives the `data-deck-drop-zone` indicator. jsdom can't produce\n\t// real geometry (getBoundingClientRect is 0), so this path is exercised by the\n\t// real-pointer e2e `it.todo`s; here we implement it for real-browser fidelity.\n\tconst [dropZone, setDropZone] = useState<DropZone | null>(null)\n\n\t// Keep the latest node + redock callback reachable from the imperative ref\n\t// seam without re-running the ref effect when they change identity. The seam\n\t// always anchors a split at the group's CURRENT active panel.\n\tconst nodeRef = useRef(node)\n\tnodeRef.current = node\n\tconst redockRef = useRef(ctx.onRedock)\n\tredockRef.current = ctx.onRedock\n\n\t// Ref-callback exposing the drop seam on the group element. Mirrors\n\t// `setSplitRef`/`__deckApplyLayout`: the gesture and the unit seam both land on\n\t// the same `onRedock` engine path. The seam owns choosing the target panel —\n\t// the group's active panel is the natural anchor for an edge split.\n\tconst setGroupRef = (el: DeckGroupElement | null): void => {\n\t\tif (el) {\n\t\t\tel.__deckHandleDrop = (draggedPanelId: string, zone: DropZone) => {\n\t\t\t\tconst current = nodeRef.current\n\t\t\t\tredockRef.current(current.id, current.active, draggedPanelId, zone)\n\t\t\t}\n\t\t}\n\t}\n\n\t// ── drag-over geometry ────────────────────────────────────────────────\n\t// Compute the hovered zone from the pointer position relative to the group\n\t// rect. `preventDefault` is required for the element to be a valid drop target.\n\tconst handleDragOver = (e: DragEvent<HTMLDivElement>): void => {\n\t\te.preventDefault()\n\t\t// Opt-in (suppressReorderOnlyDropIndicator): a `reorder-only` panel can ONLY\n\t\t// land via a within-group tab-strip reorder (handled by the strip handlers,\n\t\t// which paint no indicator). Anywhere the group body would resolve to — its\n\t\t// own edges or ANY other group — is a no-op for it, so the blue drop\n\t\t// highlight there is misleading. When opted in, suppress it entirely while\n\t\t// such a panel is in flight. Default keeps the geometry-only indicator (the\n\t\t// capability gate still rejects the drop at drop time).\n\t\tif (\n\t\t\tctx.suppressReorderOnlyDropIndicator\n\t\t\t&& activeDragPanelId !== null\n\t\t\t&& ctx.registry.get(activeDragPanelId)?.dropPolicy === 'reorder-only'\n\t\t) {\n\t\t\tsetDropZone(null)\n\t\t\treturn\n\t\t}\n\t\tconst rect = e.currentTarget.getBoundingClientRect()\n\t\tconst zone = computeDropZone(\n\t\t\t{ width: rect.width, height: rect.height },\n\t\t\t{ x: e.clientX - rect.left, y: e.clientY - rect.top },\n\t\t)\n\t\tsetDropZone(zone)\n\t}\n\n\tconst handleDragLeave = (): void => {\n\t\tsetDropZone(null)\n\t}\n\n\t// On drop, recover the dragged panel id (custom MIME first, falling back to\n\t// the source tab marker carried as text), then commit via the same seam path.\n\tconst handleDrop = (e: DragEvent<HTMLDivElement>): void => {\n\t\te.preventDefault()\n\t\tconst rect = e.currentTarget.getBoundingClientRect()\n\t\tconst zone = computeDropZone(\n\t\t\t{ width: rect.width, height: rect.height },\n\t\t\t{ x: e.clientX - rect.left, y: e.clientY - rect.top },\n\t\t)\n\t\tsetDropZone(null)\n\t\tconst dragged\n\t\t\t= e.dataTransfer.getData(DRAG_PANEL_MIME) || e.dataTransfer.getData('text/plain')\n\t\t// Validate the payload before committing a re-dock:\n\t\t// - M4: a `text/plain` drop can carry arbitrary external text (a selection,\n\t\t// a file path); it must at least be a REGISTERED panel.\n\t\t// - M2: registry membership is NOT tree membership. A registered-but-absent\n\t\t// panel (closed out of the tree / never docked) passes the registry guard\n\t\t// yet drives `movePanel`/`extractPanel` on an id missing from the tree,\n\t\t// which THROWS `panel not found`. It must be a NO-OP — also require the id\n\t\t// to be present in the CURRENT tree.\n\t\tif (!dragged || ctx.registry.get(dragged) === undefined || !ctx.isPanelInTree(dragged)) return\n\t\t// Pointer-derived insertion index for a within-group REORDER\n\t\t// (`dropPolicy:'reorder-only'`): measure this group's tab rects and map the\n\t\t// pointer x onto an index. Ignored by `handleRedock` for every non-reorder\n\t\t// path, so it is safe to compute unconditionally.\n\t\tconst tabRects = Array.from(\n\t\t\te.currentTarget.querySelectorAll<HTMLElement>('[data-deck-tab]'),\n\t\t).map((el) => {\n\t\t\tconst r = el.getBoundingClientRect()\n\t\t\treturn { left: r.left, width: r.width }\n\t\t})\n\t\tconst reorderIndex = computeReorderIndex(tabRects, e.clientX)\n\t\tctx.onRedock(node.id, node.active, dragged, zone, reorderIndex)\n\t}\n\n\t// The tab STRIP is a first-class REORDER drop target. It sits at the TOP of\n\t// the group, so a tab dropped onto it would otherwise resolve to the group's\n\t// `top` EDGE zone via `computeDropZone` — which a `reorder-only` panel rejects\n\t// as a no-op, so reordering by dragging within the tab bar would never work.\n\t// Intercept strip drops here and commit a `center` re-dock carrying the\n\t// pointer-x insertion index (`handleRedock` reorders within the group).\n\t// `stopPropagation` keeps the group's edge/center drop handlers from also\n\t// firing for the same gesture.\n\tconst handleTabStripDragOver = (e: DragEvent<HTMLDivElement>): void => {\n\t\tif (!e.dataTransfer.types.includes(DRAG_PANEL_MIME)) return\n\t\te.preventDefault()\n\t\te.stopPropagation()\n\t\tsetDropZone(null)\n\t}\n\tconst handleTabStripDrop = (e: DragEvent<HTMLDivElement>): void => {\n\t\te.preventDefault()\n\t\te.stopPropagation()\n\t\tsetDropZone(null)\n\t\tconst dragged\n\t\t\t= e.dataTransfer.getData(DRAG_PANEL_MIME) || e.dataTransfer.getData('text/plain')\n\t\tif (!dragged || ctx.registry.get(dragged) === undefined || !ctx.isPanelInTree(dragged)) return\n\t\tconst tabRects = Array.from(\n\t\t\te.currentTarget.querySelectorAll<HTMLElement>('[data-deck-tab]'),\n\t\t).map((el) => {\n\t\t\tconst r = el.getBoundingClientRect()\n\t\t\treturn { left: r.left, width: r.width }\n\t\t})\n\t\tconst reorderIndex = computeReorderIndex(tabRects, e.clientX)\n\t\tctx.onRedock(node.id, node.active, dragged, 'center', reorderIndex)\n\t}\n\n\t// Panels that contribute a tab to the strip (`hideTab` panels carry their own\n\t// chrome — e.g. the simulator's device picker — so the engine tab is omitted).\n\t// When NONE remain the tab strip is not rendered at all and the body fills the\n\t// whole group region.\n\tconst visibleTabs = node.panels.filter((panelId) => !ctx.registry.get(panelId)?.hideTab)\n\n\t// FILL LAYOUT (FIX 2a): the group must be a flex COLUMN that fills its\n\t// allotted panel region so a leaf native slot can stretch to the full area.\n\t// Without this the group div is content-height (the tab strip only), the\n\t// active body / NativeSlot measures 0 height, the view-anchor publishes a\n\t// collapsed rect, and the simulator WebContentsView is invisible. The tab\n\t// strip is `shrink: 0`; the active body takes the remaining space (flex: 1).\n\treturn (\n\t\t<div\n\t\t\tref={setGroupRef}\n\t\t\tdata-deck-group={node.id}\n\t\t\tonDragOver={handleDragOver}\n\t\t\tonDragLeave={handleDragLeave}\n\t\t\tonDrop={handleDrop}\n\t\t\tstyle={{\n\t\t\t\tposition: 'relative',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\twidth: '100%',\n\t\t\t\theight: '100%',\n\t\t\t\tminWidth: 0,\n\t\t\t\tminHeight: 0,\n\t\t\t}}\n\t\t>\n\t\t\t{visibleTabs.length > 0 ? (\n\t\t\t\t<div\n\t\t\t\trole=\"tablist\"\n\t\t\t\tstyle={{ flexShrink: 0 }}\n\t\t\t\tonDragOver={handleTabStripDragOver}\n\t\t\t\tonDrop={handleTabStripDrop}\n\t\t\t>\n\t\t\t\t{visibleTabs.map((panelId) => {\n\t\t\t\t\tconst active = panelId === node.active\n\t\t\t\t\tconst descriptor = ctx.registry.get(panelId)\n\t\t\t\t\tconst title = descriptor?.title ?? panelId\n\t\t\t\t\t// PanelCapabilities (GOAL A source): a `draggable:false` panel's tab\n\t\t\t\t\t// cannot be picked up — omit the marker entirely (an absent attribute,\n\t\t\t\t\t// not `draggable=\"false\"`, matches the \"no drag source\" contract).\n\t\t\t\t\tconst isDraggable = descriptor?.draggable !== false\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tkey={panelId}\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\tdraggable={isDraggable ? 'true' : undefined}\n\t\t\t\t\t\t\tdata-deck-tab={panelId}\n\t\t\t\t\t\t\tdata-active={active ? 'true' : 'false'}\n\t\t\t\t\t\t\tonDragStart={(e) => {\n\t\t\t\t\t\t\t\t// A press that BEGINS on the close affordance must not drag the\n\t\t\t\t\t\t\t\t// whole tab: the HTML5 drag source is the draggable ANCESTOR\n\t\t\t\t\t\t\t\t// (this tab) regardless of which descendant the pointer landed\n\t\t\t\t\t\t\t\t// on, so a descendant's stopPropagation cannot cancel it —\n\t\t\t\t\t\t\t\t// detect the origin here and abort the drag. `closest` walks up\n\t\t\t\t\t\t\t\t// from the event target; a hit means the gesture started on ×.\n\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\te.target instanceof Element\n\t\t\t\t\t\t\t\t\t&& e.target.closest('[data-deck-tab-close]') !== null\n\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\te.preventDefault()\n\t\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Record the dragged panel id so the drop target can recover it.\n\t\t\t\t\t\t\t\te.dataTransfer.setData(DRAG_PANEL_MIME, panelId)\n\t\t\t\t\t\t\t\te.dataTransfer.setData('text/plain', panelId)\n\t\t\t\t\t\t\t\te.dataTransfer.effectAllowed = 'move'\n\t\t\t\t\t\t\t\t// Also expose it to the dragover indicator path (DataTransfer\n\t\t\t\t\t\t\t\t// values are unreadable during dragover).\n\t\t\t\t\t\t\t\tactiveDragPanelId = panelId\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tonDragEnd={() => {\n\t\t\t\t\t\t\t\tactiveDragPanelId = null\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tif (!active) ctx.onActivate(node.id, panelId)\n\t\t\t\t\t\t\t\telse ctx.onActiveTabClick?.(panelId)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{title}\n\t\t\t\t\t\t\t{/* Close affordance — a `role=\"button\"` SPAN (NOT a nested <button>:\n\t\t\t\t\t\t\t an interactive button may not be a descendant of another button —\n\t\t\t\t\t\t\t invalid HTML + illegal a11y nesting). `tabIndex={0}` + the\n\t\t\t\t\t\t\t Enter/Space keydown keep it keyboard/AT-operable. Suppressed when\n\t\t\t\t\t\t\t the WHOLE tree has one panel left (`canClose` false). Activate is\n\t\t\t\t\t\t\t kept off the tab (click stopPropagation) and a tab drag that\n\t\t\t\t\t\t\t begins here is cancelled by the tab's onDragStart guard above. */}\n\t\t\t\t\t\t\t{ctx.canClose && descriptor?.closable !== false\n\t\t\t\t\t\t\t\t? (\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\trole=\"button\"\n\t\t\t\t\t\t\t\t\t\ttabIndex={0}\n\t\t\t\t\t\t\t\t\t\tdata-deck-tab-close={panelId}\n\t\t\t\t\t\t\t\t\t\taria-label={`Close ${title}`}\n\t\t\t\t\t\t\t\t\t\tonPointerDown={(e) => e.stopPropagation()}\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault()\n\t\t\t\t\t\t\t\t\t\t\tctx.onClose(panelId)\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\t\t\t\t\tif (e.key === 'Enter' || e.key === ' ') {\n\t\t\t\t\t\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault()\n\t\t\t\t\t\t\t\t\t\t\t\tctx.onClose(panelId)\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t×\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t)\n\t\t\t\t})}\n\t\t\t</div>\n\t\t\t) : null}\n\t\t\t{renderActiveBody(node, ctx)}\n\t\t\t{dropZone ? <DropIndicator zone={dropZone} /> : null}\n\t\t</div>\n\t)\n}\n\n/**\n * Translate a drop zone into the indicator overlay's geometry. `center` covers\n * the whole group (a tab-join highlight); each edge zone is a HALF-band ribbon\n * pinned to that edge. Pure presentation — the host can re-skin via the\n * `data-deck-drop-zone` attribute; these inline styles are a sane default.\n */\nfunction dropIndicatorStyle(zone: DropZone): Record<string, string> {\n\tconst base: Record<string, string> = {\n\t\tposition: 'absolute',\n\t\t// `none` so the overlay never steals the drag-over/drop events from the\n\t\t// group beneath it (otherwise the indicator itself would become the target).\n\t\tpointerEvents: 'none',\n\t\tbackground: 'rgba(64, 128, 255, 0.25)',\n\t\toutline: '2px solid rgba(64, 128, 255, 0.6)',\n\t}\n\tswitch (zone) {\n\t\tcase 'left':\n\t\t\treturn { ...base, left: '0', top: '0', width: '50%', height: '100%' }\n\t\tcase 'right':\n\t\t\treturn { ...base, right: '0', top: '0', width: '50%', height: '100%' }\n\t\tcase 'top':\n\t\t\treturn { ...base, left: '0', top: '0', width: '100%', height: '50%' }\n\t\tcase 'bottom':\n\t\t\treturn { ...base, left: '0', bottom: '0', width: '100%', height: '50%' }\n\t\tcase 'center':\n\t\tdefault:\n\t\t\treturn { ...base, left: '0', top: '0', width: '100%', height: '100%' }\n\t}\n}\n\n/** The drop-zone highlight rendered over a group during a drag-over. */\nfunction DropIndicator(props: { zone: DropZone }): ReactNode {\n\treturn <div data-deck-drop-zone={props.zone} style={dropIndicatorStyle(props.zone)} />\n}\n\n/**\n * Render a tab group's body region under DOM-panel KEEPALIVE.\n *\n * - DOM panels: ALL of the group's DOM panels are mounted SIMULTANEOUSLY, each\n * under a STABLE React key (`dom-${panelId}`) so switching the active tab never\n * remounts a body — React state + scroll persist across A→B→A. The active body\n * fills the region (flex:1); inactive ones stay in the DOM but `display:none`.\n * Each body's host renderer receives `{ active }` and is re-invoked with the\n * new flag on every activation change (no remount), so a host can fire\n * on-activation side effects off the false→true edge.\n * - Native panels: EXEMPT from keepalive. The single ACTIVE native panel mounts a\n * `NativeSlot` (keyed on the active id, so deactivation unmounts it and fires\n * `bindNativeSlot(id, null)`); inactive native panels render nothing. Keeping a\n * bound native slot mounted-but-hidden would collapse its WebContentsView rect.\n */\nfunction renderActiveBody(node: TabGroupNode, ctx: RenderContext): ReactNode {\n\tconst activeId = node.active\n\tif (!activeId) return null\n\n\tconst activeDescriptor = ctx.registry.get(activeId)\n\n\t// Native active panel → active-only NativeSlot (no keepalive).\n\tconst nativeSlot\n\t\t= activeDescriptor?.kind === 'native'\n\t\t\t? (\n\t\t\t\t<NativeSlot\n\t\t\t\t\tkey={activeId}\n\t\t\t\t\tpanelId={activeId}\n\t\t\t\t\tbindNativeSlot={ctx.bindNativeSlot}\n\t\t\t\t/>\n\t\t\t\t)\n\t\t\t: null\n\n\t// Every DOM (non-native) panel in the group is kept alive: mounted up-front,\n\t// hidden unless active. A panel with no descriptor is treated as DOM (render\n\t// via the host's renderer) — matching the pre-keepalive fallback.\n\tconst domBodies = node.panels\n\t\t.filter((panelId) => ctx.registry.get(panelId)?.kind !== 'native')\n\t\t.map((panelId) => {\n\t\t\tconst active = panelId === activeId\n\t\t\treturn (\n\t\t\t\t<div\n\t\t\t\t\tkey={`dom-${panelId}`}\n\t\t\t\t\tdata-deck-panel-body={panelId}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\t// Active body fills the region; inactive bodies stay mounted but\n\t\t\t\t\t\t// hidden (display:none preserves React state + scroll position).\n\t\t\t\t\t\t// A flex COLUMN container (not a bare block) so a host panel root\n\t\t\t\t\t\t// that fills via `flex:1`/`height:100%` actually stretches to the\n\t\t\t\t\t\t// full body height — without this such a root collapses to content\n\t\t\t\t\t\t// height and leaves dead space below it.\n\t\t\t\t\t\tdisplay: active ? 'flex' : 'none',\n\t\t\t\t\t\tflexDirection: 'column',\n\t\t\t\t\t\tflex: 1,\n\t\t\t\t\t\tminWidth: 0,\n\t\t\t\t\t\tminHeight: 0,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{ctx.renderDomPanel(panelId, { active })}\n\t\t\t\t</div>\n\t\t\t)\n\t\t})\n\n\treturn (\n\t\t<Fragment>\n\t\t\t{nativeSlot}\n\t\t\t{domBodies}\n\t\t</Fragment>\n\t)\n}\n\ninterface NativeSlotProps {\n\tpanelId: string\n\tbindNativeSlot: (panelId: string, el: HTMLElement | null) => void\n}\n\n/**\n * Empty anchor slot for an ACTIVE native panel. A ref-callback binds the live\n * element on mount and unbinds (`null`) on unmount. Keying the element on the\n * active panel id (in the parent) guarantees deactivation unmounts this slot —\n * firing the `null` cleanup — and re-activation mounts a fresh one, re-binding.\n */\nfunction NativeSlot(props: NativeSlotProps): ReactNode {\n\tconst { panelId, bindNativeSlot } = props\n\t// Keep the latest callback without re-running the ref effect on identity\n\t// churn of an inline `bindNativeSlot`.\n\tconst bindRef = useRef(bindNativeSlot)\n\tbindRef.current = bindNativeSlot\n\n\tconst setRef = useCallback(\n\t\t(el: HTMLDivElement | null) => {\n\t\t\tbindRef.current(panelId, el)\n\t\t},\n\t\t[panelId],\n\t)\n\n\t// FILL LAYOUT (FIX 2a): the empty anchor slot must fill the remaining group\n\t// region so the host's view-anchor measures the FULL panel rect, not a 0×0\n\t// box. `flex:1` claims the space below the tab strip; `min-*:0` lets it\n\t// shrink inside the flex column; `height:100%` is the belt-and-braces\n\t// fallback for any non-flex ancestor.\n\treturn (\n\t\t<div\n\t\t\tref={setRef}\n\t\t\tdata-deck-native-slot={panelId}\n\t\t\tstyle={{ flex: 1, minWidth: 0, minHeight: 0, height: '100%' }}\n\t\t/>\n\t)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/dock-react/drag-redock.ts","../../src/dock-react/split-sizing.ts","../../src/dock-react/split-view.tsx","../../src/dock-react/panel-body.tsx","../../src/dock-react/group-view.tsx","../../src/dock-react/dock-view.tsx"],"sourcesContent":["/**\n * `drag-redock` — PURE geometry + descriptor layer for tab drag-to-redock.\n *\n * This module is intentionally REACT-FREE and ELECTRON-FREE so it can run under\n * the node `vitest.config.ts` suite (its spec is `drag-redock.test.ts`). It owns\n * two responsibilities, both pure functions of their inputs:\n *\n * 1. GEOMETRY — `computeDropZone(rect, point)` maps a pointer position over a\n * group's rectangle to one of five drop zones (the four edge bands plus the\n * interior `center`). This is the only place edge-band math lives; the React\n * layer feeds it real `getBoundingClientRect` numbers during dragover.\n *\n * 2. DESCRIPTOR — `dropZoneToMutation(zone, dragged, target)` translates a zone\n * into an engine-NEUTRAL `RedockMutation` intent (`move` | `split`). The\n * descriptor only NAMES the intent; the React layer realizes it against the\n * real tree (a split of an EXISTING panel needs extract-then-split, because\n * `splitPanel` throws if the new panel already exists — see the caller).\n *\n * Keeping this split (geometry/descriptor here, engine application in React)\n * means the geometry is exhaustively unit-testable without a DOM, and the React\n * layer stays a thin gesture binding.\n */\n\n/** The five drop zones: four edge bands + the tab-joining interior. */\nexport type DropZone = 'left' | 'right' | 'top' | 'bottom' | 'center'\n\n/**\n * Engine-neutral re-dock INTENT. `move` joins the dragged panel into a tab\n * group; `split` puts it adjacent to a target panel in a new split. This is a\n * pure descriptor — it does NOT itself touch a tree (the caller applies it,\n * extract-then-split'ing for an existing panel).\n */\nexport type RedockMutation =\n\t| { kind: 'move'; panelId: string; destGroupId: string }\n\t| {\n\t\tkind: 'split'\n\t\tatPanelId: string\n\t\tdir: 'row' | 'column'\n\t\tside: 'before' | 'after'\n\t\tnewPanelId: string\n\t}\n\n/**\n * A zero/negative/non-finite rect has no meaningful edge bands; a non-finite\n * point can't be classified either. Note a non-finite WIDTH/HEIGHT (e.g.\n * Infinity) satisfies `> 0` and would otherwise slip through — `band = ef *\n * min(Infinity, h)` yields a finite band and the point is misclassified as an\n * EDGE zone — so reject finiteness EXPLICITLY (N1).\n */\nfunction isDegenerateDropInput(width: number, height: number, x: number, y: number): boolean {\n\treturn (\n\t\t!(width > 0) || !(height > 0)\n\t\t|| !Number.isFinite(width) || !Number.isFinite(height)\n\t\t|| !Number.isFinite(x) || !Number.isFinite(y)\n\t)\n}\n\n/**\n * Clamp the band fraction to [0, 0.5] (N1): a fraction > 0.5 makes the left\n * and right bands (and top/bottom) OVERLAP, so a point near the right edge of\n * a narrow rect would satisfy BOTH `inLeft` and `inRight` and be misread as\n * `left`. Capping at 0.5 keeps the two bands disjoint.\n */\nfunction clampEdgeFraction(edgeFraction: number): number {\n\treturn Number.isFinite(edgeFraction) ? Math.max(0, Math.min(0.5, edgeFraction)) : 0.25\n}\n\n/**\n * A pointer dragged past the panel edge has no band membership; clamp it to\n * the nearest edge zone. `undefined` when the point is IN the rect (the\n * caller falls through to band classification). Both axes out (a diagonal\n * corner): the axis with the LARGER overshoot magnitude wins; an exact tie is\n * broken by HORIZONTAL (the x axis).\n */\nfunction classifyOutOfRectDropZone(width: number, height: number, x: number, y: number): DropZone | undefined {\n\tconst outX = x < 0 ? x : x > width ? x - width : 0\n\tconst outY = y < 0 ? y : y > height ? y - height : 0\n\tif (outX === 0 && outY === 0) return undefined\n\n\tconst magX = Math.abs(outX)\n\tconst magY = Math.abs(outY)\n\tif (magX >= magY && magX > 0) return outX < 0 ? 'left' : 'right'\n\t// Only y out, or y overshoot strictly larger.\n\treturn outY < 0 ? 'top' : 'bottom'\n}\n\n/** Edge-band membership for an in-rect point, plus the per-axis \"is the point\n * in SOME band on this axis\" summary the zone decision branches on. */\ninterface BandMembership {\n\tinLeft: boolean\n\tinTop: boolean\n\tactiveHoriz: boolean\n\tactiveVert: boolean\n}\n\nfunction computeBandMembership(width: number, height: number, x: number, y: number, ef: number): BandMembership {\n\tconst band = ef * Math.min(width, height)\n\tconst inLeft = x < band\n\tconst inRight = x > width - band\n\tconst inTop = y < band\n\tconst inBottom = y > height - band\n\treturn { inLeft, inTop, activeHoriz: inLeft || inRight, activeVert: inTop || inBottom }\n}\n\n/**\n * Corner tie-break (both axes active): pick the edge with the SMALLER\n * normalized distance. Only the ACTIVE horizontal edge and ACTIVE vertical\n * edge can compete (left vs right and top vs bottom can't both be active for\n * a sane rect). On an exact tie, HORIZONTAL wins (`<=` on the horizontal\n * distance).\n */\nfunction cornerTieBreak(width: number, height: number, x: number, y: number, inLeft: boolean, inTop: boolean): DropZone {\n\tconst dHoriz = inLeft ? x / width : (width - x) / width\n\tconst dVert = inTop ? y / height : (height - y) / height\n\tif (dHoriz <= dVert) return inLeft ? 'left' : 'right'\n\treturn inTop ? 'top' : 'bottom'\n}\n\n/**\n * Classify an IN-RECT point by edge-band membership. No band => interior\n * (`center`, the tab-join zone). One active band => that edge directly. Both\n * axes active (a corner) => {@link cornerTieBreak}.\n */\nfunction classifyInRectDropZone(width: number, height: number, x: number, y: number, ef: number): DropZone {\n\tconst { inLeft, inTop, activeHoriz, activeVert } = computeBandMembership(width, height, x, y, ef)\n\n\tif (!activeHoriz && !activeVert) return 'center'\n\tif (activeHoriz && !activeVert) return inLeft ? 'left' : 'right'\n\tif (activeVert && !activeHoriz) return inTop ? 'top' : 'bottom'\n\n\treturn cornerTieBreak(width, height, x, y, inLeft, inTop)\n}\n\n/**\n * Classify a pointer position (RELATIVE to the rect's top-left; `(0,0)` is the\n * corner) into a drop zone.\n *\n * Band thickness = `edgeFraction * min(width, height)` — a single symmetric band\n * width derived from the SHORTER side so a wide/short panel doesn't get an\n * absurdly fat horizontal band. A point in NO band is `center` (the tab-join\n * interior). A point in TWO bands (a corner) tie-breaks by NORMALIZED distance\n * to each active edge; on an exact tie HORIZONTAL wins. A point OUTSIDE the rect\n * clamps to the nearest edge zone (per-axis; diagonal corners pick the larger\n * overshoot, horizontal breaking ties).\n */\nexport function computeDropZone(\n\trect: { width: number; height: number },\n\tpoint: { x: number; y: number },\n\tedgeFraction = 0.25,\n): DropZone {\n\tconst { width, height } = rect\n\tconst { x, y } = point\n\n\tif (isDegenerateDropInput(width, height, x, y)) return 'center'\n\n\tconst ef = clampEdgeFraction(edgeFraction)\n\n\tconst outOfRectZone = classifyOutOfRectDropZone(width, height, x, y)\n\tif (outOfRectZone !== undefined) return outOfRectZone\n\n\treturn classifyInRectDropZone(width, height, x, y, ef)\n}\n\n/**\n * Translate a drop zone into an engine-neutral re-dock descriptor.\n *\n * center => move the dragged panel into the target's tab group.\n * left/right => split the target panel horizontally (row), dragged before/after.\n * top/bottom => split the target panel vertically (column), dragged before/after.\n */\nexport function dropZoneToMutation(\n\tzone: DropZone,\n\tdragged: string,\n\ttarget: { groupId: string; panelId: string },\n): RedockMutation {\n\tif (zone === 'center') {\n\t\treturn { kind: 'move', panelId: dragged, destGroupId: target.groupId }\n\t}\n\tconst dir: 'row' | 'column' = zone === 'left' || zone === 'right' ? 'row' : 'column'\n\tconst side: 'before' | 'after' = zone === 'left' || zone === 'top' ? 'before' : 'after'\n\treturn { kind: 'split', atPanelId: target.panelId, dir, side, newPanelId: dragged }\n}\n\n/**\n * A re-dock is a NO-OP when it would not change the tree:\n *\n * 1. The drop targets the dragged panel ITSELF (`dragged === target.panelId`),\n * in ANY zone — you can neither tab a panel onto its own tab nor split a\n * panel around itself. Crucially this also guards the SPLIT case: without\n * it, `extractPanel(dragged)` then `splitPanel(atPanelId = dragged)` removes\n * the very anchor it then splits at and THROWS (the self-collapse / single-\n * panel-self-split bug).\n * 2. A `center` drop into a group the dragged panel ALREADY lives in\n * (`draggedGroupId === target.groupId`) — `movePanel` would re-append it and\n * bump the revision for no visible change (churn).\n *\n * A SPLIT onto a DIFFERENT panel of the dragged panel's own group is still a real\n * re-dock (it splits the group around that sibling), so it is NOT a no-op.\n *\n * `draggedGroupId` is the id of the tab group currently holding `dragged`\n * (`undefined` if it is not in the tree).\n */\nexport function isNoopRedock(\n\tdragged: string,\n\tdraggedGroupId: string | undefined,\n\ttarget: { groupId: string; panelId: string },\n\tzone: DropZone,\n): boolean {\n\tif (dragged === target.panelId) return true\n\tif (zone === 'center' && draggedGroupId !== undefined && draggedGroupId === target.groupId) {\n\t\treturn true\n\t}\n\treturn false\n}\n\n/**\n * Map a pointer x-position over a horizontal tab strip to an insertion index for\n * a within-strip REORDER (the `dropPolicy:'reorder-only'` gesture). The strip is\n * the dragged tab's own group; `tabRects` are the tab buttons' rects in visual\n * order (each `left` is the rect's left edge, `width` its width). The index is\n * the count of tabs whose MIDPOINT the pointer has passed: the LEFT half of a tab\n * inserts BEFORE it, the RIGHT half (and the exact midpoint) inserts AFTER it. A\n * pointer left of the first tab → 0; past the last tab → `tabRects.length`. Pure\n * (no DOM): the caller measures the rects and passes the pointer x. An empty\n * strip or a non-finite pointer → 0.\n */\nexport function computeReorderIndex(\n\ttabRects: readonly { left: number; width: number }[],\n\tpointerX: number,\n): number {\n\tif (!Number.isFinite(pointerX)) return 0\n\tlet index = 0\n\tfor (const rect of tabRects) {\n\t\tconst midpoint = rect.left + rect.width / 2\n\t\tif (pointerX >= midpoint) index += 1\n\t\telse break\n\t}\n\treturn index\n}\n\n/**\n * Translate a tab strip's VISIBLE-tab drop index into the insertion index\n * `movePanel`'s same-group reorder expects.\n *\n * `computeReorderIndex` reports how many VISIBLE-tab midpoints the pointer has\n * passed (0..`visibleTabIds.length`), COUNTING the dragged tab's own midpoint and\n * measured over rects that OMIT `hideTab` panels. `movePanel` inserts into\n * `panels.filter(p => p !== dragged)` — a different coordinate space. Two shifts\n * reconcile them:\n *\n * 1. Once the pointer passes the dragged tab's OWN midpoint the strip index has\n * counted the dragged slot, so drop it back out (−1) to get the insertion\n * slot among the OTHER visible tabs.\n * 2. Map that visible slot onto the full `panels` order (which may carry hidden\n * tabs the strip never measured): insert before whichever visible tab now\n * occupies the slot, or append when the slot is past the last visible tab.\n */\nexport function resolveReorderInsertIndex(\n\tpanels: readonly string[],\n\tvisibleTabIds: readonly string[],\n\tdraggedPanelId: string,\n\tstripInsertIndex: number,\n): number {\n\tconst draggedVisibleIndex = visibleTabIds.indexOf(draggedPanelId)\n\tconst passedOwnMidpoint = draggedVisibleIndex >= 0 && stripInsertIndex > draggedVisibleIndex\n\tconst filteredVisible = visibleTabIds.filter((id) => id !== draggedPanelId)\n\tconst filteredPanels = panels.filter((id) => id !== draggedPanelId)\n\n\tlet visibleInsert = passedOwnMidpoint ? stripInsertIndex - 1 : stripInsertIndex\n\tif (visibleInsert < 0) visibleInsert = 0\n\tif (visibleInsert > filteredVisible.length) visibleInsert = filteredVisible.length\n\n\tif (visibleInsert >= filteredVisible.length) return filteredPanels.length\n\tconst anchor = filteredVisible[visibleInsert]!\n\tconst anchorIndex = filteredPanels.indexOf(anchor)\n\treturn anchorIndex >= 0 ? anchorIndex : filteredPanels.length\n}\n","/**\n * Pure split-sizing math for the dock renderer — no react import.\n *\n * These functions translate between the model's raw per-child WEIGHTS (+ px\n * constraints) and the percentage/ratio bases the react-resizable-panels Group\n * consumes and reports. They are the single arithmetic authority behind\n * `SplitView`'s model→view sync and resize write-back; several are exported so\n * the sizing invariants can be unit-tested directly.\n */\nimport type { SizeConstraint } from '../layout/index.js'\n\n/** Epsilon (in percentage points) within which two split layouts are treated as\n * equivalent. Guards BOTH sides of the model↔view loop: we skip pushing a\n * `setLayout` when the live layout already matches the target, and skip the\n * `onLayoutChanged` write-back when the incoming layout matches the model — so\n * `setLayout`→`onLayoutChanged`→write-back→re-sync cannot loop. */\nexport const LAYOUT_EPSILON = 0.5\n\n/** TIGHT tolerance (percentage points) for the BASIS-NORMALIZED flexible-ratio\n * compare in `handleLayoutChanged`. We normalize the incoming layout's flexible\n * subset to ratios summing to 100 and compare them against the model's flexible\n * ratios (`computeFlexiblePercentages`, also summing to 100). If they match\n * within this tolerance the `onLayoutChanged` is either our own `setLayout` echo\n * OR a ratio-preserving spontaneous re-measure (mount / fixed-px re-pin /\n * container resize) — SKIP the write-back. If they differ it is a genuine user\n * resize (pointer OR keyboard) — WRITE BACK.\n *\n * Set to ~0.1pp: large enough to absorb rrp's ~3-decimal float noise on an echo,\n * yet FAR below a real drag's delta. R1's \"sub-0.5%\" drag moves a panel ~0.33pp\n * of the container; normalized over the two-flexible-child subset that is ~0.66pp\n * of ratio — comfortably above 0.1, so it is NOT mistaken for an echo and is\n * written back.\n *\n * CAVEAT: this is a flexible-RATIO tolerance (the flexible subset normalized to\n * sum 100), NOT a container-%. It is safe against rrp's 3-decimal echo noise.\n * In a pathologically WIDE split (~≥10 flexible children) a single arrow-key\n * nudge (±~5% of the container on one child) can, once normalized over many\n * flexible siblings, fall BELOW 0.1pp of ratio and be skipped — exotic and\n * self-healing (the next, larger resize writes back). If that ever matters,\n * scale the tolerance down by the flexible child count. */\nexport const FLEX_RATIO_TOLERANCE = 0.1\n\n/** Normalize raw split weights to percentages summing ~100 for `defaultSize`. */\nexport function toPercentages(sizes: readonly number[]): number[] {\n\tconst total = sizes.reduce((a, b) => a + (b > 0 ? b : 0), 0)\n\tif (total <= 0) {\n\t\t// Degenerate weights → distribute evenly.\n\t\tconst even = sizes.length > 0 ? 100 / sizes.length : 100\n\t\treturn sizes.map(() => even)\n\t}\n\treturn sizes.map((s) => ((s > 0 ? s : 0) / total) * 100)\n}\n\n/**\n * Compute the `defaultSize` percentage for each FLEXIBLE child, keyed by its\n * ORIGINAL child index. Px-sized children (a non-null `constraint` — `fixedPx`\n * locked OR `minPx` floored) are EXCLUDED from the pool: their size is px, not a\n * weight, so it must never pollute the flexible siblings' normalization (FIX E1).\n * `constraints[i]` non-null ⇒ child i is px-sized and absent from the returned map.\n */\nexport function computeFlexiblePercentages(\n\tsizes: readonly number[],\n\tconstraints: readonly (SizeConstraint | null)[] | undefined,\n): Map<number, number> {\n\tconst flexibleIndices = sizes\n\t\t.map((_, i) => i)\n\t\t.filter((i) => (constraints?.[i] ?? null) === null)\n\tconst pct = toPercentages(flexibleIndices.map((i) => sizes[i] ?? 0))\n\tconst out = new Map<number, number>()\n\tflexibleIndices.forEach((origIndex, j) => {\n\t\tout.set(origIndex, pct[j]!)\n\t})\n\treturn out\n}\n\n/**\n * The minimum weight a FLEXIBLE child may hold, given how many flexible children\n * share the split (Bug #1). A flexible `<Panel>` has no rrp `minSize` by default\n * (rrp floor is 0%), so a user can drag it to ~0 width; the resize write-back\n * would then persist a ~0 weight and the panel comes back invisible/stuck.\n *\n * The floor is `min(1, floor(90 / flexCount))` — i.e. ~1 weight unit (a flexible\n * split's weights are normalized to percentages downstream, so ~1 reads as ~1%\n * of the flexible pool). With any realistic flexible count this is exactly 1; the\n * `min(1, …)` only matters for a hypothetical 90+ flexible-child split. It is the\n * SAME value used for the rrp `minSize` (A) and the write-back clamp (B) so the\n * two defenses never disagree.\n */\nexport function flexibleFloor(flexCount: number): number {\n\t// `floor(90 / flexCount)` is ~1 for any realistic count, but goes to 0 once\n\t// flexCount > 90 — which would silently DEFEAT the floor (a 0 minSize / 0 clamp\n\t// is no floor at all). Clamp into [MIN_POSITIVE_FLOOR, 1] so the floor is always\n\t// a positive percentage even in a pathologically wide split.\n\tconst MIN_POSITIVE_FLOOR = 0.5\n\treturn Math.min(1, Math.max(MIN_POSITIVE_FLOOR, Math.floor(90 / Math.max(1, flexCount))))\n}\n\n/**\n * Clamp the FLEXIBLE entries of a full-length weights array up to `floor` (Bug #1\n * defense B). Px-sized children (a non-null constraint) are left untouched — their\n * `sizes[i]` is a preserved placeholder, not a live weight. Returns a new array;\n * a weight already ≥ floor is kept verbatim so a healthy ratio is undisturbed.\n */\nexport function clampFlexibleWeights(\n\tweights: readonly number[],\n\tconstraints: readonly (SizeConstraint | null)[] | undefined,\n): number[] {\n\tconst flexCount = weights.filter((_, i) => (constraints?.[i] ?? null) === null).length\n\tconst floor = flexibleFloor(flexCount)\n\treturn weights.map((w, i) => {\n\t\tif ((constraints?.[i] ?? null) !== null) return w // px child — leave as-is\n\t\treturn Number.isFinite(w) && w >= floor ? w : floor\n\t})\n}\n\n/** Are two panelId→percentage maps equivalent within `epsilon` percentage\n * points? Both maps must cover EXACTLY the `ids` key set — a missing key OR an\n * extra key (a key present in `a`/`b` but absent from `ids`) counts as NOT\n * equivalent so the sync is not falsely suppressed. A non-finite value\n * (NaN/±Infinity) is likewise NOT equivalent — `typeof NaN === 'number'` and\n * `Math.abs(NaN - x) > eps` is `false`, so without the `Number.isFinite` guard a\n * NaN would slip through as \"equivalent\" and wrongly suppress a legitimate\n * sync/write-back (N1). EXPORTED (pure) for direct unit coverage. */\nexport function layoutsEquivalent(\n\ta: Record<string, number>,\n\tb: Record<string, number>,\n\tids: readonly string[],\n\tepsilon: number = LAYOUT_EPSILON,\n): boolean {\n\t// Exact key-set match: any key in `a` or `b` that is not in `ids` (extra key)\n\t// makes the maps non-equivalent. `ids` is the authoritative compared set.\n\tconst idSet = new Set(ids)\n\tfor (const k of Object.keys(a)) if (!idSet.has(k)) return false\n\tfor (const k of Object.keys(b)) if (!idSet.has(k)) return false\n\tfor (const id of ids) {\n\t\tconst av = a[id]\n\t\tconst bv = b[id]\n\t\tif (!Number.isFinite(av) || !Number.isFinite(bv)) return false\n\t\tif (Math.abs((av as number) - (bv as number)) > epsilon) return false\n\t}\n\treturn true\n}\n\n/**\n * Build the panel-ID→PERCENTAGE map for an imperative `setLayout`, given the\n * model's full-length raw `sizes` (per child) and `constraints`:\n *\n * - FIXED (px-pinned) children keep their CURRENT measured percentage (read\n * from the live `getLayout()`); they are NOT derived from weights, so a\n * flexible-weights change never disturbs their pixel lock.\n * - The REMAINING percentage (100 − Σ fixed%) is distributed across the\n * FLEXIBLE children in proportion to their weights.\n *\n * Returns `null` when the map can't be built faithfully (e.g. `live` is empty —\n * jsdom's stub — or a fixed child's live % is missing), so the caller skips the\n * `setLayout` rather than pushing a corrupt total. The result always sums to\n * ~100 over all children.\n */\nexport function buildSetLayoutMap(\n\tchildIds: readonly string[],\n\tsizes: readonly number[],\n\tconstraints: readonly (SizeConstraint | null)[] | undefined,\n\tlive: Record<string, number>,\n): Record<string, number> | null {\n\tconst isFixedAt = (i: number): boolean => (constraints?.[i] ?? null) !== null\n\n\tconst fixedTotal = sumFixedLivePercent(childIds, isFixedAt, live)\n\t// Without a measured live % for a fixed child we cannot preserve its px\n\t// lock faithfully — bail so we don't corrupt the fixed child.\n\tif (fixedTotal === null) return null\n\n\tconst remaining = Math.max(0, 100 - fixedTotal)\n\n\t// Flexible weight pool.\n\tlet flexWeightTotal = 0\n\tlet flexibleCount = 0\n\tfor (let i = 0; i < childIds.length; i++) {\n\t\tif (isFixedAt(i)) continue\n\t\tconst w = sizes[i] ?? 0\n\t\tflexWeightTotal += w > 0 ? w : 0\n\t\tflexibleCount += 1\n\t}\n\n\tconst out: Record<string, number> = {}\n\tfor (let i = 0; i < childIds.length; i++) {\n\t\tconst id = childIds[i]!\n\t\tout[id] = isFixedAt(i)\n\t\t\t? live[id]!\n\t\t\t: flexibleShare(sizes[i] ?? 0, remaining, flexWeightTotal, flexibleCount)\n\t}\n\treturn out\n}\n\n/**\n * Sum the fixed children's CURRENT live percentages (preserving their px\n * lock). Null when a fixed child has no measured live % — the caller cannot\n * build a faithful map then.\n */\nfunction sumFixedLivePercent(\n\tchildIds: readonly string[],\n\tisFixedAt: (i: number) => boolean,\n\tlive: Record<string, number>,\n): number | null {\n\tlet fixedTotal = 0\n\tfor (let i = 0; i < childIds.length; i++) {\n\t\tif (!isFixedAt(i)) continue\n\t\tconst livePct = live[childIds[i]!]\n\t\tif (typeof livePct !== 'number') return null\n\t\tfixedTotal += livePct\n\t}\n\treturn fixedTotal\n}\n\n/**\n * One flexible child's percentage share of the `remaining` space. A degenerate\n * weight pool (total ≤ 0) splits the remaining space evenly instead.\n */\nfunction flexibleShare(\n\tweight: number,\n\tremaining: number,\n\tflexWeightTotal: number,\n\tflexibleCount: number,\n): number {\n\tif (flexWeightTotal <= 0) {\n\t\treturn flexibleCount > 0 ? remaining / flexibleCount : remaining\n\t}\n\treturn (remaining * (weight > 0 ? weight : 0)) / flexWeightTotal\n}\n\n/**\n * Extract an rrp `onLayoutChanged` map's FLEXIBLE subset (skipping fixed-px\n * children) in child order and NORMALIZE it by its own sum to ratios summing to\n * ~100 — the basis `computeFlexiblePercentages` already produces for the model,\n * so the two are directly comparable. Returns the original child `indices`\n * alongside the `ratios`. Null when the map is malformed (a flexible id is\n * missing / non-finite) or there is no flexible child — the caller then never\n * writes back from it.\n */\nexport function incomingFlexRatios(\n\tchildIds: readonly string[],\n\tconstraints: readonly (SizeConstraint | null)[] | undefined,\n\tlayout: Record<string, number>,\n): { indices: number[]; ratios: number[] } | null {\n\tconst indices: number[] = []\n\tconst raw: number[] = []\n\tfor (let i = 0; i < childIds.length; i++) {\n\t\tif ((constraints?.[i] ?? null) !== null) continue\n\t\tconst v = layout[childIds[i]!]\n\t\tif (typeof v !== 'number' || !Number.isFinite(v)) return null\n\t\tindices.push(i)\n\t\traw.push(v > 0 ? v : 0)\n\t}\n\tif (indices.length === 0) return null\n\treturn { indices, ratios: toPercentages(raw) }\n}\n","/**\n * `SplitView` — one split node rendered as a react-resizable-panels Group, plus\n * the model↔view sync machine (M1): it pushes the model's flexible weights into\n * the live Group via `setLayout` and writes a genuine user resize back through\n * `ctx.onApplyLayout`. The split-sizing arithmetic lives in `./split-sizing`.\n */\nimport { useCallback, useEffect, useRef, type ReactNode } from 'react'\nimport { Group, Panel, Separator } from 'react-resizable-panels'\nimport type { GroupImperativeHandle, Layout } from 'react-resizable-panels'\nimport type { LayoutNode, SplitNode } from '../layout/index.js'\nimport {\n\tbuildSetLayoutMap,\n\tclampFlexibleWeights,\n\tcomputeFlexiblePercentages,\n\tFLEX_RATIO_TOLERANCE,\n\tflexibleFloor,\n\tincomingFlexRatios,\n\tlayoutsEquivalent,\n} from './split-sizing.js'\nimport { renderNode, type RenderContext } from './dock-view.js'\n\n/** A split wrapper element augmented with the resize write-back seam plus the\n * rrp Group imperative handle (M1 model→view sync). Hosts/tests reach the live\n * split layout through `__deckGroupApi` the same way they reach the write-back\n * through `__deckApplyLayout`. */\ntype DeckSplitElement = HTMLDivElement & {\n\t__deckApplyLayout?: (weights: number[]) => void\n\t__deckGroupApi?: GroupImperativeHandle\n}\n\nexport interface SplitViewProps {\n\tnode: SplitNode\n\tctx: RenderContext\n}\n\nexport function SplitView(props: SplitViewProps): ReactNode {\n\tconst { node, ctx } = props\n\tconst orientation = node.orientation === 'row' ? 'horizontal' : 'vertical'\n\n\t// A child is FIXED (px-pinned) iff it carries a non-null constraint. Fixed\n\t// children are excluded from the flexible-percentage pool: their px panel does\n\t// not participate in weight-based sizing, so polluting `toPercentages` with\n\t// their weight would skew the flexible siblings' defaultSize.\n\t// `computeFlexiblePercentages` therefore normalizes percentages over the\n\t// FLEXIBLE indices only and maps them back by index; fixed children keep their\n\t// px defaultSize/min/max. (Both write-back seams derive fixed-ness from\n\t// `nodeRef.current.constraints` directly — see `handleLayoutChanged` and\n\t// `__deckApplyLayout` — so no render-closure `isFixed` helper is needed here.)\n\tconst percentageByIndex = computeFlexiblePercentages(node.sizes, node.constraints)\n\n\t// Bug #1 defense A: the minimum % a flexible child may shrink to. rrp's default\n\t// flexible `minSize` is 0% (a panel can be dragged to nothing); this floors it\n\t// so the simulator/editor can never be pulled to 0 width. Unitless string =\n\t// percentage (same convention as `defaultSize` above; a px `minSize` next to a\n\t// %-`defaultSize` is misparsed by rrp). The floor is keyed on the COUNT of\n\t// flexible children in THIS split (the same value the write-back clamp uses).\n\tconst flexCount = node.children.filter((_, i) => (node.constraints?.[i] ?? null) === null).length\n\tconst flexMinSize = String(flexibleFloor(flexCount))\n\n\t// ── M1 model→view sync plumbing ────────────────────────────────────────\n\t// The rrp Group's imperative handle (getLayout/setLayout) — the seam through\n\t// which the model becomes the source of truth for the visible split ratio.\n\tconst groupRef = useRef<GroupImperativeHandle | null>(null)\n\t// Keep the latest node reachable from the imperative ref + drag callbacks\n\t// without re-binding the ref on every render. The sync effect reads the\n\t// freshest `node` directly (it re-runs when `node.sizes` change), so it does\n\t// not go stale; this ref is for the imperative seams that capture once.\n\tconst nodeRef = useRef(node)\n\tnodeRef.current = node\n\n\tconst items: ReactNode[] = []\n\tnode.children.forEach((child, i) => {\n\t\tif (i > 0) {\n\t\t\titems.push(\n\t\t\t\t<Separator\n\t\t\t\t\tkey={`handle-${i}`}\n\t\t\t\t\tdata-deck-resize-handle=\"\"\n\t\t\t\t/>,\n\t\t\t)\n\t\t}\n\t\t// PIXEL-sized children (`fixedPx`/`minPx`) are sized in px, NOT weights, so\n\t\t// they are excluded from the flexible % pool (`computeFlexiblePercentages`)\n\t\t// and never normalized against weight-sized siblings. rrp needs CONSISTENT\n\t\t// units per panel — a px `minSize` on a %-`defaultSize` panel is misparsed\n\t\t// (the panel grabs the whole region), so a px floor MUST pair with a px\n\t\t// `defaultSize`.\n\t\tconst constraint = node.constraints?.[i] ?? null\n\t\tif (constraint?.fixedPx != null) {\n\t\t\t// `fixedPx` LOCKS the child: min===max + `preserve-pixel-size` keeps it\n\t\t\t// fixed while flexible siblings absorb group resizes.\n\t\t\tconst px = `${constraint.fixedPx}px`\n\t\t\titems.push(\n\t\t\t\t<Panel\n\t\t\t\t\tkey={panelKey(child)}\n\t\t\t\t\tid={child.id}\n\t\t\t\t\tdefaultSize={px}\n\t\t\t\t\tminSize={px}\n\t\t\t\t\tmaxSize={px}\n\t\t\t\t\tgroupResizeBehavior=\"preserve-pixel-size\"\n\t\t\t\t>\n\t\t\t\t\t{renderNode(child, ctx)}\n\t\t\t\t</Panel>,\n\t\t\t)\n\t\t}\n\t\telse if (constraint?.minPx != null) {\n\t\t\t// `minPx` FLOORS the child at a pixel minimum but leaves it DRAGGABLE\n\t\t\t// above it: px `defaultSize`+`minSize` (starts AT the floor, can't shrink\n\t\t\t// past it), NO `maxSize` so the user can widen it. `preserve-pixel-size`\n\t\t\t// puts the panel in rrp's pixel mode — WITHOUT it rrp misparses a px\n\t\t\t// `defaultSize` next to %-sized siblings and the panel grabs ~80% of the\n\t\t\t// region. It only governs WINDOW-resize behavior (keep pixel width, don't\n\t\t\t// scale); the user can still drag the separator to resize it (≥ minSize).\n\t\t\tconst px = `${constraint.minPx}px`\n\t\t\titems.push(\n\t\t\t\t<Panel\n\t\t\t\t\tkey={panelKey(child)}\n\t\t\t\t\tid={child.id}\n\t\t\t\t\tdefaultSize={px}\n\t\t\t\t\tminSize={px}\n\t\t\t\t\tgroupResizeBehavior=\"preserve-pixel-size\"\n\t\t\t\t>\n\t\t\t\t\t{renderNode(child, ctx)}\n\t\t\t\t</Panel>,\n\t\t\t)\n\t\t}\n\t\telse {\n\t\t\t// `defaultSize` seeds the FLEXIBLE child at mount; post-mount the model\n\t\t\t// becomes the source of truth via the imperative `setLayout` driven from\n\t\t\t// the sync effect below (M1). The value is passed as a STRING percentage:\n\t\t\t// rrp treats a NUMBER `defaultSize` as PIXELS, so `defaultSize={70}` would\n\t\t\t// be 70px (not 70%) — which mis-proportions a flexible sibling next to a\n\t\t\t// px-sized (`fixedPx`/`minPx`) panel and the sync then preserves the wrong\n\t\t\t// size. A unitless string is parsed as a percentage.\n\t\t\tconst pct = percentageByIndex.get(i)\n\t\t\titems.push(\n\t\t\t\t<Panel\n\t\t\t\t\tkey={panelKey(child)}\n\t\t\t\t\tid={child.id}\n\t\t\t\t\tdefaultSize={pct != null ? String(pct) : undefined}\n\t\t\t\t\tminSize={flexMinSize}\n\t\t\t\t>\n\t\t\t\t\t{renderNode(child, ctx)}\n\t\t\t\t</Panel>,\n\t\t\t)\n\t\t}\n\t})\n\n\t// A real rrp resize (pointer OR keyboard) — or our own `setLayout` echo / a\n\t// spontaneous re-measure — commits new per-Panel percentages here. The SINGLE\n\t// discriminator is a BASIS-NORMALIZED flexible-ratio compare (no gate, no echo\n\t// token): write back IFF the incoming layout's FLEXIBLE-child ratios differ from\n\t// the model's. `incomingFlexRatios` normalizes rrp's CONTAINER-% over the\n\t// flexible subset; `computeFlexiblePercentages` already does the same for the\n\t// model — so both bases sum to 100 and are directly comparable.\n\tconst handleLayoutChanged = (layout: Layout): void => {\n\t\tconst cur = nodeRef.current\n\t\tconst curChildIds = cur.children.map((c) => c.id)\n\t\tconst isFixedAt = (i: number): boolean => (cur.constraints?.[i] ?? null) !== null\n\n\t\tconst incoming = incomingFlexRatios(curChildIds, cur.constraints, layout)\n\t\tif (!incoming) return // malformed / no flexible child → never write back\n\t\tconst modelPctByIndex = computeFlexiblePercentages(cur.sizes, cur.constraints)\n\t\tconst modelNorm = incoming.indices.map((i) => modelPctByIndex.get(i) ?? 0)\n\n\t\t// Equal within a TIGHT tolerance → our own `setLayout` echo OR a\n\t\t// ratio-preserving spontaneous re-measure (mount / fixed-px re-pin / container\n\t\t// resize). SKIP: no model churn, no loop, no R2 corruption. Otherwise it is a\n\t\t// genuine user resize → WRITE BACK.\n\t\tconst differs = incoming.ratios.some(\n\t\t\t(r, k) => Math.abs(r - modelNorm[k]!) > FLEX_RATIO_TOLERANCE,\n\t\t)\n\t\tif (!differs) return\n\n\t\t// FULL-LENGTH weights (setSizes requires length === children.length). FIXED\n\t\t// children keep the model's existing weight — rrp reports a container-derived %\n\t\t// for the pinned panel which, written back, would corrupt the stored weight and\n\t\t// lose it when the constraint is later cleared. FLEXIBLE children take rrp's.\n\t\tconst weights = curChildIds.map((id, i) =>\n\t\t\tisFixedAt(i) ? cur.sizes[i] ?? 1 : layout[id]!,\n\t\t)\n\t\t// Bug #1 defense B: a flexible child dragged to ~0 width reports a ~0 ratio;\n\t\t// floor it before persisting so a 0-width weight can never reach the model\n\t\t// (the rrp `minSize` from A is the first defense, this is the write-back\n\t\t// backstop). Px children are untouched (clampFlexibleWeights skips them).\n\t\tctx.onApplyLayout(cur.id, clampFlexibleWeights(weights, cur.constraints))\n\t}\n\n\t// Ref callback exposing the resize write-back seam + the rrp Group imperative\n\t// handle on the split element. A drag round-trip (e2e) and the unit seam both\n\t// land on the same engine path; the `__deckGroupApi` handle lets hosts/tests\n\t// read the LIVE split layout (M1 model→view sync seam).\n\tconst setSplitRef = (el: DeckSplitElement | null): void => {\n\t\tif (el) {\n\t\t\tel.__deckApplyLayout = (weights: number[]) => {\n\t\t\t\t// Mirror handleLayoutChanged: a fixed child's stored weight is never\n\t\t\t\t// overwritten by an incoming (container-derived) value — preserve\n\t\t\t\t// node.sizes[i] for fixed indices so clearing the constraint later\n\t\t\t\t// restores the original weight. Flexible indices take the supplied value.\n\t\t\t\t// Derive fixed-ness from `nodeRef.current.constraints` (the SAME fresh\n\t\t\t\t// node we read sizes from) so this seam has no fresh-sizes/stale-constraints\n\t\t\t\t// asymmetry. Mirrors `isFixedAt` in handleLayoutChanged.\n\t\t\t\tconst cur = nodeRef.current\n\t\t\t\tconst isFixedAt = (i: number): boolean => (cur.constraints?.[i] ?? null) !== null\n\t\t\t\tconst next = weights.map((w, i) => (isFixedAt(i) ? cur.sizes[i] ?? 1 : w))\n\t\t\t\t// Bug #1 defense B (same clamp as handleLayoutChanged): floor flexible\n\t\t\t\t// weights so a near-0 drag can never persist a 0-width panel.\n\t\t\t\tctx.onApplyLayout(cur.id, clampFlexibleWeights(next, cur.constraints))\n\t\t\t}\n\t\t\t// Expose the rrp Group imperative handle (getLayout/setLayout) so a host —\n\t\t\t// and the jsdom `[M1-seam-live]` test — can reach the live split layout.\n\t\t\t// May be null until the Group mounts its handle; the getter reads the\n\t\t\t// freshest ref each call.\n\t\t\tObject.defineProperty(el, '__deckGroupApi', {\n\t\t\t\tconfigurable: true,\n\t\t\t\tget: () => groupRef.current ?? undefined,\n\t\t\t})\n\t\t}\n\t}\n\n\t// ── M1 model→view sync ─────────────────────────────────────────────────\n\t// Push the model's FLEXIBLE weights into the live Group via `setLayout` so the\n\t// model is the source of truth for the visible split post-mount (without it rrp\n\t// freezes at the mount-time `defaultSize`). Reads the FRESHEST `node` from the\n\t// ref so an external `setSizes` syncs against the latest model. Returns true if\n\t// a `setLayout` was actually pushed.\n\tconst runSync = useCallback((): boolean => {\n\t\tconst api = groupRef.current\n\t\tif (!api) return false\n\t\tconst cur = nodeRef.current\n\t\tconst curChildIds = cur.children.map((c) => c.id)\n\n\t\tconst live = api.getLayout()\n\t\t// jsdom's stub returns `{}` (no measured geometry) — buildSetLayoutMap then\n\t\t// returns null for any fixed child, and for the all-flexible case the\n\t\t// equivalence check below sees missing live ids and proceeds; but `setLayout`\n\t\t// is a no-op under jsdom anyway. In a real renderer `live` is populated.\n\t\tconst targetMap = buildSetLayoutMap(curChildIds, cur.sizes, cur.constraints, live)\n\t\tif (!targetMap) return false\n\n\t\t// SET-side redundant-push skip: don't re-push a layout the live Group already\n\t\t// satisfies. This is the loop break on the SET side — pushing an identical\n\t\t// layout would re-emit `onLayoutChanged`, but its flexible ratios match the\n\t\t// model so the normalized compare in `handleLayoutChanged` skips the write-back\n\t\t// anyway; this skip just avoids the redundant work.\n\t\tif (layoutsEquivalent(live, targetMap, curChildIds)) return false\n\n\t\tapi.setLayout(targetMap)\n\t\treturn true\n\t}, [])\n\n\t// Re-run the sync whenever the model's raw weights change (every external\n\t// `setSizes`). The model is stable DURING a user drag — the write-back lands on\n\t// release — so this effect does not fire mid-drag and never fights the live\n\t// pointer snapshot. An external `setSizes` (incl. one after an away-and-back\n\t// drag) syncs immediately (R3): there is no drag flag to get stuck.\n\tconst sizesKey = node.sizes.join(',')\n\t// Child COUNT is part of the sync key (Bug #2 follow-up): the `key={children.\n\t// length}` on the Group remounts it on a cardinality change, and a remount\n\t// reseeds the rrp layout from each Panel's `defaultSize` — for a `minPx` column\n\t// that is the floor (the device-min), NOT the user's last-dragged width. Re-\n\t// running `runSync` after the remount re-pushes the model's stored weights into\n\t// the fresh Group so a surviving split's proportions are restored rather than\n\t// snapping back to the per-Panel defaults. A pure weight resize (same count)\n\t// already re-syncs via `sizesKey`; this only adds the count edge.\n\tconst childCountKey = node.children.length\n\tuseEffect(() => {\n\t\trunSync()\n\t\t// Keyed on `sizesKey` + `childCountKey`; `runSync` reads `node`/`childIds`/\n\t\t// `constraints` fresh from the ref, so they cannot go stale relative to either.\n\t}, [sizesKey, childCountKey, runSync])\n\n\t// The split-level `data-*` attributes ride on an outer wrapper rather than\n\t// the Group itself, so hosts/tests can target them regardless of how the\n\t// library forwards unknown props. `data-deck-sizes` mirrors the model's\n\t// CURRENT raw weights so the render is a function of model.sizes.\n\treturn (\n\t\t<div\n\t\t\tref={setSplitRef}\n\t\t\tdata-deck-split={node.id}\n\t\t\tdata-orientation={node.orientation}\n\t\t\tdata-deck-sizes={node.sizes.join(',')}\n\t\t\tstyle={{ width: '100%', height: '100%' }}\n\t\t>\n\t\t\t<Group\n\t\t\t\t// Bug #2 (white-screen crash guard): KEY the Group on its child COUNT.\n\t\t\t\t// rrp v4.10 caches the previous layout in a ref keyed by group id; on a\n\t\t\t\t// render where the child count CHANGES (a panel closed/split — the split\n\t\t\t\t// id stays 'root' so the Group instance is otherwise reused), rrp's\n\t\t\t\t// commit-phase layout effect synchronously validates the STALE\n\t\t\t\t// (old-length) cached layout against the NEW (different-length)\n\t\t\t\t// constraints and throws `Invalid N panel layout`. That throw escapes\n\t\t\t\t// SplitView during commit and unmounts the whole tree (white screen).\n\t\t\t\t// Keying on `node.children.length` remounts the Group with a fresh\n\t\t\t\t// internal layout sized to the new child count ONLY when the count\n\t\t\t\t// changes — an ordinary weight resize never changes the key, so this\n\t\t\t\t// adds NO remount on drags. The model→view sync (runSync) re-binds\n\t\t\t\t// `groupRef` on remount and re-pushes the model's weights via the\n\t\t\t\t// `sizesKey` effect; the simulator/console native overlay re-anchors via\n\t\t\t\t// its slot ref callback (same path as a tab switch). See\n\t\t\t\t// dock-view-robustness.test.tsx \"3 → 2\" for the regression.\n\t\t\t\tkey={node.children.length}\n\t\t\t\tgroupRef={groupRef}\n\t\t\t\torientation={orientation}\n\t\t\t\tonLayoutChanged={handleLayoutChanged}\n\t\t\t\tstyle={{ width: '100%', height: '100%' }}\n\t\t\t>\n\t\t\t\t{items}\n\t\t\t</Group>\n\t\t</div>\n\t)\n}\n\n/** Stable React key for a child node (split id or group id). */\nfunction panelKey(node: LayoutNode): string {\n\treturn node.id\n}\n","/**\n * Tab-group body rendering under DOM-panel keepalive: `renderActiveBody` (the\n * body region) + `NativeSlot` (the anchor for an active native panel).\n */\nimport { Fragment, useCallback, useRef, type ReactNode } from 'react'\nimport type { TabGroupNode } from '../layout/index.js'\nimport type { RenderContext } from './dock-view.js'\n\n/**\n * Render a tab group's body region under DOM-panel KEEPALIVE.\n *\n * - DOM panels: ALL of the group's DOM panels are mounted SIMULTANEOUSLY, each\n * under a STABLE React key (`dom-${panelId}`) so switching the active tab never\n * remounts a body — React state + scroll persist across A→B→A. The active body\n * fills the region (flex:1); inactive ones stay in the DOM but `display:none`.\n * Each body's host renderer receives `{ active }` and is re-invoked with the\n * new flag on every activation change (no remount), so a host can fire\n * on-activation side effects off the false→true edge.\n * - Native panels: EXEMPT from keepalive. The single ACTIVE native panel mounts a\n * `NativeSlot` (keyed on the active id, so deactivation unmounts it and fires\n * `bindNativeSlot(id, null)`); inactive native panels render nothing. Keeping a\n * bound native slot mounted-but-hidden would collapse its WebContentsView rect.\n */\nexport function renderActiveBody(node: TabGroupNode, ctx: RenderContext): ReactNode {\n\tconst activeId = node.active\n\tif (!activeId) return null\n\n\tconst activeDescriptor = ctx.registry.get(activeId)\n\n\t// Native active panel → active-only NativeSlot (no keepalive).\n\tconst nativeSlot\n\t\t= activeDescriptor?.kind === 'native'\n\t\t\t? (\n\t\t\t\t<NativeSlot\n\t\t\t\t\tkey={activeId}\n\t\t\t\t\tpanelId={activeId}\n\t\t\t\t\tbindNativeSlot={ctx.bindNativeSlot}\n\t\t\t\t/>\n\t\t\t\t)\n\t\t\t: null\n\n\t// Every DOM (non-native) panel in the group is kept alive: mounted up-front,\n\t// hidden unless active. A panel with no descriptor is treated as DOM (render\n\t// via the host's renderer) — matching the pre-keepalive fallback.\n\t// Bodies are absolutely stacked and inactive ones are `display:none`, so their\n\t// DOM sibling order carries no visual meaning. Render them in a STABLE order\n\t// (by panelId) rather than tab order: a tab reorder must not move a body's DOM\n\t// node, because a web host renders bodies as iframes and moving an iframe node\n\t// reloads it. The `key` stays panel-stable so activation never remounts a body.\n\tconst domBodies = node.panels\n\t\t.filter((panelId) => ctx.registry.get(panelId)?.kind !== 'native')\n\t\t.slice()\n\t\t.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n\t\t.map((panelId) => {\n\t\t\tconst active = panelId === activeId\n\t\t\treturn (\n\t\t\t\t<div\n\t\t\t\t\tkey={`dom-${panelId}`}\n\t\t\t\t\tdata-deck-panel-body={panelId}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\t// Active body fills the region; inactive bodies stay mounted but\n\t\t\t\t\t\t// hidden (display:none preserves React state + scroll position).\n\t\t\t\t\t\t// A flex COLUMN container (not a bare block) so a host panel root\n\t\t\t\t\t\t// that fills via `flex:1`/`height:100%` actually stretches to the\n\t\t\t\t\t\t// full body height — without this such a root collapses to content\n\t\t\t\t\t\t// height and leaves dead space below it.\n\t\t\t\t\t\tdisplay: active ? 'flex' : 'none',\n\t\t\t\t\t\tflexDirection: 'column',\n\t\t\t\t\t\tflex: 1,\n\t\t\t\t\t\tminWidth: 0,\n\t\t\t\t\t\tminHeight: 0,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{ctx.renderDomPanel(panelId, { active })}\n\t\t\t\t</div>\n\t\t\t)\n\t\t})\n\n\treturn (\n\t\t<Fragment>\n\t\t\t{nativeSlot}\n\t\t\t{domBodies}\n\t\t</Fragment>\n\t)\n}\n\ninterface NativeSlotProps {\n\tpanelId: string\n\tbindNativeSlot: (panelId: string, el: HTMLElement | null) => void\n}\n\n/**\n * Empty anchor slot for an ACTIVE native panel. A ref-callback binds the live\n * element on mount and unbinds (`null`) on unmount. Keying the element on the\n * active panel id (in the parent) guarantees deactivation unmounts this slot —\n * firing the `null` cleanup — and re-activation mounts a fresh one, re-binding.\n */\nfunction NativeSlot(props: NativeSlotProps): ReactNode {\n\tconst { panelId, bindNativeSlot } = props\n\t// Keep the latest callback without re-running the ref effect on identity\n\t// churn of an inline `bindNativeSlot`.\n\tconst bindRef = useRef(bindNativeSlot)\n\tbindRef.current = bindNativeSlot\n\n\tconst setRef = useCallback(\n\t\t(el: HTMLDivElement | null) => {\n\t\t\tbindRef.current(panelId, el)\n\t\t},\n\t\t[panelId],\n\t)\n\n\t// FILL LAYOUT (FIX 2a): the empty anchor slot must fill the remaining group\n\t// region so the host's view-anchor measures the FULL panel rect, not a 0×0\n\t// box. `flex:1` claims the space below the tab strip; `min-*:0` lets it\n\t// shrink inside the flex column; `height:100%` is the belt-and-braces\n\t// fallback for any non-flex ancestor.\n\treturn (\n\t\t<div\n\t\t\tref={setRef}\n\t\t\tdata-deck-native-slot={panelId}\n\t\t\tstyle={{ flex: 1, minWidth: 0, minHeight: 0, height: '100%' }}\n\t\t/>\n\t)\n}\n","/**\n * `GroupView` — one tab GROUP: the draggable tab strip, the active body, the\n * imperative `__deckHandleDrop` seam, and a geometry-driven drop indicator while\n * a drag hovers. Owns all tab drag/drop handlers.\n */\nimport { useRef, useState, type DragEvent, type ReactNode } from 'react'\nimport type { TabGroupNode } from '../layout/index.js'\nimport { computeDropZone, computeReorderIndex, type DropZone } from './drag-redock.js'\nimport { renderActiveBody } from './panel-body.js'\nimport type { RenderContext } from './dock-view.js'\n\n/**\n * The dataTransfer MIME under which a drag carries the dragged panel id. A\n * custom type (vs `text/plain`) keeps deck drags from being confused with\n * arbitrary text drops; the panel id also stays recoverable from the source\n * tab's `data-deck-tab` attribute (the jsdom seam relies on the latter).\n */\nconst DRAG_PANEL_MIME = 'application/x-deck-panel'\n\n/** A group wrapper element augmented with the drop-handling seam. Mirrors the\n * `__deckApplyLayout` discipline on split elements: an imperative hook the\n * gesture (or a unit test) calls to commit a re-dock against the model. */\ntype DeckGroupElement = HTMLDivElement & {\n\t__deckHandleDrop?: (draggedPanelId: string, zone: DropZone) => void\n}\n\nexport interface GroupViewProps {\n\tnode: TabGroupNode\n\tctx: RenderContext\n}\n\n/**\n * One tab GROUP: tab strip (draggable tabs) + active body + the imperative\n * `__deckHandleDrop` seam + a geometry-driven drop indicator while a drag hovers.\n */\nexport function GroupView(props: GroupViewProps): ReactNode {\n\tconst { node, ctx } = props\n\n\t// The live drop zone under the pointer during a drag-over (null = no drag over\n\t// this group). Drives the `data-deck-drop-zone` indicator. jsdom can't produce\n\t// real geometry (getBoundingClientRect is 0), so this path is exercised by the\n\t// real-pointer e2e `it.todo`s; here we implement it for real-browser fidelity.\n\tconst [dropZone, setDropZone] = useState<DropZone | null>(null)\n\n\t// Keep the latest node + redock callback reachable from the imperative ref\n\t// seam without re-running the ref effect when they change identity. The seam\n\t// always anchors a split at the group's CURRENT active panel.\n\tconst nodeRef = useRef(node)\n\tnodeRef.current = node\n\tconst redockRef = useRef(ctx.onRedock)\n\tredockRef.current = ctx.onRedock\n\n\t// Ref-callback exposing the drop seam on the group element. Mirrors\n\t// `setSplitRef`/`__deckApplyLayout`: the gesture and the unit seam both land on\n\t// the same `onRedock` engine path. The seam owns choosing the target panel —\n\t// the group's active panel is the natural anchor for an edge split.\n\tconst setGroupRef = (el: DeckGroupElement | null): void => {\n\t\tif (el) {\n\t\t\tel.__deckHandleDrop = (draggedPanelId: string, zone: DropZone) => {\n\t\t\t\tconst current = nodeRef.current\n\t\t\t\tredockRef.current(current.id, current.active, draggedPanelId, zone)\n\t\t\t}\n\t\t}\n\t}\n\n\t// ── drag-over geometry ────────────────────────────────────────────────\n\t// Compute the hovered zone from the pointer position relative to the group\n\t// rect. `preventDefault` is required for the element to be a valid drop target.\n\tconst handleDragOver = (e: DragEvent<HTMLDivElement>): void => {\n\t\t// Only a genuine deck drag (carrying the deck panel MIME) may drive the drop\n\t\t// geometry — same contract the tab strip enforces. A foreign OS drag (files,\n\t\t// external text) never previews a drop indicator nor becomes a valid target.\n\t\tif (!e.dataTransfer?.types.includes(DRAG_PANEL_MIME)) return\n\t\te.preventDefault()\n\t\t// Opt-in (suppressReorderOnlyDropIndicator): a `reorder-only` panel can ONLY\n\t\t// land via a within-group tab-strip reorder (handled by the strip handlers,\n\t\t// which paint no indicator). Anywhere the group body would resolve to — its\n\t\t// own edges or ANY other group — is a no-op for it, so the blue drop\n\t\t// highlight there is misleading. When opted in, suppress it entirely while\n\t\t// such a panel is in flight. Default keeps the geometry-only indicator (the\n\t\t// capability gate still rejects the drop at drop time).\n\t\tconst draggedId = ctx.activeDragPanelId.current\n\t\tif (\n\t\t\tctx.suppressReorderOnlyDropIndicator\n\t\t\t&& draggedId !== null\n\t\t\t&& ctx.registry.get(draggedId)?.dropPolicy === 'reorder-only'\n\t\t) {\n\t\t\tsetDropZone(null)\n\t\t\treturn\n\t\t}\n\t\tconst rect = e.currentTarget.getBoundingClientRect()\n\t\tconst zone = computeDropZone(\n\t\t\t{ width: rect.width, height: rect.height },\n\t\t\t{ x: e.clientX - rect.left, y: e.clientY - rect.top },\n\t\t)\n\t\tsetDropZone(zone)\n\t}\n\n\tconst handleDragLeave = (): void => {\n\t\tsetDropZone(null)\n\t}\n\n\t// On drop, recover the dragged panel id (custom MIME first, falling back to\n\t// the source tab marker carried as text), then commit via the same seam path.\n\tconst handleDrop = (e: DragEvent<HTMLDivElement>): void => {\n\t\te.preventDefault()\n\t\tconst rect = e.currentTarget.getBoundingClientRect()\n\t\tconst zone = computeDropZone(\n\t\t\t{ width: rect.width, height: rect.height },\n\t\t\t{ x: e.clientX - rect.left, y: e.clientY - rect.top },\n\t\t)\n\t\tsetDropZone(null)\n\t\t// Only the deck's own MIME identifies a panel drag. A `text/plain` value from\n\t\t// a foreign drag may coincide with a registered panel id, so it is NOT trusted.\n\t\tconst dragged = e.dataTransfer?.getData(DRAG_PANEL_MIME)\n\t\t// Validate the payload before committing a re-dock:\n\t\t// - M2: registry membership is NOT tree membership. A registered-but-absent\n\t\t// panel (closed out of the tree / never docked) passes the registry guard\n\t\t// yet drives `movePanel`/`extractPanel` on an id missing from the tree,\n\t\t// which THROWS `panel not found`. It must be a NO-OP — also require the id\n\t\t// to be present in the CURRENT tree.\n\t\tif (!dragged || ctx.registry.get(dragged) === undefined || !ctx.isPanelInTree(dragged)) return\n\t\t// Pointer-derived insertion index for a within-group REORDER\n\t\t// (`dropPolicy:'reorder-only'`): measure this group's tab rects and map the\n\t\t// pointer x onto an index. Ignored by `handleRedock` for every non-reorder\n\t\t// path, so it is safe to compute unconditionally.\n\t\tconst tabRects = Array.from(\n\t\t\te.currentTarget.querySelectorAll<HTMLElement>('[data-deck-tab]'),\n\t\t).map((el) => {\n\t\t\tconst r = el.getBoundingClientRect()\n\t\t\treturn { left: r.left, width: r.width }\n\t\t})\n\t\tconst reorderIndex = computeReorderIndex(tabRects, e.clientX)\n\t\tctx.onRedock(node.id, node.active, dragged, zone, reorderIndex)\n\t}\n\n\t// The tab STRIP is a first-class REORDER drop target. It sits at the TOP of\n\t// the group, so a tab dropped onto it would otherwise resolve to the group's\n\t// `top` EDGE zone via `computeDropZone` — which a `reorder-only` panel rejects\n\t// as a no-op, so reordering by dragging within the tab bar would never work.\n\t// Intercept strip drops here and commit a `center` re-dock carrying the\n\t// pointer-x insertion index (`handleRedock` reorders within the group).\n\t// `stopPropagation` keeps the group's edge/center drop handlers from also\n\t// firing for the same gesture.\n\tconst handleTabStripDragOver = (e: DragEvent<HTMLDivElement>): void => {\n\t\tif (!e.dataTransfer?.types.includes(DRAG_PANEL_MIME)) return\n\t\te.preventDefault()\n\t\te.stopPropagation()\n\t\tsetDropZone(null)\n\t}\n\tconst handleTabStripDrop = (e: DragEvent<HTMLDivElement>): void => {\n\t\te.preventDefault()\n\t\te.stopPropagation()\n\t\tsetDropZone(null)\n\t\t// Only the deck's own MIME identifies a panel drag (a foreign `text/plain`\n\t\t// value may coincide with a registered id and must not drive a reorder).\n\t\tconst dragged = e.dataTransfer?.getData(DRAG_PANEL_MIME)\n\t\tif (!dragged || ctx.registry.get(dragged) === undefined || !ctx.isPanelInTree(dragged)) return\n\t\tconst tabRects = Array.from(\n\t\t\te.currentTarget.querySelectorAll<HTMLElement>('[data-deck-tab]'),\n\t\t).map((el) => {\n\t\t\tconst r = el.getBoundingClientRect()\n\t\t\treturn { left: r.left, width: r.width }\n\t\t})\n\t\tconst reorderIndex = computeReorderIndex(tabRects, e.clientX)\n\t\tctx.onRedock(node.id, node.active, dragged, 'center', reorderIndex)\n\t}\n\n\t// Panels that contribute a tab to the strip (`hideTab` panels carry their own\n\t// chrome — e.g. the simulator's device picker — so the engine tab is omitted).\n\t// When NONE remain the tab strip is not rendered at all and the body fills the\n\t// whole group region.\n\tconst visibleTabs = node.panels.filter((panelId) => !ctx.registry.get(panelId)?.hideTab)\n\n\t// FILL LAYOUT (FIX 2a): the group must be a flex COLUMN that fills its\n\t// allotted panel region so a leaf native slot can stretch to the full area.\n\t// Without this the group div is content-height (the tab strip only), the\n\t// active body / NativeSlot measures 0 height, the view-anchor publishes a\n\t// collapsed rect, and the simulator WebContentsView is invisible. The tab\n\t// strip is `shrink: 0`; the active body takes the remaining space (flex: 1).\n\treturn (\n\t\t<div\n\t\t\tref={setGroupRef}\n\t\t\tdata-deck-group={node.id}\n\t\t\tonDragOver={handleDragOver}\n\t\t\tonDragLeave={handleDragLeave}\n\t\t\tonDrop={handleDrop}\n\t\t\tstyle={{\n\t\t\t\tposition: 'relative',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\twidth: '100%',\n\t\t\t\theight: '100%',\n\t\t\t\tminWidth: 0,\n\t\t\t\tminHeight: 0,\n\t\t\t}}\n\t\t>\n\t\t\t{visibleTabs.length > 0 ? (\n\t\t\t\t<div\n\t\t\t\trole=\"tablist\"\n\t\t\t\tstyle={{ flexShrink: 0 }}\n\t\t\t\tonDragOver={handleTabStripDragOver}\n\t\t\t\tonDrop={handleTabStripDrop}\n\t\t\t>\n\t\t\t\t{visibleTabs.map((panelId) => {\n\t\t\t\t\tconst active = panelId === node.active\n\t\t\t\t\tconst descriptor = ctx.registry.get(panelId)\n\t\t\t\t\tconst title = descriptor?.title ?? panelId\n\t\t\t\t\t// PanelCapabilities (GOAL A source): a `draggable:false` panel's tab\n\t\t\t\t\t// cannot be picked up — omit the marker entirely (an absent attribute,\n\t\t\t\t\t// not `draggable=\"false\"`, matches the \"no drag source\" contract).\n\t\t\t\t\tconst isDraggable = descriptor?.draggable !== false\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tkey={panelId}\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\tdraggable={isDraggable ? 'true' : undefined}\n\t\t\t\t\t\t\tdata-deck-tab={panelId}\n\t\t\t\t\t\t\tdata-active={active ? 'true' : 'false'}\n\t\t\t\t\t\t\tonDragStart={(e) => {\n\t\t\t\t\t\t\t\t// A press that BEGINS on the close affordance must not drag the\n\t\t\t\t\t\t\t\t// whole tab: the HTML5 drag source is the draggable ANCESTOR\n\t\t\t\t\t\t\t\t// (this tab) regardless of which descendant the pointer landed\n\t\t\t\t\t\t\t\t// on, so a descendant's stopPropagation cannot cancel it —\n\t\t\t\t\t\t\t\t// detect the origin here and abort the drag. `closest` walks up\n\t\t\t\t\t\t\t\t// from the event target; a hit means the gesture started on ×.\n\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\te.target instanceof Element\n\t\t\t\t\t\t\t\t\t&& e.target.closest('[data-deck-tab-close]') !== null\n\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\te.preventDefault()\n\t\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Record the dragged panel id so the drop target can recover it.\n\t\t\t\t\t\t\t\te.dataTransfer.setData(DRAG_PANEL_MIME, panelId)\n\t\t\t\t\t\t\t\te.dataTransfer.setData('text/plain', panelId)\n\t\t\t\t\t\t\t\te.dataTransfer.effectAllowed = 'move'\n\t\t\t\t\t\t\t\t// Also expose it to the dragover indicator path (DataTransfer\n\t\t\t\t\t\t\t\t// values are unreadable during dragover).\n\t\t\t\t\t\t\t\tctx.activeDragPanelId.current = panelId\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tonDragEnd={() => {\n\t\t\t\t\t\t\t\tctx.activeDragPanelId.current = null\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tif (!active) ctx.onActivate(node.id, panelId)\n\t\t\t\t\t\t\t\telse ctx.onActiveTabClick?.(panelId)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{title}\n\t\t\t\t\t\t\t{/* Close affordance — a `role=\"button\"` SPAN (NOT a nested <button>:\n\t\t\t\t\t\t\t an interactive button may not be a descendant of another button —\n\t\t\t\t\t\t\t invalid HTML + illegal a11y nesting). `tabIndex={0}` + the\n\t\t\t\t\t\t\t Enter/Space keydown keep it keyboard/AT-operable. Suppressed when\n\t\t\t\t\t\t\t the WHOLE tree has one panel left (`canClose` false). Activate is\n\t\t\t\t\t\t\t kept off the tab (click stopPropagation) and a tab drag that\n\t\t\t\t\t\t\t begins here is cancelled by the tab's onDragStart guard above. */}\n\t\t\t\t\t\t\t{ctx.canClose && descriptor?.closable !== false\n\t\t\t\t\t\t\t\t? (\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\trole=\"button\"\n\t\t\t\t\t\t\t\t\t\ttabIndex={0}\n\t\t\t\t\t\t\t\t\t\tdata-deck-tab-close={panelId}\n\t\t\t\t\t\t\t\t\t\taria-label={`Close ${title}`}\n\t\t\t\t\t\t\t\t\t\tonPointerDown={(e) => e.stopPropagation()}\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault()\n\t\t\t\t\t\t\t\t\t\t\tctx.onClose(panelId)\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\t\t\t\t\tif (e.key === 'Enter' || e.key === ' ') {\n\t\t\t\t\t\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault()\n\t\t\t\t\t\t\t\t\t\t\t\tctx.onClose(panelId)\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t×\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t)\n\t\t\t\t})}\n\t\t\t</div>\n\t\t\t) : null}\n\t\t\t{renderActiveBody(node, ctx)}\n\t\t\t{dropZone ? <DropIndicator zone={dropZone} /> : null}\n\t\t</div>\n\t)\n}\n\n/**\n * Translate a drop zone into the indicator overlay's geometry. `center` covers\n * the whole group (a tab-join highlight); each edge zone is a HALF-band ribbon\n * pinned to that edge. Pure presentation — the host can re-skin via the\n * `data-deck-drop-zone` attribute; these inline styles are a sane default.\n */\nfunction dropIndicatorStyle(zone: DropZone): Record<string, string> {\n\tconst base: Record<string, string> = {\n\t\tposition: 'absolute',\n\t\t// `none` so the overlay never steals the drag-over/drop events from the\n\t\t// group beneath it (otherwise the indicator itself would become the target).\n\t\tpointerEvents: 'none',\n\t\tbackground: 'rgba(64, 128, 255, 0.25)',\n\t\toutline: '2px solid rgba(64, 128, 255, 0.6)',\n\t}\n\tswitch (zone) {\n\t\tcase 'left':\n\t\t\treturn { ...base, left: '0', top: '0', width: '50%', height: '100%' }\n\t\tcase 'right':\n\t\t\treturn { ...base, right: '0', top: '0', width: '50%', height: '100%' }\n\t\tcase 'top':\n\t\t\treturn { ...base, left: '0', top: '0', width: '100%', height: '50%' }\n\t\tcase 'bottom':\n\t\t\treturn { ...base, left: '0', bottom: '0', width: '100%', height: '50%' }\n\t\tcase 'center':\n\t\tdefault:\n\t\t\treturn { ...base, left: '0', top: '0', width: '100%', height: '100%' }\n\t}\n}\n\n/** The drop-zone highlight rendered over a group during a drag-over. */\nfunction DropIndicator(props: { zone: DropZone }): ReactNode {\n\treturn <div data-deck-drop-zone={props.zone} style={dropIndicatorStyle(props.zone)} />\n}\n","/**\n * `<DockView>` — React renderer for a layout-as-data tree.\n *\n * Pure function of the current model snapshot (+ registry + the two host\n * callbacks). It owns NO layout state: it subscribes to the `LayoutModel`,\n * keeps the latest tree in component state, and re-renders on every emission.\n * All structural targeting is via STABLE `data-*` attributes — see the\n * contract doc-block in `dock-view.test.tsx`.\n *\n * This file lives under `src/dock-react/` (NOT `src/layout/`), so importing\n * react here does not violate the pure-TS layout boundary. The stateful child\n * views (`SplitView`, `GroupView`) and the body renderer live in sibling\n * modules; this file owns the orchestration (`renderNode`) + the shared\n * `RenderContext`.\n */\nimport {\n\tcreateContext,\n\tuseCallback,\n\tuseContext,\n\tuseEffect,\n\tuseRef,\n\tuseState,\n\ttype ReactNode,\n} from 'react'\nimport {\n\tclosePanelForUser,\n\tcountPanels,\n\textractPanel,\n\tfindGroupById,\n\tfindPanelGroupId,\n\tmovePanel,\n\tsetActive,\n\tsetSizes,\n\tsplitPanel,\n} from '../layout/index.js'\nimport type {\n\tLayoutModel,\n\tLayoutNode,\n\tLayoutTree,\n\tPanelRegistry,\n\tSplitNode,\n\tTabGroupNode,\n} from '../layout/index.js'\nimport {\n\tdropZoneToMutation,\n\tisNoopRedock,\n\tresolveReorderInsertIndex,\n\ttype DropZone,\n} from './drag-redock.js'\nimport { SplitView } from './split-view.js'\nimport { GroupView } from './group-view.js'\n\nexport interface DockViewProps {\n\tmodel: LayoutModel\n\tregistry: PanelRegistry\n\t/**\n\t * Render a DOM panel's body. `opts.active` is `true` iff `panelId` is its tab\n\t * group's currently-active panel. Under DOM-panel keepalive every DOM panel in\n\t * a group is rendered (the inactive ones hidden), so this callback is invoked\n\t * for ALL of them and `opts.active` is RE-EVALUATED on every activation change\n\t * WITHOUT remounting the kept-alive subtree — letting a host run on-activation\n\t * side effects (e.g. data refresh) off the false→true `active` edge.\n\t */\n\trenderDomPanel: (panelId: string, opts: { active: boolean }) => ReactNode\n\tbindNativeSlot: (panelId: string, el: HTMLElement | null) => void\n\t/**\n\t * Opt-in: while a `dropPolicy:'reorder-only'` panel is dragged, paint NO drop\n\t * indicator over a group body (its own edges or any other group) — those are\n\t * no-op targets, so the highlight is misleading. Default `false` keeps the\n\t * geometry-only indicator (it paints where the pointer is; the capability\n\t * gate still rejects the drop). Hosts that want the cleaner \"no misleading\n\t * highlight\" UX pass `true`.\n\t */\n\tsuppressReorderOnlyDropIndicator?: boolean\n\t/**\n\t * Fires when the user clicks a tab that is ALREADY active. The host can use\n\t * this to toggle the panel's visibility (e.g. collapse the debug group).\n\t * When absent, clicking an active tab is a no-op (the existing behaviour).\n\t */\n\tonActiveTabClick?: (panelId: string) => void\n}\n\n/**\n * The current layout EPOCH — the model's revision, bumped once per committed\n * mutation. `DockView` provides it; descendant panels read it via\n * `useDockLayoutEpoch`.\n *\n * Its reason to exist: a native-view overlay anchored inside a dock panel\n * (a `WebContentsView` tracking its slot's geometry) re-publishes its bounds\n * only on GEOMETRY events (ResizeObserver / window-resize / splitter-drag). A\n * layout mutation that REORDERS a slot without resizing it — flipping a\n * fixed-width simulator column left↔right, moving a region — produces NO such\n * event (a same-size flex reorder fires nothing), so the overlay would freeze\n * at its old position. The epoch is the layout layer's explicit \"something\n * moved\" signal: a panel hosting a native overlay re-measures (e.g. pulses its\n * view-anchor) when the epoch changes, catching the translate the browser never\n * reports. Default 0 (outside a provider): the consuming effect runs once on\n * mount, which is harmless.\n */\nconst LayoutEpochContext = createContext<number>(0)\n\n/**\n * Read the current dock layout epoch (the model revision). Re-renders the caller\n * whenever a layout mutation commits. Keyed into a panel's effect deps, it lets\n * a native-overlay host re-measure on a reorder that fires no geometry event.\n * Returns 0 when used outside a `<DockView>`.\n */\nexport function useDockLayoutEpoch(): number {\n\treturn useContext(LayoutEpochContext)\n}\n\n/**\n * Apply a `reorder-only` panel's redock. Such a panel may ONLY reorder WITHIN\n * its own group: it never leaves the group (a center drop into another group)\n * and never edge-splits (any edge zone, own or other group) — those drops are\n * rejected here. The reorder anchors on the pointer-derived index when the\n * gesture supplies one (real drag), else on the active anchor's CURRENT index\n * (the imperative seam carries no pointer). The pointer index is a VISIBLE-tab\n * strip index (hideTab panels omitted, dragged tab's own slot counted); the\n * strip↔model coordinate reconciliation lives only here.\n */\nfunction applyReorderOnlyRedock(\n\tmodel: LayoutModel,\n\tregistry: PanelRegistry,\n\tdrop: {\n\t\tgroupId: string\n\t\tactivePanelId: string\n\t\tdraggedPanelId: string\n\t\tdraggedGroupId: string | undefined\n\t\tzone: DropZone\n\t\treorderIndex: number | undefined\n\t},\n): void {\n\tconst { groupId, activePanelId, draggedPanelId, draggedGroupId, zone, reorderIndex } = drop\n\tif (draggedGroupId === undefined || draggedGroupId !== groupId) return\n\tif (zone !== 'center') return\n\tconst group = findGroupById(model.get().root, groupId)\n\tlet index: number | undefined\n\tif (reorderIndex !== undefined && group) {\n\t\tconst visibleTabIds = group.panels.filter(\n\t\t\t(panelId) => registry.get(panelId)?.hideTab !== true,\n\t\t)\n\t\tindex = resolveReorderInsertIndex(group.panels, visibleTabIds, draggedPanelId, reorderIndex)\n\t}\n\telse {\n\t\tindex = group ? group.panels.indexOf(activePanelId) : undefined\n\t}\n\tmodel.apply((t) => movePanel(t, draggedPanelId, { groupId, index }))\n}\n\nexport function DockView(props: DockViewProps): ReactNode {\n\tconst { model, registry, renderDomPanel, bindNativeSlot, suppressReorderOnlyDropIndicator, onActiveTabClick } = props\n\n\t// Snapshot the canonical tree; re-render on every external emission. The\n\t// `epoch` mirrors the model revision (0 before the first apply) and is\n\t// provided to descendant panels via `LayoutEpochContext` so a native-overlay\n\t// host can re-measure on a reorder that fires no geometry event.\n\tconst [tree, setTree] = useState<LayoutTree>(() => model.get())\n\tconst [epoch, setEpoch] = useState<number>(0)\n\n\t// The panel id of the in-flight tab drag, INSTANCE-scoped (per DockView). Set\n\t// on `dragstart`, cleared on `dragend`/`drop`. The DataTransfer VALUE is\n\t// unreadable during `dragover` (only `types` is exposed, by spec), so the\n\t// drop-indicator path can't recover the dragged id from the event — it reads\n\t// it from here instead to decide whether to paint a highlight for THIS drag\n\t// (e.g. a `reorder-only` panel shows none). Held per instance (not module\n\t// scope) so two DockViews on one page never share a drag and unmount leaves no\n\t// residue. Threaded to every GroupView via the RenderContext.\n\tconst activeDragPanelId = useRef<string | null>(null)\n\n\tuseEffect(() => {\n\t\t// Re-sync immediately in case the model changed between the initial\n\t\t// `useState` read and effect commit, then track future emissions.\n\t\tsetTree(model.get())\n\t\tconst unsubscribe = model.subscribe((snap) => {\n\t\t\tsetTree(snap.tree)\n\t\t\tsetEpoch(snap.revision)\n\t\t})\n\t\treturn unsubscribe\n\t}, [model])\n\n\tconst handleActivate = useCallback(\n\t\t(groupId: string, panelId: string) => {\n\t\t\tmodel.apply((t) => setActive(t, groupId, panelId))\n\t\t},\n\t\t[model],\n\t)\n\n\t// Close write-back: every tab close funnels through the capability-aware\n\t// user action. The generic `closePanel` mutation remains available to\n\t// programmatic layout transforms.\n\tconst handleClose = useCallback(\n\t\t(panelId: string) => {\n\t\t\tmodel.apply((t) => closePanelForUser(t, panelId, registry))\n\t\t},\n\t\t[model, registry],\n\t)\n\n\t// Total panels across the WHOLE tree. The close affordance is suppressed only\n\t// when the entire layout has a single panel left (closing it would no-op);\n\t// a multi-panel single group still shows closes. GroupView sees only its own\n\t// node, so we compute the global count here and thread `canClose` down.\n\tconst canClose = countPanels(tree.root) > 1\n\n\t// Panel-id membership of the CURRENT tree (M2). A drop's payload must be a\n\t// panel that actually lives in the tree, not merely one that is registered —\n\t// see `handleDrop`. Derived from the snapshot the component already holds.\n\tconst isPanelInTree = useCallback(\n\t\t(panelId: string) => findPanelGroupId(tree.root, panelId) !== undefined,\n\t\t[tree],\n\t)\n\n\t// Resize write-back: a drag commit (or the `__deckApplyLayout` seam) funnels\n\t// new per-split weights here, which apply `setSizes` to the canonical model.\n\tconst handleApplyLayout = useCallback(\n\t\t(splitId: string, weights: number[]) => {\n\t\t\tmodel.apply((t) => setSizes(t, splitId, weights))\n\t\t},\n\t\t[model],\n\t)\n\n\t// Drag-to-redock commit. A tab dragged onto `groupId`'s `zone` (the GROUP seam\n\t// or a real dragover) lands here. `activePanelId` is the group's natural split\n\t// anchor (the visible body the user dropped onto). We translate the zone to an\n\t// engine-neutral descriptor, skip true no-ops (drop onto own tab center), and\n\t// apply the descriptor:\n\t// - move => single `movePanel` (joins the target tab group).\n\t// - split => extract-then-split COMPOSED in ONE `model.apply` so the whole\n\t// re-dock is a single atomic emission (one subscriber notification, one\n\t// re-render): `splitPanel` throws if the dragged panel already exists, so\n\t// an EXISTING panel must be `extractPanel`'d first, then split against the\n\t// target. Doing both inside one `apply` keeps the tree from ever being\n\t// observed in the transient extracted state.\n\tconst handleRedock = useCallback(\n\t\t(\n\t\t\tgroupId: string,\n\t\t\tactivePanelId: string,\n\t\t\tdraggedPanelId: string,\n\t\t\tzone: DropZone,\n\t\t\treorderIndex?: number,\n\t\t) => {\n\t\t\tconst target = { groupId, panelId: activePanelId }\n\t\t\t// The dragged panel's CURRENT group — needed to skip a center-drop back\n\t\t\t// into its own group (M2) and (via `dragged === target.panelId`) a\n\t\t\t// self-split (M1).\n\t\t\tconst draggedGroupId = findPanelGroupId(model.get().root, draggedPanelId)\n\n\t\t\t// ── PanelCapabilities gate (GOAL A source): a `draggable:false` panel is\n\t\t\t// a locked STRUCTURAL panel — it can never be torn into another region.\n\t\t\t// UI drag-start already refuses to lift it, but the imperative drop seam\n\t\t\t// bypasses that, so reject a locked dragged source here too (defense in\n\t\t\t// depth) — its position in the tree is fixed. ──\n\t\t\tif (registry.get(draggedPanelId)?.draggable === false) return\n\n\t\t\t// ── PanelCapabilities gate (GOAL A target): a group whose ACTIVE panel is\n\t\t\t// `draggable:false` is a locked drop ANCHOR — nothing may join or split\n\t\t\t// against it, in any zone. Checked before the no-op/reorder logic so a\n\t\t\t// locked simulator/editor can never absorb another panel. ──\n\t\t\tif (registry.get(activePanelId)?.draggable === false) return\n\n\t\t\t// ── PanelCapabilities gate (GOAL B): a `reorder-only` dragged panel is\n\t\t\t// routed to its dedicated handler BEFORE the no-op gate — its one legal\n\t\t\t// motion (a center drop into its OWN group) must OVERRIDE `isNoopRedock`\n\t\t\t// (which would swallow it as churn). ──\n\t\t\tif (registry.get(draggedPanelId)?.dropPolicy === 'reorder-only') {\n\t\t\t\tapplyReorderOnlyRedock(model, registry, {\n\t\t\t\t\tgroupId,\n\t\t\t\t\tactivePanelId,\n\t\t\t\t\tdraggedPanelId,\n\t\t\t\t\tdraggedGroupId,\n\t\t\t\t\tzone,\n\t\t\t\t\treorderIndex,\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (isNoopRedock(draggedPanelId, draggedGroupId, target, zone)) return\n\t\t\tconst mutation = dropZoneToMutation(zone, draggedPanelId, target)\n\t\t\tif (mutation.kind === 'move') {\n\t\t\t\tmodel.apply((t) => movePanel(t, mutation.panelId, { groupId: mutation.destGroupId }))\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Atomic extract-then-split for an existing dragged panel. Defensive\n\t\t\t// guard (M1): if extracting the dragged panel removed the split anchor,\n\t\t\t// abort instead of letting `splitPanel` throw on a missing anchor.\n\t\t\tmodel.apply((t) => {\n\t\t\t\tconst { tree } = extractPanel(t, mutation.newPanelId)\n\t\t\t\tif (findPanelGroupId(tree.root, mutation.atPanelId) === undefined) return t\n\t\t\t\treturn splitPanel(tree, mutation.atPanelId, mutation.dir, mutation.newPanelId, mutation.side)\n\t\t\t})\n\t\t},\n\t\t[model, registry],\n\t)\n\n\treturn (\n\t\t<LayoutEpochContext.Provider value={epoch}>\n\t\t\t{renderNode(tree.root, {\n\t\t\t\tregistry,\n\t\t\t\trenderDomPanel,\n\t\t\t\tbindNativeSlot,\n\t\t\t\tonActivate: handleActivate,\n\t\t\t\tonActiveTabClick,\n\t\t\t\tonApplyLayout: handleApplyLayout,\n\t\t\t\tonRedock: handleRedock,\n\t\t\t\tonClose: handleClose,\n\t\t\t\tcanClose,\n\t\t\t\tisPanelInTree,\n\t\t\t\tsuppressReorderOnlyDropIndicator: suppressReorderOnlyDropIndicator ?? false,\n\t\t\t\tactiveDragPanelId,\n\t\t\t})}\n\t\t</LayoutEpochContext.Provider>\n\t)\n}\n\n/** The shared per-render context threaded from `DockView` down through\n * `renderNode` into every `SplitView`/`GroupView`. Exported so the sibling view\n * modules type their `ctx` against the single source. */\nexport interface RenderContext {\n\tregistry: PanelRegistry\n\trenderDomPanel: (panelId: string, opts: { active: boolean }) => ReactNode\n\tbindNativeSlot: (panelId: string, el: HTMLElement | null) => void\n\tonActivate: (groupId: string, panelId: string) => void\n\tonActiveTabClick: ((panelId: string) => void) | undefined\n\tonApplyLayout: (splitId: string, weights: number[]) => void\n\tonRedock: (\n\t\tgroupId: string,\n\t\tactivePanelId: string,\n\t\tdraggedPanelId: string,\n\t\tzone: DropZone,\n\t\t/** Pointer-derived insertion index for a within-group reorder\n\t\t * (`dropPolicy:'reorder-only'`). Omitted by the imperative seam, which\n\t\t * falls back to the active anchor's current index. */\n\t\treorderIndex?: number,\n\t) => void\n\tonClose: (panelId: string) => void\n\t/** False when the whole tree has a single panel — suppresses every close\n\t * button so the layout can't be emptied (closePanel would no-op anyway). */\n\tcanClose: boolean\n\t/** True iff `panelId` is present in the CURRENT layout tree (M2). Registry\n\t * membership is NOT enough: a panel can be registered but absent from the tree\n\t * (closed out / never docked), and driving a re-dock on it makes the mutation\n\t * layer throw `panel not found`. A drop carrying such an id must be a no-op. */\n\tisPanelInTree: (panelId: string) => boolean\n\t/** See {@link DockViewProps.suppressReorderOnlyDropIndicator}. */\n\tsuppressReorderOnlyDropIndicator: boolean\n\t/** The in-flight tab-drag panel id, INSTANCE-scoped to the owning DockView\n\t * (set on `dragstart`, cleared on `dragend`/`drop`). The drop-indicator path\n\t * reads it during `dragover`, where the DataTransfer value is unreadable. */\n\tactiveDragPanelId: { current: string | null }\n}\n\nfunction renderNode(node: LayoutNode, ctx: RenderContext): ReactNode {\n\treturn node.kind === 'split'\n\t\t? renderSplit(node, ctx)\n\t\t: renderGroup(node, ctx)\n}\n\n/** A split is a stateful component (it holds the rrp Group imperative ref + runs\n * the M1 model→view sync effect), so render it via JSX with a STABLE key so\n * React preserves that ref / its kept-alive subtree across model emissions\n * rather than remounting on every snapshot. Mirrors `renderGroup`/`GroupView`. */\nfunction renderSplit(node: SplitNode, ctx: RenderContext): ReactNode {\n\treturn <SplitView key={node.id} node={node} ctx={ctx} />\n}\n\nfunction renderGroup(node: TabGroupNode, ctx: RenderContext): ReactNode {\n\t// A group is a stateful component (it tracks the live drop-hover zone during a\n\t// drag), so render it via JSX with a STABLE key so React preserves that hover\n\t// state across model emissions rather than remounting on every snapshot.\n\treturn <GroupView key={node.id} node={node} ctx={ctx} />\n}\n\nexport { renderNode }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab-group body rendering under DOM-panel keepalive: `renderActiveBody` (the
|
|
3
|
+
* body region) + `NativeSlot` (the anchor for an active native panel).
|
|
4
|
+
*/
|
|
5
|
+
import { type ReactNode } from 'react';
|
|
6
|
+
import type { TabGroupNode } from '../layout/index.js';
|
|
7
|
+
import type { RenderContext } from './dock-view.js';
|
|
8
|
+
/**
|
|
9
|
+
* Render a tab group's body region under DOM-panel KEEPALIVE.
|
|
10
|
+
*
|
|
11
|
+
* - DOM panels: ALL of the group's DOM panels are mounted SIMULTANEOUSLY, each
|
|
12
|
+
* under a STABLE React key (`dom-${panelId}`) so switching the active tab never
|
|
13
|
+
* remounts a body — React state + scroll persist across A→B→A. The active body
|
|
14
|
+
* fills the region (flex:1); inactive ones stay in the DOM but `display:none`.
|
|
15
|
+
* Each body's host renderer receives `{ active }` and is re-invoked with the
|
|
16
|
+
* new flag on every activation change (no remount), so a host can fire
|
|
17
|
+
* on-activation side effects off the false→true edge.
|
|
18
|
+
* - Native panels: EXEMPT from keepalive. The single ACTIVE native panel mounts a
|
|
19
|
+
* `NativeSlot` (keyed on the active id, so deactivation unmounts it and fires
|
|
20
|
+
* `bindNativeSlot(id, null)`); inactive native panels render nothing. Keeping a
|
|
21
|
+
* bound native slot mounted-but-hidden would collapse its WebContentsView rect.
|
|
22
|
+
*/
|
|
23
|
+
export declare function renderActiveBody(node: TabGroupNode, ctx: RenderContext): ReactNode;
|
|
24
|
+
//# sourceMappingURL=panel-body.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"panel-body.d.ts","sourceRoot":"","sources":["../../src/dock-react/panel-body.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAiC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,aAAa,GAAG,SAAS,CA6DlF"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure split-sizing math for the dock renderer — no react import.
|
|
3
|
+
*
|
|
4
|
+
* These functions translate between the model's raw per-child WEIGHTS (+ px
|
|
5
|
+
* constraints) and the percentage/ratio bases the react-resizable-panels Group
|
|
6
|
+
* consumes and reports. They are the single arithmetic authority behind
|
|
7
|
+
* `SplitView`'s model→view sync and resize write-back; several are exported so
|
|
8
|
+
* the sizing invariants can be unit-tested directly.
|
|
9
|
+
*/
|
|
10
|
+
import type { SizeConstraint } from '../layout/index.js';
|
|
11
|
+
/** Epsilon (in percentage points) within which two split layouts are treated as
|
|
12
|
+
* equivalent. Guards BOTH sides of the model↔view loop: we skip pushing a
|
|
13
|
+
* `setLayout` when the live layout already matches the target, and skip the
|
|
14
|
+
* `onLayoutChanged` write-back when the incoming layout matches the model — so
|
|
15
|
+
* `setLayout`→`onLayoutChanged`→write-back→re-sync cannot loop. */
|
|
16
|
+
export declare const LAYOUT_EPSILON = 0.5;
|
|
17
|
+
/** TIGHT tolerance (percentage points) for the BASIS-NORMALIZED flexible-ratio
|
|
18
|
+
* compare in `handleLayoutChanged`. We normalize the incoming layout's flexible
|
|
19
|
+
* subset to ratios summing to 100 and compare them against the model's flexible
|
|
20
|
+
* ratios (`computeFlexiblePercentages`, also summing to 100). If they match
|
|
21
|
+
* within this tolerance the `onLayoutChanged` is either our own `setLayout` echo
|
|
22
|
+
* OR a ratio-preserving spontaneous re-measure (mount / fixed-px re-pin /
|
|
23
|
+
* container resize) — SKIP the write-back. If they differ it is a genuine user
|
|
24
|
+
* resize (pointer OR keyboard) — WRITE BACK.
|
|
25
|
+
*
|
|
26
|
+
* Set to ~0.1pp: large enough to absorb rrp's ~3-decimal float noise on an echo,
|
|
27
|
+
* yet FAR below a real drag's delta. R1's "sub-0.5%" drag moves a panel ~0.33pp
|
|
28
|
+
* of the container; normalized over the two-flexible-child subset that is ~0.66pp
|
|
29
|
+
* of ratio — comfortably above 0.1, so it is NOT mistaken for an echo and is
|
|
30
|
+
* written back.
|
|
31
|
+
*
|
|
32
|
+
* CAVEAT: this is a flexible-RATIO tolerance (the flexible subset normalized to
|
|
33
|
+
* sum 100), NOT a container-%. It is safe against rrp's 3-decimal echo noise.
|
|
34
|
+
* In a pathologically WIDE split (~≥10 flexible children) a single arrow-key
|
|
35
|
+
* nudge (±~5% of the container on one child) can, once normalized over many
|
|
36
|
+
* flexible siblings, fall BELOW 0.1pp of ratio and be skipped — exotic and
|
|
37
|
+
* self-healing (the next, larger resize writes back). If that ever matters,
|
|
38
|
+
* scale the tolerance down by the flexible child count. */
|
|
39
|
+
export declare const FLEX_RATIO_TOLERANCE = 0.1;
|
|
40
|
+
/** Normalize raw split weights to percentages summing ~100 for `defaultSize`. */
|
|
41
|
+
export declare function toPercentages(sizes: readonly number[]): number[];
|
|
42
|
+
/**
|
|
43
|
+
* Compute the `defaultSize` percentage for each FLEXIBLE child, keyed by its
|
|
44
|
+
* ORIGINAL child index. Px-sized children (a non-null `constraint` — `fixedPx`
|
|
45
|
+
* locked OR `minPx` floored) are EXCLUDED from the pool: their size is px, not a
|
|
46
|
+
* weight, so it must never pollute the flexible siblings' normalization (FIX E1).
|
|
47
|
+
* `constraints[i]` non-null ⇒ child i is px-sized and absent from the returned map.
|
|
48
|
+
*/
|
|
49
|
+
export declare function computeFlexiblePercentages(sizes: readonly number[], constraints: readonly (SizeConstraint | null)[] | undefined): Map<number, number>;
|
|
50
|
+
/**
|
|
51
|
+
* The minimum weight a FLEXIBLE child may hold, given how many flexible children
|
|
52
|
+
* share the split (Bug #1). A flexible `<Panel>` has no rrp `minSize` by default
|
|
53
|
+
* (rrp floor is 0%), so a user can drag it to ~0 width; the resize write-back
|
|
54
|
+
* would then persist a ~0 weight and the panel comes back invisible/stuck.
|
|
55
|
+
*
|
|
56
|
+
* The floor is `min(1, floor(90 / flexCount))` — i.e. ~1 weight unit (a flexible
|
|
57
|
+
* split's weights are normalized to percentages downstream, so ~1 reads as ~1%
|
|
58
|
+
* of the flexible pool). With any realistic flexible count this is exactly 1; the
|
|
59
|
+
* `min(1, …)` only matters for a hypothetical 90+ flexible-child split. It is the
|
|
60
|
+
* SAME value used for the rrp `minSize` (A) and the write-back clamp (B) so the
|
|
61
|
+
* two defenses never disagree.
|
|
62
|
+
*/
|
|
63
|
+
export declare function flexibleFloor(flexCount: number): number;
|
|
64
|
+
/**
|
|
65
|
+
* Clamp the FLEXIBLE entries of a full-length weights array up to `floor` (Bug #1
|
|
66
|
+
* defense B). Px-sized children (a non-null constraint) are left untouched — their
|
|
67
|
+
* `sizes[i]` is a preserved placeholder, not a live weight. Returns a new array;
|
|
68
|
+
* a weight already ≥ floor is kept verbatim so a healthy ratio is undisturbed.
|
|
69
|
+
*/
|
|
70
|
+
export declare function clampFlexibleWeights(weights: readonly number[], constraints: readonly (SizeConstraint | null)[] | undefined): number[];
|
|
71
|
+
/** Are two panelId→percentage maps equivalent within `epsilon` percentage
|
|
72
|
+
* points? Both maps must cover EXACTLY the `ids` key set — a missing key OR an
|
|
73
|
+
* extra key (a key present in `a`/`b` but absent from `ids`) counts as NOT
|
|
74
|
+
* equivalent so the sync is not falsely suppressed. A non-finite value
|
|
75
|
+
* (NaN/±Infinity) is likewise NOT equivalent — `typeof NaN === 'number'` and
|
|
76
|
+
* `Math.abs(NaN - x) > eps` is `false`, so without the `Number.isFinite` guard a
|
|
77
|
+
* NaN would slip through as "equivalent" and wrongly suppress a legitimate
|
|
78
|
+
* sync/write-back (N1). EXPORTED (pure) for direct unit coverage. */
|
|
79
|
+
export declare function layoutsEquivalent(a: Record<string, number>, b: Record<string, number>, ids: readonly string[], epsilon?: number): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Build the panel-ID→PERCENTAGE map for an imperative `setLayout`, given the
|
|
82
|
+
* model's full-length raw `sizes` (per child) and `constraints`:
|
|
83
|
+
*
|
|
84
|
+
* - FIXED (px-pinned) children keep their CURRENT measured percentage (read
|
|
85
|
+
* from the live `getLayout()`); they are NOT derived from weights, so a
|
|
86
|
+
* flexible-weights change never disturbs their pixel lock.
|
|
87
|
+
* - The REMAINING percentage (100 − Σ fixed%) is distributed across the
|
|
88
|
+
* FLEXIBLE children in proportion to their weights.
|
|
89
|
+
*
|
|
90
|
+
* Returns `null` when the map can't be built faithfully (e.g. `live` is empty —
|
|
91
|
+
* jsdom's stub — or a fixed child's live % is missing), so the caller skips the
|
|
92
|
+
* `setLayout` rather than pushing a corrupt total. The result always sums to
|
|
93
|
+
* ~100 over all children.
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildSetLayoutMap(childIds: readonly string[], sizes: readonly number[], constraints: readonly (SizeConstraint | null)[] | undefined, live: Record<string, number>): Record<string, number> | null;
|
|
96
|
+
/**
|
|
97
|
+
* Extract an rrp `onLayoutChanged` map's FLEXIBLE subset (skipping fixed-px
|
|
98
|
+
* children) in child order and NORMALIZE it by its own sum to ratios summing to
|
|
99
|
+
* ~100 — the basis `computeFlexiblePercentages` already produces for the model,
|
|
100
|
+
* so the two are directly comparable. Returns the original child `indices`
|
|
101
|
+
* alongside the `ratios`. Null when the map is malformed (a flexible id is
|
|
102
|
+
* missing / non-finite) or there is no flexible child — the caller then never
|
|
103
|
+
* writes back from it.
|
|
104
|
+
*/
|
|
105
|
+
export declare function incomingFlexRatios(childIds: readonly string[], constraints: readonly (SizeConstraint | null)[] | undefined, layout: Record<string, number>): {
|
|
106
|
+
indices: number[];
|
|
107
|
+
ratios: number[];
|
|
108
|
+
} | null;
|
|
109
|
+
//# sourceMappingURL=split-sizing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"split-sizing.d.ts","sourceRoot":"","sources":["../../src/dock-react/split-sizing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD;;;;mEAImE;AACnE,eAAO,MAAM,cAAc,MAAM,CAAA;AAEjC;;;;;;;;;;;;;;;;;;;;;2DAqB2D;AAC3D,eAAO,MAAM,oBAAoB,MAAM,CAAA;AAEvC,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAQhE;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACzC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,GAAG,SAAS,GACzD,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAUrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAOvD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,WAAW,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,GAAG,SAAS,GACzD,MAAM,EAAE,CAOV;AAED;;;;;;;qEAOqE;AACrE,wBAAgB,iBAAiB,CAChC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACzB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACzB,GAAG,EAAE,SAAS,MAAM,EAAE,EACtB,OAAO,GAAE,MAAuB,GAC9B,OAAO,CAaT;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAChC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,GAAG,SAAS,EAC3D,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC1B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CA4B/B;AAsCD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,WAAW,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,GAAG,SAAS,EAC3D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CAYhD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SplitView` — one split node rendered as a react-resizable-panels Group, plus
|
|
3
|
+
* the model↔view sync machine (M1): it pushes the model's flexible weights into
|
|
4
|
+
* the live Group via `setLayout` and writes a genuine user resize back through
|
|
5
|
+
* `ctx.onApplyLayout`. The split-sizing arithmetic lives in `./split-sizing`.
|
|
6
|
+
*/
|
|
7
|
+
import { type ReactNode } from 'react';
|
|
8
|
+
import type { SplitNode } from '../layout/index.js';
|
|
9
|
+
import { type RenderContext } from './dock-view.js';
|
|
10
|
+
export interface SplitViewProps {
|
|
11
|
+
node: SplitNode;
|
|
12
|
+
ctx: RenderContext;
|
|
13
|
+
}
|
|
14
|
+
export declare function SplitView(props: SplitViewProps): ReactNode;
|
|
15
|
+
//# sourceMappingURL=split-view.d.ts.map
|