@adecore/ui 0.35.4 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/Button.d.ts +2 -3
  2. package/dist/Button.js +21 -46
  3. package/dist/Checkbox.js +1 -1
  4. package/dist/ChoiceCards.js +2 -2
  5. package/dist/IconPicker.d.ts +2 -1
  6. package/dist/IconPicker.js +504 -217
  7. package/dist/Input.d.ts +5 -3
  8. package/dist/Input.js +127 -112
  9. package/dist/Segmented.js +1 -1
  10. package/dist/SplitButton.d.ts +17 -0
  11. package/dist/SplitButton.js +237 -0
  12. package/dist/Stepper.js +1 -1
  13. package/dist/Switch.js +1 -1
  14. package/dist/Tile.js +2 -2
  15. package/dist/button-class.d.ts +6 -0
  16. package/dist/button-class.js +92 -0
  17. package/dist/command-palette/parts.d.ts +2 -2
  18. package/dist/command-palette/parts.js +125 -164
  19. package/dist/file-tree/empty-lines.d.ts +11 -0
  20. package/dist/file-tree/empty-lines.js +89 -0
  21. package/dist/file-tree/highlight.d.ts +4 -0
  22. package/dist/file-tree/highlight.js +75 -0
  23. package/dist/file-tree/parts.d.ts +9 -1
  24. package/dist/file-tree/parts.js +297 -205
  25. package/dist/file-tree/style.js +13 -0
  26. package/dist/file-tree/use-empty-lines.d.ts +3 -0
  27. package/dist/file-tree/use-empty-lines.js +176 -0
  28. package/dist/file-tree/use-file-tree.js +5 -1
  29. package/dist/file-tree/use-slots.d.ts +16 -2
  30. package/dist/file-tree/use-slots.js +186 -43
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +1 -0
  33. package/dist/locales/en.d.ts +1 -0
  34. package/dist/locales/en.js +1 -0
  35. package/dist/locales/nl.d.ts +1 -0
  36. package/dist/locales/nl.js +1 -0
  37. package/dist/select-shared.d.ts +1 -1
  38. package/dist/select-shared.js +1 -1
  39. package/dist/settings/SettingsDialog.js +1 -1
  40. package/dist/theme.css +33 -3
  41. package/package.json +1 -1
  42. package/src/Button.tsx +3 -36
  43. package/src/Checkbox.tsx +1 -1
  44. package/src/ChoiceCards.tsx +2 -2
  45. package/src/IconPicker.tsx +220 -136
  46. package/src/Input.tsx +19 -10
  47. package/src/Segmented.tsx +1 -1
  48. package/src/SplitButton.tsx +72 -0
  49. package/src/Stepper.tsx +6 -1
  50. package/src/Switch.tsx +1 -1
  51. package/src/Tile.tsx +2 -2
  52. package/src/button-class.ts +111 -0
  53. package/src/command-palette/parts.tsx +34 -2
  54. package/src/file-tree/empty-lines.ts +105 -0
  55. package/src/file-tree/highlight.ts +80 -0
  56. package/src/file-tree/parts.tsx +42 -11
  57. package/src/file-tree/style.ts +13 -0
  58. package/src/file-tree/use-empty-lines.ts +115 -0
  59. package/src/file-tree/use-file-tree.ts +2 -1
  60. package/src/file-tree/use-slots.ts +176 -17
  61. package/src/index.ts +1 -0
  62. package/src/locales/en.ts +1 -0
  63. package/src/locales/nl.ts +1 -0
  64. package/src/select-shared.ts +2 -1
  65. package/src/settings/SettingsDialog.tsx +1 -1
  66. package/src/theme.css +33 -3
@@ -0,0 +1,3 @@
1
+ import type { FileTree } from '@pierre/trees';
2
+ import { type FileTreeEmptyLabel } from './empty-lines.ts';
3
+ export declare function useEmptyLines(model: FileTree, frame: HTMLElement | null, emptyLabel: FileTreeEmptyLabel | undefined): (event: KeyboardEvent) => void;
@@ -0,0 +1,176 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import { useEffect, useLayoutEffect, useRef } from 'react';
3
+ import { emptyLineChanges, isEmptyLine, pastEmptyLine, skipEmptyLine } from './empty-lines.js';
4
+ import { movesFocus, visibleRows } from './helpers.js';
5
+ const POINTER_EVENTS = ['pointerdown', 'mousedown', 'click', 'dblclick', 'auxclick', 'contextmenu', 'dragstart'];
6
+ function onEmptyLine(event) {
7
+ return event.composedPath().some(node => node instanceof HTMLElement && node.dataset.emptyLine === 'true');
8
+ }
9
+ /*
10
+ * Keeps a line under every open directory that is known to be empty, and keeps the line out of
11
+ * reach: a pointer on it does nothing and a key passes it. Returns what to call with every key the
12
+ * tree hears, after anything that notes where focus was.
13
+ */
14
+ export function useEmptyLines(model, frame, emptyLabel) {
15
+ const $ = _c(12);
16
+ const latest = useRef(emptyLabel);
17
+ const reconcile = useRef(undefined);
18
+ const lastKey = useRef("");
19
+ const enabled = emptyLabel !== undefined;
20
+ let t0;
21
+ let t1;
22
+ if ($[0] !== emptyLabel) {
23
+ t0 = () => {
24
+ latest.current = emptyLabel;
25
+ reconcile.current?.();
26
+ };
27
+ t1 = [emptyLabel];
28
+ $[0] = emptyLabel;
29
+ $[1] = t0;
30
+ $[2] = t1;
31
+ } else {
32
+ t0 = $[1];
33
+ t1 = $[2];
34
+ }
35
+ useLayoutEffect(t0, t1);
36
+ let t2;
37
+ let t3;
38
+ if ($[3] !== enabled || $[4] !== model) {
39
+ t2 = () => {
40
+ if (!enabled) {
41
+ return;
42
+ }
43
+ const added = new Set();
44
+ let pending = false;
45
+ let disposed = false;
46
+ const run = () => {
47
+ pending = false;
48
+ const labelOf = latest.current;
49
+ if (disposed || labelOf === undefined) {
50
+ return;
51
+ }
52
+ const selectedLines = model.getSelectedPaths().filter(isEmptyLine);
53
+ for (const path of selectedLines) {
54
+ model.getItem(path)?.deselect();
55
+ }
56
+ const focused = skipEmptyLine(model, lastKey.current) ? model.getFocusedPath() : null;
57
+ if (focused !== null && selectedLines.length > 0) {
58
+ model.getItem(focused)?.select();
59
+ }
60
+ const {
61
+ add,
62
+ remove
63
+ } = emptyLineChanges(visibleRows(model), labelOf, path_0 => model.getItem(path_0) !== null, added);
64
+ for (const path_1 of remove) {
65
+ added.delete(path_1);
66
+ }
67
+ for (const path_2 of add) {
68
+ added.add(path_2);
69
+ }
70
+ if (add.length > 0 || remove.length > 0) {
71
+ model.batch([...remove.map(_temp), ...add.map(_temp2)]);
72
+ }
73
+ };
74
+ const schedule = () => {
75
+ if (!pending && !disposed) {
76
+ pending = true;
77
+ queueMicrotask(run);
78
+ }
79
+ };
80
+ reconcile.current = schedule;
81
+ const unsubscribe = model.subscribe(schedule);
82
+ run();
83
+ return () => {
84
+ disposed = true;
85
+ reconcile.current = undefined;
86
+ unsubscribe();
87
+ const left = [...added].filter(path_5 => model.getItem(path_5) !== null);
88
+ if (left.length > 0) {
89
+ model.batch(left.map(_temp3));
90
+ }
91
+ };
92
+ };
93
+ t3 = [model, enabled];
94
+ $[3] = enabled;
95
+ $[4] = model;
96
+ $[5] = t2;
97
+ $[6] = t3;
98
+ } else {
99
+ t2 = $[5];
100
+ t3 = $[6];
101
+ }
102
+ useEffect(t2, t3);
103
+ let t4;
104
+ let t5;
105
+ if ($[7] !== frame) {
106
+ t4 = () => {
107
+ if (frame === null) {
108
+ return;
109
+ }
110
+ const block = _temp4;
111
+ for (const type of POINTER_EVENTS) {
112
+ frame.addEventListener(type, block, true);
113
+ }
114
+ return () => {
115
+ for (const type_0 of POINTER_EVENTS) {
116
+ frame.removeEventListener(type_0, block, true);
117
+ }
118
+ };
119
+ };
120
+ t5 = [frame];
121
+ $[7] = frame;
122
+ $[8] = t4;
123
+ $[9] = t5;
124
+ } else {
125
+ t4 = $[8];
126
+ t5 = $[9];
127
+ }
128
+ useEffect(t4, t5);
129
+ let t6;
130
+ if ($[10] !== model) {
131
+ t6 = event_0 => {
132
+ lastKey.current = event_0.key;
133
+ const past = latest.current !== undefined && movesFocus(event_0) ? pastEmptyLine(model, event_0.key) : null;
134
+ if (past === null) {
135
+ return;
136
+ }
137
+ event_0.preventDefault();
138
+ event_0.stopPropagation();
139
+ if (past !== model.getFocusedPath()) {
140
+ model.scrollToPath(past, {
141
+ focus: true,
142
+ offset: "nearest"
143
+ });
144
+ }
145
+ };
146
+ $[10] = model;
147
+ $[11] = t6;
148
+ } else {
149
+ t6 = $[11];
150
+ }
151
+ return t6;
152
+ }
153
+ function _temp4(event) {
154
+ if (onEmptyLine(event)) {
155
+ event.preventDefault();
156
+ event.stopImmediatePropagation();
157
+ }
158
+ }
159
+ function _temp3(path_6) {
160
+ return {
161
+ type: "remove",
162
+ path: path_6
163
+ };
164
+ }
165
+ function _temp2(path_4) {
166
+ return {
167
+ type: "add",
168
+ path: path_4
169
+ };
170
+ }
171
+ function _temp(path_3) {
172
+ return {
173
+ type: "remove",
174
+ path: path_3
175
+ };
176
+ }
@@ -3,6 +3,7 @@ import { useFileTree as usePierreFileTree } from '@pierre/trees/react';
3
3
  import { useLayoutEffect, useRef } from 'react';
4
4
  import { FILE_TREE_ICONS } from '../file-icon.js';
5
5
  import { TREE_ROW_HEIGHT } from '../tree/style.js';
6
+ import { isEmptyLine } from './empty-lines.js';
6
7
  import { compareRows } from './helpers.js';
7
8
  import { FILE_TREE_CSS } from './style.js';
8
9
  // Model options are initial; paths and status can subsequently change through the model's mutation methods.
@@ -37,7 +38,7 @@ export function useFileTree(options) {
37
38
  }
38
39
  let t3;
39
40
  if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
40
- t3 = paths => latest.current?.(paths);
41
+ t3 = paths => latest.current?.(paths.some(isEmptyLine) ? paths.filter(_temp) : paths);
41
42
  $[4] = t3;
42
43
  } else {
43
44
  t3 = $[4];
@@ -65,4 +66,7 @@ export function useFileTree(options) {
65
66
  t5 = $[7];
66
67
  }
67
68
  return usePierreFileTree(t5);
69
+ }
70
+ function _temp(path) {
71
+ return !isEmptyLine(path);
68
72
  }
@@ -1,8 +1,11 @@
1
1
  import type { FileTree, FileTreeVisibleRow } from '@pierre/trees';
2
+ import type { LucideIcon } from 'lucide-react';
3
+ import { type FileTreeEmptyLabel } from './empty-lines.ts';
2
4
  export interface RowSlot {
3
5
  row: FileTreeVisibleRow;
4
6
  control: HTMLElement;
5
7
  decoration: HTMLElement;
8
+ icon: HTMLElement | null;
6
9
  }
7
10
  export interface Port {
8
11
  rowElement: HTMLElement;
@@ -10,6 +13,17 @@ export interface Port {
10
13
  decoration: HTMLElement;
11
14
  lightControl: HTMLElement;
12
15
  lightDecoration: HTMLElement;
16
+ icon?: HTMLElement;
17
+ lightIcon?: HTMLElement;
18
+ unhover?(): void;
13
19
  }
14
- export declare function positionPorts(ports: Iterable<Port>): void;
15
- export declare function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, label: string): RowSlot[];
20
+ export type FileTreeRowIcon = (row: FileTreeVisibleRow) => LucideIcon | undefined;
21
+ export type FileTreeDecorationOrder = 'before-status' | 'after-status';
22
+ export declare function positionPorts(ports: Iterable<Port>, order?: FileTreeDecorationOrder): void;
23
+ export interface FileTreeSlotOptions {
24
+ order: FileTreeDecorationOrder;
25
+ rowIcon?: FileTreeRowIcon;
26
+ highlight: string;
27
+ emptyLabel?: FileTreeEmptyLabel;
28
+ }
29
+ export declare function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, label: string, { order, rowIcon, highlight, emptyLabel }: FileTreeSlotOptions): RowSlot[];
@@ -1,6 +1,8 @@
1
- import { useEffect, useId, useState } from 'react';
1
+ import { useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
2
2
  import { flushSync } from 'react-dom';
3
+ import { directoryOfEmptyLine, isEmptyLine } from './empty-lines.js';
3
4
  import { pathOfRow, visibleRows, visibleRowsAround } from './helpers.js';
5
+ import { matchHighlight, rangesOfMatches } from './highlight.js';
4
6
  import { FILE_TREE_CSS } from './style.js';
5
7
  /*
6
8
  * The rows of the mounted window, read from the spacer beta.6 puts above it (its first index times the
@@ -24,38 +26,46 @@ function mountedRows(model, shadow, elements) {
24
26
  * Lines the ports up with their rows: every size first, then the spaces they reserve, then every offset,
25
27
  * then the ports, so a pass forces two layouts at most however many rows are mounted.
26
28
  */
27
- export function positionPorts(ports) {
29
+ export function positionPorts(ports, order = 'before-status') {
28
30
  const spaced = [...ports].map(port => ({
29
31
  port,
32
+ // Without a git column, after the column is right after the name.
33
+ status: order === 'after-status' ? port.rowElement.querySelector('[data-item-section="git"]') : null,
30
34
  control: port.lightControl.childNodes.length > 0 ? Math.ceil(port.lightControl.getBoundingClientRect().width) + 6 : 0,
31
35
  decoration: port.lightDecoration.childNodes.length > 0 ? Math.ceil(port.lightDecoration.getBoundingClientRect().width) + 6 : 0
32
36
  }));
33
37
  for (const {
34
38
  port,
39
+ status,
35
40
  control,
36
41
  decoration
37
42
  } of spaced) {
38
43
  setSpace(port.rowElement, '--adecore-tree-control-space', control);
39
- setSpace(port.rowElement, '--adecore-tree-decoration-space', decoration);
44
+ setSpace(port.rowElement, '--adecore-tree-decoration-space', status === null ? decoration : 0);
45
+ setSpace(port.rowElement, '--adecore-tree-status-space', status === null ? 0 : decoration);
40
46
  }
41
47
  const placed = spaced.map(({
42
48
  port,
49
+ status,
43
50
  control,
44
51
  decoration
45
52
  }) => {
46
53
  const row = port.rowElement;
47
- const content = row.querySelector('[data-item-section="content"]');
54
+ const before = status ?? row.querySelector('[data-item-section="content"]');
48
55
  const icon = row.querySelector('[data-item-section="icon"]');
56
+ const iconLeft = row.offsetLeft + (icon?.offsetLeft ?? 4);
49
57
  return {
50
58
  port,
51
59
  top: `${row.offsetTop}px`,
52
- controlLeft: `${row.offsetLeft + (icon?.offsetLeft ?? 4) - control}px`,
53
- decorationLeft: `${row.offsetLeft + (content === null ? row.offsetWidth - decoration - 2 : content.offsetLeft + content.offsetWidth + 6)}px`
60
+ iconLeft: `${iconLeft}px`,
61
+ controlLeft: `${iconLeft - control}px`,
62
+ decorationLeft: `${row.offsetLeft + (before === null ? row.offsetWidth - decoration - 2 : before.offsetLeft + before.offsetWidth + 6)}px`
54
63
  };
55
64
  });
56
65
  for (const {
57
66
  port,
58
67
  top,
68
+ iconLeft,
59
69
  controlLeft,
60
70
  decorationLeft
61
71
  } of placed) {
@@ -63,6 +73,10 @@ export function positionPorts(ports) {
63
73
  port.control.style.left = controlLeft;
64
74
  port.decoration.style.top = top;
65
75
  port.decoration.style.left = decorationLeft;
76
+ if (port.icon !== undefined) {
77
+ port.icon.style.top = top;
78
+ port.icon.style.left = iconLeft;
79
+ }
66
80
  }
67
81
  }
68
82
  // An unchanged space is left alone, so a pass that only scrolled measures once.
@@ -72,10 +86,47 @@ function setSpace(row, name, space) {
72
86
  row.style.setProperty(name, value);
73
87
  }
74
88
  }
89
+ /* An empty line draws its directory's label in place of its name and is no item to point at or read out. */
90
+ function markEmptyLine(element, label) {
91
+ element.dataset.emptyLine = 'true';
92
+ element.dataset.interactive = 'false';
93
+ element.setAttribute('aria-hidden', 'true');
94
+ const content = element.querySelector('[data-item-section="content"]');
95
+ if (content !== null && content.dataset.emptyLabel !== label) {
96
+ content.dataset.emptyLabel = label;
97
+ }
98
+ }
99
+ // The engine recycles row elements, so one that drew a line can come back as an ordinary row.
100
+ function unmarkEmptyLine(element) {
101
+ if (element.dataset.emptyLine === undefined) {
102
+ return;
103
+ }
104
+ delete element.dataset.emptyLine;
105
+ element.removeAttribute('aria-hidden');
106
+ const content = element.querySelector('[data-item-section="content"]');
107
+ if (content !== null) {
108
+ delete content.dataset.emptyLabel;
109
+ }
110
+ }
75
111
  // beta.6 owns button rows. Sibling slots keep real controls outside those buttons and in the app's stylesheet.
76
- export function useFileTreeSlots(model, frame, label) {
112
+ export function useFileTreeSlots(model, frame, label, {
113
+ order,
114
+ rowIcon,
115
+ highlight,
116
+ emptyLabel
117
+ }) {
77
118
  const prefix = useId().replaceAll(':', '');
78
119
  const [slots, setSlots] = useState([]);
120
+ const latestIcon = useRef(rowIcon);
121
+ const latestHighlight = useRef(highlight);
122
+ const latestEmpty = useRef(emptyLabel);
123
+ const rescan = useRef(undefined);
124
+ useLayoutEffect(() => {
125
+ latestIcon.current = rowIcon;
126
+ latestHighlight.current = highlight;
127
+ latestEmpty.current = emptyLabel;
128
+ rescan.current?.();
129
+ }, [rowIcon, highlight, emptyLabel]);
79
130
  useEffect(() => {
80
131
  if (frame === null) {
81
132
  return;
@@ -87,7 +138,21 @@ export function useFileTreeSlots(model, frame, label) {
87
138
  let disposed = false;
88
139
  let pending = false;
89
140
  let snapshot = null;
90
- const position = () => positionPorts(ports.values());
141
+ let marked = [];
142
+ const mark = (rows, query) => {
143
+ const matches = matchHighlight();
144
+ if (matches === null) {
145
+ return;
146
+ }
147
+ for (const range of marked) {
148
+ matches.delete(range);
149
+ }
150
+ marked = rangesOfMatches(rows, query);
151
+ for (const range_0 of marked) {
152
+ matches.add(range_0);
153
+ }
154
+ };
155
+ const position = () => positionPorts(ports.values(), order);
91
156
  const resize = new ResizeObserver(position);
92
157
  const dropPort = port => {
93
158
  port.control.remove();
@@ -96,27 +161,74 @@ export function useFileTreeSlots(model, frame, label) {
96
161
  resize.unobserve(port.lightDecoration);
97
162
  port.lightControl.remove();
98
163
  port.lightDecoration.remove();
164
+ dropIcon(port);
165
+ };
166
+ const clearPort = port_0 => {
167
+ port_0.rowElement.style.removeProperty('--adecore-tree-control-space');
168
+ port_0.rowElement.style.removeProperty('--adecore-tree-decoration-space');
169
+ port_0.rowElement.style.removeProperty('--adecore-tree-status-space');
170
+ delete port_0.rowElement.dataset.ownIcon;
171
+ dropPort(port_0);
172
+ };
173
+ const dropIcon = port_1 => {
174
+ port_1.unhover?.();
175
+ port_1.icon?.remove();
176
+ port_1.lightIcon?.remove();
177
+ port_1.icon = undefined;
178
+ port_1.lightIcon = undefined;
179
+ port_1.unhover = undefined;
99
180
  };
100
- const makePort = (element, host, kind) => {
181
+ // The icon lets every pointer event through to the row under it, which then opens, folds or selects as it would.
182
+ const makeIcon = (port_2, host) => {
101
183
  const container = document.createElement('span');
102
- container.dataset.treePort = kind;
184
+ container.dataset.treePort = 'icon';
185
+ container.dataset.itemPath = port_2.rowElement.dataset.itemPath;
103
186
  const slot = document.createElement('slot');
104
- slot.name = `${prefix}-${kind}-${serial}`;
187
+ slot.name = `${prefix}-icon-${serial++}`;
105
188
  container.append(slot);
106
- element.parentElement?.append(container);
189
+ port_2.rowElement.parentElement?.append(container);
107
190
  const target = document.createElement('span');
108
191
  target.slot = slot.name;
109
- target.dataset.treeSlot = kind;
110
- target.className = kind === 'control' ? 'adecore-tree-control' : 'adecore-tree-decoration';
192
+ target.className = 'adecore-tree-icon';
193
+ host.append(target);
194
+ // A folder under the pointer gives its chevron back, so the row still says it opens.
195
+ const show = () => {
196
+ if (port_2.rowElement.dataset.itemType === 'folder') {
197
+ container.dataset.hover = 'true';
198
+ }
199
+ };
200
+ const hide = () => {
201
+ delete container.dataset.hover;
202
+ };
203
+ port_2.rowElement.addEventListener('pointerenter', show);
204
+ port_2.rowElement.addEventListener('pointerleave', hide);
205
+ port_2.icon = container;
206
+ port_2.lightIcon = target;
207
+ port_2.unhover = () => {
208
+ port_2.rowElement.removeEventListener('pointerenter', show);
209
+ port_2.rowElement.removeEventListener('pointerleave', hide);
210
+ };
211
+ };
212
+ const makePort = (element, host_0, kind) => {
213
+ const container_0 = document.createElement('span');
214
+ container_0.dataset.treePort = kind;
215
+ const slot_0 = document.createElement('slot');
216
+ slot_0.name = `${prefix}-${kind}-${serial}`;
217
+ container_0.append(slot_0);
218
+ element.parentElement?.append(container_0);
219
+ const target_0 = document.createElement('span');
220
+ target_0.slot = slot_0.name;
221
+ target_0.dataset.treeSlot = kind;
222
+ target_0.className = kind === 'control' ? 'adecore-tree-control' : 'adecore-tree-decoration';
111
223
  // React delegates to this portal target before the event reaches the engine's shadow tree.
112
224
  for (const type of ['click', 'dblclick', 'keydown', 'keyup', 'pointerdown', 'dragstart']) {
113
- target.addEventListener(type, event => event.stopPropagation());
225
+ target_0.addEventListener(type, event => event.stopPropagation());
114
226
  }
115
- host.append(target);
116
- resize.observe(target);
227
+ host_0.append(target_0);
228
+ resize.observe(target_0);
117
229
  return {
118
- container,
119
- target
230
+ container: container_0,
231
+ target: target_0
120
232
  };
121
233
  };
122
234
  const scan = () => {
@@ -148,63 +260,93 @@ export function useFileTreeSlots(model, frame, label) {
148
260
  }
149
261
  shadow.querySelector('[role="tree"]')?.setAttribute('aria-label', label);
150
262
  const elements = [...shadow.querySelectorAll('[data-type="item"]:not([data-item-parked="true"]):not([data-file-tree-sticky-row="true"])')];
151
- const rows = mountedRows(model, shadow, elements);
152
- const byPath = new Map(rows.map(row => [pathOfRow(row), row]));
153
- const byIndex = new Map(rows.map(row_0 => [row_0.index, row_0]));
263
+ const rows_0 = mountedRows(model, shadow, elements);
264
+ const byPath = new Map(rows_0.map(row => [pathOfRow(row), row]));
265
+ const byIndex = new Map(rows_0.map(row_0 => [row_0.index, row_0]));
154
266
  const live = new Set(elements);
155
- for (const [element_0, port_0] of ports) {
267
+ for (const [element_0, port_3] of ports) {
156
268
  if (!live.has(element_0)) {
157
- dropPort(port_0);
269
+ dropPort(port_3);
158
270
  ports.delete(element_0);
159
271
  }
160
272
  }
161
273
  const next = [];
274
+ const named = [];
162
275
  for (const element_1 of elements) {
163
276
  const path = element_1.dataset.itemPath;
164
277
  const row_1 = path === undefined ? undefined : byPath.get(path);
165
278
  if (row_1 === undefined) {
166
279
  continue;
167
280
  }
281
+ if (isEmptyLine(row_1.path)) {
282
+ const recycled = ports.get(element_1);
283
+ if (recycled !== undefined) {
284
+ clearPort(recycled);
285
+ ports.delete(element_1);
286
+ }
287
+ const directory = byPath.get(directoryOfEmptyLine(row_1.path));
288
+ markEmptyLine(element_1, (directory === undefined ? undefined : latestEmpty.current?.(directory)) ?? '');
289
+ continue;
290
+ }
291
+ unmarkEmptyLine(element_1);
292
+ named.push(element_1);
168
293
  element_1.dataset.interactive = 'true';
169
294
  element_1.dataset.joinedStart = String(row_1.isSelected && byIndex.get(row_1.index - 1)?.isSelected === true);
170
295
  element_1.dataset.joinedEnd = String(row_1.isSelected && byIndex.get(row_1.index + 1)?.isSelected === true);
171
- let port_1 = ports.get(element_1);
172
- if (port_1 === undefined) {
296
+ let port_4 = ports.get(element_1);
297
+ if (port_4 === undefined) {
173
298
  serial++;
174
299
  const control = makePort(element_1, current, 'control');
175
300
  const decoration = makePort(element_1, current, 'decoration');
176
- port_1 = {
301
+ port_4 = {
177
302
  rowElement: element_1,
178
303
  control: control.container,
179
304
  decoration: decoration.container,
180
305
  lightControl: control.target,
181
306
  lightDecoration: decoration.target
182
307
  };
183
- ports.set(element_1, port_1);
308
+ ports.set(element_1, port_4);
309
+ }
310
+ if (port_4.control.dataset.itemPath !== row_1.path) {
311
+ port_4.control.dataset.itemPath = row_1.path;
312
+ }
313
+ if (port_4.decoration.dataset.itemPath !== row_1.path) {
314
+ port_4.decoration.dataset.itemPath = row_1.path;
315
+ }
316
+ port_4.lightControl.dataset.itemPath = row_1.path;
317
+ port_4.lightDecoration.dataset.itemPath = row_1.path;
318
+ const ownIcon = latestIcon.current?.(row_1) !== undefined;
319
+ if (ownIcon && port_4.icon === undefined) {
320
+ makeIcon(port_4, current);
321
+ } else if (!ownIcon && port_4.icon !== undefined) {
322
+ dropIcon(port_4);
184
323
  }
185
- if (port_1.control.dataset.itemPath !== row_1.path) {
186
- port_1.control.dataset.itemPath = row_1.path;
324
+ if (port_4.icon !== undefined && port_4.icon.dataset.itemPath !== row_1.path) {
325
+ port_4.icon.dataset.itemPath = row_1.path;
187
326
  }
188
- if (port_1.decoration.dataset.itemPath !== row_1.path) {
189
- port_1.decoration.dataset.itemPath = row_1.path;
327
+ if (ownIcon) {
328
+ element_1.dataset.ownIcon = 'true';
329
+ } else if (element_1.dataset.ownIcon !== undefined) {
330
+ delete element_1.dataset.ownIcon;
190
331
  }
191
- port_1.lightControl.dataset.itemPath = row_1.path;
192
- port_1.lightDecoration.dataset.itemPath = row_1.path;
193
332
  next.push({
194
333
  row: row_1,
195
- control: port_1.lightControl,
196
- decoration: port_1.lightDecoration
334
+ control: port_4.lightControl,
335
+ decoration: port_4.lightDecoration,
336
+ icon: port_4.lightIcon ?? null
197
337
  });
198
338
  }
199
339
  const key = next.map(({
200
340
  row: row_2,
201
- control: control_0
202
- }) => `${control_0.slot}:${row_2.path}:${row_2.isSelected}:${row_2.isExpanded}:${row_2.isFocused}:${row_2.name}`).join('|');
341
+ control: control_0,
342
+ icon
343
+ }) => `${control_0.slot}:${row_2.path}:${row_2.isSelected}:${row_2.isExpanded}:${row_2.isFocused}:${row_2.name}:${icon?.slot ?? ''}`).join('|');
203
344
  if (snapshot !== key) {
204
345
  snapshot = key;
205
346
  // Recycled rows must paint with their new portal contents in the same frame.
206
347
  flushSync(() => setSlots(next));
207
348
  }
349
+ mark(named, latestHighlight.current);
208
350
  position();
209
351
  };
210
352
  const schedule = () => {
@@ -213,6 +355,7 @@ export function useFileTreeSlots(model, frame, label) {
213
355
  queueMicrotask(scan);
214
356
  }
215
357
  };
358
+ rescan.current = schedule;
216
359
  const shifts = new MutationObserver(position);
217
360
  shifts.observe(frame, {
218
361
  attributes: true,
@@ -227,17 +370,17 @@ export function useFileTreeSlots(model, frame, label) {
227
370
  schedule();
228
371
  return () => {
229
372
  disposed = true;
373
+ rescan.current = undefined;
230
374
  unsubscribe();
231
375
  changes.disconnect();
232
376
  shifts.disconnect();
233
377
  resize.disconnect();
234
378
  style?.remove();
235
- for (const port_2 of ports.values()) {
236
- port_2.rowElement.style.removeProperty('--adecore-tree-control-space');
237
- port_2.rowElement.style.removeProperty('--adecore-tree-decoration-space');
238
- dropPort(port_2);
379
+ mark([], '');
380
+ for (const port_5 of ports.values()) {
381
+ clearPort(port_5);
239
382
  }
240
383
  };
241
- }, [frame, model, label, prefix]);
384
+ }, [frame, model, label, order, prefix]);
242
385
  return slots;
243
386
  }
package/dist/index.d.ts CHANGED
@@ -57,6 +57,7 @@ export { Skeleton, type SkeletonProps } from './Skeleton.tsx';
57
57
  export { Slider, type SliderProps } from './Slider.tsx';
58
58
  export { SlidingColumn, type SlidingColumnProps } from './SlidingColumn.tsx';
59
59
  export { Spinner, type SpinnerProps } from './Spinner.tsx';
60
+ export { SplitButton, type SplitButtonProps } from './SplitButton.tsx';
60
61
  export { Stepper, type StepperProps } from './Stepper.tsx';
61
62
  export { Surface, type SurfaceProps } from './Surface.tsx';
62
63
  export { Switch, type SwitchProps } from './Switch.tsx';
package/dist/index.js CHANGED
@@ -54,6 +54,7 @@ export { Skeleton } from "./Skeleton.js";
54
54
  export { Slider } from "./Slider.js";
55
55
  export { SlidingColumn } from "./SlidingColumn.js";
56
56
  export { Spinner } from "./Spinner.js";
57
+ export { SplitButton } from "./SplitButton.js";
57
58
  export { Stepper } from "./Stepper.js";
58
59
  export { Surface } from "./Surface.js";
59
60
  export { Switch } from "./Switch.js";
@@ -49,6 +49,7 @@ declare const _default: {
49
49
  iconPicker: {
50
50
  label: string;
51
51
  none: string;
52
+ placeholder: string;
52
53
  search: string;
53
54
  clear: string;
54
55
  noMatch: string;
@@ -49,6 +49,7 @@ export default {
49
49
  iconPicker: {
50
50
  label: 'Icon',
51
51
  none: 'No icon',
52
+ placeholder: 'None',
52
53
  search: 'Search icons',
53
54
  clear: 'Clear search',
54
55
  noMatch: 'No icon matches “{{query}}”'
@@ -49,6 +49,7 @@ declare const _default: {
49
49
  iconPicker: {
50
50
  label: string;
51
51
  none: string;
52
+ placeholder: string;
52
53
  search: string;
53
54
  clear: string;
54
55
  noMatch: string;
@@ -49,6 +49,7 @@ export default {
49
49
  iconPicker: {
50
50
  label: 'Icoon',
51
51
  none: 'Geen icoon',
52
+ placeholder: 'Geen',
52
53
  search: 'Iconen zoeken',
53
54
  clear: 'Zoekopdracht wissen',
54
55
  noMatch: 'Geen icoon past bij “{{query}}”'
@@ -16,7 +16,7 @@ export declare const TRIGGER_SIZE: {
16
16
  readonly md: 'h-8 gap-1.5 px-2';
17
17
  };
18
18
  export declare const TRIGGER_VARIANT: {
19
- readonly outlined: 'max-w-56 rounded-lg border border-border bg-surface-raised text-text';
19
+ readonly outlined: 'max-w-56 rounded-lg border border-field-border bg-field text-text hover:bg-surface-hover data-[popup-open]:bg-surface-hover data-disabled:hover:bg-field';
20
20
  readonly ghost: 'rounded-md text-text-muted hover:bg-surface-hover hover:text-text data-[popup-open]:bg-surface-active data-[popup-open]:text-text data-disabled:hover:bg-transparent data-disabled:hover:text-text-muted';
21
21
  };
22
22
  export declare const TRIGGER_DISABLED = "data-disabled:cursor-default data-disabled:opacity-50";