@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
package/LICENSE
CHANGED
|
@@ -1,13 +1,102 @@
|
|
|
1
|
-
MIT License
|
|
1
|
+
# Functional Source License, Version 1.1, MIT Future License
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Abbreviation
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
FSL-1.1-MIT
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 - present Bas Milius
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the MIT license that is effective on the second anniversary of the date we make
|
|
91
|
+
the Software available. On or after that date, you may use the Software under
|
|
92
|
+
the MIT license, in which case the following will apply:
|
|
93
|
+
|
|
94
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
95
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
96
|
+
the Software without restriction, including without limitation the rights to
|
|
97
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
98
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
99
|
+
so, subject to the following conditions:
|
|
11
100
|
|
|
12
101
|
The above copyright notice and this permission notice shall be included in all
|
|
13
102
|
copies or substantial portions of the Software.
|
package/README.md
CHANGED
package/dist/Select.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ChevronDown } from 'lucide-react';
|
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { Icon } from './Icon.js';
|
|
7
7
|
import { MENU_LABEL, MenuCheck } from './menu/parts.js';
|
|
8
|
+
import { renderPositioner } from './popup-layer.js';
|
|
8
9
|
const isGroup = (entry) => Array.isArray(entry.items);
|
|
9
10
|
const TRIGGER_SIZE = {
|
|
10
11
|
sm: 'h-7 gap-1 px-2',
|
|
@@ -28,7 +29,7 @@ export function Select({ value, onValueChange, items, label, placeholder, size =
|
|
|
28
29
|
const groups = items.length > 0 && isGroup(items[0]) ? items : null;
|
|
29
30
|
const flat = groups ? groups.flatMap((group) => group.items) : items;
|
|
30
31
|
const selected = flat.find((item) => item.value === value) ?? null;
|
|
31
|
-
return (_jsxs(BaseSelect.Root, { items: flat.map((item) => ({ value: item.value, label: item.label })), value: value, disabled: disabled, onValueChange: (next) => onValueChange(next), children: [_jsxs(BaseSelect.Trigger, { ref: ref, "aria-label": label, className: clsx('flex shrink-0 items-center whitespace-nowrap text-xs', TRIGGER_SIZE[size], TRIGGER_VARIANT[variant], TRIGGER_DISABLED, className), children: [selected?.icon, _jsx(BaseSelect.Value, { className: clsx(truncateValue && 'truncate'), children: () => selected?.label ?? placeholder ?? t('action.select') }), _jsx(BaseSelect.Icon, { className: "ml-auto flex shrink-0 pl-1", children: _jsx(Icon, { icon: ChevronDown, size: 14 }) })] }), _jsx(BaseSelect.Portal, { children: _jsx(BaseSelect.Positioner, { className: "popup-positioner", side: "bottom", align: align, sideOffset: 6, alignItemWithTrigger: false, children: _jsx(BaseSelect.Popup, { className: "menu-popup", children: _jsx(BaseSelect.List, { className: "max-h-80 overflow-auto", children: groups
|
|
32
|
+
return (_jsxs(BaseSelect.Root, { items: flat.map((item) => ({ value: item.value, label: item.label })), value: value, disabled: disabled, onValueChange: (next) => onValueChange(next), children: [_jsxs(BaseSelect.Trigger, { ref: ref, "aria-label": label, className: clsx('flex shrink-0 items-center whitespace-nowrap text-xs', TRIGGER_SIZE[size], TRIGGER_VARIANT[variant], TRIGGER_DISABLED, className), children: [selected?.icon, _jsx(BaseSelect.Value, { className: clsx(truncateValue && 'truncate'), children: () => selected?.label ?? placeholder ?? t('action.select') }), _jsx(BaseSelect.Icon, { className: "ml-auto flex shrink-0 pl-1", children: _jsx(Icon, { icon: ChevronDown, size: 14 }) })] }), _jsx(BaseSelect.Portal, { children: _jsx(BaseSelect.Positioner, { className: "popup-positioner", render: renderPositioner, side: "bottom", align: align, sideOffset: 6, alignItemWithTrigger: false, children: _jsx(BaseSelect.Popup, { className: "menu-popup", children: _jsx(BaseSelect.List, { className: "max-h-80 overflow-auto", children: groups
|
|
32
33
|
? groups.map((group) => (_jsxs(BaseSelect.Group, { children: [_jsx(BaseSelect.GroupLabel, { className: MENU_LABEL, children: group.label }), group.items.map((item) => (_jsx(Row, { item: item, picked: item.value === value }, item.value)))] }, group.label)))
|
|
33
34
|
: flat.map((item) => _jsx(Row, { item: item, picked: item.value === value }, item.value)) }) }) }) })] }));
|
|
34
35
|
}
|
|
@@ -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,34 @@
|
|
|
1
|
+
export const SHIFT_PROPERTY = '--adecore-tree-shift';
|
|
2
|
+
const LINE_WIDTH = 16;
|
|
3
|
+
const MIN_THUMB = 24;
|
|
4
|
+
export function clampShift(value, max) {
|
|
5
|
+
return Math.min(Math.max(value, 0), Math.max(max, 0));
|
|
6
|
+
}
|
|
7
|
+
export function shiftNeed(textRight, limit, shift) {
|
|
8
|
+
return Math.max(0, Math.ceil(textRight + shift - limit));
|
|
9
|
+
}
|
|
10
|
+
export function maxShift(needs) {
|
|
11
|
+
return needs.reduce((max, need) => Math.max(max, need), 0);
|
|
12
|
+
}
|
|
13
|
+
export function sidewaysDelta(step, pageWidth) {
|
|
14
|
+
const swapped = step.shiftKey && step.deltaX === 0;
|
|
15
|
+
const sideways = swapped ? step.deltaY : step.deltaX;
|
|
16
|
+
const across = swapped ? 0 : step.deltaY;
|
|
17
|
+
if (sideways === 0 || Math.abs(sideways) <= Math.abs(across)) {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
if (step.deltaMode === 1) {
|
|
21
|
+
return sideways * LINE_WIDTH;
|
|
22
|
+
}
|
|
23
|
+
if (step.deltaMode === 2) {
|
|
24
|
+
return sideways * pageWidth;
|
|
25
|
+
}
|
|
26
|
+
return sideways;
|
|
27
|
+
}
|
|
28
|
+
export function shiftThumb(shift, max, track, view) {
|
|
29
|
+
if (max <= 0 || track <= 0) {
|
|
30
|
+
return { left: 0, width: Math.max(track, 0) };
|
|
31
|
+
}
|
|
32
|
+
const width = Math.min(track, Math.max(MIN_THUMB, Math.round((track * view) / (view + max))));
|
|
33
|
+
return { left: Math.round(((track - width) * clampShift(shift, max)) / max), width };
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const FILE_TREE_CSS: string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { treeRowStyle } from '../tree/style.js';
|
|
2
|
+
// beta.6 splits the extension into nested grids; flatten them to truncate the end of the entire name.
|
|
3
|
+
export const FILE_TREE_CSS = `
|
|
4
|
+
${treeRowStyle('[data-type="item"]', 'data-item-selected')}
|
|
5
|
+
[data-type="item"] { overflow: clip; padding-inline: 4px 8px; --trees-item-row-gap: var(--tree-row-gap, 6px); }
|
|
6
|
+
[data-type="item"]::after {
|
|
7
|
+
content: "";
|
|
8
|
+
flex: none;
|
|
9
|
+
order: -100;
|
|
10
|
+
margin-inline: calc(var(--adecore-tree-shift, 0px) * -1) calc(var(--trees-item-row-gap) * -1);
|
|
11
|
+
}
|
|
12
|
+
[data-icon-name="file-tree-icon-chevron"] { width: 12px; height: 12px; }
|
|
13
|
+
[data-item-section="spacing"] { padding: 0; margin-inline-end: -6px; }
|
|
14
|
+
[data-item-section="spacing-item"] { position: relative; flex: none; width: 16px; margin: 0; border: 0; background: transparent; opacity: 1; transform: none; transition: none; }
|
|
15
|
+
[data-item-section="spacing-item"] + [data-item-section="spacing-item"] { margin: 0; }
|
|
16
|
+
[data-item-section="spacing-item"]::before { content: ""; position: absolute; inset-block: 0; inset-inline-start: 7px; width: 1px; background: var(--border); }
|
|
17
|
+
[data-item-section="icon"] { width: 16px; margin-inline-start: var(--adecore-tree-control-space, 0px); }
|
|
18
|
+
[data-item-section="content"] { white-space: nowrap; max-width: none; flex: 1 1 auto; margin-inline-end: var(--adecore-tree-decoration-space, 0px); }
|
|
19
|
+
[data-item-section="content"] :where([data-truncate-group-container], [data-truncate-group-container] div, [data-truncate-container], [data-truncate-container] div) { display: inline; }
|
|
20
|
+
[data-item-section="content"] [data-truncate-content] { direction: ltr; }
|
|
21
|
+
[data-item-section="content"] :where([data-truncate-content="overflow"], [data-truncate-marker-cell], [data-truncate-fill]) { display: none; }
|
|
22
|
+
[data-tree-port] { position: absolute; z-index: 4; display: flex; align-items: center; height: 25px; pointer-events: none; }
|
|
23
|
+
[data-tree-port] > slot { pointer-events: auto; }
|
|
24
|
+
`;
|