@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,300 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Loader2 } from 'lucide-react';
|
|
4
|
+
import { memo, useCallback } from 'react';
|
|
5
|
+
import { useDraggable, useDroppable } from '@dnd-kit/core';
|
|
6
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
7
|
+
|
|
8
|
+
import { useTreeContext } from '../context/TreeContext';
|
|
9
|
+
import { radiusClass, rowStateClasses } from '../data/appearance';
|
|
10
|
+
import type { FlatRow, TreeRowRenderProps } from '../types';
|
|
11
|
+
import { TreeChevron } from './TreeChevron';
|
|
12
|
+
import { treeRowDomId } from './TreeContent';
|
|
13
|
+
import { TreeDropIndicator } from './TreeDropIndicator';
|
|
14
|
+
import { TreeIcon } from './TreeIcon';
|
|
15
|
+
import { TreeIndentGuides } from './TreeIndentGuides';
|
|
16
|
+
import { TreeLabel } from './TreeLabel';
|
|
17
|
+
import { TreeRenameInput } from './TreeRenameInput';
|
|
18
|
+
|
|
19
|
+
export interface TreeRowProps<T> {
|
|
20
|
+
row: FlatRow<T>;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* TreeRow — single row in a virtualised tree.
|
|
26
|
+
*
|
|
27
|
+
* Memoised: re-renders only when `row` reference or `className` change.
|
|
28
|
+
* `row` is treated as immutable — the parent should not mutate node
|
|
29
|
+
* objects in place.
|
|
30
|
+
*/
|
|
31
|
+
function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
|
|
32
|
+
const ctx = useTreeContext<T>();
|
|
33
|
+
const {
|
|
34
|
+
appearance,
|
|
35
|
+
activationMode,
|
|
36
|
+
showIndentGuides,
|
|
37
|
+
selected,
|
|
38
|
+
focused,
|
|
39
|
+
matchingIds,
|
|
40
|
+
select,
|
|
41
|
+
setSelectedIds,
|
|
42
|
+
clickSelect,
|
|
43
|
+
toggle,
|
|
44
|
+
setFocus,
|
|
45
|
+
activate,
|
|
46
|
+
getItemName,
|
|
47
|
+
renderIcon,
|
|
48
|
+
renderLabel,
|
|
49
|
+
renderActions,
|
|
50
|
+
renderContextMenu,
|
|
51
|
+
renamingId,
|
|
52
|
+
commitRename,
|
|
53
|
+
cancelRename,
|
|
54
|
+
clipboard,
|
|
55
|
+
dnd,
|
|
56
|
+
} = ctx;
|
|
57
|
+
|
|
58
|
+
const { node, level, isFolder, isExpanded, isLoading, posInSet, setSize } = row;
|
|
59
|
+
const isSelected = selected.has(node.id);
|
|
60
|
+
const isFocused = focused === node.id;
|
|
61
|
+
const isMatchingSearch = matchingIds.has(node.id);
|
|
62
|
+
const isMultiSelect = ctx.selectionMode === 'multiple';
|
|
63
|
+
const isCut =
|
|
64
|
+
clipboard?.kind === 'cut' && clipboard.ids.includes(node.id);
|
|
65
|
+
|
|
66
|
+
const slot: TreeRowRenderProps<T> = {
|
|
67
|
+
node,
|
|
68
|
+
level,
|
|
69
|
+
isSelected,
|
|
70
|
+
isExpanded,
|
|
71
|
+
isFocused,
|
|
72
|
+
isFolder,
|
|
73
|
+
isLoading,
|
|
74
|
+
isMatchingSearch,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Folders always toggle on single click regardless of `activationMode`.
|
|
78
|
+
// Leaves dispatch by mode:
|
|
79
|
+
// single-click → click activates {preview:false}
|
|
80
|
+
// double-click → click only selects; dblclick activates {preview:false}
|
|
81
|
+
// single-click-preview → click activates {preview:true}; dblclick activates {preview:false}
|
|
82
|
+
const isRenaming = renamingId === node.id;
|
|
83
|
+
const isDragging = dnd.draggingIds.has(node.id);
|
|
84
|
+
const dropTarget = dnd.dropTarget;
|
|
85
|
+
const isDropTarget = dropTarget?.id === node.id;
|
|
86
|
+
const dropPosition = isDropTarget ? dropTarget!.position : null;
|
|
87
|
+
|
|
88
|
+
// ─── DnD wiring ──────────────────────────────────────────────────
|
|
89
|
+
// Hooks are called unconditionally so we don't violate rules-of-
|
|
90
|
+
// hooks; the `disabled` flag short-circuits @dnd-kit when DnD is off
|
|
91
|
+
// or the row is in inline rename.
|
|
92
|
+
//
|
|
93
|
+
// Drop-zone resolution (before/inside/after) is centralised in
|
|
94
|
+
// `<TreeDndProvider>` via `onDragMove` — rows don't need a
|
|
95
|
+
// `onPointerMove` of their own. Saves a listener × N rows.
|
|
96
|
+
const dndDisabled = !dnd.active || isRenaming || node.disabled;
|
|
97
|
+
const draggable = useDraggable({ id: node.id, disabled: dndDisabled });
|
|
98
|
+
/**
|
|
99
|
+
* dnd-kit's DESCRIPTIVE aria only.
|
|
100
|
+
*
|
|
101
|
+
* `draggable.attributes` also carries `role: 'button'`, `tabIndex: 0` and
|
|
102
|
+
* `aria-disabled`, and it was spread AFTER this row's own attributes — so
|
|
103
|
+
* while drag was enabled the row stopped being a `treeitem` (a tree of
|
|
104
|
+
* buttons announces as a list of buttons) and every row became tab-stoppable,
|
|
105
|
+
* defeating the roving tabindex the keyboard navigation depends on. tsc
|
|
106
|
+
* reported it as three duplicate props; the symptom was only audible.
|
|
107
|
+
*/
|
|
108
|
+
const dragAria = {
|
|
109
|
+
'aria-roledescription': draggable.attributes['aria-roledescription'],
|
|
110
|
+
'aria-describedby': draggable.attributes['aria-describedby'],
|
|
111
|
+
'aria-pressed': draggable.attributes['aria-pressed'],
|
|
112
|
+
};
|
|
113
|
+
const droppable = useDroppable({ id: node.id, disabled: dndDisabled });
|
|
114
|
+
|
|
115
|
+
const setRowEl = useCallback(
|
|
116
|
+
(el: HTMLDivElement | null) => {
|
|
117
|
+
draggable.setNodeRef(el);
|
|
118
|
+
droppable.setNodeRef(el);
|
|
119
|
+
},
|
|
120
|
+
[draggable, droppable],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const isAllowedDrop =
|
|
124
|
+
dropPosition && !isDragging
|
|
125
|
+
? dnd.isAllowedDrop(node, dropPosition)
|
|
126
|
+
: true;
|
|
127
|
+
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
128
|
+
if (node.disabled || isRenaming) return;
|
|
129
|
+
setFocus(node.id);
|
|
130
|
+
// Multi-select: full Finder/Explorer semantics — plain replaces, meta
|
|
131
|
+
// toggles, shift extends range from anchor, shift+meta unions range.
|
|
132
|
+
// Single-select: clickSelect collapses to {id}. None: no-op.
|
|
133
|
+
if (isMultiSelect) {
|
|
134
|
+
clickSelect(node.id, { shift: e.shiftKey, meta: e.metaKey || e.ctrlKey });
|
|
135
|
+
} else {
|
|
136
|
+
select(node.id);
|
|
137
|
+
}
|
|
138
|
+
if (isFolder) {
|
|
139
|
+
// Don't toggle on shift/meta clicks — those are pure selection edits.
|
|
140
|
+
if (e.shiftKey || e.metaKey || e.ctrlKey) return;
|
|
141
|
+
toggle(node.id);
|
|
142
|
+
} else if (activationMode === 'single-click') {
|
|
143
|
+
// Selection-only modifier clicks should not activate the leaf.
|
|
144
|
+
if (e.shiftKey || e.metaKey || e.ctrlKey) return;
|
|
145
|
+
activate(node, { preview: false });
|
|
146
|
+
} else if (activationMode === 'single-click-preview') {
|
|
147
|
+
if (e.shiftKey || e.metaKey || e.ctrlKey) return;
|
|
148
|
+
activate(node, { preview: true });
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const handleDoubleClick = () => {
|
|
153
|
+
if (node.disabled || isRenaming) return;
|
|
154
|
+
if (isFolder) return;
|
|
155
|
+
activate(node, { preview: false });
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Finder/Explorer semantics: right-click on an unselected row switches
|
|
159
|
+
// selection to that single row (so menu actions apply to it). Right-
|
|
160
|
+
// click on a row already in the selection leaves the multi-selection
|
|
161
|
+
// intact (so destructive bulk actions stay safe).
|
|
162
|
+
const handleContextMenu = () => {
|
|
163
|
+
if (node.disabled || isRenaming) return;
|
|
164
|
+
setFocus(node.id);
|
|
165
|
+
if (!isSelected) {
|
|
166
|
+
setSelectedIds([node.id]);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const trigger = (
|
|
171
|
+
<div
|
|
172
|
+
ref={dnd.active ? setRowEl : undefined}
|
|
173
|
+
id={treeRowDomId(node.id)}
|
|
174
|
+
role="treeitem"
|
|
175
|
+
aria-level={level + 1}
|
|
176
|
+
aria-posinset={posInSet}
|
|
177
|
+
aria-setsize={setSize}
|
|
178
|
+
aria-expanded={isFolder ? isExpanded : undefined}
|
|
179
|
+
aria-selected={ctx.selectionMode === 'none' ? undefined : isSelected}
|
|
180
|
+
aria-disabled={node.disabled || undefined}
|
|
181
|
+
data-tree-row=""
|
|
182
|
+
data-id={node.id}
|
|
183
|
+
data-activation-mode={activationMode}
|
|
184
|
+
data-selected={isSelected ? 'true' : undefined}
|
|
185
|
+
data-clipboard={isCut ? 'cut' : undefined}
|
|
186
|
+
data-dragging={isDragging ? 'true' : undefined}
|
|
187
|
+
data-focused={isFocused && !isSelected ? 'true' : undefined}
|
|
188
|
+
data-folder={isFolder || undefined}
|
|
189
|
+
data-expanded={isExpanded || undefined}
|
|
190
|
+
tabIndex={-1}
|
|
191
|
+
style={{
|
|
192
|
+
paddingLeft: 6 + level * appearance.indent,
|
|
193
|
+
// `auto` sizing lets a multi-line label (e.g. title + meta in a
|
|
194
|
+
// chat-history list) grow the row instead of overflowing a fixed
|
|
195
|
+
// height. `fixed` keeps the classic single-line explorer row.
|
|
196
|
+
...(appearance.rowSizing === 'auto'
|
|
197
|
+
? {
|
|
198
|
+
minHeight: 'var(--tree-row-height)',
|
|
199
|
+
paddingTop: 4,
|
|
200
|
+
paddingBottom: 4,
|
|
201
|
+
}
|
|
202
|
+
: { height: 'var(--tree-row-height)' }),
|
|
203
|
+
gap: 'var(--tree-gap)',
|
|
204
|
+
}}
|
|
205
|
+
{...(dnd.active ? draggable.listeners : {})}
|
|
206
|
+
{...(dnd.active ? dragAria : {})}
|
|
207
|
+
onClick={handleClick}
|
|
208
|
+
onDoubleClick={handleDoubleClick}
|
|
209
|
+
onContextMenu={handleContextMenu}
|
|
210
|
+
onFocus={() => setFocus(node.id)}
|
|
211
|
+
className={cn(
|
|
212
|
+
'group/row relative flex w-full select-none items-center pr-2 text-left',
|
|
213
|
+
'transition-colors outline-none',
|
|
214
|
+
node.disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
|
215
|
+
radiusClass(appearance),
|
|
216
|
+
rowStateClasses(appearance),
|
|
217
|
+
'focus-visible:ring-1 focus-visible:ring-ring/50',
|
|
218
|
+
isMatchingSearch && 'ring-1 ring-primary/30',
|
|
219
|
+
isCut && 'opacity-60',
|
|
220
|
+
isDragging && 'opacity-40',
|
|
221
|
+
node.disabled && 'opacity-50',
|
|
222
|
+
className,
|
|
223
|
+
)}
|
|
224
|
+
>
|
|
225
|
+
{/* Active-row left indicator (VSCode style) */}
|
|
226
|
+
{appearance.showActiveIndicator && isSelected ? (
|
|
227
|
+
<span
|
|
228
|
+
aria-hidden
|
|
229
|
+
className={cn(
|
|
230
|
+
'absolute left-0 top-1 bottom-1 w-0.5 rounded-r-full',
|
|
231
|
+
'bg-foreground/30 group-focus-within/tree:bg-primary',
|
|
232
|
+
)}
|
|
233
|
+
/>
|
|
234
|
+
) : null}
|
|
235
|
+
|
|
236
|
+
{/* DnD drop indicator — top/bottom line for sibling reorder,
|
|
237
|
+
fill for "drop into folder". */}
|
|
238
|
+
{dropPosition && !isDragging ? (
|
|
239
|
+
<TreeDropIndicator
|
|
240
|
+
position={dropPosition}
|
|
241
|
+
indent={6 + level * appearance.indent}
|
|
242
|
+
invalid={!isAllowedDrop}
|
|
243
|
+
/>
|
|
244
|
+
) : null}
|
|
245
|
+
|
|
246
|
+
{showIndentGuides && level > 0 ? (
|
|
247
|
+
<TreeIndentGuides level={level} indent={appearance.indent} />
|
|
248
|
+
) : null}
|
|
249
|
+
|
|
250
|
+
<TreeChevron isExpanded={isExpanded} isFolder={isFolder} />
|
|
251
|
+
|
|
252
|
+
{isLoading ? (
|
|
253
|
+
<Loader2
|
|
254
|
+
aria-hidden
|
|
255
|
+
strokeWidth={appearance.iconStrokeWidth}
|
|
256
|
+
style={{ width: 'var(--tree-icon-size)', height: 'var(--tree-icon-size)' }}
|
|
257
|
+
className="shrink-0 animate-spin text-muted-foreground/70"
|
|
258
|
+
/>
|
|
259
|
+
) : renderIcon ? (
|
|
260
|
+
renderIcon(slot)
|
|
261
|
+
) : (isFolder ? appearance.hideFolderIcons : appearance.hideLeafIcons) ? null : (
|
|
262
|
+
<TreeIcon isFolder={isFolder} isExpanded={isExpanded} />
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
<span
|
|
266
|
+
className="flex min-w-0 flex-1 items-center"
|
|
267
|
+
style={{ gap: 'var(--tree-gap)' }}
|
|
268
|
+
>
|
|
269
|
+
{renamingId === node.id ? (
|
|
270
|
+
<TreeRenameInput
|
|
271
|
+
initialValue={getItemName(node)}
|
|
272
|
+
isFolder={isFolder}
|
|
273
|
+
onCommit={(next) => commitRename(node.id, next)}
|
|
274
|
+
onCancel={cancelRename}
|
|
275
|
+
/>
|
|
276
|
+
) : renderLabel ? (
|
|
277
|
+
renderLabel(slot)
|
|
278
|
+
) : (
|
|
279
|
+
<TreeLabel isMatchingSearch={isMatchingSearch}>{getItemName(node)}</TreeLabel>
|
|
280
|
+
)}
|
|
281
|
+
</span>
|
|
282
|
+
|
|
283
|
+
{renderActions ? (
|
|
284
|
+
<span
|
|
285
|
+
className="ml-auto flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover/row:opacity-100 group-focus-within/row:opacity-100"
|
|
286
|
+
onClick={(e) => e.stopPropagation()}
|
|
287
|
+
>
|
|
288
|
+
{renderActions(slot)}
|
|
289
|
+
</span>
|
|
290
|
+
) : null}
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (renderContextMenu) {
|
|
295
|
+
return <>{renderContextMenu(slot, trigger)}</>;
|
|
296
|
+
}
|
|
297
|
+
return trigger;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export const TreeRow = memo(TreeRowRaw) as typeof TreeRowRaw;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Search, X } from 'lucide-react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
import { useTreeContext } from '../context/TreeContext';
|
|
7
|
+
import { useTreeSearch } from '../context/hooks';
|
|
8
|
+
|
|
9
|
+
export interface TreeSearchInputProps {
|
|
10
|
+
className?: string;
|
|
11
|
+
showMatches?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function TreeSearchInput({ className, showMatches = true }: TreeSearchInputProps) {
|
|
15
|
+
const { labels } = useTreeContext();
|
|
16
|
+
const { query, setQuery, matchCount } = useTreeSearch();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className={cn(
|
|
21
|
+
'flex items-center gap-2 rounded-md border border-border bg-background px-2',
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
>
|
|
25
|
+
<Search aria-hidden className="size-3.5 shrink-0 text-muted-foreground" />
|
|
26
|
+
<input
|
|
27
|
+
type="search"
|
|
28
|
+
value={query}
|
|
29
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
30
|
+
placeholder={labels.searchPlaceholder}
|
|
31
|
+
className="h-7 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
32
|
+
/>
|
|
33
|
+
{showMatches && query.trim().length > 0 ? (
|
|
34
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
35
|
+
{labels.searchMatches(matchCount)}
|
|
36
|
+
</span>
|
|
37
|
+
) : null}
|
|
38
|
+
{query.length > 0 ? (
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
onClick={() => setQuery('')}
|
|
42
|
+
aria-label="Clear search"
|
|
43
|
+
className="shrink-0 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
44
|
+
>
|
|
45
|
+
<X aria-hidden className="size-3.5" />
|
|
46
|
+
</button>
|
|
47
|
+
) : null}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
4
|
+
|
|
5
|
+
export interface TreeSkeletonProps {
|
|
6
|
+
rows?: number;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function TreeSkeleton({ rows = 6, className }: TreeSkeletonProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className={cn('flex flex-col gap-1 p-2', className)} aria-hidden>
|
|
13
|
+
{Array.from({ length: rows }).map((_, i) => (
|
|
14
|
+
<div key={i} className="flex items-center gap-2" style={{ paddingLeft: (i % 3) * 16 }}>
|
|
15
|
+
<span className="size-4 shrink-0 animate-pulse rounded bg-muted" />
|
|
16
|
+
<span className="size-4 shrink-0 animate-pulse rounded bg-muted" />
|
|
17
|
+
<span className="h-3 flex-1 animate-pulse rounded bg-muted" />
|
|
18
|
+
</div>
|
|
19
|
+
))}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export { TreeChevron } from './TreeChevron';
|
|
4
|
+
export type { TreeChevronProps } from './TreeChevron';
|
|
5
|
+
export { TreeIcon } from './TreeIcon';
|
|
6
|
+
export type { TreeIconProps } from './TreeIcon';
|
|
7
|
+
export { TreeLabel } from './TreeLabel';
|
|
8
|
+
export type { TreeLabelProps } from './TreeLabel';
|
|
9
|
+
export { TreeRow } from './TreeRow';
|
|
10
|
+
export type { TreeRowProps } from './TreeRow';
|
|
11
|
+
export { TreeContent } from './TreeContent';
|
|
12
|
+
export type { TreeContentProps } from './TreeContent';
|
|
13
|
+
export { TreeSearchInput } from './TreeSearchInput';
|
|
14
|
+
export type { TreeSearchInputProps } from './TreeSearchInput';
|
|
15
|
+
export { TreeEmpty } from './TreeEmpty';
|
|
16
|
+
export type { TreeEmptyProps } from './TreeEmpty';
|
|
17
|
+
export { TreeSkeleton } from './TreeSkeleton';
|
|
18
|
+
export type { TreeSkeletonProps } from './TreeSkeleton';
|
|
19
|
+
export { TreeError } from './TreeError';
|
|
20
|
+
export type { TreeErrorProps } from './TreeError';
|
|
21
|
+
export { TreeIndentGuides } from './TreeIndentGuides';
|
|
22
|
+
export type { TreeIndentGuidesProps } from './TreeIndentGuides';
|
|
23
|
+
export { TreeRenameInput } from './TreeRenameInput';
|
|
24
|
+
export type { TreeRenameInputProps } from './TreeRenameInput';
|
|
25
|
+
export { TreeDropIndicator } from './TreeDropIndicator';
|
|
26
|
+
export type { TreeDropIndicatorProps } from './TreeDropIndicator';
|
|
27
|
+
export { TreeEmptyArea } from './TreeEmptyArea';
|
|
28
|
+
export type { TreeEmptyAreaProps } from './TreeEmptyArea';
|