@adecore/ui 0.35.5 → 0.37.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/dist/Button.d.ts +2 -3
- package/dist/Button.js +21 -46
- package/dist/Checkbox.js +1 -1
- package/dist/ChoiceCards.js +2 -2
- package/dist/IconPicker.d.ts +2 -1
- package/dist/IconPicker.js +504 -217
- package/dist/Input.d.ts +5 -3
- package/dist/Input.js +127 -112
- package/dist/Segmented.js +1 -1
- package/dist/SplitButton.d.ts +17 -0
- package/dist/SplitButton.js +237 -0
- package/dist/Stepper.js +1 -1
- package/dist/Switch.js +1 -1
- package/dist/Tile.js +2 -2
- package/dist/button-class.d.ts +6 -0
- package/dist/button-class.js +92 -0
- package/dist/command-palette/context.d.ts +10 -0
- package/dist/command-palette/context.js +5 -0
- package/dist/command-palette/index.parts.d.ts +1 -0
- package/dist/command-palette/index.parts.js +2 -1
- package/dist/command-palette/parts.d.ts +1 -1
- package/dist/command-palette/parts.js +12 -3
- package/dist/command-palette/virtual.d.ts +20 -0
- package/dist/command-palette/virtual.js +155 -0
- package/dist/file-tree/empty-lines.d.ts +11 -0
- package/dist/file-tree/empty-lines.js +89 -0
- package/dist/file-tree/highlight.d.ts +4 -0
- package/dist/file-tree/highlight.js +75 -0
- package/dist/file-tree/parts.d.ts +9 -1
- package/dist/file-tree/parts.js +297 -205
- package/dist/file-tree/style.js +13 -0
- package/dist/file-tree/use-empty-lines.d.ts +3 -0
- package/dist/file-tree/use-empty-lines.js +176 -0
- package/dist/file-tree/use-file-tree.js +5 -1
- package/dist/file-tree/use-slots.d.ts +16 -2
- package/dist/file-tree/use-slots.js +186 -43
- package/dist/format/index.d.ts +1 -1
- package/dist/format/index.js +1 -1
- package/dist/format/locale.d.ts +1 -0
- package/dist/format/locale.js +4 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/locales/en.d.ts +1 -0
- package/dist/locales/en.js +1 -0
- package/dist/locales/nl.d.ts +1 -0
- package/dist/locales/nl.js +1 -0
- package/dist/select-shared.d.ts +1 -1
- package/dist/select-shared.js +1 -1
- package/dist/settings/SettingsDialog.js +1 -1
- package/dist/theme.css +33 -3
- package/package.json +2 -1
- package/src/Button.tsx +3 -36
- package/src/Checkbox.tsx +1 -1
- package/src/ChoiceCards.tsx +2 -2
- package/src/IconPicker.tsx +220 -136
- package/src/Input.tsx +19 -10
- package/src/Segmented.tsx +1 -1
- package/src/SplitButton.tsx +72 -0
- package/src/Stepper.tsx +6 -1
- package/src/Switch.tsx +1 -1
- package/src/Tile.tsx +2 -2
- package/src/button-class.ts +111 -0
- package/src/command-palette/context.ts +16 -0
- package/src/command-palette/index.parts.ts +2 -0
- package/src/command-palette/parts.tsx +11 -12
- package/src/command-palette/virtual.tsx +147 -0
- package/src/file-tree/empty-lines.ts +105 -0
- package/src/file-tree/highlight.ts +80 -0
- package/src/file-tree/parts.tsx +42 -11
- package/src/file-tree/style.ts +13 -0
- package/src/file-tree/use-empty-lines.ts +115 -0
- package/src/file-tree/use-file-tree.ts +2 -1
- package/src/file-tree/use-slots.ts +176 -17
- package/src/format/index.ts +1 -1
- package/src/format/locale.ts +13 -7
- package/src/index.ts +3 -0
- package/src/locales/en.ts +1 -0
- package/src/locales/nl.ts +1 -0
- package/src/select-shared.ts +2 -1
- package/src/settings/SettingsDialog.tsx +1 -1
- package/src/theme.css +33 -3
|
@@ -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
|
|
15
|
-
export
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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}-${
|
|
187
|
+
slot.name = `${prefix}-icon-${serial++}`;
|
|
105
188
|
container.append(slot);
|
|
106
|
-
|
|
189
|
+
port_2.rowElement.parentElement?.append(container);
|
|
107
190
|
const target = document.createElement('span');
|
|
108
191
|
target.slot = slot.name;
|
|
109
|
-
target.
|
|
110
|
-
target
|
|
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
|
-
|
|
225
|
+
target_0.addEventListener(type, event => event.stopPropagation());
|
|
114
226
|
}
|
|
115
|
-
|
|
116
|
-
resize.observe(
|
|
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
|
|
152
|
-
const byPath = new Map(
|
|
153
|
-
const byIndex = new Map(
|
|
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,
|
|
267
|
+
for (const [element_0, port_3] of ports) {
|
|
156
268
|
if (!live.has(element_0)) {
|
|
157
|
-
dropPort(
|
|
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
|
|
172
|
-
if (
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
186
|
-
|
|
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 (
|
|
189
|
-
|
|
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:
|
|
196
|
-
decoration:
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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/format/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { formatAge, formatClock, formatDateTime, formatDay, formatDayClock, formatDayClockWithYear, formatDayWithYear, formatHour, formatMoment, formatNumericDate, formatWeekday, formatWeekdayClock, formatWeekdayDay, isSameDay } from './datetime.ts';
|
|
2
2
|
export { formatAgo, formatClockDuration, formatCountdown, formatDuration, formatElapsedShort, formatPreciseDuration } from './duration.ts';
|
|
3
|
-
export { formatLocale, labelCollator, setFormatSource, systemLocale, useFormatLocale, type FormatSource } from './locale.ts';
|
|
3
|
+
export { formatLocale, labelCollator, nameCollator, setFormatSource, systemLocale, useFormatLocale, type FormatSource } from './locale.ts';
|
|
4
4
|
export { formatBytes, formatDecimal, formatMoney, formatNumber, formatNumeral, formatPercent, formatRounded, formatTokens, formatUsdSignificant } from './number.ts';
|
|
5
5
|
export { FORMAT_LANGUAGE, FORMAT_REGIONS, FORMAT_REGION_CHOICES, FORMAT_SYSTEM, formatRegionFrom, regionName } from './regions.ts';
|
|
6
6
|
export { localTimeZone } from './time-zone.ts';
|
package/dist/format/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Every number, date, time and duration a person reads, written the way the language and the region they set ask for. */
|
|
2
2
|
export { formatAge, formatClock, formatDateTime, formatDay, formatDayClock, formatDayClockWithYear, formatDayWithYear, formatHour, formatMoment, formatNumericDate, formatWeekday, formatWeekdayClock, formatWeekdayDay, isSameDay } from './datetime.js';
|
|
3
3
|
export { formatAgo, formatClockDuration, formatCountdown, formatDuration, formatElapsedShort, formatPreciseDuration } from './duration.js';
|
|
4
|
-
export { formatLocale, labelCollator, setFormatSource, systemLocale, useFormatLocale } from './locale.js';
|
|
4
|
+
export { formatLocale, labelCollator, nameCollator, setFormatSource, systemLocale, useFormatLocale } from './locale.js';
|
|
5
5
|
export { formatBytes, formatDecimal, formatMoney, formatNumber, formatNumeral, formatPercent, formatRounded, formatTokens, formatUsdSignificant } from './number.js';
|
|
6
6
|
export { FORMAT_LANGUAGE, FORMAT_REGIONS, FORMAT_REGION_CHOICES, FORMAT_SYSTEM, formatRegionFrom, regionName } from './regions.js';
|
|
7
7
|
export { localTimeZone } from './time-zone.js';
|
package/dist/format/locale.d.ts
CHANGED
|
@@ -14,5 +14,6 @@ export declare const numberFormatter: (options: Intl.NumberFormatOptions) => Int
|
|
|
14
14
|
export declare const dateFormatter: (options: Intl.DateTimeFormatOptions) => Intl.DateTimeFormat;
|
|
15
15
|
export declare const wordFormatter: (options: Intl.DateTimeFormatOptions) => Intl.DateTimeFormat;
|
|
16
16
|
export declare const relativeFormatter: (options: Intl.RelativeTimeFormatOptions) => Intl.RelativeTimeFormat;
|
|
17
|
+
export declare const nameCollator: (options: Intl.CollatorOptions) => Intl.Collator;
|
|
17
18
|
export declare const labelCollator: () => Intl.Collator;
|
|
18
19
|
export declare const nameOrder: (left: string, right: string) => number;
|
package/dist/format/locale.js
CHANGED
|
@@ -75,11 +75,7 @@ const subscribe = onChange => source.subscribe(onChange);
|
|
|
75
75
|
export const useFormatLocale = () => {
|
|
76
76
|
return useSyncExternalStore(subscribe, formatLocale, formatLocale);
|
|
77
77
|
};
|
|
78
|
-
/*
|
|
79
|
-
* A formatter per options object, keyed on the object itself, so a module-level spec builds its
|
|
80
|
-
* formatter once and a list of a thousand rows constructs nothing. A region change throws the entry
|
|
81
|
-
* away, since the locale is part of what was cached.
|
|
82
|
-
*/
|
|
78
|
+
/* A module-level options object shares its formatter across rows; a changed locale replaces it. */
|
|
83
79
|
const cacheFor = (store, options, locale, build) => {
|
|
84
80
|
const hit = store.get(options);
|
|
85
81
|
if (hit !== undefined && hit.locale === locale) {
|
|
@@ -96,6 +92,7 @@ const numbers = new WeakMap();
|
|
|
96
92
|
const dates = new WeakMap();
|
|
97
93
|
const words = new WeakMap();
|
|
98
94
|
const relatives = new WeakMap();
|
|
95
|
+
const names = new WeakMap();
|
|
99
96
|
export const numberFormatter = options => cacheFor(numbers, options, formatLocale(), (locale, spec) => new Intl.NumberFormat(locale, spec));
|
|
100
97
|
export const dateFormatter = options => cacheFor(dates, options, formatLocale(), (locale, spec) => new Intl.DateTimeFormat(locale, spec));
|
|
101
98
|
/* The same date, written in the language the words come from. Never shown whole: only its month,
|
|
@@ -103,6 +100,8 @@ export const dateFormatter = options => cacheFor(dates, options, formatLocale(),
|
|
|
103
100
|
export const wordFormatter = options => cacheFor(words, options, wordLocale(), (locale, spec) => new Intl.DateTimeFormat(locale, spec));
|
|
104
101
|
/* `3 min. geleden` is words as much as a number, so the language writes it, the way it writes a month. */
|
|
105
102
|
export const relativeFormatter = options => cacheFor(relatives, options, wordLocale(), (locale, spec) => new Intl.RelativeTimeFormat(locale, spec));
|
|
103
|
+
/* Proper names keep the runtime locale; a shared options object reuses its collator. */
|
|
104
|
+
export const nameCollator = options => cacheFor(names, options, undefined, (locale, spec) => new Intl.Collator(locale, spec));
|
|
106
105
|
const collators = new Map();
|
|
107
106
|
/*
|
|
108
107
|
* How two labels a person reads are put in order. Labels are words, so the language writes them and
|