@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.
Files changed (54) hide show
  1. package/LICENSE +97 -8
  2. package/README.md +1 -1
  3. package/dist/Select.js +2 -1
  4. package/dist/file-tree/helpers.d.ts +42 -0
  5. package/dist/file-tree/helpers.js +167 -0
  6. package/dist/file-tree/index.parts.d.ts +3 -0
  7. package/dist/file-tree/index.parts.js +3 -0
  8. package/dist/file-tree/parts.d.ts +20 -0
  9. package/dist/file-tree/parts.js +150 -0
  10. package/dist/file-tree/shift.d.ts +15 -0
  11. package/dist/file-tree/shift.js +34 -0
  12. package/dist/file-tree/style.d.ts +1 -0
  13. package/dist/file-tree/style.js +24 -0
  14. package/dist/file-tree/use-file-tree-shift.d.ts +6 -0
  15. package/dist/file-tree/use-file-tree-shift.js +142 -0
  16. package/dist/file-tree/use-file-tree.d.ts +5 -0
  17. package/dist/file-tree/use-file-tree.js +26 -0
  18. package/dist/file-tree/use-slots.d.ts +7 -0
  19. package/dist/file-tree/use-slots.js +171 -0
  20. package/dist/index.d.ts +7 -0
  21. package/dist/index.js +3 -0
  22. package/dist/locales/en.json +5 -0
  23. package/dist/locales/nl.json +5 -0
  24. package/dist/menu/parts.js +2 -1
  25. package/dist/popover/parts.js +2 -1
  26. package/dist/popup-layer.d.ts +6 -0
  27. package/dist/popup-layer.js +30 -0
  28. package/dist/theme.css +104 -1
  29. package/dist/tree/index.parts.d.ts +1 -0
  30. package/dist/tree/index.parts.js +1 -0
  31. package/dist/tree/parts.d.ts +27 -0
  32. package/dist/tree/parts.js +113 -0
  33. package/dist/tree/style.d.ts +2 -0
  34. package/dist/tree/style.js +29 -0
  35. package/package.json +6 -4
  36. package/src/Select.tsx +9 -1
  37. package/src/file-tree/helpers.ts +203 -0
  38. package/src/file-tree/index.parts.ts +25 -0
  39. package/src/file-tree/parts.tsx +206 -0
  40. package/src/file-tree/shift.ts +48 -0
  41. package/src/file-tree/style.ts +25 -0
  42. package/src/file-tree/use-file-tree-shift.tsx +169 -0
  43. package/src/file-tree/use-file-tree.ts +31 -0
  44. package/src/file-tree/use-slots.ts +191 -0
  45. package/src/index.ts +17 -0
  46. package/src/locales/en.json +5 -0
  47. package/src/locales/nl.json +5 -0
  48. package/src/menu/parts.tsx +2 -0
  49. package/src/popover/parts.tsx +2 -0
  50. package/src/popup-layer.ts +34 -0
  51. package/src/theme.css +104 -1
  52. package/src/tree/index.parts.ts +10 -0
  53. package/src/tree/parts.tsx +170 -0
  54. package/src/tree/style.ts +30 -0
@@ -0,0 +1,142 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { clampShift, maxShift, SHIFT_PROPERTY, shiftNeed, shiftThumb, sidewaysDelta } from './shift.js';
4
+ const ROWS = '[data-type="item"]:not([data-item-parked="true"])';
5
+ const HIDE_AFTER = 900;
6
+ const NEAR_BOTTOM = 16;
7
+ function boxesOf(element) {
8
+ return element.getClientRects().length > 0 ? [element] : [...element.children].flatMap((child) => boxesOf(child));
9
+ }
10
+ function limitOf(row, content, gap) {
11
+ const start = content.getBoundingClientRect().left;
12
+ let limit = Number.POSITIVE_INFINITY;
13
+ for (const part of [...row.children].filter((child) => child !== content).flatMap((child) => boxesOf(child))) {
14
+ const rect = part.getBoundingClientRect();
15
+ if (rect.width === 0 || rect.left < start) {
16
+ continue;
17
+ }
18
+ if (part.childNodes.length === 0 && Number.parseFloat(getComputedStyle(part).flexGrow) > 0) {
19
+ continue;
20
+ }
21
+ limit = Math.min(limit, rect.left - gap);
22
+ }
23
+ if (limit !== Number.POSITIVE_INFINITY) {
24
+ return limit - (Number.parseFloat(getComputedStyle(content).marginInlineEnd) || 0);
25
+ }
26
+ return (row.getBoundingClientRect().right -
27
+ Number.parseFloat(getComputedStyle(row).paddingInlineEnd) -
28
+ (Number.parseFloat(getComputedStyle(content).marginInlineEnd) || 0));
29
+ }
30
+ function needOf(row, shift, gap, range) {
31
+ const content = row.querySelector(':scope > [data-item-section="content"]');
32
+ if (content === null || row.getBoundingClientRect().width === 0) {
33
+ return null;
34
+ }
35
+ range.selectNodeContents(content);
36
+ return shiftNeed(range.getBoundingClientRect().right, limitOf(row, content, gap), shift);
37
+ }
38
+ export function useFileTreeShift(model, resetKey) {
39
+ const [frame, setFrame] = useState(null);
40
+ const [track, setTrack] = useState(null);
41
+ const [thumb, setThumb] = useState(null);
42
+ useEffect(() => {
43
+ if (frame === null || track === null || thumb === null) {
44
+ return;
45
+ }
46
+ const range = document.createRange();
47
+ let root = null;
48
+ let exact = 0;
49
+ let drawn = 0;
50
+ let max = 0;
51
+ let pending = 0;
52
+ let hideTimer = 0;
53
+ let scrolling = false;
54
+ let near = false;
55
+ const draw = () => {
56
+ const shift = Math.round(exact);
57
+ if (shift !== drawn) {
58
+ drawn = shift;
59
+ frame.style.setProperty(SHIFT_PROPERTY, `${shift}px`);
60
+ }
61
+ const { left, width } = shiftThumb(shift, max, track.clientWidth, frame.clientWidth);
62
+ thumb.style.left = `${left}px`;
63
+ thumb.style.width = `${width}px`;
64
+ track.toggleAttribute('data-visible', max > 0 && (scrolling || near));
65
+ };
66
+ const measure = () => {
67
+ pending = 0;
68
+ if (root === null) {
69
+ root = model.getFileTreeContainer()?.shadowRoot ?? null;
70
+ if (root === null) {
71
+ return;
72
+ }
73
+ rows.observe(root, { childList: true, subtree: true, characterData: true });
74
+ }
75
+ const drawnRows = [...root.querySelectorAll(ROWS)];
76
+ const gap = drawnRows.length === 0 ? 0 : Number.parseFloat(getComputedStyle(drawnRows[0]).columnGap) || 0;
77
+ max = maxShift(drawnRows.map((row) => needOf(row, drawn, gap, range)).filter((need) => need !== null));
78
+ exact = clampShift(exact, max);
79
+ draw();
80
+ };
81
+ const schedule = () => {
82
+ if (pending === 0) {
83
+ pending = requestAnimationFrame(measure);
84
+ }
85
+ };
86
+ const rows = new MutationObserver(schedule);
87
+ const mounts = new MutationObserver(schedule);
88
+ mounts.observe(frame, { childList: true, subtree: true });
89
+ const sizes = new ResizeObserver(schedule);
90
+ sizes.observe(frame);
91
+ const onWheel = (event) => {
92
+ const delta = sidewaysDelta(event, frame.clientWidth);
93
+ if (delta === 0 || max === 0) {
94
+ return;
95
+ }
96
+ scrolling = true;
97
+ window.clearTimeout(hideTimer);
98
+ hideTimer = window.setTimeout(() => {
99
+ scrolling = false;
100
+ draw();
101
+ }, HIDE_AFTER);
102
+ const next = clampShift(exact + delta, max);
103
+ if (next !== exact) {
104
+ event.preventDefault();
105
+ exact = next;
106
+ }
107
+ draw();
108
+ };
109
+ const onPointerMove = (event) => {
110
+ const bottom = track.getBoundingClientRect().bottom;
111
+ const isNear = event.clientY >= bottom - NEAR_BOTTOM;
112
+ if (isNear !== near) {
113
+ near = isNear;
114
+ draw();
115
+ }
116
+ };
117
+ const onPointerLeave = () => {
118
+ if (near) {
119
+ near = false;
120
+ draw();
121
+ }
122
+ };
123
+ frame.addEventListener('wheel', onWheel, { passive: false });
124
+ frame.addEventListener('pointermove', onPointerMove);
125
+ frame.addEventListener('pointerleave', onPointerLeave);
126
+ schedule();
127
+ return () => {
128
+ frame.removeEventListener('wheel', onWheel);
129
+ frame.removeEventListener('pointermove', onPointerMove);
130
+ frame.removeEventListener('pointerleave', onPointerLeave);
131
+ rows.disconnect();
132
+ mounts.disconnect();
133
+ sizes.disconnect();
134
+ cancelAnimationFrame(pending);
135
+ window.clearTimeout(hideTimer);
136
+ frame.style.removeProperty(SHIFT_PROPERTY);
137
+ track.removeAttribute('data-visible');
138
+ };
139
+ }, [frame, track, thumb, model, resetKey]);
140
+ const bar = (_jsx("div", { "aria-hidden": true, className: "adecore-tree-shift", children: _jsx("div", { ref: setTrack, className: "adecore-tree-shift-track", children: _jsx("div", { ref: setThumb, className: "adecore-tree-shift-thumb" }) }) }));
141
+ return { attach: setFrame, bar };
142
+ }
@@ -0,0 +1,5 @@
1
+ import type { FileTreeOptions as PierreOptions } from '@pierre/trees';
2
+ type WithoutSizing<Options> = Options extends unknown ? Omit<Options, 'itemHeight' | 'density'> : never;
3
+ export type FileTreeOptions = WithoutSizing<PierreOptions>;
4
+ export declare function useFileTree(options: FileTreeOptions): import("@pierre/trees/react").UseFileTreeResult;
5
+ export {};
@@ -0,0 +1,26 @@
1
+ import { useFileTree as usePierreFileTree } from '@pierre/trees/react';
2
+ import { useLayoutEffect, useRef } from 'react';
3
+ import { FILE_TREE_ICONS } from '../file-icon.js';
4
+ import { TREE_ROW_HEIGHT } from '../tree/style.js';
5
+ import { compareRows } from './helpers.js';
6
+ import { FILE_TREE_CSS } from './style.js';
7
+ // Model options are initial; paths and status can subsequently change through the model's mutation methods.
8
+ export function useFileTree(options) {
9
+ const latest = useRef(options.onSelectionChange);
10
+ useLayoutEffect(() => {
11
+ latest.current = options.onSelectionChange;
12
+ }, [options.onSelectionChange]);
13
+ return usePierreFileTree({
14
+ composition: { contextMenu: { enabled: false } },
15
+ icons: FILE_TREE_ICONS,
16
+ initialExpansion: 'closed',
17
+ search: false,
18
+ stickyFolders: false,
19
+ sort: compareRows,
20
+ ...options,
21
+ density: 'compact',
22
+ itemHeight: TREE_ROW_HEIGHT,
23
+ onSelectionChange: (paths) => latest.current?.(paths),
24
+ unsafeCSS: `${FILE_TREE_CSS}\n${options.unsafeCSS ?? ''}`
25
+ });
26
+ }
@@ -0,0 +1,7 @@
1
+ import type { FileTree, FileTreeVisibleRow } from '@pierre/trees';
2
+ export interface RowSlot {
3
+ row: FileTreeVisibleRow;
4
+ control: HTMLElement;
5
+ decoration: HTMLElement;
6
+ }
7
+ export declare function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, label: string): RowSlot[];
@@ -0,0 +1,171 @@
1
+ import { useEffect, useId, useState } from 'react';
2
+ import { pathOfRow, visibleRows } from './helpers.js';
3
+ import { FILE_TREE_CSS } from './style.js';
4
+ // beta.6 owns button rows. Sibling slots keep real controls outside those buttons and in the app's stylesheet.
5
+ export function useFileTreeSlots(model, frame, label) {
6
+ const prefix = useId().replaceAll(':', '');
7
+ const [slots, setSlots] = useState([]);
8
+ useEffect(() => {
9
+ if (frame === null) {
10
+ return;
11
+ }
12
+ const ports = new Map();
13
+ let shadow = null;
14
+ let style = null;
15
+ let serial = 0;
16
+ let disposed = false;
17
+ let pending = false;
18
+ let snapshot = null;
19
+ const position = () => {
20
+ for (const port of ports.values()) {
21
+ const row = port.rowElement;
22
+ const content = row.querySelector('[data-item-section="content"]');
23
+ const icon = row.querySelector('[data-item-section="icon"]');
24
+ const control = port.lightControl;
25
+ const decoration = port.lightDecoration;
26
+ const controlSpace = control.childNodes.length > 0 ? Math.ceil(control.getBoundingClientRect().width) + 6 : 0;
27
+ const decorationSpace = decoration.childNodes.length > 0 ? Math.ceil(decoration.getBoundingClientRect().width) + 6 : 0;
28
+ row.style.setProperty('--adecore-tree-control-space', `${controlSpace}px`);
29
+ row.style.setProperty('--adecore-tree-decoration-space', `${decorationSpace}px`);
30
+ port.control.style.top = `${row.offsetTop}px`;
31
+ port.control.style.left = `${row.offsetLeft + (icon?.offsetLeft ?? 4) - controlSpace}px`;
32
+ port.decoration.style.top = `${row.offsetTop}px`;
33
+ port.decoration.style.left = `${row.offsetLeft + (content === null ? row.offsetWidth - decorationSpace - 2 : content.offsetLeft + content.offsetWidth + 6)}px`;
34
+ }
35
+ };
36
+ const resize = new ResizeObserver(position);
37
+ const scan = () => {
38
+ pending = false;
39
+ if (disposed) {
40
+ return;
41
+ }
42
+ const current = model.getFileTreeContainer();
43
+ if (!current?.shadowRoot) {
44
+ return;
45
+ }
46
+ if (shadow !== current.shadowRoot) {
47
+ shadow = current.shadowRoot;
48
+ style = document.createElement('style');
49
+ style.dataset.adecoreTreeStyle = 'true';
50
+ style.textContent = FILE_TREE_CSS;
51
+ shadow.append(style);
52
+ changes.observe(shadow, {
53
+ childList: true,
54
+ subtree: true,
55
+ attributes: true,
56
+ attributeFilter: ['data-item-path', 'aria-selected', 'aria-expanded']
57
+ });
58
+ changes.observe(current, { childList: true, subtree: true });
59
+ resize.observe(current);
60
+ }
61
+ shadow.querySelector('[role="tree"]')?.setAttribute('aria-label', label);
62
+ const rows = visibleRows(model);
63
+ const byPath = new Map(rows.map((row) => [pathOfRow(row), row]));
64
+ const elements = [
65
+ ...shadow.querySelectorAll('[data-type="item"]:not([data-item-parked="true"]):not([data-file-tree-sticky-row="true"])')
66
+ ];
67
+ const live = new Set(elements);
68
+ for (const [element, port] of ports) {
69
+ if (!live.has(element)) {
70
+ port.control.remove();
71
+ port.decoration.remove();
72
+ resize.unobserve(port.lightControl);
73
+ resize.unobserve(port.lightDecoration);
74
+ port.lightControl.remove();
75
+ port.lightDecoration.remove();
76
+ ports.delete(element);
77
+ }
78
+ }
79
+ const next = [];
80
+ for (const element of elements) {
81
+ const path = element.dataset.itemPath;
82
+ const row = path === undefined ? undefined : byPath.get(path);
83
+ if (row === undefined) {
84
+ continue;
85
+ }
86
+ element.dataset.interactive = 'true';
87
+ element.dataset.joinedStart = String(row.isSelected && rows[row.index - 1]?.isSelected === true);
88
+ element.dataset.joinedEnd = String(row.isSelected && rows[row.index + 1]?.isSelected === true);
89
+ let port = ports.get(element);
90
+ if (port === undefined) {
91
+ const makePort = (kind) => {
92
+ const container = document.createElement('span');
93
+ container.dataset.treePort = kind;
94
+ const slot = document.createElement('slot');
95
+ slot.name = `${prefix}-${kind}-${serial}`;
96
+ container.append(slot);
97
+ element.parentElement?.append(container);
98
+ const target = document.createElement('span');
99
+ target.slot = slot.name;
100
+ target.dataset.treeSlot = kind;
101
+ target.className = kind === 'control' ? 'adecore-tree-control' : 'adecore-tree-decoration';
102
+ // React delegates to this portal target before the event reaches the engine's shadow tree.
103
+ for (const type of ['click', 'dblclick', 'keydown', 'keyup', 'pointerdown', 'dragstart']) {
104
+ target.addEventListener(type, (event) => event.stopPropagation());
105
+ }
106
+ current.append(target);
107
+ resize.observe(target);
108
+ return { container, target };
109
+ };
110
+ serial++;
111
+ const control = makePort('control');
112
+ const decoration = makePort('decoration');
113
+ port = {
114
+ rowElement: element,
115
+ control: control.container,
116
+ decoration: decoration.container,
117
+ lightControl: control.target,
118
+ lightDecoration: decoration.target
119
+ };
120
+ ports.set(element, port);
121
+ }
122
+ if (port.control.dataset.itemPath !== row.path) {
123
+ port.control.dataset.itemPath = row.path;
124
+ }
125
+ if (port.decoration.dataset.itemPath !== row.path) {
126
+ port.decoration.dataset.itemPath = row.path;
127
+ }
128
+ port.lightControl.dataset.itemPath = row.path;
129
+ port.lightDecoration.dataset.itemPath = row.path;
130
+ next.push({ row, control: port.lightControl, decoration: port.lightDecoration });
131
+ }
132
+ const key = next
133
+ .map(({ row, control }) => `${control.slot}:${row.path}:${row.isSelected}:${row.isExpanded}:${row.isFocused}:${row.name}`)
134
+ .join('|');
135
+ if (snapshot !== key) {
136
+ snapshot = key;
137
+ setSlots(next);
138
+ }
139
+ position();
140
+ };
141
+ const schedule = () => {
142
+ if (!pending && !disposed) {
143
+ pending = true;
144
+ queueMicrotask(scan);
145
+ }
146
+ };
147
+ const shifts = new MutationObserver(position);
148
+ shifts.observe(frame, { attributes: true, attributeFilter: ['style'] });
149
+ const changes = new MutationObserver(schedule);
150
+ changes.observe(frame, { childList: true, subtree: true });
151
+ const unsubscribe = model.subscribe(schedule);
152
+ schedule();
153
+ return () => {
154
+ disposed = true;
155
+ unsubscribe();
156
+ changes.disconnect();
157
+ shifts.disconnect();
158
+ resize.disconnect();
159
+ style?.remove();
160
+ for (const port of ports.values()) {
161
+ port.rowElement.style.removeProperty('--adecore-tree-control-space');
162
+ port.rowElement.style.removeProperty('--adecore-tree-decoration-space');
163
+ port.control.remove();
164
+ port.decoration.remove();
165
+ port.lightControl.remove();
166
+ port.lightDecoration.remove();
167
+ }
168
+ };
169
+ }, [frame, model, label, prefix]);
170
+ return slots;
171
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * as ContextMenu from './context-menu/index.parts.ts';
2
+ export * as FileTree from './file-tree/index.parts.ts';
3
+ export * as Tree from './tree/index.parts.ts';
2
4
  export * as Dialog from './dialog/index.parts.ts';
3
5
  export * as KeyValueList from './key-value-list/index.parts.ts';
4
6
  export * as Menu from './menu/index.parts.ts';
@@ -76,3 +78,8 @@ export { useContentSize, type ContentSize } from './useContentSize.ts';
76
78
  export { useMeasuredWidth } from './useMeasuredWidth.ts';
77
79
  export { useNow, useTickingText } from './useNow.ts';
78
80
  export { ZOOM_PRESETS } from './zoom.ts';
81
+ export { useFileTree, type FileTreeOptions } from './file-tree/use-file-tree.ts';
82
+ export type { FileTreeRootProps } from './file-tree/parts.tsx';
83
+ export type { FileTree as FileTreeModel, FileTreeVisibleRow, FileTreeDragAndDropConfig, FileTreeDropContext, FileTreeDropResult } from '@pierre/trees';
84
+ export type { FoldKeyOf, SortRow } from './file-tree/helpers.ts';
85
+ export type { TreeRootProps, TreeRowProps, TreeChevronProps, TreeChevronSlotProps, TreeLabelProps, TreeDecorationProps, TreeControlProps, TreeCheckboxProps } from './tree/parts.tsx';
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@
3
3
  * `exports.test.ts` turns any change to this list into a diff someone has to accept.
4
4
  */
5
5
  export * as ContextMenu from './context-menu/index.parts.js';
6
+ export * as FileTree from './file-tree/index.parts.js';
7
+ export * as Tree from './tree/index.parts.js';
6
8
  export * as Dialog from './dialog/index.parts.js';
7
9
  export * as KeyValueList from './key-value-list/index.parts.js';
8
10
  export * as Menu from './menu/index.parts.js';
@@ -74,3 +76,4 @@ export { useContentSize } from './useContentSize.js';
74
76
  export { useMeasuredWidth } from './useMeasuredWidth.js';
75
77
  export { useNow, useTickingText } from './useNow.js';
76
78
  export { ZOOM_PRESETS } from './zoom.js';
79
+ export { useFileTree } from './file-tree/use-file-tree.js';
@@ -59,5 +59,10 @@
59
59
  "tabs": {
60
60
  "more": "More",
61
61
  "close": "Close tab"
62
+ },
63
+ "tree": {
64
+ "label": "Files",
65
+ "expand": "Expand",
66
+ "collapse": "Collapse"
62
67
  }
63
68
  }
@@ -59,5 +59,10 @@
59
59
  "tabs": {
60
60
  "more": "Meer",
61
61
  "close": "Tabblad sluiten"
62
+ },
63
+ "tree": {
64
+ "label": "Bestanden",
65
+ "expand": "Uitvouwen",
66
+ "collapse": "Invouwen"
62
67
  }
63
68
  }
@@ -9,6 +9,7 @@ import { useRender } from '@base-ui-components/react/use-render';
9
9
  import { withClass } from '../class-name.js';
10
10
  import { Icon } from '../Icon.js';
11
11
  import { Tooltip } from '../Tooltip.js';
12
+ import { renderPositioner } from '../popup-layer.js';
12
13
  const MenuKindContext = createContext('menu');
13
14
  /*
14
15
  * A menu hangs under its trigger, lined up with its start, unless there is no room. A context menu
@@ -45,7 +46,7 @@ export const ContextMenuTrigger = BaseContextMenu.Trigger;
45
46
  */
46
47
  export function MenuPopup({ side, align, sideOffset, alignOffset, collisionPadding, collisionBoundary, collisionAvoidance, anchor, sticky, positionMethod, className, ...props }) {
47
48
  const defaults = PLACEMENT[useContext(MenuKindContext)];
48
- return (_jsx(BaseMenu.Portal, { children: _jsx(BaseMenu.Positioner, { className: "popup-positioner", side: side ?? defaults.side, align: align ?? defaults.align, sideOffset: sideOffset ?? defaults.sideOffset, alignOffset: alignOffset ?? defaults.alignOffset, collisionPadding: collisionPadding, collisionBoundary: collisionBoundary, collisionAvoidance: collisionAvoidance, anchor: anchor, sticky: sticky, positionMethod: positionMethod, children: _jsx(BaseMenu.Popup, { className: withClass('menu-popup', className), ...props }) }) }));
49
+ return (_jsx(BaseMenu.Portal, { children: _jsx(BaseMenu.Positioner, { className: "popup-positioner", render: renderPositioner, side: side ?? defaults.side, align: align ?? defaults.align, sideOffset: sideOffset ?? defaults.sideOffset, alignOffset: alignOffset ?? defaults.alignOffset, collisionPadding: collisionPadding, collisionBoundary: collisionBoundary, collisionAvoidance: collisionAvoidance, anchor: anchor, sticky: sticky, positionMethod: positionMethod, children: _jsx(BaseMenu.Popup, { className: withClass('menu-popup', className), ...props }) }) }));
49
50
  }
50
51
  export function MenuItem({ unstyled = false, className, ...props }) {
51
52
  return _jsx(BaseMenu.Item, { className: unstyled ? className : withClass('menu-item', className), ...props });
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
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.js';
5
+ import { renderPositioner } from '../popup-layer.js';
5
6
  /*
6
7
  * The surface a popup is drawn on. `menu` is the padded card menus use, `picker` a card whose
7
8
  * content runs to its edges (a search field on top of a list), and `plain` leaves the surface to the caller.
@@ -18,7 +19,7 @@ export const PopoverTitle = BasePopover.Title;
18
19
  export const PopoverDescription = BasePopover.Description;
19
20
  /* The portal, the positioner and the popup of a popover: under its trigger and lined up with its start unless it says otherwise. */
20
21
  export function PopoverPopup({ variant = 'menu', side = 'bottom', align = 'start', sideOffset = 8, alignOffset, collisionPadding, collisionBoundary, collisionAvoidance, anchor, sticky, positionMethod, className, ...props }) {
21
- return (_jsx(BasePopover.Portal, { children: _jsx(BasePopover.Positioner, { className: "popup-positioner", side: side, align: align, sideOffset: sideOffset, alignOffset: alignOffset, collisionPadding: collisionPadding, collisionBoundary: collisionBoundary, collisionAvoidance: collisionAvoidance, anchor: anchor, sticky: sticky, positionMethod: positionMethod, children: _jsx(BasePopover.Popup, { className: withClass(POPUP_VARIANT[variant], className), ...props }) }) }));
22
+ return (_jsx(BasePopover.Portal, { children: _jsx(BasePopover.Positioner, { className: "popup-positioner", render: renderPositioner, side: side, align: align, sideOffset: sideOffset, alignOffset: alignOffset, collisionPadding: collisionPadding, collisionBoundary: collisionBoundary, collisionAvoidance: collisionAvoidance, anchor: anchor, sticky: sticky, positionMethod: positionMethod, children: _jsx(BasePopover.Popup, { className: withClass(POPUP_VARIANT[variant], className), ...props }) }) }));
22
23
  }
23
24
  export const PreviewCardRoot = BasePreviewCard.Root;
24
25
  export const PreviewCardTrigger = BasePreviewCard.Trigger;
@@ -0,0 +1,6 @@
1
+ import { type ComponentProps } from 'react';
2
+ export declare const renderPositioner: (props: ComponentProps<'div'>, state: {
3
+ open: boolean;
4
+ }) => import("react").FunctionComponentElement<import("react").ClassAttributes<HTMLDivElement> & import("react").HTMLAttributes<HTMLDivElement> & {
5
+ readonly open: boolean;
6
+ }>;
@@ -0,0 +1,30 @@
1
+ import { createElement, useLayoutEffect } from 'react';
2
+ // One attribute serves every open popup, so it stays until the last one closes.
3
+ const open = new Set();
4
+ /*
5
+ * Marks the root with `data-popup-open` while a popup is open, which the theme reads to turn the
6
+ * window's drag regions off. An attribute on the root instead of `:root:has(...)`, which made the
7
+ * browser restyle the whole page on every DOM insertion.
8
+ */
9
+ function usePopupLayer(isOpen) {
10
+ useLayoutEffect(() => {
11
+ if (!isOpen) {
12
+ return;
13
+ }
14
+ const self = Symbol('popup');
15
+ open.add(self);
16
+ document.documentElement.setAttribute('data-popup-open', '');
17
+ return () => {
18
+ open.delete(self);
19
+ if (open.size === 0) {
20
+ document.documentElement.removeAttribute('data-popup-open');
21
+ }
22
+ };
23
+ }, [isOpen]);
24
+ }
25
+ function LayerPositioner({ open: isOpen, ...props }) {
26
+ usePopupLayer(isOpen);
27
+ return createElement('div', props);
28
+ }
29
+ /* 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. */
30
+ export const renderPositioner = (props, state) => createElement(LayerPositioner, { ...props, open: state.open });
package/dist/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:has(.popup-positioner[data-open]) * { -webkit-app-region: no-drag; }
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
+ }
@@ -0,0 +1 @@
1
+ export { TreeRoot as Root, TreeRow as Row, TreeChevron as Chevron, TreeChevronSlot as ChevronSlot, TreeLabel as Label, TreeDecoration as Decoration, TreeControl as Control, TreeCheckbox as Checkbox } from './parts.tsx';
@@ -0,0 +1 @@
1
+ export { TreeRoot as Root, TreeRow as Row, TreeChevron as Chevron, TreeChevronSlot as ChevronSlot, TreeLabel as Label, TreeDecoration as Decoration, TreeControl as Control, TreeCheckbox as Checkbox } from './parts.js';
@@ -0,0 +1,27 @@
1
+ import { useRender } from '@base-ui-components/react/use-render';
2
+ import { type CheckboxProps } from '../Checkbox.tsx';
3
+ export type TreeRootProps = useRender.ComponentProps<'div'>;
4
+ export declare function TreeRoot({ render, className, ref, children, ...props }: TreeRootProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
5
+ export type TreeRowProps = useRender.ComponentProps<'div'> & {
6
+ level?: number;
7
+ selected?: boolean;
8
+ joinedStart?: boolean;
9
+ joinedEnd?: boolean;
10
+ interactive?: boolean;
11
+ };
12
+ export declare function TreeRow({ level, selected, joinedStart, joinedEnd, interactive, render, className, ref, style, children, ...props }: TreeRowProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
13
+ export type TreeChevronSlotProps = useRender.ComponentProps<'span'>;
14
+ export declare function TreeChevronSlot({ render, className, ref, ...props }: TreeChevronSlotProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
15
+ export type TreeChevronProps = useRender.ComponentProps<'button'> & {
16
+ expanded: boolean;
17
+ onExpandedChange?(expanded: boolean): void;
18
+ };
19
+ export declare function TreeChevron({ expanded, onExpandedChange, render, className, ref, onClick, onDoubleClick, ...props }: TreeChevronProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
20
+ export type TreeLabelProps = useRender.ComponentProps<'span'>;
21
+ export declare function TreeLabel({ render, className, ref, ...props }: TreeLabelProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
22
+ export type TreeDecorationProps = useRender.ComponentProps<'span'>;
23
+ export declare function TreeDecoration({ render, className, ref, ...props }: TreeDecorationProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
24
+ export type TreeControlProps = useRender.ComponentProps<'span'>;
25
+ export declare function TreeControl({ render, className, ref, onClick, onDoubleClick, onKeyDown, onPointerDown, ...props }: TreeControlProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
26
+ export type TreeCheckboxProps = CheckboxProps;
27
+ export declare function TreeCheckbox(props: TreeCheckboxProps): import("react").JSX.Element;