@djangocfg/widget-data 0.1.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.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/package.json +122 -0
- package/src/DataGrid/README.md +48 -0
- package/src/DataGrid/context/DataGridContext.tsx +442 -0
- package/src/DataGrid/context/index.ts +4 -0
- package/src/DataGrid/index.ts +34 -0
- package/src/DataGrid/lazy.tsx +23 -0
- package/src/DataGrid/parts/DataGrid.tsx +125 -0
- package/src/DataGrid/parts/DataGridCell.tsx +30 -0
- package/src/DataGrid/parts/DataGridHeader.tsx +111 -0
- package/src/DataGrid/parts/DataGridPagination.tsx +105 -0
- package/src/DataGrid/parts/DataGridRow.tsx +84 -0
- package/src/DataGrid/parts/index.ts +7 -0
- package/src/DataGrid/types.ts +95 -0
- package/src/DataTable/README.md +42 -0
- package/src/DataTable/context/DataTableContext.tsx +434 -0
- package/src/DataTable/context/index.ts +4 -0
- package/src/DataTable/index.ts +33 -0
- package/src/DataTable/lazy.tsx +23 -0
- package/src/DataTable/parts/DataTable.tsx +142 -0
- package/src/DataTable/parts/DataTableHeader.tsx +103 -0
- package/src/DataTable/parts/DataTablePagination.tsx +115 -0
- package/src/DataTable/parts/DataTableRow.tsx +96 -0
- package/src/DataTable/parts/index.ts +6 -0
- package/src/DataTable/types.ts +88 -0
- package/src/FileIcon/FileIcon.tsx +87 -0
- package/src/FileIcon/get-file-icon.ts +73 -0
- package/src/FileIcon/icons/icon-data.ts +399 -0
- package/src/FileIcon/index.ts +19 -0
- package/src/FileIcon/loader.ts +29 -0
- package/src/FileIcon/specialFolders.ts +110 -0
- package/src/FileIcon/treeAdapter.tsx +49 -0
- package/src/JsonTree/JsonViewer.tsx +720 -0
- package/src/JsonTree/README.md +142 -0
- package/src/JsonTree/index.tsx +11 -0
- package/src/JsonTree/lazy.tsx +22 -0
- package/src/JsonTree/types.ts +91 -0
- package/src/Kanban/context/KanbanContext.tsx +165 -0
- package/src/Kanban/context/index.ts +4 -0
- package/src/Kanban/index.ts +31 -0
- package/src/Kanban/lazy.tsx +23 -0
- package/src/Kanban/parts/Kanban.tsx +77 -0
- package/src/Kanban/parts/KanbanCard.tsx +76 -0
- package/src/Kanban/parts/KanbanColumn.tsx +79 -0
- package/src/Kanban/parts/KanbanDragOverlay.tsx +46 -0
- package/src/Kanban/parts/index.ts +6 -0
- package/src/Kanban/types.ts +45 -0
- package/src/Listbox/context/ListboxProvider.tsx +616 -0
- package/src/Listbox/context/index.ts +8 -0
- package/src/Listbox/index.ts +28 -0
- package/src/Listbox/lazy.tsx +31 -0
- package/src/Listbox/parts/ListboxGroup.tsx +71 -0
- package/src/Listbox/parts/ListboxItem.tsx +151 -0
- package/src/Listbox/parts/index.ts +12 -0
- package/src/Listbox/types.ts +98 -0
- package/src/Masonry/README.md +79 -0
- package/src/Masonry/context/MasonryProvider.tsx +1205 -0
- package/src/Masonry/context/index.ts +8 -0
- package/src/Masonry/index.ts +18 -0
- package/src/Masonry/lazy.tsx +20 -0
- package/src/Masonry/parts/index.ts +2 -0
- package/src/Masonry/types.ts +44 -0
- package/src/Timeline/context/TimelineProvider.tsx +474 -0
- package/src/Timeline/context/index.ts +16 -0
- package/src/Timeline/index.ts +29 -0
- package/src/Timeline/lazy.tsx +27 -0
- package/src/Timeline/parts/index.ts +2 -0
- package/src/Timeline/types.ts +25 -0
- package/src/Tree/FinderTree.tsx +42 -0
- package/src/Tree/README.md +542 -0
- package/src/Tree/TreeDndProvider.tsx +137 -0
- package/src/Tree/TreeRoot.tsx +290 -0
- package/src/Tree/components/TreeChevron.tsx +48 -0
- package/src/Tree/components/TreeContent.tsx +70 -0
- package/src/Tree/components/TreeDropIndicator.tsx +65 -0
- package/src/Tree/components/TreeEmpty.tsx +22 -0
- package/src/Tree/components/TreeEmptyArea.tsx +160 -0
- package/src/Tree/components/TreeError.tsx +24 -0
- package/src/Tree/components/TreeIcon.tsx +39 -0
- package/src/Tree/components/TreeIndentGuides.tsx +33 -0
- package/src/Tree/components/TreeLabel.tsx +39 -0
- package/src/Tree/components/TreeRenameInput.tsx +114 -0
- package/src/Tree/components/TreeRow.tsx +300 -0
- package/src/Tree/components/TreeSearchInput.tsx +50 -0
- package/src/Tree/components/TreeSkeleton.tsx +22 -0
- package/src/Tree/components/index.ts +28 -0
- package/src/Tree/context/TreeContext.tsx +409 -0
- package/src/Tree/context/TreeContextValue.ts +139 -0
- package/src/Tree/context/async-children/collect-ids.ts +27 -0
- package/src/Tree/context/async-children/index.ts +8 -0
- package/src/Tree/context/async-children/use-async-children.ts +157 -0
- package/src/Tree/context/clipboard/index.ts +4 -0
- package/src/Tree/context/clipboard/use-clipboard.ts +115 -0
- package/src/Tree/context/dnd/index.ts +8 -0
- package/src/Tree/context/dnd/use-dnd.ts +194 -0
- package/src/Tree/context/expansion/index.ts +4 -0
- package/src/Tree/context/expansion/use-expansion.ts +55 -0
- package/src/Tree/context/hooks.ts +177 -0
- package/src/Tree/context/index.ts +16 -0
- package/src/Tree/context/menu/builtin-actions.ts +358 -0
- package/src/Tree/context/menu/index.ts +11 -0
- package/src/Tree/context/menu/render.tsx +104 -0
- package/src/Tree/context/menu/use-resolved-menu.ts +141 -0
- package/src/Tree/context/persist/index.ts +4 -0
- package/src/Tree/context/persist/use-persist-sync.ts +74 -0
- package/src/Tree/context/rename/index.ts +4 -0
- package/src/Tree/context/rename/use-rename.ts +113 -0
- package/src/Tree/context/selection/index.ts +4 -0
- package/src/Tree/context/selection/use-selection.ts +146 -0
- package/src/Tree/context/state/index.ts +6 -0
- package/src/Tree/context/state/initial.ts +41 -0
- package/src/Tree/context/state/reducer.ts +76 -0
- package/src/Tree/context/state/types.ts +46 -0
- package/src/Tree/data/appearance.ts +237 -0
- package/src/Tree/data/childCache.ts +43 -0
- package/src/Tree/data/clipboard.ts +33 -0
- package/src/Tree/data/createDemoTree.ts +42 -0
- package/src/Tree/data/dnd.ts +123 -0
- package/src/Tree/data/finderShortcuts.ts +67 -0
- package/src/Tree/data/flatten.ts +66 -0
- package/src/Tree/data/index.ts +45 -0
- package/src/Tree/data/persist.ts +62 -0
- package/src/Tree/data/renameUtils.ts +51 -0
- package/src/Tree/data/selection.ts +161 -0
- package/src/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
- package/src/Tree/hooks/finder-hotkeys/index.ts +8 -0
- package/src/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
- package/src/Tree/hooks/index.ts +25 -0
- package/src/Tree/hooks/keyboard/activation.ts +27 -0
- package/src/Tree/hooks/keyboard/arrow-nav.ts +26 -0
- package/src/Tree/hooks/keyboard/expand-collapse.ts +54 -0
- package/src/Tree/hooks/keyboard/index.ts +10 -0
- package/src/Tree/hooks/keyboard/types.ts +39 -0
- package/src/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
- package/src/Tree/hooks/type-ahead/index.ts +5 -0
- package/src/Tree/hooks/type-ahead/match-prefix.ts +42 -0
- package/src/Tree/hooks/type-ahead/use-tree-type-ahead.ts +89 -0
- package/src/Tree/index.tsx +135 -0
- package/src/Tree/lazy.tsx +14 -0
- package/src/Tree/types/activation.ts +30 -0
- package/src/Tree/types/adapter.ts +70 -0
- package/src/Tree/types/index.ts +27 -0
- package/src/Tree/types/labels.ts +97 -0
- package/src/Tree/types/loader.ts +9 -0
- package/src/Tree/types/node.ts +38 -0
- package/src/Tree/types/root-props.ts +158 -0
- package/src/Tree/types/selection.ts +3 -0
- package/src/Tree/types/slots.ts +64 -0
- package/src/index.ts +21 -0
- package/src/stories/_fixtures/categories.ts +51 -0
- package/src/stories/_fixtures/finder-store.ts +196 -0
- package/src/stories/_fixtures/fs.ts +83 -0
- package/src/stories/_fixtures/index.ts +5 -0
- package/src/stories/_fixtures/org.ts +46 -0
- package/src/stories/_fixtures/status.ts +21 -0
- package/src/styles/index.css +10 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface TreeLabels {
|
|
4
|
+
loading: string;
|
|
5
|
+
empty: string;
|
|
6
|
+
error: string;
|
|
7
|
+
searchPlaceholder: string;
|
|
8
|
+
searchMatches: (count: number) => string;
|
|
9
|
+
ariaLabel: string;
|
|
10
|
+
|
|
11
|
+
// ---- CRUD flow copy (used by built-in adapter actions + dialogs) ----
|
|
12
|
+
|
|
13
|
+
/** Default context-menu item labels. */
|
|
14
|
+
actionOpen: string;
|
|
15
|
+
actionRename: string;
|
|
16
|
+
actionDuplicate: string;
|
|
17
|
+
actionCut: string;
|
|
18
|
+
actionCopy: string;
|
|
19
|
+
actionPaste: string;
|
|
20
|
+
actionDelete: string;
|
|
21
|
+
actionNewFile: string;
|
|
22
|
+
actionNewFolder: string;
|
|
23
|
+
|
|
24
|
+
/** Delete confirmation dialog. */
|
|
25
|
+
confirmDeleteTitle: (count: number) => string;
|
|
26
|
+
confirmDeleteMessage: (names: string[]) => string;
|
|
27
|
+
confirmDeleteOk: string;
|
|
28
|
+
confirmDeleteCancel: string;
|
|
29
|
+
|
|
30
|
+
/** New file prompt. */
|
|
31
|
+
newFileTitle: string;
|
|
32
|
+
newFileMessage: string;
|
|
33
|
+
newFilePlaceholder: string;
|
|
34
|
+
newFileDefault: string;
|
|
35
|
+
|
|
36
|
+
/** New folder prompt. */
|
|
37
|
+
newFolderTitle: string;
|
|
38
|
+
newFolderMessage: string;
|
|
39
|
+
newFolderPlaceholder: string;
|
|
40
|
+
newFolderDefault: string;
|
|
41
|
+
|
|
42
|
+
/** Rename prompt (used when inline rename is unavailable / disabled). */
|
|
43
|
+
renameTitle: string;
|
|
44
|
+
renameMessage: string;
|
|
45
|
+
|
|
46
|
+
/** Name validation. */
|
|
47
|
+
invalidNameEmpty: string;
|
|
48
|
+
|
|
49
|
+
/** Suffix used by the default `duplicate` flow when the consumer's adapter
|
|
50
|
+
* needs a hint name. Receives the source name. */
|
|
51
|
+
duplicateSuffix: (name: string) => string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const DEFAULT_TREE_LABELS: TreeLabels = {
|
|
55
|
+
loading: 'Loading…',
|
|
56
|
+
empty: 'Nothing to show',
|
|
57
|
+
error: 'Failed to load',
|
|
58
|
+
searchPlaceholder: 'Search…',
|
|
59
|
+
searchMatches: (n) => `${n} match${n === 1 ? '' : 'es'}`,
|
|
60
|
+
ariaLabel: 'Tree',
|
|
61
|
+
|
|
62
|
+
actionOpen: 'Open',
|
|
63
|
+
actionRename: 'Rename',
|
|
64
|
+
actionDuplicate: 'Duplicate',
|
|
65
|
+
actionCut: 'Cut',
|
|
66
|
+
actionCopy: 'Copy',
|
|
67
|
+
actionPaste: 'Paste',
|
|
68
|
+
actionDelete: 'Delete',
|
|
69
|
+
actionNewFile: 'New file',
|
|
70
|
+
actionNewFolder: 'New folder',
|
|
71
|
+
|
|
72
|
+
confirmDeleteTitle: (n) =>
|
|
73
|
+
n === 1 ? 'Delete item?' : `Delete ${n} items?`,
|
|
74
|
+
confirmDeleteMessage: (names) =>
|
|
75
|
+
names.length === 1
|
|
76
|
+
? `"${names[0]}" will be removed. This action cannot be undone.`
|
|
77
|
+
: `${names.length} items will be removed. This action cannot be undone.`,
|
|
78
|
+
confirmDeleteOk: 'Delete',
|
|
79
|
+
confirmDeleteCancel: 'Cancel',
|
|
80
|
+
|
|
81
|
+
newFileTitle: 'New file',
|
|
82
|
+
newFileMessage: 'File name',
|
|
83
|
+
newFilePlaceholder: 'untitled.txt',
|
|
84
|
+
newFileDefault: 'untitled.txt',
|
|
85
|
+
|
|
86
|
+
newFolderTitle: 'New folder',
|
|
87
|
+
newFolderMessage: 'Folder name',
|
|
88
|
+
newFolderPlaceholder: 'untitled folder',
|
|
89
|
+
newFolderDefault: 'untitled folder',
|
|
90
|
+
|
|
91
|
+
renameTitle: 'Rename',
|
|
92
|
+
renameMessage: 'New name',
|
|
93
|
+
|
|
94
|
+
invalidNameEmpty: 'Name cannot be empty',
|
|
95
|
+
|
|
96
|
+
duplicateSuffix: (name) => `${name} copy`,
|
|
97
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { TreeNode } from './node';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Async loader: called the first time a folder is expanded with no inline
|
|
7
|
+
* `children`. Result is cached; concurrent expansions are de-duplicated.
|
|
8
|
+
*/
|
|
9
|
+
export type TreeLoadChildren<T> = (node: TreeNode<T>) => Promise<TreeNode<T>[]>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export type TreeItemId = string;
|
|
4
|
+
|
|
5
|
+
/** A single node in the consumer's tree data. Generic over your payload. */
|
|
6
|
+
export interface TreeNode<T = unknown> {
|
|
7
|
+
id: TreeItemId;
|
|
8
|
+
data: T;
|
|
9
|
+
/** Inline children. Omit (and provide a `loadChildren`) for async loading. */
|
|
10
|
+
children?: TreeNode<T>[];
|
|
11
|
+
/**
|
|
12
|
+
* Set to `true` to mark a node as a folder even when its `children` array
|
|
13
|
+
* is empty (e.g. an unloaded async folder). Default: derived from
|
|
14
|
+
* `Array.isArray(children)`.
|
|
15
|
+
*/
|
|
16
|
+
isFolder?: boolean;
|
|
17
|
+
/** Disable interaction. */
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Internal flat-row representation used by the renderer + keyboard nav.
|
|
23
|
+
* Produced by `flattenTree`; consumed by `TreeRow`, the keyboard hook,
|
|
24
|
+
* and the type-ahead hook.
|
|
25
|
+
*/
|
|
26
|
+
export interface FlatRow<T> {
|
|
27
|
+
node: TreeNode<T>;
|
|
28
|
+
level: number;
|
|
29
|
+
parentId: TreeItemId | null;
|
|
30
|
+
isFolder: boolean;
|
|
31
|
+
isExpanded: boolean;
|
|
32
|
+
isLoading: boolean;
|
|
33
|
+
hasError: boolean;
|
|
34
|
+
/** 1-based position among visible siblings (for `aria-posinset`). */
|
|
35
|
+
posInSet: number;
|
|
36
|
+
/** Count of visible siblings sharing this row's parent (for `aria-setsize`). */
|
|
37
|
+
setSize: number;
|
|
38
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { TreeAppearance } from '../data/appearance';
|
|
6
|
+
import type { TreeActivateOptions, TreeActivationMode } from './activation';
|
|
7
|
+
import type {
|
|
8
|
+
TreeAdapter,
|
|
9
|
+
TreeBuiltinAction,
|
|
10
|
+
TreeMovePosition,
|
|
11
|
+
} from './adapter';
|
|
12
|
+
import type { TreeLabels } from './labels';
|
|
13
|
+
import type { TreeLoadChildren } from './loader';
|
|
14
|
+
import type { TreeItemId, TreeNode } from './node';
|
|
15
|
+
import type { TreeSelectionMode } from './selection';
|
|
16
|
+
import type {
|
|
17
|
+
TreeContextMenuActionsResolver,
|
|
18
|
+
TreeContextMenuSlot,
|
|
19
|
+
TreeRowSlot,
|
|
20
|
+
} from './slots';
|
|
21
|
+
|
|
22
|
+
export interface TreeRootProps<T> {
|
|
23
|
+
/** Root nodes. Top-level items are rendered directly (no synthetic root). */
|
|
24
|
+
data: TreeNode<T>[];
|
|
25
|
+
/** Returns the human-readable name for a node (used by search/type-ahead). */
|
|
26
|
+
getItemName: (node: TreeNode<T>) => string;
|
|
27
|
+
|
|
28
|
+
/** Async loader for folders without inline `children`. */
|
|
29
|
+
loadChildren?: TreeLoadChildren<T>;
|
|
30
|
+
|
|
31
|
+
/** Selection behaviour. Default: `'single'`. */
|
|
32
|
+
selectionMode?: TreeSelectionMode;
|
|
33
|
+
/** Pointer activation behaviour. Default: `'single-click'`. */
|
|
34
|
+
activationMode?: TreeActivationMode;
|
|
35
|
+
/** Initially expanded ids. */
|
|
36
|
+
initialExpandedIds?: TreeItemId[];
|
|
37
|
+
/** Initially selected ids. */
|
|
38
|
+
initialSelectedIds?: TreeItemId[];
|
|
39
|
+
/** Pixels of indent per nesting level. Default: 16. (Shortcut for `appearance.indent`.) */
|
|
40
|
+
indent?: number;
|
|
41
|
+
/** Cosmetic configuration: density, sizes, accent intensity, radius. */
|
|
42
|
+
appearance?: TreeAppearance;
|
|
43
|
+
|
|
44
|
+
/** Triggered when selection changes. */
|
|
45
|
+
onSelectionChange?: (selectedIds: TreeItemId[]) => void;
|
|
46
|
+
/** Triggered when expanded set changes. */
|
|
47
|
+
onExpansionChange?: (expandedIds: TreeItemId[]) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Triggered when a leaf is activated (Enter / dblclick / click depending
|
|
50
|
+
* on `activationMode`). Folders never call this — they toggle instead.
|
|
51
|
+
*/
|
|
52
|
+
onActivate?: (node: TreeNode<T>, opts: TreeActivateOptions) => void;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Optional predicate. Nodes returning `false` (and their descendants) are
|
|
56
|
+
* not rendered and not searchable. Use this to hide dot-files, system
|
|
57
|
+
* entries, or anything else the consumer wants to filter out.
|
|
58
|
+
*/
|
|
59
|
+
filterNode?: (node: TreeNode<T>) => boolean;
|
|
60
|
+
|
|
61
|
+
/** Show built-in search input. Default: false. */
|
|
62
|
+
enableSearch?: boolean;
|
|
63
|
+
/** Type printable letters to jump to a matching name. Default: true. */
|
|
64
|
+
enableTypeAhead?: boolean;
|
|
65
|
+
/** Render vertical indent guides under expanded folders. Default: false. */
|
|
66
|
+
showIndentGuides?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Allow inline rename. When true, F2 starts an inline `<input>`; the
|
|
69
|
+
* value is committed through `adapter.rename`. Requires `adapter.rename`.
|
|
70
|
+
* Default: false.
|
|
71
|
+
*/
|
|
72
|
+
enableInlineRename?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Enable Finder/Explorer keyboard shortcuts (delete, rename, duplicate,
|
|
75
|
+
* new file/folder, cut/copy/paste). Bindings only fire when the tree
|
|
76
|
+
* container has focus. Individual shortcuts are still gated by the
|
|
77
|
+
* adapter — `⌘⌫` does nothing if `adapter.remove` is undefined.
|
|
78
|
+
* Default: false.
|
|
79
|
+
*/
|
|
80
|
+
enableFinderHotkeys?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Enable drag-and-drop reorder + move-into-folder. Requires
|
|
83
|
+
* `adapter.move`. Powered by `@dnd-kit/core` — pointer + keyboard
|
|
84
|
+
* sensors, accessible.
|
|
85
|
+
* Default: false.
|
|
86
|
+
*/
|
|
87
|
+
enableDnD?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Custom drop validation. Returns `true` to allow, `false` to forbid.
|
|
90
|
+
* The default validator rejects self-drops and cycles (dropping a
|
|
91
|
+
* folder into its own descendant). Use this to add domain rules
|
|
92
|
+
* (read-only branches, type matching, …).
|
|
93
|
+
*/
|
|
94
|
+
canDrop?: (ctx: {
|
|
95
|
+
source: TreeNode<T>[];
|
|
96
|
+
target: TreeNode<T> | null;
|
|
97
|
+
position: TreeMovePosition;
|
|
98
|
+
}) => boolean;
|
|
99
|
+
|
|
100
|
+
/** Custom row renderer. Falls back to the default <TreeRow />. */
|
|
101
|
+
renderRow?: TreeRowSlot<T>;
|
|
102
|
+
/** Replace default folder/file icon. */
|
|
103
|
+
renderIcon?: TreeRowSlot<T>;
|
|
104
|
+
/** Replace default label rendering. */
|
|
105
|
+
renderLabel?: TreeRowSlot<T>;
|
|
106
|
+
/** Right-side actions slot (per row). */
|
|
107
|
+
renderActions?: TreeRowSlot<T>;
|
|
108
|
+
/** Wrap each row in a context menu (right-click). Receives the row meta + trigger element. */
|
|
109
|
+
renderContextMenu?: TreeContextMenuSlot<T>;
|
|
110
|
+
/**
|
|
111
|
+
* Declarative right-click menu — short-form. Pass `(row) => [items]` and the
|
|
112
|
+
* Tree builds a `<ContextMenu>` for you with sensible defaults. Ignored if
|
|
113
|
+
* `renderContextMenu` is also set. Return `null`/`undefined`/`[]` to skip
|
|
114
|
+
* the menu for that row.
|
|
115
|
+
*/
|
|
116
|
+
contextMenuActions?: TreeContextMenuActionsResolver<T>;
|
|
117
|
+
|
|
118
|
+
/** Override built-in copy in your locale. */
|
|
119
|
+
labels?: Partial<TreeLabels>;
|
|
120
|
+
/** Persist expanded + (optional) selected ids in localStorage under this key. */
|
|
121
|
+
persistKey?: string;
|
|
122
|
+
/** Persist selection alongside expansion. Default: false. */
|
|
123
|
+
persistSelection?: boolean;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* CRUD adapter. When set, Tree builds default context-menu items and
|
|
127
|
+
* Finder hotkeys for every method the adapter defines. Methods that
|
|
128
|
+
* are not provided produce no UI — no greyed-out items.
|
|
129
|
+
*/
|
|
130
|
+
adapter?: TreeAdapter<T>;
|
|
131
|
+
/**
|
|
132
|
+
* Which built-in actions to expose in the auto-built context menu. If
|
|
133
|
+
* omitted, every action whose adapter method exists is shown.
|
|
134
|
+
*
|
|
135
|
+
* Pass `[]` to suppress the auto-built menu entirely while keeping the
|
|
136
|
+
* adapter for hotkeys / DnD.
|
|
137
|
+
*/
|
|
138
|
+
defaultMenuItems?: TreeBuiltinAction[];
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Imperative handle for outer code. The provided ref receives a
|
|
142
|
+
* stable handle to `useTreeActions` once Tree mounts. Lets host
|
|
143
|
+
* components trigger `refresh(id)` / `refreshAll()` from outside
|
|
144
|
+
* Tree (e.g. after a transport-level mutation completes).
|
|
145
|
+
*/
|
|
146
|
+
actionsRef?: React.MutableRefObject<TreeActionsHandle | null>;
|
|
147
|
+
|
|
148
|
+
className?: string;
|
|
149
|
+
style?: CSSProperties;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Subset of `useTreeActions()` exposed via `<TreeRoot actionsRef={…}>`. */
|
|
153
|
+
export interface TreeActionsHandle {
|
|
154
|
+
refresh: (id: string) => Promise<void>;
|
|
155
|
+
refreshAll: () => Promise<void>;
|
|
156
|
+
expandAll: () => void;
|
|
157
|
+
collapseAll: () => void;
|
|
158
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { TreeNode } from './node';
|
|
6
|
+
|
|
7
|
+
export interface TreeRowRenderProps<T> {
|
|
8
|
+
node: TreeNode<T>;
|
|
9
|
+
level: number;
|
|
10
|
+
isSelected: boolean;
|
|
11
|
+
isExpanded: boolean;
|
|
12
|
+
isFocused: boolean;
|
|
13
|
+
isFolder: boolean;
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
isMatchingSearch: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type TreeRowSlot<T> = (props: TreeRowRenderProps<T>) => ReactNode;
|
|
19
|
+
|
|
20
|
+
export type TreeContextMenuSlot<T> = (
|
|
21
|
+
props: TreeRowRenderProps<T>,
|
|
22
|
+
trigger: ReactNode,
|
|
23
|
+
) => ReactNode;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Declarative context-menu item. Pass `'separator'` (string) in place of an
|
|
27
|
+
* object to insert a `<ContextMenuSeparator />` between groups.
|
|
28
|
+
*
|
|
29
|
+
* For more advanced needs (submenus, checkbox items, custom JSX), drop down
|
|
30
|
+
* to `renderContextMenu` instead.
|
|
31
|
+
*/
|
|
32
|
+
export interface TreeContextMenuAction<T> {
|
|
33
|
+
/** Stable React key. */
|
|
34
|
+
id: string;
|
|
35
|
+
label: ReactNode;
|
|
36
|
+
/** Lucide-style icon component. Rendered as `<icon className="size-4" />`. */
|
|
37
|
+
icon?: ComponentType<{ className?: string }>;
|
|
38
|
+
/** Right-aligned keyboard hint (e.g. `'⌘C'`, `'↵'`). Cosmetic. */
|
|
39
|
+
shortcut?: ReactNode;
|
|
40
|
+
/** Disable the item — still rendered, not selectable. */
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/** Style as destructive (red). */
|
|
43
|
+
destructive?: boolean;
|
|
44
|
+
/** Click / Enter handler. Receives the row meta. */
|
|
45
|
+
onSelect: (props: TreeRowRenderProps<T>) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type TreeContextMenuItem<T> = TreeContextMenuAction<T> | 'separator';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Context passed to a `contextMenuActions` resolver. `row` is the row the
|
|
52
|
+
* user right-clicked. `selectedNodes` is the multi-selection at the
|
|
53
|
+
* moment the menu opens — when the right-clicked row was not in the
|
|
54
|
+
* selection, Tree first switches selection to that single row, so
|
|
55
|
+
* `selectedNodes` always contains the right-clicked node.
|
|
56
|
+
*/
|
|
57
|
+
export interface TreeContextMenuActionsContext<T> extends TreeRowRenderProps<T> {
|
|
58
|
+
/** Multi-selection at the moment the menu opens. Always non-empty. */
|
|
59
|
+
selectedNodes: TreeNode<T>[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type TreeContextMenuActionsResolver<T> = (
|
|
63
|
+
ctx: TreeContextMenuActionsContext<T>,
|
|
64
|
+
) => TreeContextMenuItem<T>[] | null | undefined;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data surfaces: the components that put rows, nodes and cards on screen.
|
|
3
|
+
*
|
|
4
|
+
* A root barrel EXISTS here, unlike in the retired ui-tools, and the reason
|
|
5
|
+
* the ban was there does not apply: ui-tools mixed compiled `dist/` subpaths with raw
|
|
6
|
+
* `src/` ones, so a root barrel and a subpath resolved the same module twice
|
|
7
|
+
* and a `TreeProvider` context went missing. Every subpath here is source, so
|
|
8
|
+
* there is one resolution and one module instance.
|
|
9
|
+
*
|
|
10
|
+
* It still costs: importing the root pulls all nine trees. Prefer a subpath —
|
|
11
|
+
* `@djangocfg/widget-data/tree` — in an app that wants one.
|
|
12
|
+
*/
|
|
13
|
+
export * from './Tree';
|
|
14
|
+
export * from './FileIcon';
|
|
15
|
+
export * from './JsonTree';
|
|
16
|
+
export * from './DataGrid';
|
|
17
|
+
export * from './DataTable';
|
|
18
|
+
export * from './Kanban';
|
|
19
|
+
export * from './Listbox';
|
|
20
|
+
export * from './Masonry';
|
|
21
|
+
export * from './Timeline';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { TreeNode } from '../../Tree';
|
|
2
|
+
|
|
3
|
+
export interface Category {
|
|
4
|
+
name: string;
|
|
5
|
+
count?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const CATEGORY_TREE: TreeNode<Category>[] = [
|
|
9
|
+
{
|
|
10
|
+
id: 'electronics',
|
|
11
|
+
data: { name: 'Electronics', count: 1240 },
|
|
12
|
+
children: [
|
|
13
|
+
{
|
|
14
|
+
id: 'computers',
|
|
15
|
+
data: { name: 'Computers', count: 410 },
|
|
16
|
+
children: [
|
|
17
|
+
{ id: 'laptops', data: { name: 'Laptops', count: 220 } },
|
|
18
|
+
{ id: 'desktops', data: { name: 'Desktops', count: 95 } },
|
|
19
|
+
{ id: 'accessories', data: { name: 'Accessories', count: 95 } },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'phones',
|
|
24
|
+
data: { name: 'Phones', count: 530 },
|
|
25
|
+
children: [
|
|
26
|
+
{ id: 'smartphones', data: { name: 'Smartphones', count: 480 } },
|
|
27
|
+
{ id: 'phone-cases', data: { name: 'Cases', count: 50 } },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{ id: 'audio', data: { name: 'Audio', count: 300 } },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'home',
|
|
35
|
+
data: { name: 'Home & Garden', count: 870 },
|
|
36
|
+
children: [
|
|
37
|
+
{ id: 'furniture', data: { name: 'Furniture', count: 320 } },
|
|
38
|
+
{ id: 'kitchen', data: { name: 'Kitchen', count: 290 } },
|
|
39
|
+
{ id: 'garden', data: { name: 'Garden', count: 260 } },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'apparel',
|
|
44
|
+
data: { name: 'Apparel', count: 1900 },
|
|
45
|
+
children: [
|
|
46
|
+
{ id: 'mens', data: { name: "Men's", count: 800 } },
|
|
47
|
+
{ id: 'womens', data: { name: "Women's", count: 900 } },
|
|
48
|
+
{ id: 'kids', data: { name: 'Kids', count: 200 } },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
];
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
TreeAdapter,
|
|
5
|
+
TreeItemId,
|
|
6
|
+
TreeMovePosition,
|
|
7
|
+
TreeNode,
|
|
8
|
+
} from '../../Tree';
|
|
9
|
+
|
|
10
|
+
export interface FsItem {
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const FINDER_INITIAL: TreeNode<FsItem>[] = [
|
|
15
|
+
{
|
|
16
|
+
id: 'docs',
|
|
17
|
+
data: { name: 'docs' },
|
|
18
|
+
children: [
|
|
19
|
+
{ id: 'docs/readme.md', data: { name: 'readme.md' } },
|
|
20
|
+
{ id: 'docs/license.md', data: { name: 'license.md' } },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'src',
|
|
25
|
+
data: { name: 'src' },
|
|
26
|
+
children: [
|
|
27
|
+
{ id: 'src/index.ts', data: { name: 'index.ts' } },
|
|
28
|
+
{ id: 'src/utils.ts', data: { name: 'utils.ts' } },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{ id: 'package.json', data: { name: 'package.json' } },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
interface Row {
|
|
35
|
+
id: TreeItemId;
|
|
36
|
+
data: FsItem;
|
|
37
|
+
parentId: TreeItemId | null;
|
|
38
|
+
isFolder: boolean;
|
|
39
|
+
order: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Tiny in-memory store for the Finder story.
|
|
44
|
+
*
|
|
45
|
+
* A flat `Row[]` is easier to mutate than a recursive `TreeNode<T>[]`;
|
|
46
|
+
* we project it back into a `TreeNode<FsItem>[]` on every render.
|
|
47
|
+
*
|
|
48
|
+
* Production hosts would normally drive this through their own data
|
|
49
|
+
* layer (REST, WebSocket, Wails → Go, …) — this implementation only
|
|
50
|
+
* exists so the story is fully interactive without a backend.
|
|
51
|
+
*/
|
|
52
|
+
export function useFinderStore(initial: TreeNode<FsItem>[]) {
|
|
53
|
+
const [rows, setRows] = useState<Row[]>(() => {
|
|
54
|
+
const out: Row[] = [];
|
|
55
|
+
let counter = 0;
|
|
56
|
+
const walk = (nodes: TreeNode<FsItem>[], parentId: TreeItemId | null) => {
|
|
57
|
+
for (const n of nodes) {
|
|
58
|
+
out.push({
|
|
59
|
+
id: n.id,
|
|
60
|
+
data: n.data,
|
|
61
|
+
parentId,
|
|
62
|
+
isFolder: Array.isArray(n.children) || !!n.isFolder,
|
|
63
|
+
order: counter++,
|
|
64
|
+
});
|
|
65
|
+
if (Array.isArray(n.children)) walk(n.children, n.id);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
walk(initial, null);
|
|
69
|
+
return out;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const tree = projectTree(rows);
|
|
73
|
+
|
|
74
|
+
let nextId = rows.length;
|
|
75
|
+
const mintId = (): TreeItemId =>
|
|
76
|
+
`node-${++nextId}-${Math.random().toString(36).slice(2, 6)}`;
|
|
77
|
+
|
|
78
|
+
const collectDescendants = (id: TreeItemId, acc: Set<TreeItemId>) => {
|
|
79
|
+
acc.add(id);
|
|
80
|
+
for (const r of rows) if (r.parentId === id) collectDescendants(r.id, acc);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const resolveDropParent = (
|
|
84
|
+
target: TreeNode<FsItem> | null,
|
|
85
|
+
position: TreeMovePosition,
|
|
86
|
+
): TreeItemId | null => {
|
|
87
|
+
if (!target) return null;
|
|
88
|
+
const targetRow = rows.find((r) => r.id === target.id);
|
|
89
|
+
if (!targetRow) return null;
|
|
90
|
+
return position === 'inside' && targetRow.isFolder
|
|
91
|
+
? targetRow.id
|
|
92
|
+
: targetRow.parentId;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const adapter: TreeAdapter<FsItem> = {
|
|
96
|
+
remove: async (nodes) => {
|
|
97
|
+
const doomed = new Set<TreeItemId>();
|
|
98
|
+
for (const n of nodes) collectDescendants(n.id, doomed);
|
|
99
|
+
setRows((rs) => rs.filter((r) => !doomed.has(r.id)));
|
|
100
|
+
},
|
|
101
|
+
rename: async (node, name) => {
|
|
102
|
+
setRows((rs) =>
|
|
103
|
+
rs.map((r) => (r.id === node.id ? { ...r, data: { ...r.data, name } } : r)),
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
createFile: async (parent, name) => {
|
|
107
|
+
setRows((rs) => [
|
|
108
|
+
...rs,
|
|
109
|
+
{
|
|
110
|
+
id: mintId(),
|
|
111
|
+
data: { name },
|
|
112
|
+
parentId: parent?.id ?? null,
|
|
113
|
+
isFolder: false,
|
|
114
|
+
order: rs.length,
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
},
|
|
118
|
+
createFolder: async (parent, name) => {
|
|
119
|
+
setRows((rs) => [
|
|
120
|
+
...rs,
|
|
121
|
+
{
|
|
122
|
+
id: mintId(),
|
|
123
|
+
data: { name },
|
|
124
|
+
parentId: parent?.id ?? null,
|
|
125
|
+
isFolder: true,
|
|
126
|
+
order: rs.length,
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
},
|
|
130
|
+
duplicate: async (nodes) => {
|
|
131
|
+
setRows((rs) => {
|
|
132
|
+
const next = [...rs];
|
|
133
|
+
let order = rs.length;
|
|
134
|
+
for (const n of nodes) {
|
|
135
|
+
const src = rs.find((r) => r.id === n.id);
|
|
136
|
+
if (!src) continue;
|
|
137
|
+
next.push({
|
|
138
|
+
id: mintId(),
|
|
139
|
+
data: { ...src.data, name: `${src.data.name} copy` },
|
|
140
|
+
parentId: src.parentId,
|
|
141
|
+
isFolder: src.isFolder,
|
|
142
|
+
order: order++,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return next;
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
move: async (nodes, target, position) => {
|
|
149
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
150
|
+
const nextParent = resolveDropParent(target, position);
|
|
151
|
+
setRows((rs) =>
|
|
152
|
+
rs.map((r) => (nodeIds.has(r.id) ? { ...r, parentId: nextParent } : r)),
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
copy: async (nodes, target, position) => {
|
|
156
|
+
const nextParent = resolveDropParent(target, position);
|
|
157
|
+
setRows((rs) => {
|
|
158
|
+
let order = rs.length;
|
|
159
|
+
const copies: Row[] = nodes.map((n) => {
|
|
160
|
+
const src = rs.find((r) => r.id === n.id)!;
|
|
161
|
+
return {
|
|
162
|
+
id: mintId(),
|
|
163
|
+
data: { ...src.data },
|
|
164
|
+
parentId: nextParent,
|
|
165
|
+
isFolder: src.isFolder,
|
|
166
|
+
order: order++,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
return [...rs, ...copies];
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
validateName: (name) =>
|
|
173
|
+
name.trim().length === 0 ? 'Name cannot be empty' : null,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return { tree, adapter };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function projectTree(rows: Row[]): TreeNode<FsItem>[] {
|
|
180
|
+
const byParent = new Map<TreeItemId | null, Row[]>();
|
|
181
|
+
for (const r of [...rows].sort((a, b) => a.order - b.order)) {
|
|
182
|
+
const arr = byParent.get(r.parentId) ?? [];
|
|
183
|
+
arr.push(r);
|
|
184
|
+
byParent.set(r.parentId, arr);
|
|
185
|
+
}
|
|
186
|
+
const build = (parentId: TreeItemId | null): TreeNode<FsItem>[] => {
|
|
187
|
+
const list = byParent.get(parentId) ?? [];
|
|
188
|
+
return list.map<TreeNode<FsItem>>((r) => ({
|
|
189
|
+
id: r.id,
|
|
190
|
+
data: r.data,
|
|
191
|
+
isFolder: r.isFolder,
|
|
192
|
+
children: r.isFolder ? build(r.id) : undefined,
|
|
193
|
+
}));
|
|
194
|
+
};
|
|
195
|
+
return build(null);
|
|
196
|
+
}
|