@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.
- package/bundle/index.d.ts +1918 -835
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +22137 -16840
- package/bundle/index.js.map +1 -1
- package/bundle/style.css +1 -1
- package/dist/ac-text.d.ts +1 -1
- package/dist/ac-text.js +3 -2
- package/dist/ac-text.js.map +1 -1
- package/dist/{ac-token-BAa6ke09.d.ts → ac-token--_vCHXWw.d.ts} +2 -2
- package/dist/{ac-token-BAa6ke09.d.ts.map → ac-token--_vCHXWw.d.ts.map} +1 -1
- package/dist/ac-token.d.ts +1 -1
- package/dist/ac-token.js +3 -2
- package/dist/ac-token.js.map +1 -1
- package/dist/{button-Bn0BuLpO.js → button-DsFbV4XD.js} +5 -4
- package/dist/button-DsFbV4XD.js.map +1 -0
- package/dist/button.d.ts.map +1 -1
- package/dist/button.js +1 -1
- package/dist/checkbox.js +1 -1
- package/dist/{colour-picker-0stHB90J.js → colour-picker-Ddsk-7E_.js} +4 -3
- package/dist/{colour-picker-0stHB90J.js.map → colour-picker-Ddsk-7E_.js.map} +1 -1
- package/dist/colour-picker.js +1 -1
- package/dist/crumbs.d.ts +1 -1
- package/dist/crumbs.js +4 -2
- package/dist/crumbs.js.map +1 -1
- package/dist/data-display.js +1 -1
- package/dist/{data-grid-C8qIo1-u.d.ts → data-grid-Bh4YrQGK.d.ts} +2 -2
- package/dist/{data-grid-C8qIo1-u.d.ts.map → data-grid-Bh4YrQGK.d.ts.map} +1 -1
- package/dist/data-grid.d.ts +1 -1
- package/dist/data-grid.js +4 -2
- package/dist/data-grid.js.map +1 -1
- package/dist/{defaults-BCiDhZbF.js → defaults-BJwDsMH4.js} +27 -20
- package/dist/defaults-BJwDsMH4.js.map +1 -0
- package/dist/editable-label-rzXkjf1H.d.ts +75 -0
- package/dist/editable-label-rzXkjf1H.d.ts.map +1 -0
- package/dist/editable-label.d.ts +2 -75
- package/dist/editable-label.js +2 -1
- package/dist/editable-label.js.map +1 -1
- package/dist/{fallbacks-DEE1nCTt.js → fallbacks-DNI9xt0F.js} +2 -2
- package/dist/{fallbacks-DEE1nCTt.js.map → fallbacks-DNI9xt0F.js.map} +1 -1
- package/dist/{grouped-item-lister-_pMDwIo1.d.ts → grouped-item-lister-DnWB-eFT.d.ts} +2 -2
- package/dist/{grouped-item-lister-_pMDwIo1.d.ts.map → grouped-item-lister-DnWB-eFT.d.ts.map} +1 -1
- package/dist/grouped-item-lister.d.ts +1 -1
- package/dist/{hex-editor-Bk6rxhfg.js → hex-editor-BiRfwTz1.js} +2 -2
- package/dist/{hex-editor-Bk6rxhfg.js.map → hex-editor-BiRfwTz1.js.map} +1 -1
- package/dist/hex.js +1 -1
- package/dist/{icon-DpA9x2Ve.d.ts → icon-CDGprSdE.d.ts} +1 -12
- package/dist/icon-CDGprSdE.d.ts.map +1 -0
- package/dist/{icons-Bm2ByQ3s.js → icon-DcEEGQa_.js} +38 -12
- package/dist/icon-DcEEGQa_.js.map +1 -0
- package/dist/icons.d.ts +3 -3
- package/dist/icons.js +9 -3
- package/dist/icons.js.map +1 -0
- package/dist/{index-CKtLhgYj.d.ts → index-9AhaHIT2.d.ts} +3 -3
- package/dist/{index-CKtLhgYj.d.ts.map → index-9AhaHIT2.d.ts.map} +1 -1
- package/dist/index-CG2vIgPv.d.ts +101 -0
- package/dist/index-CG2vIgPv.d.ts.map +1 -0
- package/dist/{index-BtGFYfaK.d.ts → index-CS9yM8L-.d.ts} +14 -4
- package/dist/index-CS9yM8L-.d.ts.map +1 -0
- package/dist/index.d.ts +861 -71
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4257 -931
- package/dist/index.js.map +1 -1
- package/dist/interaction-D5XdKzbR.js +2 -0
- package/dist/labelled-radial-input.js +2 -1
- package/dist/labelled-radial-input.js.map +1 -1
- package/dist/labelled-slider-input-DzigjMwP.js +808 -0
- package/dist/labelled-slider-input-DzigjMwP.js.map +1 -0
- package/dist/labelled-slider-input.d.ts +2 -0
- package/dist/labelled-slider-input.js +2 -0
- package/dist/{types-DlVNJez_.d.ts → list-selection-types-DSuRNWpx.d.ts} +10 -2
- package/dist/list-selection-types-DSuRNWpx.d.ts.map +1 -0
- package/dist/menu-CXDFJxNm.js +825 -0
- package/dist/menu-CXDFJxNm.js.map +1 -0
- package/dist/{menu-item-Bs2MYyHq.d.ts → menu-item-CZi9vy5O.d.ts} +4 -2
- package/dist/menu-item-CZi9vy5O.d.ts.map +1 -0
- package/dist/menu-item-D2S4i8u1.js +809 -0
- package/dist/menu-item-D2S4i8u1.js.map +1 -0
- package/dist/menu.d.ts +2 -2
- package/dist/menu.js +2 -1
- package/dist/miller.d.ts +1 -1
- package/dist/miller.js +3 -2
- package/dist/miller.js.map +1 -1
- package/dist/narrowed-text.js +4 -2
- package/dist/narrowed-text.js.map +1 -1
- package/dist/notification-pill-DyZydXoZ.d.ts.map +1 -1
- package/dist/notifier.js +4 -3
- package/dist/notifier.js.map +1 -1
- package/dist/panel.d.ts +13 -13
- package/dist/panel.d.ts.map +1 -1
- package/dist/panel.js +37 -253
- package/dist/panel.js.map +1 -1
- package/dist/plots.js +1 -1
- package/dist/polar-pad.js +1 -1
- package/dist/{radial-input-Dk8Wydp5.js → radial-input-Dp5qz0ly.js} +2 -2
- package/dist/{radial-input-Dk8Wydp5.js.map → radial-input-Dp5qz0ly.js.map} +1 -1
- package/dist/radial-input.js +1 -1
- package/dist/range-input.js +1 -1
- package/dist/range.js +1 -1
- package/dist/registry-BEtb8_wB.js +46 -0
- package/dist/registry-BEtb8_wB.js.map +1 -0
- package/dist/select-horiz.js +3 -2
- package/dist/select-horiz.js.map +1 -1
- package/dist/{slider-input-XppH3vcy.d.ts → slider-input-BfRUW2ZB.d.ts} +16 -3
- package/dist/slider-input-BfRUW2ZB.d.ts.map +1 -0
- package/dist/{slider-input-CT4gcF-o.js → slider-input-De4eoT11.js} +382 -67
- package/dist/slider-input-De4eoT11.js.map +1 -0
- package/dist/slider-input.d.ts +1 -1
- package/dist/slider-input.js +1 -1
- package/dist/snackbar.js +1 -1
- package/dist/split-layout-BFSbUZQv.js +969 -0
- package/dist/split-layout-BFSbUZQv.js.map +1 -0
- package/dist/split-layout-JK3xWVX2.d.ts +179 -0
- package/dist/split-layout-JK3xWVX2.d.ts.map +1 -0
- package/dist/split-layout.d.ts +2 -60
- package/dist/split-layout.js +1 -617
- package/dist/style.css +1 -1
- package/dist/swipe.js +1 -1
- package/dist/{tab-list-D_1cYhB_.d.ts → tab-list-CGyPt07Z.d.ts} +10 -2
- package/dist/tab-list-CGyPt07Z.d.ts.map +1 -0
- package/dist/tabs.d.ts +1 -1
- package/dist/tabs.js +128 -27
- package/dist/tabs.js.map +1 -1
- package/dist/{tickled-controller-BtevrRLV.d.ts → tickled-controller-CRzdIEjJ.d.ts} +7 -1
- package/dist/tickled-controller-CRzdIEjJ.d.ts.map +1 -0
- package/dist/{tickled-controller-DkAU-0qL.js → tickled-controller-h9GmJ_bU.js} +8 -2
- package/dist/tickled-controller-h9GmJ_bU.js.map +1 -0
- package/dist/titlebar-Bj5pfdw_.d.ts +50 -0
- package/dist/titlebar-Bj5pfdw_.d.ts.map +1 -0
- package/dist/titlebar.d.ts +2 -0
- package/dist/titlebar.js +324 -0
- package/dist/titlebar.js.map +1 -0
- package/dist/tooltip-BlvBUQxx.d.ts.map +1 -1
- package/dist/{tree-BO6BXI2b.js → tree-Ch4W8qXQ.js} +4 -3
- package/dist/{tree-BO6BXI2b.js.map → tree-Ch4W8qXQ.js.map} +1 -1
- package/dist/tree.d.ts +1 -1
- package/dist/tree.js +1 -1
- package/dist/vertical-list-ClnqTkF3.js +814 -0
- package/dist/vertical-list-ClnqTkF3.js.map +1 -0
- package/dist/vertical-list.d.ts +8 -9
- package/dist/vertical-list.d.ts.map +1 -1
- package/dist/vertical-list.js +1 -736
- package/dist/{xy-axis-B6yMjckW.js → xy-axis-DWizs0bt.js} +109 -3
- package/dist/xy-axis-DWizs0bt.js.map +1 -0
- package/dist/xy-pad.js +1 -1
- package/docs-user/README.md +5 -0
- package/docs-user/detail-list.md +398 -0
- package/docs-user/dock.md +461 -0
- package/docs-user/icons.md +4 -0
- package/docs-user/index.json +28 -3
- package/docs-user/labelled-slider-input.md +198 -0
- package/docs-user/llms.txt +6 -1
- package/docs-user/panel.md +20 -1
- package/docs-user/slider-input.md +55 -15
- package/docs-user/split-layout.md +136 -170
- package/docs-user/tabs.md +54 -0
- package/docs-user/titlebar.md +102 -0
- package/docs-user/toolbar.md +95 -22
- package/docs-user/twosplit-layout.md +254 -0
- package/docs-user/user-catalog.md +8 -1
- package/docs-user/user-theming.md +109 -0
- package/llms.txt +6 -1
- package/package.json +1 -2
- package/dist/button-Bn0BuLpO.js.map +0 -1
- package/dist/defaults-BCiDhZbF.js.map +0 -1
- package/dist/editable-label.d.ts.map +0 -1
- package/dist/icon-DpA9x2Ve.d.ts.map +0 -1
- package/dist/icons-Bm2ByQ3s.js.map +0 -1
- package/dist/index-BtGFYfaK.d.ts.map +0 -1
- package/dist/menu-DIfU70AA.js +0 -1609
- package/dist/menu-DIfU70AA.js.map +0 -1
- package/dist/menu-item-Bs2MYyHq.d.ts.map +0 -1
- package/dist/slider-input-CT4gcF-o.js.map +0 -1
- package/dist/slider-input-XppH3vcy.d.ts.map +0 -1
- package/dist/split-layout.d.ts.map +0 -1
- package/dist/split-layout.js.map +0 -1
- package/dist/tab-list-D_1cYhB_.d.ts.map +0 -1
- package/dist/tickled-controller-BtevrRLV.d.ts.map +0 -1
- package/dist/tickled-controller-DkAU-0qL.js.map +0 -1
- package/dist/types-DlVNJez_.d.ts.map +0 -1
- package/dist/vertical-list.js.map +0 -1
- 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.
|
package/docs-user/icons.md
CHANGED
|
@@ -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
|
|
package/docs-user/index.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ixfx/components",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"generated": "2026-
|
|
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
|
|
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).",
|