@djangocfg/ui-tools 2.1.416 → 2.1.418
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/dist/audio-player/index.cjs +2098 -0
- package/dist/audio-player/index.cjs.map +1 -0
- package/dist/audio-player/index.css +65 -0
- package/dist/audio-player/index.css.map +1 -0
- package/dist/audio-player/index.d.cts +166 -0
- package/dist/audio-player/index.d.ts +166 -0
- package/dist/audio-player/index.mjs +2075 -0
- package/dist/audio-player/index.mjs.map +1 -0
- package/dist/composer-registry/index.cjs +45 -0
- package/dist/composer-registry/index.cjs.map +1 -0
- package/dist/composer-registry/index.d.cts +73 -0
- package/dist/composer-registry/index.d.ts +73 -0
- package/dist/composer-registry/index.mjs +39 -0
- package/dist/composer-registry/index.mjs.map +1 -0
- package/dist/tree/index.cjs +82 -63
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +15 -1
- package/dist/tree/index.d.ts +15 -1
- package/dist/tree/index.mjs +83 -64
- package/dist/tree/index.mjs.map +1 -1
- package/package.json +38 -17
- package/src/tools/chat/composer/Composer.tsx +8 -8
- package/src/tools/chat/context/ChatProvider.tsx +13 -78
- package/src/tools/chat/hooks/useAutoFocusOnStreamEnd.ts +12 -15
- package/src/tools/chat/hooks/useFocusOnEmptyClick.ts +4 -5
- package/src/tools/chat/launcher/header/ChatHeader.tsx +14 -19
- package/src/tools/chat/launcher/header/ChatHeaderActionButton.tsx +8 -12
- package/src/tools/data/Tree/TreeRoot.tsx +33 -109
- package/src/tools/data/Tree/context/TreeContext.tsx +22 -3
- package/src/tools/data/Tree/context/menu/index.ts +1 -0
- package/src/tools/data/Tree/context/menu/render.tsx +75 -0
- package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +16 -2
- package/src/tools/data/Tree/index.tsx +1 -0
- package/src/tools/data/Tree/types/index.ts +1 -1
- package/src/tools/data/Tree/types/root-props.ts +16 -0
- package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +6 -9
- package/src/tools/dev/OpenapiViewer/components/DocsLayout/index.tsx +2 -4
- package/src/tools/forms/CodeEditor/components/Editor.tsx +19 -0
- package/src/tools/forms/CodeEditor/types/index.ts +7 -0
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +40 -0
- package/src/tools/forms/MarkdownEditor/styles.css +174 -21
- package/src/tools/forms/NotionEditor/CustomKeymap.ts +48 -0
- package/src/tools/forms/NotionEditor/LinkDialog.tsx +133 -0
- package/src/tools/forms/NotionEditor/NotionEditor.tsx +304 -0
- package/src/tools/forms/NotionEditor/SlashExtension.ts +32 -0
- package/src/tools/forms/NotionEditor/SlashList.tsx +136 -0
- package/src/tools/forms/NotionEditor/TaskItemView.tsx +41 -0
- package/src/tools/forms/NotionEditor/createSlashSuggestion.ts +121 -0
- package/src/tools/forms/NotionEditor/extensions.ts +105 -0
- package/src/tools/forms/NotionEditor/index.ts +1 -0
- package/src/tools/forms/NotionEditor/lazy.tsx +44 -0
- package/src/tools/forms/NotionEditor/slashItems.ts +159 -0
- package/src/tools/forms/NotionEditor/styles.css +478 -0
- package/src/tools/forms/NotionEditor/types.ts +28 -0
- package/src/tools/input/SpeechRecognition/widgets/VoiceComposerSlot.tsx +11 -12
- package/src/tools/integration/ComposerRegistry/index.ts +105 -0
- package/src/tools/media/AudioPlayer/Player.tsx +2 -0
- package/src/tools/media/AudioPlayer/PlayerShell.tsx +29 -22
- package/src/tools/media/AudioPlayer/lazy.tsx +30 -42
- package/src/tools/media/AudioPlayer/parts/Controls/IconButton.tsx +10 -11
- package/src/tools/media/AudioPlayer/parts/Controls/VolumeControl.tsx +52 -115
- package/src/tools/media/AudioPlayer/types.ts +8 -0
- package/src/tools/media/ImageViewer/components/ImageViewer.tsx +8 -0
- package/src/tools/media/ImageViewer/types.ts +4 -0
- package/src/tools/media/VideoPlayer/VideoPlayer.tsx +20 -1
- package/src/tools/media/VideoPlayer/types.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.418",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -66,10 +66,15 @@
|
|
|
66
66
|
"import": "./dist/file-icon/index.mjs",
|
|
67
67
|
"require": "./dist/file-icon/index.cjs"
|
|
68
68
|
},
|
|
69
|
+
"./composer-registry": {
|
|
70
|
+
"types": "./dist/composer-registry/index.d.ts",
|
|
71
|
+
"import": "./dist/composer-registry/index.mjs",
|
|
72
|
+
"require": "./dist/composer-registry/index.cjs"
|
|
73
|
+
},
|
|
69
74
|
"./audio-player": {
|
|
70
|
-
"types": "./
|
|
71
|
-
"import": "./
|
|
72
|
-
"require": "./
|
|
75
|
+
"types": "./dist/audio-player/index.d.ts",
|
|
76
|
+
"import": "./dist/audio-player/index.mjs",
|
|
77
|
+
"require": "./dist/audio-player/index.cjs"
|
|
73
78
|
},
|
|
74
79
|
"./chat": {
|
|
75
80
|
"types": "./src/tools/chat/lazy.tsx",
|
|
@@ -111,6 +116,11 @@
|
|
|
111
116
|
"import": "./src/tools/forms/MarkdownEditor/lazy.tsx",
|
|
112
117
|
"require": "./src/tools/forms/MarkdownEditor/lazy.tsx"
|
|
113
118
|
},
|
|
119
|
+
"./notion-editor": {
|
|
120
|
+
"types": "./src/tools/forms/NotionEditor/lazy.tsx",
|
|
121
|
+
"import": "./src/tools/forms/NotionEditor/lazy.tsx",
|
|
122
|
+
"require": "./src/tools/forms/NotionEditor/lazy.tsx"
|
|
123
|
+
},
|
|
114
124
|
"./markdown-message": {
|
|
115
125
|
"types": "./src/tools/dev/MarkdownMessage/index.ts",
|
|
116
126
|
"import": "./src/tools/dev/MarkdownMessage/index.ts",
|
|
@@ -254,8 +264,8 @@
|
|
|
254
264
|
"test:watch": "vitest"
|
|
255
265
|
},
|
|
256
266
|
"peerDependencies": {
|
|
257
|
-
"@djangocfg/i18n": "^2.1.
|
|
258
|
-
"@djangocfg/ui-core": "^2.1.
|
|
267
|
+
"@djangocfg/i18n": "^2.1.418",
|
|
268
|
+
"@djangocfg/ui-core": "^2.1.418",
|
|
259
269
|
"consola": "^3.4.2",
|
|
260
270
|
"lodash-es": "^4.18.1",
|
|
261
271
|
"lucide-react": "^0.545.0",
|
|
@@ -274,14 +284,25 @@
|
|
|
274
284
|
"@rjsf/utils": "^6.1.2",
|
|
275
285
|
"@rjsf/validator-ajv8": "^6.1.2",
|
|
276
286
|
"@rpldy/uploady": "^1.8.5",
|
|
277
|
-
"@tiptap/core": "^3.
|
|
278
|
-
"@tiptap/extension-
|
|
279
|
-
"@tiptap/extension-
|
|
280
|
-
"@tiptap/
|
|
281
|
-
"@tiptap/
|
|
282
|
-
"@tiptap/
|
|
283
|
-
"@tiptap/
|
|
284
|
-
"@tiptap/
|
|
287
|
+
"@tiptap/core": "^3.23.0",
|
|
288
|
+
"@tiptap/extension-bubble-menu": "^3.23.0",
|
|
289
|
+
"@tiptap/extension-code-block-lowlight": "^3.23.0",
|
|
290
|
+
"@tiptap/extension-highlight": "^3.23.0",
|
|
291
|
+
"@tiptap/extension-mention": "^3.23.0",
|
|
292
|
+
"@tiptap/extension-placeholder": "^3.23.0",
|
|
293
|
+
"@tiptap/extension-table": "^3.23.0",
|
|
294
|
+
"@tiptap/extension-table-cell": "^3.23.0",
|
|
295
|
+
"@tiptap/extension-table-header": "^3.23.0",
|
|
296
|
+
"@tiptap/extension-table-row": "^3.23.0",
|
|
297
|
+
"@tiptap/extension-task-item": "^3.23.0",
|
|
298
|
+
"@tiptap/extension-task-list": "^3.23.0",
|
|
299
|
+
"@tiptap/markdown": "^3.23.0",
|
|
300
|
+
"@tiptap/pm": "^3.23.0",
|
|
301
|
+
"@tiptap/react": "^3.23.0",
|
|
302
|
+
"@tiptap/starter-kit": "^3.23.0",
|
|
303
|
+
"@tiptap/suggestion": "^3.23.0",
|
|
304
|
+
"lowlight": "^3.3.0",
|
|
305
|
+
"tiptap-extension-global-drag-handle": "^0.1.18",
|
|
285
306
|
"@wavesurfer/react": "^1.0.12",
|
|
286
307
|
"@radix-ui/react-slot": "^1.2.4",
|
|
287
308
|
"@radix-ui/react-direction": "^1.1.1",
|
|
@@ -319,9 +340,9 @@
|
|
|
319
340
|
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
320
341
|
},
|
|
321
342
|
"devDependencies": {
|
|
322
|
-
"@djangocfg/i18n": "^2.1.
|
|
323
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
324
|
-
"@djangocfg/ui-core": "^2.1.
|
|
343
|
+
"@djangocfg/i18n": "^2.1.418",
|
|
344
|
+
"@djangocfg/typescript-config": "^2.1.418",
|
|
345
|
+
"@djangocfg/ui-core": "^2.1.418",
|
|
325
346
|
"@types/lodash-es": "^4.17.12",
|
|
326
347
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
327
348
|
"@types/node": "^25.2.3",
|
|
@@ -4,6 +4,7 @@ import { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
4
4
|
|
|
5
5
|
import { Textarea } from '@djangocfg/ui-core/components';
|
|
6
6
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
7
|
+
import { attachComposer } from '@djangocfg/ui-tools/composer-registry';
|
|
7
8
|
|
|
8
9
|
import { useChatContextOptional } from '../context';
|
|
9
10
|
import type { UseChatComposerReturn } from '../hooks/useChatComposer';
|
|
@@ -174,10 +175,10 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
174
175
|
// host overrides it explicitly — see §3.1 of the redesign doc.
|
|
175
176
|
const layout: ComposerLayout = layoutProp ?? (size === 'sm' ? 'inline' : 'stacked');
|
|
176
177
|
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
178
|
+
// Publish the composer's imperative handle to the cross-tool
|
|
179
|
+
// composer registry (`@djangocfg/ui-tools/composer-registry`).
|
|
180
|
+
// Consumers (useAutoFocusOnStreamEnd, VoiceComposerSlot) read it
|
|
181
|
+
// from there — works the same inside or outside a ChatProvider.
|
|
181
182
|
const composerFocus = composer.focus;
|
|
182
183
|
const composerSetValue = composer.setValue;
|
|
183
184
|
const textareaRef = composer.textareaRef;
|
|
@@ -189,8 +190,8 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
189
190
|
// ours so we don't overwrite it (child effects run before parent).
|
|
190
191
|
const hasCustomTextarea = !!slots?.Textarea;
|
|
191
192
|
useEffect(() => {
|
|
192
|
-
if (
|
|
193
|
-
|
|
193
|
+
if (hasCustomTextarea) return;
|
|
194
|
+
return attachComposer({
|
|
194
195
|
focus: composerFocus,
|
|
195
196
|
moveCursorToEnd: () => {
|
|
196
197
|
const el = textareaRef.current;
|
|
@@ -201,8 +202,7 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
201
202
|
getValue: () => getValueRef.current(),
|
|
202
203
|
setValue: composerSetValue,
|
|
203
204
|
});
|
|
204
|
-
|
|
205
|
-
}, [register, hasCustomTextarea, composerFocus, composerSetValue, textareaRef]);
|
|
205
|
+
}, [hasCustomTextarea, composerFocus, composerSetValue, textareaRef]);
|
|
206
206
|
|
|
207
207
|
const inlineStart = composerSlots?.inlineStart;
|
|
208
208
|
const inlineEnd = composerSlots?.inlineEnd;
|
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
useEffect,
|
|
9
9
|
useMemo,
|
|
10
10
|
useRef,
|
|
11
|
-
useState,
|
|
12
11
|
} from 'react';
|
|
13
12
|
|
|
13
|
+
import { getActiveComposer } from '@djangocfg/ui-tools/composer-registry';
|
|
14
|
+
|
|
14
15
|
import type { ChatConfig, ChatLabels, ChatTransport } from '../types';
|
|
15
16
|
import { DEFAULT_LABELS } from '../types';
|
|
16
17
|
import type { BlockRegistry } from '../messages/blocks';
|
|
@@ -34,18 +35,12 @@ import type { ChatAudioConfig, UseChatAudioReturn } from '../core/audio/types';
|
|
|
34
35
|
* Consumed by `VoiceComposerSlot` for the focus / move-caret behaviour
|
|
35
36
|
* during live dictation.
|
|
36
37
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
getValue?: () => string;
|
|
44
|
-
/** Replace the current draft text. Voice dictation uses this to push
|
|
45
|
-
* interim + final transcripts into the composer without owning a
|
|
46
|
-
* controlled binding. */
|
|
47
|
-
setValue?: (value: string) => void;
|
|
48
|
-
}
|
|
38
|
+
// `ComposerHandle` lives in `@djangocfg/ui-tools/composer-registry` —
|
|
39
|
+
// the cross-tool registry shared by chat (producer) and
|
|
40
|
+
// speech-recognition (consumer). Re-export here so existing call sites
|
|
41
|
+
// `import { ComposerHandle } from '@djangocfg/ui-tools/chat'` keep
|
|
42
|
+
// working unchanged.
|
|
43
|
+
export type { ComposerHandle } from '@djangocfg/ui-tools/composer-registry';
|
|
49
44
|
|
|
50
45
|
export interface ChatContextValue extends UseChatReturn {
|
|
51
46
|
layout: UseChatLayoutReturn;
|
|
@@ -56,13 +51,6 @@ export interface ChatContextValue extends UseChatReturn {
|
|
|
56
51
|
* Components like ``AudioToggle`` use this to auto-hide when there
|
|
57
52
|
* is nothing to mute. */
|
|
58
53
|
hasAudio: boolean;
|
|
59
|
-
/** Composer registry. The built-in `<Composer>` calls
|
|
60
|
-
* `registerComposer({ focus })` on mount; custom composers (e.g.
|
|
61
|
-
* cmdop's MarkdownEditor wrapper) do the same via the
|
|
62
|
-
* `useRegisterComposer` helper. Read it via `composer?.focus()` —
|
|
63
|
-
* null until any composer has mounted. Plan64 follow-up. */
|
|
64
|
-
composer: ComposerHandle | null;
|
|
65
|
-
registerComposer: (handle: ComposerHandle | null) => void;
|
|
66
54
|
/** Registry of `kind` → renderer for `message.blocks`. `null` when the
|
|
67
55
|
* host wired none — `<MessageBubble>` then uses `BUILTIN_BLOCK_REGISTRY`. */
|
|
68
56
|
blockRegistry: BlockRegistry | null;
|
|
@@ -70,48 +58,6 @@ export interface ChatContextValue extends UseChatReturn {
|
|
|
70
58
|
|
|
71
59
|
const Ctx = createContext<ChatContextValue | null>(null);
|
|
72
60
|
|
|
73
|
-
// ─── Dual-bundle guard ────────────────────────────────────────────────────
|
|
74
|
-
//
|
|
75
|
-
// `@djangocfg/ui-tools` ships its root export through `dist/` (compiled) but
|
|
76
|
-
// most subpaths resolve to raw `src/`. If a consumer mixes them — e.g.
|
|
77
|
-
// `ChatRoot` from the root barrel and `VoiceComposerSlot` from
|
|
78
|
-
// `@djangocfg/ui-tools/speech-recognition` — `createContext(...)` runs twice
|
|
79
|
-
// and produces two distinct context instances. The Composer registers its
|
|
80
|
-
// handle on one; `useChatContextOptional()` in the voice slot reads from the
|
|
81
|
-
// other, sees `null`, and silently drops every transcript.
|
|
82
|
-
//
|
|
83
|
-
// We tag the global with our module identity. When more than one tag is
|
|
84
|
-
// observed the consumer is told exactly what to do.
|
|
85
|
-
type GuardSlot = { stamps: Set<symbol>; warned: boolean };
|
|
86
|
-
const GLOBAL_KEY = '__djangocfg_chat_ctx_stamps__';
|
|
87
|
-
const stamp = Symbol('djangocfg.chat.ctx');
|
|
88
|
-
function markCtxLoad(): void {
|
|
89
|
-
if (typeof globalThis === 'undefined') return;
|
|
90
|
-
const g = globalThis as unknown as Record<string, GuardSlot | undefined>;
|
|
91
|
-
let slot = g[GLOBAL_KEY];
|
|
92
|
-
if (!slot) {
|
|
93
|
-
slot = { stamps: new Set(), warned: false };
|
|
94
|
-
g[GLOBAL_KEY] = slot;
|
|
95
|
-
}
|
|
96
|
-
slot.stamps.add(stamp);
|
|
97
|
-
if (slot.stamps.size > 1 && !slot.warned && process.env.NODE_ENV !== 'production') {
|
|
98
|
-
slot.warned = true;
|
|
99
|
-
// eslint-disable-next-line no-console
|
|
100
|
-
console.warn(
|
|
101
|
-
'[@djangocfg/ui-tools/chat] Two ChatProvider context instances detected — ' +
|
|
102
|
-
'this means `@djangocfg/ui-tools` was loaded twice (one bundle via the root ' +
|
|
103
|
-
'`.` export → `dist/`, another via a `./chat` / `./speech-recognition` subpath → `src/`). ' +
|
|
104
|
-
'Symptom: `useChatContextOptional()` returns `null` for descendants of `<ChatProvider>`, ' +
|
|
105
|
-
'so VoiceComposerSlot drops transcripts, useChatReset silently no-ops, etc. ' +
|
|
106
|
-
'\n\nFix: import every Chat surface from the SAME subpath. Recommended:\n' +
|
|
107
|
-
" import { ChatRoot, ChatLauncher, useChatContextOptional, … } from '@djangocfg/ui-tools/chat';\n" +
|
|
108
|
-
" import { VoiceComposerSlot } from '@djangocfg/ui-tools/speech-recognition';\n" +
|
|
109
|
-
'\n(See packages/ui-tools/src/tools/Chat/README.md → "Anti-patterns".)',
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
markCtxLoad();
|
|
114
|
-
|
|
115
61
|
export interface ChatProviderProps {
|
|
116
62
|
transport: ChatTransport;
|
|
117
63
|
config?: ChatConfig;
|
|
@@ -240,26 +186,17 @@ export function ChatProvider({
|
|
|
240
186
|
);
|
|
241
187
|
}, [audio]);
|
|
242
188
|
|
|
243
|
-
// Composer registry — kept in state (not a ref) so consumers
|
|
244
|
-
// observing `ctx.composer` re-render when a composer mounts /
|
|
245
|
-
// unmounts. The setter is the API surface; pass it to the composer
|
|
246
|
-
// on mount and call with `null` on unmount.
|
|
247
|
-
const [composer, setComposer] = useState<ComposerHandle | null>(null);
|
|
248
|
-
const registerComposer = useCallback((handle: ComposerHandle | null) => {
|
|
249
|
-
setComposer(handle);
|
|
250
|
-
}, []);
|
|
251
|
-
|
|
252
189
|
// Re-focus the composer on the streaming → idle edge. Lives here (not
|
|
253
190
|
// in `ChatRoot`) so it works for *every* usage pattern — `ChatRoot`,
|
|
254
191
|
// a hand-rolled `ChatProvider` + `Composer` layout, or headless — as
|
|
255
|
-
// long as a composer registered its handle.
|
|
256
|
-
|
|
257
|
-
|
|
192
|
+
// long as a composer registered its handle. The active handle lives
|
|
193
|
+
// in `@djangocfg/ui-tools/composer-registry` — a single cross-tool
|
|
194
|
+
// registry shared with `<VoiceComposerSlot>`.
|
|
258
195
|
useStreamEndFocus({
|
|
259
196
|
isStreaming: chat.isStreaming,
|
|
260
197
|
enabled: autoFocusOnStreamEnd,
|
|
261
198
|
delayMs: 0,
|
|
262
|
-
resolveTarget: () =>
|
|
199
|
+
resolveTarget: () => getActiveComposer(),
|
|
263
200
|
});
|
|
264
201
|
|
|
265
202
|
const value = useMemo<ChatContextValue>(
|
|
@@ -270,11 +207,9 @@ export function ChatProvider({
|
|
|
270
207
|
labels,
|
|
271
208
|
audio: audioApi,
|
|
272
209
|
hasAudio,
|
|
273
|
-
composer,
|
|
274
|
-
registerComposer,
|
|
275
210
|
blockRegistry: blockRegistry ?? null,
|
|
276
211
|
}),
|
|
277
|
-
[chat, layout, config, labels, audioApi, hasAudio,
|
|
212
|
+
[chat, layout, config, labels, audioApi, hasAudio, blockRegistry],
|
|
278
213
|
);
|
|
279
214
|
|
|
280
215
|
return (
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { type RefObject, useEffect
|
|
3
|
+
import { type RefObject, useEffect } from 'react';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
attachComposer,
|
|
7
|
+
getActiveComposer,
|
|
8
|
+
type ComposerHandle,
|
|
9
|
+
} from '@djangocfg/ui-tools/composer-registry';
|
|
10
|
+
import { useChatContextOptional } from '../context';
|
|
6
11
|
import { useStreamEndFocus, type Focusable } from './useStreamEndFocus';
|
|
7
12
|
|
|
8
13
|
export type { Focusable } from './useStreamEndFocus';
|
|
@@ -63,18 +68,14 @@ export function useAutoFocusOnStreamEnd(
|
|
|
63
68
|
// Prefer the prop (caller knows best), fall back to context.
|
|
64
69
|
const isStreaming = isStreamingProp ?? ctx?.isStreaming ?? false;
|
|
65
70
|
|
|
66
|
-
// Keep latest ctx-composer in a ref so target resolution always sees
|
|
67
|
-
// the freshest registered handle.
|
|
68
|
-
const composerHandleRef = useRef<ComposerHandle | null>(null);
|
|
69
|
-
composerHandleRef.current = ctx?.composer ?? null;
|
|
70
|
-
|
|
71
71
|
useStreamEndFocus({
|
|
72
72
|
isStreaming,
|
|
73
73
|
enabled,
|
|
74
74
|
delayMs,
|
|
75
|
-
// Resolve in priority order: explicit ref >
|
|
75
|
+
// Resolve in priority order: explicit ref > active composer
|
|
76
|
+
// (from the cross-tool registry).
|
|
76
77
|
resolveTarget: () =>
|
|
77
|
-
(targetRef?.current as Focusable | null) ??
|
|
78
|
+
(targetRef?.current as Focusable | null) ?? getActiveComposer(),
|
|
78
79
|
});
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -94,8 +95,6 @@ export function useAutoFocusOnStreamEnd(
|
|
|
94
95
|
* No-op when called outside a `<ChatProvider>`.
|
|
95
96
|
*/
|
|
96
97
|
export function useRegisterComposer(handle: ComposerHandle): void {
|
|
97
|
-
const ctx = useChatContextOptional();
|
|
98
|
-
const register = ctx?.registerComposer;
|
|
99
98
|
const focus = handle.focus;
|
|
100
99
|
const moveCursorToEnd = handle.moveCursorToEnd;
|
|
101
100
|
// Forward `getValue/setValue` too — voice dictation reads/writes the
|
|
@@ -104,8 +103,6 @@ export function useRegisterComposer(handle: ComposerHandle): void {
|
|
|
104
103
|
const getValue = handle.getValue;
|
|
105
104
|
const setValue = handle.setValue;
|
|
106
105
|
useEffect(() => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return () => register(null);
|
|
110
|
-
}, [register, focus, moveCursorToEnd, getValue, setValue]);
|
|
106
|
+
return attachComposer({ focus, moveCursorToEnd, getValue, setValue });
|
|
107
|
+
}, [focus, moveCursorToEnd, getValue, setValue]);
|
|
111
108
|
}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import { useCallback, useRef } from 'react';
|
|
4
4
|
import type { MouseEvent as ReactMouseEvent, RefObject } from 'react';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { getActiveComposer } from '@djangocfg/ui-tools/composer-registry';
|
|
7
|
+
|
|
8
|
+
import { useChatContextOptional } from '../context';
|
|
7
9
|
import type { Focusable } from './useAutoFocusOnStreamEnd';
|
|
8
10
|
|
|
9
11
|
export interface UseFocusOnEmptyClickOptions {
|
|
@@ -51,9 +53,6 @@ export function useFocusOnEmptyClick(
|
|
|
51
53
|
const { targetRef, enabled = true, skipWhileStreaming = true } = options;
|
|
52
54
|
const ctx = useChatContextOptional();
|
|
53
55
|
|
|
54
|
-
const composerHandleRef = useRef<ComposerHandle | null>(null);
|
|
55
|
-
composerHandleRef.current = ctx?.composer ?? null;
|
|
56
|
-
|
|
57
56
|
const isStreamingRef = useRef(false);
|
|
58
57
|
isStreamingRef.current = ctx?.isStreaming ?? false;
|
|
59
58
|
|
|
@@ -83,7 +82,7 @@ export function useFocusOnEmptyClick(
|
|
|
83
82
|
explicit.focus?.();
|
|
84
83
|
return;
|
|
85
84
|
}
|
|
86
|
-
|
|
85
|
+
getActiveComposer()?.focus?.();
|
|
87
86
|
},
|
|
88
87
|
[enabled, skipWhileStreaming, targetRef],
|
|
89
88
|
);
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
Button,
|
|
8
8
|
Tooltip,
|
|
9
9
|
TooltipContent,
|
|
10
|
-
TooltipProvider,
|
|
11
10
|
TooltipTrigger,
|
|
12
11
|
} from '@djangocfg/ui-core/components';
|
|
13
12
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
@@ -66,24 +65,20 @@ export function ChatHeader({
|
|
|
66
65
|
{actions}
|
|
67
66
|
{closeSlot ??
|
|
68
67
|
(showClose && onClose && (
|
|
69
|
-
<
|
|
70
|
-
<
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
tier, already above the dock — no override needed. */}
|
|
84
|
-
<TooltipContent side="bottom">{closeLabel}</TooltipContent>
|
|
85
|
-
</Tooltip>
|
|
86
|
-
</TooltipProvider>
|
|
68
|
+
<Tooltip>
|
|
69
|
+
<TooltipTrigger asChild>
|
|
70
|
+
<Button
|
|
71
|
+
variant="ghost"
|
|
72
|
+
size="sm"
|
|
73
|
+
onClick={onClose}
|
|
74
|
+
aria-label={closeLabel}
|
|
75
|
+
className="-mr-1 h-7 w-7 p-0"
|
|
76
|
+
>
|
|
77
|
+
<X className="h-4 w-4" />
|
|
78
|
+
</Button>
|
|
79
|
+
</TooltipTrigger>
|
|
80
|
+
<TooltipContent side="bottom">{closeLabel}</TooltipContent>
|
|
81
|
+
</Tooltip>
|
|
87
82
|
))}
|
|
88
83
|
</div>
|
|
89
84
|
</header>
|
|
@@ -6,7 +6,6 @@ import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
|
6
6
|
import {
|
|
7
7
|
Tooltip,
|
|
8
8
|
TooltipContent,
|
|
9
|
-
TooltipProvider,
|
|
10
9
|
TooltipTrigger,
|
|
11
10
|
} from '@djangocfg/ui-core/components';
|
|
12
11
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
@@ -102,19 +101,16 @@ export const ChatHeaderActionButton = forwardRef<HTMLButtonElement, ChatHeaderAc
|
|
|
102
101
|
// Opted out — keep the native `title` only.
|
|
103
102
|
if (tooltip === false) return button;
|
|
104
103
|
|
|
105
|
-
// Floating tooltip on hover *and* keyboard focus.
|
|
106
|
-
//
|
|
107
|
-
// host wiring an ambient provider; Radix dedupes nested providers.
|
|
104
|
+
// Floating tooltip on hover *and* keyboard focus.
|
|
105
|
+
// Host app mounts <UiProviders> (which includes TooltipProvider).
|
|
108
106
|
const tooltipLabel = tooltip ?? ariaLabel;
|
|
109
107
|
return (
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
</Tooltip>
|
|
117
|
-
</TooltipProvider>
|
|
108
|
+
<Tooltip>
|
|
109
|
+
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
110
|
+
{/* Tooltip portals to <body> in ui-core's anchored-overlay tier,
|
|
111
|
+
already above the dock — no z-index override needed. */}
|
|
112
|
+
<TooltipContent side="bottom">{tooltipLabel}</TooltipContent>
|
|
113
|
+
</Tooltip>
|
|
118
114
|
);
|
|
119
115
|
},
|
|
120
116
|
);
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect,
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
4
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
-
import {
|
|
6
|
-
ContextMenu,
|
|
7
|
-
ContextMenuContent,
|
|
8
|
-
ContextMenuItem,
|
|
9
|
-
ContextMenuSeparator,
|
|
10
|
-
ContextMenuShortcut,
|
|
11
|
-
ContextMenuTrigger,
|
|
12
|
-
} from '@djangocfg/ui-core/components';
|
|
13
5
|
|
|
14
|
-
import {
|
|
6
|
+
import { TreeProvider, useTreeContext } from './context/TreeContext';
|
|
15
7
|
import { TreeDndProvider } from './TreeDndProvider';
|
|
16
8
|
import { TreeContent, treeRowDomId } from './components/TreeContent';
|
|
17
9
|
import { TreeEmptyArea } from './components/TreeEmptyArea';
|
|
@@ -22,70 +14,7 @@ import {
|
|
|
22
14
|
useTreeTypeAhead,
|
|
23
15
|
useTreeFinderHotkeys,
|
|
24
16
|
} from './hooks';
|
|
25
|
-
import type {
|
|
26
|
-
TreeContextMenuItem,
|
|
27
|
-
TreeContextMenuSlot,
|
|
28
|
-
TreeRootProps,
|
|
29
|
-
TreeRowRenderProps,
|
|
30
|
-
} from './types';
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Render an array of declarative menu items as a themed `<ContextMenu>`
|
|
34
|
-
* wrapped around the supplied trigger element. Pure presentational layer
|
|
35
|
-
* — the caller resolves and merges items.
|
|
36
|
-
*/
|
|
37
|
-
function renderItemsAsContextMenu<T>(
|
|
38
|
-
rowProps: TreeRowRenderProps<T>,
|
|
39
|
-
items: TreeContextMenuItem<T>[],
|
|
40
|
-
trigger: React.ReactNode,
|
|
41
|
-
): React.ReactNode {
|
|
42
|
-
return (
|
|
43
|
-
<ContextMenu>
|
|
44
|
-
<ContextMenuTrigger asChild>{trigger}</ContextMenuTrigger>
|
|
45
|
-
<ContextMenuContent>
|
|
46
|
-
{items.map((item, idx) => {
|
|
47
|
-
if (item === 'separator') {
|
|
48
|
-
return <ContextMenuSeparator key={`sep-${idx}`} />;
|
|
49
|
-
}
|
|
50
|
-
const Icon = item.icon;
|
|
51
|
-
return (
|
|
52
|
-
<ContextMenuItem
|
|
53
|
-
key={item.id}
|
|
54
|
-
disabled={item.disabled}
|
|
55
|
-
variant={item.destructive ? 'destructive' : undefined}
|
|
56
|
-
onSelect={() => item.onSelect(rowProps)}
|
|
57
|
-
>
|
|
58
|
-
{Icon ? <Icon /> : null}
|
|
59
|
-
{item.label}
|
|
60
|
-
{item.shortcut ? (
|
|
61
|
-
<ContextMenuShortcut>{item.shortcut}</ContextMenuShortcut>
|
|
62
|
-
) : null}
|
|
63
|
-
</ContextMenuItem>
|
|
64
|
-
);
|
|
65
|
-
})}
|
|
66
|
-
</ContextMenuContent>
|
|
67
|
-
</ContextMenu>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Drop trailing / leading / duplicate separators so a merged menu never
|
|
73
|
-
* shows a separator next to a section header or another separator.
|
|
74
|
-
*/
|
|
75
|
-
function tidyMenuItems<T>(items: TreeContextMenuItem<T>[]): TreeContextMenuItem<T>[] {
|
|
76
|
-
const out: TreeContextMenuItem<T>[] = [];
|
|
77
|
-
for (const it of items) {
|
|
78
|
-
if (it === 'separator') {
|
|
79
|
-
if (out.length === 0) continue;
|
|
80
|
-
if (out[out.length - 1] === 'separator') continue;
|
|
81
|
-
out.push(it);
|
|
82
|
-
} else {
|
|
83
|
-
out.push(it);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
while (out.length > 0 && out[out.length - 1] === 'separator') out.pop();
|
|
87
|
-
return out;
|
|
88
|
-
}
|
|
17
|
+
import type { TreeActionsHandle, TreeRootProps } from './types';
|
|
89
18
|
|
|
90
19
|
/**
|
|
91
20
|
* High-level entry point. Wraps Provider + (optional) search bar + content.
|
|
@@ -126,6 +55,7 @@ function TreeRoot<T>(props: TreeRootProps<T>) {
|
|
|
126
55
|
persistSelection = false,
|
|
127
56
|
adapter,
|
|
128
57
|
defaultMenuItems,
|
|
58
|
+
actionsRef,
|
|
129
59
|
className,
|
|
130
60
|
style,
|
|
131
61
|
} = props;
|
|
@@ -171,6 +101,7 @@ function TreeRoot<T>(props: TreeRootProps<T>) {
|
|
|
171
101
|
enableTypeAhead={enableTypeAhead}
|
|
172
102
|
enableFinderHotkeys={enableFinderHotkeys}
|
|
173
103
|
renderRow={renderRow}
|
|
104
|
+
actionsRef={actionsRef}
|
|
174
105
|
/>
|
|
175
106
|
</TreeProvider>
|
|
176
107
|
);
|
|
@@ -183,6 +114,7 @@ interface TreeRootShellProps<T> {
|
|
|
183
114
|
enableTypeAhead: boolean;
|
|
184
115
|
enableFinderHotkeys: boolean;
|
|
185
116
|
renderRow?: TreeRootProps<T>['renderRow'];
|
|
117
|
+
actionsRef?: React.MutableRefObject<TreeActionsHandle | null>;
|
|
186
118
|
}
|
|
187
119
|
|
|
188
120
|
function TreeRootShell<T>({
|
|
@@ -192,10 +124,33 @@ function TreeRootShell<T>({
|
|
|
192
124
|
enableTypeAhead,
|
|
193
125
|
enableFinderHotkeys,
|
|
194
126
|
renderRow,
|
|
127
|
+
actionsRef,
|
|
195
128
|
}: TreeRootShellProps<T>) {
|
|
196
129
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
197
130
|
const ctx = useTreeContext<T>();
|
|
198
131
|
|
|
132
|
+
// Publish the action handle to the outer ref so host code can call
|
|
133
|
+
// refresh / refreshAll after a mutation that originated outside Tree.
|
|
134
|
+
// Effect runs after mount; the ref stays populated until unmount.
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!actionsRef) return;
|
|
137
|
+
actionsRef.current = {
|
|
138
|
+
refresh: ctx.refresh,
|
|
139
|
+
refreshAll: ctx.refreshAll,
|
|
140
|
+
expandAll: ctx.expandAll,
|
|
141
|
+
collapseAll: ctx.collapseAll,
|
|
142
|
+
};
|
|
143
|
+
return () => {
|
|
144
|
+
if (actionsRef.current) actionsRef.current = null;
|
|
145
|
+
};
|
|
146
|
+
}, [
|
|
147
|
+
actionsRef,
|
|
148
|
+
ctx.refresh,
|
|
149
|
+
ctx.refreshAll,
|
|
150
|
+
ctx.expandAll,
|
|
151
|
+
ctx.collapseAll,
|
|
152
|
+
]);
|
|
153
|
+
|
|
199
154
|
// Keyboard navigation (↑↓ ←→ Home/End Enter Esc Cmd+A, Shift-extend) —
|
|
200
155
|
// scoped via callback ref.
|
|
201
156
|
const isMulti = ctx.selectionMode === 'multiple';
|
|
@@ -288,27 +243,9 @@ function TreeRootShell<T>({
|
|
|
288
243
|
enabled: enableTypeAhead,
|
|
289
244
|
});
|
|
290
245
|
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
// here so TreeRow keeps reading a single source of truth.
|
|
295
|
-
const finalRenderContextMenu = useMemo<TreeContextMenuSlot<T> | undefined>(() => {
|
|
296
|
-
if (ctx.renderContextMenu) return ctx.renderContextMenu;
|
|
297
|
-
const resolve = ctx.resolvedContextMenuActions;
|
|
298
|
-
if (!resolve) return undefined;
|
|
299
|
-
return (rowProps, trigger) => {
|
|
300
|
-
const items = resolve(rowProps);
|
|
301
|
-
const cleaned = items ? tidyMenuItems(items) : null;
|
|
302
|
-
if (!cleaned || cleaned.length === 0) return trigger;
|
|
303
|
-
return renderItemsAsContextMenu(rowProps, cleaned, trigger);
|
|
304
|
-
};
|
|
305
|
-
}, [ctx.renderContextMenu, ctx.resolvedContextMenuActions]);
|
|
306
|
-
|
|
307
|
-
const childCtx = useMemo(
|
|
308
|
-
() => ({ ...ctx, renderContextMenu: finalRenderContextMenu }),
|
|
309
|
-
[ctx, finalRenderContextMenu],
|
|
310
|
-
);
|
|
311
|
-
|
|
246
|
+
// Tree body. `ctx.renderContextMenu` is already the final slot —
|
|
247
|
+
// TreeProvider does the declarative→slot merge centrally, so Shell
|
|
248
|
+
// doesn't re-derive it (and no nested provider override is needed).
|
|
312
249
|
const treeBody = (
|
|
313
250
|
<div
|
|
314
251
|
ref={setContainerRef}
|
|
@@ -336,23 +273,10 @@ function TreeRootShell<T>({
|
|
|
336
273
|
</div>
|
|
337
274
|
);
|
|
338
275
|
|
|
339
|
-
// If we computed a different renderContextMenu slot than the one in
|
|
340
|
-
// the parent provider, override it via a thin nested provider so
|
|
341
|
-
// <TreeRow> reads the merged slot. Otherwise skip the wrapper to keep
|
|
342
|
-
// the render cheap.
|
|
343
|
-
const body =
|
|
344
|
-
finalRenderContextMenu === ctx.renderContextMenu ? (
|
|
345
|
-
treeBody
|
|
346
|
-
) : (
|
|
347
|
-
<TreeContext.Provider value={childCtx as never}>
|
|
348
|
-
{treeBody}
|
|
349
|
-
</TreeContext.Provider>
|
|
350
|
-
);
|
|
351
|
-
|
|
352
276
|
// Wrap in @dnd-kit context only when DnD is active — `TreeDndProvider`
|
|
353
277
|
// short-circuits to a fragment otherwise, so we don't pay the
|
|
354
278
|
// sensor-registration cost.
|
|
355
|
-
return <TreeDndProvider>{
|
|
279
|
+
return <TreeDndProvider>{treeBody}</TreeDndProvider>;
|
|
356
280
|
}
|
|
357
281
|
|
|
358
282
|
export default TreeRoot;
|