@adecore/ui 0.16.0 → 0.17.0-beta.2
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/LICENSE +97 -8
- package/README.md +1 -1
- package/dist/Select.js +2 -1
- package/dist/file-tree/helpers.d.ts +42 -0
- package/dist/file-tree/helpers.js +167 -0
- package/dist/file-tree/index.parts.d.ts +3 -0
- package/dist/file-tree/index.parts.js +3 -0
- package/dist/file-tree/parts.d.ts +20 -0
- package/dist/file-tree/parts.js +150 -0
- package/dist/file-tree/shift.d.ts +15 -0
- package/dist/file-tree/shift.js +34 -0
- package/dist/file-tree/style.d.ts +1 -0
- package/dist/file-tree/style.js +24 -0
- package/dist/file-tree/use-file-tree-shift.d.ts +6 -0
- package/dist/file-tree/use-file-tree-shift.js +142 -0
- package/dist/file-tree/use-file-tree.d.ts +5 -0
- package/dist/file-tree/use-file-tree.js +26 -0
- package/dist/file-tree/use-slots.d.ts +7 -0
- package/dist/file-tree/use-slots.js +171 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +3 -0
- package/dist/locales/en.json +5 -0
- package/dist/locales/nl.json +5 -0
- package/dist/menu/parts.js +2 -1
- package/dist/popover/parts.js +2 -1
- package/dist/popup-layer.d.ts +6 -0
- package/dist/popup-layer.js +30 -0
- package/dist/theme.css +104 -1
- package/dist/tree/index.parts.d.ts +1 -0
- package/dist/tree/index.parts.js +1 -0
- package/dist/tree/parts.d.ts +27 -0
- package/dist/tree/parts.js +113 -0
- package/dist/tree/style.d.ts +2 -0
- package/dist/tree/style.js +29 -0
- package/package.json +6 -4
- package/src/Select.tsx +9 -1
- package/src/file-tree/helpers.ts +203 -0
- package/src/file-tree/index.parts.ts +25 -0
- package/src/file-tree/parts.tsx +206 -0
- package/src/file-tree/shift.ts +48 -0
- package/src/file-tree/style.ts +25 -0
- package/src/file-tree/use-file-tree-shift.tsx +169 -0
- package/src/file-tree/use-file-tree.ts +31 -0
- package/src/file-tree/use-slots.ts +191 -0
- package/src/index.ts +17 -0
- package/src/locales/en.json +5 -0
- package/src/locales/nl.json +5 -0
- package/src/menu/parts.tsx +2 -0
- package/src/popover/parts.tsx +2 -0
- package/src/popup-layer.ts +34 -0
- package/src/theme.css +104 -1
- package/src/tree/index.parts.ts +10 -0
- package/src/tree/parts.tsx +170 -0
- package/src/tree/style.ts +30 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { treeRowStyle } from '../tree/style.ts';
|
|
2
|
+
|
|
3
|
+
// beta.6 splits the extension into nested grids; flatten them to truncate the end of the entire name.
|
|
4
|
+
export const FILE_TREE_CSS = `
|
|
5
|
+
${treeRowStyle('[data-type="item"]', 'data-item-selected')}
|
|
6
|
+
[data-type="item"] { overflow: clip; padding-inline: 4px 8px; --trees-item-row-gap: var(--tree-row-gap, 6px); }
|
|
7
|
+
[data-type="item"]::after {
|
|
8
|
+
content: "";
|
|
9
|
+
flex: none;
|
|
10
|
+
order: -100;
|
|
11
|
+
margin-inline: calc(var(--adecore-tree-shift, 0px) * -1) calc(var(--trees-item-row-gap) * -1);
|
|
12
|
+
}
|
|
13
|
+
[data-icon-name="file-tree-icon-chevron"] { width: 12px; height: 12px; }
|
|
14
|
+
[data-item-section="spacing"] { padding: 0; margin-inline-end: -6px; }
|
|
15
|
+
[data-item-section="spacing-item"] { position: relative; flex: none; width: 16px; margin: 0; border: 0; background: transparent; opacity: 1; transform: none; transition: none; }
|
|
16
|
+
[data-item-section="spacing-item"] + [data-item-section="spacing-item"] { margin: 0; }
|
|
17
|
+
[data-item-section="spacing-item"]::before { content: ""; position: absolute; inset-block: 0; inset-inline-start: 7px; width: 1px; background: var(--border); }
|
|
18
|
+
[data-item-section="icon"] { width: 16px; margin-inline-start: var(--adecore-tree-control-space, 0px); }
|
|
19
|
+
[data-item-section="content"] { white-space: nowrap; max-width: none; flex: 1 1 auto; margin-inline-end: var(--adecore-tree-decoration-space, 0px); }
|
|
20
|
+
[data-item-section="content"] :where([data-truncate-group-container], [data-truncate-group-container] div, [data-truncate-container], [data-truncate-container] div) { display: inline; }
|
|
21
|
+
[data-item-section="content"] [data-truncate-content] { direction: ltr; }
|
|
22
|
+
[data-item-section="content"] :where([data-truncate-content="overflow"], [data-truncate-marker-cell], [data-truncate-fill]) { display: none; }
|
|
23
|
+
[data-tree-port] { position: absolute; z-index: 4; display: flex; align-items: center; height: 25px; pointer-events: none; }
|
|
24
|
+
[data-tree-port] > slot { pointer-events: auto; }
|
|
25
|
+
`;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactElement } from 'react';
|
|
2
|
+
import type { FileTree } from '@pierre/trees';
|
|
3
|
+
import { clampShift, maxShift, SHIFT_PROPERTY, shiftNeed, shiftThumb, sidewaysDelta } from './shift.ts';
|
|
4
|
+
|
|
5
|
+
const ROWS = '[data-type="item"]:not([data-item-parked="true"])';
|
|
6
|
+
|
|
7
|
+
const HIDE_AFTER = 900;
|
|
8
|
+
|
|
9
|
+
const NEAR_BOTTOM = 16;
|
|
10
|
+
|
|
11
|
+
function boxesOf(element: Element): Element[] {
|
|
12
|
+
return element.getClientRects().length > 0 ? [element] : [...element.children].flatMap((child) => boxesOf(child));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function limitOf(row: HTMLElement, content: Element, gap: number): number {
|
|
16
|
+
const start = content.getBoundingClientRect().left;
|
|
17
|
+
let limit = Number.POSITIVE_INFINITY;
|
|
18
|
+
for (const part of [...row.children].filter((child) => child !== content).flatMap((child) => boxesOf(child))) {
|
|
19
|
+
const rect = part.getBoundingClientRect();
|
|
20
|
+
if (rect.width === 0 || rect.left < start) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (part.childNodes.length === 0 && Number.parseFloat(getComputedStyle(part).flexGrow) > 0) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
limit = Math.min(limit, rect.left - gap);
|
|
27
|
+
}
|
|
28
|
+
if (limit !== Number.POSITIVE_INFINITY) {
|
|
29
|
+
return limit - (Number.parseFloat(getComputedStyle(content).marginInlineEnd) || 0);
|
|
30
|
+
}
|
|
31
|
+
return (
|
|
32
|
+
row.getBoundingClientRect().right -
|
|
33
|
+
Number.parseFloat(getComputedStyle(row).paddingInlineEnd) -
|
|
34
|
+
(Number.parseFloat(getComputedStyle(content).marginInlineEnd) || 0)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function needOf(row: HTMLElement, shift: number, gap: number, range: Range): number | null {
|
|
39
|
+
const content = row.querySelector(':scope > [data-item-section="content"]');
|
|
40
|
+
if (content === null || row.getBoundingClientRect().width === 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
range.selectNodeContents(content);
|
|
44
|
+
return shiftNeed(range.getBoundingClientRect().right, limitOf(row, content, gap), shift);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useFileTreeShift(model: FileTree, resetKey?: string): { attach(node: HTMLElement | null): void; bar: ReactElement } {
|
|
48
|
+
const [frame, setFrame] = useState<HTMLElement | null>(null);
|
|
49
|
+
const [track, setTrack] = useState<HTMLDivElement | null>(null);
|
|
50
|
+
const [thumb, setThumb] = useState<HTMLDivElement | null>(null);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (frame === null || track === null || thumb === null) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const range = document.createRange();
|
|
57
|
+
let root: ShadowRoot | null = null;
|
|
58
|
+
let exact = 0;
|
|
59
|
+
let drawn = 0;
|
|
60
|
+
let max = 0;
|
|
61
|
+
let pending = 0;
|
|
62
|
+
let hideTimer = 0;
|
|
63
|
+
let scrolling = false;
|
|
64
|
+
let near = false;
|
|
65
|
+
|
|
66
|
+
const draw = (): void => {
|
|
67
|
+
const shift = Math.round(exact);
|
|
68
|
+
if (shift !== drawn) {
|
|
69
|
+
drawn = shift;
|
|
70
|
+
frame.style.setProperty(SHIFT_PROPERTY, `${shift}px`);
|
|
71
|
+
}
|
|
72
|
+
const { left, width } = shiftThumb(shift, max, track.clientWidth, frame.clientWidth);
|
|
73
|
+
thumb.style.left = `${left}px`;
|
|
74
|
+
thumb.style.width = `${width}px`;
|
|
75
|
+
track.toggleAttribute('data-visible', max > 0 && (scrolling || near));
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const measure = (): void => {
|
|
79
|
+
pending = 0;
|
|
80
|
+
if (root === null) {
|
|
81
|
+
root = model.getFileTreeContainer()?.shadowRoot ?? null;
|
|
82
|
+
if (root === null) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
rows.observe(root, { childList: true, subtree: true, characterData: true });
|
|
86
|
+
}
|
|
87
|
+
const drawnRows = [...root.querySelectorAll<HTMLElement>(ROWS)];
|
|
88
|
+
const gap = drawnRows.length === 0 ? 0 : Number.parseFloat(getComputedStyle(drawnRows[0]!).columnGap) || 0;
|
|
89
|
+
max = maxShift(drawnRows.map((row) => needOf(row, drawn, gap, range)).filter((need) => need !== null));
|
|
90
|
+
exact = clampShift(exact, max);
|
|
91
|
+
draw();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const schedule = (): void => {
|
|
95
|
+
if (pending === 0) {
|
|
96
|
+
pending = requestAnimationFrame(measure);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const rows = new MutationObserver(schedule);
|
|
101
|
+
const mounts = new MutationObserver(schedule);
|
|
102
|
+
mounts.observe(frame, { childList: true, subtree: true });
|
|
103
|
+
const sizes = new ResizeObserver(schedule);
|
|
104
|
+
sizes.observe(frame);
|
|
105
|
+
|
|
106
|
+
const onWheel = (event: WheelEvent): void => {
|
|
107
|
+
const delta = sidewaysDelta(event, frame.clientWidth);
|
|
108
|
+
if (delta === 0 || max === 0) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
scrolling = true;
|
|
112
|
+
window.clearTimeout(hideTimer);
|
|
113
|
+
hideTimer = window.setTimeout(() => {
|
|
114
|
+
scrolling = false;
|
|
115
|
+
draw();
|
|
116
|
+
}, HIDE_AFTER);
|
|
117
|
+
const next = clampShift(exact + delta, max);
|
|
118
|
+
if (next !== exact) {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
exact = next;
|
|
121
|
+
}
|
|
122
|
+
draw();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const onPointerMove = (event: PointerEvent): void => {
|
|
126
|
+
const bottom = track.getBoundingClientRect().bottom;
|
|
127
|
+
const isNear = event.clientY >= bottom - NEAR_BOTTOM;
|
|
128
|
+
if (isNear !== near) {
|
|
129
|
+
near = isNear;
|
|
130
|
+
draw();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const onPointerLeave = (): void => {
|
|
135
|
+
if (near) {
|
|
136
|
+
near = false;
|
|
137
|
+
draw();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
frame.addEventListener('wheel', onWheel, { passive: false });
|
|
142
|
+
frame.addEventListener('pointermove', onPointerMove);
|
|
143
|
+
frame.addEventListener('pointerleave', onPointerLeave);
|
|
144
|
+
schedule();
|
|
145
|
+
|
|
146
|
+
return () => {
|
|
147
|
+
frame.removeEventListener('wheel', onWheel);
|
|
148
|
+
frame.removeEventListener('pointermove', onPointerMove);
|
|
149
|
+
frame.removeEventListener('pointerleave', onPointerLeave);
|
|
150
|
+
rows.disconnect();
|
|
151
|
+
mounts.disconnect();
|
|
152
|
+
sizes.disconnect();
|
|
153
|
+
cancelAnimationFrame(pending);
|
|
154
|
+
window.clearTimeout(hideTimer);
|
|
155
|
+
frame.style.removeProperty(SHIFT_PROPERTY);
|
|
156
|
+
track.removeAttribute('data-visible');
|
|
157
|
+
};
|
|
158
|
+
}, [frame, track, thumb, model, resetKey]);
|
|
159
|
+
|
|
160
|
+
const bar = (
|
|
161
|
+
<div aria-hidden className="adecore-tree-shift">
|
|
162
|
+
<div ref={setTrack} className="adecore-tree-shift-track">
|
|
163
|
+
<div ref={setThumb} className="adecore-tree-shift-thumb" />
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return { attach: setFrame, bar };
|
|
169
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FileTreeOptions as PierreOptions } from '@pierre/trees';
|
|
2
|
+
import { useFileTree as usePierreFileTree } from '@pierre/trees/react';
|
|
3
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
4
|
+
import { FILE_TREE_ICONS } from '../file-icon.ts';
|
|
5
|
+
import { TREE_ROW_HEIGHT } from '../tree/style.ts';
|
|
6
|
+
import { compareRows } from './helpers.ts';
|
|
7
|
+
import { FILE_TREE_CSS } from './style.ts';
|
|
8
|
+
|
|
9
|
+
type WithoutSizing<Options> = Options extends unknown ? Omit<Options, 'itemHeight' | 'density'> : never;
|
|
10
|
+
export type FileTreeOptions = WithoutSizing<PierreOptions>;
|
|
11
|
+
|
|
12
|
+
// Model options are initial; paths and status can subsequently change through the model's mutation methods.
|
|
13
|
+
export function useFileTree(options: FileTreeOptions) {
|
|
14
|
+
const latest = useRef(options.onSelectionChange);
|
|
15
|
+
useLayoutEffect(() => {
|
|
16
|
+
latest.current = options.onSelectionChange;
|
|
17
|
+
}, [options.onSelectionChange]);
|
|
18
|
+
return usePierreFileTree({
|
|
19
|
+
composition: { contextMenu: { enabled: false } },
|
|
20
|
+
icons: FILE_TREE_ICONS,
|
|
21
|
+
initialExpansion: 'closed',
|
|
22
|
+
search: false,
|
|
23
|
+
stickyFolders: false,
|
|
24
|
+
sort: compareRows,
|
|
25
|
+
...options,
|
|
26
|
+
density: 'compact',
|
|
27
|
+
itemHeight: TREE_ROW_HEIGHT,
|
|
28
|
+
onSelectionChange: (paths) => latest.current?.(paths),
|
|
29
|
+
unsafeCSS: `${FILE_TREE_CSS}\n${options.unsafeCSS ?? ''}`
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { useEffect, useId, useState } from 'react';
|
|
2
|
+
import type { FileTree, FileTreeVisibleRow } from '@pierre/trees';
|
|
3
|
+
import { pathOfRow, visibleRows } from './helpers.ts';
|
|
4
|
+
import { FILE_TREE_CSS } from './style.ts';
|
|
5
|
+
|
|
6
|
+
export interface RowSlot {
|
|
7
|
+
row: FileTreeVisibleRow;
|
|
8
|
+
control: HTMLElement;
|
|
9
|
+
decoration: HTMLElement;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Port {
|
|
13
|
+
rowElement: HTMLElement;
|
|
14
|
+
control: HTMLElement;
|
|
15
|
+
decoration: HTMLElement;
|
|
16
|
+
lightControl: HTMLElement;
|
|
17
|
+
lightDecoration: HTMLElement;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// beta.6 owns button rows. Sibling slots keep real controls outside those buttons and in the app's stylesheet.
|
|
21
|
+
export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, label: string) {
|
|
22
|
+
const prefix = useId().replaceAll(':', '');
|
|
23
|
+
const [slots, setSlots] = useState<RowSlot[]>([]);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (frame === null) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const ports = new Map<HTMLElement, Port>();
|
|
30
|
+
let shadow: ShadowRoot | null = null;
|
|
31
|
+
let style: HTMLStyleElement | null = null;
|
|
32
|
+
let serial = 0;
|
|
33
|
+
let disposed = false;
|
|
34
|
+
let pending = false;
|
|
35
|
+
let snapshot: string | null = null;
|
|
36
|
+
|
|
37
|
+
const position = (): void => {
|
|
38
|
+
for (const port of ports.values()) {
|
|
39
|
+
const row = port.rowElement;
|
|
40
|
+
const content = row.querySelector<HTMLElement>('[data-item-section="content"]');
|
|
41
|
+
const icon = row.querySelector<HTMLElement>('[data-item-section="icon"]');
|
|
42
|
+
const control = port.lightControl;
|
|
43
|
+
const decoration = port.lightDecoration;
|
|
44
|
+
const controlSpace = control.childNodes.length > 0 ? Math.ceil(control.getBoundingClientRect().width) + 6 : 0;
|
|
45
|
+
const decorationSpace = decoration.childNodes.length > 0 ? Math.ceil(decoration.getBoundingClientRect().width) + 6 : 0;
|
|
46
|
+
row.style.setProperty('--adecore-tree-control-space', `${controlSpace}px`);
|
|
47
|
+
row.style.setProperty('--adecore-tree-decoration-space', `${decorationSpace}px`);
|
|
48
|
+
port.control.style.top = `${row.offsetTop}px`;
|
|
49
|
+
port.control.style.left = `${row.offsetLeft + (icon?.offsetLeft ?? 4) - controlSpace}px`;
|
|
50
|
+
port.decoration.style.top = `${row.offsetTop}px`;
|
|
51
|
+
port.decoration.style.left = `${row.offsetLeft + (content === null ? row.offsetWidth - decorationSpace - 2 : content.offsetLeft + content.offsetWidth + 6)}px`;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const resize = new ResizeObserver(position);
|
|
56
|
+
const scan = (): void => {
|
|
57
|
+
pending = false;
|
|
58
|
+
if (disposed) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const current = model.getFileTreeContainer();
|
|
62
|
+
if (!current?.shadowRoot) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (shadow !== current.shadowRoot) {
|
|
66
|
+
shadow = current.shadowRoot;
|
|
67
|
+
style = document.createElement('style');
|
|
68
|
+
style.dataset.adecoreTreeStyle = 'true';
|
|
69
|
+
style.textContent = FILE_TREE_CSS;
|
|
70
|
+
shadow.append(style);
|
|
71
|
+
changes.observe(shadow, {
|
|
72
|
+
childList: true,
|
|
73
|
+
subtree: true,
|
|
74
|
+
attributes: true,
|
|
75
|
+
attributeFilter: ['data-item-path', 'aria-selected', 'aria-expanded']
|
|
76
|
+
});
|
|
77
|
+
changes.observe(current, { childList: true, subtree: true });
|
|
78
|
+
resize.observe(current);
|
|
79
|
+
}
|
|
80
|
+
shadow.querySelector('[role="tree"]')?.setAttribute('aria-label', label);
|
|
81
|
+
const rows = visibleRows(model);
|
|
82
|
+
const byPath = new Map(rows.map((row) => [pathOfRow(row), row]));
|
|
83
|
+
const elements = [
|
|
84
|
+
...shadow.querySelectorAll<HTMLElement>('[data-type="item"]:not([data-item-parked="true"]):not([data-file-tree-sticky-row="true"])')
|
|
85
|
+
];
|
|
86
|
+
const live = new Set(elements);
|
|
87
|
+
for (const [element, port] of ports) {
|
|
88
|
+
if (!live.has(element)) {
|
|
89
|
+
port.control.remove();
|
|
90
|
+
port.decoration.remove();
|
|
91
|
+
resize.unobserve(port.lightControl);
|
|
92
|
+
resize.unobserve(port.lightDecoration);
|
|
93
|
+
port.lightControl.remove();
|
|
94
|
+
port.lightDecoration.remove();
|
|
95
|
+
ports.delete(element);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const next: RowSlot[] = [];
|
|
99
|
+
for (const element of elements) {
|
|
100
|
+
const path = element.dataset.itemPath;
|
|
101
|
+
const row = path === undefined ? undefined : byPath.get(path);
|
|
102
|
+
if (row === undefined) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
element.dataset.interactive = 'true';
|
|
106
|
+
element.dataset.joinedStart = String(row.isSelected && rows[row.index - 1]?.isSelected === true);
|
|
107
|
+
element.dataset.joinedEnd = String(row.isSelected && rows[row.index + 1]?.isSelected === true);
|
|
108
|
+
let port = ports.get(element);
|
|
109
|
+
if (port === undefined) {
|
|
110
|
+
const makePort = (kind: string): { container: HTMLElement; target: HTMLElement } => {
|
|
111
|
+
const container = document.createElement('span');
|
|
112
|
+
container.dataset.treePort = kind;
|
|
113
|
+
const slot = document.createElement('slot');
|
|
114
|
+
slot.name = `${prefix}-${kind}-${serial}`;
|
|
115
|
+
container.append(slot);
|
|
116
|
+
element.parentElement?.append(container);
|
|
117
|
+
const target = document.createElement('span');
|
|
118
|
+
target.slot = slot.name;
|
|
119
|
+
target.dataset.treeSlot = kind;
|
|
120
|
+
target.className = kind === 'control' ? 'adecore-tree-control' : 'adecore-tree-decoration';
|
|
121
|
+
// React delegates to this portal target before the event reaches the engine's shadow tree.
|
|
122
|
+
for (const type of ['click', 'dblclick', 'keydown', 'keyup', 'pointerdown', 'dragstart']) {
|
|
123
|
+
target.addEventListener(type, (event) => event.stopPropagation());
|
|
124
|
+
}
|
|
125
|
+
current.append(target);
|
|
126
|
+
resize.observe(target);
|
|
127
|
+
return { container, target };
|
|
128
|
+
};
|
|
129
|
+
serial++;
|
|
130
|
+
const control = makePort('control');
|
|
131
|
+
const decoration = makePort('decoration');
|
|
132
|
+
port = {
|
|
133
|
+
rowElement: element,
|
|
134
|
+
control: control.container,
|
|
135
|
+
decoration: decoration.container,
|
|
136
|
+
lightControl: control.target,
|
|
137
|
+
lightDecoration: decoration.target
|
|
138
|
+
};
|
|
139
|
+
ports.set(element, port);
|
|
140
|
+
}
|
|
141
|
+
if (port.control.dataset.itemPath !== row.path) {
|
|
142
|
+
port.control.dataset.itemPath = row.path;
|
|
143
|
+
}
|
|
144
|
+
if (port.decoration.dataset.itemPath !== row.path) {
|
|
145
|
+
port.decoration.dataset.itemPath = row.path;
|
|
146
|
+
}
|
|
147
|
+
port.lightControl.dataset.itemPath = row.path;
|
|
148
|
+
port.lightDecoration.dataset.itemPath = row.path;
|
|
149
|
+
next.push({ row, control: port.lightControl, decoration: port.lightDecoration });
|
|
150
|
+
}
|
|
151
|
+
const key = next
|
|
152
|
+
.map(({ row, control }) => `${control.slot}:${row.path}:${row.isSelected}:${row.isExpanded}:${row.isFocused}:${row.name}`)
|
|
153
|
+
.join('|');
|
|
154
|
+
if (snapshot !== key) {
|
|
155
|
+
snapshot = key;
|
|
156
|
+
setSlots(next);
|
|
157
|
+
}
|
|
158
|
+
position();
|
|
159
|
+
};
|
|
160
|
+
const schedule = (): void => {
|
|
161
|
+
if (!pending && !disposed) {
|
|
162
|
+
pending = true;
|
|
163
|
+
queueMicrotask(scan);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const shifts = new MutationObserver(position);
|
|
167
|
+
shifts.observe(frame, { attributes: true, attributeFilter: ['style'] });
|
|
168
|
+
const changes = new MutationObserver(schedule);
|
|
169
|
+
changes.observe(frame, { childList: true, subtree: true });
|
|
170
|
+
const unsubscribe = model.subscribe(schedule);
|
|
171
|
+
schedule();
|
|
172
|
+
return () => {
|
|
173
|
+
disposed = true;
|
|
174
|
+
unsubscribe();
|
|
175
|
+
changes.disconnect();
|
|
176
|
+
shifts.disconnect();
|
|
177
|
+
resize.disconnect();
|
|
178
|
+
style?.remove();
|
|
179
|
+
for (const port of ports.values()) {
|
|
180
|
+
port.rowElement.style.removeProperty('--adecore-tree-control-space');
|
|
181
|
+
port.rowElement.style.removeProperty('--adecore-tree-decoration-space');
|
|
182
|
+
port.control.remove();
|
|
183
|
+
port.decoration.remove();
|
|
184
|
+
port.lightControl.remove();
|
|
185
|
+
port.lightDecoration.remove();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}, [frame, model, label, prefix]);
|
|
189
|
+
|
|
190
|
+
return slots;
|
|
191
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export * as ContextMenu from './context-menu/index.parts.ts';
|
|
7
|
+
export * as FileTree from './file-tree/index.parts.ts';
|
|
8
|
+
export * as Tree from './tree/index.parts.ts';
|
|
7
9
|
export * as Dialog from './dialog/index.parts.ts';
|
|
8
10
|
export * as KeyValueList from './key-value-list/index.parts.ts';
|
|
9
11
|
export * as Menu from './menu/index.parts.ts';
|
|
@@ -105,3 +107,18 @@ export { useContentSize, type ContentSize } from './useContentSize.ts';
|
|
|
105
107
|
export { useMeasuredWidth } from './useMeasuredWidth.ts';
|
|
106
108
|
export { useNow, useTickingText } from './useNow.ts';
|
|
107
109
|
export { ZOOM_PRESETS } from './zoom.ts';
|
|
110
|
+
|
|
111
|
+
export { useFileTree, type FileTreeOptions } from './file-tree/use-file-tree.ts';
|
|
112
|
+
export type { FileTreeRootProps } from './file-tree/parts.tsx';
|
|
113
|
+
export type { FileTree as FileTreeModel, FileTreeVisibleRow, FileTreeDragAndDropConfig, FileTreeDropContext, FileTreeDropResult } from '@pierre/trees';
|
|
114
|
+
export type { FoldKeyOf, SortRow } from './file-tree/helpers.ts';
|
|
115
|
+
export type {
|
|
116
|
+
TreeRootProps,
|
|
117
|
+
TreeRowProps,
|
|
118
|
+
TreeChevronProps,
|
|
119
|
+
TreeChevronSlotProps,
|
|
120
|
+
TreeLabelProps,
|
|
121
|
+
TreeDecorationProps,
|
|
122
|
+
TreeControlProps,
|
|
123
|
+
TreeCheckboxProps
|
|
124
|
+
} from './tree/parts.tsx';
|
package/src/locales/en.json
CHANGED
package/src/locales/nl.json
CHANGED
package/src/menu/parts.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { useRender } from '@base-ui-components/react/use-render';
|
|
|
8
8
|
import { withClass } from '../class-name.ts';
|
|
9
9
|
import { Icon } from '../Icon.tsx';
|
|
10
10
|
import { Tooltip } from '../Tooltip.tsx';
|
|
11
|
+
import { renderPositioner } from '../popup-layer.ts';
|
|
11
12
|
|
|
12
13
|
/* Where a popup opens from decides where it goes when a call site says nothing. */
|
|
13
14
|
type MenuKind = 'menu' | 'context' | 'submenu';
|
|
@@ -96,6 +97,7 @@ export function MenuPopup({
|
|
|
96
97
|
<BaseMenu.Portal>
|
|
97
98
|
<BaseMenu.Positioner
|
|
98
99
|
className="popup-positioner"
|
|
100
|
+
render={renderPositioner}
|
|
99
101
|
side={side ?? defaults.side}
|
|
100
102
|
align={align ?? defaults.align}
|
|
101
103
|
sideOffset={sideOffset ?? defaults.sideOffset}
|
package/src/popover/parts.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import type { ComponentProps } from 'react';
|
|
|
2
2
|
import { Popover as BasePopover } from '@base-ui-components/react/popover';
|
|
3
3
|
import { PreviewCard as BasePreviewCard } from '@base-ui-components/react/preview-card';
|
|
4
4
|
import { withClass } from '../class-name.ts';
|
|
5
|
+
import { renderPositioner } from '../popup-layer.ts';
|
|
5
6
|
|
|
6
7
|
/*
|
|
7
8
|
* The surface a popup is drawn on. `menu` is the padded card menus use, `picker` a card whose
|
|
@@ -55,6 +56,7 @@ export function PopoverPopup({
|
|
|
55
56
|
<BasePopover.Portal>
|
|
56
57
|
<BasePopover.Positioner
|
|
57
58
|
className="popup-positioner"
|
|
59
|
+
render={renderPositioner}
|
|
58
60
|
side={side}
|
|
59
61
|
align={align}
|
|
60
62
|
sideOffset={sideOffset}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createElement, useLayoutEffect, type ComponentProps } from 'react';
|
|
2
|
+
|
|
3
|
+
// One attribute serves every open popup, so it stays until the last one closes.
|
|
4
|
+
const open = new Set<symbol>();
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Marks the root with `data-popup-open` while a popup is open, which the theme reads to turn the
|
|
8
|
+
* window's drag regions off. An attribute on the root instead of `:root:has(...)`, which made the
|
|
9
|
+
* browser restyle the whole page on every DOM insertion.
|
|
10
|
+
*/
|
|
11
|
+
function usePopupLayer(isOpen: boolean): void {
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
if (!isOpen) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const self = Symbol('popup');
|
|
17
|
+
open.add(self);
|
|
18
|
+
document.documentElement.setAttribute('data-popup-open', '');
|
|
19
|
+
return () => {
|
|
20
|
+
open.delete(self);
|
|
21
|
+
if (open.size === 0) {
|
|
22
|
+
document.documentElement.removeAttribute('data-popup-open');
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}, [isOpen]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function LayerPositioner({ open: isOpen, ...props }: ComponentProps<'div'> & { readonly open: boolean }) {
|
|
29
|
+
usePopupLayer(isOpen);
|
|
30
|
+
return createElement('div', props);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* The `render` of a popup's positioner, which knows whether the popup is open and so marks the root for as long as it is. */
|
|
34
|
+
export const renderPositioner = (props: ComponentProps<'div'>, state: { open: boolean }) => createElement(LayerPositioner, { ...props, open: state.open });
|
package/src/theme.css
CHANGED
|
@@ -424,7 +424,7 @@
|
|
|
424
424
|
/* A press on a window's drag region goes to the system and never reaches the page, so a popup
|
|
425
425
|
that closes on a press outside it would stay open over a custom title bar. While one is open
|
|
426
426
|
nothing drags, the way a native title bar takes that first press to close a menu. */
|
|
427
|
-
:root
|
|
427
|
+
:root[data-popup-open] * { -webkit-app-region: no-drag; }
|
|
428
428
|
.dialog-backdrop { position: fixed; inset: 0; z-index: var(--z-dialog-backdrop); background: color-mix(in srgb, var(--bg) 55%, transparent); }
|
|
429
429
|
.dialog-backdrop[data-starting-style], .dialog-backdrop[data-ending-style] { opacity: 0; }
|
|
430
430
|
.dialog-popup {
|
|
@@ -563,3 +563,106 @@
|
|
|
563
563
|
.sliding-column[data-resizing],
|
|
564
564
|
.sliding-column[data-instant] { transition: none; }
|
|
565
565
|
}
|
|
566
|
+
|
|
567
|
+
@layer components {
|
|
568
|
+
.adecore-tree {
|
|
569
|
+
--tree-row-height: 25px;
|
|
570
|
+
--tree-row-gap: 6px;
|
|
571
|
+
--tree-row-radius: 6px;
|
|
572
|
+
--trees-bg-override: transparent;
|
|
573
|
+
--trees-padding-inline-override: 8px;
|
|
574
|
+
--trees-bg-muted-override: var(--surface-hover);
|
|
575
|
+
--trees-fg-override: var(--text);
|
|
576
|
+
--trees-fg-muted-override: var(--text-muted);
|
|
577
|
+
--trees-selected-bg-override: var(--surface-active);
|
|
578
|
+
--trees-selected-fg-override: var(--text);
|
|
579
|
+
--trees-border-color-override: var(--border);
|
|
580
|
+
--trees-accent-override: var(--accent);
|
|
581
|
+
--trees-focus-ring-color-override: var(--accent);
|
|
582
|
+
--trees-selected-focused-border-color-override: var(--accent);
|
|
583
|
+
--trees-status-ignored-override: var(--text-faint);
|
|
584
|
+
--trees-git-ignored-color-override: var(--text-faint);
|
|
585
|
+
--trees-git-modified-color-override: var(--status-needs-you);
|
|
586
|
+
--trees-git-added-color-override: var(--status-idle);
|
|
587
|
+
--trees-git-untracked-color-override: var(--status-idle);
|
|
588
|
+
--trees-git-deleted-color-override: var(--status-error);
|
|
589
|
+
--trees-git-renamed-color-override: var(--status-running);
|
|
590
|
+
--trees-font-family-override: var(--font-sans);
|
|
591
|
+
--trees-font-size-override: var(--text-xs);
|
|
592
|
+
}
|
|
593
|
+
.adecore-tree-row {
|
|
594
|
+
margin-inline: 8px;
|
|
595
|
+
padding-inline-end: 8px;
|
|
596
|
+
}
|
|
597
|
+
.adecore-tree-guide {
|
|
598
|
+
pointer-events: none;
|
|
599
|
+
position: absolute;
|
|
600
|
+
inset-block: 0;
|
|
601
|
+
width: 1px;
|
|
602
|
+
background: var(--border);
|
|
603
|
+
}
|
|
604
|
+
.adecore-tree-chevron-slot {
|
|
605
|
+
display: grid;
|
|
606
|
+
place-items: center;
|
|
607
|
+
flex: none;
|
|
608
|
+
width: 16px;
|
|
609
|
+
height: 16px;
|
|
610
|
+
padding: 0;
|
|
611
|
+
border: 0;
|
|
612
|
+
background: transparent;
|
|
613
|
+
color: var(--text-muted);
|
|
614
|
+
}
|
|
615
|
+
.adecore-tree-label {
|
|
616
|
+
min-width: 0;
|
|
617
|
+
overflow: hidden;
|
|
618
|
+
text-overflow: ellipsis;
|
|
619
|
+
white-space: nowrap;
|
|
620
|
+
}
|
|
621
|
+
.adecore-tree-control,
|
|
622
|
+
.adecore-tree-decoration {
|
|
623
|
+
display: inline-flex;
|
|
624
|
+
flex: none;
|
|
625
|
+
align-items: center;
|
|
626
|
+
gap: 6px;
|
|
627
|
+
}
|
|
628
|
+
.adecore-tree-decoration {
|
|
629
|
+
font-variant-numeric: tabular-nums;
|
|
630
|
+
}
|
|
631
|
+
.adecore-file-tree {
|
|
632
|
+
position: relative;
|
|
633
|
+
min-height: 0;
|
|
634
|
+
overflow: hidden;
|
|
635
|
+
}
|
|
636
|
+
.adecore-file-tree-host {
|
|
637
|
+
display: block;
|
|
638
|
+
height: 100%;
|
|
639
|
+
}
|
|
640
|
+
.adecore-tree-shift {
|
|
641
|
+
pointer-events: none;
|
|
642
|
+
position: sticky;
|
|
643
|
+
bottom: 0;
|
|
644
|
+
height: 0;
|
|
645
|
+
}
|
|
646
|
+
.adecore-tree-shift-track {
|
|
647
|
+
position: absolute;
|
|
648
|
+
inset-inline: 8px;
|
|
649
|
+
bottom: 2px;
|
|
650
|
+
height: 6px;
|
|
651
|
+
opacity: 0;
|
|
652
|
+
transition: opacity 150ms ease-out;
|
|
653
|
+
}
|
|
654
|
+
.adecore-tree-shift-track[data-visible] {
|
|
655
|
+
opacity: 1;
|
|
656
|
+
}
|
|
657
|
+
.adecore-tree-shift-thumb {
|
|
658
|
+
position: absolute;
|
|
659
|
+
inset-block: 0;
|
|
660
|
+
border-radius: 999px;
|
|
661
|
+
background: var(--text-faint);
|
|
662
|
+
}
|
|
663
|
+
@media (prefers-reduced-motion: reduce) {
|
|
664
|
+
.adecore-tree-shift-track {
|
|
665
|
+
transition: none;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|