@dimina-kit/electron-deck 0.1.0-dev.20260616102751 → 0.1.0-dev.20260624040247
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 +26 -24
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/layout-client.d.ts +1 -1
- package/dist/dock-react/dock-view.d.ts +14 -9
- package/dist/dock-react/dock-view.d.ts.map +1 -1
- package/dist/dock-react/drag-redock.d.ts +15 -0
- package/dist/dock-react/drag-redock.d.ts.map +1 -1
- package/dist/dock-react/index.d.ts +2 -2
- package/dist/dock-react/index.d.ts.map +1 -1
- package/dist/dock-react/index.js +201 -42
- package/dist/dock-react/index.js.map +1 -1
- package/dist/{electron-deck-DfFKeFEC.js → electron-deck-C9_Yu3fd.js} +56 -62
- package/dist/electron-deck-C9_Yu3fd.js.map +1 -0
- package/dist/electron-deck.d.ts +2 -2
- package/dist/host/capability.d.ts +1 -1
- package/dist/host/control-bus.d.ts +3 -3
- package/dist/host/index.d.ts +1 -1
- package/dist/host/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/deck-app.d.ts +57 -63
- package/dist/internal/deck-app.d.ts.map +1 -1
- package/dist/internal/electron-types.d.ts +3 -3
- package/dist/internal/trust-set.d.ts +3 -3
- package/dist/internal/wire-transport.d.ts +5 -5
- package/dist/internal/wire-transport.d.ts.map +1 -1
- package/dist/layout/index.d.ts +3 -1
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +2 -2
- package/dist/layout/mutations.d.ts.map +1 -1
- package/dist/layout/sanitize.d.ts +27 -0
- package/dist/layout/sanitize.d.ts.map +1 -0
- package/dist/layout/serialize.d.ts.map +1 -1
- package/dist/layout/types.d.ts +55 -7
- package/dist/layout/types.d.ts.map +1 -1
- package/dist/layout/user-actions.d.ts +9 -0
- package/dist/layout/user-actions.d.ts.map +1 -0
- package/dist/{layout-CZtF0auJ.js → layout-oJQ9hind.js} +73 -6
- package/dist/layout-oJQ9hind.js.map +1 -0
- package/dist/main/compositor.d.ts +1 -1
- package/dist/main/index.d.ts +2 -2
- package/dist/main/view-handle.d.ts +8 -8
- package/dist/types.d.ts +2 -2
- package/dist/view-handle-CR-yWNfr.js.map +1 -1
- package/package.json +7 -5
- package/dist/electron-deck-DfFKeFEC.js.map +0 -1
- package/dist/layout-CZtF0auJ.js.map +0 -1
package/dist/layout/types.d.ts
CHANGED
|
@@ -8,13 +8,23 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export type Orientation = 'row' | 'column';
|
|
10
10
|
/**
|
|
11
|
-
* Per-child
|
|
12
|
-
* (validation lives in `serialize.ts`, not the type)
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Per-child pixel size constraint. EXACTLY ONE of the two keys is set; the value
|
|
12
|
+
* is a finite number > 0 (validation lives in `serialize.ts`, not the type):
|
|
13
|
+
*
|
|
14
|
+
* - `fixedPx` — LOCKED to exactly N px (min === max). A fixed child is excluded
|
|
15
|
+
* from the flexible weight pool: it never resizes, weight changes don't touch
|
|
16
|
+
* it.
|
|
17
|
+
* - `minPx` — a FLEXIBLE FLOOR: the child is weight-sized like an unconstrained
|
|
18
|
+
* child (participates in the flex pool, resizable), but never shrinks below N
|
|
19
|
+
* px. Use for a panel that must keep a minimum size while still being
|
|
20
|
+
* draggable (e.g. a simulator column floored at the device width).
|
|
21
|
+
*
|
|
22
|
+
* KEY SEMANTIC: "is this child fixed?" === "does the constraint carry `fixedPx`?"
|
|
23
|
+
* A `minPx` child is NOT fixed — it is flexible-with-a-floor.
|
|
15
24
|
*/
|
|
16
25
|
export interface SizeConstraint {
|
|
17
|
-
readonly fixedPx
|
|
26
|
+
readonly fixedPx?: number;
|
|
27
|
+
readonly minPx?: number;
|
|
18
28
|
}
|
|
19
29
|
export interface SplitNode {
|
|
20
30
|
readonly kind: 'split';
|
|
@@ -44,7 +54,45 @@ export interface LayoutTree {
|
|
|
44
54
|
readonly version: 1;
|
|
45
55
|
readonly root: LayoutNode;
|
|
46
56
|
}
|
|
47
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Per-panel drag/drop capability policy. All fields are OPTIONAL and
|
|
59
|
+
* DEFAULT-PERMISSIVE, so existing registrations keep today's behavior (a fully
|
|
60
|
+
* draggable, closable panel that may move/split freely). The fields are orthogonal:
|
|
61
|
+
* `draggable` governs whether the panel is a drag SOURCE / drop ANCHOR at all;
|
|
62
|
+
* `dropPolicy` governs where a draggable panel may LAND; `closable` governs
|
|
63
|
+
* whether DockView exposes the panel's close affordance.
|
|
64
|
+
*/
|
|
65
|
+
export interface PanelCapabilities {
|
|
66
|
+
/**
|
|
67
|
+
* When `false`: the panel's tab cannot be picked up (its tab is not
|
|
68
|
+
* `draggable`) AND it is not a valid drop ANCHOR — no other panel may
|
|
69
|
+
* join/split against the group while this panel is that group's active tab.
|
|
70
|
+
* `undefined` is treated as `true`.
|
|
71
|
+
*/
|
|
72
|
+
readonly draggable?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Drop/move policy for THIS panel when it is the one being dragged:
|
|
75
|
+
* - `'free'` (default): may move to any group or edge-split anywhere.
|
|
76
|
+
* - `'reorder-only'`: may ONLY reorder within its CURRENT tab group — it
|
|
77
|
+
* never leaves the group and never edge-splits.
|
|
78
|
+
* `undefined` is treated as `'free'`.
|
|
79
|
+
*/
|
|
80
|
+
readonly dropPolicy?: 'free' | 'reorder-only';
|
|
81
|
+
/**
|
|
82
|
+
* When `false`, DockView does not render a close affordance for this panel.
|
|
83
|
+
* `undefined` is treated as `true`.
|
|
84
|
+
*/
|
|
85
|
+
readonly closable?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* When `true`, this panel contributes NO tab to the group's tab strip. A
|
|
88
|
+
* group whose every panel hides its tab renders no tab strip at all (its body
|
|
89
|
+
* fills the whole region). Use for structural/chrome-owning panels that carry
|
|
90
|
+
* their own header (e.g. a simulator panel that draws its own device picker),
|
|
91
|
+
* where the engine tab would be redundant. `undefined` is treated as `false`.
|
|
92
|
+
*/
|
|
93
|
+
readonly hideTab?: boolean;
|
|
94
|
+
}
|
|
95
|
+
export interface DomPanelDescriptor extends PanelCapabilities {
|
|
48
96
|
readonly kind: 'dom';
|
|
49
97
|
readonly id: string;
|
|
50
98
|
readonly title?: string;
|
|
@@ -56,7 +104,7 @@ export interface DomPanelDescriptor {
|
|
|
56
104
|
export interface NativeHandleRef {
|
|
57
105
|
readonly id: string;
|
|
58
106
|
}
|
|
59
|
-
export interface NativePanelDescriptor {
|
|
107
|
+
export interface NativePanelDescriptor extends PanelCapabilities {
|
|
60
108
|
readonly kind: 'native';
|
|
61
109
|
readonly id: string;
|
|
62
110
|
readonly title?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/layout/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,QAAQ,CAAA;AAE1C
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/layout/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LayoutTree, PanelRegistry } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Close a panel only when its descriptor permits user closure.
|
|
4
|
+
*
|
|
5
|
+
* Missing descriptors and an omitted `closable` capability retain the historic
|
|
6
|
+
* close behavior. Only an explicit `closable:false` blocks the operation.
|
|
7
|
+
*/
|
|
8
|
+
export declare function closePanelForUser(tree: LayoutTree, panelId: string, registry: PanelRegistry): LayoutTree;
|
|
9
|
+
//# sourceMappingURL=user-actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-actions.d.ts","sourceRoot":"","sources":["../../src/layout/user-actions.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE3D;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAChC,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,aAAa,GACrB,UAAU,CAEZ"}
|
|
@@ -98,11 +98,14 @@ function collectProblems(root, knownPanelIds) {
|
|
|
98
98
|
continue;
|
|
99
99
|
}
|
|
100
100
|
const keys = Object.keys(c);
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
101
|
+
const hasFixed = keys.includes("fixedPx");
|
|
102
|
+
const hasMin = keys.includes("minPx");
|
|
103
|
+
if (keys.length !== 1 || !(hasFixed || hasMin)) problems.push(`split ${id ?? "?"}: constraint must have exactly one of 'fixedPx' or 'minPx', got [${keys.join(", ")}]`);
|
|
104
|
+
const cKey = hasFixed ? "fixedPx" : "minPx";
|
|
105
|
+
const cVal = hasFixed ? c.fixedPx : c.minPx;
|
|
106
|
+
if (typeof cVal !== "number" || !Number.isFinite(cVal) || cVal <= 0) problems.push(`split ${id ?? "?"}: constraint ${cKey} must be a finite number > 0, got ${String(cVal)}`);
|
|
104
107
|
}
|
|
105
|
-
if (everyConstrained) problems.push(`split ${id ?? "?"}: all children are
|
|
108
|
+
if (everyConstrained) problems.push(`split ${id ?? "?"}: all children are px-sized constraints; at least one must be weight-sized`);
|
|
106
109
|
}
|
|
107
110
|
}
|
|
108
111
|
if (!children) problems.push(`split ${id ?? "?"}: children is not an array`);
|
|
@@ -147,6 +150,52 @@ function parseLayout(json) {
|
|
|
147
150
|
};
|
|
148
151
|
}
|
|
149
152
|
//#endregion
|
|
153
|
+
//#region src/layout/sanitize.ts
|
|
154
|
+
/** The healed-up value for a collapsed flexible child. A small positive weight:
|
|
155
|
+
* enough to make the panel visible without stealing meaningful space from its
|
|
156
|
+
* healthy siblings (the user can re-drag afterwards). */
|
|
157
|
+
var HEALED_FLEX_WEIGHT = 1;
|
|
158
|
+
/** Is child `i` of `node` flexible (weight-sized) — i.e. has no px constraint? */
|
|
159
|
+
function isFlexibleAt(node, i) {
|
|
160
|
+
return (node.constraints?.[i] ?? null) === null;
|
|
161
|
+
}
|
|
162
|
+
function sanitizeNode(node) {
|
|
163
|
+
if (node.kind === "tabs") return node;
|
|
164
|
+
const children = node.children.map(sanitizeNode);
|
|
165
|
+
let changed = children.some((c, i) => c !== node.children[i]);
|
|
166
|
+
const sizes = node.sizes.map((w, i) => {
|
|
167
|
+
if (!isFlexibleAt(node, i)) return w;
|
|
168
|
+
if (typeof w === "number" && Number.isFinite(w) && w > 0) return w;
|
|
169
|
+
changed = true;
|
|
170
|
+
return HEALED_FLEX_WEIGHT;
|
|
171
|
+
});
|
|
172
|
+
if (!changed) return node;
|
|
173
|
+
const rebuilt = {
|
|
174
|
+
kind: "split",
|
|
175
|
+
id: node.id,
|
|
176
|
+
orientation: node.orientation,
|
|
177
|
+
children,
|
|
178
|
+
sizes
|
|
179
|
+
};
|
|
180
|
+
return node.constraints !== void 0 ? {
|
|
181
|
+
...rebuilt,
|
|
182
|
+
constraints: node.constraints
|
|
183
|
+
} : rebuilt;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Return a tree in which every FLEXIBLE child with a non-positive (≤ tiny
|
|
187
|
+
* threshold) weight is healed to a minimum positive weight, recursively. Px
|
|
188
|
+
* children are untouched. Pure: the input is never mutated; a fully-healthy tree
|
|
189
|
+
* is returned structurally unchanged (same weights).
|
|
190
|
+
*/
|
|
191
|
+
function sanitizeFlexibleWeights(tree) {
|
|
192
|
+
const root = sanitizeNode(tree.root);
|
|
193
|
+
return root === tree.root ? tree : {
|
|
194
|
+
version: 1,
|
|
195
|
+
root
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
150
199
|
//#region src/layout/mutations.ts
|
|
151
200
|
function tg(id, panels, active) {
|
|
152
201
|
return {
|
|
@@ -517,6 +566,24 @@ function createLayoutModel(initial) {
|
|
|
517
566
|
};
|
|
518
567
|
}
|
|
519
568
|
//#endregion
|
|
520
|
-
|
|
569
|
+
//#region src/layout/user-actions.ts
|
|
570
|
+
/**
|
|
571
|
+
* Capability-aware layout operations for user-triggered interactions.
|
|
572
|
+
*
|
|
573
|
+
* Keep these separate from the pure mutations: callers may still use
|
|
574
|
+
* `closePanel` for restore/migration/programmatic layout transforms, while UI
|
|
575
|
+
* entry points consistently honor registry capabilities.
|
|
576
|
+
*/
|
|
577
|
+
/**
|
|
578
|
+
* Close a panel only when its descriptor permits user closure.
|
|
579
|
+
*
|
|
580
|
+
* Missing descriptors and an omitted `closable` capability retain the historic
|
|
581
|
+
* close behavior. Only an explicit `closable:false` blocks the operation.
|
|
582
|
+
*/
|
|
583
|
+
function closePanelForUser(tree, panelId, registry) {
|
|
584
|
+
return registry.get(panelId)?.closable === false ? tree : closePanel(tree, panelId);
|
|
585
|
+
}
|
|
586
|
+
//#endregion
|
|
587
|
+
export { insertPanel as a, setConstraint as c, sanitizeFlexibleWeights as d, parseLayout as f, createPanelRegistry as h, extractPanel as i, setSizes as l, validateTree as m, createLayoutModel as n, movePanel as o, serializeLayout as p, closePanel as r, setActive as s, closePanelForUser as t, splitPanel as u };
|
|
521
588
|
|
|
522
|
-
//# sourceMappingURL=layout-
|
|
589
|
+
//# sourceMappingURL=layout-oJQ9hind.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout-oJQ9hind.js","names":[],"sources":["../src/layout/registry.ts","../src/layout/serialize.ts","../src/layout/sanitize.ts","../src/layout/mutations.ts","../src/layout/model.ts","../src/layout/user-actions.ts"],"sourcesContent":["/**\n * Panel registry — pure in-memory map of panel descriptors. Pure TS.\n */\nimport type { Disposable, PanelDescriptor, PanelRegistry } from './types.js'\n\nexport function createPanelRegistry(): PanelRegistry {\n\tconst map = new Map<string, PanelDescriptor>()\n\treturn {\n\t\tregister(p: PanelDescriptor): Disposable {\n\t\t\tmap.set(p.id, p)\n\t\t\treturn {\n\t\t\t\tdispose(): void {\n\t\t\t\t\t// Only drop if this exact descriptor is still registered, so a\n\t\t\t\t\t// re-register under the same id isn't undone by a stale handle.\n\t\t\t\t\tif (map.get(p.id) === p) map.delete(p.id)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\tget(id: string): PanelDescriptor | undefined {\n\t\t\treturn map.get(id)\n\t\t},\n\t\tlist(): readonly PanelDescriptor[] {\n\t\t\treturn [...map.values()]\n\t\t},\n\t}\n}\n","/**\n * Serialize / parse / validate — structural integrity + acyclicity. Pure TS.\n *\n * Default-DENY: parseLayout throws on any tree validateTree would flag.\n */\nimport type { LayoutNode, LayoutTree } from './types.js'\n\nconst MAX_DEPTH = 64\n\nexport function serializeLayout(t: LayoutTree): string {\n\treturn JSON.stringify(t)\n}\n\n/**\n * Walk an arbitrary (possibly malformed / cyclic / shared-ref) node graph and\n * collect structural problems. Never recurses infinitely: a node already on the\n * current path (cycle) or already visited elsewhere (shared ref) is reported and\n * not descended into.\n */\nfunction collectProblems(\n\troot: unknown,\n\tknownPanelIds: ReadonlySet<string> | null,\n): string[] {\n\tconst problems: string[] = []\n\tconst seenIds = new Set<string>()\n\tconst seenObjects = new Set<object>()\n\tconst panelOwners = new Map<string, string>()\n\n\tconst visit = (node: unknown, depth: number, path: Set<object>): void => {\n\t\tif (depth > MAX_DEPTH) {\n\t\t\tproblems.push(`depth exceeds ${MAX_DEPTH}`)\n\t\t\treturn\n\t\t}\n\t\tif (node === null || typeof node !== 'object') {\n\t\t\tproblems.push(`node is not an object: ${String(node)}`)\n\t\t\treturn\n\t\t}\n\t\tconst obj = node as object\n\t\tif (path.has(obj)) {\n\t\t\tproblems.push('cycle detected: node reachable from itself')\n\t\t\treturn\n\t\t}\n\t\tif (seenObjects.has(obj)) {\n\t\t\tproblems.push('shared node reference: same object appears twice')\n\t\t\treturn\n\t\t}\n\t\tseenObjects.add(obj)\n\n\t\tconst n = node as { kind?: unknown; id?: unknown }\n\t\tconst kind = n.kind\n\t\tconst id = typeof n.id === 'string' ? n.id : undefined\n\n\t\tif (id === undefined) {\n\t\t\tproblems.push(`node missing string id (kind=${String(kind)})`)\n\t\t}\n\t\telse if (seenIds.has(id)) {\n\t\t\tproblems.push(`duplicate node id: ${id}`)\n\t\t}\n\t\telse {\n\t\t\tseenIds.add(id)\n\t\t}\n\n\t\tif (kind === 'tabs') {\n\t\t\tconst tg = node as { panels?: unknown; active?: unknown }\n\t\t\tconst panels = Array.isArray(tg.panels) ? (tg.panels as unknown[]) : null\n\t\t\tif (!panels) {\n\t\t\t\tproblems.push(`tabs ${id ?? '?'}: panels is not an array`)\n\t\t\t}\n\t\t\telse {\n\t\t\t\tif (panels.length === 0) {\n\t\t\t\t\tproblems.push(`tabs ${id ?? '?'}: empty tabgroup`)\n\t\t\t\t}\n\t\t\t\tfor (const p of panels) {\n\t\t\t\t\tif (typeof p !== 'string') {\n\t\t\t\t\t\tproblems.push(`tabs ${id ?? '?'}: non-string panel id`)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tconst owner = panelOwners.get(p)\n\t\t\t\t\tif (owner !== undefined) {\n\t\t\t\t\t\tproblems.push(`duplicate panel id across groups: ${p}`)\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tpanelOwners.set(p, id ?? '?')\n\t\t\t\t\t}\n\t\t\t\t\tif (knownPanelIds && !knownPanelIds.has(p)) {\n\t\t\t\t\t\tproblems.push(`orphan panel not in known panel ids: ${p}`)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst active = tg.active\n\t\t\t\tif (typeof active !== 'string' || !panels.includes(active)) {\n\t\t\t\t\tproblems.push(`tabs ${id ?? '?'}: active ${String(active)} not in panels`)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse if (kind === 'split') {\n\t\t\tconst sp = node as { children?: unknown; sizes?: unknown; orientation?: unknown; constraints?: unknown }\n\t\t\tconst children = Array.isArray(sp.children) ? (sp.children as unknown[]) : null\n\t\t\tconst sizes = Array.isArray(sp.sizes) ? (sp.sizes as unknown[]) : null\n\t\t\tconst orientation = sp.orientation\n\t\t\tif (orientation !== 'row' && orientation !== 'column') {\n\t\t\t\tproblems.push(`split ${id ?? '?'}: invalid orientation ${String(orientation)}`)\n\t\t\t}\n\t\t\t// OPTIONAL constraints — validate the field's INTRINSIC format (array\n\t\t\t// shape + per-entry rules + exactly-one-of fixedPx/minPx + all-FIXED\n\t\t\t// guard) INDEPENDENTLY of `children` validity. Only the LENGTH-vs-children\n\t\t\t// comparison is gated on `children` being a valid array (done below).\n\t\t\tif (sp.constraints !== undefined) {\n\t\t\t\tconst constraints = Array.isArray(sp.constraints) ? (sp.constraints as unknown[]) : null\n\t\t\t\tif (!constraints) {\n\t\t\t\t\tproblems.push(`split ${id ?? '?'}: constraints is not an array`)\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// Tracks whether EVERY child is PX-SIZED (a non-null constraint —\n\t\t\t\t\t// `fixedPx` OR `minPx`). Both are sized in px and excluded from the\n\t\t\t\t\t// weight pool, so an all-constrained split trips the rrp footgun\n\t\t\t\t\t// (needs >= 1 weight-sized child); only a `null` child clears it.\n\t\t\t\t\tlet everyConstrained = constraints.length > 0\n\t\t\t\t\tfor (const c of constraints) {\n\t\t\t\t\t\tif (c === null) {\n\t\t\t\t\t\t\teveryConstrained = false\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (typeof c !== 'object') {\n\t\t\t\t\t\t\teveryConstrained = false\n\t\t\t\t\t\t\tproblems.push(`split ${id ?? '?'}: constraint is not null nor an object: ${String(c)}`)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Exactly ONE of `fixedPx` / `minPx`, value finite > 0.\n\t\t\t\t\t\tconst keys = Object.keys(c as object)\n\t\t\t\t\t\tconst hasFixed = keys.includes('fixedPx')\n\t\t\t\t\t\tconst hasMin = keys.includes('minPx')\n\t\t\t\t\t\tif (keys.length !== 1 || !(hasFixed || hasMin)) {\n\t\t\t\t\t\t\tproblems.push(`split ${id ?? '?'}: constraint must have exactly one of 'fixedPx' or 'minPx', got [${keys.join(', ')}]`)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst cKey = hasFixed ? 'fixedPx' : 'minPx'\n\t\t\t\t\t\tconst cVal = hasFixed ? (c as { fixedPx?: unknown }).fixedPx : (c as { minPx?: unknown }).minPx\n\t\t\t\t\t\tif (typeof cVal !== 'number' || !Number.isFinite(cVal) || cVal <= 0) {\n\t\t\t\t\t\t\tproblems.push(`split ${id ?? '?'}: constraint ${cKey} must be a finite number > 0, got ${String(cVal)}`)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// Guard the rrp footgun: an all-px-sized split has no weight-sized\n\t\t\t\t\t// child to absorb leftover space (rrp v4.10 requires >= 1).\n\t\t\t\t\tif (everyConstrained) {\n\t\t\t\t\t\tproblems.push(`split ${id ?? '?'}: all children are px-sized constraints; at least one must be weight-sized`)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!children) {\n\t\t\t\tproblems.push(`split ${id ?? '?'}: children is not an array`)\n\t\t\t}\n\t\t\telse {\n\t\t\t\tif (children.length < 2) {\n\t\t\t\t\tproblems.push(`split ${id ?? '?'}: must have >= 2 children, has ${children.length}`)\n\t\t\t\t}\n\t\t\t\tif (!sizes || sizes.length !== children.length) {\n\t\t\t\t\tproblems.push(\n\t\t\t\t\t\t`split ${id ?? '?'}: sizes ${sizes ? sizes.length : 'missing'} != children ${children.length}`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tfor (const s of sizes) {\n\t\t\t\t\t\tif (typeof s !== 'number' || !Number.isFinite(s)) {\n\t\t\t\t\t\t\tproblems.push(`split ${id ?? '?'}: non-finite size ${String(s)}`)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// LENGTH-vs-children comparison (gated on a valid children array).\n\t\t\t\tif (sp.constraints !== undefined && Array.isArray(sp.constraints)) {\n\t\t\t\t\tconst constraints = sp.constraints as unknown[]\n\t\t\t\t\tif (constraints.length !== children.length) {\n\t\t\t\t\t\tproblems.push(\n\t\t\t\t\t\t\t`split ${id ?? '?'}: constraints ${constraints.length} != children ${children.length}`,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst nextPath = new Set(path)\n\t\t\t\tnextPath.add(obj)\n\t\t\t\tfor (const child of children) {\n\t\t\t\t\tvisit(child, depth + 1, nextPath)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tproblems.push(`unknown node kind: ${String(kind)}`)\n\t\t}\n\t}\n\n\tvisit(root, 0, new Set())\n\treturn problems\n}\n\nexport function validateTree(t: LayoutTree, knownPanelIds: ReadonlySet<string>): string[] {\n\tif (t === null || typeof t !== 'object') return ['tree is not an object']\n\tif ((t as { version?: unknown }).version !== 1) {\n\t\treturn [`unsupported version: ${String((t as { version?: unknown }).version)}`]\n\t}\n\treturn collectProblems((t as { root?: unknown }).root, knownPanelIds)\n}\n\nexport function parseLayout(json: string): LayoutTree {\n\tlet raw: unknown\n\ttry {\n\t\traw = JSON.parse(json)\n\t}\n\tcatch {\n\t\tthrow new Error('parseLayout: input is not valid JSON')\n\t}\n\tif (raw === null || typeof raw !== 'object') {\n\t\tthrow new Error('parseLayout: top-level value is not an object')\n\t}\n\tconst obj = raw as { version?: unknown; root?: unknown }\n\tif (obj.version !== 1) {\n\t\tthrow new Error(`parseLayout: unsupported version ${String(obj.version)}`)\n\t}\n\tif (obj.root === undefined || obj.root === null) {\n\t\tthrow new Error('parseLayout: missing root')\n\t}\n\t// Validate structure with no knownPanelIds constraint (orphan check N/A on\n\t// pure deserialize — caller pairs with their own registry).\n\tconst problems = collectProblems(obj.root, null)\n\tif (problems.length > 0) {\n\t\tthrow new Error(`parseLayout: illegal layout — ${problems.join('; ')}`)\n\t}\n\treturn { version: 1, root: obj.root as LayoutNode }\n}\n","/**\n * Self-healing for persisted layout trees — pure TS (no react/electron).\n *\n * A FLEXIBLE child (`constraints[i]` null / no constraints) has no enforced\n * lower weight bound in the serialized tree: `validateTree` only rejects\n * NON-FINITE sizes, not non-positive ones. A user who dragged a flexible panel\n * to ~0 width writes a ~0 (or 0, or negative) weight into the tree; on the next\n * restore that panel comes back at 0 width, invisible and effectively stuck.\n *\n * `sanitizeFlexibleWeights` heals such a tree: every flexible child whose weight\n * is NON-POSITIVE (≤ 0 or non-finite) is lifted to a minimum positive weight, so\n * a restored panel is always visible. A small-but-POSITIVE weight is left as-is\n * — weights are a relative ratio (any positive scale is valid), so a tiny\n * positive weight is healthy, not collapsed. PX-SIZED children\n * (`fixedPx`/`minPx`) are sized in pixels, NOT weights — their `sizes[i]` entry\n * is a preserved-but-unused placeholder — so they are left untouched. The\n * function is pure (clone-on-write, never mutates the input).\n */\nimport type { LayoutNode, LayoutTree, SizeConstraint, SplitNode } from './types.js'\n\n/** The healed-up value for a collapsed flexible child. A small positive weight:\n * enough to make the panel visible without stealing meaningful space from its\n * healthy siblings (the user can re-drag afterwards). */\nconst HEALED_FLEX_WEIGHT = 1\n\n/** Is child `i` of `node` flexible (weight-sized) — i.e. has no px constraint? */\nfunction isFlexibleAt(node: SplitNode, i: number): boolean {\n\treturn (node.constraints?.[i] ?? null) === null\n}\n\nfunction sanitizeNode(node: LayoutNode): LayoutNode {\n\tif (node.kind === 'tabs') return node\n\n\t// Recurse first so nested splits are healed regardless of this level.\n\tconst children = node.children.map(sanitizeNode)\n\n\t// Heal this split's FLEXIBLE child weights. ONLY a NON-POSITIVE weight (≤ 0 or\n\t// non-finite — the \"dragged to 0 / negative\" collapse) is healed; every\n\t// POSITIVE weight is kept verbatim, however small. Weights are a RELATIVE ratio\n\t// (any positive scale is valid — `[0.001, 0.006]` is the same layout as\n\t// `[1, 6]`), so a small-but-positive weight is healthy and must not be rewritten\n\t// (that would crush the ratio). Px children are never touched.\n\tlet changed = children.some((c, i) => c !== node.children[i])\n\tconst sizes = node.sizes.map((w, i) => {\n\t\tif (!isFlexibleAt(node, i)) return w\n\t\tif (typeof w === 'number' && Number.isFinite(w) && w > 0) return w\n\t\tchanged = true\n\t\treturn HEALED_FLEX_WEIGHT\n\t})\n\n\tif (!changed) return node\n\tconst rebuilt: SplitNode = {\n\t\tkind: 'split',\n\t\tid: node.id,\n\t\torientation: node.orientation,\n\t\tchildren,\n\t\tsizes,\n\t}\n\t// Preserve the constraints array verbatim (only weights are touched).\n\treturn node.constraints !== undefined\n\t\t? { ...rebuilt, constraints: node.constraints as readonly (SizeConstraint | null)[] }\n\t\t: rebuilt\n}\n\n/**\n * Return a tree in which every FLEXIBLE child with a non-positive (≤ tiny\n * threshold) weight is healed to a minimum positive weight, recursively. Px\n * children are untouched. Pure: the input is never mutated; a fully-healthy tree\n * is returned structurally unchanged (same weights).\n */\nexport function sanitizeFlexibleWeights(tree: LayoutTree): LayoutTree {\n\tconst root = sanitizeNode(tree.root)\n\treturn root === tree.root ? tree : { version: 1, root }\n}\n","/**\n * Pure tree mutations. Every function returns a NEW tree and never mutates its\n * input (clone-on-write). All structural invariants (#1) are funnelled through\n * `normalizeRoot`, which collapses empty tabgroups + single-child splits,\n * cascading upward, while keeping the root a LayoutNode.\n */\nimport type { LayoutNode, LayoutTree, Orientation, SizeConstraint, SplitNode, TabGroupNode } from './types.js'\n\n// ───────────────────────── helpers ─────────────────────────\n\nfunction tg(id: string, panels: string[], active: string): TabGroupNode {\n\treturn { kind: 'tabs', id, panels, active }\n}\n\n/** Re-derive active after panels changed: keep current active if still present,\n * else select panels[min(removedIndex, len-1)] (clamp to last). */\nfunction deriveActive(panels: string[], prevActive: string, removedIndex: number): string {\n\tif (panels.length === 0) return ''\n\tif (panels.includes(prevActive)) return prevActive\n\tconst idx = Math.min(removedIndex, panels.length - 1)\n\treturn panels[Math.max(0, idx)]!\n}\n\n/**\n * Collapse a (already child-normalized) node:\n * - tabgroup with 0 panels => null (drop)\n * - split: drop null children; if 0 left => null; if 1 left => that child;\n * else rebuild with repaired sizes.\n */\nfunction normalize(node: LayoutNode): LayoutNode | null {\n\tif (node.kind === 'tabs') {\n\t\treturn node.panels.length === 0 ? null : node\n\t}\n\t// split: normalize children first.\n\tconst kept: LayoutNode[] = []\n\tconst keptSizes: number[] = []\n\tconst keptConstraints: (SizeConstraint | null)[] = []\n\tconst hadConstraints = node.constraints !== undefined\n\tnode.children.forEach((child, i) => {\n\t\tconst n = normalize(child)\n\t\tif (n !== null) {\n\t\t\tkept.push(n)\n\t\t\tkeptSizes.push(node.sizes[i] ?? 1)\n\t\t\tif (hadConstraints) keptConstraints.push(node.constraints![i] ?? null)\n\t\t}\n\t})\n\tif (kept.length === 0) return null\n\tif (kept.length === 1) return kept[0]!\n\tconst rebuilt: SplitNode = { kind: 'split', id: node.id, orientation: node.orientation, children: kept, sizes: keptSizes }\n\tif (!hadConstraints) return rebuilt\n\t// M3 repair: dropping children can leave a multi-child split whose survivors\n\t// are ALL fixed-px (the sole flexible child was the one removed). That tree is\n\t// rejected by validateTree (rrp needs >= 1 weight-sized child). Deterministically\n\t// clear the LAST survivor's constraint so >= 1 flexible child remains.\n\tconst allFixed = keptConstraints.length > 0 && !keptConstraints.some(c => c === null)\n\tif (allFixed) {\n\t\tkeptConstraints[keptConstraints.length - 1] = null\n\t}\n\treturn { ...rebuilt, constraints: keptConstraints }\n}\n\n/** Apply collapse to the root, guaranteeing the result is a LayoutNode. A root\n * that collapses to nothing is illegal upstream (we never empty the last group\n * without a replacement), so we only reach here with a survivor. */\nfunction normalizeRoot(root: LayoutNode): LayoutNode {\n\tconst n = normalize(root)\n\tif (n === null) {\n\t\tthrow new Error('mutation would empty the entire layout')\n\t}\n\treturn n\n}\n\ninterface Located {\n\tgroup: TabGroupNode\n}\n\nfunction findGroupContaining(root: LayoutNode, panelId: string): Located | null {\n\tlet found: TabGroupNode | null = null\n\tconst walk = (n: LayoutNode): void => {\n\t\tif (found) return\n\t\tif (n.kind === 'tabs') {\n\t\t\tif (n.panels.includes(panelId)) found = n\n\t\t}\n\t\telse {\n\t\t\tn.children.forEach(walk)\n\t\t}\n\t}\n\twalk(root)\n\treturn found ? { group: found } : null\n}\n\nfunction findGroupById(root: LayoutNode, groupId: string): TabGroupNode | null {\n\tlet found: TabGroupNode | null = null\n\tconst walk = (n: LayoutNode): void => {\n\t\tif (found) return\n\t\tif (n.kind === 'tabs') {\n\t\t\tif (n.id === groupId) found = n\n\t\t}\n\t\telse {\n\t\t\tn.children.forEach(walk)\n\t\t}\n\t}\n\twalk(root)\n\treturn found\n}\n\n/** Collect every panelId present anywhere in the tree. */\nfunction collectPanelIds(root: LayoutNode): Set<string> {\n\tconst out = new Set<string>()\n\tconst walk = (n: LayoutNode): void => {\n\t\tif (n.kind === 'tabs') {\n\t\t\tfor (const p of n.panels) out.add(p)\n\t\t}\n\t\telse {\n\t\t\tn.children.forEach(walk)\n\t\t}\n\t}\n\twalk(root)\n\treturn out\n}\n\n/** True if `panelId` exists in any tabgroup of the tree. */\nfunction hasPanel(root: LayoutNode, id: string): boolean {\n\treturn collectPanelIds(root).has(id)\n}\n\n/** Collect every node id present anywhere in the tree (splits + tabgroups). */\nfunction collectNodeIds(root: LayoutNode): Set<string> {\n\tconst out = new Set<string>()\n\tconst walk = (n: LayoutNode): void => {\n\t\tout.add(n.id)\n\t\tif (n.kind === 'split') n.children.forEach(walk)\n\t}\n\twalk(root)\n\treturn out\n}\n\n/**\n * Mint an id from `base` that is not already in `taken`. If `base` is free,\n * return it; otherwise append an incrementing `#N` suffix until free. Mutates\n * `taken` so successive calls in the same mutation can't collide with each other.\n */\nfunction freshId(taken: Set<string>, base: string): string {\n\tlet candidate = base\n\tlet n = 2\n\twhile (taken.has(candidate)) {\n\t\tcandidate = `${base}#${n}`\n\t\tn += 1\n\t}\n\ttaken.add(candidate)\n\treturn candidate\n}\n\nfunction findSplitById(root: LayoutNode, splitId: string): SplitNode | null {\n\tlet found: SplitNode | null = null\n\tconst walk = (n: LayoutNode): void => {\n\t\tif (found) return\n\t\tif (n.kind === 'split') {\n\t\t\tif (n.id === splitId) found = n\n\t\t\tn.children.forEach(walk)\n\t\t}\n\t}\n\twalk(root)\n\treturn found\n}\n\n/**\n * Rebuild the tree, replacing the node with matching id by `replacement`\n * (which may be null to delete — handled by collapse). This produces a fresh\n * object graph (clone-on-write); untouched branches are still rebuilt so the\n * input is never shared.\n */\nfunction replaceNode(node: LayoutNode, targetId: string, replacement: LayoutNode): LayoutNode {\n\tif (node.id === targetId) return replacement\n\tif (node.kind === 'tabs') return tg(node.id, [...node.panels], node.active)\n\tconst out: SplitNode = {\n\t\tkind: 'split',\n\t\tid: node.id,\n\t\torientation: node.orientation,\n\t\tchildren: node.children.map(c => replaceNode(c, targetId, replacement)),\n\t\tsizes: [...node.sizes],\n\t}\n\treturn node.constraints !== undefined ? { ...out, constraints: [...node.constraints] } : out\n}\n\nfunction cloneNode(node: LayoutNode): LayoutNode {\n\tif (node.kind === 'tabs') return tg(node.id, [...node.panels], node.active)\n\tconst out: SplitNode = {\n\t\tkind: 'split',\n\t\tid: node.id,\n\t\torientation: node.orientation,\n\t\tchildren: node.children.map(cloneNode),\n\t\tsizes: [...node.sizes],\n\t}\n\treturn node.constraints !== undefined ? { ...out, constraints: [...node.constraints] } : out\n}\n\nfunction wrap(root: LayoutNode): LayoutTree {\n\treturn { version: 1, root }\n}\n\n// ───────────────────────── mutations ─────────────────────────\n\nexport function setSizes(t: LayoutTree, splitId: string, sizes: readonly number[]): LayoutTree {\n\tconst target = findSplitById(t.root, splitId)\n\tif (!target) throw new Error(`setSizes: split not found: ${splitId}`)\n\tif (sizes.length !== target.children.length) {\n\t\tthrow new Error(\n\t\t\t`setSizes: sizes length ${sizes.length} != children length ${target.children.length}`,\n\t\t)\n\t}\n\tif (!sizes.every(s => Number.isFinite(s))) {\n\t\tthrow new Error(`setSizes: every size must be a finite number, got [${sizes.join(', ')}]`)\n\t}\n\tconst replacement: SplitNode = {\n\t\tkind: 'split',\n\t\tid: target.id,\n\t\torientation: target.orientation,\n\t\tchildren: target.children.map(cloneNode),\n\t\tsizes: [...sizes],\n\t}\n\t// setSizes must NOT touch constraints — carry them through unchanged.\n\tconst withConstraints: SplitNode = target.constraints !== undefined\n\t\t? { ...replacement, constraints: [...target.constraints] }\n\t\t: replacement\n\treturn wrap(replaceNode(t.root, splitId, withConstraints))\n}\n\n/**\n * Set (or clear) the fixed-px constraint on child `childIndex` of split\n * `splitId`. Pure (clone-on-write). Lazily materializes an all-null constraints\n * array aligned with `children` when absent, then writes the slot. Clearing\n * (`constraint === null`) keeps the array even if all entries become null.\n */\nexport function setConstraint(\n\tt: LayoutTree,\n\tsplitId: string,\n\tchildIndex: number,\n\tconstraint: SizeConstraint | null,\n): LayoutTree {\n\tconst target = findSplitById(t.root, splitId)\n\tif (!target) throw new Error(`setConstraint: split not found: ${splitId}`)\n\tif (!Number.isInteger(childIndex)) {\n\t\tthrow new Error(`setConstraint: childIndex must be an integer, got ${childIndex}`)\n\t}\n\tif (childIndex < 0 || childIndex >= target.children.length) {\n\t\tthrow new Error(\n\t\t\t`setConstraint: childIndex ${childIndex} out of range [0, ${target.children.length})`,\n\t\t)\n\t}\n\tconst base: (SizeConstraint | null)[] = target.constraints !== undefined\n\t\t? [...target.constraints]\n\t\t: target.children.map(() => null)\n\tbase[childIndex] = constraint\n\t// M3 guard: never produce an all-FIXED-px split. If applying this constraint\n\t// would leave EVERY child `fixedPx`-locked (0 weight-sized children),\n\t// `validateTree`/`parseLayout` would later reject the serialized tree (rrp\n\t// requires >= 1 weight-sized child). You cannot fixedPx-pin the LAST flexible\n\t// child — treat it as a NO-OP. A `minPx` child is FLEXIBLE (weight-sized with a\n\t// floor), so it does NOT count toward \"all fixed\" — pinning a `minPx` floor is\n\t// always allowed.\n\tif (base.length > 0 && base.every(c => c !== null)) {\n\t\treturn t\n\t}\n\tconst replacement: SplitNode = {\n\t\tkind: 'split',\n\t\tid: target.id,\n\t\torientation: target.orientation,\n\t\tchildren: target.children.map(cloneNode),\n\t\tsizes: [...target.sizes],\n\t\tconstraints: base,\n\t}\n\treturn wrap(replaceNode(t.root, splitId, replacement))\n}\n\nexport function setActive(t: LayoutTree, groupId: string, panelId: string): LayoutTree {\n\tconst group = findGroupById(t.root, groupId)\n\tif (!group) throw new Error(`setActive: group not found: ${groupId}`)\n\tif (!group.panels.includes(panelId)) {\n\t\tthrow new Error(`setActive: panel ${panelId} not in group ${groupId}`)\n\t}\n\tconst replacement = tg(group.id, [...group.panels], panelId)\n\treturn wrap(replaceNode(t.root, groupId, replacement))\n}\n\n/** Remove a panel from whichever group holds it; collapse. */\nfunction removePanel(root: LayoutNode, panelId: string): LayoutNode {\n\tconst located = findGroupContaining(root, panelId)\n\tif (!located) throw new Error(`panel not found: ${panelId}`)\n\tconst { group } = located\n\tconst removedIndex = group.panels.indexOf(panelId)\n\tconst panels = group.panels.filter(p => p !== panelId)\n\tconst replacement = tg(group.id, panels, deriveActive(panels, group.active, removedIndex))\n\tconst replaced = replaceNode(root, group.id, replacement)\n\treturn normalizeRoot(replaced)\n}\n\nexport function closePanel(t: LayoutTree, panelId: string): LayoutTree {\n\t// Last-panel no-op guard: closing the SOLE panel in the whole tree would\n\t// empty the layout (normalizeRoot throws). Instead return the tree unchanged\n\t// so hosts can wire a close button without special-casing the final panel.\n\t// An UNKNOWN id is NOT \"the last panel\" — only short-circuit when the panel\n\t// actually EXISTS and is the only one; otherwise fall through to removePanel,\n\t// which throws \"panel not found\" for an unknown id (behavior preserved).\n\tconst ids = collectPanelIds(t.root)\n\tif (ids.size === 1 && ids.has(panelId)) return t\n\treturn wrap(removePanel(t.root, panelId))\n}\n\nexport function extractPanel(\n\tt: LayoutTree,\n\tpanelId: string,\n): { tree: LayoutTree; extracted: string } {\n\tconst located = findGroupContaining(t.root, panelId)\n\tif (!located) throw new Error(`extractPanel: panel not found: ${panelId}`)\n\treturn { tree: wrap(removePanel(t.root, panelId)), extracted: panelId }\n}\n\nexport function insertPanel(\n\tt: LayoutTree,\n\tpanelId: string,\n\tdest: { groupId: string; index?: number },\n): LayoutTree {\n\tif (hasPanel(t.root, panelId)) {\n\t\tthrow new Error(`insertPanel: panel already exists in the tree: ${panelId}`)\n\t}\n\tconst group = findGroupById(t.root, dest.groupId)\n\tif (!group) throw new Error(`insertPanel: dest group not found: ${dest.groupId}`)\n\tconst panels = [...group.panels]\n\tconst idx = clampInsertIndex(dest.index, panels.length)\n\tpanels.splice(idx, 0, panelId)\n\tconst replacement = tg(group.id, panels, group.active)\n\treturn wrap(normalizeRoot(replaceNode(t.root, group.id, replacement)))\n}\n\nfunction clampInsertIndex(index: number | undefined, len: number): number {\n\tif (index === undefined) return len\n\tif (index < 0) return 0\n\tif (index > len) return len\n\treturn index\n}\n\nexport function movePanel(\n\tt: LayoutTree,\n\tpanelId: string,\n\tdest: { groupId: string; index?: number },\n): LayoutTree {\n\tconst src = findGroupContaining(t.root, panelId)\n\tif (!src) throw new Error(`movePanel: panel not found: ${panelId}`)\n\tconst destGroup = findGroupById(t.root, dest.groupId)\n\tif (!destGroup) throw new Error(`movePanel: dest group not found: ${dest.groupId}`)\n\n\tif (src.group.id === destGroup.id) {\n\t\t// Same-group reorder: remove then re-insert at the clamped index.\n\t\tconst without = src.group.panels.filter(p => p !== panelId)\n\t\tconst idx = clampInsertIndex(dest.index, without.length)\n\t\tconst panels = [...without]\n\t\tpanels.splice(idx, 0, panelId)\n\t\tconst replacement = tg(src.group.id, panels, src.group.active)\n\t\treturn wrap(normalizeRoot(replaceNode(t.root, src.group.id, replacement)))\n\t}\n\n\t// Cross-group: remove from source (re-derive active + maybe collapse), then\n\t// insert into dest. Compute the destination panels against the ORIGINAL dest\n\t// group so the clamp matches its pre-move length.\n\tconst removedIndex = src.group.panels.indexOf(panelId)\n\tconst srcPanels = src.group.panels.filter(p => p !== panelId)\n\tconst srcReplacement = tg(src.group.id, srcPanels, deriveActive(srcPanels, src.group.active, removedIndex))\n\n\tconst destPanels = [...destGroup.panels]\n\tconst idx = clampInsertIndex(dest.index, destPanels.length)\n\tdestPanels.splice(idx, 0, panelId)\n\tconst destReplacement = tg(destGroup.id, destPanels, destGroup.active)\n\n\tlet root = replaceNode(t.root, src.group.id, srcReplacement)\n\troot = replaceNode(root, destGroup.id, destReplacement)\n\treturn wrap(normalizeRoot(root))\n}\n\nexport function splitPanel(\n\tt: LayoutTree,\n\tatPanelId: string,\n\tdir: Orientation,\n\tnewPanelId: string,\n\tside: 'before' | 'after',\n): LayoutTree {\n\tif (hasPanel(t.root, newPanelId)) {\n\t\tthrow new Error(`splitPanel: new panel already exists in the tree: ${newPanelId}`)\n\t}\n\tconst located = findGroupContaining(t.root, atPanelId)\n\tif (!located) throw new Error(`splitPanel: panel not found: ${atPanelId}`)\n\tconst { group } = located\n\n\t// Generated node ids must be unique within the current tree: a node literally\n\t// named `${origGroupId}__sp` could already exist and produce a duplicate-id\n\t// tree. Mint collision-free ids off the deterministic bases.\n\tconst taken = collectNodeIds(t.root)\n\n\t// Extract atPanelId into its own new group.\n\tconst origGroupId = group.id\n\tconst extractedGroup = tg(freshId(taken, `${origGroupId}__split`), [atPanelId], atPanelId)\n\tconst newGroup = tg(freshId(taken, `${origGroupId}__new`), [newPanelId], newPanelId)\n\n\tconst children: LayoutNode[] = side === 'after'\n\t\t? [extractedGroup, newGroup]\n\t\t: [newGroup, extractedGroup]\n\tconst newSplit: SplitNode = {\n\t\tkind: 'split',\n\t\tid: freshId(taken, `${origGroupId}__sp`),\n\t\torientation: dir,\n\t\tchildren,\n\t\tsizes: [1, 1],\n\t}\n\n\tif (group.panels.length === 1) {\n\t\t// Replace the whole group with the new split.\n\t\treturn wrap(normalizeRoot(replaceNode(t.root, origGroupId, newSplit)))\n\t}\n\n\t// Multi-panel group: pull atPanelId out, keep the rest in the orig group,\n\t// and place the new split where the orig group was... but the orig group must\n\t// survive too. So: replace orig group with a split [remainingGroup, newSplit]?\n\t// The contract only requires: atPanel + newPanel live in different groups, and\n\t// the orig group keeps the remaining panels. Replace orig group with a split\n\t// of [origRemaining, newSplit] using `dir` orientation.\n\tconst removedIndex = group.panels.indexOf(atPanelId)\n\tconst remaining = group.panels.filter(p => p !== atPanelId)\n\tconst origRemainingGroup = tg(origGroupId, remaining, deriveActive(remaining, group.active, removedIndex))\n\n\tconst outerChildren: LayoutNode[] = side === 'after'\n\t\t? [origRemainingGroup, newSplit]\n\t\t: [newSplit, origRemainingGroup]\n\tconst outerSplit: SplitNode = {\n\t\tkind: 'split',\n\t\tid: freshId(taken, `${origGroupId}__outer`),\n\t\torientation: dir,\n\t\tchildren: outerChildren,\n\t\tsizes: [1, 1],\n\t}\n\treturn wrap(normalizeRoot(replaceNode(t.root, origGroupId, outerSplit)))\n}\n","/**\n * Observable layout model — single-writer, synchronous, call-ordered.\n *\n * INV-3: mutations assume an already-validated (acyclic, integrity-checked) tree\n * — callers feeding hand-built trees should parseLayout/validateTree first.\n *\n * Conventions (pinned by model.test.ts):\n * - revision starts at 0 for the initial tree.\n * - subscribe() does NOT replay; first emission is on the first successful\n * apply, carrying revision 1.\n * - +1 per successful apply; a throwing mutation does NOT advance revision and\n * does NOT notify (the throw propagates to the caller).\n */\nimport type { LayoutModel, LayoutSnapshot, LayoutTree } from './types.js'\n\nexport function createLayoutModel(initial: LayoutTree): LayoutModel {\n\t// Own the initial tree: take a structural snapshot so a later external mutation\n\t// of the caller's `initial` object can't leak into model state. The tree is\n\t// readonly-typed / treated as immutable, so a deep clone is the cheapest\n\t// correct ownership stance. (The `next` from each apply is already a fresh tree\n\t// produced by the pure mutations, so it needs no further clone.)\n\tlet current: LayoutTree = structuredClone(initial)\n\tlet revision = 0\n\tconst subscribers = new Set<(snap: LayoutSnapshot) => void>()\n\n\t// Re-entrancy guard: a subscriber may call apply() during notification. We\n\t// must not re-enter and bump `revision` mid-delivery (subscribers would then\n\t// observe a non-monotonic sequence). Instead, enqueue and drain FIFO so each\n\t// apply fully commits + notifies before the next begins.\n\tlet applying = false\n\tconst queue: ((t: LayoutTree) => LayoutTree)[] = []\n\n\tconst commitAndNotify = (mut: (t: LayoutTree) => LayoutTree): void => {\n\t\t// Compute first; if it throws, nothing below runs — revision stays put\n\t\t// and no subscriber is notified.\n\t\tconst next = mut(current)\n\t\tcurrent = next\n\t\trevision += 1\n\t\tconst snap: LayoutSnapshot = { tree: current, revision }\n\t\t// Snapshot the subscriber set so unsubscribe during delivery is safe.\n\t\t// Isolate subscriber errors: a throw from one must not abort delivery to\n\t\t// the rest, nor make apply() throw after a committed state. The model's\n\t\t// job is delivery, not subscriber correctness — swallow and continue.\n\t\tfor (const fn of [...subscribers]) {\n\t\t\ttry {\n\t\t\t\tfn(snap)\n\t\t\t}\n\t\t\tcatch {\n\t\t\t\t// intentionally swallowed — see above.\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tget(): LayoutTree {\n\t\t\treturn current\n\t\t},\n\t\t/**\n\t\t * `mut` MUST be pure (clone-on-write); the built-in mutations satisfy this.\n\t\t */\n\t\tapply(mut: (t: LayoutTree) => LayoutTree): void {\n\t\t\tif (applying) {\n\t\t\t\t// Re-entrant call (a subscriber applied during notification): queue it\n\t\t\t\t// and let the in-flight drain pass process it after the current pass.\n\t\t\t\tqueue.push(mut)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tapplying = true\n\t\t\ttry {\n\t\t\t\t// Top-level (non-re-entrant) apply: a throw here propagates to the\n\t\t\t\t// caller and does NOT advance revision / notify. (`finally` below\n\t\t\t\t// still resets `applying`, so a throw can't wedge the model.)\n\t\t\t\tcommitAndNotify(mut)\n\t\t\t\t// Drain re-entrant mutations enqueued during notification. Each queued\n\t\t\t\t// apply was deferred and is therefore fire-and-forget: its original\n\t\t\t\t// caller already returned, so it cannot deliver a synchronous throw\n\t\t\t\t// back. Run each as an INDEPENDENT transaction — a throw skips that\n\t\t\t\t// item (no revision bump, no notify) and we continue draining the rest\n\t\t\t\t// in FIFO order. This keeps a thrown queued mut from bubbling into the\n\t\t\t\t// already-committed outer apply() and from disrupting the order /\n\t\t\t\t// processing of later queued items.\n\t\t\t\twhile (queue.length > 0) {\n\t\t\t\t\tconst queuedMut = queue.shift()!\n\t\t\t\t\ttry {\n\t\t\t\t\t\tcommitAndNotify(queuedMut)\n\t\t\t\t\t}\n\t\t\t\t\tcatch {\n\t\t\t\t\t\t// Deferred apply: swallow + continue — see above.\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfinally {\n\t\t\t\tapplying = false\n\t\t\t}\n\t\t},\n\t\tsubscribe(fn: (snap: LayoutSnapshot) => void): () => void {\n\t\t\tsubscribers.add(fn)\n\t\t\treturn (): void => {\n\t\t\t\tsubscribers.delete(fn)\n\t\t\t}\n\t\t},\n\t}\n}\n","/**\n * Capability-aware layout operations for user-triggered interactions.\n *\n * Keep these separate from the pure mutations: callers may still use\n * `closePanel` for restore/migration/programmatic layout transforms, while UI\n * entry points consistently honor registry capabilities.\n */\nimport { closePanel } from './mutations.js'\nimport type { LayoutTree, PanelRegistry } from './types.js'\n\n/**\n * Close a panel only when its descriptor permits user closure.\n *\n * Missing descriptors and an omitted `closable` capability retain the historic\n * close behavior. Only an explicit `closable:false` blocks the operation.\n */\nexport function closePanelForUser(\n\ttree: LayoutTree,\n\tpanelId: string,\n\tregistry: PanelRegistry,\n): LayoutTree {\n\treturn registry.get(panelId)?.closable === false ? tree : closePanel(tree, panelId)\n}\n"],"mappings":"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"}
|
|
@@ -111,7 +111,7 @@ export interface Compositor {
|
|
|
111
111
|
/** Fold the intent state to EMPTY and commit, removing every native view
|
|
112
112
|
* this window's compositor mounted from the host. The resulting commit is
|
|
113
113
|
* REMOVALS-ONLY, so it reuses {@link commit}'s teardown-friendly
|
|
114
|
-
* "destroyed host + only removals → silent" path (
|
|
114
|
+
* "destroyed host + only removals → silent" path (commit failure semantics): on an
|
|
115
115
|
* already-destroyed host it makes zero host calls and throws nothing.
|
|
116
116
|
*
|
|
117
117
|
* Optional on the interface so a partial test double (or a caller that only
|
package/dist/main/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The connection layer (foundation.md §4): one `Connection` per trusted
|
|
5
5
|
* webContents, owning a single `DisposableRegistry` lifetime segment that tears
|
|
6
|
-
* down deterministically on hard-destroy or soft-reuse. devtools (and
|
|
7
|
-
* consume this as the substrate for connection-scoped resource ownership.
|
|
6
|
+
* down deterministically on hard-destroy or soft-reuse. devtools (and other
|
|
7
|
+
* downstream hosts) consume this as the substrate for connection-scoped resource ownership.
|
|
8
8
|
*/
|
|
9
9
|
export { createConnectionRegistry, type Connection, type ConnectionRegistry, } from './connection.js';
|
|
10
10
|
export { DisposableRegistry, toDisposable, type Disposable, type DisposeFn, } from './disposable.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ViewHandle` — the per-view orchestrator (
|
|
2
|
+
* `ViewHandle` — the per-view orchestrator (view-handle.md「placeIn 与挂载」/「dispose(viewScope LIFO 序)」/「moveTo 跨窗迁移」). One handle
|
|
3
3
|
* owns ONE native view and threads it through a window's z-planner. It composes
|
|
4
4
|
* three INJECTED primitives and nothing else (no deck-app, no Electron):
|
|
5
5
|
* - a {@link NativeView} (its `ref` + a `setBounds` sink) — the native surface;
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
* (scope.ts cross-layer LIFO): the native view is detached and the placement
|
|
13
13
|
* sink goes inert.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* handle 直接驱动 bounds. The handle drives `setBounds` DIRECTLY on its native view; the
|
|
16
16
|
* Compositor stays a pure z-order planner (mount/unmount/commit only) and never
|
|
17
17
|
* sees geometry.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
19
|
+
* per-window teardown 顺序 (view-handle.md「dispose(viewScope LIFO 序)」). The viewScope owns the native detach
|
|
20
20
|
* FIRST and the sink-disable LAST, so LIFO teardown runs the sink-disable
|
|
21
21
|
* (STEP0) BEFORE the detach (STEP1): a late `place` frame can never drive a
|
|
22
22
|
* native effect on a half-torn-down view.
|
|
23
23
|
*
|
|
24
|
-
* Cross-window move (
|
|
24
|
+
* Cross-window move (view-handle.md「moveTo 跨窗迁移」/ compositor-and-teardown.md「moveTo 事务状态机」). {@link ViewHandle.moveTo}
|
|
25
25
|
* migrates the view to another `{ compositor, windowScope }` as TWO independent
|
|
26
26
|
* Compositor commits, guarded by a per-view async mutex (THE migrationLock —
|
|
27
27
|
* each handle is one view). The current placement is a MUTABLE token so the
|
|
@@ -51,9 +51,9 @@ export type Placement = {
|
|
|
51
51
|
visible: false;
|
|
52
52
|
};
|
|
53
53
|
/** The native surface a handle drives: its z-order identity (`ref`) plus the
|
|
54
|
-
* `setBounds` sink the handle calls directly (
|
|
54
|
+
* `setBounds` sink the handle calls directly (handle 直接驱动 bounds). `destroy` (optional)
|
|
55
55
|
* destroys the backing native view (its WebContents) — owned by the viewScope so
|
|
56
|
-
* it runs on teardown AFTER the detach (keepAlive
|
|
56
|
+
* it runs on teardown AFTER the detach (keepAlive「保活寿命归 Scope、淘汰策略归 host」lifetime/leak fix).
|
|
57
57
|
* Optional so fakes that don't model a native WebContents stay valid.
|
|
58
58
|
*
|
|
59
59
|
* `webContents` / `capturePage` (optional) expose the backing native view's
|
|
@@ -84,7 +84,7 @@ export interface ViewHandle {
|
|
|
84
84
|
* detaches (unmount + commit) but keeps the native view alive. */
|
|
85
85
|
applyPlacement(p: Placement): void;
|
|
86
86
|
/**
|
|
87
|
-
* Cross-window move (
|
|
87
|
+
* Cross-window move (view-handle.md「moveTo 跨窗迁移」/ compositor-and-teardown.md「moveTo 事务状态机」). Migrate the view from its
|
|
88
88
|
* current placement (`src`) to `dest` as TWO independent Compositor commits,
|
|
89
89
|
* serialized by a per-view async mutex (migrationLock):
|
|
90
90
|
*
|
|
@@ -95,7 +95,7 @@ export interface ViewHandle {
|
|
|
95
95
|
*
|
|
96
96
|
* On success the compositor token moves to `dest` (later `applyPlacement`
|
|
97
97
|
* drives the dest host). With `rehome:true`, the viewScope is re-parented under
|
|
98
|
-
* dest's `windowScope` (lifetime follows display).
|
|
98
|
+
* dest's `windowScope` (lifetime follows display). moveTo 迁移显示而非寿命: moveTo moves DISPLAY
|
|
99
99
|
* (and, with rehome, LIFETIME) — it does NOT carry capability grants; the dest
|
|
100
100
|
* window's own control layer issues its own grant. Terminal (Promise<void>, not
|
|
101
101
|
* chainable).
|
package/dist/types.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface Disposable {
|
|
|
16
16
|
* `runtime.view` / `DeckViewHandle`, `runtime.scopes` / `DeckSession`,
|
|
17
17
|
* `runtime.grants`, `runtime.layout` — is fully built and wired, but has NO
|
|
18
18
|
* production consumer yet: the only callers are `examples/layout-demo` and
|
|
19
|
-
* `spike/popout`. The devtools
|
|
19
|
+
* `spike/popout`. The devtools (and other downstream) host integrates through the `RuntimeBackend`
|
|
20
20
|
* lifecycle path with `ownsWindows:true` and never touches this surface.
|
|
21
21
|
*
|
|
22
22
|
* Until a SECOND, real consumer adopts it, these signatures are NOT API-stable —
|
|
@@ -300,7 +300,7 @@ export interface ViewCreateOptions {
|
|
|
300
300
|
* `runtime.scopes.create()` is accepted (provenance check). */
|
|
301
301
|
readonly scope?: DeckSession;
|
|
302
302
|
/**
|
|
303
|
-
* Opt-in keep-alive eviction policy (
|
|
303
|
+
* Opt-in keep-alive eviction policy (opt-in LRU helper). When set, the framework disposes
|
|
304
304
|
* the least-recently-VISIBLE HIDDEN view in this view's group once the group's
|
|
305
305
|
* HIDDEN count exceeds `max` — destroying that view's native WebContents.
|
|
306
306
|
* Currently-visible views are NEVER evicted. Views sharing the same `max` form
|