@djangocfg/ui-tools 2.1.413 โ 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.
- package/dist/file-icon/index.d.cts +1 -1
- package/dist/file-icon/index.d.ts +1 -1
- package/dist/slots-ClRpIzoh.d.cts +88 -0
- package/dist/slots-ClRpIzoh.d.ts +88 -0
- package/dist/tree/index.cjs +1994 -276
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +717 -72
- package/dist/tree/index.d.ts +717 -72
- package/dist/tree/index.mjs +1984 -279
- package/dist/tree/index.mjs.map +1 -1
- package/package.json +10 -6
- package/src/tools/chat/README.md +111 -1
- package/src/tools/chat/composer/Composer.tsx +138 -17
- package/src/tools/chat/composer/ComposerRichTextarea.tsx +25 -0
- package/src/tools/chat/composer/index.ts +22 -0
- package/src/tools/chat/composer/slash/README.md +187 -0
- package/src/tools/chat/composer/slash/SlashHighlightTextarea.tsx +144 -0
- package/src/tools/chat/composer/slash/SlashMenu.tsx +142 -0
- package/src/tools/chat/composer/slash/SlashToken.tsx +57 -0
- package/src/tools/chat/composer/slash/index.ts +44 -0
- package/src/tools/chat/composer/slash/labels.ts +19 -0
- package/src/tools/chat/composer/slash/state.ts +168 -0
- package/src/tools/chat/composer/slash/types.ts +64 -0
- package/src/tools/chat/composer/slash/useSlashCommands.ts +204 -0
- package/src/tools/chat/composer/types.ts +8 -0
- package/src/tools/chat/shell/SuggestedPrompts.tsx +194 -0
- package/src/tools/chat/shell/index.ts +6 -0
- package/src/tools/data/Listbox/lazy.tsx +1 -1
- package/src/tools/data/Masonry/lazy.tsx +1 -1
- package/src/tools/data/Timeline/lazy.tsx +1 -1
- package/src/tools/data/Tree/FinderTree.tsx +42 -0
- package/src/tools/data/Tree/README.md +337 -208
- package/src/tools/data/Tree/TreeDndProvider.tsx +137 -0
- package/src/tools/data/Tree/TreeRoot.tsx +170 -55
- package/src/tools/data/Tree/__tests__/dnd.test.ts +160 -0
- package/src/tools/data/Tree/__tests__/keyboard.test.ts +137 -0
- package/src/tools/data/Tree/__tests__/renameUtils.test.ts +52 -0
- package/src/tools/data/Tree/__tests__/selection.test.ts +227 -0
- package/src/tools/data/Tree/components/TreeDropIndicator.tsx +65 -0
- package/src/tools/data/Tree/components/TreeEmptyArea.tsx +160 -0
- package/src/tools/data/Tree/components/TreeRenameInput.tsx +114 -0
- package/src/tools/data/Tree/components/TreeRow.tsx +92 -8
- package/src/tools/data/Tree/components/index.ts +6 -0
- package/src/tools/data/Tree/context/TreeContext.tsx +204 -363
- package/src/tools/data/Tree/context/TreeContextValue.ts +139 -0
- package/src/tools/data/Tree/context/async-children/collect-ids.ts +27 -0
- package/src/tools/data/Tree/context/async-children/index.ts +8 -0
- package/src/tools/data/Tree/context/async-children/use-async-children.ts +157 -0
- package/src/tools/data/Tree/context/clipboard/index.ts +4 -0
- package/src/tools/data/Tree/context/clipboard/use-clipboard.ts +115 -0
- package/src/tools/data/Tree/context/dnd/index.ts +8 -0
- package/src/tools/data/Tree/context/dnd/use-dnd.ts +194 -0
- package/src/tools/data/Tree/context/expansion/index.ts +4 -0
- package/src/tools/data/Tree/context/expansion/use-expansion.ts +55 -0
- package/src/tools/data/Tree/context/hooks.ts +68 -1
- package/src/tools/data/Tree/context/index.ts +3 -0
- package/src/tools/data/Tree/context/menu/builtin-actions.ts +357 -0
- package/src/tools/data/Tree/context/menu/index.ts +10 -0
- package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +127 -0
- package/src/tools/data/Tree/context/persist/index.ts +4 -0
- package/src/tools/data/Tree/context/persist/use-persist-sync.ts +74 -0
- package/src/tools/data/Tree/context/rename/index.ts +4 -0
- package/src/tools/data/Tree/context/rename/use-rename.ts +113 -0
- package/src/tools/data/Tree/context/selection/index.ts +4 -0
- package/src/tools/data/Tree/context/selection/use-selection.ts +146 -0
- package/src/tools/data/Tree/context/state/index.ts +6 -0
- package/src/tools/data/Tree/context/state/initial.ts +41 -0
- package/src/tools/data/Tree/context/state/reducer.ts +76 -0
- package/src/tools/data/Tree/context/state/types.ts +46 -0
- package/src/tools/data/Tree/data/clipboard.ts +33 -0
- package/src/tools/data/Tree/data/dnd.ts +123 -0
- package/src/tools/data/Tree/data/finderShortcuts.ts +67 -0
- package/src/tools/data/Tree/data/index.ts +19 -0
- package/src/tools/data/Tree/data/renameUtils.ts +51 -0
- package/src/tools/data/Tree/data/selection.ts +157 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/index.ts +8 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
- package/src/tools/data/Tree/hooks/index.ts +23 -4
- package/src/tools/data/Tree/hooks/keyboard/activation.ts +27 -0
- package/src/tools/data/Tree/hooks/keyboard/arrow-nav.ts +26 -0
- package/src/tools/data/Tree/hooks/keyboard/expand-collapse.ts +54 -0
- package/src/tools/data/Tree/hooks/keyboard/index.ts +10 -0
- package/src/tools/data/Tree/hooks/keyboard/types.ts +39 -0
- package/src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
- package/src/tools/data/Tree/hooks/type-ahead/index.ts +5 -0
- package/src/tools/data/Tree/hooks/type-ahead/match-prefix.ts +42 -0
- package/src/tools/data/Tree/hooks/{useTreeTypeAhead.ts โ type-ahead/use-tree-type-ahead.ts} +8 -19
- package/src/tools/data/Tree/index.tsx +25 -2
- package/src/tools/data/Tree/types/activation.ts +30 -0
- package/src/tools/data/Tree/types/adapter.ts +70 -0
- package/src/tools/data/Tree/types/index.ts +27 -0
- package/src/tools/data/Tree/types/labels.ts +97 -0
- package/src/tools/data/Tree/types/loader.ts +9 -0
- package/src/tools/data/Tree/types/node.ts +38 -0
- package/src/tools/data/Tree/types/root-props.ts +142 -0
- package/src/tools/data/Tree/types/selection.ts +3 -0
- package/src/tools/data/Tree/types/slots.ts +64 -0
- package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +1 -1
- package/src/tools/dev/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +1 -1
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +85 -0
- package/src/tools/forms/MarkdownEditor/index.ts +1 -0
- package/src/tools/forms/MarkdownEditor/lazy.tsx +6 -0
- package/src/tools/forms/MarkdownEditor/slash/SlashCommandNode.ts +162 -0
- package/src/tools/forms/MarkdownEditor/slash/index.ts +4 -0
- package/src/tools/forms/MarkdownEditor/slash/syncSlashNode.ts +97 -0
- package/src/tools/forms/MarkdownEditor/slash/types.ts +13 -0
- package/src/tools/forms/MarkdownEditor/styles.css +18 -0
- package/src/tools/index.ts +2 -2
- package/dist/types-j2vhn4Kv.d.cts +0 -241
- package/dist/types-j2vhn4Kv.d.ts +0 -241
- package/src/tools/data/Tree/hooks/useTreeKeyboard.ts +0 -171
- package/src/tools/data/Tree/types.ts +0 -217
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.416",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -254,8 +254,8 @@
|
|
|
254
254
|
"test:watch": "vitest"
|
|
255
255
|
},
|
|
256
256
|
"peerDependencies": {
|
|
257
|
-
"@djangocfg/i18n": "^2.1.
|
|
258
|
-
"@djangocfg/ui-core": "^2.1.
|
|
257
|
+
"@djangocfg/i18n": "^2.1.416",
|
|
258
|
+
"@djangocfg/ui-core": "^2.1.416",
|
|
259
259
|
"consola": "^3.4.2",
|
|
260
260
|
"lodash-es": "^4.18.1",
|
|
261
261
|
"lucide-react": "^0.545.0",
|
|
@@ -265,6 +265,10 @@
|
|
|
265
265
|
"zustand": "^5.0.0"
|
|
266
266
|
},
|
|
267
267
|
"dependencies": {
|
|
268
|
+
"@dnd-kit/core": "^6.3.1",
|
|
269
|
+
"@dnd-kit/modifiers": "^9.0.0",
|
|
270
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
271
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
268
272
|
"@floating-ui/dom": "^1.7.4",
|
|
269
273
|
"@rjsf/core": "^6.1.2",
|
|
270
274
|
"@rjsf/utils": "^6.1.2",
|
|
@@ -315,9 +319,9 @@
|
|
|
315
319
|
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
316
320
|
},
|
|
317
321
|
"devDependencies": {
|
|
318
|
-
"@djangocfg/i18n": "^2.1.
|
|
319
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
320
|
-
"@djangocfg/ui-core": "^2.1.
|
|
322
|
+
"@djangocfg/i18n": "^2.1.416",
|
|
323
|
+
"@djangocfg/typescript-config": "^2.1.416",
|
|
324
|
+
"@djangocfg/ui-core": "^2.1.416",
|
|
321
325
|
"@types/lodash-es": "^4.17.12",
|
|
322
326
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
323
327
|
"@types/node": "^25.2.3",
|
package/src/tools/chat/README.md
CHANGED
|
@@ -762,7 +762,7 @@ inherit the composer `size` via context, no prop needed).
|
|
|
762
762
|
| `<ComposerToolPill icon label active onRemove>` | `inlineStart` | Gemini-style capsule for a selected tool/mode with an optional `ร` to clear it. |
|
|
763
763
|
| `<ComposerModelPicker value options onChange>` | `inlineEnd` | "Flash-Lite โพ" pill โ opens a radio-group model picker. |
|
|
764
764
|
| `<ComposerBanner variant title description actions onDismiss>` | `blockStart` | Standalone notice bubble above the composer (upsell / quota / info). |
|
|
765
|
-
| `<ComposerRichTextarea mentions>` | `slots.Textarea` (Tier B) | Ready-made TipTap chat editor โ unstyled, no toolbar, size-matched height, Enter-to-send, `@`-mention support. |
|
|
765
|
+
| `<ComposerRichTextarea mentions slashCommands>` | `slots.Textarea` (Tier B) | Ready-made TipTap chat editor โ unstyled, no toolbar, size-matched height, Enter-to-send, `@`-mention + `/slash`-chip support. |
|
|
766
766
|
|
|
767
767
|
```tsx
|
|
768
768
|
<Composer
|
|
@@ -790,6 +790,82 @@ editor shows a text caret and a `mousedown` on the bare surface (not a
|
|
|
790
790
|
button) focuses the editor. Works for both the plain `<textarea>` and the
|
|
791
791
|
TipTap (`contenteditable`) backend.
|
|
792
792
|
|
|
793
|
+
### Slash commands
|
|
794
|
+
|
|
795
|
+
A `/verb` palette layered on top of the composer โ same idea as
|
|
796
|
+
mentions, but triggered only when the buffer **starts** with `/` (so it
|
|
797
|
+
never conflicts with `@`-mention matching, which runs anywhere in the
|
|
798
|
+
editor). Works with both the plain `<textarea>` and the TipTap-backed
|
|
799
|
+
`<ComposerRichTextarea>`; in-editor chip highlighting ships for both
|
|
800
|
+
paths.
|
|
801
|
+
|
|
802
|
+
```tsx
|
|
803
|
+
import { Composer, type SlashCommand } from '@djangocfg/ui-tools/chat';
|
|
804
|
+
import { RefreshCw } from 'lucide-react';
|
|
805
|
+
|
|
806
|
+
const commands: SlashCommand[] = [
|
|
807
|
+
{
|
|
808
|
+
id: 'clear',
|
|
809
|
+
token: '/clear',
|
|
810
|
+
label: 'Clear conversation',
|
|
811
|
+
description: 'Discard the current session.',
|
|
812
|
+
icon: <RefreshCw className="h-3.5 w-3.5" />,
|
|
813
|
+
autoExecute: true,
|
|
814
|
+
onExecute: () => chatActions.clearSession(),
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
id: 'connect',
|
|
818
|
+
token: '/connect',
|
|
819
|
+
label: 'Connect to machine',
|
|
820
|
+
argHint: '<hostname>',
|
|
821
|
+
// default insert behavior โ user types argument and submits as normal message
|
|
822
|
+
},
|
|
823
|
+
];
|
|
824
|
+
|
|
825
|
+
<Composer
|
|
826
|
+
composer={composer}
|
|
827
|
+
composerSlots={{ slashCommands: { commands } }}
|
|
828
|
+
/>;
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
**Behavior matrix:**
|
|
832
|
+
|
|
833
|
+
- Default (insert) โ Pick โ inserts `/verb ` into the buffer with a
|
|
834
|
+
subtle highlight. Cursor sits ready for argument entry. User submits
|
|
835
|
+
as a normal chat message; the host reads `composer.value` to grab the
|
|
836
|
+
args and dispatches `onExecute` itself.
|
|
837
|
+
- `autoExecute: true` โ Pick โ `onExecute()` fires immediately and the
|
|
838
|
+
buffer clears. For action commands (`/clear`, `/settings`) that don't
|
|
839
|
+
produce a chat message.
|
|
840
|
+
|
|
841
|
+
**Visual highlight:**
|
|
842
|
+
|
|
843
|
+
- In the plain textarea โ an overlay-mirror paints `/verb` with a
|
|
844
|
+
primary tint while preserving the native caret + selection.
|
|
845
|
+
- In `<ComposerRichTextarea>` (TipTap) โ pass the same verb list as
|
|
846
|
+
`slashCommands` to the editor and a `SlashCommandNode` atom
|
|
847
|
+
extension paints the chip in-flow. Same primary-tinted styling as
|
|
848
|
+
the plain mirror; the atom flattens to the bare `/verb` token in
|
|
849
|
+
`getText()` / `getMarkdown()` so `composer.value` stays in plain
|
|
850
|
+
string form and the slash hook keeps driving the menu unchanged.
|
|
851
|
+
|
|
852
|
+
**Keyboard:** `/` opens the menu when at buffer start. โ/โ navigate.
|
|
853
|
+
Enter or Tab pick. Esc closes. Click outside dismisses.
|
|
854
|
+
|
|
855
|
+
**Empty state:** typing `/xyzz` keeps the menu open with a "No commands
|
|
856
|
+
match" row instead of disappearing โ discoverability over jank.
|
|
857
|
+
|
|
858
|
+
**Submit gate:** commands with `argHint` block submit until the user
|
|
859
|
+
types something after the verb (`/note ` โ Send disabled, Enter no-op;
|
|
860
|
+
`/note hello` โ Send active). `autoExecute` commands block submit
|
|
861
|
+
unconditionally โ they are run from the menu, not dispatched as text.
|
|
862
|
+
The gate works for the plain `<Textarea>`, `<SlashHighlightTextarea>`,
|
|
863
|
+
the TipTap `<ComposerRichTextarea>`, and any `slots.SendButton`.
|
|
864
|
+
|
|
865
|
+
See [`composer/slash/README.md`](./composer/slash/README.md) for the
|
|
866
|
+
full API reference (types, hook contract, mirror internals, direct
|
|
867
|
+
`useSlashCommands` usage, mentions coexistence).
|
|
868
|
+
|
|
793
869
|
## Three usage patterns
|
|
794
870
|
|
|
795
871
|
### 1. One-line preset
|
|
@@ -808,6 +884,39 @@ TipTap (`contenteditable`) backend.
|
|
|
808
884
|
</ChatProvider>
|
|
809
885
|
```
|
|
810
886
|
|
|
887
|
+
### Suggested prompts โ `<SuggestedPrompts>`
|
|
888
|
+
|
|
889
|
+
Starter-prompt surface every chat host wants (ChatGPT / Claude / cmdop
|
|
890
|
+
all ship a flavour of it). Drop into `slots.empty` to seed the
|
|
891
|
+
composer from a click. Two layouts: `chips` (flat rounded buttons,
|
|
892
|
+
default) and `grid` (2-col cards with `description`).
|
|
893
|
+
|
|
894
|
+
```tsx
|
|
895
|
+
import { SuggestedPrompts, type SuggestedPromptItem } from '@djangocfg/ui-tools/chat';
|
|
896
|
+
import { HeartPulse, Server, Sparkles } from 'lucide-react';
|
|
897
|
+
|
|
898
|
+
const PROMPTS: readonly SuggestedPromptItem[] = [
|
|
899
|
+
{ id: 'machines', label: 'List my machines', prompt: 'List my connected machines', icon: <Server className="size-3.5" /> },
|
|
900
|
+
{ id: 'health', label: 'Run a health check', prompt: 'Run a health check and report issues', icon: <HeartPulse className="size-3.5" /> },
|
|
901
|
+
];
|
|
902
|
+
|
|
903
|
+
<ChatRoot
|
|
904
|
+
slots={{
|
|
905
|
+
empty: ({ setValue, focus }) => (
|
|
906
|
+
<SuggestedPrompts
|
|
907
|
+
items={PROMPTS}
|
|
908
|
+
onPick={(item) => { setValue(item.prompt); focus(); }}
|
|
909
|
+
hero={<Sparkles className="size-6 text-primary" />}
|
|
910
|
+
title="How can I help?"
|
|
911
|
+
description="Pick a starter, or just start typing."
|
|
912
|
+
/>
|
|
913
|
+
),
|
|
914
|
+
}}
|
|
915
|
+
/>
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
`renderItem` is an escape hatch for one-off chip styling without forking the component.
|
|
919
|
+
|
|
811
920
|
### 3. Headless
|
|
812
921
|
|
|
813
922
|
```tsx
|
|
@@ -952,6 +1061,7 @@ plus `Overview` (MDX) and `Showcase` (a one-screen living demo):
|
|
|
952
1061
|
| `slots.jumpToLatest` | Sticky overlay | `ReactNode` |
|
|
953
1062
|
| `composer.slots.actionsStart` / `actionsEnd` | Composer action clusters | `ComposerAction[]` |
|
|
954
1063
|
| `composer.slots.blockStart` | Above composer textarea | `ReactNode` |
|
|
1064
|
+
| `composer.slots.slashCommands` | `/verb` palette (buffer-start trigger) | `SlashConfig` (`{ commands: SlashCommand[] }`) |
|
|
955
1065
|
| `composer.footer` | Below composer | `ComposerFooterProps \| false` |
|
|
956
1066
|
| `composer.render` | Replace `<Composer>` | `({ composer, config }) => ReactNode` |
|
|
957
1067
|
| `messages.render` | Replace each bubble | `(m, i) => ReactNode` |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { forwardRef, useEffect, useRef, useState } from 'react';
|
|
3
|
+
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';
|
|
@@ -13,6 +13,9 @@ import { ComposerActionBar } from './ComposerActionBar';
|
|
|
13
13
|
import { ComposerButton } from './ComposerButton';
|
|
14
14
|
import { ComposerFooter } from './ComposerFooter';
|
|
15
15
|
import { ComposerSizeProvider } from './size-context';
|
|
16
|
+
import { SlashHighlightTextarea } from './slash/SlashHighlightTextarea';
|
|
17
|
+
import { SlashMenu } from './slash/SlashMenu';
|
|
18
|
+
import { useSlashCommands } from './slash/useSlashCommands';
|
|
16
19
|
import { useComposerActions } from './useComposerActions';
|
|
17
20
|
import { useComposerAttach } from './useComposerAttach';
|
|
18
21
|
import type {
|
|
@@ -205,6 +208,63 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
205
208
|
const inlineEnd = composerSlots?.inlineEnd;
|
|
206
209
|
const blockStart = composerSlots?.blockStart;
|
|
207
210
|
|
|
211
|
+
// โโ Slash commands โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
212
|
+
// When the host wires `composerSlots.slashCommands`, an internal
|
|
213
|
+
// `<SlashMenu>` is rendered as a floating popover above the input
|
|
214
|
+
// surface and โ/โ/Enter/Tab/Esc are routed to the slash hook while
|
|
215
|
+
// the menu is open. The hook is always called (React rules) โ it
|
|
216
|
+
// is a no-op when `commands` is the empty array.
|
|
217
|
+
const slashCommands = composerSlots?.slashCommands?.commands;
|
|
218
|
+
const slashEnabled = !!slashCommands && slashCommands.length > 0;
|
|
219
|
+
const slash = useSlashCommands({
|
|
220
|
+
value: composer.value,
|
|
221
|
+
commands: slashCommands ?? [],
|
|
222
|
+
onApply: composer.setValue,
|
|
223
|
+
});
|
|
224
|
+
const slashOnKeyDown = slash.onKeyDown;
|
|
225
|
+
const slashIsOpen = slashEnabled && slash.isOpen;
|
|
226
|
+
// When slash is enabled and the current buffer is a `/verb` missing
|
|
227
|
+
// a required argument (or an `autoExecute` action), submit is blocked:
|
|
228
|
+
// the Send button reads disabled and Enter is a no-op. The host-facing
|
|
229
|
+
// `composer` is wrapped into `gatedComposer` so every downstream
|
|
230
|
+
// surface (built-in Send, custom SendSlot, TipTap `ComposerRichTextarea`,
|
|
231
|
+
// plain `<Textarea>`) shares the same gate without each having to
|
|
232
|
+
// know about slash state.
|
|
233
|
+
const slashAllowsSubmit = slashEnabled ? slash.canSubmit : true;
|
|
234
|
+
const gatedComposer = useMemo<UseChatComposerReturn>(() => {
|
|
235
|
+
if (slashAllowsSubmit) return composer;
|
|
236
|
+
const baseOnKeyDown = composer.textareaProps.onKeyDown;
|
|
237
|
+
return {
|
|
238
|
+
...composer,
|
|
239
|
+
canSubmit: false,
|
|
240
|
+
submit: async () => {
|
|
241
|
+
// Slash gate refuses โ swallow programmatic submit attempts.
|
|
242
|
+
},
|
|
243
|
+
textareaProps: {
|
|
244
|
+
...composer.textareaProps,
|
|
245
|
+
onKeyDown: (e) => {
|
|
246
|
+
// Block Enter (without Shift) so the textarea's own submit
|
|
247
|
+
// binding cannot fire while the slash gate is closed. Cmd /
|
|
248
|
+
// Ctrl + Enter is treated the same. Other keys (history,
|
|
249
|
+
// arrows, etc.) pass through to the original handler.
|
|
250
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
baseOnKeyDown(e);
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}, [composer, slashAllowsSubmit]);
|
|
259
|
+
// Capture-phase handler โ runs before the textarea / TipTap own
|
|
260
|
+
// submit binding, so Enter / Tab pick a verb instead of sending.
|
|
261
|
+
const handleSlashKeyDownCapture = !slashEnabled
|
|
262
|
+
? undefined
|
|
263
|
+
: (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
264
|
+
if (!slash.isOpen) return;
|
|
265
|
+
slashOnKeyDown(e);
|
|
266
|
+
};
|
|
267
|
+
|
|
208
268
|
// Clicking the padding around the input (not a button/link) should focus
|
|
209
269
|
// the editor โ same affordance as ChatGPT/Gemini. Works for both the
|
|
210
270
|
// plain `<textarea>` and the TipTap (`contenteditable`) backend by
|
|
@@ -282,14 +342,14 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
282
342
|
|
|
283
343
|
// Merge built-in send/stop/attach descriptors with host arrays.
|
|
284
344
|
const { actionsStart, actionsEnd } = useComposerActions({
|
|
285
|
-
composer,
|
|
345
|
+
composer: gatedComposer,
|
|
286
346
|
isStreaming,
|
|
287
347
|
isDisabled,
|
|
288
348
|
showAttachmentButton,
|
|
289
349
|
onPickFiles: pickFiles,
|
|
290
350
|
actionsStart: composerSlots?.actionsStart,
|
|
291
351
|
actionsEnd: composerSlots?.actionsEnd,
|
|
292
|
-
onSend: () => void
|
|
352
|
+
onSend: () => void gatedComposer.submit(),
|
|
293
353
|
onCancel,
|
|
294
354
|
micSendSwap,
|
|
295
355
|
});
|
|
@@ -300,33 +360,45 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
300
360
|
const ActionBarSlot = slots?.ActionBar;
|
|
301
361
|
|
|
302
362
|
// โโ Textarea โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
363
|
+
// When slash commands are enabled and the host hasn't supplied its own
|
|
364
|
+
// Textarea slot, swap in the overlay-mirror `<SlashHighlightTextarea>`
|
|
365
|
+
// so a leading `/verb` reads as a styled chip while the user types.
|
|
366
|
+
const sharedTextareaClass = cn(
|
|
367
|
+
'flex-1 resize-none border-0 bg-transparent shadow-none',
|
|
368
|
+
'focus-visible:ring-0 focus-visible:outline-none',
|
|
369
|
+
sz.textarea,
|
|
370
|
+
sz.text,
|
|
371
|
+
ap.textarea,
|
|
372
|
+
ap.text,
|
|
373
|
+
slotProps?.textarea?.className,
|
|
374
|
+
textareaClassName,
|
|
375
|
+
);
|
|
303
376
|
const textareaNode = TextareaSlot ? (
|
|
304
377
|
<TextareaSlot
|
|
305
|
-
composer={
|
|
378
|
+
composer={gatedComposer}
|
|
306
379
|
placeholder={placeholder}
|
|
307
380
|
disabled={isDisabled}
|
|
308
381
|
size={size}
|
|
309
382
|
className={slotProps?.textarea?.className ?? textareaClassName}
|
|
310
383
|
/>
|
|
384
|
+
) : slashEnabled ? (
|
|
385
|
+
<SlashHighlightTextarea
|
|
386
|
+
composer={gatedComposer}
|
|
387
|
+
placeholder={placeholder}
|
|
388
|
+
disabled={isDisabled}
|
|
389
|
+
size={size}
|
|
390
|
+
textareaClassName={sharedTextareaClass}
|
|
391
|
+
/>
|
|
311
392
|
) : (
|
|
312
393
|
<Textarea
|
|
313
|
-
{...
|
|
394
|
+
{...gatedComposer.textareaProps}
|
|
314
395
|
rows={1}
|
|
315
396
|
placeholder={placeholder}
|
|
316
397
|
aria-label={placeholder}
|
|
317
398
|
aria-multiline="true"
|
|
318
399
|
aria-keyshortcuts="Enter"
|
|
319
400
|
disabled={isDisabled}
|
|
320
|
-
className={
|
|
321
|
-
'flex-1 resize-none border-0 bg-transparent shadow-none',
|
|
322
|
-
'focus-visible:ring-0 focus-visible:outline-none',
|
|
323
|
-
sz.textarea,
|
|
324
|
-
sz.text,
|
|
325
|
-
ap.textarea,
|
|
326
|
-
ap.text,
|
|
327
|
-
slotProps?.textarea?.className,
|
|
328
|
-
textareaClassName,
|
|
329
|
-
)}
|
|
401
|
+
className={sharedTextareaClass}
|
|
330
402
|
/>
|
|
331
403
|
);
|
|
332
404
|
|
|
@@ -359,9 +431,9 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
359
431
|
const customSend = SendSlot ? (
|
|
360
432
|
<SendSlot
|
|
361
433
|
streaming={isStreaming}
|
|
362
|
-
disabled={!
|
|
434
|
+
disabled={!gatedComposer.canSubmit}
|
|
363
435
|
size={size}
|
|
364
|
-
onSend={() => void
|
|
436
|
+
onSend={() => void gatedComposer.submit()}
|
|
365
437
|
onCancel={onCancel}
|
|
366
438
|
{...slotProps?.send}
|
|
367
439
|
/>
|
|
@@ -428,6 +500,51 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
428
500
|
/>
|
|
429
501
|
) : null;
|
|
430
502
|
|
|
503
|
+
// Click outside the slash menu (and outside the composer surface)
|
|
504
|
+
// dismisses it. Mounted only while the menu is open to keep the
|
|
505
|
+
// listener footprint minimal.
|
|
506
|
+
const slashMenuRef = useRef<HTMLDivElement>(null);
|
|
507
|
+
const slashClear = slash.clear;
|
|
508
|
+
useEffect(() => {
|
|
509
|
+
if (!slashIsOpen) return;
|
|
510
|
+
const onPointerDown = (e: MouseEvent) => {
|
|
511
|
+
const target = e.target as Node | null;
|
|
512
|
+
if (!target) return;
|
|
513
|
+
if (slashMenuRef.current?.contains(target)) return;
|
|
514
|
+
// Clicks inside the input surface (textarea, action buttons) are
|
|
515
|
+
// fine โ only outside-of-composer interactions should dismiss.
|
|
516
|
+
// `surfaceRef` is only mounted in the stacked layout; fall back
|
|
517
|
+
// to the active editable node so the inline layout works too.
|
|
518
|
+
if (surfaceRef.current?.contains(target)) return;
|
|
519
|
+
const editable = composer.textareaRef.current;
|
|
520
|
+
if (editable && editable.contains(target)) return;
|
|
521
|
+
slashClear();
|
|
522
|
+
};
|
|
523
|
+
document.addEventListener('mousedown', onPointerDown);
|
|
524
|
+
return () => document.removeEventListener('mousedown', onPointerDown);
|
|
525
|
+
}, [slashIsOpen, slashClear, composer.textareaRef]);
|
|
526
|
+
|
|
527
|
+
// Slash-command floating popover. Anchored to the top of the input
|
|
528
|
+
// surface; appears just above it so it doesn't cover the textarea.
|
|
529
|
+
const slashOverlay =
|
|
530
|
+
slashIsOpen ? (
|
|
531
|
+
<div
|
|
532
|
+
className={cn(
|
|
533
|
+
'pointer-events-auto absolute left-2.5 right-2.5 z-20',
|
|
534
|
+
'bottom-full mb-1.5',
|
|
535
|
+
)}
|
|
536
|
+
>
|
|
537
|
+
<SlashMenu
|
|
538
|
+
ref={slashMenuRef}
|
|
539
|
+
matches={slash.matches}
|
|
540
|
+
highlight={slash.highlight}
|
|
541
|
+
query={slash.query}
|
|
542
|
+
onPick={slash.pick}
|
|
543
|
+
onHighlight={slash.setHighlight}
|
|
544
|
+
/>
|
|
545
|
+
</div>
|
|
546
|
+
) : null;
|
|
547
|
+
|
|
431
548
|
// Start cluster sits left of the textarea, end cluster right of it โ
|
|
432
549
|
// `[๐][actionsStart] [textarea] [actionsEnd][๐][โถ]` (ยง3.6).
|
|
433
550
|
if (layout === 'inline') {
|
|
@@ -437,6 +554,7 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
437
554
|
<div
|
|
438
555
|
ref={ref}
|
|
439
556
|
{...dragProps}
|
|
557
|
+
onKeyDownCapture={handleSlashKeyDownCapture}
|
|
440
558
|
className={cn(
|
|
441
559
|
'relative border-t border-border bg-background/95',
|
|
442
560
|
sz.containerPadding,
|
|
@@ -446,6 +564,7 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
446
564
|
>
|
|
447
565
|
{hiddenFileInput}
|
|
448
566
|
{dragOverlay}
|
|
567
|
+
{slashOverlay}
|
|
449
568
|
{trayNode ? <div className="mb-1.5">{trayNode}</div> : null}
|
|
450
569
|
<div
|
|
451
570
|
className={cn(
|
|
@@ -554,6 +673,7 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
554
673
|
<div
|
|
555
674
|
ref={ref}
|
|
556
675
|
{...dragProps}
|
|
676
|
+
onKeyDownCapture={handleSlashKeyDownCapture}
|
|
557
677
|
className={cn(
|
|
558
678
|
'relative border-t border-border bg-background/95',
|
|
559
679
|
sz.containerPadding,
|
|
@@ -563,6 +683,7 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
563
683
|
>
|
|
564
684
|
{hiddenFileInput}
|
|
565
685
|
{dragOverlay}
|
|
686
|
+
{slashOverlay}
|
|
566
687
|
{trayNode ? <div className="mb-2">{trayNode}</div> : null}
|
|
567
688
|
{surfaceWrapper}
|
|
568
689
|
{footerNode}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
LazyMarkdownEditor as MarkdownEditor,
|
|
7
7
|
type MarkdownEditorHandle,
|
|
8
8
|
type MentionConfig,
|
|
9
|
+
type SlashCommandInfo,
|
|
9
10
|
} from '@djangocfg/ui-tools/markdown-editor';
|
|
10
11
|
|
|
11
12
|
import { useRegisterComposer } from '../hooks/useAutoFocusOnStreamEnd';
|
|
@@ -21,6 +22,23 @@ const MIN_HEIGHT: Record<ComposerSize, number> = {
|
|
|
21
22
|
export interface ComposerRichTextareaProps extends ComposerTextareaProps {
|
|
22
23
|
/** `@`-mention autocomplete config. Omit for a plain rich textarea. */
|
|
23
24
|
mentions?: MentionConfig;
|
|
25
|
+
/**
|
|
26
|
+
* Slash-command verb list. When set, a leading `/verb` in the editor
|
|
27
|
+
* is painted as an atom chip (same primary-tinted styling as the
|
|
28
|
+
* plain `<SlashHighlightTextarea>` mirror). The chat composer's slash
|
|
29
|
+
* menu already lives outside the editor and keeps working without
|
|
30
|
+
* this prop โ pass the verb list here when you also want the
|
|
31
|
+
* in-editor highlight to match.
|
|
32
|
+
*
|
|
33
|
+
* Structurally compatible with `SlashCommand` from
|
|
34
|
+
* `@djangocfg/ui-tools/chat` โ passing the same array you wire into
|
|
35
|
+
* `composerSlots.slashCommands` is the normal pattern.
|
|
36
|
+
*
|
|
37
|
+
* Note: like `mentions`, the underlying TipTap extension is captured
|
|
38
|
+
* once on first render. If you may register slash commands later,
|
|
39
|
+
* pass `[]` from the very first render and mutate the list in place.
|
|
40
|
+
*/
|
|
41
|
+
slashCommands?: readonly SlashCommandInfo[];
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
/**
|
|
@@ -42,10 +60,16 @@ export function ComposerRichTextarea({
|
|
|
42
60
|
size,
|
|
43
61
|
className,
|
|
44
62
|
mentions,
|
|
63
|
+
slashCommands,
|
|
45
64
|
}: ComposerRichTextareaProps) {
|
|
46
65
|
// Tiptap captures the mentions object once on first render โ keep its
|
|
47
66
|
// identity stable so the suggestion plugin stays wired.
|
|
48
67
|
const stableMentions = useMemo(() => mentions, [mentions]);
|
|
68
|
+
// `slashCommands` flows through every render โ the editor's
|
|
69
|
+
// SlashCommandNode is registered once on first mount (gated on
|
|
70
|
+
// `slashCommands !== undefined`) and the current list is read via a
|
|
71
|
+
// ref by the value-sync effect, so runtime additions / removals
|
|
72
|
+
// still light up the chip.
|
|
49
73
|
|
|
50
74
|
// The TipTap editor has no `<textarea>`, so the composer's default
|
|
51
75
|
// `focus` handle (which targets `textareaRef`) is a no-op here.
|
|
@@ -73,6 +97,7 @@ export function ComposerRichTextarea({
|
|
|
73
97
|
value={composer.value}
|
|
74
98
|
onChange={composer.setValue}
|
|
75
99
|
mentions={stableMentions}
|
|
100
|
+
slashCommands={slashCommands}
|
|
76
101
|
placeholder={placeholder}
|
|
77
102
|
minHeight={MIN_HEIGHT[size]}
|
|
78
103
|
showToolbar={false}
|
|
@@ -40,6 +40,28 @@ export {
|
|
|
40
40
|
} from './useComposerAttach';
|
|
41
41
|
export { useComposerAttachContext } from './AttachContext';
|
|
42
42
|
export { fileToAttachment, revokeAttachmentUrl } from './fileToAttachment';
|
|
43
|
+
export {
|
|
44
|
+
parseSlashState,
|
|
45
|
+
filterCommands,
|
|
46
|
+
applyCommand,
|
|
47
|
+
resolveCommandAction,
|
|
48
|
+
extractSlashToken,
|
|
49
|
+
useSlashCommands,
|
|
50
|
+
SlashMenu,
|
|
51
|
+
SlashToken,
|
|
52
|
+
SlashHighlightTextarea,
|
|
53
|
+
DEFAULT_SLASH_LABELS,
|
|
54
|
+
type SlashCommand,
|
|
55
|
+
type SlashState,
|
|
56
|
+
type SlashConfig,
|
|
57
|
+
type SlashCommandAction,
|
|
58
|
+
type UseSlashCommandsOptions,
|
|
59
|
+
type UseSlashCommandsReturn,
|
|
60
|
+
type SlashMenuProps,
|
|
61
|
+
type SlashTokenProps,
|
|
62
|
+
type SlashHighlightTextareaProps,
|
|
63
|
+
type SlashMenuLabels,
|
|
64
|
+
} from './slash';
|
|
43
65
|
export type {
|
|
44
66
|
ComposerAction,
|
|
45
67
|
ComposerActionVisibility,
|