@adecore/ui 0.16.0 → 0.17.0-beta.1

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 (44) hide show
  1. package/dist/file-tree/helpers.d.ts +42 -0
  2. package/dist/file-tree/helpers.js +167 -0
  3. package/dist/file-tree/index.parts.d.ts +3 -0
  4. package/dist/file-tree/index.parts.js +3 -0
  5. package/dist/file-tree/parts.d.ts +20 -0
  6. package/dist/file-tree/parts.js +150 -0
  7. package/dist/file-tree/shift.d.ts +15 -0
  8. package/dist/file-tree/shift.js +35 -0
  9. package/dist/file-tree/style.d.ts +1 -0
  10. package/dist/file-tree/style.js +25 -0
  11. package/dist/file-tree/use-file-tree-shift.d.ts +6 -0
  12. package/dist/file-tree/use-file-tree-shift.js +143 -0
  13. package/dist/file-tree/use-file-tree.d.ts +5 -0
  14. package/dist/file-tree/use-file-tree.js +26 -0
  15. package/dist/file-tree/use-slots.d.ts +7 -0
  16. package/dist/file-tree/use-slots.js +171 -0
  17. package/dist/index.d.ts +7 -0
  18. package/dist/index.js +3 -0
  19. package/dist/locales/en.json +5 -0
  20. package/dist/locales/nl.json +5 -0
  21. package/dist/theme.css +103 -0
  22. package/dist/tree/index.parts.d.ts +1 -0
  23. package/dist/tree/index.parts.js +1 -0
  24. package/dist/tree/parts.d.ts +27 -0
  25. package/dist/tree/parts.js +113 -0
  26. package/dist/tree/style.d.ts +2 -0
  27. package/dist/tree/style.js +29 -0
  28. package/package.json +6 -4
  29. package/src/file-tree/LICENSE +110 -0
  30. package/src/file-tree/helpers.ts +204 -0
  31. package/src/file-tree/index.parts.ts +25 -0
  32. package/src/file-tree/parts.tsx +206 -0
  33. package/src/file-tree/shift.ts +49 -0
  34. package/src/file-tree/style.ts +26 -0
  35. package/src/file-tree/use-file-tree-shift.tsx +170 -0
  36. package/src/file-tree/use-file-tree.ts +31 -0
  37. package/src/file-tree/use-slots.ts +191 -0
  38. package/src/index.ts +17 -0
  39. package/src/locales/en.json +5 -0
  40. package/src/locales/nl.json +5 -0
  41. package/src/theme.css +103 -0
  42. package/src/tree/index.parts.ts +10 -0
  43. package/src/tree/parts.tsx +170 -0
  44. package/src/tree/style.ts +30 -0
@@ -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';
@@ -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
  }
package/src/theme.css CHANGED
@@ -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,10 @@
1
+ export {
2
+ TreeRoot as Root,
3
+ TreeRow as Row,
4
+ TreeChevron as Chevron,
5
+ TreeChevronSlot as ChevronSlot,
6
+ TreeLabel as Label,
7
+ TreeDecoration as Decoration,
8
+ TreeControl as Control,
9
+ TreeCheckbox as Checkbox
10
+ } from './parts.tsx';
@@ -0,0 +1,170 @@
1
+ import type { MouseEvent, KeyboardEvent, PointerEvent } from 'react';
2
+ import clsx from 'clsx';
3
+ import { useRender } from '@base-ui-components/react/use-render';
4
+ import { ChevronRight } from 'lucide-react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Checkbox as UICheckbox, type CheckboxProps } from '../Checkbox.tsx';
7
+ import { Icon } from '../Icon.tsx';
8
+ import { treeRowStyle } from './style.ts';
9
+
10
+ export type TreeRootProps = useRender.ComponentProps<'div'>;
11
+
12
+ export function TreeRoot({ render, className, ref, children, ...props }: TreeRootProps) {
13
+ return useRender({
14
+ render,
15
+ ref,
16
+ defaultTagName: 'div',
17
+ props: {
18
+ role: 'tree',
19
+ ...props,
20
+ className: clsx('adecore-tree', className),
21
+ children: (
22
+ <>
23
+ <style>{treeRowStyle('.adecore-tree-row')}</style>
24
+ {children}
25
+ </>
26
+ )
27
+ }
28
+ });
29
+ }
30
+
31
+ export type TreeRowProps = useRender.ComponentProps<'div'> & {
32
+ level?: number;
33
+ selected?: boolean;
34
+ joinedStart?: boolean;
35
+ joinedEnd?: boolean;
36
+ interactive?: boolean;
37
+ };
38
+
39
+ export function TreeRow({
40
+ level = 1,
41
+ selected = false,
42
+ joinedStart = false,
43
+ joinedEnd = false,
44
+ interactive = true,
45
+ render,
46
+ className,
47
+ ref,
48
+ style,
49
+ children,
50
+ ...props
51
+ }: TreeRowProps) {
52
+ return useRender({
53
+ render,
54
+ ref,
55
+ defaultTagName: 'div',
56
+ props: {
57
+ role: 'treeitem',
58
+ 'aria-level': level,
59
+ 'aria-selected': selected,
60
+ ...props,
61
+ 'data-selected': selected,
62
+ 'data-joined-start': joinedStart,
63
+ 'data-joined-end': joinedEnd,
64
+ 'data-interactive': interactive,
65
+ className: clsx('adecore-tree-row', className),
66
+ style: { paddingInlineStart: 4 + (Math.max(1, level) - 1) * 16, ...style },
67
+ children: (
68
+ <>
69
+ {Array.from({ length: Math.max(0, level - 1) }, (_, index) => (
70
+ <span key={index} aria-hidden className="adecore-tree-guide" style={{ insetInlineStart: 11 + index * 16 }} />
71
+ ))}
72
+ {children}
73
+ </>
74
+ )
75
+ }
76
+ });
77
+ }
78
+
79
+ export type TreeChevronSlotProps = useRender.ComponentProps<'span'>;
80
+
81
+ export function TreeChevronSlot({ render, className, ref, ...props }: TreeChevronSlotProps) {
82
+ return useRender({
83
+ render,
84
+ ref,
85
+ defaultTagName: 'span',
86
+ props: { 'aria-hidden': true, ...props, className: clsx('adecore-tree-chevron-slot', className) }
87
+ });
88
+ }
89
+
90
+ export type TreeChevronProps = useRender.ComponentProps<'button'> & {
91
+ expanded: boolean;
92
+ onExpandedChange?(expanded: boolean): void;
93
+ };
94
+
95
+ export function TreeChevron({ expanded, onExpandedChange, render, className, ref, onClick, onDoubleClick, ...props }: TreeChevronProps) {
96
+ const { t } = useTranslation('ui');
97
+ return useRender({
98
+ render,
99
+ ref,
100
+ defaultTagName: 'button',
101
+ props: {
102
+ type: 'button',
103
+ tabIndex: -1,
104
+ 'aria-label': t(expanded ? 'tree.collapse' : 'tree.expand'),
105
+ ...props,
106
+ className: clsx('adecore-tree-chevron-slot', className),
107
+ onClick: (event: MouseEvent<HTMLButtonElement>) => {
108
+ event.stopPropagation();
109
+ onClick?.(event);
110
+ if (!event.defaultPrevented) {
111
+ onExpandedChange?.(!expanded);
112
+ }
113
+ },
114
+ onDoubleClick: (event: MouseEvent<HTMLButtonElement>) => {
115
+ event.stopPropagation();
116
+ onDoubleClick?.(event);
117
+ },
118
+ children: <Icon icon={ChevronRight} size={12} className={expanded ? 'rotate-90' : undefined} />
119
+ }
120
+ });
121
+ }
122
+
123
+ export type TreeLabelProps = useRender.ComponentProps<'span'>;
124
+ export function TreeLabel({ render, className, ref, ...props }: TreeLabelProps) {
125
+ return useRender({ render, ref, defaultTagName: 'span', props: { ...props, className: clsx('adecore-tree-label', className) } });
126
+ }
127
+
128
+ export type TreeDecorationProps = useRender.ComponentProps<'span'>;
129
+ export function TreeDecoration({ render, className, ref, ...props }: TreeDecorationProps) {
130
+ return useRender({ render, ref, defaultTagName: 'span', props: { ...props, className: clsx('adecore-tree-decoration', className) } });
131
+ }
132
+
133
+ export type TreeControlProps = useRender.ComponentProps<'span'>;
134
+ export function TreeControl({ render, className, ref, onClick, onDoubleClick, onKeyDown, onPointerDown, ...props }: TreeControlProps) {
135
+ return useRender({
136
+ render,
137
+ ref,
138
+ defaultTagName: 'span',
139
+ props: {
140
+ ...props,
141
+ 'data-tree-control': true,
142
+ className: clsx('adecore-tree-control', className),
143
+ onClick: (event: MouseEvent<HTMLSpanElement>) => {
144
+ event.stopPropagation();
145
+ onClick?.(event);
146
+ },
147
+ onDoubleClick: (event: MouseEvent<HTMLSpanElement>) => {
148
+ event.stopPropagation();
149
+ onDoubleClick?.(event);
150
+ },
151
+ onKeyDown: (event: KeyboardEvent<HTMLSpanElement>) => {
152
+ event.stopPropagation();
153
+ onKeyDown?.(event);
154
+ },
155
+ onPointerDown: (event: PointerEvent<HTMLSpanElement>) => {
156
+ event.stopPropagation();
157
+ onPointerDown?.(event);
158
+ }
159
+ }
160
+ });
161
+ }
162
+
163
+ export type TreeCheckboxProps = CheckboxProps;
164
+ export function TreeCheckbox(props: TreeCheckboxProps) {
165
+ return (
166
+ <TreeControl>
167
+ <UICheckbox {...props} />
168
+ </TreeControl>
169
+ );
170
+ }
@@ -0,0 +1,30 @@
1
+ export const TREE_ROW_HEIGHT = 25;
2
+
3
+ export function treeRowStyle(selector: string, selected = 'data-selected'): string {
4
+ return `
5
+ ${selector} {
6
+ box-sizing: border-box;
7
+ display: flex;
8
+ position: relative;
9
+ align-items: center;
10
+ height: var(--tree-row-height, 25px);
11
+ min-width: 0;
12
+ gap: var(--tree-row-gap, 6px);
13
+ border: 0;
14
+ border-radius: var(--tree-row-radius, 6px);
15
+ background: transparent;
16
+ background-clip: padding-box;
17
+ color: var(--text);
18
+ font-family: var(--font-sans);
19
+ font-size: var(--text-xs);
20
+ line-height: var(--tree-row-height, 25px);
21
+ text-align: start;
22
+ outline: none;
23
+ }
24
+ ${selector}[data-interactive="true"]:hover { background: var(--surface-hover); }
25
+ ${selector}[${selected}="true"] { background: var(--surface-active); color: var(--text); }
26
+ ${selector}[data-joined-start="true"] { border-start-start-radius: 0; border-start-end-radius: 0; }
27
+ ${selector}[data-joined-end="true"] { border-end-start-radius: 0; border-end-end-radius: 0; }
28
+ ${selector}:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
29
+ `;
30
+ }