@adea-ai/ui 0.64.0 → 0.65.0

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.
Files changed (39) hide show
  1. package/README.md +50 -0
  2. package/dist/NOTICE +59 -1
  3. package/dist/components/layout/split-layout/drop.d.ts +13 -0
  4. package/dist/components/layout/split-layout/drop.d.ts.map +1 -0
  5. package/dist/components/layout/split-layout/drop.js +50 -0
  6. package/dist/components/layout/split-layout/drop.js.map +1 -0
  7. package/dist/components/layout/split-layout/geometry.d.ts +14 -0
  8. package/dist/components/layout/split-layout/geometry.d.ts.map +1 -0
  9. package/dist/components/layout/split-layout/geometry.js +44 -0
  10. package/dist/components/layout/split-layout/geometry.js.map +1 -0
  11. package/dist/components/layout/split-layout/index.d.ts +5 -0
  12. package/dist/components/layout/split-layout/index.d.ts.map +1 -0
  13. package/dist/components/layout/split-layout/index.js +4 -0
  14. package/dist/components/layout/split-layout/model.d.ts +51 -0
  15. package/dist/components/layout/split-layout/model.d.ts.map +1 -0
  16. package/dist/components/layout/split-layout/model.js +255 -0
  17. package/dist/components/layout/split-layout/model.js.map +1 -0
  18. package/dist/components/layout/split-layout/split-layout.d.ts +30 -0
  19. package/dist/components/layout/split-layout/split-layout.d.ts.map +1 -0
  20. package/dist/components/layout/split-layout/split-layout.js +312 -0
  21. package/dist/components/layout/split-layout/split-layout.js.map +1 -0
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -1
  25. package/dist/r/registry.json +41 -0
  26. package/dist/r/split-layout.json +41 -0
  27. package/dist/r/src/components/layout/split-layout/drop.ts +29 -0
  28. package/dist/r/src/components/layout/split-layout/geometry.ts +40 -0
  29. package/dist/r/src/components/layout/split-layout/index.ts +4 -0
  30. package/dist/r/src/components/layout/split-layout/model.ts +336 -0
  31. package/dist/r/src/components/layout/split-layout/split-layout.tsx +346 -0
  32. package/package.json +10 -2
  33. package/registry.json +41 -0
  34. package/src/components/layout/split-layout/drop.ts +29 -0
  35. package/src/components/layout/split-layout/geometry.ts +40 -0
  36. package/src/components/layout/split-layout/index.ts +4 -0
  37. package/src/components/layout/split-layout/model.ts +336 -0
  38. package/src/components/layout/split-layout/split-layout.tsx +346 -0
  39. package/src/index.ts +1 -0
package/README.md CHANGED
@@ -218,6 +218,56 @@ acceptance and each application's mounted chat journey remain separate gates.
218
218
 
219
219
  Apache-2.0.
220
220
 
221
+ ## Binary layout model checkpoint
222
+
223
+ UI issue #22 extracts the existing attributed bb/Muxy Adea adaptation rather
224
+ than introducing another layout tree. `createLayoutState`, `splitPane`,
225
+ `movePane`, `closePane`, `undoClosePane`, `focusPane`, `swapPanes`,
226
+ `resizeSplit`, `normalizeLayout` and the reading-order helpers are pure.
227
+ Leaves extend `{kind: 'leaf', id: string}` with any host-owned payload. Closing
228
+ the last leaf calls an injected placeholder factory; it never selects a harness,
229
+ starts a terminal or deletes a project. Eight leaves/depth eight and 10–90%
230
+ ratios preserve the accepted contract. Construct or validate a tree before
231
+ editing it; the shared constructor validates visual structure and identities,
232
+ while applications still decode/scoped-persist preferences and retain unknown
233
+ future versions for recovery.
234
+
235
+ The model and maintained accessible renderer are implemented in this draft.
236
+ Stable content ownership and movement are exercised in component fixtures.
237
+ The packed renderer gate includes the UI #20 size-token correction and the host
238
+ presentation fixture. It checks the real package in compiled and Solid
239
+ conditions; complete shell composition, production adoption and release remain
240
+ required.
241
+ No persistent state, session services, native drag region or host framework is
242
+ imported into this model.
243
+
244
+ ### Host pane presentation hooks
245
+
246
+ `SplitLayout` can keep its default pane header while allowing a host to supply
247
+ inline visible content with `renderPaneLabel`, opt pane regions into keyboard
248
+ Tab navigation with `paneTabIndex={0}`, and provide a domain-specific splitter
249
+ name with `labelForSeparator`. Pane regions remain programmatically focusable
250
+ by default (`paneTabIndex={-1}`), and separators keep their orientation-based
251
+ names unless the host supplies a label.
252
+
253
+ `renderPaneLabel` receives a stable `Accessor<L>` once for each leaf owner.
254
+ Read `leaf()` inside the returned JSX so Solid tracks payload changes while the
255
+ same pane owner is retained during split and move operations. This visible
256
+ header content does not define the region's accessible name or the close
257
+ button's name: both continue to come from `labelForLeaf`, so hosts should
258
+ provide an accessible domain label there as well.
259
+
260
+ The `HostPresentation` story shows a decorative icon and host title, keyboard
261
+ pane stops, and a workspace-specific separator name. A narrow-width component
262
+ fixture also checks that a long host title does not widen its pane header.
263
+
264
+ `bun run check:packed-layout` packs the real artifact, installs it in a disposable
265
+ consumer with peers omitted and scripts disabled, and exercises the direct model
266
+ subpath in native Node and the Solid source condition. It verifies zero runtime
267
+ imports and a 3 KiB gzip model budget (measured baseline: 2,455 bytes). This is
268
+ model-subpath evidence; it does not waive UI #16's package attribution/conditional
269
+ export corrections or the separate root compatibility gate.
270
+
221
271
  ### Controlled appearance editor
222
272
 
223
273
  `AppearanceEditor` and `AppearancePopover` retain the accepted Zeron-derived
package/dist/NOTICE CHANGED
@@ -49,6 +49,65 @@ Origin UI / coss (AGPL-3.0-or-later) was consulted for visual ideas only. No
49
49
  code, class string, or asset from that project is reproduced here, because its
50
50
  licence is incompatible with this distribution.
51
51
 
52
+ ## Strict binary layout extracted from the accepted Adea donor port
53
+
54
+ `packages/ui/src/components/layout/split-layout/model.ts` is extracted from
55
+ Adea `packages/dev-view/src/layout/operations.ts` at
56
+ `0322ab09ff5e9775bfddc0bf2d81e1df436dbef7`, whose MIT portions substantially
57
+ translate bb `apps/app/src/lib/split-layout/ops.ts` at
58
+ `52a9256373d4d36f9b60e9e2a7f333464091a2ac` and Muxy
59
+ `Muxy/Models/Workspace/SplitNode.swift` at
60
+ `5c5be8697c57a2fe70cda97fdbaf7c912e2e31b6`.
61
+
62
+ Copyright (c) 2026 Michael Yong
63
+ Copyright (c) 2026 Muxy
64
+
65
+ Permission is hereby granted, free of charge, to any person obtaining a copy
66
+ of this software and associated documentation files (the "Software"), to deal
67
+ in the Software without restriction, including without limitation the rights
68
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
69
+ copies of the Software, and to permit persons to whom the Software is
70
+ furnished to do so, subject to the following conditions:
71
+
72
+ The above copyright notice and this permission notice shall be included in all
73
+ copies or substantial portions of the Software.
74
+
75
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
76
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
77
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
78
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
79
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
80
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
81
+ SOFTWARE.
82
+
83
+ The accepted strict binary representation, reading-order insertion, parent
84
+ collapse, exact leaf identity, resize/normalize, move-at-cap and undo semantics
85
+ are retained. Opaque host leaves replace private domain types and the host
86
+ supplies the last-pane placeholder. Construction validates visual identities
87
+ and structural bounds; application scope/persistence, thread/runtime identity,
88
+ harness launch and process authority are excluded. Nearest test behavior from
89
+ Adea `packages/dev-view/tests/layout.test.ts` and bb `ops.test.ts` is preserved
90
+ where it applies to the accepted binary model. The accessible renderer and
91
+ application adoption remain separate gates under UI issue #22.
92
+
93
+ ### Binary frame renderer continuation
94
+
95
+ `geometry.ts` translates `areaFrames` from the pinned Muxy source above. Its
96
+ fractional frames, horizontal/vertical offsets and exact node references are
97
+ retained; ratio repair uses the accepted 0.1–0.9 range rather than Muxy's 0–1.
98
+ `split-layout.tsx` adapts the accepted Adea pane-header/close/focus behavior
99
+ through generic callbacks, stable keyed leaf owners and Corvu separators.
100
+ It excludes application commands, persistence, harness/thread authority and
101
+ manual pointer listeners. This renderer is independently written against the
102
+ accepted model; it is not a port of Muxy's native UI or an additional layout tree.
103
+ The MIT attribution and permission above cover the translated frame algorithm.
104
+
105
+ The pane-drag continuation retains Adea's nearest-edge placement ordering and
106
+ pane-title gesture, replacing plaintext leaf IDs and string casts with typed
107
+ intent, an instance-local active drag payload, and cancellation/removal checks.
108
+ Header action composition injects application keyboard controls without owning
109
+ shortcuts or domain transitions. No new upstream source or license is introduced.
110
+
52
111
  Busy composer action translated from KiroCrew
53
112
  --------------------------------------------
54
113
  Original: website/src/components/BusySendButton.tsx
@@ -71,7 +130,6 @@ action and unavailable reasons. No runtime queue or force-reset authority added.
71
130
  Nearest tests: BusySendButton.menuKeyboard.test.tsx and
72
131
  BusySendButton.slotScope.test.tsx; controlled mode replaces donor storage tests,
73
132
  while keyboard/focus expectations are exercised in Chromium and WebKit.
74
-
75
133
  ## Plain transcript follow translated from KiroCrew
76
134
 
77
135
  `packages/ui/src/components/conversation/scroll-follow-core.ts`,
@@ -0,0 +1,13 @@
1
+ import type { SplitLayoutBranch } from './model';
2
+ export type PaneDropIntent = {
3
+ direction: SplitLayoutBranch['direction'];
4
+ placement: 'before' | 'after';
5
+ };
6
+ /** Accepted Adea closest-edge intent; invalid/outside geometry cannot authorize a move. */
7
+ export declare function paneDropIntent(clientX: number, clientY: number, rect: {
8
+ left: number;
9
+ top: number;
10
+ width: number;
11
+ height: number;
12
+ }): PaneDropIntent | undefined;
13
+ //# sourceMappingURL=drop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drop.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/split-layout/drop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAChD,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,CAAA;IACzC,SAAS,EAAE,QAAQ,GAAG,OAAO,CAAA;CAC9B,CAAA;AACD,2FAA2F;AAC3F,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjE,cAAc,GAAG,SAAS,CAkB5B"}
@@ -0,0 +1,50 @@
1
+ //#region src/components/layout/split-layout/drop.ts
2
+ /** Accepted Adea closest-edge intent; invalid/outside geometry cannot authorize a move. */
3
+ function paneDropIntent(clientX, clientY, rect) {
4
+ const { left, top, width, height } = rect;
5
+ if (![
6
+ clientX,
7
+ clientY,
8
+ left,
9
+ top,
10
+ width,
11
+ height
12
+ ].every(Number.isFinite) || width <= 0 || height <= 0) return;
13
+ const x = clientX - left;
14
+ const y = clientY - top;
15
+ if (x < 0 || y < 0 || x > width || y > height) return;
16
+ return [
17
+ {
18
+ distance: x,
19
+ intent: {
20
+ direction: "row",
21
+ placement: "before"
22
+ }
23
+ },
24
+ {
25
+ distance: width - x,
26
+ intent: {
27
+ direction: "row",
28
+ placement: "after"
29
+ }
30
+ },
31
+ {
32
+ distance: y,
33
+ intent: {
34
+ direction: "column",
35
+ placement: "before"
36
+ }
37
+ },
38
+ {
39
+ distance: height - y,
40
+ intent: {
41
+ direction: "column",
42
+ placement: "after"
43
+ }
44
+ }
45
+ ].reduce((best, next) => next.distance < best.distance ? next : best).intent;
46
+ }
47
+ //#endregion
48
+ export { paneDropIntent };
49
+
50
+ //# sourceMappingURL=drop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drop.js","names":[],"sources":["../../../../src/components/layout/split-layout/drop.ts"],"sourcesContent":["import type { SplitLayoutBranch } from './model'\nexport type PaneDropIntent = {\n direction: SplitLayoutBranch['direction']\n placement: 'before' | 'after'\n}\n/** Accepted Adea closest-edge intent; invalid/outside geometry cannot authorize a move. */\nexport function paneDropIntent(\n clientX: number,\n clientY: number,\n rect: { left: number; top: number; width: number; height: number }\n): PaneDropIntent | undefined {\n const { left, top, width, height } = rect\n if (\n ![clientX, clientY, left, top, width, height].every(Number.isFinite) ||\n width <= 0 ||\n height <= 0\n )\n return\n const x = clientX - left\n const y = clientY - top\n if (x < 0 || y < 0 || x > width || y > height) return\n const candidates: { distance: number; intent: PaneDropIntent }[] = [\n { distance: x, intent: { direction: 'row', placement: 'before' } },\n { distance: width - x, intent: { direction: 'row', placement: 'after' } },\n { distance: y, intent: { direction: 'column', placement: 'before' } },\n { distance: height - y, intent: { direction: 'column', placement: 'after' } },\n ]\n return candidates.reduce((best, next) => (next.distance < best.distance ? next : best)).intent\n}\n"],"mappings":";;AAMA,SAAgB,eACd,SACA,SACA,MAC4B;CAC5B,MAAM,EAAE,MAAM,KAAK,OAAO,WAAW;CACrC,IACE,CAAC;EAAC;EAAS;EAAS;EAAM;EAAK;EAAO;CAAM,CAAC,CAAC,MAAM,OAAO,QAAQ,KACnE,SAAS,KACT,UAAU,GAEV;CACF,MAAM,IAAI,UAAU;CACpB,MAAM,IAAI,UAAU;CACpB,IAAI,IAAI,KAAK,IAAI,KAAK,IAAI,SAAS,IAAI,QAAQ;CAO/C,OAAO;EALL;GAAE,UAAU;GAAG,QAAQ;IAAE,WAAW;IAAO,WAAW;GAAS;EAAE;EACjE;GAAE,UAAU,QAAQ;GAAG,QAAQ;IAAE,WAAW;IAAO,WAAW;GAAQ;EAAE;EACxE;GAAE,UAAU;GAAG,QAAQ;IAAE,WAAW;IAAU,WAAW;GAAS;EAAE;EACpE;GAAE,UAAU,SAAS;GAAG,QAAQ;IAAE,WAAW;IAAU,WAAW;GAAQ;EAAE;CAEvE,CAAA,CAAW,QAAQ,MAAM,SAAU,KAAK,WAAW,KAAK,WAAW,OAAO,IAAK,CAAC,CAAC;AAC1F"}
@@ -0,0 +1,14 @@
1
+ import { type SplitLayoutLeaf, type SplitLayoutNode } from './model';
2
+ export type LayoutRect = Readonly<{
3
+ x: number;
4
+ y: number;
5
+ width: number;
6
+ height: number;
7
+ }>;
8
+ export type LayoutFrame<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readonly<{
9
+ node: SplitLayoutNode<L>;
10
+ rect: LayoutRect;
11
+ }>;
12
+ /** Stable leaf DOM can use these fractions without being reparented into a changing nested tree. */
13
+ export declare function computeLayoutFrames<L extends SplitLayoutLeaf>(node: SplitLayoutNode<L>): ReadonlyMap<string, LayoutFrame<L>>;
14
+ //# sourceMappingURL=geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/split-layout/geometry.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,eAAe,EACrB,MAAM,SAAS,CAAA;AAChB,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAC1F,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI,QAAQ,CAAC;IAC9E,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,CAAA;IACxB,IAAI,EAAE,UAAU,CAAA;CACjB,CAAC,CAAA;AACF,oGAAoG;AACpG,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,eAAe,EAC3D,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,GACvB,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAoBrC"}
@@ -0,0 +1,44 @@
1
+ import { MAX_SPLIT_RATIO, MIN_SPLIT_RATIO } from "./model.js";
2
+ //#region src/components/layout/split-layout/geometry.ts
3
+ /** Stable leaf DOM can use these fractions without being reparented into a changing nested tree. */
4
+ function computeLayoutFrames(node) {
5
+ const frames = /* @__PURE__ */ new Map();
6
+ const visit = (current, rect) => {
7
+ frames.set(current.id, {
8
+ node: current,
9
+ rect
10
+ });
11
+ if (current.kind === "leaf") return;
12
+ const ratio = Number.isFinite(current.ratio) ? Math.min(MAX_SPLIT_RATIO, Math.max(MIN_SPLIT_RATIO, current.ratio)) : .5;
13
+ const row = current.direction === "row";
14
+ const first = row ? {
15
+ ...rect,
16
+ width: rect.width * ratio
17
+ } : {
18
+ ...rect,
19
+ height: rect.height * ratio
20
+ };
21
+ const second = row ? {
22
+ ...rect,
23
+ x: rect.x + first.width,
24
+ width: rect.width - first.width
25
+ } : {
26
+ ...rect,
27
+ y: rect.y + first.height,
28
+ height: rect.height - first.height
29
+ };
30
+ visit(current.children[0], first);
31
+ visit(current.children[1], second);
32
+ };
33
+ visit(node, {
34
+ x: 0,
35
+ y: 0,
36
+ width: 1,
37
+ height: 1
38
+ });
39
+ return frames;
40
+ }
41
+ //#endregion
42
+ export { computeLayoutFrames };
43
+
44
+ //# sourceMappingURL=geometry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.js","names":[],"sources":["../../../../src/components/layout/split-layout/geometry.ts"],"sourcesContent":["/*\n * Translated from Muxy/Models/Workspace/SplitNode.swift areaFrames at\n * 5c5be8697c57a2fe70cda97fdbaf7c912e2e31b6.\n * Copyright (c) 2026 Muxy. SPDX-License-Identifier: MIT. See NOTICE.\n */\nimport {\n MAX_SPLIT_RATIO,\n MIN_SPLIT_RATIO,\n type SplitLayoutLeaf,\n type SplitLayoutNode,\n} from './model'\nexport type LayoutRect = Readonly<{ x: number; y: number; width: number; height: number }>\nexport type LayoutFrame<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readonly<{\n node: SplitLayoutNode<L>\n rect: LayoutRect\n}>\n/** Stable leaf DOM can use these fractions without being reparented into a changing nested tree. */\nexport function computeLayoutFrames<L extends SplitLayoutLeaf>(\n node: SplitLayoutNode<L>\n): ReadonlyMap<string, LayoutFrame<L>> {\n const frames = new Map<string, LayoutFrame<L>>()\n const visit = (current: SplitLayoutNode<L>, rect: LayoutRect) => {\n frames.set(current.id, { node: current, rect })\n if (current.kind === 'leaf') return\n const ratio = Number.isFinite(current.ratio)\n ? Math.min(MAX_SPLIT_RATIO, Math.max(MIN_SPLIT_RATIO, current.ratio))\n : 0.5\n const row = current.direction === 'row'\n const first = row\n ? { ...rect, width: rect.width * ratio }\n : { ...rect, height: rect.height * ratio }\n const second = row\n ? { ...rect, x: rect.x + first.width, width: rect.width - first.width }\n : { ...rect, y: rect.y + first.height, height: rect.height - first.height }\n visit(current.children[0], first)\n visit(current.children[1], second)\n }\n visit(node, { x: 0, y: 0, width: 1, height: 1 })\n return frames\n}\n"],"mappings":";;;AAiBA,SAAgB,oBACd,MACqC;CACrC,MAAM,yBAAS,IAAI,IAA4B;CAC/C,MAAM,SAAS,SAA6B,SAAqB;EAC/D,OAAO,IAAI,QAAQ,IAAI;GAAE,MAAM;GAAS;EAAK,CAAC;EAC9C,IAAI,QAAQ,SAAS,QAAQ;EAC7B,MAAM,QAAQ,OAAO,SAAS,QAAQ,KAAK,IACvC,KAAK,IAAI,iBAAiB,KAAK,IAAI,iBAAiB,QAAQ,KAAK,CAAC,IAClE;EACJ,MAAM,MAAM,QAAQ,cAAc;EAClC,MAAM,QAAQ,MACV;GAAE,GAAG;GAAM,OAAO,KAAK,QAAQ;EAAM,IACrC;GAAE,GAAG;GAAM,QAAQ,KAAK,SAAS;EAAM;EAC3C,MAAM,SAAS,MACX;GAAE,GAAG;GAAM,GAAG,KAAK,IAAI,MAAM;GAAO,OAAO,KAAK,QAAQ,MAAM;EAAM,IACpE;GAAE,GAAG;GAAM,GAAG,KAAK,IAAI,MAAM;GAAQ,QAAQ,KAAK,SAAS,MAAM;EAAO;EAC5E,MAAM,QAAQ,SAAS,IAAI,KAAK;EAChC,MAAM,QAAQ,SAAS,IAAI,MAAM;CACnC;CACA,MAAM,MAAM;EAAE,GAAG;EAAG,GAAG;EAAG,OAAO;EAAG,QAAQ;CAAE,CAAC;CAC/C,OAAO;AACT"}
@@ -0,0 +1,5 @@
1
+ /** Issue-backed binary layout with stable opaque leaf owners; scoped persistence belongs to the host. */
2
+ export * from './model';
3
+ export * from './geometry';
4
+ export * from './split-layout';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/split-layout/index.ts"],"names":[],"mappings":"AAAA,yGAAyG;AACzG,cAAc,SAAS,CAAA;AACvB,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
@@ -0,0 +1,4 @@
1
+ import { MAX_LAYOUT_DEPTH, MAX_LAYOUT_LEAVES, MAX_SPLIT_RATIO, MIN_SPLIT_RATIO, closePane, countLeaves, createLayoutState, focusPane, layoutDepth, listLeaves, movePane, neighborLeaf, normalizeLayout, resizeSplit, splitPane, swapPanes, undoClosePane } from "./model.js";
2
+ import { computeLayoutFrames } from "./geometry.js";
3
+ import { SplitLayout } from "./split-layout.js";
4
+ export { MAX_LAYOUT_DEPTH, MAX_LAYOUT_LEAVES, MAX_SPLIT_RATIO, MIN_SPLIT_RATIO, SplitLayout, closePane, computeLayoutFrames, countLeaves, createLayoutState, focusPane, layoutDepth, listLeaves, movePane, neighborLeaf, normalizeLayout, resizeSplit, splitPane, swapPanes, undoClosePane };
@@ -0,0 +1,51 @@
1
+ /** A visual identity only. Extend this with host-owned payload; UI never interprets it. */
2
+ export type SplitLayoutLeaf = Readonly<{
3
+ kind: 'leaf';
4
+ id: string;
5
+ }>;
6
+ export type SplitLayoutBranch<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readonly<{
7
+ kind: 'split';
8
+ id: string;
9
+ direction: 'row' | 'column';
10
+ ratio: number;
11
+ children: readonly [SplitLayoutNode<L>, SplitLayoutNode<L>];
12
+ }>;
13
+ export type SplitLayoutNode<L extends SplitLayoutLeaf = SplitLayoutLeaf> = L | SplitLayoutBranch<L>;
14
+ export declare const MAX_LAYOUT_LEAVES = 8;
15
+ export declare const MAX_LAYOUT_DEPTH = 8;
16
+ export declare const MIN_SPLIT_RATIO = 0.1;
17
+ export declare const MAX_SPLIT_RATIO = 0.9;
18
+ export type SplitLayoutState<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readonly<{
19
+ center: SplitLayoutNode<L>;
20
+ focusedLeafId: string;
21
+ closed: readonly Readonly<{
22
+ center: SplitLayoutNode<L>;
23
+ leafId: string;
24
+ }>[];
25
+ }>;
26
+ export type SplitPaneInput<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readonly<{
27
+ direction: SplitLayoutBranch<L>['direction'];
28
+ placement: 'before' | 'after';
29
+ leaf: L;
30
+ splitId: string;
31
+ }>;
32
+ export declare function listLeaves<L extends SplitLayoutLeaf>(node: SplitLayoutNode<L>): readonly L[];
33
+ export declare function countLeaves<L extends SplitLayoutLeaf>(node: SplitLayoutNode<L>): number;
34
+ export declare function layoutDepth<L extends SplitLayoutLeaf>(node: SplitLayoutNode<L>): number;
35
+ export declare function createLayoutState<L extends SplitLayoutLeaf>(center: SplitLayoutNode<L>): SplitLayoutState<L>;
36
+ export declare function splitPane<L extends SplitLayoutLeaf>(state: SplitLayoutState<L>, targetLeafId: string, input: SplitPaneInput<L>): SplitLayoutState<L>;
37
+ export declare function closePane<L extends SplitLayoutLeaf>(state: SplitLayoutState<L>, leafId: string, createPlaceholderLeaf: () => L): SplitLayoutState<L>;
38
+ export declare function undoClosePane<L extends SplitLayoutLeaf>(state: SplitLayoutState<L>): SplitLayoutState<L>;
39
+ export declare function focusPane<L extends SplitLayoutLeaf>(state: SplitLayoutState<L>, leafId: string): SplitLayoutState<L>;
40
+ export declare function swapPanes<L extends SplitLayoutLeaf>(state: SplitLayoutState<L>, firstLeafId: string, secondLeafId: string): SplitLayoutState<L>;
41
+ export declare function movePane<L extends SplitLayoutLeaf>(state: SplitLayoutState<L>, leafId: string, targetLeafId: string, placement: 'before' | 'after', direction: SplitLayoutBranch<L>['direction'], splitId: string): SplitLayoutState<L>;
42
+ export declare function resizeSplit<L extends SplitLayoutLeaf>(state: SplitLayoutState<L>, splitId: string, ratio: number): SplitLayoutState<L>;
43
+ /**
44
+ * Pure repair walk ported from bb's size normalization: every split ratio is
45
+ * clamped into the normative range and a non-finite ratio falls back to an
46
+ * even split. Structure, IDs, and focus are untouched.
47
+ */
48
+ export declare function normalizeLayout<L extends SplitLayoutLeaf>(state: SplitLayoutState<L>): SplitLayoutState<L>;
49
+ /** The leaf immediately after (`1`) or before (`-1`) the given leaf in reading order. */
50
+ export declare function neighborLeaf<L extends SplitLayoutLeaf>(state: SplitLayoutState<L>, leafId: string, step: 1 | -1): L | undefined;
51
+ //# sourceMappingURL=model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/split-layout/model.ts"],"names":[],"mappings":"AAiBA,2FAA2F;AAC3F,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AACpE,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI,QAAQ,CAAC;IACpF,IAAI,EAAE,OAAO,CAAA;IACb,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,KAAK,GAAG,QAAQ,CAAA;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,SAAS,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAA;CAC5D,CAAC,CAAA;AACF,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAA;AAEnG,eAAO,MAAM,iBAAiB,IAAI,CAAA;AAClC,eAAO,MAAM,gBAAgB,IAAI,CAAA;AACjC,eAAO,MAAM,eAAe,MAAM,CAAA;AAClC,eAAO,MAAM,eAAe,MAAM,CAAA;AAElC,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI,QAAQ,CAAC;IACnF,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAA;IAC1B,aAAa,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,SAAS,QAAQ,CAAC;QAAE,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAA;CAC5E,CAAC,CAAA;AAEF,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI,QAAQ,CAAC;IACjF,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAA;IAC5C,SAAS,EAAE,QAAQ,GAAG,OAAO,CAAA;IAC7B,IAAI,EAAE,CAAC,CAAA;IACP,OAAO,EAAE,MAAM,CAAA;CAChB,CAAC,CAAA;AAEF,wBAAgB,UAAU,CAAC,CAAC,SAAS,eAAe,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,CAI5F;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,eAAe,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,MAAM,CAEvF;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,eAAe,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,MAAM,CAIvF;AA0FD,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,eAAe,EACzD,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,GACzB,gBAAgB,CAAC,CAAC,CAAC,CAKrB;AAED,wBAAgB,SAAS,CAAC,CAAC,SAAS,eAAe,EACjD,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC1B,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,GACvB,gBAAgB,CAAC,CAAC,CAAC,CAmBrB;AAED,wBAAgB,SAAS,CAAC,CAAC,SAAS,eAAe,EACjD,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC1B,MAAM,EAAE,MAAM,EACd,qBAAqB,EAAE,MAAM,CAAC,GAC7B,gBAAgB,CAAC,CAAC,CAAC,CAmBrB;AAED,wBAAgB,aAAa,CAAC,CAAC,SAAS,eAAe,EACrD,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,GACzB,gBAAgB,CAAC,CAAC,CAAC,CAQrB;AAED,wBAAgB,SAAS,CAAC,CAAC,SAAS,eAAe,EACjD,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC1B,MAAM,EAAE,MAAM,GACb,gBAAgB,CAAC,CAAC,CAAC,CAIrB;AAED,wBAAgB,SAAS,CAAC,CAAC,SAAS,eAAe,EACjD,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC1B,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,gBAAgB,CAAC,CAAC,CAAC,CAmBrB;AAED,wBAAgB,QAAQ,CAAC,CAAC,SAAS,eAAe,EAChD,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC1B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,QAAQ,GAAG,OAAO,EAC7B,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,EAC5C,OAAO,EAAE,MAAM,GACd,gBAAgB,CAAC,CAAC,CAAC,CAYrB;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,eAAe,EACnD,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC1B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,gBAAgB,CAAC,CAAC,CAAC,CAkBrB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,eAAe,EACvD,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,GACzB,gBAAgB,CAAC,CAAC,CAAC,CAgBrB;AAED,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,CAAC,SAAS,eAAe,EACpD,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC1B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,GACX,CAAC,GAAG,SAAS,CAKf"}
@@ -0,0 +1,255 @@
1
+ //#region src/components/layout/split-layout/model.ts
2
+ var MAX_LAYOUT_LEAVES = 8;
3
+ var MAX_LAYOUT_DEPTH = 8;
4
+ var MIN_SPLIT_RATIO = .1;
5
+ var MAX_SPLIT_RATIO = .9;
6
+ function listLeaves(node) {
7
+ return node.kind === "leaf" ? [node] : [...listLeaves(node.children[0]), ...listLeaves(node.children[1])];
8
+ }
9
+ function countLeaves(node) {
10
+ return node.kind === "leaf" ? 1 : countLeaves(node.children[0]) + countLeaves(node.children[1]);
11
+ }
12
+ function layoutDepth(node) {
13
+ return node.kind === "leaf" ? 1 : 1 + Math.max(layoutDepth(node.children[0]), layoutDepth(node.children[1]));
14
+ }
15
+ function replaceLeaf(node, leafId, replacement) {
16
+ if (node.kind === "leaf") return node.id === leafId ? replacement : node;
17
+ const first = replaceLeaf(node.children[0], leafId, replacement);
18
+ const second = replaceLeaf(node.children[1], leafId, replacement);
19
+ return first === node.children[0] && second === node.children[1] ? node : {
20
+ ...node,
21
+ children: [first, second]
22
+ };
23
+ }
24
+ function removeLeaf(node, leafId) {
25
+ if (node.kind === "leaf") return node.id === leafId ? null : node;
26
+ const first = removeLeaf(node.children[0], leafId);
27
+ const second = removeLeaf(node.children[1], leafId);
28
+ if (!first) return second;
29
+ if (!second) return first;
30
+ return first === node.children[0] && second === node.children[1] ? node : {
31
+ ...node,
32
+ children: [first, second]
33
+ };
34
+ }
35
+ function containsLeaf(node, leafId) {
36
+ return node.kind === "leaf" ? node.id === leafId : containsLeaf(node.children[0], leafId) || containsLeaf(node.children[1], leafId);
37
+ }
38
+ function assertUniqueInput(state, leaf, splitId) {
39
+ assertLayoutTree(leaf);
40
+ if (typeof splitId !== "string" || splitId.length === 0) throw new Error("invalid_state: split identity must be nonempty");
41
+ const ids = /* @__PURE__ */ new Set();
42
+ const visit = (node) => {
43
+ if (ids.has(node.id)) throw new Error("invalid_state: duplicate pane id");
44
+ ids.add(node.id);
45
+ if (node.kind === "split") node.children.forEach(visit);
46
+ };
47
+ visit(state.center);
48
+ if (ids.has(leaf.id) || ids.has(splitId) || leaf.id === splitId) throw new Error("invalid_state: duplicate pane id");
49
+ }
50
+ /** Visual-tree validation only; applications still decode and scope persisted preferences. */
51
+ function assertLayoutTree(center) {
52
+ const ids = /* @__PURE__ */ new Set();
53
+ const seen = /* @__PURE__ */ new WeakSet();
54
+ let leaves = 0;
55
+ const visit = (node, depth) => {
56
+ if (depth > 8) throw new Error("limit_exceeded: layout depth exceeds eight");
57
+ if (!node || typeof node !== "object") throw new Error("invalid_state: missing layout node");
58
+ if (seen.has(node)) throw new Error("invalid_state: cyclic or reused layout node");
59
+ seen.add(node);
60
+ if (typeof node.id !== "string" || node.id.length === 0) throw new Error("invalid_state: layout identity must be nonempty");
61
+ if (ids.has(node.id)) throw new Error("invalid_state: duplicate pane id");
62
+ ids.add(node.id);
63
+ if (node.kind === "leaf") {
64
+ leaves += 1;
65
+ if (leaves > 8) throw new Error("limit_exceeded: layout has more than eight leaves");
66
+ return;
67
+ }
68
+ if (node.kind !== "split" || !Array.isArray(node.children) || node.children.length !== 2 || node.direction !== "row" && node.direction !== "column") throw new Error("invalid_state: layout must be a strict binary row or column tree");
69
+ visit(node.children[0], depth + 1);
70
+ visit(node.children[1], depth + 1);
71
+ };
72
+ visit(center, 1);
73
+ }
74
+ function createLayoutState(center) {
75
+ assertLayoutTree(center);
76
+ const first = listLeaves(center)[0];
77
+ if (!first) throw new Error("invalid_state: layout requires a leaf");
78
+ return {
79
+ center,
80
+ focusedLeafId: first.id,
81
+ closed: []
82
+ };
83
+ }
84
+ function splitPane(state, targetLeafId, input) {
85
+ if (!containsLeaf(state.center, targetLeafId)) return state;
86
+ if (countLeaves(state.center) >= 8) throw new Error("limit_exceeded: center layout has eight leaves");
87
+ assertUniqueInput(state, input.leaf, input.splitId);
88
+ const target = listLeaves(state.center).find((leaf) => leaf.id === targetLeafId);
89
+ if (!target) return state;
90
+ const children = input.placement === "before" ? [input.leaf, target] : [target, input.leaf];
91
+ const center = replaceLeaf(state.center, targetLeafId, {
92
+ kind: "split",
93
+ id: input.splitId,
94
+ direction: input.direction,
95
+ ratio: .5,
96
+ children
97
+ });
98
+ if (layoutDepth(center) > 8) throw new Error("limit_exceeded: center layout depth exceeds eight");
99
+ return {
100
+ ...state,
101
+ center,
102
+ focusedLeafId: input.leaf.id,
103
+ closed: []
104
+ };
105
+ }
106
+ function closePane(state, leafId, createPlaceholderLeaf) {
107
+ if (!containsLeaf(state.center, leafId)) return state;
108
+ const before = state.center;
109
+ const closedIndex = listLeaves(before).findIndex((leaf) => leaf.id === leafId);
110
+ const removed = removeLeaf(before, leafId);
111
+ const center = removed ?? createPlaceholderLeaf();
112
+ if (!removed) {
113
+ if (center.kind !== "leaf") throw new Error("invalid_state: placeholder must be a leaf");
114
+ assertLayoutTree(center);
115
+ }
116
+ const leaves = listLeaves(center);
117
+ const fallback = leaves[Math.min(closedIndex, leaves.length - 1)] ?? leaves[0];
118
+ if (!fallback) throw new Error("invalid_state: layout requires a leaf");
119
+ return {
120
+ center,
121
+ focusedLeafId: state.focusedLeafId === leafId ? fallback.id : state.focusedLeafId,
122
+ closed: [...state.closed, {
123
+ center: before,
124
+ leafId
125
+ }]
126
+ };
127
+ }
128
+ function undoClosePane(state) {
129
+ const previous = state.closed.at(-1);
130
+ if (!previous) return state;
131
+ return {
132
+ center: previous.center,
133
+ focusedLeafId: previous.leafId,
134
+ closed: state.closed.slice(0, -1)
135
+ };
136
+ }
137
+ function focusPane(state, leafId) {
138
+ return containsLeaf(state.center, leafId) && state.focusedLeafId !== leafId ? {
139
+ ...state,
140
+ focusedLeafId: leafId
141
+ } : state;
142
+ }
143
+ function swapPanes(state, firstLeafId, secondLeafId) {
144
+ if (firstLeafId === secondLeafId) return state;
145
+ const leaves = listLeaves(state.center);
146
+ const first = leaves.find((leaf) => leaf.id === firstLeafId);
147
+ const second = leaves.find((leaf) => leaf.id === secondLeafId);
148
+ if (!first || !second) return state;
149
+ const swap = (node) => {
150
+ if (node.kind === "leaf") {
151
+ if (node.id === firstLeafId) return second;
152
+ if (node.id === secondLeafId) return first;
153
+ return node;
154
+ }
155
+ const left = swap(node.children[0]);
156
+ const right = swap(node.children[1]);
157
+ return left === node.children[0] && right === node.children[1] ? node : {
158
+ ...node,
159
+ children: [left, right]
160
+ };
161
+ };
162
+ return {
163
+ ...state,
164
+ center: swap(state.center),
165
+ closed: []
166
+ };
167
+ }
168
+ function movePane(state, leafId, targetLeafId, placement, direction, splitId) {
169
+ if (leafId === targetLeafId) return state;
170
+ const moving = listLeaves(state.center).find((leaf) => leaf.id === leafId);
171
+ if (!moving || !containsLeaf(state.center, targetLeafId)) return state;
172
+ const detached = removeLeaf(state.center, leafId);
173
+ if (!detached) return state;
174
+ return {
175
+ ...splitPane({
176
+ center: detached,
177
+ focusedLeafId: state.focusedLeafId,
178
+ closed: state.closed
179
+ }, targetLeafId, {
180
+ direction,
181
+ placement,
182
+ leaf: moving,
183
+ splitId
184
+ }),
185
+ focusedLeafId: leafId
186
+ };
187
+ }
188
+ function resizeSplit(state, splitId, ratio) {
189
+ if (!Number.isFinite(ratio)) return state;
190
+ const nextRatio = Math.min(MAX_SPLIT_RATIO, Math.max(MIN_SPLIT_RATIO, ratio));
191
+ let found = false;
192
+ const visit = (node) => {
193
+ if (node.kind === "leaf") return node;
194
+ if (node.id === splitId) {
195
+ found = true;
196
+ return node.ratio === nextRatio ? node : {
197
+ ...node,
198
+ ratio: nextRatio
199
+ };
200
+ }
201
+ const first = visit(node.children[0]);
202
+ const second = visit(node.children[1]);
203
+ return first === node.children[0] && second === node.children[1] ? node : {
204
+ ...node,
205
+ children: [first, second]
206
+ };
207
+ };
208
+ const center = visit(state.center);
209
+ return found && center !== state.center ? {
210
+ ...state,
211
+ center,
212
+ closed: []
213
+ } : state;
214
+ }
215
+ /**
216
+ * Pure repair walk ported from bb's size normalization: every split ratio is
217
+ * clamped into the normative range and a non-finite ratio falls back to an
218
+ * even split. Structure, IDs, and focus are untouched.
219
+ */
220
+ function normalizeLayout(state) {
221
+ const visit = (node) => {
222
+ if (node.kind === "leaf") return {
223
+ node,
224
+ changed: false
225
+ };
226
+ const first = visit(node.children[0]);
227
+ const second = visit(node.children[1]);
228
+ const ratio = Number.isFinite(node.ratio) ? Math.min(MAX_SPLIT_RATIO, Math.max(MIN_SPLIT_RATIO, node.ratio)) : 1 / 2;
229
+ const changed = first.changed || second.changed || ratio !== node.ratio;
230
+ return {
231
+ node: changed ? {
232
+ ...node,
233
+ ratio,
234
+ children: [first.node, second.node]
235
+ } : node,
236
+ changed
237
+ };
238
+ };
239
+ const result = visit(state.center);
240
+ return result.changed ? {
241
+ ...state,
242
+ center: result.node
243
+ } : state;
244
+ }
245
+ /** The leaf immediately after (`1`) or before (`-1`) the given leaf in reading order. */
246
+ function neighborLeaf(state, leafId, step) {
247
+ const leaves = listLeaves(state.center);
248
+ const index = leaves.findIndex((leaf) => leaf.id === leafId);
249
+ if (index < 0) return void 0;
250
+ return leaves[index + step];
251
+ }
252
+ //#endregion
253
+ export { MAX_LAYOUT_DEPTH, MAX_LAYOUT_LEAVES, MAX_SPLIT_RATIO, MIN_SPLIT_RATIO, closePane, countLeaves, createLayoutState, focusPane, layoutDepth, listLeaves, movePane, neighborLeaf, normalizeLayout, resizeSplit, splitPane, swapPanes, undoClosePane };
254
+
255
+ //# sourceMappingURL=model.js.map