@djangocfg/ui-tools 2.1.415 → 2.1.417

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 (140) hide show
  1. package/dist/audio-player/index.cjs +2099 -0
  2. package/dist/audio-player/index.cjs.map +1 -0
  3. package/dist/audio-player/index.css +65 -0
  4. package/dist/audio-player/index.css.map +1 -0
  5. package/dist/audio-player/index.d.cts +174 -0
  6. package/dist/audio-player/index.d.ts +174 -0
  7. package/dist/audio-player/index.mjs +2076 -0
  8. package/dist/audio-player/index.mjs.map +1 -0
  9. package/dist/composer-registry/index.cjs +45 -0
  10. package/dist/composer-registry/index.cjs.map +1 -0
  11. package/dist/composer-registry/index.d.cts +73 -0
  12. package/dist/composer-registry/index.d.ts +73 -0
  13. package/dist/composer-registry/index.mjs +39 -0
  14. package/dist/composer-registry/index.mjs.map +1 -0
  15. package/dist/file-icon/index.d.cts +1 -1
  16. package/dist/file-icon/index.d.ts +1 -1
  17. package/dist/slots-ClRpIzoh.d.cts +88 -0
  18. package/dist/slots-ClRpIzoh.d.ts +88 -0
  19. package/dist/tree/index.cjs +2019 -279
  20. package/dist/tree/index.cjs.map +1 -1
  21. package/dist/tree/index.d.cts +731 -72
  22. package/dist/tree/index.d.ts +731 -72
  23. package/dist/tree/index.mjs +2009 -282
  24. package/dist/tree/index.mjs.map +1 -1
  25. package/package.json +18 -9
  26. package/src/tools/chat/README.md +111 -1
  27. package/src/tools/chat/composer/Composer.tsx +146 -25
  28. package/src/tools/chat/composer/ComposerRichTextarea.tsx +25 -0
  29. package/src/tools/chat/composer/index.ts +22 -0
  30. package/src/tools/chat/composer/slash/README.md +187 -0
  31. package/src/tools/chat/composer/slash/SlashHighlightTextarea.tsx +144 -0
  32. package/src/tools/chat/composer/slash/SlashMenu.tsx +142 -0
  33. package/src/tools/chat/composer/slash/SlashToken.tsx +57 -0
  34. package/src/tools/chat/composer/slash/index.ts +44 -0
  35. package/src/tools/chat/composer/slash/labels.ts +19 -0
  36. package/src/tools/chat/composer/slash/state.ts +168 -0
  37. package/src/tools/chat/composer/slash/types.ts +64 -0
  38. package/src/tools/chat/composer/slash/useSlashCommands.ts +204 -0
  39. package/src/tools/chat/composer/types.ts +8 -0
  40. package/src/tools/chat/context/ChatProvider.tsx +13 -78
  41. package/src/tools/chat/hooks/useAutoFocusOnStreamEnd.ts +12 -15
  42. package/src/tools/chat/hooks/useFocusOnEmptyClick.ts +4 -5
  43. package/src/tools/chat/launcher/header/ChatHeader.tsx +14 -19
  44. package/src/tools/chat/launcher/header/ChatHeaderActionButton.tsx +8 -12
  45. package/src/tools/chat/shell/SuggestedPrompts.tsx +194 -0
  46. package/src/tools/chat/shell/index.ts +6 -0
  47. package/src/tools/data/Listbox/lazy.tsx +1 -1
  48. package/src/tools/data/Masonry/lazy.tsx +1 -1
  49. package/src/tools/data/Timeline/lazy.tsx +1 -1
  50. package/src/tools/data/Tree/FinderTree.tsx +42 -0
  51. package/src/tools/data/Tree/README.md +337 -208
  52. package/src/tools/data/Tree/TreeDndProvider.tsx +137 -0
  53. package/src/tools/data/Tree/TreeRoot.tsx +111 -72
  54. package/src/tools/data/Tree/__tests__/dnd.test.ts +160 -0
  55. package/src/tools/data/Tree/__tests__/keyboard.test.ts +137 -0
  56. package/src/tools/data/Tree/__tests__/renameUtils.test.ts +52 -0
  57. package/src/tools/data/Tree/__tests__/selection.test.ts +227 -0
  58. package/src/tools/data/Tree/components/TreeDropIndicator.tsx +65 -0
  59. package/src/tools/data/Tree/components/TreeEmptyArea.tsx +160 -0
  60. package/src/tools/data/Tree/components/TreeRenameInput.tsx +114 -0
  61. package/src/tools/data/Tree/components/TreeRow.tsx +103 -8
  62. package/src/tools/data/Tree/components/index.ts +6 -0
  63. package/src/tools/data/Tree/context/TreeContext.tsx +223 -363
  64. package/src/tools/data/Tree/context/TreeContextValue.ts +139 -0
  65. package/src/tools/data/Tree/context/async-children/collect-ids.ts +27 -0
  66. package/src/tools/data/Tree/context/async-children/index.ts +8 -0
  67. package/src/tools/data/Tree/context/async-children/use-async-children.ts +157 -0
  68. package/src/tools/data/Tree/context/clipboard/index.ts +4 -0
  69. package/src/tools/data/Tree/context/clipboard/use-clipboard.ts +115 -0
  70. package/src/tools/data/Tree/context/dnd/index.ts +8 -0
  71. package/src/tools/data/Tree/context/dnd/use-dnd.ts +194 -0
  72. package/src/tools/data/Tree/context/expansion/index.ts +4 -0
  73. package/src/tools/data/Tree/context/expansion/use-expansion.ts +55 -0
  74. package/src/tools/data/Tree/context/hooks.ts +68 -1
  75. package/src/tools/data/Tree/context/index.ts +3 -0
  76. package/src/tools/data/Tree/context/menu/builtin-actions.ts +357 -0
  77. package/src/tools/data/Tree/context/menu/index.ts +11 -0
  78. package/src/tools/data/Tree/context/menu/render.tsx +75 -0
  79. package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +141 -0
  80. package/src/tools/data/Tree/context/persist/index.ts +4 -0
  81. package/src/tools/data/Tree/context/persist/use-persist-sync.ts +74 -0
  82. package/src/tools/data/Tree/context/rename/index.ts +4 -0
  83. package/src/tools/data/Tree/context/rename/use-rename.ts +113 -0
  84. package/src/tools/data/Tree/context/selection/index.ts +4 -0
  85. package/src/tools/data/Tree/context/selection/use-selection.ts +146 -0
  86. package/src/tools/data/Tree/context/state/index.ts +6 -0
  87. package/src/tools/data/Tree/context/state/initial.ts +41 -0
  88. package/src/tools/data/Tree/context/state/reducer.ts +76 -0
  89. package/src/tools/data/Tree/context/state/types.ts +46 -0
  90. package/src/tools/data/Tree/data/clipboard.ts +33 -0
  91. package/src/tools/data/Tree/data/dnd.ts +123 -0
  92. package/src/tools/data/Tree/data/finderShortcuts.ts +67 -0
  93. package/src/tools/data/Tree/data/index.ts +19 -0
  94. package/src/tools/data/Tree/data/renameUtils.ts +51 -0
  95. package/src/tools/data/Tree/data/selection.ts +157 -0
  96. package/src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
  97. package/src/tools/data/Tree/hooks/finder-hotkeys/index.ts +8 -0
  98. package/src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
  99. package/src/tools/data/Tree/hooks/index.ts +23 -4
  100. package/src/tools/data/Tree/hooks/keyboard/activation.ts +27 -0
  101. package/src/tools/data/Tree/hooks/keyboard/arrow-nav.ts +26 -0
  102. package/src/tools/data/Tree/hooks/keyboard/expand-collapse.ts +54 -0
  103. package/src/tools/data/Tree/hooks/keyboard/index.ts +10 -0
  104. package/src/tools/data/Tree/hooks/keyboard/types.ts +39 -0
  105. package/src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
  106. package/src/tools/data/Tree/hooks/type-ahead/index.ts +5 -0
  107. package/src/tools/data/Tree/hooks/type-ahead/match-prefix.ts +42 -0
  108. package/src/tools/data/Tree/hooks/{useTreeTypeAhead.ts → type-ahead/use-tree-type-ahead.ts} +8 -19
  109. package/src/tools/data/Tree/index.tsx +26 -2
  110. package/src/tools/data/Tree/types/activation.ts +30 -0
  111. package/src/tools/data/Tree/types/adapter.ts +70 -0
  112. package/src/tools/data/Tree/types/index.ts +27 -0
  113. package/src/tools/data/Tree/types/labels.ts +97 -0
  114. package/src/tools/data/Tree/types/loader.ts +9 -0
  115. package/src/tools/data/Tree/types/node.ts +38 -0
  116. package/src/tools/data/Tree/types/root-props.ts +158 -0
  117. package/src/tools/data/Tree/types/selection.ts +3 -0
  118. package/src/tools/data/Tree/types/slots.ts +64 -0
  119. package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +6 -9
  120. package/src/tools/dev/OpenapiViewer/components/DocsLayout/index.tsx +2 -4
  121. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +85 -0
  122. package/src/tools/forms/MarkdownEditor/index.ts +1 -0
  123. package/src/tools/forms/MarkdownEditor/lazy.tsx +6 -0
  124. package/src/tools/forms/MarkdownEditor/slash/SlashCommandNode.ts +162 -0
  125. package/src/tools/forms/MarkdownEditor/slash/index.ts +4 -0
  126. package/src/tools/forms/MarkdownEditor/slash/syncSlashNode.ts +97 -0
  127. package/src/tools/forms/MarkdownEditor/slash/types.ts +13 -0
  128. package/src/tools/forms/MarkdownEditor/styles.css +18 -0
  129. package/src/tools/input/SpeechRecognition/widgets/VoiceComposerSlot.tsx +11 -12
  130. package/src/tools/integration/ComposerRegistry/index.ts +105 -0
  131. package/src/tools/media/AudioPlayer/Player.tsx +2 -0
  132. package/src/tools/media/AudioPlayer/PlayerShell.tsx +37 -22
  133. package/src/tools/media/AudioPlayer/lazy.tsx +30 -42
  134. package/src/tools/media/AudioPlayer/parts/Controls/IconButton.tsx +10 -11
  135. package/src/tools/media/AudioPlayer/parts/Controls/VolumeControl.tsx +52 -115
  136. package/src/tools/media/AudioPlayer/types.ts +15 -0
  137. package/dist/types-j2vhn4Kv.d.cts +0 -241
  138. package/dist/types-j2vhn4Kv.d.ts +0 -241
  139. package/src/tools/data/Tree/hooks/useTreeKeyboard.ts +0 -171
  140. package/src/tools/data/Tree/types.ts +0 -217
@@ -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,3 @@
1
+ 'use client';
2
+
3
+ export type TreeSelectionMode = 'none' | 'single' | 'multiple';
@@ -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;
@@ -12,7 +12,6 @@ import {
12
12
  Tooltip,
13
13
  TooltipContent,
14
14
  TooltipTrigger,
15
- SafeTooltipProvider,
16
15
  } from '@djangocfg/ui-core/components';
17
16
  import { cn } from '@djangocfg/ui-core/lib';
18
17
 
@@ -119,13 +118,11 @@ export function MetaActions({ anchor, presentSections }: MetaActionsProps) {
119
118
  ) : null;
120
119
 
121
120
  return (
122
- <SafeTooltipProvider delayDuration={200}>
123
- <div className="flex items-center gap-0.5">
124
- <IconButton label={linkLabel} onClick={copyLink} active={linkCopied}>
125
- {linkIcon}
126
- </IconButton>
127
- {toggleAllNode}
128
- </div>
129
- </SafeTooltipProvider>
121
+ <div className="flex items-center gap-0.5">
122
+ <IconButton label={linkLabel} onClick={copyLink} active={linkCopied}>
123
+ {linkIcon}
124
+ </IconButton>
125
+ {toggleAllNode}
126
+ </div>
130
127
  );
131
128
  }
@@ -3,7 +3,7 @@
3
3
  import React, { useCallback, useMemo, useRef, useState } from 'react';
4
4
  import { keyBy } from 'lodash-es';
5
5
 
6
- import { Skeleton, TooltipProvider } from '@djangocfg/ui-core/components';
6
+ import { Skeleton } from '@djangocfg/ui-core/components';
7
7
  import { useMediaQuery } from '@djangocfg/ui-core/hooks';
8
8
 
9
9
  import useOpenApiSchema from '../../hooks/useOpenApiSchema';
@@ -234,8 +234,7 @@ export const DocsLayout: React.FC = () => {
234
234
  // ─── Desktop ──────────────────────────────────────────────────────────
235
235
 
236
236
  return (
237
- <TooltipProvider delayDuration={350}>
238
- <div className="grid grid-cols-[260px_minmax(0,1fr)] items-start">
237
+ <div className="grid grid-cols-[260px_minmax(0,1fr)] items-start">
239
238
  <EndpointDraftSync schemaId={currentSchema?.id ?? null} />
240
239
  <div
241
240
  className="sticky top-[var(--navbar-height,64px)]"
@@ -283,6 +282,5 @@ export const DocsLayout: React.FC = () => {
283
282
  above the whole layout (sidebar + navbar included). */}
284
283
  <SlideInPlayground open={slideOpen} onClose={handleCloseSlide} />
285
284
  </div>
286
- </TooltipProvider>
287
285
  );
288
286
  };
@@ -13,6 +13,9 @@ import {
13
13
  } from 'lucide-react';
14
14
  import { createMentionSuggestion } from './createMentionSuggestion';
15
15
  import { mentionPresets } from './mentionPresets';
16
+ import { SlashCommandNode } from './slash/SlashCommandNode';
17
+ import { syncLeadingSlashNode } from './slash/syncSlashNode';
18
+ import type { SlashCommandInfo } from './slash/types';
16
19
  import { SubmitOnEnter } from './submitOnEnter';
17
20
  import type { MentionAttrs, MentionConfig } from './types';
18
21
  import './styles.css';
@@ -84,6 +87,23 @@ export interface MarkdownEditorProps {
84
87
  * no-op for the live editor.
85
88
  */
86
89
  mentions?: MentionConfig;
90
+ /**
91
+ * Slash-command verb list. When set, the editor registers a
92
+ * `SlashCommandNode` extension and replaces a leading `/verb` (whose
93
+ * verb is in this list) in the document with an atomic chip — the
94
+ * TipTap analogue of the plain `<SlashHighlightTextarea>` mirror.
95
+ *
96
+ * Like `mentions`, this is captured by `useEditor` exactly once on
97
+ * first render. To register slash commands at all, pass `[]` from
98
+ * the very first render and mutate / swap as needed. The conversion
99
+ * effect re-reads the list on every value sync (it lives in a ref),
100
+ * so verbs added later are recognised next time the buffer is set.
101
+ *
102
+ * Lives in `@djangocfg/ui-tools/markdown-editor` (not chat) so the
103
+ * editor stays a leaf dependency. Structurally compatible with the
104
+ * chat package's `SlashCommand[]` — pass either directly.
105
+ */
106
+ slashCommands?: readonly SlashCommandInfo[];
87
107
  /** Called when mentioned IDs change */
88
108
  onMentionIdsChange?: (ids: string[]) => void;
89
109
  /**
@@ -136,6 +156,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
136
156
  showToolbar = true,
137
157
  unstyled = false,
138
158
  mentions,
159
+ slashCommands,
139
160
  onMentionIdsChange,
140
161
  onSubmit,
141
162
  },
@@ -150,6 +171,35 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
150
171
  onSubmitRef.current = onSubmit;
151
172
  const isExternalUpdate = useRef(false);
152
173
 
174
+ // Slash list lives in a ref so verbs added at runtime (e.g. context-
175
+ // aware commands registered after a first reply) are still recognised
176
+ // by the post-`setContent` conversion pass. The editor extension itself
177
+ // is registered once on mount — that is intentional and matches how
178
+ // `mentions` works (Tiptap's `useEditor` captures extensions once).
179
+ const slashCommandsRef = useRef<readonly SlashCommandInfo[]>(
180
+ slashCommands ?? [],
181
+ );
182
+ slashCommandsRef.current = slashCommands ?? [];
183
+ const slashEnabledOnMountRef = useRef<boolean>(slashCommands !== undefined);
184
+ const slashWarnedRef = useRef(false);
185
+ if (
186
+ process.env.NODE_ENV !== 'production' &&
187
+ !slashEnabledOnMountRef.current &&
188
+ slashCommands !== undefined &&
189
+ !slashWarnedRef.current
190
+ ) {
191
+ slashWarnedRef.current = true;
192
+ // eslint-disable-next-line no-console
193
+ console.warn(
194
+ '[MarkdownEditor] `slashCommands` flipped from undefined to a list ' +
195
+ 'after mount. Tiptap only installs the SlashCommandNode extension ' +
196
+ 'on first render — the in-editor chip will NOT appear for this ' +
197
+ 'editor instance. Pass `[]` from the very first render and mutate ' +
198
+ 'the array in place (or swap references) so the conversion effect ' +
199
+ 'sees the new verbs.',
200
+ );
201
+ }
202
+
153
203
  // ── Dev-mode trap detector ──
154
204
  // Tiptap initialises the editor once with the extensions array from
155
205
  // first render. If `mentions` is undefined on mount and becomes
@@ -194,6 +244,16 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
194
244
  }),
195
245
  ];
196
246
 
247
+ // SlashCommandNode — TipTap atom that paints the `/verb` chip.
248
+ // Registered whenever `slashCommands` was defined on first render
249
+ // (the empty array still counts as "I want this feature"). The
250
+ // node has no suggestion plugin of its own — the chat composer's
251
+ // floating menu drives selection and the editor only needs to
252
+ // know how to render + serialize the atom.
253
+ if (slashEnabledOnMountRef.current) {
254
+ exts.push(SlashCommandNode);
255
+ }
256
+
197
257
  if (mentions) {
198
258
  // ── Why .extend() with renderMarkdown ──
199
259
  //
@@ -260,6 +320,20 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
260
320
  contentType: 'markdown',
261
321
  onUpdate: ({ editor }) => {
262
322
  if (isExternalUpdate.current) return;
323
+ // Promote a freshly-typed `/verb ` text run into a chip so users
324
+ // who type the command without going through the menu still see
325
+ // the highlight — matches the plain mirror's behaviour where
326
+ // `extractSlashToken` works for any leading slash. The helper is
327
+ // idempotent and a no-op when no leading text-slash exists.
328
+ if (slashEnabledOnMountRef.current) {
329
+ const replaced = syncLeadingSlashNode(editor, slashCommandsRef.current);
330
+ if (replaced) {
331
+ // The chain() above triggers an extra onUpdate. The string
332
+ // serialisation is unchanged (atom flattens to its token via
333
+ // `renderText`), so we still want the outer onChange to fire
334
+ // with the canonical value. Falling through is fine.
335
+ }
336
+ }
263
337
  onChange(getMarkdown(editor));
264
338
 
265
339
  if (onMentionIdsChange) {
@@ -284,6 +358,17 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
284
358
  if (current !== value) {
285
359
  isExternalUpdate.current = true;
286
360
  editor.commands.setContent(value, { contentType: 'markdown', emitUpdate: false });
361
+ // After the new content is written, convert a leading `/verb` text
362
+ // run into a `SlashCommandNode` atom so the chip appears. The
363
+ // helper is a no-op when (a) slash commands are not enabled,
364
+ // (b) the buffer doesn't start with a known verb, or (c) the
365
+ // document already begins with the atom. `emitUpdate: false` on
366
+ // setContent + `isExternalUpdate.current` still being `true`
367
+ // suppresses the onUpdate -> onChange feedback loop the chain()
368
+ // call would otherwise trigger.
369
+ if (slashEnabledOnMountRef.current) {
370
+ syncLeadingSlashNode(editor, slashCommandsRef.current);
371
+ }
287
372
  isExternalUpdate.current = false;
288
373
  }
289
374
  }, [value, editor]);
@@ -7,3 +7,4 @@ export type {
7
7
  MentionMarkdownRenderer,
8
8
  } from './types';
9
9
  export { mentionPresets } from './mentionPresets';
10
+ export type { SlashCommandInfo } from './slash/types';
@@ -65,3 +65,9 @@ export type {
65
65
  MentionAttrs,
66
66
  MentionMarkdownRenderer,
67
67
  } from './types';
68
+
69
+ // Slash command surface — opt-in by passing `slashCommands` to
70
+ // `MarkdownEditor` (or `<ComposerRichTextarea>`, which forwards). The
71
+ // node itself is mounted by the editor on first render. `SlashCommandInfo`
72
+ // is structurally compatible with the chat package's `SlashCommand`.
73
+ export type { SlashCommandInfo } from './slash/types';