@djangocfg/ui-tools 2.1.407 → 2.1.409
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/README.md +9 -10
- package/dist/file-icon/index.cjs +449 -61
- package/dist/file-icon/index.cjs.map +1 -1
- package/dist/file-icon/index.d.cts +56 -18
- package/dist/file-icon/index.d.ts +56 -18
- package/dist/file-icon/index.mjs +448 -62
- package/dist/file-icon/index.mjs.map +1 -1
- package/dist/tree/index.cjs +49 -22
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +9 -3
- package/dist/tree/index.d.ts +9 -3
- package/dist/tree/index.mjs +49 -22
- package/dist/tree/index.mjs.map +1 -1
- package/dist/{types-B_zhyAqR.d.cts → types-eEu8SeiQ.d.cts} +4 -0
- package/dist/{types-B_zhyAqR.d.ts → types-eEu8SeiQ.d.ts} +4 -0
- package/package.json +8 -13
- package/src/components/FloatingToolbar/index.tsx +37 -3
- package/src/lib/page-snapshot/__tests__/capture-integration.test.ts +85 -0
- package/src/lib/page-snapshot/__tests__/engine.test.ts +36 -0
- package/src/lib/page-snapshot/__tests__/redaction-integration.test.ts +99 -0
- package/src/lib/page-snapshot/__tests__/tokens.test.ts +17 -0
- package/src/lib/page-snapshot/capture/__tests__/budget.test.ts +49 -0
- package/src/lib/page-snapshot/capture/__tests__/chrome-filter.test.ts +47 -0
- package/src/lib/page-snapshot/capture/__tests__/fold.test.ts +66 -0
- package/src/lib/page-snapshot/capture/__tests__/scope.test.ts +74 -0
- package/src/lib/page-snapshot/capture/__tests__/walk.test.ts +129 -0
- package/src/lib/page-snapshot/capture/accessible-name.ts +73 -0
- package/src/lib/page-snapshot/capture/budget.ts +95 -0
- package/src/lib/page-snapshot/capture/chrome-filter.ts +81 -0
- package/src/lib/page-snapshot/capture/classify.ts +111 -0
- package/src/lib/page-snapshot/capture/dom-utils.ts +111 -0
- package/src/lib/page-snapshot/capture/fold.ts +96 -0
- package/src/lib/page-snapshot/capture/scope.ts +169 -0
- package/src/lib/page-snapshot/capture/walk.ts +250 -0
- package/src/lib/page-snapshot/cst/__tests__/serialize.test.ts +50 -0
- package/src/lib/page-snapshot/cst/directives.ts +47 -0
- package/src/lib/page-snapshot/cst/payload.ts +50 -0
- package/src/lib/page-snapshot/cst/serialize.ts +84 -0
- package/src/lib/page-snapshot/cst/types.ts +115 -0
- package/src/lib/page-snapshot/engine.ts +176 -0
- package/src/lib/page-snapshot/index.ts +93 -0
- package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +72 -0
- package/src/lib/page-snapshot/react/PageSnapshotPreview.tsx +78 -0
- package/src/lib/page-snapshot/react/__tests__/PageSnapshotChip.test.tsx +54 -0
- package/src/lib/page-snapshot/react/__tests__/provider.test.tsx +103 -0
- package/src/lib/page-snapshot/react/__tests__/use-page-snapshot-toggle.test.tsx +62 -0
- package/src/lib/page-snapshot/react/provider.tsx +162 -0
- package/src/lib/page-snapshot/react/use-page-snapshot-toggle.ts +47 -0
- package/src/lib/page-snapshot/react/use-page-snapshot.ts +67 -0
- package/src/lib/page-snapshot/redaction/__tests__/audit.test.ts +25 -0
- package/src/lib/page-snapshot/redaction/__tests__/heuristics.test.ts +73 -0
- package/src/lib/page-snapshot/redaction/__tests__/luhn.test.ts +26 -0
- package/src/lib/page-snapshot/redaction/__tests__/patterns.test.ts +60 -0
- package/src/lib/page-snapshot/redaction/audit.ts +58 -0
- package/src/lib/page-snapshot/redaction/heuristics.ts +75 -0
- package/src/lib/page-snapshot/redaction/index.ts +75 -0
- package/src/lib/page-snapshot/redaction/luhn.ts +25 -0
- package/src/lib/page-snapshot/redaction/patterns.ts +111 -0
- package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +24 -0
- package/src/lib/page-snapshot/refs/registry.ts +46 -0
- package/src/lib/page-snapshot/staleness/__tests__/hash.test.ts +34 -0
- package/src/lib/page-snapshot/staleness/hash.ts +20 -0
- package/src/lib/page-snapshot/tokens.ts +15 -0
- package/src/tools/AudioPlayer/context/PlayerProvider.tsx +13 -14
- package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +55 -6
- package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +2 -5
- package/src/tools/Chat/README.md +277 -39
- package/src/tools/Chat/composer/Composer.tsx +471 -0
- package/src/tools/Chat/composer/ComposerActionBar.tsx +65 -0
- package/src/tools/Chat/composer/ComposerBanner.tsx +128 -0
- package/src/tools/Chat/composer/ComposerButton.tsx +64 -0
- package/src/tools/Chat/composer/ComposerFooter.tsx +90 -0
- package/src/tools/Chat/composer/ComposerMenuButton.tsx +62 -0
- package/src/tools/Chat/composer/ComposerModelPicker.tsx +104 -0
- package/src/tools/Chat/composer/ComposerRichTextarea.tsx +88 -0
- package/src/tools/Chat/composer/ComposerToolPill.tsx +95 -0
- package/src/tools/Chat/composer/index.ts +45 -0
- package/src/tools/Chat/composer/size-context.tsx +26 -0
- package/src/tools/Chat/composer/types.ts +143 -0
- package/src/tools/Chat/composer/useComposerActions.tsx +164 -0
- package/src/tools/Chat/context/ChatProvider.tsx +54 -3
- package/src/tools/Chat/core/__tests__/metadata.test.ts +69 -0
- package/src/tools/Chat/core/index.ts +23 -1
- package/src/tools/Chat/core/markdown.ts +1 -1
- package/src/tools/Chat/core/metadata.ts +47 -0
- package/src/tools/Chat/core/payload-dispatch.ts +1 -1
- package/src/tools/Chat/core/transport/http.ts +71 -32
- package/src/tools/Chat/core/transport/sse.ts +18 -10
- package/src/tools/Chat/highlight/HighlightOverlay.tsx +101 -0
- package/src/tools/Chat/highlight/README.md +103 -0
- package/src/tools/Chat/highlight/SpotlightCanvas.tsx +153 -0
- package/src/tools/Chat/highlight/__tests__/HighlightOverlay.test.tsx +112 -0
- package/src/tools/Chat/highlight/__tests__/resolveRef.test.ts +55 -0
- package/src/tools/Chat/highlight/index.ts +21 -0
- package/src/tools/Chat/highlight/resolveRef.ts +42 -0
- package/src/tools/Chat/highlight/types.ts +49 -0
- package/src/tools/Chat/highlight/useHighlightTargets.ts +128 -0
- package/src/tools/Chat/hooks/index.ts +0 -5
- package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +28 -47
- package/src/tools/Chat/hooks/useChat.ts +47 -14
- package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
- package/src/tools/Chat/hooks/useChatLayout.ts +1 -1
- package/src/tools/Chat/hooks/useStreamEndFocus.ts +54 -0
- package/src/tools/Chat/index.ts +25 -219
- package/src/tools/Chat/launcher/ChatDock.tsx +1 -1
- package/src/tools/Chat/launcher/ChatLauncher.tsx +1 -1
- package/src/tools/Chat/launcher/{ChatHeader.tsx → header/ChatHeader.tsx} +24 -11
- package/src/tools/Chat/launcher/{ChatHeaderActionButton.tsx → header/ChatHeaderActionButton.tsx} +34 -3
- package/src/tools/Chat/launcher/{ChatHeaderLanguageButton.tsx → header/ChatHeaderLanguageButton.tsx} +2 -2
- package/src/tools/Chat/launcher/{ChatHeaderModeToggle.tsx → header/ChatHeaderModeToggle.tsx} +1 -1
- package/src/tools/Chat/launcher/{ChatHeaderResetButton.tsx → header/ChatHeaderResetButton.tsx} +2 -1
- package/src/tools/Chat/launcher/{HeaderSlots.tsx → header/HeaderSlots.tsx} +3 -3
- package/src/tools/Chat/launcher/header/index.ts +26 -0
- package/src/tools/Chat/launcher/index.ts +3 -10
- package/src/tools/Chat/lazy.tsx +38 -284
- package/src/tools/Chat/{components → messages}/MessageBubble.tsx +58 -5
- package/src/tools/Chat/{components → messages}/MessageList.tsx +8 -25
- package/src/tools/Chat/messages/blocks/MessageBlocks.tsx +131 -0
- package/src/tools/Chat/messages/blocks/builtin.tsx +91 -0
- package/src/tools/Chat/messages/blocks/index.ts +12 -0
- package/src/tools/Chat/messages/blocks/registry.tsx +42 -0
- package/src/tools/Chat/messages/blocks/renderers/AudioBlock.tsx +20 -0
- package/src/tools/Chat/messages/blocks/renderers/CodeBlock.tsx +19 -0
- package/src/tools/Chat/messages/blocks/renderers/GalleryBlock.tsx +26 -0
- package/src/tools/Chat/messages/blocks/renderers/ImageBlock.tsx +27 -0
- package/src/tools/Chat/messages/blocks/renderers/JsonBlock.tsx +12 -0
- package/src/tools/Chat/messages/blocks/renderers/LottieBlock.tsx +11 -0
- package/src/tools/Chat/messages/blocks/renderers/MapBlock.tsx +36 -0
- package/src/tools/Chat/messages/blocks/renderers/MermaidBlock.tsx +11 -0
- package/src/tools/Chat/messages/blocks/renderers/VideoBlock.tsx +24 -0
- package/src/tools/Chat/messages/blocks/renderers/types.ts +8 -0
- package/src/tools/Chat/{components → messages}/index.ts +11 -5
- package/src/tools/Chat/public.ts +212 -0
- package/src/tools/Chat/shell/ChatRoot.tsx +345 -0
- package/src/tools/Chat/{components → shell}/EmptyState.tsx +4 -2
- package/src/tools/Chat/shell/index.ts +15 -0
- package/src/tools/Chat/types/block.ts +120 -0
- package/src/tools/Chat/types/config.ts +0 -5
- package/src/tools/Chat/types/index.ts +17 -0
- package/src/tools/Chat/types/message.ts +3 -0
- package/src/tools/Chat/utils/index.ts +4 -0
- package/src/tools/CodeEditor/README.md +4 -6
- package/src/tools/CodeEditor/components/DiffEditor.tsx +48 -13
- package/src/tools/CodeEditor/components/Editor.tsx +96 -44
- package/src/tools/CodeEditor/context/EditorProvider.tsx +34 -17
- package/src/tools/CodeEditor/hooks/useEditorTheme.ts +92 -99
- package/src/tools/CodeEditor/hooks/useMonaco.ts +37 -22
- package/src/tools/CodeEditor/lazy.tsx +6 -0
- package/src/tools/CodeEditor/lib/index.ts +1 -1
- package/src/tools/CodeEditor/lib/themes.ts +3 -39
- package/src/tools/CronScheduler/CronScheduler.client.tsx +230 -61
- package/src/tools/CronScheduler/components/CustomInput.tsx +21 -4
- package/src/tools/CronScheduler/components/DayChips.tsx +13 -11
- package/src/tools/CronScheduler/components/MonthDayGrid.tsx +4 -4
- package/src/tools/CronScheduler/components/SchedulePreview.tsx +7 -3
- package/src/tools/CronScheduler/components/TimeSelector.tsx +1 -1
- package/src/tools/CronScheduler/index.tsx +1 -1
- package/src/tools/CronScheduler/types/index.ts +8 -3
- package/src/tools/CronScheduler/utils/cron-humanize.ts +61 -16
- package/src/tools/CronScheduler/utils/cron-parser.ts +13 -4
- package/src/tools/FileIcon/FileIcon.tsx +24 -39
- package/src/tools/FileIcon/get-file-icon.ts +73 -0
- package/src/tools/FileIcon/icons/icon-data.ts +399 -0
- package/src/tools/FileIcon/index.ts +4 -0
- package/src/tools/FileIcon/loader.ts +17 -35
- package/src/tools/FileIcon/specialFolders.ts +18 -0
- package/src/tools/Gallery/components/lightbox/GalleryLightbox.tsx +112 -35
- package/src/tools/Gallery/components/media/GalleryVideo.tsx +21 -2
- package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +11 -1
- package/src/tools/Gallery/hooks/usePreloadImages.ts +54 -7
- package/src/tools/ImageViewer/components/ImageInfo.tsx +12 -1
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +51 -43
- package/src/tools/ImageViewer/components/ImageViewer.tsx +96 -24
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +13 -0
- package/src/tools/ImageViewer/utils/constants.ts +3 -0
- package/src/tools/ImageViewer/utils/index.ts +1 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +4 -1
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +5 -3
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +7 -4
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +3 -1
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +23 -3
- package/src/tools/JsonForm/widgets/ColorWidget.tsx +20 -12
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +14 -9
- package/src/tools/JsonForm/widgets/RadioWidget.tsx +78 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +1 -0
- package/src/tools/JsonForm/widgets/SliderWidget.tsx +7 -4
- package/src/tools/JsonForm/widgets/TextWidget.tsx +41 -17
- package/src/tools/JsonForm/widgets/index.ts +1 -0
- package/src/tools/JsonTree/components/JsonContent.tsx +115 -40
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +177 -72
- package/src/tools/LottiePlayer/index.tsx +14 -4
- package/src/tools/LottiePlayer/lazy.tsx +11 -3
- package/src/tools/LottiePlayer/types.ts +31 -1
- package/src/tools/LottiePlayer/useLottie.ts +32 -9
- package/src/tools/LottiePlayer/usePrefersReducedMotion.ts +46 -0
- package/src/tools/Map/components/LayerSwitcher.tsx +54 -21
- package/src/tools/Map/components/MapCluster.tsx +28 -21
- package/src/tools/Map/components/MapContainer.tsx +11 -4
- package/src/tools/Map/components/MapLegend.tsx +46 -15
- package/src/tools/Map/components/MapMarker.tsx +31 -2
- package/src/tools/Map/hooks/useMapEvents.ts +64 -105
- package/src/tools/MarkdownEditor/MarkdownEditor.tsx +61 -6
- package/src/tools/MarkdownEditor/MentionList.tsx +37 -4
- package/src/tools/MarkdownEditor/createMentionSuggestion.ts +11 -0
- package/src/tools/MarkdownEditor/lazy.tsx +32 -7
- package/src/tools/MarkdownEditor/styles.css +13 -0
- package/src/tools/MarkdownMessage/CodeBlock.tsx +40 -17
- package/src/tools/MarkdownMessage/MarkdownMessage.tsx +26 -6
- package/src/tools/MarkdownMessage/components.tsx +22 -9
- package/src/tools/MarkdownMessage/types.ts +24 -1
- package/src/tools/Mermaid/Mermaid.client.tsx +27 -5
- package/src/tools/Mermaid/components/MermaidErrorPanel.tsx +31 -0
- package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +14 -17
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +264 -168
- package/src/tools/Mermaid/hooks/useMermaidValidation.ts +76 -10
- package/src/tools/Mermaid/index.tsx +6 -0
- package/src/tools/Mermaid/utils/mermaid-helpers.ts +141 -18
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +11 -1
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +49 -20
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +7 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +7 -4
- package/src/tools/OpenapiViewer/constants.ts +3 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +73 -11
- package/src/tools/OpenapiViewer/utils/schemaExport.ts +26 -6
- package/src/tools/PrettyCode/PrettyCode.client.tsx +23 -16
- package/src/tools/PrettyCode/lazy.tsx +1 -1
- package/src/tools/SpeechRecognition/README.md +1 -1
- package/src/tools/SpeechRecognition/__tests__/language.test.ts +9 -3
- package/src/tools/SpeechRecognition/components/RecordingPulse.tsx +59 -0
- package/src/tools/SpeechRecognition/components/index.ts +2 -0
- package/src/tools/SpeechRecognition/core/engine/external.ts +24 -7
- package/src/tools/SpeechRecognition/core/language.ts +23 -6
- package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +36 -5
- package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +18 -11
- package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +94 -26
- package/src/tools/SpeechRecognition/widgets/index.ts +1 -1
- package/src/tools/Tree/README.md +4 -8
- package/src/tools/Tree/TreeRoot.tsx +22 -10
- package/src/tools/Tree/components/TreeContent.tsx +24 -4
- package/src/tools/Tree/components/TreeLabel.tsx +8 -2
- package/src/tools/Tree/components/TreeRow.tsx +16 -6
- package/src/tools/Tree/data/flatten.ts +10 -4
- package/src/tools/Tree/types.ts +4 -0
- package/src/tools/Uploader/components/UploadAddButton.tsx +29 -6
- package/src/tools/Uploader/components/UploadDropzone.tsx +63 -7
- package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +19 -5
- package/src/tools/Uploader/components/UploadPreviewItem.tsx +47 -17
- package/src/tools/Uploader/components/UploadPreviewList.tsx +24 -12
- package/src/tools/Uploader/utils/formatters.ts +8 -3
- package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +1 -0
- package/src/tools/VideoPlayer/canvas/{jsx.d.ts → jsx-augmentation.ts} +12 -19
- package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +1 -0
- package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +1 -0
- package/src/tools/VideoPlayer/parts/fullscreen.tsx +1 -1
- package/src/tools/VideoPlayer/parts/pip.tsx +1 -1
- package/src/tools/VideoPlayer/parts/playback-rate.tsx +1 -1
- package/src/tools/VideoPlayer/parts/seek-bar.tsx +2 -2
- package/src/tools/VideoPlayer/parts/volume.tsx +2 -2
- package/src/tools/index.ts +2 -1
- package/src/tools/Chat/components/AudioToggle.tsx +0 -78
- package/src/tools/Chat/components/ChatRoot.tsx +0 -305
- package/src/tools/Chat/components/Composer.tsx +0 -216
- package/src/tools/Chat/hooks/useChatScroll.ts +0 -145
- package/src/tools/Chat/types.ts +0 -9
- package/src/tools/JsonTree/components/JsonToolbar.tsx +0 -95
- package/src/tools/JsonTree/hooks/useElementCorner.ts +0 -84
- package/src/tools/JsonTree/hooks/useNavbarHeight.ts +0 -83
- package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +0 -121
- package/src/tools/Tour/README.md +0 -373
- package/src/tools/Tour/components/Tour.tsx +0 -12
- package/src/tools/Tour/components/TourContent.tsx +0 -171
- package/src/tools/Tour/components/TourNavigation.tsx +0 -77
- package/src/tools/Tour/components/TourProgress.tsx +0 -88
- package/src/tools/Tour/components/TourSpotlight.tsx +0 -199
- package/src/tools/Tour/components/index.ts +0 -5
- package/src/tools/Tour/context/TourContext.ts +0 -19
- package/src/tools/Tour/context/TourProvider.tsx +0 -292
- package/src/tools/Tour/context/index.ts +0 -2
- package/src/tools/Tour/hooks/index.ts +0 -3
- package/src/tools/Tour/hooks/useKeyboardNavigation.ts +0 -59
- package/src/tools/Tour/hooks/useStepTarget.ts +0 -121
- package/src/tools/Tour/hooks/useTour.ts +0 -42
- package/src/tools/Tour/index.ts +0 -38
- package/src/tools/Tour/types/index.ts +0 -224
- package/src/tools/Tour/utils/dom.ts +0 -98
- package/src/tools/Tour/utils/index.ts +0 -3
- package/src/tools/Tour/utils/logger.ts +0 -3
- package/src/tools/Tour/utils/scrollIntoView.ts +0 -24
- /package/src/tools/Chat/{config.ts → constants.ts} +0 -0
- /package/src/tools/Chat/launcher/{ChatHeaderAudioToggle.tsx → header/ChatHeaderAudioToggle.tsx} +0 -0
- /package/src/tools/Chat/{components → messages}/Attachments.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/JumpToLatest.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/MessageActions.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/Sources.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/StreamingIndicator.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/ToolCalls.tsx +0 -0
- /package/src/tools/Chat/{components → shell}/ErrorBanner.tsx +0 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useEffect, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
import { Textarea } from '@djangocfg/ui-core/components';
|
|
6
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
7
|
+
|
|
8
|
+
import { useChatContextOptional } from '../context';
|
|
9
|
+
import type { UseChatComposerReturn } from '../hooks/useChatComposer';
|
|
10
|
+
import { Attachments } from '../messages/Attachments';
|
|
11
|
+
import { ComposerActionBar } from './ComposerActionBar';
|
|
12
|
+
import { ComposerButton } from './ComposerButton';
|
|
13
|
+
import { ComposerFooter } from './ComposerFooter';
|
|
14
|
+
import { ComposerSizeProvider } from './size-context';
|
|
15
|
+
import { useComposerActions } from './useComposerActions';
|
|
16
|
+
import type {
|
|
17
|
+
ComposerAppearance,
|
|
18
|
+
ComposerFooterProps,
|
|
19
|
+
ComposerLayout,
|
|
20
|
+
ComposerSize,
|
|
21
|
+
ComposerSlotComponents,
|
|
22
|
+
ComposerSlotProps,
|
|
23
|
+
ComposerSlots,
|
|
24
|
+
} from './types';
|
|
25
|
+
|
|
26
|
+
export type { ComposerSize, ComposerAppearance } from './types';
|
|
27
|
+
|
|
28
|
+
export interface ComposerProps {
|
|
29
|
+
composer: UseChatComposerReturn;
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
showAttachmentButton?: boolean;
|
|
33
|
+
onPickFiles?: () => void;
|
|
34
|
+
className?: string;
|
|
35
|
+
textareaClassName?: string;
|
|
36
|
+
/** Visual size — controls textarea height + button slot size.
|
|
37
|
+
*
|
|
38
|
+
* - ``sm`` — dense compact composer (admin sidebars, etc); defaults
|
|
39
|
+
* to the `inline` single-row layout.
|
|
40
|
+
* - ``md`` — default. Stacked layout (textarea + action bar).
|
|
41
|
+
* - ``lg`` — generous textarea. Use when the chat is the page's
|
|
42
|
+
* primary surface (onboarding, support).
|
|
43
|
+
*/
|
|
44
|
+
size?: ComposerSize;
|
|
45
|
+
/** Spaciousness of the surface — orthogonal to `size`.
|
|
46
|
+
*
|
|
47
|
+
* - ``compact`` — default; embedded composer (FAB panel, dock).
|
|
48
|
+
* - ``full`` — full-page chat; roomier surface, taller textarea,
|
|
49
|
+
* larger padding + radius. The host owns the centered max-width
|
|
50
|
+
* container; the composer just makes itself more spacious.
|
|
51
|
+
*/
|
|
52
|
+
appearance?: ComposerAppearance;
|
|
53
|
+
/** Show "Stop" button instead of "Send" while streaming. */
|
|
54
|
+
isStreaming?: boolean;
|
|
55
|
+
onCancel?: () => void;
|
|
56
|
+
|
|
57
|
+
// ── Slot system ──────────────────────────────────────────────────────────
|
|
58
|
+
/** Layout geometry. Default `stacked`; `size="sm"` falls back to `inline`. */
|
|
59
|
+
layout?: ComposerLayout;
|
|
60
|
+
/** Tier A — declarative action descriptors + raw-node escape hatches. */
|
|
61
|
+
composerSlots?: ComposerSlots;
|
|
62
|
+
/** Tier B — replace a primitive entirely. */
|
|
63
|
+
slots?: ComposerSlotComponents;
|
|
64
|
+
/** Per-slot prop overrides for the built-in primitives. */
|
|
65
|
+
slotProps?: ComposerSlotProps;
|
|
66
|
+
/** Telegram-style mic↔send swap. Default `true`. */
|
|
67
|
+
micSendSwap?: boolean;
|
|
68
|
+
/** Footer toolbar config below the input surface. `false` hides it. */
|
|
69
|
+
footer?: ComposerFooterProps | false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const SIZE_CLASSES: Record<ComposerSize, {
|
|
73
|
+
surface: string;
|
|
74
|
+
textarea: string;
|
|
75
|
+
text: string;
|
|
76
|
+
containerPadding: string;
|
|
77
|
+
/** inline-layout slot height enforcement */
|
|
78
|
+
inlineSlot: string;
|
|
79
|
+
}> = {
|
|
80
|
+
sm: {
|
|
81
|
+
surface: 'rounded-xl px-2 py-1',
|
|
82
|
+
textarea: 'min-h-7 max-h-44 px-1.5 py-1',
|
|
83
|
+
text: 'text-sm',
|
|
84
|
+
containerPadding: 'px-2 pt-1.5 pb-[max(0.375rem,env(safe-area-inset-bottom))]',
|
|
85
|
+
inlineSlot: '[&>:not(textarea)]:h-7',
|
|
86
|
+
},
|
|
87
|
+
md: {
|
|
88
|
+
surface: 'rounded-2xl px-2 py-1.5',
|
|
89
|
+
textarea: 'min-h-9 max-h-60 px-2 py-1.5',
|
|
90
|
+
text: 'text-base sm:text-sm',
|
|
91
|
+
containerPadding: 'px-2.5 pt-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]',
|
|
92
|
+
inlineSlot: '[&>:not(textarea)]:h-9',
|
|
93
|
+
},
|
|
94
|
+
lg: {
|
|
95
|
+
surface: 'rounded-2xl px-3 py-2.5',
|
|
96
|
+
textarea: 'min-h-12 max-h-72 px-2.5 py-2',
|
|
97
|
+
text: 'text-base',
|
|
98
|
+
containerPadding: 'px-3.5 pt-3 pb-[max(0.875rem,env(safe-area-inset-bottom))]',
|
|
99
|
+
inlineSlot: '[&>:not(textarea)]:h-12',
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Per-size cluster gap for the stacked grid layout. */
|
|
104
|
+
const GAP: Record<ComposerSize, string> = {
|
|
105
|
+
sm: 'gap-0.5',
|
|
106
|
+
md: 'gap-1',
|
|
107
|
+
lg: 'gap-1.5',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Extra spaciousness layered on top of `size` when `appearance="full"`.
|
|
111
|
+
* Empty strings for `compact` keep the current geometry untouched. */
|
|
112
|
+
const APPEARANCE_CLASSES: Record<ComposerAppearance, {
|
|
113
|
+
surface: string;
|
|
114
|
+
textarea: string;
|
|
115
|
+
text: string;
|
|
116
|
+
containerPadding: string;
|
|
117
|
+
}> = {
|
|
118
|
+
compact: { surface: '', textarea: '', text: '', containerPadding: '' },
|
|
119
|
+
full: {
|
|
120
|
+
surface: 'rounded-3xl px-3 py-2',
|
|
121
|
+
textarea: 'min-h-10 max-h-80 px-2.5 py-2',
|
|
122
|
+
text: 'text-base',
|
|
123
|
+
containerPadding: 'px-4 pt-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]',
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Composer(
|
|
128
|
+
{
|
|
129
|
+
composer,
|
|
130
|
+
placeholder = 'Type a message...',
|
|
131
|
+
disabled,
|
|
132
|
+
showAttachmentButton = false,
|
|
133
|
+
onPickFiles,
|
|
134
|
+
className,
|
|
135
|
+
textareaClassName,
|
|
136
|
+
size = 'md',
|
|
137
|
+
appearance = 'compact',
|
|
138
|
+
isStreaming: isStreamingProp,
|
|
139
|
+
onCancel: onCancelProp,
|
|
140
|
+
layout: layoutProp,
|
|
141
|
+
composerSlots,
|
|
142
|
+
slots,
|
|
143
|
+
slotProps,
|
|
144
|
+
micSendSwap = true,
|
|
145
|
+
footer,
|
|
146
|
+
},
|
|
147
|
+
ref,
|
|
148
|
+
) {
|
|
149
|
+
const ctx = useChatContextOptional();
|
|
150
|
+
const isStreaming = isStreamingProp ?? ctx?.isStreaming ?? false;
|
|
151
|
+
const onCancel = onCancelProp ?? ctx?.cancelStream;
|
|
152
|
+
const isDisabled = disabled ?? isStreaming;
|
|
153
|
+
const sz = SIZE_CLASSES[size];
|
|
154
|
+
// `appearance` is orthogonal to `size` — `full` layers extra
|
|
155
|
+
// spaciousness (radius, padding, textarea height) over the chosen size.
|
|
156
|
+
const ap = APPEARANCE_CLASSES[appearance];
|
|
157
|
+
// `size="sm"` defaults to the single-row `inline` layout unless the
|
|
158
|
+
// host overrides it explicitly — see §3.1 of the redesign doc.
|
|
159
|
+
const layout: ComposerLayout = layoutProp ?? (size === 'sm' ? 'inline' : 'stacked');
|
|
160
|
+
|
|
161
|
+
// Register the composer's handle with the chat context so other parts
|
|
162
|
+
// of the tree (useAutoFocusOnStreamEnd, VoiceComposerSlot) can drive it
|
|
163
|
+
// imperatively. No-op when used outside a ChatProvider.
|
|
164
|
+
const register = ctx?.registerComposer;
|
|
165
|
+
const composerFocus = composer.focus;
|
|
166
|
+
const composerSetValue = composer.setValue;
|
|
167
|
+
const textareaRef = composer.textareaRef;
|
|
168
|
+
const getValueRef = useRef<() => string>(() => composer.value);
|
|
169
|
+
getValueRef.current = () => composer.value;
|
|
170
|
+
// A custom `Textarea` slot (e.g. the TipTap `<ComposerRichTextarea>`)
|
|
171
|
+
// has no `<textarea>` — the default `composer.focus` would be a no-op.
|
|
172
|
+
// In that case the slot registers its own imperative handle; skip
|
|
173
|
+
// ours so we don't overwrite it (child effects run before parent).
|
|
174
|
+
const hasCustomTextarea = !!slots?.Textarea;
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!register || hasCustomTextarea) return;
|
|
177
|
+
register({
|
|
178
|
+
focus: composerFocus,
|
|
179
|
+
moveCursorToEnd: () => {
|
|
180
|
+
const el = textareaRef.current;
|
|
181
|
+
if (!el) return;
|
|
182
|
+
const end = el.value.length;
|
|
183
|
+
el.setSelectionRange(end, end);
|
|
184
|
+
},
|
|
185
|
+
getValue: () => getValueRef.current(),
|
|
186
|
+
setValue: composerSetValue,
|
|
187
|
+
});
|
|
188
|
+
return () => register(null);
|
|
189
|
+
}, [register, hasCustomTextarea, composerFocus, composerSetValue, textareaRef]);
|
|
190
|
+
|
|
191
|
+
const inlineStart = composerSlots?.inlineStart;
|
|
192
|
+
const inlineEnd = composerSlots?.inlineEnd;
|
|
193
|
+
const blockStart = composerSlots?.blockStart;
|
|
194
|
+
|
|
195
|
+
// Clicking the padding around the input (not a button/link) should focus
|
|
196
|
+
// the editor — same affordance as ChatGPT/Gemini. Works for both the
|
|
197
|
+
// plain `<textarea>` and the TipTap (`contenteditable`) backend by
|
|
198
|
+
// querying the focusable node inside the surface itself, so it does not
|
|
199
|
+
// depend on which textarea variant is mounted.
|
|
200
|
+
const surfaceRef = useRef<HTMLDivElement>(null);
|
|
201
|
+
const handleSurfaceMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
202
|
+
if (isDisabled) return;
|
|
203
|
+
const target = e.target as HTMLElement;
|
|
204
|
+
// A real interactive element (or the editor itself) handles its own
|
|
205
|
+
// focus — only act on clicks that land on the bare surface padding.
|
|
206
|
+
if (
|
|
207
|
+
target.closest(
|
|
208
|
+
'textarea, input, button, a, [contenteditable="true"], [role="button"], [role="menu"]',
|
|
209
|
+
)
|
|
210
|
+
) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const editable = surfaceRef.current?.querySelector<HTMLElement>(
|
|
214
|
+
'textarea, [contenteditable="true"]',
|
|
215
|
+
);
|
|
216
|
+
if (!editable) return;
|
|
217
|
+
// Prevent the mousedown from blurring/again-selecting and move focus
|
|
218
|
+
// (caret lands at the end via the textarea's own selection on focus).
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
editable.focus();
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Merge built-in send/stop/attach descriptors with host arrays.
|
|
224
|
+
const { actionsStart, actionsEnd } = useComposerActions({
|
|
225
|
+
composer,
|
|
226
|
+
isStreaming,
|
|
227
|
+
isDisabled,
|
|
228
|
+
showAttachmentButton,
|
|
229
|
+
onPickFiles,
|
|
230
|
+
actionsStart: composerSlots?.actionsStart,
|
|
231
|
+
actionsEnd: composerSlots?.actionsEnd,
|
|
232
|
+
onSend: () => void composer.submit(),
|
|
233
|
+
onCancel,
|
|
234
|
+
micSendSwap,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const SendSlot = slots?.SendButton;
|
|
238
|
+
const AttachSlot = slots?.AttachButton;
|
|
239
|
+
const TextareaSlot = slots?.Textarea;
|
|
240
|
+
const ActionBarSlot = slots?.ActionBar;
|
|
241
|
+
|
|
242
|
+
// ── Textarea ───────────────────────────────────────────────────────────
|
|
243
|
+
const textareaNode = TextareaSlot ? (
|
|
244
|
+
<TextareaSlot
|
|
245
|
+
composer={composer}
|
|
246
|
+
placeholder={placeholder}
|
|
247
|
+
disabled={isDisabled}
|
|
248
|
+
size={size}
|
|
249
|
+
className={slotProps?.textarea?.className ?? textareaClassName}
|
|
250
|
+
/>
|
|
251
|
+
) : (
|
|
252
|
+
<Textarea
|
|
253
|
+
{...composer.textareaProps}
|
|
254
|
+
rows={1}
|
|
255
|
+
placeholder={placeholder}
|
|
256
|
+
aria-label={placeholder}
|
|
257
|
+
aria-multiline="true"
|
|
258
|
+
aria-keyshortcuts="Enter"
|
|
259
|
+
disabled={isDisabled}
|
|
260
|
+
className={cn(
|
|
261
|
+
'flex-1 resize-none border-0 bg-transparent shadow-none',
|
|
262
|
+
'focus-visible:ring-0 focus-visible:outline-none',
|
|
263
|
+
sz.textarea,
|
|
264
|
+
sz.text,
|
|
265
|
+
ap.textarea,
|
|
266
|
+
ap.text,
|
|
267
|
+
slotProps?.textarea?.className,
|
|
268
|
+
textareaClassName,
|
|
269
|
+
)}
|
|
270
|
+
/>
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// ── Action bar ─────────────────────────────────────────────────────────
|
|
274
|
+
const actionBarNode = ActionBarSlot ? (
|
|
275
|
+
<ActionBarSlot
|
|
276
|
+
actionsStart={actionsStart}
|
|
277
|
+
actionsEnd={actionsEnd}
|
|
278
|
+
size={size}
|
|
279
|
+
layout={layout}
|
|
280
|
+
inlineStart={inlineStart}
|
|
281
|
+
inlineEnd={inlineEnd}
|
|
282
|
+
/>
|
|
283
|
+
) : (
|
|
284
|
+
<ComposerActionBar
|
|
285
|
+
actionsStart={actionsStart}
|
|
286
|
+
actionsEnd={actionsEnd}
|
|
287
|
+
size={size}
|
|
288
|
+
layout={layout}
|
|
289
|
+
inlineStart={inlineStart}
|
|
290
|
+
inlineEnd={inlineEnd}
|
|
291
|
+
className={slotProps?.actionBar?.className}
|
|
292
|
+
/>
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Tier B fully-custom Send / Attach replacements override the
|
|
296
|
+
// descriptor-driven action bar. They render alongside it for hosts
|
|
297
|
+
// that need a branded button — the built-in send still lives in
|
|
298
|
+
// `actionsEnd` unless the host also drops it from `composerSlots`.
|
|
299
|
+
const customSend = SendSlot ? (
|
|
300
|
+
<SendSlot
|
|
301
|
+
streaming={isStreaming}
|
|
302
|
+
disabled={!composer.canSubmit}
|
|
303
|
+
size={size}
|
|
304
|
+
onSend={() => void composer.submit()}
|
|
305
|
+
onCancel={onCancel}
|
|
306
|
+
{...slotProps?.send}
|
|
307
|
+
/>
|
|
308
|
+
) : null;
|
|
309
|
+
const customAttach =
|
|
310
|
+
AttachSlot && showAttachmentButton ? (
|
|
311
|
+
<AttachSlot
|
|
312
|
+
disabled={isDisabled}
|
|
313
|
+
size={size}
|
|
314
|
+
onClick={() => onPickFiles?.()}
|
|
315
|
+
{...slotProps?.attach}
|
|
316
|
+
/>
|
|
317
|
+
) : null;
|
|
318
|
+
|
|
319
|
+
// ── Footer ─────────────────────────────────────────────────────────────
|
|
320
|
+
const footerNode =
|
|
321
|
+
footer === false ? null : (
|
|
322
|
+
<ComposerFooter
|
|
323
|
+
{...footer}
|
|
324
|
+
value={composer.value}
|
|
325
|
+
size={footer?.size ?? size}
|
|
326
|
+
/>
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const trayNode =
|
|
330
|
+
blockStart ??
|
|
331
|
+
(composer.attachments.length > 0 ? (
|
|
332
|
+
<Attachments
|
|
333
|
+
attachments={composer.attachments}
|
|
334
|
+
onRemove={(a) => composer.removeAttachment(a.id)}
|
|
335
|
+
/>
|
|
336
|
+
) : null);
|
|
337
|
+
|
|
338
|
+
// ── Inline (compact single-row) layout ─────────────────────────────────
|
|
339
|
+
// Start cluster sits left of the textarea, end cluster right of it —
|
|
340
|
+
// `[📎][actionsStart] [textarea] [actionsEnd][🎙][▶]` (§3.6).
|
|
341
|
+
if (layout === 'inline') {
|
|
342
|
+
return (
|
|
343
|
+
<ComposerSizeProvider value={size}>
|
|
344
|
+
<div
|
|
345
|
+
ref={ref}
|
|
346
|
+
className={cn(
|
|
347
|
+
'border-t border-border bg-background/95',
|
|
348
|
+
sz.containerPadding,
|
|
349
|
+
ap.containerPadding,
|
|
350
|
+
className,
|
|
351
|
+
)}
|
|
352
|
+
>
|
|
353
|
+
{trayNode ? <div className="mb-1.5">{trayNode}</div> : null}
|
|
354
|
+
<div
|
|
355
|
+
className={cn(
|
|
356
|
+
'flex items-end gap-1.5 [&>:not(textarea)]:shrink-0',
|
|
357
|
+
sz.inlineSlot,
|
|
358
|
+
)}
|
|
359
|
+
>
|
|
360
|
+
{customAttach}
|
|
361
|
+
{ActionBarSlot ? (
|
|
362
|
+
actionBarNode
|
|
363
|
+
) : (
|
|
364
|
+
<ComposerActionBar
|
|
365
|
+
actionsStart={actionsStart}
|
|
366
|
+
actionsEnd={[]}
|
|
367
|
+
size={size}
|
|
368
|
+
layout={layout}
|
|
369
|
+
inlineStart={inlineStart}
|
|
370
|
+
cluster="start"
|
|
371
|
+
/>
|
|
372
|
+
)}
|
|
373
|
+
{textareaNode}
|
|
374
|
+
{ActionBarSlot ? null : (
|
|
375
|
+
<ComposerActionBar
|
|
376
|
+
actionsStart={[]}
|
|
377
|
+
actionsEnd={actionsEnd}
|
|
378
|
+
size={size}
|
|
379
|
+
layout={layout}
|
|
380
|
+
inlineEnd={inlineEnd}
|
|
381
|
+
cluster="end"
|
|
382
|
+
/>
|
|
383
|
+
)}
|
|
384
|
+
{customSend}
|
|
385
|
+
</div>
|
|
386
|
+
{footerNode}
|
|
387
|
+
</div>
|
|
388
|
+
</ComposerSizeProvider>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ── Stacked (default) layout — two-tier surface ────────────────────────
|
|
393
|
+
// The textarea always occupies the full top row; the action bar (attach /
|
|
394
|
+
// tools on the left, mic / send on the right) sits in a fixed row below.
|
|
395
|
+
// The draft never shares a baseline with the buttons — the text stays put
|
|
396
|
+
// whether it is one line or many.
|
|
397
|
+
const surfaceWrapper = (
|
|
398
|
+
<div
|
|
399
|
+
ref={surfaceRef}
|
|
400
|
+
onMouseDown={handleSurfaceMouseDown}
|
|
401
|
+
className={cn(
|
|
402
|
+
// Raised input panel — reads against the chat background via the
|
|
403
|
+
// neutral `muted` surface + a hairline ring (Gemini/ChatGPT
|
|
404
|
+
// style), not a heavy drawn border. Focus firms up the ring
|
|
405
|
+
// without a hard accent frame.
|
|
406
|
+
'flex flex-col rounded-2xl text-foreground',
|
|
407
|
+
'bg-muted ring-1 ring-border',
|
|
408
|
+
'focus-within:ring-ring/50',
|
|
409
|
+
'transition-colors',
|
|
410
|
+
// The whole surface reads as a text field — the padding around the
|
|
411
|
+
// editor shows a text caret and is click-to-focus.
|
|
412
|
+
'cursor-text',
|
|
413
|
+
sz.surface,
|
|
414
|
+
ap.surface,
|
|
415
|
+
)}
|
|
416
|
+
>
|
|
417
|
+
{/* Tier B: a fully-custom ActionBar renders below the textarea. */}
|
|
418
|
+
{ActionBarSlot ? (
|
|
419
|
+
<>
|
|
420
|
+
{textareaNode}
|
|
421
|
+
<div className="mt-1 flex items-center gap-1.5">
|
|
422
|
+
{customAttach}
|
|
423
|
+
{actionBarNode}
|
|
424
|
+
{customSend}
|
|
425
|
+
</div>
|
|
426
|
+
</>
|
|
427
|
+
) : (
|
|
428
|
+
<>
|
|
429
|
+
{/* primary — textarea, full width on top */}
|
|
430
|
+
<div className="flex">{textareaNode}</div>
|
|
431
|
+
|
|
432
|
+
{/* action bar — leading cluster left, trailing cluster right */}
|
|
433
|
+
<div className={cn('mt-1 flex items-center justify-between', GAP[size])}>
|
|
434
|
+
<div className={cn('flex items-center', GAP[size])}>
|
|
435
|
+
{customAttach}
|
|
436
|
+
{inlineStart}
|
|
437
|
+
{actionsStart.map((action) => (
|
|
438
|
+
<ComposerButton key={action.id} action={action} size={size} />
|
|
439
|
+
))}
|
|
440
|
+
</div>
|
|
441
|
+
<div className={cn('flex items-center', GAP[size])}>
|
|
442
|
+
{inlineEnd}
|
|
443
|
+
{actionsEnd.map((action) => (
|
|
444
|
+
<ComposerButton key={action.id} action={action} size={size} />
|
|
445
|
+
))}
|
|
446
|
+
{customSend}
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
</>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<ComposerSizeProvider value={size}>
|
|
456
|
+
<div
|
|
457
|
+
ref={ref}
|
|
458
|
+
className={cn(
|
|
459
|
+
'border-t border-border bg-background/95',
|
|
460
|
+
sz.containerPadding,
|
|
461
|
+
ap.containerPadding,
|
|
462
|
+
className,
|
|
463
|
+
)}
|
|
464
|
+
>
|
|
465
|
+
{trayNode ? <div className="mb-2">{trayNode}</div> : null}
|
|
466
|
+
{surfaceWrapper}
|
|
467
|
+
{footerNode}
|
|
468
|
+
</div>
|
|
469
|
+
</ComposerSizeProvider>
|
|
470
|
+
);
|
|
471
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
4
|
+
|
|
5
|
+
import { ComposerButton } from './ComposerButton';
|
|
6
|
+
import type { ActionBarProps } from './types';
|
|
7
|
+
|
|
8
|
+
const GAP: Record<ActionBarProps['size'], string> = {
|
|
9
|
+
sm: 'gap-0.5',
|
|
10
|
+
md: 'gap-1',
|
|
11
|
+
lg: 'gap-1.5',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Renders the `blockEnd` action bar — `actionsStart` left, `actionsEnd`
|
|
16
|
+
* right, `justify-between`. Wrapped in `role="toolbar"` only when it
|
|
17
|
+
* actually carries interactive controls.
|
|
18
|
+
*
|
|
19
|
+
* `cluster` renders just one side — used by the `inline` layout, which
|
|
20
|
+
* places the start cluster before the textarea and the end cluster after.
|
|
21
|
+
*/
|
|
22
|
+
export function ComposerActionBar({
|
|
23
|
+
actionsStart,
|
|
24
|
+
actionsEnd,
|
|
25
|
+
size,
|
|
26
|
+
inlineStart,
|
|
27
|
+
inlineEnd,
|
|
28
|
+
className,
|
|
29
|
+
cluster,
|
|
30
|
+
}: ActionBarProps & { className?: string; cluster?: 'start' | 'end' }) {
|
|
31
|
+
const hasInteractive = actionsStart.length > 0 || actionsEnd.length > 0;
|
|
32
|
+
const gap = GAP[size];
|
|
33
|
+
|
|
34
|
+
const startCluster = (
|
|
35
|
+
<div className={cn('flex items-center', gap)}>
|
|
36
|
+
{inlineStart}
|
|
37
|
+
{actionsStart.map((action) => (
|
|
38
|
+
<ComposerButton key={action.id} action={action} size={size} />
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
const endCluster = (
|
|
43
|
+
<div className={cn('flex items-center', gap)}>
|
|
44
|
+
{inlineEnd}
|
|
45
|
+
{actionsEnd.map((action) => (
|
|
46
|
+
<ComposerButton key={action.id} action={action} size={size} />
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Single-cluster mode — no toolbar wrapper, the caller owns layout.
|
|
52
|
+
if (cluster === 'start') return startCluster;
|
|
53
|
+
if (cluster === 'end') return endCluster;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
role={hasInteractive ? 'toolbar' : undefined}
|
|
58
|
+
aria-label={hasInteractive ? 'Composer actions' : undefined}
|
|
59
|
+
className={cn('flex w-full items-center justify-between', className)}
|
|
60
|
+
>
|
|
61
|
+
{startCluster}
|
|
62
|
+
{endCluster}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { X } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
7
|
+
|
|
8
|
+
/** One action button in the banner. */
|
|
9
|
+
export interface ComposerBannerAction {
|
|
10
|
+
/** Stable key. */
|
|
11
|
+
id: string;
|
|
12
|
+
label: ReactNode;
|
|
13
|
+
onClick: () => void;
|
|
14
|
+
/** `primary` — filled accent; `outline` — bordered neutral. Default `outline`. */
|
|
15
|
+
variant?: 'primary' | 'outline';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ComposerBannerProps {
|
|
19
|
+
/** Leading icon (Lucide element). */
|
|
20
|
+
icon?: ReactNode;
|
|
21
|
+
/** Bold headline. */
|
|
22
|
+
title: ReactNode;
|
|
23
|
+
/** Secondary line under the title. */
|
|
24
|
+
description?: ReactNode;
|
|
25
|
+
/** Up to two trailing action buttons. */
|
|
26
|
+
actions?: ComposerBannerAction[];
|
|
27
|
+
/** Accent tint: `info` neutral, `warning` destructive, `upgrade` primary. */
|
|
28
|
+
variant?: 'info' | 'warning' | 'upgrade';
|
|
29
|
+
/** Show a dismiss `×` in the top-right corner. */
|
|
30
|
+
onDismiss?: () => void;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Icon-tint per variant — keeps the surface neutral, accents the icon only. */
|
|
35
|
+
const ICON_TINT: Record<NonNullable<ComposerBannerProps['variant']>, string> = {
|
|
36
|
+
info: 'text-muted-foreground',
|
|
37
|
+
warning: 'text-destructive',
|
|
38
|
+
upgrade: 'text-primary',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const ACTION_VARIANT: Record<
|
|
42
|
+
NonNullable<ComposerBannerAction['variant']>,
|
|
43
|
+
string
|
|
44
|
+
> = {
|
|
45
|
+
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
46
|
+
outline:
|
|
47
|
+
'border border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A standalone notice bubble that sits above the composer — its own
|
|
52
|
+
* rounded surface, separated by a gap (drop it into
|
|
53
|
+
* `composerSlots.blockStart`). Mirrors ChatGPT's "free limit reached"
|
|
54
|
+
* banner: leading icon, title + description, trailing action buttons.
|
|
55
|
+
*/
|
|
56
|
+
export function ComposerBanner({
|
|
57
|
+
icon,
|
|
58
|
+
title,
|
|
59
|
+
description,
|
|
60
|
+
actions,
|
|
61
|
+
variant = 'info',
|
|
62
|
+
onDismiss,
|
|
63
|
+
className,
|
|
64
|
+
}: ComposerBannerProps) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
role="status"
|
|
68
|
+
className={cn(
|
|
69
|
+
'flex items-start gap-3 rounded-2xl border border-border bg-muted/60 px-3.5 py-3',
|
|
70
|
+
'text-sm text-foreground',
|
|
71
|
+
className,
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
{icon ? (
|
|
75
|
+
<span
|
|
76
|
+
className={cn('mt-0.5 flex shrink-0 [&_svg]:size-5', ICON_TINT[variant])}
|
|
77
|
+
aria-hidden
|
|
78
|
+
>
|
|
79
|
+
{icon}
|
|
80
|
+
</span>
|
|
81
|
+
) : null}
|
|
82
|
+
|
|
83
|
+
<div className="min-w-0 flex-1">
|
|
84
|
+
<p className="font-semibold leading-snug">{title}</p>
|
|
85
|
+
{description ? (
|
|
86
|
+
<p className="mt-0.5 text-xs text-muted-foreground">{description}</p>
|
|
87
|
+
) : null}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{actions && actions.length > 0 ? (
|
|
91
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
92
|
+
{actions.map((action) => (
|
|
93
|
+
<button
|
|
94
|
+
key={action.id}
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={action.onClick}
|
|
97
|
+
className={cn(
|
|
98
|
+
'inline-flex h-8 cursor-pointer items-center justify-center rounded-lg px-3',
|
|
99
|
+
'text-xs font-medium transition-colors',
|
|
100
|
+
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
|
101
|
+
ACTION_VARIANT[action.variant ?? 'outline'],
|
|
102
|
+
)}
|
|
103
|
+
>
|
|
104
|
+
{action.label}
|
|
105
|
+
</button>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
) : null}
|
|
109
|
+
|
|
110
|
+
{onDismiss ? (
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={onDismiss}
|
|
114
|
+
aria-label="Dismiss"
|
|
115
|
+
title="Dismiss"
|
|
116
|
+
className={cn(
|
|
117
|
+
'inline-flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-lg',
|
|
118
|
+
'text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground',
|
|
119
|
+
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
|
120
|
+
'[&_svg]:size-4',
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
<X aria-hidden />
|
|
124
|
+
</button>
|
|
125
|
+
) : null}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|