@ixfx/components 0.6.2 → 0.7.1

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 (181) hide show
  1. package/bundle/index.d.ts +1918 -835
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +22137 -16840
  4. package/bundle/index.js.map +1 -1
  5. package/bundle/style.css +1 -1
  6. package/dist/ac-text.d.ts +1 -1
  7. package/dist/ac-text.js +3 -2
  8. package/dist/ac-text.js.map +1 -1
  9. package/dist/{ac-token-BAa6ke09.d.ts → ac-token--_vCHXWw.d.ts} +2 -2
  10. package/dist/{ac-token-BAa6ke09.d.ts.map → ac-token--_vCHXWw.d.ts.map} +1 -1
  11. package/dist/ac-token.d.ts +1 -1
  12. package/dist/ac-token.js +3 -2
  13. package/dist/ac-token.js.map +1 -1
  14. package/dist/{button-Bn0BuLpO.js → button-DsFbV4XD.js} +5 -4
  15. package/dist/button-DsFbV4XD.js.map +1 -0
  16. package/dist/button.d.ts.map +1 -1
  17. package/dist/button.js +1 -1
  18. package/dist/checkbox.js +1 -1
  19. package/dist/{colour-picker-0stHB90J.js → colour-picker-Ddsk-7E_.js} +4 -3
  20. package/dist/{colour-picker-0stHB90J.js.map → colour-picker-Ddsk-7E_.js.map} +1 -1
  21. package/dist/colour-picker.js +1 -1
  22. package/dist/crumbs.d.ts +1 -1
  23. package/dist/crumbs.js +4 -2
  24. package/dist/crumbs.js.map +1 -1
  25. package/dist/data-display.js +1 -1
  26. package/dist/{data-grid-C8qIo1-u.d.ts → data-grid-Bh4YrQGK.d.ts} +2 -2
  27. package/dist/{data-grid-C8qIo1-u.d.ts.map → data-grid-Bh4YrQGK.d.ts.map} +1 -1
  28. package/dist/data-grid.d.ts +1 -1
  29. package/dist/data-grid.js +4 -2
  30. package/dist/data-grid.js.map +1 -1
  31. package/dist/{defaults-BCiDhZbF.js → defaults-BJwDsMH4.js} +27 -20
  32. package/dist/defaults-BJwDsMH4.js.map +1 -0
  33. package/dist/editable-label-rzXkjf1H.d.ts +75 -0
  34. package/dist/editable-label-rzXkjf1H.d.ts.map +1 -0
  35. package/dist/editable-label.d.ts +2 -75
  36. package/dist/editable-label.js +2 -1
  37. package/dist/editable-label.js.map +1 -1
  38. package/dist/{fallbacks-DEE1nCTt.js → fallbacks-DNI9xt0F.js} +2 -2
  39. package/dist/{fallbacks-DEE1nCTt.js.map → fallbacks-DNI9xt0F.js.map} +1 -1
  40. package/dist/{grouped-item-lister-_pMDwIo1.d.ts → grouped-item-lister-DnWB-eFT.d.ts} +2 -2
  41. package/dist/{grouped-item-lister-_pMDwIo1.d.ts.map → grouped-item-lister-DnWB-eFT.d.ts.map} +1 -1
  42. package/dist/grouped-item-lister.d.ts +1 -1
  43. package/dist/{hex-editor-Bk6rxhfg.js → hex-editor-BiRfwTz1.js} +2 -2
  44. package/dist/{hex-editor-Bk6rxhfg.js.map → hex-editor-BiRfwTz1.js.map} +1 -1
  45. package/dist/hex.js +1 -1
  46. package/dist/{icon-DpA9x2Ve.d.ts → icon-CDGprSdE.d.ts} +1 -12
  47. package/dist/icon-CDGprSdE.d.ts.map +1 -0
  48. package/dist/{icons-Bm2ByQ3s.js → icon-DcEEGQa_.js} +38 -12
  49. package/dist/icon-DcEEGQa_.js.map +1 -0
  50. package/dist/icons.d.ts +3 -3
  51. package/dist/icons.js +9 -3
  52. package/dist/icons.js.map +1 -0
  53. package/dist/{index-CKtLhgYj.d.ts → index-9AhaHIT2.d.ts} +3 -3
  54. package/dist/{index-CKtLhgYj.d.ts.map → index-9AhaHIT2.d.ts.map} +1 -1
  55. package/dist/index-CG2vIgPv.d.ts +101 -0
  56. package/dist/index-CG2vIgPv.d.ts.map +1 -0
  57. package/dist/{index-BtGFYfaK.d.ts → index-CS9yM8L-.d.ts} +14 -4
  58. package/dist/index-CS9yM8L-.d.ts.map +1 -0
  59. package/dist/index.d.ts +861 -71
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +4257 -931
  62. package/dist/index.js.map +1 -1
  63. package/dist/interaction-D5XdKzbR.js +2 -0
  64. package/dist/labelled-radial-input.js +2 -1
  65. package/dist/labelled-radial-input.js.map +1 -1
  66. package/dist/labelled-slider-input-DzigjMwP.js +808 -0
  67. package/dist/labelled-slider-input-DzigjMwP.js.map +1 -0
  68. package/dist/labelled-slider-input.d.ts +2 -0
  69. package/dist/labelled-slider-input.js +2 -0
  70. package/dist/{types-DlVNJez_.d.ts → list-selection-types-DSuRNWpx.d.ts} +10 -2
  71. package/dist/list-selection-types-DSuRNWpx.d.ts.map +1 -0
  72. package/dist/menu-CXDFJxNm.js +825 -0
  73. package/dist/menu-CXDFJxNm.js.map +1 -0
  74. package/dist/{menu-item-Bs2MYyHq.d.ts → menu-item-CZi9vy5O.d.ts} +4 -2
  75. package/dist/menu-item-CZi9vy5O.d.ts.map +1 -0
  76. package/dist/menu-item-D2S4i8u1.js +809 -0
  77. package/dist/menu-item-D2S4i8u1.js.map +1 -0
  78. package/dist/menu.d.ts +2 -2
  79. package/dist/menu.js +2 -1
  80. package/dist/miller.d.ts +1 -1
  81. package/dist/miller.js +3 -2
  82. package/dist/miller.js.map +1 -1
  83. package/dist/narrowed-text.js +4 -2
  84. package/dist/narrowed-text.js.map +1 -1
  85. package/dist/notification-pill-DyZydXoZ.d.ts.map +1 -1
  86. package/dist/notifier.js +4 -3
  87. package/dist/notifier.js.map +1 -1
  88. package/dist/panel.d.ts +13 -13
  89. package/dist/panel.d.ts.map +1 -1
  90. package/dist/panel.js +37 -253
  91. package/dist/panel.js.map +1 -1
  92. package/dist/plots.js +1 -1
  93. package/dist/polar-pad.js +1 -1
  94. package/dist/{radial-input-Dk8Wydp5.js → radial-input-Dp5qz0ly.js} +2 -2
  95. package/dist/{radial-input-Dk8Wydp5.js.map → radial-input-Dp5qz0ly.js.map} +1 -1
  96. package/dist/radial-input.js +1 -1
  97. package/dist/range-input.js +1 -1
  98. package/dist/range.js +1 -1
  99. package/dist/registry-BEtb8_wB.js +46 -0
  100. package/dist/registry-BEtb8_wB.js.map +1 -0
  101. package/dist/select-horiz.js +3 -2
  102. package/dist/select-horiz.js.map +1 -1
  103. package/dist/{slider-input-XppH3vcy.d.ts → slider-input-BfRUW2ZB.d.ts} +16 -3
  104. package/dist/slider-input-BfRUW2ZB.d.ts.map +1 -0
  105. package/dist/{slider-input-CT4gcF-o.js → slider-input-De4eoT11.js} +382 -67
  106. package/dist/slider-input-De4eoT11.js.map +1 -0
  107. package/dist/slider-input.d.ts +1 -1
  108. package/dist/slider-input.js +1 -1
  109. package/dist/snackbar.js +1 -1
  110. package/dist/split-layout-BFSbUZQv.js +969 -0
  111. package/dist/split-layout-BFSbUZQv.js.map +1 -0
  112. package/dist/split-layout-JK3xWVX2.d.ts +179 -0
  113. package/dist/split-layout-JK3xWVX2.d.ts.map +1 -0
  114. package/dist/split-layout.d.ts +2 -60
  115. package/dist/split-layout.js +1 -617
  116. package/dist/style.css +1 -1
  117. package/dist/swipe.js +1 -1
  118. package/dist/{tab-list-D_1cYhB_.d.ts → tab-list-CGyPt07Z.d.ts} +10 -2
  119. package/dist/tab-list-CGyPt07Z.d.ts.map +1 -0
  120. package/dist/tabs.d.ts +1 -1
  121. package/dist/tabs.js +128 -27
  122. package/dist/tabs.js.map +1 -1
  123. package/dist/{tickled-controller-BtevrRLV.d.ts → tickled-controller-CRzdIEjJ.d.ts} +7 -1
  124. package/dist/tickled-controller-CRzdIEjJ.d.ts.map +1 -0
  125. package/dist/{tickled-controller-DkAU-0qL.js → tickled-controller-h9GmJ_bU.js} +8 -2
  126. package/dist/tickled-controller-h9GmJ_bU.js.map +1 -0
  127. package/dist/titlebar-Bj5pfdw_.d.ts +50 -0
  128. package/dist/titlebar-Bj5pfdw_.d.ts.map +1 -0
  129. package/dist/titlebar.d.ts +2 -0
  130. package/dist/titlebar.js +324 -0
  131. package/dist/titlebar.js.map +1 -0
  132. package/dist/tooltip-BlvBUQxx.d.ts.map +1 -1
  133. package/dist/{tree-BO6BXI2b.js → tree-Ch4W8qXQ.js} +4 -3
  134. package/dist/{tree-BO6BXI2b.js.map → tree-Ch4W8qXQ.js.map} +1 -1
  135. package/dist/tree.d.ts +1 -1
  136. package/dist/tree.js +1 -1
  137. package/dist/vertical-list-ClnqTkF3.js +814 -0
  138. package/dist/vertical-list-ClnqTkF3.js.map +1 -0
  139. package/dist/vertical-list.d.ts +8 -9
  140. package/dist/vertical-list.d.ts.map +1 -1
  141. package/dist/vertical-list.js +1 -736
  142. package/dist/{xy-axis-B6yMjckW.js → xy-axis-DWizs0bt.js} +109 -3
  143. package/dist/xy-axis-DWizs0bt.js.map +1 -0
  144. package/dist/xy-pad.js +1 -1
  145. package/docs-user/README.md +5 -0
  146. package/docs-user/detail-list.md +398 -0
  147. package/docs-user/dock.md +461 -0
  148. package/docs-user/icons.md +4 -0
  149. package/docs-user/index.json +28 -3
  150. package/docs-user/labelled-slider-input.md +198 -0
  151. package/docs-user/llms.txt +6 -1
  152. package/docs-user/panel.md +20 -1
  153. package/docs-user/slider-input.md +55 -15
  154. package/docs-user/split-layout.md +136 -170
  155. package/docs-user/tabs.md +54 -0
  156. package/docs-user/titlebar.md +102 -0
  157. package/docs-user/toolbar.md +95 -22
  158. package/docs-user/twosplit-layout.md +254 -0
  159. package/docs-user/user-catalog.md +8 -1
  160. package/docs-user/user-theming.md +109 -0
  161. package/llms.txt +6 -1
  162. package/package.json +1 -2
  163. package/dist/button-Bn0BuLpO.js.map +0 -1
  164. package/dist/defaults-BCiDhZbF.js.map +0 -1
  165. package/dist/editable-label.d.ts.map +0 -1
  166. package/dist/icon-DpA9x2Ve.d.ts.map +0 -1
  167. package/dist/icons-Bm2ByQ3s.js.map +0 -1
  168. package/dist/index-BtGFYfaK.d.ts.map +0 -1
  169. package/dist/menu-DIfU70AA.js +0 -1609
  170. package/dist/menu-DIfU70AA.js.map +0 -1
  171. package/dist/menu-item-Bs2MYyHq.d.ts.map +0 -1
  172. package/dist/slider-input-CT4gcF-o.js.map +0 -1
  173. package/dist/slider-input-XppH3vcy.d.ts.map +0 -1
  174. package/dist/split-layout.d.ts.map +0 -1
  175. package/dist/split-layout.js.map +0 -1
  176. package/dist/tab-list-D_1cYhB_.d.ts.map +0 -1
  177. package/dist/tickled-controller-BtevrRLV.d.ts.map +0 -1
  178. package/dist/tickled-controller-DkAU-0qL.js.map +0 -1
  179. package/dist/types-DlVNJez_.d.ts.map +0 -1
  180. package/dist/vertical-list.js.map +0 -1
  181. package/dist/xy-axis-B6yMjckW.js.map +0 -1
@@ -0,0 +1,461 @@
1
+ # Dock (`ixfx-dock-host` / `ixfx-dock-panel`)
2
+
3
+ Dockable, movable panels like VS Code / Photoshop. Drag a panel by its
4
+ titlebar (solo) or its tab (tabbed) over another panel; depending on cursor
5
+ position it becomes a **tab** in that group or **splits** the target to the
6
+ top/bottom/left/right. Infinitely composable, serializable, and restrictable.
7
+
8
+ ```
9
+ <ixfx-dock-host layout='{"version":1,"root":{…}}'>
10
+ <ixfx-dock-panel panel-id="explorer" label="Explorer" closable>…</ixfx-dock-panel>
11
+ <ixfx-dock-panel panel-id="editor" label="Editor" closable>…</ixfx-dock-panel>
12
+ </ixfx-dock-host>
13
+ ```
14
+
15
+ A panel's display text is the `label` attribute (the `title` *property* in
16
+ JS). A legacy `title` attribute is adopted once and then removed, so browsers
17
+ never show it as a hover tooltip.
18
+
19
+ ## Architecture
20
+
21
+ - **Flat panel pool + model-driven host.** Panels stay direct light-DOM
22
+ children of the host for their whole life — they are never reparented
23
+ within a host. The host owns an in-memory `DockNode` tree (`dock-model.ts`),
24
+ renders split/tab scaffolding in shadow DOM, and projects panels into leaves
25
+ via `slot="dock-panel-{id}"`. Moving a panel = model mutation → re-render →
26
+ slot reassignment; panel DOM and state are never destroyed.
27
+ - **Split nodes** render as a single `ixfx-split-layout` (n-way) with one
28
+ wrapper child per child node. Splits of the *other* direction are emitted as
29
+ nested `<ixfx-split-layout>` directly (the split element recurses into them).
30
+ Per-child weights ride on `size` attributes; stack collapse rides on
31
+ `collapsed` / `when-small` attributes.
32
+ - **Stack (leaf) nodes** always render the same chrome: an `ixfx-tab-list`
33
+ strip (with generated `ixfx-tab-list-item`s, one per panel) plus a single
34
+ body slot for the active panel — so titlebars are consistent whether a stack
35
+ holds one panel or many. Docked panels are `presentation="tabbed"`
36
+ (content only); the tab item is the drag handle and carries title / icon /
37
+ close / per-panel toolbar. `ixfx-tab-panels` is deliberately not used — the
38
+ host owns slot projection. Panels outside a host (or not currently docked)
39
+ render `presentation="solo"` with an `ixfx-titlebar` and flat content.
40
+
41
+ ## Data model (`types.ts` / `dock-model.ts`)
42
+
43
+ ```ts
44
+ type DockLayout = { version: 1; root: DockNode };
45
+ type DockNode = DockSplitNode | DockStackNode;
46
+ type DockSplitNode = { kind: 'split'; direction: 'row' | 'column';
47
+ sizes: number[]; children: DockNode[] };
48
+ type DockStackNode = { kind: 'stack'; panels: string[]; active: string;
49
+ name?: string; // app-defined container name, e.g. 'primary-sidebar'
50
+ hideLabels?: boolean; // icon-only tabs (→ ixfx-tab-list hide-labels)
51
+ collapsed?: boolean; preCollapseSize?: string };
52
+ ```
53
+
54
+ `direction` uses `ixfx-split-layout`'s flexbox sense: `row` = side by side,
55
+ `column` = stacked. A "split horizontal" drop (horizontal divider, panels
56
+ stacked vertically) maps to `direction: "column"`.
57
+
58
+ `normalize()` runs after every mutation: empty *unnamed* stacks are removed
59
+ (named empty stacks are kept so the name stays addressable), ≤1-child
60
+ splits dissolve, same-direction child splits flatten into their parent,
61
+ `sizes` stays parallel to `children`, and `active` always points at a member.
62
+ The tree is plain and mutable, owned exclusively by the host.
63
+
64
+ Named containers: give a stack leaf `name` (e.g. `"primary-sidebar"`) in the
65
+ initial layout JSON. Names must be unique per layout (first match wins),
66
+ serialize with `saveLayout()` / `loadLayout()`, and render as
67
+ `data-dock-name` on the leaf. Explicit leaf close still removes the stack.
68
+ When a `tabs` drop empties a named stack into an *unnamed* one, the name
69
+ (and its content rules) follows the panels to the merged leaf instead of
70
+ leaving an empty placeholder behind.
71
+
72
+ Spatial moves: `getNeighbours()` reports what sits above / below / left /
73
+ right of a leaf (flat `{ name, path }` lists, most-overlapping first).
74
+ `moveLeaf()` swaps one step with the longest-edge neighbour (both leaves keep
75
+ their size slots); `moveLeafToEdge()` relocates to a layout edge, keeping the
76
+ rest of the tree intact and preserving the leaf's absolute size. Both return
77
+ `false` when there is nowhere to go.
78
+
79
+ ## Drop targeting — the 5-zone model
80
+
81
+ While dragging, the host overlay highlights the resulting region:
82
+
83
+ | Region | Operation | Result |
84
+ |---|---|---|
85
+ | leaf centre (~50% box) | `tabs` | insert into that stack (at the hovered strip gap when over the strip) |
86
+ | leaf left / right band | `split-vertical` | new stack beside the leaf |
87
+ | leaf top / bottom band | `split-horizontal` | new stack above/below the leaf |
88
+ | host gutter / edges (outside all leaves) | split **root** | new stack against the whole host |
89
+
90
+ When the target's parent split already matches the drop direction the new
91
+ stack is inserted beside it (50/50 weight split); otherwise the target is
92
+ wrapped in a new split. `Escape` or `pointercancel` cancels with no mutation.
93
+
94
+ ## Junction resize
95
+
96
+ Where a parent gutter line meets a nested split's gutter (T and + crossings
97
+ of three or four panels), the host renders a small hover-revealed handle
98
+ (colour: `--dock-junction-color`, default `var(--accent)`). Dragging it
99
+ starts the vertical and horizontal gutter drags at once via
100
+ `ixfx-split-layout`'s `beginGutterDrag()`, resizing all adjacent panels with
101
+ one gesture — as in VS Code. Handle positions are recomputed on renders,
102
+ split resizes, and host resizes; handles are hidden during dock drags.
103
+
104
+ ## Restrictions
105
+
106
+ - Host `accepts` attribute: space-separated `tabs`, `horizontal`, `vertical`,
107
+ convenience `all`; `""` / `none` rejects everything. Default allows all.
108
+ - Panel `accepts` attribute: applies when that panel is a **solo** drop
109
+ target. Default unrestricted.
110
+ - `host.canAccept = (panelId, op) => boolean` — finest-grained veto, consulted
111
+ per candidate zone.
112
+ - Leaf content rules by panel `kind` (default `'any'`, matched literally):
113
+ `setLeafExclusiveContents(name, kinds)` admits only those kinds into the
114
+ named leaf, `setLeafDenyContents(name, kinds)` refuses those kinds.
115
+ `clearLeafContentRestrictions(name)` lifts both; `[]` seals (exclusive) or
116
+ lifts (deny). Rules are host policy — kept across `loadLayout()`, never
117
+ serialized; already-docked panels are grandfathered.
118
+
119
+ A zone lights up only if every applicable rule allows it; disallowed zones
120
+ are inert. Content rules govern tabbing *into* a leaf (`tabs` drops,
121
+ `addPanel`, re-homing); edge splits beside the leaf stay allowed because they
122
+ create a new sibling leaf. Programmatic fallbacks (`addPanel` without a
123
+ target, external panels, leaf-close re-homing) use the first stack admitting
124
+ the panel's kind.
125
+
126
+ ## Events
127
+
128
+ `ixfx-dock-host`: `dock-change` (`{ layout }` after structural mutations),
129
+ `dock-panel-added`, `dock-panel-removed`, `dock-panel-activated`,
130
+ `dock-panel-drag-start`.
131
+
132
+ Cross-window (see [Cross-window docking](#cross-window-docking-electron-etc)):
133
+ `dock-panel-drag-out` / `dock-panel-drag-in`
134
+ (`{ panelId, clientX, clientY, screenX, screenY }`) when the drag pointer
135
+ leaves / re-enters every host's bounds, and `dock-panel-tear-out`
136
+ (`{ panelId, snapshot, clientX, clientY, screenX, screenY }`) when a panel is
137
+ released with no host under the pointer.
138
+
139
+ `ixfx-dock-panel`: `dock-panel-drag-start` (`{ panelId, pointerId, clientX,
140
+ clientY }`), plus `close` / `toggle` forwarded from its `ixfx-titlebar`.
141
+ The panel removes itself on close; the host's pool sync routes that through
142
+ the model and emits `dock-panel-removed` + `dock-change`.
143
+
144
+ The host consumes `ixfx-split-layout` events: `ixfx-split-resize` /
145
+ `ixfx-split-change` (live gutter weights folded into `DockSplitNode.sizes`),
146
+ `ixfx-split-collapse` / `ixfx-split-expand` (→ `DockStackNode.collapsed`), and
147
+ `ixfx-split-close` (cancelable — prevented and rerouted so panel elements
148
+ survive, re-homed into the first other stack admitting each kind).
149
+
150
+ ## Public API — `ixfx-dock-host`
151
+
152
+ ```ts
153
+ saveLayout(): DockLayout // folds live split weights from the DOM
154
+ loadLayout(layout: DockLayout): void
155
+ addPanel(panel: DockPanelElement | DockPanelInit, target?: DockAddPanelTarget): boolean
156
+ type DockAddPanelTarget = {
157
+ container?: string; // named stack leaf — takes precedence over `stack`
158
+ stack?: string; // a panel-id in the target stack (legacy lookup)
159
+ index?: number; op?: DockOperation
160
+ } // false when the target container denies the panel kind
161
+ removePanel(panelId: string): boolean // removes the element from the DOM too
162
+ activatePanel(panelId: string): boolean
163
+ getPanelIds(): string[]
164
+ setLeafExclusiveContents(name: string, kinds: string[]): void
165
+ setLeafDenyContents(name: string, kinds: string[]): void
166
+ clearLeafContentRestrictions(name: string): void
167
+ getLeafContentRestrictions(name: string): DockLeafContentRules | null
168
+ getContainerNames(): string[]
169
+ getContainer(name: string): { name: string; panels: string[]; active: string } | null
170
+ getContainerPanels(name: string): string[] // ids in tab order
171
+ getContainerElements(name: string): DockPanelElement[] // iterate children
172
+ // Spatial: leaves by `name` or numeric `path`; neighbours are `{ name, path }`
173
+ getNeighbours(leaf: DockLeafRef): DockNeighbours | null
174
+ moveLeaf(leaf: DockLeafRef, direction: 'up' | 'down' | 'left' | 'right'): boolean
175
+ moveLeafToEdge(leaf: DockLeafRef, edge: 'top' | 'bottom' | 'left' | 'right'): boolean
176
+ canAccept?: (panelId: string, op: DockOperation) => boolean
177
+
178
+ // Cross-window hooks (serializable in/out; no element handoff)
179
+ getPanelSnapshot(panelId: string): DockPanelSnapshot | null
180
+ detachPanel(panelId: string): DockPanelSnapshot | null // snapshot + removePanel
181
+ queryDropTarget(x: number, y: number, panelId?: string): DockDropTarget | null
182
+ previewExternalDrop(x: number, y: number, panelId?: string): DockDropTarget | null
183
+ endExternalDrop(): void
184
+ acceptExternalPanel(snap: DockPanelSnapshot | DockPanelInit,
185
+ at?: { clientX: number; clientY: number }): DockPanelElement | null
186
+ ```
187
+
188
+ The optional `layout` attribute accepts `DockLayout` JSON as the initial
189
+ layout; without it, all pooled panels go into one root stack in source order.
190
+ Panels present in the pool but missing from a loaded layout are appended to
191
+ the first stack.
192
+
193
+ ## Leaf toolbars
194
+
195
+ The host renders no strip chrome of its own. To add toolbar items to a leaf,
196
+ give it a name in the layout and provide light-DOM content in the matching
197
+ `toolbar-<name>` slot — the host forwards it into that leaf's tab-strip
198
+ toolbar (unnamed leaves have no toolbar target):
199
+
200
+ ```html
201
+ <ixfx-dock-host layout='{"version":1,"root":{"kind":"stack","panels":["a"],"active":"a","name":"editor"}}'>
202
+ <ixfx-dock-panel panel-id="a" label="A">…</ixfx-dock-panel>
203
+ <div slot="toolbar-editor">
204
+ <ixfx-menu-trigger label="+" placement="bottom-end">
205
+ <ixfx-menu-container slot="menu">
206
+ <ixfx-menu-item command="new-document" label="New document"></ixfx-menu-item>
207
+ </ixfx-menu-container>
208
+ </ixfx-menu-trigger>
209
+ </div>
210
+ </ixfx-dock-host>
211
+ ```
212
+
213
+ Menu commands surface as bubbling `menu-command` events (`{ command, checked,
214
+ args }`) — listen on the host. Multiple elements may share one toolbar slot;
215
+ content for unknown names is inert until a leaf with that name appears.
216
+
217
+ ## Cross-host drags
218
+
219
+ Panels can be dragged between hosts (register via the shared drag
220
+ coordinator automatically). The source host releases the panel from its model
221
+ and the target host adopts the element — the one sanctioned reparent
222
+ (`connectedCallback` re-runs on the panel; it holds no per-host state).
223
+ `panel-id` must be unique across all hosts that can exchange panels.
224
+
225
+ This works only *within one document*. The drag coordinator is module state, so
226
+ it never spans separate windows (`BrowserWindow` / `WebContentsView` / a popped
227
+ `window.open`). Moving a panel across that boundary is described next.
228
+
229
+ ## Cross-window docking (Electron, etc.)
230
+
231
+ The dock deliberately ships **no** window/IPC/Node dependency. Instead it
232
+ exposes the events and methods an outer coordinator (your Electron main
233
+ process, a `SharedWorker`, `BroadcastChannel`, `postMessage`…) needs to move a
234
+ panel between windows. Nothing DOM crosses the boundary — you transport a
235
+ plain, structured-clone-safe **`DockPanelSnapshot`** and rebuild the panel on
236
+ the far side.
237
+
238
+ ```ts
239
+ interface DockPanelSnapshot { // structured-clone safe
240
+ panelId: string; title: string; iconName: string;
241
+ closable: boolean; collapsible: boolean; kind: string; accepts: string;
242
+ contentHTML: string; // best-effort innerHTML snapshot; see note below
243
+ }
244
+ interface DockDropTarget { // serializable hit-test result
245
+ operation: 'tabs' | 'split-horizontal' | 'split-vertical';
246
+ targetPath: number[]; // stack path in the target host model; [] = root
247
+ side: 'before' | 'after';
248
+ gapIndex?: number;
249
+ }
250
+ ```
251
+
252
+ > **`contentHTML`** is a convenience for panels whose body is static markup.
253
+ > If a panel holds live state, framework-managed DOM, canvas/WebGL, or running
254
+ > timers, ignore it and rebuild the body from your own model in the new window
255
+ > (you already do this to render the panel the first time). The snapshot is
256
+ > identity + chrome; content ownership stays with your app.
257
+
258
+ ### 1. Panel → new window (tear-out)
259
+
260
+ While a drag is in progress the host fires boundary events as the pointer
261
+ leaves and re-enters the union of all host rects:
262
+
263
+ | Event | When | Use |
264
+ |---|---|---|
265
+ | `dock-panel-drag-out` | pointer left every host | start a native drag image / detached preview window that follows the cursor |
266
+ | `dock-panel-drag-in` | pointer came back over a host | tear down that preview; the in-page drop overlay takes over again |
267
+ | `dock-panel-tear-out` | released with no host under the pointer | create the real window and move the panel |
268
+
269
+ All three carry `clientX/clientY` (source viewport) **and** `screenX/screenY`
270
+ (desktop) so the receiver can place a window. `dock-panel-tear-out` also
271
+ carries a ready-made `snapshot`.
272
+
273
+ ```ts
274
+ host.addEventListener('dock-panel-tear-out', (e) => {
275
+ const { snapshot, screenX, screenY } = e.detail;
276
+ window.dockBridge.openPanelWindow({ snapshot, screenX, screenY }); // → main process
277
+ host.detachPanel(snapshot.panelId); // snapshot already taken; drop it here
278
+ });
279
+ ```
280
+
281
+ ```ts
282
+ // main process
283
+ ipcMain.handle('dock:open-panel-window', (_e, { snapshot, screenX, screenY }) => {
284
+ const win = new BrowserWindow({ x: screenX - 40, y: screenY - 20, width: 480, height: 360, /* … */ });
285
+ win.loadFile('panel-window.html');
286
+ win.webContents.once('did-finish-load', () =>
287
+ win.webContents.send('dock:mount-panel', snapshot));
288
+ });
289
+ ```
290
+
291
+ ```ts
292
+ // panel-window.html renderer — a host that starts empty
293
+ ipcRenderer.on('dock:mount-panel', (_e, snapshot) => {
294
+ document.querySelector('ixfx-dock-host').acceptExternalPanel(snapshot);
295
+ });
296
+ ```
297
+
298
+ Because `pointerup` outside the OS window is not reliably delivered to a
299
+ renderer, prefer starting a native follow-the-cursor window on
300
+ `dock-panel-drag-out` and finalising from the **main** process on the global
301
+ mouse-up, rather than depending on `dock-panel-tear-out` firing. `tear-out`
302
+ is the best-effort path for release inside the window but between hosts.
303
+
304
+ ### 2. Window → this host (tear-in)
305
+
306
+ Detecting an OS-level window drag is the app's job — Electron gives you the
307
+ screen cursor (`screen.getCursorScreenPoint()`) and window bounds
308
+ (`win.getBounds()`), so the main process can tell, per frame, when a dragged
309
+ panel-window is hovering another window and where. Forward that to the target
310
+ renderer, which drives the host imperatively:
311
+
312
+ ```ts
313
+ // target renderer, fed pointer position + the dragged panel's snapshot over IPC
314
+ ipcRenderer.on('dock:external-drag-move', (_e, { snapshot, clientX, clientY }) => {
315
+ host.previewExternalDrop(clientX, clientY, snapshot.panelId); // lights the overlay
316
+ });
317
+ ipcRenderer.on('dock:external-drag-leave', () => host.endExternalDrop());
318
+ ipcRenderer.on('dock:external-drag-drop', (_e, { snapshot, clientX, clientY }) => {
319
+ const placed = host.acceptExternalPanel(snapshot, { clientX, clientY });
320
+ if (placed) ipcRenderer.send('dock:close-source-window'); // panel now lives here
321
+ });
322
+ ```
323
+
324
+ `previewExternalDrop` / `queryDropTarget` return the `DockDropTarget` that a
325
+ drop at that point would produce (or `null` when the point misses the host or
326
+ the zone is vetoed by `accepts` / `canAccept`), so the coordinator can also
327
+ show a yes/no cursor on the source side. `acceptExternalPanel` honours the
328
+ same restrictions and falls back to the first stack when `at` is omitted or
329
+ outside the host.
330
+
331
+ ### Coordinate spaces
332
+
333
+ Events give you both. `client*` is the source renderer's viewport;
334
+ `screen*` is the desktop. To convert a desktop point into a target renderer's
335
+ viewport, the app subtracts that window's content bounds (main process:
336
+ `win.getContentBounds()`), accounting for `devicePixelRatio`. Keep the
337
+ conversion in the coordinator — the component only ever speaks the two spaces
338
+ it can measure.
339
+
340
+ ### Identity
341
+
342
+ `panelId` must be unique across every host that can exchange panels, in every
343
+ window. `acceptExternalPanel` returns `null` if that id is already docked in
344
+ the target host (adopt / focus it yourself instead).
345
+
346
+ ## Serialization
347
+
348
+ `saveLayout()` folds live gutter weights from each rendered
349
+ `ixfx-split-layout` (via `getSizes()`) into `DockSplitNode.sizes` before
350
+ serializing, so a save → drag → save round-trip preserves user adjustments.
351
+ `DockStackNode.collapsed` / `preCollapseSize` serialize so a saved layout can
352
+ restore collapse state without a live element.
353
+
354
+ ## CSS variables
355
+
356
+ | Variable | Purpose | Default |
357
+ |---|---|---|
358
+ | `--dock-drop-zone-color` | drop highlight colour | `var(--accent)` |
359
+ | `--dock-drop-zone-opacity` | drop highlight opacity | `0.25` |
360
+ | `--dock-collapsed-size` | collapsed stack track size | `40px` |
361
+ | `--dock-tab-height` | tab item height, uniform across all leaf strips | `30px` |
362
+ | `--dock-split-bar-size` | thickness of the split-layout bars between leaves | `2px` |
363
+ | `--dock-junction-color` | junction resize handle affordance colour | `var(--accent)` |
364
+
365
+ Splitter styling delegates to `ixfx-split-layout` (`--split-*`); solo panel
366
+ chrome delegates to `ixfx-titlebar`, and the leaf body is the docked panel
367
+ surface. Each leaf carries a hairline border (`var(--border-subtle)`) with
368
+ slight rounding (`var(--radius-m)`). An empty
369
+ host renders a placeholder; replace its content with
370
+ `<div slot="empty">…</div>`.
371
+
372
+ ## Implementation notes from the Electron demo
373
+
374
+ A standalone Electron app demonstrates cross-window docking at
375
+ `../electron-dock-demo/`. Key insights:
376
+
377
+ ### Ghost window pattern
378
+
379
+ For tear-out, create a frameless, transparent, always-on-top `BrowserWindow`
380
+ that follows the cursor. The renderer sends `dock-panel-drag-out` (pointer left
381
+ every host) and you open the ghost; subsequent `pointermove` updates its
382
+ position. `dock-panel-tear-out` (released over empty space) creates the real
383
+ panel window and tells the source to detach.
384
+
385
+ ### Tear-out verdict
386
+
387
+ The **source keeps its panel** until the main process replies with a verdict:
388
+
389
+ | Drop point | Verdict | Effect |
390
+ |---|---|---|
391
+ | Inside another window | `transferred` | target hosts the panel; source detaches |
392
+ | Inside another window, but vetoed | `floated` | spawn panel window; source detaches |
393
+ | Over empty desktop | `floated` | spawn panel window; source detaches |
394
+ | Over source window's non-host chrome | `cancelled` | panel stays put; nothing happens |
395
+
396
+ This prevents losing a panel when a target vetoes the drop — the source still
397
+ has it. The renderer checks for `bridge.onTearOutVerdict` to know whether to
398
+ wait for a verdict or fall back to direct detach.
399
+
400
+ ### Tear-in (panel window → another window)
401
+
402
+ Electron's `BrowserWindow` fires `move` on drag and `moved` on drop. The main
403
+ process hit-tests the cursor against every other window's content bounds:
404
+
405
+ ```ts
406
+ // main process
407
+ win.on('move', () => {
408
+ const point = screen.getCursorScreenPoint();
409
+ const target = windowAt(point, win.id); // hit-test vs other windows
410
+ if (target) {
411
+ target.webContents.send('dock:external-drag-move', { snapshot, clientX, clientY });
412
+ }
413
+ });
414
+ win.on('moved', () => finishDrag(win));
415
+ ```
416
+
417
+ **Platform quirk:** Windows/Linux may not fire `moved`. Use a fallback timer
418
+ (≈180ms) to detect drag end:
419
+
420
+ ```ts
421
+ win.on('move', () => {
422
+ if (dragIn.endTimer) clearTimeout(dragIn.endTimer);
423
+ dragIn.endTimer = setTimeout(() => finishDrag(win), 180);
424
+ });
425
+ ```
426
+
427
+ ### Coordinates
428
+
429
+ - `screen.getCursorScreenPoint()` + `win.getContentBounds()` are **DIP** (device
430
+ independent pixels)
431
+ - `PointerEvent.clientX/Y` are **CSS px**
432
+ - Equal at `devicePixelRatio: 1`; on fractional displays expect a few px of
433
+ slop in ghost position.
434
+
435
+ ### IPC bridge
436
+
437
+ Use a preload script with `contextBridge.exposeInMainWorld` to expose a typed
438
+ bridge. The renderer calls imperative methods (`dragOut`, `dragMove`,
439
+ `tearOut`, `accepted`, `rejected`) and the main process pushes events
440
+ (`dock:external-drag-move`, `dock:external-drag-drop`, etc.).
441
+
442
+ ### Content snapshots
443
+
444
+ `DockPanelSnapshot.contentHTML` captures `innerHTML`. For **stateful panels**
445
+ (e.g., live input edits), mirror the value back into the attribute — a
446
+ `input` event listener on the panel body keeps the attribute in sync across
447
+ body replacements. Better: rebuild content from your own model on the receiving
448
+ side instead of relying on DOM snapshotting.
449
+
450
+ ## Deviations from the original plan (and why)
451
+
452
+ - **Tab-strip reorder** is handled by the dock drag coordinator (gap-index
453
+ insertion) rather than `ixfx-tab-list`'s own `drag-mode="reorder"` — running
454
+ both would race two ghosts for the same pointer. The gap computation
455
+ reuses the tab-list midpoint rule.
456
+ - **Closing a leaf via `ixfx-split-close`** re-homes its panels into the first
457
+ other stack admitting each kind (or the pool if none) instead of dropping
458
+ them on the floor.
459
+ - **No default strip chrome.** The plan's per-strip split buttons are gone;
460
+ leaves expose `toolbar-<name>` slots instead (see [Leaf
461
+ toolbars](#leaf-toolbars)), so apps own their strip actions.
@@ -9,6 +9,10 @@ A centralized SVG icon system for `@ixfx/components`. Icons are stored by name,
9
9
  | `chevron-down` | `<ixfx-split-button>`, `<ixfx-panel>` | `currentColor` |
10
10
  | `check` | Available for use | `currentColor` |
11
11
  | `close` | `<ixfx-panel>`, `<ixfx-split-button>` | `currentColor` |
12
+ | `add` | Available for use | `currentColor` |
13
+ | `file` | Available for use | `currentColor` |
14
+ | `folder` | Available for use | `currentColor` |
15
+ | `search` | Available for use | `currentColor` |
12
16
 
13
17
  ## Overriding a built-in icon
14
18
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ixfx/components",
3
- "version": "0.6.2",
4
- "generated": "2026-08-30T20:26:05.718Z",
3
+ "version": "0.7.1",
4
+ "generated": "2026-09-06T10:36:57.248Z",
5
5
  "components": [
6
6
  {
7
7
  "name": "ac-text",
@@ -43,6 +43,16 @@
43
43
  "summary": "A virtualised data grid with sortable/resizable/reorderable columns, single/multi selection, grouping, and inline cell editing. Handles 200+ rows with windowed rendering and keyboard navigation.",
44
44
  "path": "docs-user/data-grid.md"
45
45
  },
46
+ {
47
+ "name": "detail-list",
48
+ "summary": "A list that scales horizontally, not vertically. Items are laid out in equal-width columns; they flow top→bottom down a column, then wrap to the top of the next column to the right. The component has ",
49
+ "path": "docs-user/detail-list.md"
50
+ },
51
+ {
52
+ "name": "dock",
53
+ "summary": "Dockable, movable panels like VS Code / Photoshop. Drag a panel by its",
54
+ "path": "docs-user/dock.md"
55
+ },
46
56
  {
47
57
  "name": "editable-label",
48
58
  "summary": "Inline editable text components. `ixfx-editable-number` extends `ixfx-editable-label` with drag-to-adjust and fill-bar visualization.",
@@ -78,6 +88,11 @@
78
88
  "summary": "A composite range input that pairs an editable label (`ixfx-editable-number`) with a slider (`ixfx-range-input`). The two controls are always in sync — dragging the slider updates the number, and edit",
79
89
  "path": "docs-user/labelled-range-input.md"
80
90
  },
91
+ {
92
+ "name": "labelled-slider-input",
93
+ "summary": "A composite slider input that pairs an editable label (`ixfx-editable-number`) with a slider (`ixfx-slider-input`). The two controls are always in sync — dragging the slider updates the number, and ed",
94
+ "path": "docs-user/labelled-slider-input.md"
95
+ },
81
96
  {
82
97
  "name": "led",
83
98
  "summary": "An LED indicator component with color mixing and optional blinking animation.",
@@ -160,7 +175,7 @@
160
175
  },
161
176
  {
162
177
  "name": "split-layout",
163
- "summary": "A Lit web component that lays out two elements within a container with a split bar to adjust their proportions.",
178
+ "summary": "A Lit web component that lays out any number of panels along one axis with draggable gutters between them.",
164
179
  "path": "docs-user/split-layout.md"
165
180
  },
166
181
  {
@@ -178,6 +193,11 @@
178
193
  "summary": "A canvas-based timeline component with tracks, events, zoom/pan, and vertical track resizing. Suitable for visualizing time-series data, event logs, or scheduling.",
179
194
  "path": "docs-user/timeline.md"
180
195
  },
196
+ {
197
+ "name": "titlebar",
198
+ "summary": "Standalone titlebar chrome extracted from `ixfx-panel`: title text, icon, toolbar slot, close button and an optional collapse/expand caret. Use it when you need a panel-style header without the collap",
199
+ "path": "docs-user/titlebar.md"
200
+ },
181
201
  {
182
202
  "name": "toolbar",
183
203
  "summary": "Two components for building toolbars:",
@@ -193,6 +213,11 @@
193
213
  "summary": "Three components share a common `TreeComponent` interface: **`ixfx-tree-list`**, **`ixfx-miller-list`**, and **`ixfx-crumb-navigation`**. This document describes the shared API, plus component-specifi",
194
214
  "path": "docs-user/tree.md"
195
215
  },
216
+ {
217
+ "name": "twosplit-layout",
218
+ "summary": "A Lit web component that lays out two elements within a container with a split bar to adjust their proportions.",
219
+ "path": "docs-user/twosplit-layout.md"
220
+ },
196
221
  {
197
222
  "name": "util",
198
223
  "summary": "A tooltip component that renders a value with optional icon and formatted number display. Uses native render root (no shadow DOM).",