@adecore/ui 0.16.0-beta.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,42 @@
1
+ import type { FileTree, FileTreeDirectoryHandle, FileTreeVisibleRow } from '@pierre/trees';
2
+ export interface SortRow {
3
+ isDirectory: boolean;
4
+ segments: readonly string[];
5
+ }
6
+ export declare function directoryHandle(model: FileTree, path: string): FileTreeDirectoryHandle | null;
7
+ export declare function pathOfRow(row: FileTreeVisibleRow): string;
8
+ export declare function visibleRows(model: FileTree): FileTreeVisibleRow[];
9
+ export declare function applyExpansion(model: FileTree, collapsed: ReadonlySet<string>, keyOf?: FoldKeyOf): void;
10
+ export declare function resetExpandedPaths(model: FileTree, paths: readonly string[], expanded: ReadonlySet<string>): void;
11
+ export declare function rowPathOf(event: {
12
+ nativeEvent: Event;
13
+ }): string | null;
14
+ export declare function menuTargetsOf(row: string, selected: readonly string[]): string[];
15
+ export declare function extendsSelection(event: {
16
+ shiftKey: boolean;
17
+ metaKey: boolean;
18
+ ctrlKey: boolean;
19
+ }): boolean;
20
+ export declare function movesFocus(event: {
21
+ key: string;
22
+ shiftKey: boolean;
23
+ metaKey: boolean;
24
+ ctrlKey: boolean;
25
+ altKey: boolean;
26
+ }): boolean;
27
+ export declare function selectOnly(model: FileTree, path: string | null): void;
28
+ export declare function followFocus(model: FileTree, onMoved?: (path: string) => void): () => void;
29
+ export declare function focusRow(model: FileTree, path: string): boolean;
30
+ export declare function ancestorDirsOf(treePath: string): string[];
31
+ export declare function withoutClosedBranches(open: ReadonlySet<string>, known: ReadonlySet<string>): Set<string>;
32
+ export declare function newlyExpanded(before: ReadonlySet<string>, after: ReadonlySet<string>): string[];
33
+ export declare function mergeExpanded(remembered: ReadonlySet<string>, reported: ReadonlySet<string>, known: ReadonlySet<string>): Set<string>;
34
+ export declare function compareRows(left: SortRow, right: SortRow): number;
35
+ export declare function dirPathOf(rowPath: string): string;
36
+ export type FoldKeyOf = (rowPath: string) => string | null;
37
+ export declare function collapsedPathsOf(rows: readonly FileTreeVisibleRow[], keyOf?: FoldKeyOf): string[];
38
+ export declare function mergeCollapsedPaths(current: string[], rows: readonly FileTreeVisibleRow[], keyOf?: FoldKeyOf): string[];
39
+ export declare function expansionChanges(rows: readonly FileTreeVisibleRow[], collapsed: ReadonlySet<string>, keyOf?: FoldKeyOf): {
40
+ collapse: string[];
41
+ expand: string[];
42
+ };
@@ -0,0 +1,167 @@
1
+ // Expanded folders can reveal more flattened rows; cap custom-key cycles.
2
+ const EXPANSION_PASSES = 32;
3
+ export function directoryHandle(model, path) {
4
+ const item = model.getItem(path);
5
+ return item?.isDirectory() ? item : null;
6
+ }
7
+ export function pathOfRow(row) {
8
+ return row.isFlattened ? (row.flattenedSegments?.findLast((segment) => segment.isTerminal)?.path ?? row.path) : row.path;
9
+ }
10
+ export function visibleRows(model) {
11
+ return model.getVisibleRows(0, model.getVisibleCount()).map((row) => ({ ...row, path: pathOfRow(row) }));
12
+ }
13
+ export function applyExpansion(model, collapsed, keyOf) {
14
+ for (let pass = 0; pass < EXPANSION_PASSES; pass++) {
15
+ const { collapse, expand } = expansionChanges(visibleRows(model), collapsed, keyOf);
16
+ if (collapse.length === 0 && expand.length === 0) {
17
+ return;
18
+ }
19
+ for (const path of collapse) {
20
+ directoryHandle(model, path)?.collapse();
21
+ }
22
+ for (const path of expand) {
23
+ directoryHandle(model, path)?.expand();
24
+ }
25
+ }
26
+ }
27
+ export function resetExpandedPaths(model, paths, expanded) {
28
+ model.resetPaths(paths, { initialExpandedPaths: [...expanded] });
29
+ // @pierre/trees beta.6 restores expansion with its default sort, so custom sorting needs a second pass.
30
+ for (const path of expanded) {
31
+ directoryHandle(model, path)?.expand();
32
+ }
33
+ }
34
+ export function rowPathOf(event) {
35
+ for (const node of event.nativeEvent.composedPath()) {
36
+ const path = node instanceof HTMLElement ? node.dataset.itemPath : undefined;
37
+ if (path) {
38
+ return path;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+ export function menuTargetsOf(row, selected) {
44
+ return selected.includes(row) && selected.length > 1 ? [...selected] : [row];
45
+ }
46
+ export function extendsSelection(event) {
47
+ return event.shiftKey || event.metaKey || event.ctrlKey;
48
+ }
49
+ const FOCUS_KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End']);
50
+ export function movesFocus(event) {
51
+ return FOCUS_KEYS.has(event.key) && !event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey;
52
+ }
53
+ export function selectOnly(model, path) {
54
+ for (const selected of model.getSelectedPaths()) {
55
+ if (selected !== path) {
56
+ model.getItem(selected)?.deselect();
57
+ }
58
+ }
59
+ const item = path === null ? null : model.getItem(path);
60
+ if (item && !item.isSelected()) {
61
+ item.select();
62
+ }
63
+ }
64
+ export function followFocus(model, onMoved) {
65
+ const from = model.getFocusedPath();
66
+ const timer = window.setTimeout(() => {
67
+ const path = model.getFocusedPath();
68
+ selectOnly(model, path);
69
+ if (path !== null && path !== from) {
70
+ onMoved?.(path);
71
+ }
72
+ }, 0);
73
+ return () => window.clearTimeout(timer);
74
+ }
75
+ export function focusRow(model, path) {
76
+ const selector = `[data-type="item"][data-item-path="${CSS.escape(path)}"]:not([data-item-parked="true"])`;
77
+ const row = model.getFileTreeContainer()?.shadowRoot?.querySelector(selector) ?? null;
78
+ row?.focus();
79
+ return row !== null;
80
+ }
81
+ export function ancestorDirsOf(treePath) {
82
+ const segments = treePath.split('/').filter((segment) => segment !== '');
83
+ segments.pop();
84
+ const dirs = [];
85
+ let prefix = '';
86
+ for (const segment of segments) {
87
+ prefix = `${prefix}${segment}/`;
88
+ dirs.push(prefix);
89
+ }
90
+ return dirs;
91
+ }
92
+ export function withoutClosedBranches(open, known) {
93
+ return new Set([...open].filter((path) => ancestorDirsOf(path).every((dir) => open.has(dir) || !known.has(dir))));
94
+ }
95
+ export function newlyExpanded(before, after) {
96
+ return [...after].filter((path) => !before.has(path));
97
+ }
98
+ export function mergeExpanded(remembered, reported, known) {
99
+ const merged = new Set(reported);
100
+ for (const path of remembered) {
101
+ if (!known.has(path)) {
102
+ merged.add(path);
103
+ }
104
+ }
105
+ return merged;
106
+ }
107
+ export function compareRows(left, right) {
108
+ const shared = Math.min(left.segments.length, right.segments.length);
109
+ for (let i = 0; i < shared; i++) {
110
+ const leftSegment = left.segments[i];
111
+ const rightSegment = right.segments[i];
112
+ if (leftSegment === rightSegment) {
113
+ continue;
114
+ }
115
+ const leftIsDirectory = left.isDirectory || i < left.segments.length - 1;
116
+ const rightIsDirectory = right.isDirectory || i < right.segments.length - 1;
117
+ if (leftIsDirectory !== rightIsDirectory) {
118
+ return leftIsDirectory ? -1 : 1;
119
+ }
120
+ // Case is not a reason to split two names apart; the raw compare only breaks a tie.
121
+ return leftSegment.localeCompare(rightSegment, undefined, { numeric: true, sensitivity: 'base' }) || leftSegment.localeCompare(rightSegment);
122
+ }
123
+ // One path is the head of the other, so the shorter one is the directory the longer one sits in.
124
+ return left.segments.length - right.segments.length;
125
+ }
126
+ export function dirPathOf(rowPath) {
127
+ return rowPath.endsWith('/') ? rowPath.slice(0, -1) : rowPath;
128
+ }
129
+ function plainKey(rowPath) {
130
+ return dirPathOf(rowPath);
131
+ }
132
+ export function collapsedPathsOf(rows, keyOf = plainKey) {
133
+ return rows.flatMap((row) => {
134
+ const key = row.kind === 'directory' && !row.isExpanded ? keyOf(row.path) : null;
135
+ return key === null ? [] : [key];
136
+ });
137
+ }
138
+ export function mergeCollapsedPaths(current, rows, keyOf = plainKey) {
139
+ const known = new Set(rows.flatMap((row) => (row.kind === 'directory' ? [keyOf(row.path)] : [])));
140
+ const folded = new Set(collapsedPathsOf(rows, keyOf));
141
+ const next = current.filter((dir) => !known.has(dir) || folded.has(dir));
142
+ const existing = new Set(current);
143
+ for (const dir of folded) {
144
+ if (!existing.has(dir)) {
145
+ next.push(dir);
146
+ }
147
+ }
148
+ // Selection also notifies subscribers; unchanged folds must not restart the shared-state cycle.
149
+ return next.length === current.length && next.every((dir, index) => dir === current[index]) ? current : next;
150
+ }
151
+ export function expansionChanges(rows, collapsed, keyOf = plainKey) {
152
+ const changes = { collapse: [], expand: [] };
153
+ for (const row of rows) {
154
+ const key = row.kind === 'directory' ? keyOf(row.path) : null;
155
+ if (key === null) {
156
+ continue;
157
+ }
158
+ const folded = collapsed.has(key);
159
+ if (folded && row.isExpanded) {
160
+ changes.collapse.push(row.path);
161
+ }
162
+ if (!folded && !row.isExpanded) {
163
+ changes.expand.push(row.path);
164
+ }
165
+ }
166
+ return changes;
167
+ }
@@ -0,0 +1,3 @@
1
+ export { FileTreeRoot as Root } from './parts.tsx';
2
+ export { TreeCheckbox as Checkbox, TreeControl as Control, TreeDecoration as Decoration } from '../tree/parts.tsx';
3
+ export { ancestorDirsOf, applyExpansion, collapsedPathsOf, compareRows, directoryHandle, dirPathOf, expansionChanges, extendsSelection, focusRow, followFocus, menuTargetsOf, mergeCollapsedPaths, mergeExpanded, movesFocus, newlyExpanded, pathOfRow, resetExpandedPaths, rowPathOf, selectOnly, visibleRows, withoutClosedBranches } from './helpers.ts';
@@ -0,0 +1,3 @@
1
+ export { FileTreeRoot as Root } from './parts.js';
2
+ export { TreeCheckbox as Checkbox, TreeControl as Control, TreeDecoration as Decoration } from '../tree/parts.js';
3
+ export { ancestorDirsOf, applyExpansion, collapsedPathsOf, compareRows, directoryHandle, dirPathOf, expansionChanges, extendsSelection, focusRow, followFocus, menuTargetsOf, mergeCollapsedPaths, mergeExpanded, movesFocus, newlyExpanded, pathOfRow, resetExpandedPaths, rowPathOf, selectOnly, visibleRows, withoutClosedBranches } from './helpers.js';
@@ -0,0 +1,20 @@
1
+ import { type ReactNode } from 'react';
2
+ import { useRender } from '@base-ui-components/react/use-render';
3
+ import { type FileTreeProps as PierreProps } from '@pierre/trees/react';
4
+ import type { FileTree, FileTreeVisibleRow } from '@pierre/trees';
5
+ export type FileTreeRootProps = useRender.ComponentProps<'div'> & {
6
+ model: FileTree;
7
+ label?: string;
8
+ resetKey?: string;
9
+ selectionFollowsFocus?: boolean;
10
+ onActivate?(path: string): void;
11
+ onFocusMove?(path: string): void;
12
+ onLoadChildren?(path: string): void;
13
+ onRowContextMenu?(path: string, targets: readonly string[], event: MouseEvent): void;
14
+ onRowDragStart?(path: string, targets: readonly string[], event: DragEvent): void;
15
+ onExpandedPathsChange?(paths: readonly string[]): void;
16
+ renderControl?(row: FileTreeVisibleRow): ReactNode;
17
+ renderDecoration?(row: FileTreeVisibleRow): ReactNode;
18
+ treeProps?: Omit<PierreProps, 'model'>;
19
+ };
20
+ export declare function FileTreeRoot({ model, label, resetKey, selectionFollowsFocus, onActivate, onFocusMove, onLoadChildren, onExpandedPathsChange, onRowContextMenu, onRowDragStart, renderControl, renderDecoration, treeProps, render, className, ref, children, onClick, onKeyDownCapture, ...props }: FileTreeRootProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,150 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { useRender } from '@base-ui-components/react/use-render';
5
+ import { FileTree as PierreFileTree } from '@pierre/trees/react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import clsx from 'clsx';
8
+ import { mergeRefs } from '../merge-refs.js';
9
+ import { directoryHandle, extendsSelection, followFocus, movesFocus, menuTargetsOf, rowPathOf, visibleRows } from './helpers.js';
10
+ import { useFileTreeShift } from './use-file-tree-shift.js';
11
+ import { useFileTreeSlots } from './use-slots.js';
12
+ function inSlot(event) {
13
+ return event.nativeEvent.composedPath().some((node) => node instanceof HTMLElement && node.hasAttribute('data-tree-slot'));
14
+ }
15
+ export function FileTreeRoot({ model, label, resetKey, selectionFollowsFocus = true, onActivate, onFocusMove, onLoadChildren, onExpandedPathsChange, onRowContextMenu, onRowDragStart, renderControl, renderDecoration, treeProps, render, className, ref, children, onClick, onKeyDownCapture, ...props }) {
16
+ const { t } = useTranslation('ui');
17
+ const [frame, setFrame] = useState(null);
18
+ const { attach, bar } = useFileTreeShift(model, resetKey);
19
+ const frameRef = useCallback((node) => {
20
+ setFrame(node);
21
+ attach(node);
22
+ mergeRefs(ref)(node);
23
+ }, [ref, attach]);
24
+ const slots = useFileTreeSlots(model, frame, label ?? t('tree.label'));
25
+ const latestLoads = useRef({ onLoadChildren, onExpandedPathsChange });
26
+ useLayoutEffect(() => {
27
+ latestLoads.current = { onLoadChildren, onExpandedPathsChange };
28
+ }, [onLoadChildren, onExpandedPathsChange]);
29
+ const cancelFocus = useRef(undefined);
30
+ useEffect(() => () => cancelFocus.current?.(), [model]);
31
+ useEffect(() => {
32
+ let previous = new Set();
33
+ let disposed = false;
34
+ let pending = false;
35
+ const report = () => {
36
+ pending = false;
37
+ if (disposed) {
38
+ return;
39
+ }
40
+ const rows = visibleRows(model).filter((row) => row.kind === 'directory');
41
+ const known = new Set(rows.map((row) => row.path));
42
+ const expanded = new Set([...previous].filter((path) => !known.has(path)));
43
+ for (const row of rows) {
44
+ if (row.isExpanded) {
45
+ expanded.add(row.path);
46
+ }
47
+ }
48
+ const opened = [...expanded].filter((path) => !previous.has(path));
49
+ const changed = opened.length > 0 || [...previous].some((path) => !expanded.has(path));
50
+ previous = expanded;
51
+ if (changed) {
52
+ latestLoads.current.onExpandedPathsChange?.([...expanded]);
53
+ }
54
+ for (const path of opened) {
55
+ latestLoads.current.onLoadChildren?.(path);
56
+ }
57
+ };
58
+ const schedule = () => {
59
+ if (!pending) {
60
+ pending = true;
61
+ queueMicrotask(report);
62
+ }
63
+ };
64
+ const unsubscribe = model.subscribe(schedule);
65
+ report();
66
+ return () => {
67
+ disposed = true;
68
+ unsubscribe();
69
+ };
70
+ }, [model]);
71
+ useEffect(() => {
72
+ if (frame === null) {
73
+ return;
74
+ }
75
+ const onClickRow = (event) => {
76
+ if (event.defaultPrevented || inSlot({ nativeEvent: event }) || extendsSelection(event)) {
77
+ return;
78
+ }
79
+ const path = rowPathOf({ nativeEvent: event });
80
+ if (path !== null && directoryHandle(model, path) === null) {
81
+ onActivate?.(path);
82
+ }
83
+ };
84
+ const onKey = (event) => {
85
+ if (event.defaultPrevented || inSlot({ nativeEvent: event })) {
86
+ return;
87
+ }
88
+ if (movesFocus(event) && selectionFollowsFocus) {
89
+ cancelFocus.current?.();
90
+ // beta.6 stops the key in its own handler; selection must follow after that handler moves focus.
91
+ cancelFocus.current = followFocus(model, onFocusMove);
92
+ }
93
+ else if (event.key === 'Enter' && !event.altKey && !event.metaKey && !event.ctrlKey) {
94
+ const path = model.getFocusedPath();
95
+ if (path === null) {
96
+ return;
97
+ }
98
+ event.preventDefault();
99
+ event.stopPropagation();
100
+ const directory = directoryHandle(model, path);
101
+ if (directory) {
102
+ directory.toggle();
103
+ }
104
+ else {
105
+ onActivate?.(path);
106
+ }
107
+ }
108
+ };
109
+ const onContext = (event) => {
110
+ if (inSlot({ nativeEvent: event })) {
111
+ return;
112
+ }
113
+ const path = rowPathOf({ nativeEvent: event });
114
+ if (path !== null) {
115
+ onRowContextMenu?.(path, menuTargetsOf(path, model.getSelectedPaths()), event);
116
+ }
117
+ };
118
+ const onDrag = (event) => {
119
+ if (inSlot({ nativeEvent: event })) {
120
+ return;
121
+ }
122
+ const path = rowPathOf({ nativeEvent: event });
123
+ if (path !== null) {
124
+ onRowDragStart?.(path, menuTargetsOf(path, model.getSelectedPaths()), event);
125
+ }
126
+ };
127
+ frame.addEventListener('click', onClickRow);
128
+ frame.addEventListener('keydown', onKey, true);
129
+ frame.addEventListener('contextmenu', onContext);
130
+ frame.addEventListener('dragstart', onDrag);
131
+ return () => {
132
+ frame.removeEventListener('click', onClickRow);
133
+ frame.removeEventListener('keydown', onKey, true);
134
+ frame.removeEventListener('contextmenu', onContext);
135
+ frame.removeEventListener('dragstart', onDrag);
136
+ };
137
+ }, [frame, model, selectionFollowsFocus, onActivate, onFocusMove, onRowContextMenu, onRowDragStart]);
138
+ return useRender({
139
+ render,
140
+ ref: frameRef,
141
+ defaultTagName: 'div',
142
+ props: {
143
+ ...props,
144
+ className: clsx('adecore-tree adecore-file-tree', className),
145
+ onClick,
146
+ onKeyDownCapture,
147
+ children: (_jsxs(_Fragment, { children: [_jsx(PierreFileTree, { ...treeProps, model: model, className: clsx('adecore-file-tree-host', treeProps?.className) }), slots.map(({ row, control, decoration }) => (_jsxs("span", { style: { display: 'contents' }, children: [createPortal(renderControl?.(row), control), createPortal(renderDecoration?.(row), decoration)] }, control.slot))), children, bar] }))
148
+ }
149
+ });
150
+ }
@@ -0,0 +1,15 @@
1
+ export declare const SHIFT_PROPERTY = "--adecore-tree-shift";
2
+ export declare function clampShift(value: number, max: number): number;
3
+ export declare function shiftNeed(textRight: number, limit: number, shift: number): number;
4
+ export declare function maxShift(needs: readonly number[]): number;
5
+ export interface WheelStep {
6
+ readonly deltaX: number;
7
+ readonly deltaY: number;
8
+ readonly deltaMode: number;
9
+ readonly shiftKey: boolean;
10
+ }
11
+ export declare function sidewaysDelta(step: WheelStep, pageWidth: number): number;
12
+ export declare function shiftThumb(shift: number, max: number, track: number, view: number): {
13
+ left: number;
14
+ width: number;
15
+ };
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: FSL-1.1-MIT
2
+ export const SHIFT_PROPERTY = '--adecore-tree-shift';
3
+ const LINE_WIDTH = 16;
4
+ const MIN_THUMB = 24;
5
+ export function clampShift(value, max) {
6
+ return Math.min(Math.max(value, 0), Math.max(max, 0));
7
+ }
8
+ export function shiftNeed(textRight, limit, shift) {
9
+ return Math.max(0, Math.ceil(textRight + shift - limit));
10
+ }
11
+ export function maxShift(needs) {
12
+ return needs.reduce((max, need) => Math.max(max, need), 0);
13
+ }
14
+ export function sidewaysDelta(step, pageWidth) {
15
+ const swapped = step.shiftKey && step.deltaX === 0;
16
+ const sideways = swapped ? step.deltaY : step.deltaX;
17
+ const across = swapped ? 0 : step.deltaY;
18
+ if (sideways === 0 || Math.abs(sideways) <= Math.abs(across)) {
19
+ return 0;
20
+ }
21
+ if (step.deltaMode === 1) {
22
+ return sideways * LINE_WIDTH;
23
+ }
24
+ if (step.deltaMode === 2) {
25
+ return sideways * pageWidth;
26
+ }
27
+ return sideways;
28
+ }
29
+ export function shiftThumb(shift, max, track, view) {
30
+ if (max <= 0 || track <= 0) {
31
+ return { left: 0, width: Math.max(track, 0) };
32
+ }
33
+ const width = Math.min(track, Math.max(MIN_THUMB, Math.round((track * view) / (view + max))));
34
+ return { left: Math.round(((track - width) * clampShift(shift, max)) / max), width };
35
+ }
@@ -0,0 +1 @@
1
+ export declare const FILE_TREE_CSS: string;
@@ -0,0 +1,25 @@
1
+ // SPDX-License-Identifier: FSL-1.1-MIT
2
+ import { treeRowStyle } from '../tree/style.js';
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,6 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { FileTree } from '@pierre/trees';
3
+ export declare function useFileTreeShift(model: FileTree, resetKey?: string): {
4
+ attach(node: HTMLElement | null): void;
5
+ bar: ReactElement;
6
+ };
@@ -0,0 +1,143 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // SPDX-License-Identifier: FSL-1.1-MIT
3
+ import { useEffect, useState } from 'react';
4
+ import { clampShift, maxShift, SHIFT_PROPERTY, shiftNeed, shiftThumb, sidewaysDelta } from './shift.js';
5
+ const ROWS = '[data-type="item"]:not([data-item-parked="true"])';
6
+ const HIDE_AFTER = 900;
7
+ const NEAR_BOTTOM = 16;
8
+ function boxesOf(element) {
9
+ return element.getClientRects().length > 0 ? [element] : [...element.children].flatMap((child) => boxesOf(child));
10
+ }
11
+ function limitOf(row, content, gap) {
12
+ const start = content.getBoundingClientRect().left;
13
+ let limit = Number.POSITIVE_INFINITY;
14
+ for (const part of [...row.children].filter((child) => child !== content).flatMap((child) => boxesOf(child))) {
15
+ const rect = part.getBoundingClientRect();
16
+ if (rect.width === 0 || rect.left < start) {
17
+ continue;
18
+ }
19
+ if (part.childNodes.length === 0 && Number.parseFloat(getComputedStyle(part).flexGrow) > 0) {
20
+ continue;
21
+ }
22
+ limit = Math.min(limit, rect.left - gap);
23
+ }
24
+ if (limit !== Number.POSITIVE_INFINITY) {
25
+ return limit - (Number.parseFloat(getComputedStyle(content).marginInlineEnd) || 0);
26
+ }
27
+ return (row.getBoundingClientRect().right -
28
+ Number.parseFloat(getComputedStyle(row).paddingInlineEnd) -
29
+ (Number.parseFloat(getComputedStyle(content).marginInlineEnd) || 0));
30
+ }
31
+ function needOf(row, shift, gap, range) {
32
+ const content = row.querySelector(':scope > [data-item-section="content"]');
33
+ if (content === null || row.getBoundingClientRect().width === 0) {
34
+ return null;
35
+ }
36
+ range.selectNodeContents(content);
37
+ return shiftNeed(range.getBoundingClientRect().right, limitOf(row, content, gap), shift);
38
+ }
39
+ export function useFileTreeShift(model, resetKey) {
40
+ const [frame, setFrame] = useState(null);
41
+ const [track, setTrack] = useState(null);
42
+ const [thumb, setThumb] = useState(null);
43
+ useEffect(() => {
44
+ if (frame === null || track === null || thumb === null) {
45
+ return;
46
+ }
47
+ const range = document.createRange();
48
+ let root = null;
49
+ let exact = 0;
50
+ let drawn = 0;
51
+ let max = 0;
52
+ let pending = 0;
53
+ let hideTimer = 0;
54
+ let scrolling = false;
55
+ let near = false;
56
+ const draw = () => {
57
+ const shift = Math.round(exact);
58
+ if (shift !== drawn) {
59
+ drawn = shift;
60
+ frame.style.setProperty(SHIFT_PROPERTY, `${shift}px`);
61
+ }
62
+ const { left, width } = shiftThumb(shift, max, track.clientWidth, frame.clientWidth);
63
+ thumb.style.left = `${left}px`;
64
+ thumb.style.width = `${width}px`;
65
+ track.toggleAttribute('data-visible', max > 0 && (scrolling || near));
66
+ };
67
+ const measure = () => {
68
+ pending = 0;
69
+ if (root === null) {
70
+ root = model.getFileTreeContainer()?.shadowRoot ?? null;
71
+ if (root === null) {
72
+ return;
73
+ }
74
+ rows.observe(root, { childList: true, subtree: true, characterData: true });
75
+ }
76
+ const drawnRows = [...root.querySelectorAll(ROWS)];
77
+ const gap = drawnRows.length === 0 ? 0 : Number.parseFloat(getComputedStyle(drawnRows[0]).columnGap) || 0;
78
+ max = maxShift(drawnRows.map((row) => needOf(row, drawn, gap, range)).filter((need) => need !== null));
79
+ exact = clampShift(exact, max);
80
+ draw();
81
+ };
82
+ const schedule = () => {
83
+ if (pending === 0) {
84
+ pending = requestAnimationFrame(measure);
85
+ }
86
+ };
87
+ const rows = new MutationObserver(schedule);
88
+ const mounts = new MutationObserver(schedule);
89
+ mounts.observe(frame, { childList: true, subtree: true });
90
+ const sizes = new ResizeObserver(schedule);
91
+ sizes.observe(frame);
92
+ const onWheel = (event) => {
93
+ const delta = sidewaysDelta(event, frame.clientWidth);
94
+ if (delta === 0 || max === 0) {
95
+ return;
96
+ }
97
+ scrolling = true;
98
+ window.clearTimeout(hideTimer);
99
+ hideTimer = window.setTimeout(() => {
100
+ scrolling = false;
101
+ draw();
102
+ }, HIDE_AFTER);
103
+ const next = clampShift(exact + delta, max);
104
+ if (next !== exact) {
105
+ event.preventDefault();
106
+ exact = next;
107
+ }
108
+ draw();
109
+ };
110
+ const onPointerMove = (event) => {
111
+ const bottom = track.getBoundingClientRect().bottom;
112
+ const isNear = event.clientY >= bottom - NEAR_BOTTOM;
113
+ if (isNear !== near) {
114
+ near = isNear;
115
+ draw();
116
+ }
117
+ };
118
+ const onPointerLeave = () => {
119
+ if (near) {
120
+ near = false;
121
+ draw();
122
+ }
123
+ };
124
+ frame.addEventListener('wheel', onWheel, { passive: false });
125
+ frame.addEventListener('pointermove', onPointerMove);
126
+ frame.addEventListener('pointerleave', onPointerLeave);
127
+ schedule();
128
+ return () => {
129
+ frame.removeEventListener('wheel', onWheel);
130
+ frame.removeEventListener('pointermove', onPointerMove);
131
+ frame.removeEventListener('pointerleave', onPointerLeave);
132
+ rows.disconnect();
133
+ mounts.disconnect();
134
+ sizes.disconnect();
135
+ cancelAnimationFrame(pending);
136
+ window.clearTimeout(hideTimer);
137
+ frame.style.removeProperty(SHIFT_PROPERTY);
138
+ track.removeAttribute('data-visible');
139
+ };
140
+ }, [frame, track, thumb, model, resetKey]);
141
+ 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" }) }) }));
142
+ return { attach: setFrame, bar };
143
+ }
@@ -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[];