@ixfx/components 0.6.2 → 0.7.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.
- package/bundle/index.d.ts +1490 -615
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +20477 -15855
- package/bundle/index.js.map +1 -1
- package/bundle/style.css +1 -1
- package/dist/ac-text.js +3 -2
- package/dist/ac-text.js.map +1 -1
- package/dist/ac-token.js +3 -2
- package/dist/ac-token.js.map +1 -1
- package/dist/{button-Bn0BuLpO.js → button-Dat2yHAU.js} +4 -3
- package/dist/button-Dat2yHAU.js.map +1 -0
- package/dist/button.js +1 -1
- package/dist/checkbox.js +1 -1
- package/dist/{colour-picker-0stHB90J.js → colour-picker-BSDBL-QL.js} +4 -3
- package/dist/{colour-picker-0stHB90J.js.map → colour-picker-BSDBL-QL.js.map} +1 -1
- package/dist/colour-picker.js +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.js +4 -2
- package/dist/data-grid.js.map +1 -1
- package/dist/{defaults-BCiDhZbF.js → defaults-BkWCPJFL.js} +3 -20
- package/dist/defaults-BkWCPJFL.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/{icons-Bm2ByQ3s.js → icon-CvnPanhA.js} +3 -6
- package/dist/icon-CvnPanhA.js.map +1 -0
- package/dist/icons.js +8 -2
- package/dist/icons.js.map +1 -0
- package/dist/index-CG2vIgPv.d.ts +101 -0
- package/dist/index-CG2vIgPv.d.ts.map +1 -0
- package/dist/index.d.ts +655 -65
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3682 -924
- 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-BjRvlgPy.js +825 -0
- package/dist/menu-BjRvlgPy.js.map +1 -0
- package/dist/menu-item-B-zYrTTi.js +797 -0
- package/dist/menu-item-B-zYrTTi.js.map +1 -0
- 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/notifier.js +3 -2
- 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-CkW7q09Q.js +21 -0
- package/dist/registry-CkW7q09Q.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-MPwq_1Jj.d.ts} +2 -1
- package/dist/tab-list-MPwq_1Jj.d.ts.map +1 -0
- package/dist/tabs.d.ts +1 -1
- package/dist/tabs.js +96 -14
- 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-DaCfpliv.d.ts +50 -0
- package/dist/titlebar-DaCfpliv.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/{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-CxHC8z5E.js} +109 -3
- package/dist/xy-axis-CxHC8z5E.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 +310 -0
- package/docs-user/index.json +27 -2
- 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 +34 -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/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/icons-Bm2ByQ3s.js.map +0 -1
- package/dist/menu-DIfU70AA.js +0 -1609
- package/dist/menu-DIfU70AA.js.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,398 @@
|
|
|
1
|
+
# `ixfx-detail-list`
|
|
2
|
+
|
|
3
|
+
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 a fixed vertical extent and scrolls horizontally.
|
|
4
|
+
|
|
5
|
+
Selection semantics match `ixfx-vertical-list` — same modes, same `list-*` events, same keyboard gestures — but navigation is a 2-D grid (Up/Down move within a column, Left/Right jump columns).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
1. [Quick start](#quick-start)
|
|
12
|
+
2. [Item types](#item-types)
|
|
13
|
+
3. [Properties](#properties)
|
|
14
|
+
4. [Selection](#selection)
|
|
15
|
+
5. [Interaction modes](#interaction-modes)
|
|
16
|
+
6. [Custom checkbox](#custom-checkbox)
|
|
17
|
+
7. [Events](#events)
|
|
18
|
+
8. [Keyboard navigation](#keyboard-navigation)
|
|
19
|
+
9. [Incremental search](#incremental-search)
|
|
20
|
+
10. [Filter predicate](#filter-predicate)
|
|
21
|
+
11. [Sorting](#sorting)
|
|
22
|
+
12. [Asynchronous data](#asynchronous-data)
|
|
23
|
+
13. [CSS variables](#css-variables)
|
|
24
|
+
14. [CSS parts](#css-parts)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
### Declarative HTML items
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<ixfx-detail-list id="my-list" selection-mode="single">
|
|
34
|
+
<li>Alpha</li>
|
|
35
|
+
<li>Bravo</li>
|
|
36
|
+
<li>Charlie</li>
|
|
37
|
+
</ixfx-detail-list>
|
|
38
|
+
|
|
39
|
+
<script type="module">
|
|
40
|
+
import '@ixfx/components';
|
|
41
|
+
|
|
42
|
+
const list = document.querySelector('#my-list');
|
|
43
|
+
list.addEventListener('list-select', ({ detail }) => {
|
|
44
|
+
const [item] = detail.selected;
|
|
45
|
+
console.log('selected:', item?.textContent);
|
|
46
|
+
});
|
|
47
|
+
</script>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Items flow down columns (vertically) then wrap to the next column:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Alpha Golf November Uniform
|
|
54
|
+
Bravo Hotel Oscar Victor
|
|
55
|
+
Charlie India Papa Whiskey
|
|
56
|
+
Delta Juliett Quebec X-ray
|
|
57
|
+
Echo Kilo Romeo Yankee
|
|
58
|
+
Foxtrot Lima Sierra Zulu
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Programmatic items
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import type { DetailListElement } from '@ixfx/components';
|
|
65
|
+
|
|
66
|
+
const list = document.querySelector<DetailListElement>('ixfx-detail-list')!;
|
|
67
|
+
|
|
68
|
+
list.addItem('Document A');
|
|
69
|
+
list.addItem('Image 01.png');
|
|
70
|
+
|
|
71
|
+
// Or pass an element directly
|
|
72
|
+
const li = document.createElement('li');
|
|
73
|
+
li.textContent = 'Custom item';
|
|
74
|
+
list.addItem(li);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Item types
|
|
80
|
+
|
|
81
|
+
### Plain `<li>` elements
|
|
82
|
+
|
|
83
|
+
The simplest item type. Items are positioned by CSS flexbox into columns.
|
|
84
|
+
|
|
85
|
+
```html
|
|
86
|
+
<ixfx-detail-list>
|
|
87
|
+
<li>Item one</li>
|
|
88
|
+
<li>Item two</li>
|
|
89
|
+
</ixfx-detail-list>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Attributes supported on `<li>`:**
|
|
93
|
+
|
|
94
|
+
| Attribute | Description |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `data-search-label` | Override searchable text (defaults to `textContent`) |
|
|
97
|
+
| `data-key` | Stable identity for selection persistence across sort/rebuild |
|
|
98
|
+
| `data-value` | Alternative to `data-key` |
|
|
99
|
+
| `data-icon` | Icon name to display (injects `<ixfx-icon>`) |
|
|
100
|
+
| `not-checked` | In `checked` mode, renders without a checkbox and cannot be checked |
|
|
101
|
+
| `hidden` | Hides the item from view and navigation |
|
|
102
|
+
|
|
103
|
+
### Object items
|
|
104
|
+
|
|
105
|
+
Pass an array of objects and configure how to display them:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
const files = [
|
|
109
|
+
{ name: 'Report Q1.pdf', size: 220, kind: 'doc' },
|
|
110
|
+
{ name: 'Photo 01.png', size: 1400, kind: 'img' },
|
|
111
|
+
{ name: 'Notes.txt', size: 3, kind: 'doc' },
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
list.items = files;
|
|
115
|
+
list.displayProperty = 'name';
|
|
116
|
+
list.keyProperty = 'name'; // Preserves selection across rebuilds
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Object-related properties:**
|
|
120
|
+
|
|
121
|
+
| Property | Type | Description |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `items` | `readonly unknown[]` | Array of objects to render |
|
|
124
|
+
| `displayProperty` | `string` | Property name to use as label |
|
|
125
|
+
| `keyProperty` | `string` | Property name for stable identity |
|
|
126
|
+
| `iconProperty` | `string` | Property name for icon name |
|
|
127
|
+
| `formatter` | `(item: unknown, index: number) => string \| Node` | Custom renderer |
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
list.formatter = (item, index) => {
|
|
131
|
+
const file = item as { name: string; size: number };
|
|
132
|
+
return `${file.name} (${file.size} KB)`;
|
|
133
|
+
};
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Retrieve the original data for an element:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const data = list.getItemData(liElement);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Properties
|
|
145
|
+
|
|
146
|
+
| Property | Attribute | Type | Default | Description |
|
|
147
|
+
|---|---|---|---|---|
|
|
148
|
+
| `selectionMode` | `selection-mode` | `'single' \| 'multiple' \| 'none'` | `'single'` | How selection works |
|
|
149
|
+
| `interactionMode` | `interaction-mode` | `'implicit' \| 'standard' \| 'manual' \| 'checked' \| 'sticky' \| 'vscode'` | `'standard'` | Click/keyboard behavior |
|
|
150
|
+
| `sort` | `sort` | `'none' \| 'ascending' \| 'descending'` | `'none'` | Sort direction |
|
|
151
|
+
| `sortBy` | `sort-by` | `string` | — | Object property to sort by |
|
|
152
|
+
| `sortComparator` | — | `(a, b) => number` | — | Custom sort function |
|
|
153
|
+
| `filterPredicate` | — | `(el: Element) => boolean` | — | Filter visible items |
|
|
154
|
+
| `itemWrap` | `item-wrap` | `'stop' \| 'wrap'` | `'stop'` | Keyboard wrap behavior |
|
|
155
|
+
| `provider` | — | `IDataProvider` | — | Async data source |
|
|
156
|
+
| `checkboxRenderer` | — | `function \| object` | — | Custom checkbox |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Selection
|
|
161
|
+
|
|
162
|
+
### Getters
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const selected = list.selectedItem; // First selected element or undefined
|
|
166
|
+
const allSelected = list.selectedItems; // Array of all selected elements
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Methods
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
list.select(item); // Select an item
|
|
173
|
+
list.deselect(item); // Deselect an item
|
|
174
|
+
list.selectMany([item1, item2]); // Select multiple
|
|
175
|
+
list.clearSelection(); // Clear all selections
|
|
176
|
+
list.selectAll(); // Select all visible items (if permitted)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Interaction modes
|
|
182
|
+
|
|
183
|
+
| Mode | Description |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `standard` | Click replaces selection. Ctrl+click toggles. Shift+click creates range. Cmd+A selects all. |
|
|
186
|
+
| `implicit` | Click always replaces selection (no modifier support). |
|
|
187
|
+
| `checked` | Checkbox column toggles selection; rows marked `not-checked` have no checkbox. |
|
|
188
|
+
| `sticky` | Every click toggles. Cmd+A selects all. |
|
|
189
|
+
| `vscode` | Like `standard`, but Shift+Arrow extends selection range. |
|
|
190
|
+
| `manual` | No auto-selection; drive state via `select()` / `deselect()` programmatically. |
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Custom checkbox
|
|
195
|
+
|
|
196
|
+
In `checked` mode, provide a custom checkbox renderer:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
list.checkboxRenderer = (checked) => checked
|
|
200
|
+
? html`<span>✓</span>`
|
|
201
|
+
: html``;
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Or use the shorthand object form:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
list.checkboxRenderer = { checked: '✓', unchecked: '' };
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Events
|
|
213
|
+
|
|
214
|
+
| Event | Detail | Description |
|
|
215
|
+
|---|---|---|
|
|
216
|
+
| `list-select` | `{ selected: Element[] }` | Selection changed |
|
|
217
|
+
| `list-tickle` | `{ item: Element }` | Hover/focus cursor moved |
|
|
218
|
+
| `list-activate` | `{ item: Element }` | Item activated (Enter/double-click) |
|
|
219
|
+
| `list-item-click` | `{ item: Element }` | Item clicked |
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
list.addEventListener('list-select', (e) => {
|
|
223
|
+
console.log('Selected:', [...e.detail.selected].map(el => el.textContent));
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Keyboard navigation
|
|
230
|
+
|
|
231
|
+
The list must have focus for keyboard navigation. Click an item to focus, or set `tabindex="0"` on the element.
|
|
232
|
+
|
|
233
|
+
| Key | Action |
|
|
234
|
+
|---|---|
|
|
235
|
+
| ↑ / ↓ | Move up/down within the current column |
|
|
236
|
+
| ← / → | Jump to previous/next column (same row) |
|
|
237
|
+
| Home | Jump to first item |
|
|
238
|
+
| End | Jump to last item |
|
|
239
|
+
| Enter | Activate item (fires `list-activate`) |
|
|
240
|
+
| Space | Toggle selection (in modes that support it) |
|
|
241
|
+
| Escape | Clear selection / close search |
|
|
242
|
+
| Ctrl+F | Open incremental search |
|
|
243
|
+
| Ctrl+A | Select all (in modes that support it) |
|
|
244
|
+
|
|
245
|
+
### Wrapping
|
|
246
|
+
|
|
247
|
+
Set `item-wrap="wrap"` to enable wraparound navigation:
|
|
248
|
+
|
|
249
|
+
```html
|
|
250
|
+
<ixfx-detail-list item-wrap="wrap">...</ixfx-detail-list>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
With wrapping:
|
|
254
|
+
- Down from last row → first row of next column (or first column if at end)
|
|
255
|
+
- Up from first row → last row of previous column (or last column if at start)
|
|
256
|
+
- Right from last column → first column
|
|
257
|
+
- Left from first column → last column
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Incremental search
|
|
262
|
+
|
|
263
|
+
Press Ctrl+F (or Cmd+F on Mac) to open the search overlay. Type to filter items in real-time using fuzzy matching.
|
|
264
|
+
|
|
265
|
+
Search matches against `data-search-label` (or `textContent` as fallback). Use `data-search-label` to match against a different string than what's displayed:
|
|
266
|
+
|
|
267
|
+
```html
|
|
268
|
+
<li data-search-label="document a alpha">Document A</li>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Highlight styling
|
|
272
|
+
|
|
273
|
+
Matched characters are highlighted with the `detail-list-search` highlight key. Style matches with:
|
|
274
|
+
|
|
275
|
+
```css
|
|
276
|
+
::highlight(detail-list-search) {
|
|
277
|
+
background: yellow;
|
|
278
|
+
color: black;
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Filter predicate
|
|
285
|
+
|
|
286
|
+
Use `filterPredicate` to programmatically hide items:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// Show only .doc files
|
|
290
|
+
list.filterPredicate = (el) => el.getAttribute('data-kind') === 'doc';
|
|
291
|
+
|
|
292
|
+
// Clear filter
|
|
293
|
+
list.filterPredicate = undefined;
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Filtered items are hidden from view, keyboard navigation, and selection.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Sorting
|
|
301
|
+
|
|
302
|
+
### Simple sort
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
list.sort = 'ascending'; // or 'descending'
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Sort by property
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
list.sortBy = 'size'; // Sort by 'size' property
|
|
312
|
+
list.sort = 'ascending';
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Custom comparator
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
list.sortComparator = (a, b) => {
|
|
319
|
+
// Sort by kind first, then by label
|
|
320
|
+
const kindCompare = a.data.kind.localeCompare(b.data.kind);
|
|
321
|
+
if (kindCompare !== 0) return kindCompare;
|
|
322
|
+
return a.label.localeCompare(b.label);
|
|
323
|
+
};
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The comparator receives `{ element, data, label }` where:
|
|
327
|
+
- `element` — the `<li>` DOM element
|
|
328
|
+
- `data` — the original object from `items` array
|
|
329
|
+
- `label` — the display string
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Asynchronous data
|
|
334
|
+
|
|
335
|
+
Use a `provider` for async data loading:
|
|
336
|
+
|
|
337
|
+
### Promise provider
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
list.provider = async () => {
|
|
341
|
+
const response = await fetch('/api/items');
|
|
342
|
+
return response.json();
|
|
343
|
+
};
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Streaming provider
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
list.provider = async function*() {
|
|
350
|
+
for (let i = 0; i < 100; i++) {
|
|
351
|
+
yield [{ id: i, name: `Item ${i}` }];
|
|
352
|
+
await delay(100);
|
|
353
|
+
}
|
|
354
|
+
}();
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
The provider function can return:
|
|
358
|
+
- A promise that resolves to an array
|
|
359
|
+
- An async iterable that yields arrays (streaming)
|
|
360
|
+
|
|
361
|
+
Streaming providers extend columns to the right as batches arrive.
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## CSS variables
|
|
366
|
+
|
|
367
|
+
| Variable | Default | Description |
|
|
368
|
+
|---|---|---|
|
|
369
|
+
| `--detail-list-bg` | `surface-3` | Background color |
|
|
370
|
+
| `--detail-list-text` | `surface-3-text` | Text color |
|
|
371
|
+
| `--detail-list-border` | `border` | Border color |
|
|
372
|
+
| `--detail-list-radius` | `radius-s` | Border radius |
|
|
373
|
+
| `--detail-list-column-width` | `220px` | Width of each column |
|
|
374
|
+
| `--detail-list-column-gap` | `8px` | Gap between columns |
|
|
375
|
+
| `--detail-list-item-height` | `28px` | Height of each item |
|
|
376
|
+
| `--detail-list-item-padding` | `0 var(--space-m)` | Item padding |
|
|
377
|
+
| `--detail-list-scrollbar-width` | `auto` | Scrollbar width |
|
|
378
|
+
| `--detail-list-scrollbar-gutter` | `auto` | Scrollbar gutter |
|
|
379
|
+
| `--detail-list-scrollbar-color` | `auto` | Scrollbar color |
|
|
380
|
+
|
|
381
|
+
Plus all `--item-bg-*`, `--item-text-*` variables from the theme for selection/tickle states.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## CSS parts
|
|
386
|
+
|
|
387
|
+
| Part | Description |
|
|
388
|
+
|---|---|
|
|
389
|
+
| `list` | The `<ul>` container |
|
|
390
|
+
| `search-overlay` | The Ctrl+F search overlay |
|
|
391
|
+
| `checkbox` | The checkbox element (in `checked` mode) |
|
|
392
|
+
| `loading` | The loading indicator |
|
|
393
|
+
|
|
394
|
+
```css
|
|
395
|
+
ixfx-detail-list::part(search-overlay) {
|
|
396
|
+
background: rgba(0, 0, 0, 0.8);
|
|
397
|
+
}
|
|
398
|
+
```
|
|
@@ -0,0 +1,310 @@
|
|
|
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" title="Explorer" closable>…</ixfx-dock-panel>
|
|
11
|
+
<ixfx-dock-panel panel-id="editor" title="Editor" closable>…</ixfx-dock-panel>
|
|
12
|
+
</ixfx-dock-host>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
- **Flat panel pool + model-driven host.** Panels stay direct light-DOM
|
|
18
|
+
children of the host for their whole life — they are never reparented
|
|
19
|
+
within a host. The host owns an in-memory `DockNode` tree (`dock-model.ts`),
|
|
20
|
+
renders split/tab scaffolding in shadow DOM, and projects panels into leaves
|
|
21
|
+
via `slot="dock-panel-{id}"`. Moving a panel = model mutation → re-render →
|
|
22
|
+
slot reassignment; panel DOM and state are never destroyed.
|
|
23
|
+
- **Split nodes** render as a single `ixfx-split-layout` (n-way) with one
|
|
24
|
+
wrapper child per child node. Splits of the *other* direction are emitted as
|
|
25
|
+
nested `<ixfx-split-layout>` directly (the split element recurses into them).
|
|
26
|
+
Per-child weights ride on `size` attributes; stack collapse rides on
|
|
27
|
+
`collapsed` / `when-small` attributes.
|
|
28
|
+
- **Stack (leaf) nodes** always render the same chrome: an `ixfx-tab-list`
|
|
29
|
+
strip (with generated `ixfx-tab-list-item`s, one per panel) plus a single
|
|
30
|
+
body slot for the active panel — so titlebars are consistent whether a stack
|
|
31
|
+
holds one panel or many. Docked panels are `presentation="tabbed"`
|
|
32
|
+
(content only); the tab item is the drag handle and carries title / icon /
|
|
33
|
+
close / per-panel toolbar. `ixfx-tab-panels` is deliberately not used — the
|
|
34
|
+
host owns slot projection. Panels outside a host (or not currently docked)
|
|
35
|
+
render `presentation="solo"` with an `ixfx-titlebar` and flat content.
|
|
36
|
+
|
|
37
|
+
## Data model (`types.ts` / `dock-model.ts`)
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
type DockLayout = { version: 1; root: DockNode };
|
|
41
|
+
type DockNode = DockSplitNode | DockStackNode;
|
|
42
|
+
type DockSplitNode = { kind: 'split'; direction: 'row' | 'column';
|
|
43
|
+
sizes: number[]; children: DockNode[] };
|
|
44
|
+
type DockStackNode = { kind: 'stack'; panels: string[]; active: string;
|
|
45
|
+
collapsed?: boolean; preCollapseSize?: string };
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`direction` uses `ixfx-split-layout`'s flexbox sense: `row` = side by side,
|
|
49
|
+
`column` = stacked. A "split horizontal" drop (horizontal divider, panels
|
|
50
|
+
stacked vertically) maps to `direction: "column"`.
|
|
51
|
+
|
|
52
|
+
`normalize()` runs after every mutation: empty stacks are removed, ≤1-child
|
|
53
|
+
splits dissolve, same-direction child splits flatten into their parent,
|
|
54
|
+
`sizes` stays parallel to `children`, and `active` always points at a member.
|
|
55
|
+
The tree is plain and mutable, owned exclusively by the host.
|
|
56
|
+
|
|
57
|
+
## Drop targeting — the 5-zone model
|
|
58
|
+
|
|
59
|
+
While dragging, the host overlay highlights the resulting region:
|
|
60
|
+
|
|
61
|
+
| Region | Operation | Result |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| leaf centre (~50% box) | `tabs` | insert into that stack (at the hovered strip gap when over the strip) |
|
|
64
|
+
| leaf left / right band | `split-vertical` | new stack beside the leaf |
|
|
65
|
+
| leaf top / bottom band | `split-horizontal` | new stack above/below the leaf |
|
|
66
|
+
| host gutter / edges (outside all leaves) | split **root** | new stack against the whole host |
|
|
67
|
+
|
|
68
|
+
When the target's parent split already matches the drop direction the new
|
|
69
|
+
stack is inserted beside it (50/50 weight split); otherwise the target is
|
|
70
|
+
wrapped in a new split. `Escape` or `pointercancel` cancels with no mutation.
|
|
71
|
+
|
|
72
|
+
## Junction resize
|
|
73
|
+
|
|
74
|
+
Where a parent gutter line meets a nested split's gutter (T and + crossings
|
|
75
|
+
of three or four panels), the host renders a small hover-revealed handle
|
|
76
|
+
(colour: `--dock-junction-color`, default `var(--accent)`). Dragging it
|
|
77
|
+
starts the vertical and horizontal gutter drags at once via
|
|
78
|
+
`ixfx-split-layout`'s `beginGutterDrag()`, resizing all adjacent panels with
|
|
79
|
+
one gesture — as in VS Code. Handle positions are recomputed on renders,
|
|
80
|
+
split resizes, and host resizes; handles are hidden during dock drags.
|
|
81
|
+
|
|
82
|
+
## Restrictions
|
|
83
|
+
|
|
84
|
+
- Host `accepts` attribute: space-separated `tabs`, `horizontal`, `vertical`,
|
|
85
|
+
convenience `all`; `""` / `none` rejects everything. Default allows all.
|
|
86
|
+
- Panel `accepts` attribute: applies when that panel is a **solo** drop
|
|
87
|
+
target. Default unrestricted.
|
|
88
|
+
- `host.canAccept = (panelId, op) => boolean` — finest-grained veto, consulted
|
|
89
|
+
per candidate zone.
|
|
90
|
+
|
|
91
|
+
A zone lights up only if all three allow it; disallowed zones are inert.
|
|
92
|
+
|
|
93
|
+
## Events
|
|
94
|
+
|
|
95
|
+
`ixfx-dock-host`: `dock-change` (`{ layout }` after structural mutations),
|
|
96
|
+
`dock-panel-added`, `dock-panel-removed`, `dock-panel-activated`,
|
|
97
|
+
`dock-panel-drag-start`.
|
|
98
|
+
|
|
99
|
+
Cross-window (see [Cross-window docking](#cross-window-docking-electron-etc)):
|
|
100
|
+
`dock-panel-drag-out` / `dock-panel-drag-in`
|
|
101
|
+
(`{ panelId, clientX, clientY, screenX, screenY }`) when the drag pointer
|
|
102
|
+
leaves / re-enters every host's bounds, and `dock-panel-tear-out`
|
|
103
|
+
(`{ panelId, snapshot, clientX, clientY, screenX, screenY }`) when a panel is
|
|
104
|
+
released with no host under the pointer.
|
|
105
|
+
|
|
106
|
+
`ixfx-dock-panel`: `dock-panel-drag-start` (`{ panelId, pointerId, clientX,
|
|
107
|
+
clientY }`), plus `close` / `toggle` forwarded from its `ixfx-titlebar`.
|
|
108
|
+
The panel removes itself on close; the host's pool sync routes that through
|
|
109
|
+
the model and emits `dock-panel-removed` + `dock-change`.
|
|
110
|
+
|
|
111
|
+
The host consumes `ixfx-split-layout` events: `ixfx-split-resize` /
|
|
112
|
+
`ixfx-split-change` (live gutter weights folded into `DockSplitNode.sizes`),
|
|
113
|
+
`ixfx-split-collapse` / `ixfx-split-expand` (→ `DockStackNode.collapsed`), and
|
|
114
|
+
`ixfx-split-close` (cancelable — prevented and rerouted so panel elements
|
|
115
|
+
survive, re-homed into the first other stack).
|
|
116
|
+
|
|
117
|
+
## Public API — `ixfx-dock-host`
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
saveLayout(): DockLayout // folds live split weights from the DOM
|
|
121
|
+
loadLayout(layout: DockLayout): void
|
|
122
|
+
addPanel(panel: DockPanelElement | DockPanelInit, target?: {
|
|
123
|
+
stack?: string; index?: number; op?: DockOperation // stack = a panel-id in the target stack
|
|
124
|
+
}): void
|
|
125
|
+
removePanel(panelId: string): boolean // removes the element from the DOM too
|
|
126
|
+
activatePanel(panelId: string): boolean
|
|
127
|
+
getPanelIds(): string[]
|
|
128
|
+
canAccept?: (panelId: string, op: DockOperation) => boolean
|
|
129
|
+
|
|
130
|
+
// Cross-window hooks (serializable in/out; no element handoff)
|
|
131
|
+
getPanelSnapshot(panelId: string): DockPanelSnapshot | null
|
|
132
|
+
detachPanel(panelId: string): DockPanelSnapshot | null // snapshot + removePanel
|
|
133
|
+
queryDropTarget(x: number, y: number, panelId?: string): DockDropTarget | null
|
|
134
|
+
previewExternalDrop(x: number, y: number, panelId?: string): DockDropTarget | null
|
|
135
|
+
endExternalDrop(): void
|
|
136
|
+
acceptExternalPanel(snap: DockPanelSnapshot | DockPanelInit,
|
|
137
|
+
at?: { clientX: number; clientY: number }): DockPanelElement | null
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The optional `layout` attribute accepts `DockLayout` JSON as the initial
|
|
141
|
+
layout; without it, all pooled panels go into one root stack in source order.
|
|
142
|
+
Panels present in the pool but missing from a loaded layout are appended to
|
|
143
|
+
the first stack.
|
|
144
|
+
|
|
145
|
+
## Cross-host drags
|
|
146
|
+
|
|
147
|
+
Panels can be dragged between hosts (register via the shared drag
|
|
148
|
+
coordinator automatically). The source host releases the panel from its model
|
|
149
|
+
and the target host adopts the element — the one sanctioned reparent
|
|
150
|
+
(`connectedCallback` re-runs on the panel; it holds no per-host state).
|
|
151
|
+
`panel-id` must be unique across all hosts that can exchange panels.
|
|
152
|
+
|
|
153
|
+
This works only *within one document*. The drag coordinator is module state, so
|
|
154
|
+
it never spans separate windows (`BrowserWindow` / `WebContentsView` / a popped
|
|
155
|
+
`window.open`). Moving a panel across that boundary is described next.
|
|
156
|
+
|
|
157
|
+
## Cross-window docking (Electron, etc.)
|
|
158
|
+
|
|
159
|
+
The dock deliberately ships **no** window/IPC/Node dependency. Instead it
|
|
160
|
+
exposes the events and methods an outer coordinator (your Electron main
|
|
161
|
+
process, a `SharedWorker`, `BroadcastChannel`, `postMessage`…) needs to move a
|
|
162
|
+
panel between windows. Nothing DOM crosses the boundary — you transport a
|
|
163
|
+
plain, structured-clone-safe **`DockPanelSnapshot`** and rebuild the panel on
|
|
164
|
+
the far side.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
interface DockPanelSnapshot { // structured-clone safe
|
|
168
|
+
panelId: string; title: string; iconName: string;
|
|
169
|
+
closable: boolean; collapsible: boolean; accepts: string;
|
|
170
|
+
contentHTML: string; // best-effort innerHTML snapshot; see note below
|
|
171
|
+
}
|
|
172
|
+
interface DockDropTarget { // serializable hit-test result
|
|
173
|
+
operation: 'tabs' | 'split-horizontal' | 'split-vertical';
|
|
174
|
+
targetPath: number[]; // stack path in the target host model; [] = root
|
|
175
|
+
side: 'before' | 'after';
|
|
176
|
+
gapIndex?: number;
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
> **`contentHTML`** is a convenience for panels whose body is static markup.
|
|
181
|
+
> If a panel holds live state, framework-managed DOM, canvas/WebGL, or running
|
|
182
|
+
> timers, ignore it and rebuild the body from your own model in the new window
|
|
183
|
+
> (you already do this to render the panel the first time). The snapshot is
|
|
184
|
+
> identity + chrome; content ownership stays with your app.
|
|
185
|
+
|
|
186
|
+
### 1. Panel → new window (tear-out)
|
|
187
|
+
|
|
188
|
+
While a drag is in progress the host fires boundary events as the pointer
|
|
189
|
+
leaves and re-enters the union of all host rects:
|
|
190
|
+
|
|
191
|
+
| Event | When | Use |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `dock-panel-drag-out` | pointer left every host | start a native drag image / detached preview window that follows the cursor |
|
|
194
|
+
| `dock-panel-drag-in` | pointer came back over a host | tear down that preview; the in-page drop overlay takes over again |
|
|
195
|
+
| `dock-panel-tear-out` | released with no host under the pointer | create the real window and move the panel |
|
|
196
|
+
|
|
197
|
+
All three carry `clientX/clientY` (source viewport) **and** `screenX/screenY`
|
|
198
|
+
(desktop) so the receiver can place a window. `dock-panel-tear-out` also
|
|
199
|
+
carries a ready-made `snapshot`.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
host.addEventListener('dock-panel-tear-out', (e) => {
|
|
203
|
+
const { snapshot, screenX, screenY } = e.detail;
|
|
204
|
+
window.dockBridge.openPanelWindow({ snapshot, screenX, screenY }); // → main process
|
|
205
|
+
host.detachPanel(snapshot.panelId); // snapshot already taken; drop it here
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
// main process
|
|
211
|
+
ipcMain.handle('dock:open-panel-window', (_e, { snapshot, screenX, screenY }) => {
|
|
212
|
+
const win = new BrowserWindow({ x: screenX - 40, y: screenY - 20, width: 480, height: 360, /* … */ });
|
|
213
|
+
win.loadFile('panel-window.html');
|
|
214
|
+
win.webContents.once('did-finish-load', () =>
|
|
215
|
+
win.webContents.send('dock:mount-panel', snapshot));
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
// panel-window.html renderer — a host that starts empty
|
|
221
|
+
ipcRenderer.on('dock:mount-panel', (_e, snapshot) => {
|
|
222
|
+
document.querySelector('ixfx-dock-host').acceptExternalPanel(snapshot);
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Because `pointerup` outside the OS window is not reliably delivered to a
|
|
227
|
+
renderer, prefer starting a native follow-the-cursor window on
|
|
228
|
+
`dock-panel-drag-out` and finalising from the **main** process on the global
|
|
229
|
+
mouse-up, rather than depending on `dock-panel-tear-out` firing. `tear-out`
|
|
230
|
+
is the best-effort path for release inside the window but between hosts.
|
|
231
|
+
|
|
232
|
+
### 2. Window → this host (tear-in)
|
|
233
|
+
|
|
234
|
+
Detecting an OS-level window drag is the app's job — Electron gives you the
|
|
235
|
+
screen cursor (`screen.getCursorScreenPoint()`) and window bounds
|
|
236
|
+
(`win.getBounds()`), so the main process can tell, per frame, when a dragged
|
|
237
|
+
panel-window is hovering another window and where. Forward that to the target
|
|
238
|
+
renderer, which drives the host imperatively:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
// target renderer, fed pointer position + the dragged panel's snapshot over IPC
|
|
242
|
+
ipcRenderer.on('dock:external-drag-move', (_e, { snapshot, clientX, clientY }) => {
|
|
243
|
+
host.previewExternalDrop(clientX, clientY, snapshot.panelId); // lights the overlay
|
|
244
|
+
});
|
|
245
|
+
ipcRenderer.on('dock:external-drag-leave', () => host.endExternalDrop());
|
|
246
|
+
ipcRenderer.on('dock:external-drag-drop', (_e, { snapshot, clientX, clientY }) => {
|
|
247
|
+
const placed = host.acceptExternalPanel(snapshot, { clientX, clientY });
|
|
248
|
+
if (placed) ipcRenderer.send('dock:close-source-window'); // panel now lives here
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`previewExternalDrop` / `queryDropTarget` return the `DockDropTarget` that a
|
|
253
|
+
drop at that point would produce (or `null` when the point misses the host or
|
|
254
|
+
the zone is vetoed by `accepts` / `canAccept`), so the coordinator can also
|
|
255
|
+
show a yes/no cursor on the source side. `acceptExternalPanel` honours the
|
|
256
|
+
same restrictions and falls back to the first stack when `at` is omitted or
|
|
257
|
+
outside the host.
|
|
258
|
+
|
|
259
|
+
### Coordinate spaces
|
|
260
|
+
|
|
261
|
+
Events give you both. `client*` is the source renderer's viewport;
|
|
262
|
+
`screen*` is the desktop. To convert a desktop point into a target renderer's
|
|
263
|
+
viewport, the app subtracts that window's content bounds (main process:
|
|
264
|
+
`win.getContentBounds()`), accounting for `devicePixelRatio`. Keep the
|
|
265
|
+
conversion in the coordinator — the component only ever speaks the two spaces
|
|
266
|
+
it can measure.
|
|
267
|
+
|
|
268
|
+
### Identity
|
|
269
|
+
|
|
270
|
+
`panelId` must be unique across every host that can exchange panels, in every
|
|
271
|
+
window. `acceptExternalPanel` returns `null` if that id is already docked in
|
|
272
|
+
the target host (adopt / focus it yourself instead).
|
|
273
|
+
|
|
274
|
+
## Serialization
|
|
275
|
+
|
|
276
|
+
`saveLayout()` folds live gutter weights from each rendered
|
|
277
|
+
`ixfx-split-layout` (via `getSizes()`) into `DockSplitNode.sizes` before
|
|
278
|
+
serializing, so a save → drag → save round-trip preserves user adjustments.
|
|
279
|
+
`DockStackNode.collapsed` / `preCollapseSize` serialize so a saved layout can
|
|
280
|
+
restore collapse state without a live element.
|
|
281
|
+
|
|
282
|
+
## CSS variables
|
|
283
|
+
|
|
284
|
+
| Variable | Purpose | Default |
|
|
285
|
+
|---|---|---|
|
|
286
|
+
| `--dock-drop-zone-color` | drop highlight colour | `var(--accent)` |
|
|
287
|
+
| `--dock-drop-zone-opacity` | drop highlight opacity | `0.25` |
|
|
288
|
+
| `--dock-collapsed-size` | collapsed stack track size | `40px` |
|
|
289
|
+
| `--dock-tab-height` | tab item height, uniform across all leaf strips | `30px` |
|
|
290
|
+
| `--dock-split-bar-size` | thickness of the split-layout bars between leaves | `2px` |
|
|
291
|
+
| `--dock-junction-color` | junction resize handle affordance colour | `var(--accent)` |
|
|
292
|
+
|
|
293
|
+
Splitter styling delegates to `ixfx-split-layout` (`--split-*`); solo panel
|
|
294
|
+
chrome delegates to `ixfx-titlebar`, and the leaf body is the docked panel
|
|
295
|
+
surface. Each leaf carries a hairline border (`var(--border-subtle)`) with
|
|
296
|
+
slight rounding (`var(--radius-m)`). An empty
|
|
297
|
+
host renders a placeholder; replace its content with
|
|
298
|
+
`<div slot="empty">…</div>`.
|
|
299
|
+
|
|
300
|
+
## Deviations from the original plan (and why)
|
|
301
|
+
|
|
302
|
+
- **Tab-strip reorder** is handled by the dock drag coordinator (gap-index
|
|
303
|
+
insertion) rather than `ixfx-tab-list`'s own `drag-mode="reorder"` — running
|
|
304
|
+
both would race two ghosts for the same pointer. The gap computation
|
|
305
|
+
reuses the tab-list midpoint rule.
|
|
306
|
+
- **Closing a leaf via `ixfx-split-close`** re-homes its panels into the first
|
|
307
|
+
other stack (or the pool if none) instead of dropping them on the floor.
|
|
308
|
+
- **Group toolbar** renders per-strip split buttons (`↕` / `↔`) that split the
|
|
309
|
+
stack's active panel out — host-authored chrome in the strip's toolbar slot,
|
|
310
|
+
as the plan describes.
|