@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.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/package.json +122 -0
  4. package/src/DataGrid/README.md +48 -0
  5. package/src/DataGrid/context/DataGridContext.tsx +442 -0
  6. package/src/DataGrid/context/index.ts +4 -0
  7. package/src/DataGrid/index.ts +34 -0
  8. package/src/DataGrid/lazy.tsx +23 -0
  9. package/src/DataGrid/parts/DataGrid.tsx +125 -0
  10. package/src/DataGrid/parts/DataGridCell.tsx +30 -0
  11. package/src/DataGrid/parts/DataGridHeader.tsx +111 -0
  12. package/src/DataGrid/parts/DataGridPagination.tsx +105 -0
  13. package/src/DataGrid/parts/DataGridRow.tsx +84 -0
  14. package/src/DataGrid/parts/index.ts +7 -0
  15. package/src/DataGrid/types.ts +95 -0
  16. package/src/DataTable/README.md +42 -0
  17. package/src/DataTable/context/DataTableContext.tsx +434 -0
  18. package/src/DataTable/context/index.ts +4 -0
  19. package/src/DataTable/index.ts +33 -0
  20. package/src/DataTable/lazy.tsx +23 -0
  21. package/src/DataTable/parts/DataTable.tsx +142 -0
  22. package/src/DataTable/parts/DataTableHeader.tsx +103 -0
  23. package/src/DataTable/parts/DataTablePagination.tsx +115 -0
  24. package/src/DataTable/parts/DataTableRow.tsx +96 -0
  25. package/src/DataTable/parts/index.ts +6 -0
  26. package/src/DataTable/types.ts +88 -0
  27. package/src/FileIcon/FileIcon.tsx +87 -0
  28. package/src/FileIcon/get-file-icon.ts +73 -0
  29. package/src/FileIcon/icons/icon-data.ts +399 -0
  30. package/src/FileIcon/index.ts +19 -0
  31. package/src/FileIcon/loader.ts +29 -0
  32. package/src/FileIcon/specialFolders.ts +110 -0
  33. package/src/FileIcon/treeAdapter.tsx +49 -0
  34. package/src/JsonTree/JsonViewer.tsx +720 -0
  35. package/src/JsonTree/README.md +142 -0
  36. package/src/JsonTree/index.tsx +11 -0
  37. package/src/JsonTree/lazy.tsx +22 -0
  38. package/src/JsonTree/types.ts +91 -0
  39. package/src/Kanban/context/KanbanContext.tsx +165 -0
  40. package/src/Kanban/context/index.ts +4 -0
  41. package/src/Kanban/index.ts +31 -0
  42. package/src/Kanban/lazy.tsx +23 -0
  43. package/src/Kanban/parts/Kanban.tsx +77 -0
  44. package/src/Kanban/parts/KanbanCard.tsx +76 -0
  45. package/src/Kanban/parts/KanbanColumn.tsx +79 -0
  46. package/src/Kanban/parts/KanbanDragOverlay.tsx +46 -0
  47. package/src/Kanban/parts/index.ts +6 -0
  48. package/src/Kanban/types.ts +45 -0
  49. package/src/Listbox/context/ListboxProvider.tsx +616 -0
  50. package/src/Listbox/context/index.ts +8 -0
  51. package/src/Listbox/index.ts +28 -0
  52. package/src/Listbox/lazy.tsx +31 -0
  53. package/src/Listbox/parts/ListboxGroup.tsx +71 -0
  54. package/src/Listbox/parts/ListboxItem.tsx +151 -0
  55. package/src/Listbox/parts/index.ts +12 -0
  56. package/src/Listbox/types.ts +98 -0
  57. package/src/Masonry/README.md +79 -0
  58. package/src/Masonry/context/MasonryProvider.tsx +1205 -0
  59. package/src/Masonry/context/index.ts +8 -0
  60. package/src/Masonry/index.ts +18 -0
  61. package/src/Masonry/lazy.tsx +20 -0
  62. package/src/Masonry/parts/index.ts +2 -0
  63. package/src/Masonry/types.ts +44 -0
  64. package/src/Timeline/context/TimelineProvider.tsx +474 -0
  65. package/src/Timeline/context/index.ts +16 -0
  66. package/src/Timeline/index.ts +29 -0
  67. package/src/Timeline/lazy.tsx +27 -0
  68. package/src/Timeline/parts/index.ts +2 -0
  69. package/src/Timeline/types.ts +25 -0
  70. package/src/Tree/FinderTree.tsx +42 -0
  71. package/src/Tree/README.md +542 -0
  72. package/src/Tree/TreeDndProvider.tsx +137 -0
  73. package/src/Tree/TreeRoot.tsx +290 -0
  74. package/src/Tree/components/TreeChevron.tsx +48 -0
  75. package/src/Tree/components/TreeContent.tsx +70 -0
  76. package/src/Tree/components/TreeDropIndicator.tsx +65 -0
  77. package/src/Tree/components/TreeEmpty.tsx +22 -0
  78. package/src/Tree/components/TreeEmptyArea.tsx +160 -0
  79. package/src/Tree/components/TreeError.tsx +24 -0
  80. package/src/Tree/components/TreeIcon.tsx +39 -0
  81. package/src/Tree/components/TreeIndentGuides.tsx +33 -0
  82. package/src/Tree/components/TreeLabel.tsx +39 -0
  83. package/src/Tree/components/TreeRenameInput.tsx +114 -0
  84. package/src/Tree/components/TreeRow.tsx +300 -0
  85. package/src/Tree/components/TreeSearchInput.tsx +50 -0
  86. package/src/Tree/components/TreeSkeleton.tsx +22 -0
  87. package/src/Tree/components/index.ts +28 -0
  88. package/src/Tree/context/TreeContext.tsx +409 -0
  89. package/src/Tree/context/TreeContextValue.ts +139 -0
  90. package/src/Tree/context/async-children/collect-ids.ts +27 -0
  91. package/src/Tree/context/async-children/index.ts +8 -0
  92. package/src/Tree/context/async-children/use-async-children.ts +157 -0
  93. package/src/Tree/context/clipboard/index.ts +4 -0
  94. package/src/Tree/context/clipboard/use-clipboard.ts +115 -0
  95. package/src/Tree/context/dnd/index.ts +8 -0
  96. package/src/Tree/context/dnd/use-dnd.ts +194 -0
  97. package/src/Tree/context/expansion/index.ts +4 -0
  98. package/src/Tree/context/expansion/use-expansion.ts +55 -0
  99. package/src/Tree/context/hooks.ts +177 -0
  100. package/src/Tree/context/index.ts +16 -0
  101. package/src/Tree/context/menu/builtin-actions.ts +358 -0
  102. package/src/Tree/context/menu/index.ts +11 -0
  103. package/src/Tree/context/menu/render.tsx +104 -0
  104. package/src/Tree/context/menu/use-resolved-menu.ts +141 -0
  105. package/src/Tree/context/persist/index.ts +4 -0
  106. package/src/Tree/context/persist/use-persist-sync.ts +74 -0
  107. package/src/Tree/context/rename/index.ts +4 -0
  108. package/src/Tree/context/rename/use-rename.ts +113 -0
  109. package/src/Tree/context/selection/index.ts +4 -0
  110. package/src/Tree/context/selection/use-selection.ts +146 -0
  111. package/src/Tree/context/state/index.ts +6 -0
  112. package/src/Tree/context/state/initial.ts +41 -0
  113. package/src/Tree/context/state/reducer.ts +76 -0
  114. package/src/Tree/context/state/types.ts +46 -0
  115. package/src/Tree/data/appearance.ts +237 -0
  116. package/src/Tree/data/childCache.ts +43 -0
  117. package/src/Tree/data/clipboard.ts +33 -0
  118. package/src/Tree/data/createDemoTree.ts +42 -0
  119. package/src/Tree/data/dnd.ts +123 -0
  120. package/src/Tree/data/finderShortcuts.ts +67 -0
  121. package/src/Tree/data/flatten.ts +66 -0
  122. package/src/Tree/data/index.ts +45 -0
  123. package/src/Tree/data/persist.ts +62 -0
  124. package/src/Tree/data/renameUtils.ts +51 -0
  125. package/src/Tree/data/selection.ts +161 -0
  126. package/src/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
  127. package/src/Tree/hooks/finder-hotkeys/index.ts +8 -0
  128. package/src/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
  129. package/src/Tree/hooks/index.ts +25 -0
  130. package/src/Tree/hooks/keyboard/activation.ts +27 -0
  131. package/src/Tree/hooks/keyboard/arrow-nav.ts +26 -0
  132. package/src/Tree/hooks/keyboard/expand-collapse.ts +54 -0
  133. package/src/Tree/hooks/keyboard/index.ts +10 -0
  134. package/src/Tree/hooks/keyboard/types.ts +39 -0
  135. package/src/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
  136. package/src/Tree/hooks/type-ahead/index.ts +5 -0
  137. package/src/Tree/hooks/type-ahead/match-prefix.ts +42 -0
  138. package/src/Tree/hooks/type-ahead/use-tree-type-ahead.ts +89 -0
  139. package/src/Tree/index.tsx +135 -0
  140. package/src/Tree/lazy.tsx +14 -0
  141. package/src/Tree/types/activation.ts +30 -0
  142. package/src/Tree/types/adapter.ts +70 -0
  143. package/src/Tree/types/index.ts +27 -0
  144. package/src/Tree/types/labels.ts +97 -0
  145. package/src/Tree/types/loader.ts +9 -0
  146. package/src/Tree/types/node.ts +38 -0
  147. package/src/Tree/types/root-props.ts +158 -0
  148. package/src/Tree/types/selection.ts +3 -0
  149. package/src/Tree/types/slots.ts +64 -0
  150. package/src/index.ts +21 -0
  151. package/src/stories/_fixtures/categories.ts +51 -0
  152. package/src/stories/_fixtures/finder-store.ts +196 -0
  153. package/src/stories/_fixtures/fs.ts +83 -0
  154. package/src/stories/_fixtures/index.ts +5 -0
  155. package/src/stories/_fixtures/org.ts +46 -0
  156. package/src/stories/_fixtures/status.ts +21 -0
  157. package/src/styles/index.css +10 -0
@@ -0,0 +1,160 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import { useDroppable } from '@dnd-kit/core';
5
+ import {
6
+ ContextMenu,
7
+ ContextMenuContent,
8
+ ContextMenuItem,
9
+ ContextMenuSeparator,
10
+ ContextMenuShortcut,
11
+ ContextMenuTrigger,
12
+ } from '@djangocfg/ui-core/components';
13
+ import { cn } from '@djangocfg/ui-core/lib';
14
+
15
+ import { useTreeContext } from '../context/TreeContext';
16
+ import {
17
+ buildDefaultMenuItems,
18
+ type BuiltinActionContext,
19
+ } from '../context/menu';
20
+ import { TREE_ROOT_DROP_ID } from '../data/dnd';
21
+ import type { TreeContextMenuItem } from '../types';
22
+
23
+ export interface TreeEmptyAreaProps {
24
+ className?: string;
25
+ }
26
+
27
+ /**
28
+ * Fills the remaining vertical space below `<TreeContent>` so the user
29
+ * can right-click "into nothing" to get a Finder/Explorer-style empty
30
+ * area menu (paste / new file / new folder at root), and so DnD has a
31
+ * root drop target.
32
+ *
33
+ * Built-in actions are derived the same way as for rows — through
34
+ * `buildDefaultMenuItems`, with `targetNode = null` (root) and
35
+ * `selectedNodes = []` (nothing under the right-click). Items whose
36
+ * `available()` predicate fails are simply skipped, so a tree without
37
+ * `adapter.createFile/Folder` and without a clipboard payload shows
38
+ * no menu at all.
39
+ */
40
+ export function TreeEmptyArea({ className }: TreeEmptyAreaProps) {
41
+ const ctx = useTreeContext();
42
+ const {
43
+ adapter,
44
+ labels,
45
+ getItemName,
46
+ clipboard,
47
+ cutToClipboard,
48
+ copyToClipboard,
49
+ pasteFromClipboard,
50
+ startRename,
51
+ inlineRenameEnabled,
52
+ dnd,
53
+ } = ctx;
54
+
55
+ // Root drop target — receives drops that miss every row. The shared
56
+ // `TreeDndProvider.onDragMove` detects when `e.over.id === ROOT_DROP_ID`
57
+ // and pushes `{ id: null, position: 'inside' }` into `dnd.dropTarget`,
58
+ // so this component doesn't need an `onPointerMove` of its own.
59
+ const { setNodeRef: setDroppableRef, isOver } = useDroppable({
60
+ id: TREE_ROOT_DROP_ID,
61
+ disabled: !dnd.active,
62
+ });
63
+
64
+ // Resolve menu items every render — they depend on clipboard state
65
+ // (paste shows/hides) and on whether the adapter exposes
66
+ // createFile / createFolder.
67
+ const items = useMemo<TreeContextMenuItem<unknown>[] | null>(() => {
68
+ if (!adapter) return null;
69
+ const builtinCtx: BuiltinActionContext<unknown> = {
70
+ adapter,
71
+ labels,
72
+ selectedNodes: [],
73
+ targetNode: null,
74
+ getName: getItemName,
75
+ startInlineRename: inlineRenameEnabled ? startRename : undefined,
76
+ clipboard: {
77
+ hasItems: !!clipboard && clipboard.ids.length > 0,
78
+ cut: cutToClipboard,
79
+ copy: copyToClipboard,
80
+ paste: () => pasteFromClipboard(null, 'inside'),
81
+ },
82
+ };
83
+ return buildDefaultMenuItems(builtinCtx);
84
+ }, [
85
+ adapter,
86
+ labels,
87
+ getItemName,
88
+ inlineRenameEnabled,
89
+ startRename,
90
+ clipboard,
91
+ cutToClipboard,
92
+ copyToClipboard,
93
+ pasteFromClipboard,
94
+ ]);
95
+
96
+ const surface = (
97
+ <div
98
+ ref={dnd.active ? setDroppableRef : undefined}
99
+ data-tree-empty-area=""
100
+ data-drop-active={dnd.active && isOver ? 'true' : undefined}
101
+ className={cn(
102
+ // Soaks up the remaining vertical space inside the scroll
103
+ // container so right-click on whitespace lands here, not on
104
+ // the scroll viewport.
105
+ 'min-h-[2.5rem] flex-1',
106
+ dnd.active && isOver && 'bg-primary/5 rounded-sm ring-1 ring-primary/30',
107
+ className,
108
+ )}
109
+ />
110
+ );
111
+
112
+ // No items and no DnD overlap → render plain spacer. Wrapping in a
113
+ // `<ContextMenu>` with zero items would still capture right-clicks
114
+ // and show an empty popover — that's worse than browser default.
115
+ if (!items || items.length === 0) {
116
+ return surface;
117
+ }
118
+
119
+ return (
120
+ <ContextMenu>
121
+ <ContextMenuTrigger asChild>{surface}</ContextMenuTrigger>
122
+ <ContextMenuContent>
123
+ {items.map((item, idx) => {
124
+ if (item === 'separator') {
125
+ return <ContextMenuSeparator key={`sep-${idx}`} />;
126
+ }
127
+ const Icon = item.icon;
128
+ return (
129
+ <ContextMenuItem
130
+ key={item.id}
131
+ disabled={item.disabled}
132
+ variant={item.destructive ? 'destructive' : undefined}
133
+ // Pass a synthetic "no-target" row props — the built-in
134
+ // action handlers we surface here (new-file / new-folder
135
+ // / paste) don't read row-specific fields anyway.
136
+ onSelect={() =>
137
+ item.onSelect({
138
+ node: undefined as never,
139
+ level: 0,
140
+ isSelected: false,
141
+ isExpanded: false,
142
+ isFocused: false,
143
+ isFolder: false,
144
+ isLoading: false,
145
+ isMatchingSearch: false,
146
+ })
147
+ }
148
+ >
149
+ {Icon ? <Icon /> : null}
150
+ {item.label}
151
+ {item.shortcut ? (
152
+ <ContextMenuShortcut>{item.shortcut}</ContextMenuShortcut>
153
+ ) : null}
154
+ </ContextMenuItem>
155
+ );
156
+ })}
157
+ </ContextMenuContent>
158
+ </ContextMenu>
159
+ );
160
+ }
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+
3
+ import { AlertCircle } from 'lucide-react';
4
+ import { cn } from '@djangocfg/ui-core/lib';
5
+
6
+ export interface TreeErrorProps {
7
+ children: React.ReactNode;
8
+ className?: string;
9
+ }
10
+
11
+ export function TreeError({ children, className }: TreeErrorProps) {
12
+ return (
13
+ <div
14
+ className={cn(
15
+ 'flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive',
16
+ className,
17
+ )}
18
+ role="alert"
19
+ >
20
+ <AlertCircle aria-hidden className="mt-0.5 size-4 shrink-0" />
21
+ <span>{children}</span>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ import { File, Folder, FolderOpen } from 'lucide-react';
4
+ import { memo } from 'react';
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+
7
+ import { useTreeContext } from '../context/TreeContext';
8
+
9
+ export interface TreeIconProps {
10
+ isFolder: boolean;
11
+ isExpanded: boolean;
12
+ className?: string;
13
+ }
14
+
15
+ /**
16
+ * TreeIcon — file/folder icon for a tree row.
17
+ *
18
+ * Memoised: re-renders only when `isFolder`, `isExpanded` or `className`
19
+ * change. Reads appearance from context — context updates will still
20
+ * trigger re-render as expected.
21
+ */
22
+ function TreeIconRaw({ isFolder, isExpanded, className }: TreeIconProps) {
23
+ const { appearance } = useTreeContext();
24
+ const Icon = isFolder ? (isExpanded ? FolderOpen : Folder) : File;
25
+ return (
26
+ <Icon
27
+ aria-hidden
28
+ strokeWidth={appearance.iconStrokeWidth}
29
+ style={{ width: 'var(--tree-icon-size)', height: 'var(--tree-icon-size)' }}
30
+ className={cn(
31
+ 'shrink-0',
32
+ isFolder ? 'text-foreground/70' : 'text-muted-foreground/80',
33
+ className,
34
+ )}
35
+ />
36
+ );
37
+ }
38
+
39
+ export const TreeIcon = memo(TreeIconRaw);
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+
3
+ import { useTreeContext } from '../context/TreeContext';
4
+
5
+ export interface TreeIndentGuidesProps {
6
+ level: number;
7
+ indent: number;
8
+ }
9
+
10
+ /**
11
+ * Vertical guide lines under nested rows. Renders one absolute-positioned
12
+ * 1px column per ancestor level. Decorative — `aria-hidden` and
13
+ * pointer-events disabled. Opacity comes from the tree appearance.
14
+ */
15
+ export function TreeIndentGuides({ level, indent }: TreeIndentGuidesProps) {
16
+ const { appearance } = useTreeContext();
17
+ if (level <= 0) return null;
18
+ return (
19
+ <span
20
+ aria-hidden
21
+ className="pointer-events-none absolute inset-y-0 left-0"
22
+ style={{ width: 8 + level * indent, opacity: appearance.indentGuideOpacity }}
23
+ >
24
+ {Array.from({ length: level }).map((_, i) => (
25
+ <span
26
+ key={i}
27
+ className="absolute inset-y-0 w-px bg-border"
28
+ style={{ left: 8 + i * indent + indent / 2 - 0.5 }}
29
+ />
30
+ ))}
31
+ </span>
32
+ );
33
+ }
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ import { memo } from 'react';
4
+ import { cn } from '@djangocfg/ui-core/lib';
5
+
6
+ export interface TreeLabelProps {
7
+ children: React.ReactNode;
8
+ isMatchingSearch?: boolean;
9
+ className?: string;
10
+ }
11
+
12
+ /**
13
+ * TreeLabel — truncated label for a tree row.
14
+ *
15
+ * Memoised: re-renders only when `children`, `isMatchingSearch` or
16
+ * `className` change. `children` is compared by reference.
17
+ */
18
+ function TreeLabelRaw({ children, isMatchingSearch, className }: TreeLabelProps) {
19
+ return (
20
+ <span
21
+ // Colour is set inline via the semantic token rather than the
22
+ // `text-foreground` Tailwind utility: that class isn't always
23
+ // emitted into a consuming app's stylesheet (the bundler only
24
+ // generates utilities it sees referenced in scanned source), so
25
+ // relying on it left the label rendering as UA-default black on
26
+ // a dark surface. `var(--foreground)` always resolves.
27
+ style={{ fontSize: 'var(--tree-font-size)', color: 'var(--foreground)' }}
28
+ className={cn(
29
+ 'truncate leading-tight tracking-[-0.005em]',
30
+ isMatchingSearch && 'font-medium',
31
+ className,
32
+ )}
33
+ >
34
+ {children}
35
+ </span>
36
+ );
37
+ }
38
+
39
+ export const TreeLabel = memo(TreeLabelRaw);
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { cn } from '@djangocfg/ui-core/lib';
5
+
6
+ import { autoSelectRange } from '../data/renameUtils';
7
+
8
+ export interface TreeRenameInputProps {
9
+ initialValue: string;
10
+ isFolder: boolean;
11
+ /** Called with the new (trimmed) name when the user presses Enter / blurs. */
12
+ onCommit: (nextName: string) => void | Promise<unknown>;
13
+ /** Called when the user presses Escape. */
14
+ onCancel: () => void;
15
+ className?: string;
16
+ }
17
+
18
+ /**
19
+ * Inline rename input rendered in place of `<TreeLabel>` while a row is
20
+ * being renamed. Mounts focused with the *base* portion of the name
21
+ * pre-selected (Finder behaviour — `foo.txt` selects `foo`).
22
+ *
23
+ * Behaviour:
24
+ * - Enter → commit
25
+ * - Escape → cancel (no adapter call)
26
+ * - blur → commit (matches Finder; intentional even for empty
27
+ * names — the host validates and re-opens on error)
28
+ * - all other keys are stopped from bubbling so Tree's container
29
+ * hotkeys (↑↓ delete F2 etc.) don't fire while typing.
30
+ */
31
+ export function TreeRenameInput({
32
+ initialValue,
33
+ isFolder,
34
+ onCommit,
35
+ onCancel,
36
+ className,
37
+ }: TreeRenameInputProps) {
38
+ const [value, setValue] = useState(initialValue);
39
+ const inputRef = useRef<HTMLInputElement>(null);
40
+ // Track whether we already committed/cancelled so the blur handler
41
+ // doesn't fire a second time after Enter/Escape.
42
+ const settledRef = useRef(false);
43
+
44
+ useEffect(() => {
45
+ const el = inputRef.current;
46
+ if (!el) return;
47
+ el.focus();
48
+ const [start, end] = autoSelectRange(initialValue, isFolder);
49
+ // setSelectionRange on a freshly focused input — schedule on next
50
+ // tick to dodge browser quirks where focus() resets the selection.
51
+ requestAnimationFrame(() => {
52
+ try {
53
+ el.setSelectionRange(start, end);
54
+ } catch {
55
+ /* Some input types reject setSelectionRange — safe to ignore. */
56
+ }
57
+ });
58
+ // Run only once on mount.
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, []);
61
+
62
+ const commit = () => {
63
+ if (settledRef.current) return;
64
+ settledRef.current = true;
65
+ void onCommit(value);
66
+ };
67
+
68
+ const cancel = () => {
69
+ if (settledRef.current) return;
70
+ settledRef.current = true;
71
+ onCancel();
72
+ };
73
+
74
+ return (
75
+ <input
76
+ ref={inputRef}
77
+ type="text"
78
+ value={value}
79
+ // Tree's container hotkeys (delete / arrows / F2 …) must not fire
80
+ // while the user is typing the new name.
81
+ onKeyDown={(e) => {
82
+ e.stopPropagation();
83
+ if (e.key === 'Enter') {
84
+ e.preventDefault();
85
+ commit();
86
+ } else if (e.key === 'Escape') {
87
+ e.preventDefault();
88
+ cancel();
89
+ }
90
+ }}
91
+ onChange={(e) => setValue(e.target.value)}
92
+ onBlur={commit}
93
+ // Prevent click/dblclick on the row from re-firing while the input
94
+ // is mounted (otherwise a focused click commits + re-selects).
95
+ onClick={(e) => e.stopPropagation()}
96
+ onDoubleClick={(e) => e.stopPropagation()}
97
+ onMouseDown={(e) => e.stopPropagation()}
98
+ // Right-click inside the input should be the native input menu,
99
+ // not the row's context menu.
100
+ onContextMenu={(e) => e.stopPropagation()}
101
+ className={cn(
102
+ 'min-w-0 flex-1 rounded-sm border border-primary/50 bg-background',
103
+ 'px-1 py-0 text-foreground outline-none',
104
+ 'focus:ring-1 focus:ring-primary/40',
105
+ className,
106
+ )}
107
+ style={{
108
+ // Match the row's font metrics so the input doesn't visibly jolt.
109
+ fontSize: 'var(--tree-font-size)',
110
+ height: 'calc(var(--tree-row-height) - 4px)',
111
+ }}
112
+ />
113
+ );
114
+ }