@djangocfg/ui-tools 2.1.415 → 2.1.416

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 (110) hide show
  1. package/dist/file-icon/index.d.cts +1 -1
  2. package/dist/file-icon/index.d.ts +1 -1
  3. package/dist/slots-ClRpIzoh.d.cts +88 -0
  4. package/dist/slots-ClRpIzoh.d.ts +88 -0
  5. package/dist/tree/index.cjs +1994 -276
  6. package/dist/tree/index.cjs.map +1 -1
  7. package/dist/tree/index.d.cts +717 -72
  8. package/dist/tree/index.d.ts +717 -72
  9. package/dist/tree/index.mjs +1984 -279
  10. package/dist/tree/index.mjs.map +1 -1
  11. package/package.json +10 -6
  12. package/src/tools/chat/README.md +111 -1
  13. package/src/tools/chat/composer/Composer.tsx +138 -17
  14. package/src/tools/chat/composer/ComposerRichTextarea.tsx +25 -0
  15. package/src/tools/chat/composer/index.ts +22 -0
  16. package/src/tools/chat/composer/slash/README.md +187 -0
  17. package/src/tools/chat/composer/slash/SlashHighlightTextarea.tsx +144 -0
  18. package/src/tools/chat/composer/slash/SlashMenu.tsx +142 -0
  19. package/src/tools/chat/composer/slash/SlashToken.tsx +57 -0
  20. package/src/tools/chat/composer/slash/index.ts +44 -0
  21. package/src/tools/chat/composer/slash/labels.ts +19 -0
  22. package/src/tools/chat/composer/slash/state.ts +168 -0
  23. package/src/tools/chat/composer/slash/types.ts +64 -0
  24. package/src/tools/chat/composer/slash/useSlashCommands.ts +204 -0
  25. package/src/tools/chat/composer/types.ts +8 -0
  26. package/src/tools/chat/shell/SuggestedPrompts.tsx +194 -0
  27. package/src/tools/chat/shell/index.ts +6 -0
  28. package/src/tools/data/Listbox/lazy.tsx +1 -1
  29. package/src/tools/data/Masonry/lazy.tsx +1 -1
  30. package/src/tools/data/Timeline/lazy.tsx +1 -1
  31. package/src/tools/data/Tree/FinderTree.tsx +42 -0
  32. package/src/tools/data/Tree/README.md +337 -208
  33. package/src/tools/data/Tree/TreeDndProvider.tsx +137 -0
  34. package/src/tools/data/Tree/TreeRoot.tsx +170 -55
  35. package/src/tools/data/Tree/__tests__/dnd.test.ts +160 -0
  36. package/src/tools/data/Tree/__tests__/keyboard.test.ts +137 -0
  37. package/src/tools/data/Tree/__tests__/renameUtils.test.ts +52 -0
  38. package/src/tools/data/Tree/__tests__/selection.test.ts +227 -0
  39. package/src/tools/data/Tree/components/TreeDropIndicator.tsx +65 -0
  40. package/src/tools/data/Tree/components/TreeEmptyArea.tsx +160 -0
  41. package/src/tools/data/Tree/components/TreeRenameInput.tsx +114 -0
  42. package/src/tools/data/Tree/components/TreeRow.tsx +92 -8
  43. package/src/tools/data/Tree/components/index.ts +6 -0
  44. package/src/tools/data/Tree/context/TreeContext.tsx +204 -363
  45. package/src/tools/data/Tree/context/TreeContextValue.ts +139 -0
  46. package/src/tools/data/Tree/context/async-children/collect-ids.ts +27 -0
  47. package/src/tools/data/Tree/context/async-children/index.ts +8 -0
  48. package/src/tools/data/Tree/context/async-children/use-async-children.ts +157 -0
  49. package/src/tools/data/Tree/context/clipboard/index.ts +4 -0
  50. package/src/tools/data/Tree/context/clipboard/use-clipboard.ts +115 -0
  51. package/src/tools/data/Tree/context/dnd/index.ts +8 -0
  52. package/src/tools/data/Tree/context/dnd/use-dnd.ts +194 -0
  53. package/src/tools/data/Tree/context/expansion/index.ts +4 -0
  54. package/src/tools/data/Tree/context/expansion/use-expansion.ts +55 -0
  55. package/src/tools/data/Tree/context/hooks.ts +68 -1
  56. package/src/tools/data/Tree/context/index.ts +3 -0
  57. package/src/tools/data/Tree/context/menu/builtin-actions.ts +357 -0
  58. package/src/tools/data/Tree/context/menu/index.ts +10 -0
  59. package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +127 -0
  60. package/src/tools/data/Tree/context/persist/index.ts +4 -0
  61. package/src/tools/data/Tree/context/persist/use-persist-sync.ts +74 -0
  62. package/src/tools/data/Tree/context/rename/index.ts +4 -0
  63. package/src/tools/data/Tree/context/rename/use-rename.ts +113 -0
  64. package/src/tools/data/Tree/context/selection/index.ts +4 -0
  65. package/src/tools/data/Tree/context/selection/use-selection.ts +146 -0
  66. package/src/tools/data/Tree/context/state/index.ts +6 -0
  67. package/src/tools/data/Tree/context/state/initial.ts +41 -0
  68. package/src/tools/data/Tree/context/state/reducer.ts +76 -0
  69. package/src/tools/data/Tree/context/state/types.ts +46 -0
  70. package/src/tools/data/Tree/data/clipboard.ts +33 -0
  71. package/src/tools/data/Tree/data/dnd.ts +123 -0
  72. package/src/tools/data/Tree/data/finderShortcuts.ts +67 -0
  73. package/src/tools/data/Tree/data/index.ts +19 -0
  74. package/src/tools/data/Tree/data/renameUtils.ts +51 -0
  75. package/src/tools/data/Tree/data/selection.ts +157 -0
  76. package/src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
  77. package/src/tools/data/Tree/hooks/finder-hotkeys/index.ts +8 -0
  78. package/src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
  79. package/src/tools/data/Tree/hooks/index.ts +23 -4
  80. package/src/tools/data/Tree/hooks/keyboard/activation.ts +27 -0
  81. package/src/tools/data/Tree/hooks/keyboard/arrow-nav.ts +26 -0
  82. package/src/tools/data/Tree/hooks/keyboard/expand-collapse.ts +54 -0
  83. package/src/tools/data/Tree/hooks/keyboard/index.ts +10 -0
  84. package/src/tools/data/Tree/hooks/keyboard/types.ts +39 -0
  85. package/src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
  86. package/src/tools/data/Tree/hooks/type-ahead/index.ts +5 -0
  87. package/src/tools/data/Tree/hooks/type-ahead/match-prefix.ts +42 -0
  88. package/src/tools/data/Tree/hooks/{useTreeTypeAhead.ts → type-ahead/use-tree-type-ahead.ts} +8 -19
  89. package/src/tools/data/Tree/index.tsx +25 -2
  90. package/src/tools/data/Tree/types/activation.ts +30 -0
  91. package/src/tools/data/Tree/types/adapter.ts +70 -0
  92. package/src/tools/data/Tree/types/index.ts +27 -0
  93. package/src/tools/data/Tree/types/labels.ts +97 -0
  94. package/src/tools/data/Tree/types/loader.ts +9 -0
  95. package/src/tools/data/Tree/types/node.ts +38 -0
  96. package/src/tools/data/Tree/types/root-props.ts +142 -0
  97. package/src/tools/data/Tree/types/selection.ts +3 -0
  98. package/src/tools/data/Tree/types/slots.ts +64 -0
  99. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +85 -0
  100. package/src/tools/forms/MarkdownEditor/index.ts +1 -0
  101. package/src/tools/forms/MarkdownEditor/lazy.tsx +6 -0
  102. package/src/tools/forms/MarkdownEditor/slash/SlashCommandNode.ts +162 -0
  103. package/src/tools/forms/MarkdownEditor/slash/index.ts +4 -0
  104. package/src/tools/forms/MarkdownEditor/slash/syncSlashNode.ts +97 -0
  105. package/src/tools/forms/MarkdownEditor/slash/types.ts +13 -0
  106. package/src/tools/forms/MarkdownEditor/styles.css +18 -0
  107. package/dist/types-j2vhn4Kv.d.cts +0 -241
  108. package/dist/types-j2vhn4Kv.d.ts +0 -241
  109. package/src/tools/data/Tree/hooks/useTreeKeyboard.ts +0 -171
  110. package/src/tools/data/Tree/types.ts +0 -217
@@ -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
+ }
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import { Loader2 } from 'lucide-react';
4
- import { memo } from 'react';
4
+ import { memo, useCallback } from 'react';
5
+ import { useDraggable, useDroppable } from '@dnd-kit/core';
5
6
  import { cn } from '@djangocfg/ui-core/lib';
6
7
 
7
8
  import { useTreeContext } from '../context/TreeContext';
@@ -9,9 +10,11 @@ import { radiusClass, rowStateClasses } from '../data/appearance';
9
10
  import type { FlatRow, TreeRowRenderProps } from '../types';
10
11
  import { TreeChevron } from './TreeChevron';
11
12
  import { treeRowDomId } from './TreeContent';
13
+ import { TreeDropIndicator } from './TreeDropIndicator';
12
14
  import { TreeIcon } from './TreeIcon';
13
15
  import { TreeIndentGuides } from './TreeIndentGuides';
14
16
  import { TreeLabel } from './TreeLabel';
17
+ import { TreeRenameInput } from './TreeRenameInput';
15
18
 
16
19
  export interface TreeRowProps<T> {
17
20
  row: FlatRow<T>;
@@ -36,6 +39,7 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
36
39
  matchingIds,
37
40
  select,
38
41
  setSelectedIds,
42
+ clickSelect,
39
43
  toggle,
40
44
  setFocus,
41
45
  activate,
@@ -44,6 +48,11 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
44
48
  renderLabel,
45
49
  renderActions,
46
50
  renderContextMenu,
51
+ renamingId,
52
+ commitRename,
53
+ cancelRename,
54
+ clipboard,
55
+ dnd,
47
56
  } = ctx;
48
57
 
49
58
  const { node, level, isFolder, isExpanded, isLoading, posInSet, setSize } = row;
@@ -51,6 +60,8 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
51
60
  const isFocused = focused === node.id;
52
61
  const isMatchingSearch = matchingIds.has(node.id);
53
62
  const isMultiSelect = ctx.selectionMode === 'multiple';
63
+ const isCut =
64
+ clipboard?.kind === 'cut' && clipboard.ids.includes(node.id);
54
65
 
55
66
  const slot: TreeRowRenderProps<T> = {
56
67
  node,
@@ -68,33 +79,82 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
68
79
  // single-click → click activates {preview:false}
69
80
  // double-click → click only selects; dblclick activates {preview:false}
70
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
+ const droppable = useDroppable({ id: node.id, disabled: dndDisabled });
99
+
100
+ const setRowEl = useCallback(
101
+ (el: HTMLDivElement | null) => {
102
+ draggable.setNodeRef(el);
103
+ droppable.setNodeRef(el);
104
+ },
105
+ [draggable, droppable],
106
+ );
107
+
108
+ const isAllowedDrop =
109
+ dropPosition && !isDragging
110
+ ? dnd.isAllowedDrop(node, dropPosition)
111
+ : true;
71
112
  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
72
- if (node.disabled) return;
113
+ if (node.disabled || isRenaming) return;
73
114
  setFocus(node.id);
74
- // Multi-select: a plain click replaces the selection; ⌘/Ctrl-click
75
- // toggles the clicked row (classic file-manager / VSCode behaviour).
76
- if (isMultiSelect && !(e.metaKey || e.ctrlKey)) {
77
- setSelectedIds([node.id]);
115
+ // Multi-select: full Finder/Explorer semantics plain replaces, meta
116
+ // toggles, shift extends range from anchor, shift+meta unions range.
117
+ // Single-select: clickSelect collapses to {id}. None: no-op.
118
+ if (isMultiSelect) {
119
+ clickSelect(node.id, { shift: e.shiftKey, meta: e.metaKey || e.ctrlKey });
78
120
  } else {
79
121
  select(node.id);
80
122
  }
81
123
  if (isFolder) {
124
+ // Don't toggle on shift/meta clicks — those are pure selection edits.
125
+ if (e.shiftKey || e.metaKey || e.ctrlKey) return;
82
126
  toggle(node.id);
83
127
  } else if (activationMode === 'single-click') {
128
+ // Selection-only modifier clicks should not activate the leaf.
129
+ if (e.shiftKey || e.metaKey || e.ctrlKey) return;
84
130
  activate(node, { preview: false });
85
131
  } else if (activationMode === 'single-click-preview') {
132
+ if (e.shiftKey || e.metaKey || e.ctrlKey) return;
86
133
  activate(node, { preview: true });
87
134
  }
88
135
  };
89
136
 
90
137
  const handleDoubleClick = () => {
91
- if (node.disabled) return;
138
+ if (node.disabled || isRenaming) return;
92
139
  if (isFolder) return;
93
140
  activate(node, { preview: false });
94
141
  };
95
142
 
143
+ // Finder/Explorer semantics: right-click on an unselected row switches
144
+ // selection to that single row (so menu actions apply to it). Right-
145
+ // click on a row already in the selection leaves the multi-selection
146
+ // intact (so destructive bulk actions stay safe).
147
+ const handleContextMenu = () => {
148
+ if (node.disabled || isRenaming) return;
149
+ setFocus(node.id);
150
+ if (!isSelected) {
151
+ setSelectedIds([node.id]);
152
+ }
153
+ };
154
+
96
155
  const trigger = (
97
156
  <div
157
+ ref={dnd.active ? setRowEl : undefined}
98
158
  id={treeRowDomId(node.id)}
99
159
  role="treeitem"
100
160
  aria-level={level + 1}
@@ -107,6 +167,8 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
107
167
  data-id={node.id}
108
168
  data-activation-mode={activationMode}
109
169
  data-selected={isSelected ? 'true' : undefined}
170
+ data-clipboard={isCut ? 'cut' : undefined}
171
+ data-dragging={isDragging ? 'true' : undefined}
110
172
  data-focused={isFocused && !isSelected ? 'true' : undefined}
111
173
  data-folder={isFolder || undefined}
112
174
  data-expanded={isExpanded || undefined}
@@ -116,8 +178,11 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
116
178
  height: 'var(--tree-row-height)',
117
179
  gap: 'var(--tree-gap)',
118
180
  }}
181
+ {...(dnd.active ? draggable.listeners : {})}
182
+ {...(dnd.active ? draggable.attributes : {})}
119
183
  onClick={handleClick}
120
184
  onDoubleClick={handleDoubleClick}
185
+ onContextMenu={handleContextMenu}
121
186
  onFocus={() => setFocus(node.id)}
122
187
  className={cn(
123
188
  'group/row relative flex w-full select-none items-center pr-2 text-left',
@@ -127,6 +192,8 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
127
192
  rowStateClasses(appearance),
128
193
  'focus-visible:ring-1 focus-visible:ring-ring/50',
129
194
  isMatchingSearch && 'ring-1 ring-primary/30',
195
+ isCut && 'opacity-60',
196
+ isDragging && 'opacity-40',
130
197
  node.disabled && 'opacity-50',
131
198
  className,
132
199
  )}
@@ -142,6 +209,16 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
142
209
  />
143
210
  ) : null}
144
211
 
212
+ {/* DnD drop indicator — top/bottom line for sibling reorder,
213
+ fill for "drop into folder". */}
214
+ {dropPosition && !isDragging ? (
215
+ <TreeDropIndicator
216
+ position={dropPosition}
217
+ indent={6 + level * appearance.indent}
218
+ invalid={!isAllowedDrop}
219
+ />
220
+ ) : null}
221
+
145
222
  {showIndentGuides && level > 0 ? (
146
223
  <TreeIndentGuides level={level} indent={appearance.indent} />
147
224
  ) : null}
@@ -165,7 +242,14 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
165
242
  className="flex min-w-0 flex-1 items-center"
166
243
  style={{ gap: 'var(--tree-gap)' }}
167
244
  >
168
- {renderLabel ? (
245
+ {renamingId === node.id ? (
246
+ <TreeRenameInput
247
+ initialValue={getItemName(node)}
248
+ isFolder={isFolder}
249
+ onCommit={(next) => commitRename(node.id, next)}
250
+ onCancel={cancelRename}
251
+ />
252
+ ) : renderLabel ? (
169
253
  renderLabel(slot)
170
254
  ) : (
171
255
  <TreeLabel isMatchingSearch={isMatchingSearch}>{getItemName(node)}</TreeLabel>
@@ -20,3 +20,9 @@ export { TreeError } from './TreeError';
20
20
  export type { TreeErrorProps } from './TreeError';
21
21
  export { TreeIndentGuides } from './TreeIndentGuides';
22
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';