@djangocfg/ui-tools 2.1.404 → 2.1.408
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 -11
- 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 +13 -16
- 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/lazy.tsx +13 -27
- package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +2 -5
- package/src/tools/Chat/README.md +267 -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 +326 -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 +106 -26
- 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/README.md +87 -230
- package/src/tools/VideoPlayer/VideoPlayer.tsx +82 -0
- package/src/tools/VideoPlayer/canvas/canvas-dispatcher.tsx +34 -0
- package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +39 -0
- package/src/tools/VideoPlayer/canvas/iframe-canvas.tsx +33 -0
- package/src/tools/VideoPlayer/canvas/index.ts +12 -0
- package/src/tools/VideoPlayer/canvas/jsx-augmentation.ts +47 -0
- package/src/tools/VideoPlayer/canvas/native-canvas.tsx +38 -0
- package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +40 -0
- package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +78 -0
- package/src/tools/VideoPlayer/index.ts +51 -65
- package/src/tools/VideoPlayer/lazy.tsx +11 -54
- package/src/tools/VideoPlayer/parts/controls-bar.tsx +35 -0
- package/src/tools/VideoPlayer/parts/fullscreen.tsx +19 -0
- package/src/tools/VideoPlayer/parts/index.ts +15 -0
- package/src/tools/VideoPlayer/parts/pip.tsx +19 -0
- package/src/tools/VideoPlayer/parts/play-button.tsx +19 -0
- package/src/tools/VideoPlayer/parts/playback-rate.tsx +31 -0
- package/src/tools/VideoPlayer/parts/poster.tsx +3 -0
- package/src/tools/VideoPlayer/parts/seek-bar.tsx +26 -0
- package/src/tools/VideoPlayer/parts/volume.tsx +32 -0
- package/src/tools/VideoPlayer/styles/video-player.css +141 -0
- package/src/tools/VideoPlayer/types.ts +82 -0
- package/src/tools/VideoPlayer/utils/parse-embed-url.ts +70 -0
- package/src/tools/VideoPlayer/utils/vimeo-id.ts +24 -0
- package/src/tools/VideoPlayer/utils/youtube-id.ts +64 -0
- package/src/tools/index.ts +37 -29
- 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/VideoPlayer/components/VideoControls.tsx +0 -138
- package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +0 -172
- package/src/tools/VideoPlayer/components/VideoPlayer.tsx +0 -201
- package/src/tools/VideoPlayer/components/index.ts +0 -14
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +0 -52
- package/src/tools/VideoPlayer/context/index.ts +0 -8
- package/src/tools/VideoPlayer/hooks/index.ts +0 -12
- package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +0 -71
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +0 -117
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +0 -284
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +0 -505
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +0 -397
- package/src/tools/VideoPlayer/providers/index.ts +0 -8
- package/src/tools/VideoPlayer/types/index.ts +0 -38
- package/src/tools/VideoPlayer/types/player.ts +0 -116
- package/src/tools/VideoPlayer/types/provider.ts +0 -93
- package/src/tools/VideoPlayer/types/sources.ts +0 -97
- package/src/tools/VideoPlayer/utils/debug.ts +0 -14
- package/src/tools/VideoPlayer/utils/fileSource.ts +0 -78
- package/src/tools/VideoPlayer/utils/index.ts +0 -12
- package/src/tools/VideoPlayer/utils/resolvers.ts +0 -75
- /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
|
@@ -61,12 +61,28 @@ function mapStatusToCode(status: number): string {
|
|
|
61
61
|
return 'http_error';
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Derive an AbortSignal that fires after `timeoutMs` or when the caller's
|
|
66
|
+
* `signal` aborts. The returned `clear()` MUST be called once the request
|
|
67
|
+
* settles — otherwise the timer keeps the AbortController (and its abort
|
|
68
|
+
* listener on the parent signal) alive until the full timeout elapses,
|
|
69
|
+
* leaking one timer + one listener per request.
|
|
70
|
+
*/
|
|
71
|
+
function withTimeout(
|
|
72
|
+
signal: AbortSignal | undefined,
|
|
73
|
+
timeoutMs: number,
|
|
74
|
+
): { signal: AbortSignal; clear: () => void } {
|
|
65
75
|
const ctrl = new AbortController();
|
|
66
76
|
const onAbort = () => ctrl.abort();
|
|
67
77
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
68
|
-
setTimeout(() => ctrl.abort(), timeoutMs);
|
|
69
|
-
return
|
|
78
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
79
|
+
return {
|
|
80
|
+
signal: ctrl.signal,
|
|
81
|
+
clear: () => {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
signal?.removeEventListener('abort', onAbort);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
export function createHttpTransport(config: HttpTransportConfig): ChatTransport {
|
|
@@ -85,13 +101,18 @@ export function createHttpTransport(config: HttpTransportConfig): ChatTransport
|
|
|
85
101
|
|
|
86
102
|
return {
|
|
87
103
|
async createSession(opts?: CreateSessionOptions): Promise<SessionInfo> {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
const t = withTimeout(undefined, timeout);
|
|
105
|
+
try {
|
|
106
|
+
const res = await fetchImpl(`${base}/sessions`, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: await buildHeaders(),
|
|
109
|
+
body: JSON.stringify({ slug: config.slug, metadata: opts?.metadata ?? {} }),
|
|
110
|
+
signal: t.signal,
|
|
111
|
+
});
|
|
112
|
+
return await jsonOrThrow<SessionInfo>(res, 'createSession');
|
|
113
|
+
} finally {
|
|
114
|
+
t.clear();
|
|
115
|
+
}
|
|
95
116
|
},
|
|
96
117
|
|
|
97
118
|
async loadHistory(sessionId, cursor, limit): Promise<HistoryPage> {
|
|
@@ -101,12 +122,17 @@ export function createHttpTransport(config: HttpTransportConfig): ChatTransport
|
|
|
101
122
|
const url = `${base}/sessions/${encodeURIComponent(sessionId)}/history${
|
|
102
123
|
params.toString() ? `?${params.toString()}` : ''
|
|
103
124
|
}`;
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
125
|
+
const t = withTimeout(undefined, timeout);
|
|
126
|
+
try {
|
|
127
|
+
const res = await fetchImpl(url, {
|
|
128
|
+
method: 'GET',
|
|
129
|
+
headers: await buildHeaders(),
|
|
130
|
+
signal: t.signal,
|
|
131
|
+
});
|
|
132
|
+
return await jsonOrThrow<HistoryPage>(res, 'loadHistory');
|
|
133
|
+
} finally {
|
|
134
|
+
t.clear();
|
|
135
|
+
}
|
|
110
136
|
},
|
|
111
137
|
|
|
112
138
|
async *stream(
|
|
@@ -139,26 +165,39 @@ export function createHttpTransport(config: HttpTransportConfig): ChatTransport
|
|
|
139
165
|
|
|
140
166
|
async send(sessionId, content, options?: SendOptions): Promise<ChatMessage> {
|
|
141
167
|
const url = `${base}/sessions/${encodeURIComponent(sessionId)}/messages/buffered`;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
168
|
+
// Honour a caller-supplied signal as-is; otherwise apply our own
|
|
169
|
+
// timeout (and clean it up in `finally`).
|
|
170
|
+
const t = options?.signal ? null : withTimeout(undefined, timeout);
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetchImpl(url, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: await buildHeaders(),
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
content,
|
|
177
|
+
attachments: options?.attachments ?? [],
|
|
178
|
+
metadata: options?.metadata ?? {},
|
|
179
|
+
}),
|
|
180
|
+
signal: options?.signal ?? t!.signal,
|
|
181
|
+
});
|
|
182
|
+
return await jsonOrThrow<ChatMessage>(res, 'send');
|
|
183
|
+
} finally {
|
|
184
|
+
t?.clear();
|
|
185
|
+
}
|
|
153
186
|
},
|
|
154
187
|
|
|
155
188
|
async closeSession(sessionId: string): Promise<void> {
|
|
156
189
|
const url = `${base}/sessions/${encodeURIComponent(sessionId)}`;
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
190
|
+
const t = withTimeout(undefined, timeout);
|
|
191
|
+
let res: Response;
|
|
192
|
+
try {
|
|
193
|
+
res = await fetchImpl(url, {
|
|
194
|
+
method: 'DELETE',
|
|
195
|
+
headers: await buildHeaders(),
|
|
196
|
+
signal: t.signal,
|
|
197
|
+
});
|
|
198
|
+
} finally {
|
|
199
|
+
t.clear();
|
|
200
|
+
}
|
|
162
201
|
if (!res.ok && res.status !== 404) {
|
|
163
202
|
throw new TransportError(`closeSession failed (${res.status})`, mapStatusToCode(res.status));
|
|
164
203
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ChatStreamEvent } from '../../types';
|
|
10
|
-
import { LIMITS } from '../../
|
|
10
|
+
import { LIMITS } from '../../constants';
|
|
11
11
|
|
|
12
12
|
interface RawEvent {
|
|
13
13
|
event?: string;
|
|
@@ -49,12 +49,23 @@ export async function* parseSSE(
|
|
|
49
49
|
const reader = response.body.getReader();
|
|
50
50
|
const decoder = new TextDecoder();
|
|
51
51
|
let buffer = '';
|
|
52
|
-
let lastChunkAt = Date.now();
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
// Race `reader.read()` against an idle timer so a fully stalled
|
|
54
|
+
// connection — one that accepts the request but never sends a byte —
|
|
55
|
+
// is surfaced as an error instead of hanging the generator forever.
|
|
56
|
+
// The previous check only ran *after* a successful read, so it could
|
|
57
|
+
// never fire while `reader.read()` itself was blocked.
|
|
58
|
+
const readWithIdleTimeout = (): Promise<ReadableStreamReadResult<Uint8Array>> => {
|
|
59
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
60
|
+
const idle = new Promise<never>((_, reject) => {
|
|
61
|
+
timer = setTimeout(
|
|
62
|
+
() => reject(new Error(`SSE idle timeout (${idleMs}ms)`)),
|
|
63
|
+
idleMs,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
return Promise.race([reader.read(), idle]).finally(() => {
|
|
67
|
+
if (timer) clearTimeout(timer);
|
|
68
|
+
}) as Promise<ReadableStreamReadResult<Uint8Array>>;
|
|
58
69
|
};
|
|
59
70
|
|
|
60
71
|
try {
|
|
@@ -62,10 +73,9 @@ export async function* parseSSE(
|
|
|
62
73
|
if (options.signal?.aborted) {
|
|
63
74
|
return;
|
|
64
75
|
}
|
|
65
|
-
const { value, done } = await
|
|
76
|
+
const { value, done } = await readWithIdleTimeout();
|
|
66
77
|
if (done) break;
|
|
67
78
|
|
|
68
|
-
lastChunkAt = Date.now();
|
|
69
79
|
buffer += decoder.decode(value, { stream: true });
|
|
70
80
|
|
|
71
81
|
// Split on blank line which delimits SSE events.
|
|
@@ -86,8 +96,6 @@ export async function* parseSSE(
|
|
|
86
96
|
|
|
87
97
|
separator = buffer.indexOf('\n\n');
|
|
88
98
|
}
|
|
89
|
-
|
|
90
|
-
idleCheck();
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
// Flush any trailing partial event.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
|
|
6
|
+
import { SpotlightCanvas } from './SpotlightCanvas';
|
|
7
|
+
import type { PointDirective, SpotlightRect } from './types';
|
|
8
|
+
import { useHighlightTargets } from './useHighlightTargets';
|
|
9
|
+
import { type RefResolver } from './resolveRef';
|
|
10
|
+
|
|
11
|
+
/** Padding (px) drawn around each highlighted element. */
|
|
12
|
+
const HIGHLIGHT_PADDING = 6;
|
|
13
|
+
/** Corner radius (px) of the spotlight cut-out. */
|
|
14
|
+
const HIGHLIGHT_RADIUS = 6;
|
|
15
|
+
|
|
16
|
+
/** Props for `HighlightOverlay`. */
|
|
17
|
+
export interface HighlightOverlayProps {
|
|
18
|
+
/** `point` directives from the AI's reply. Empty hides the overlay. */
|
|
19
|
+
directives: PointDirective[];
|
|
20
|
+
/** Resolves a CST ref to a live element (the snapshot's registry). */
|
|
21
|
+
resolver: RefResolver | null;
|
|
22
|
+
/** Auto-dismiss after N ms; 0 keeps it until directives clear. */
|
|
23
|
+
ttlMs?: number;
|
|
24
|
+
/** Called when the overlay dismisses (scrim click, Esc, or TTL). */
|
|
25
|
+
onDismiss?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Draws an AI-driven spotlight over elements the assistant pointed at.
|
|
30
|
+
*
|
|
31
|
+
* Read-only: it highlights and optionally focuses elements — it never
|
|
32
|
+
* changes the user's data. The page stays interactive underneath; the
|
|
33
|
+
* scrim is purely visual and dismissable.
|
|
34
|
+
*/
|
|
35
|
+
export function HighlightOverlay({
|
|
36
|
+
directives,
|
|
37
|
+
resolver,
|
|
38
|
+
ttlMs = 6000,
|
|
39
|
+
onDismiss,
|
|
40
|
+
}: HighlightOverlayProps) {
|
|
41
|
+
const targets = useHighlightTargets(directives, resolver);
|
|
42
|
+
|
|
43
|
+
// Auto-dismiss after the TTL once something is showing.
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (targets.length === 0 || ttlMs <= 0) return;
|
|
46
|
+
const timer = window.setTimeout(() => onDismiss?.(), ttlMs);
|
|
47
|
+
return () => window.clearTimeout(timer);
|
|
48
|
+
}, [targets.length, ttlMs, onDismiss]);
|
|
49
|
+
|
|
50
|
+
// Dismiss on Escape.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (targets.length === 0) return;
|
|
53
|
+
const onKey = (e: KeyboardEvent) => {
|
|
54
|
+
if (e.key === 'Escape') onDismiss?.();
|
|
55
|
+
};
|
|
56
|
+
window.addEventListener('keydown', onKey);
|
|
57
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
58
|
+
}, [targets.length, onDismiss]);
|
|
59
|
+
|
|
60
|
+
if (targets.length === 0 || typeof document === 'undefined') return null;
|
|
61
|
+
|
|
62
|
+
const rects: SpotlightRect[] = targets.map((t) => ({
|
|
63
|
+
rect: t.rect,
|
|
64
|
+
padding: HIGHLIGHT_PADDING,
|
|
65
|
+
radius: HIGHLIGHT_RADIUS,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
return createPortal(
|
|
69
|
+
<div
|
|
70
|
+
className="pointer-events-none fixed inset-0 z-[60]"
|
|
71
|
+
data-chat-highlight-overlay=""
|
|
72
|
+
>
|
|
73
|
+
{/* The scrim is click-dismissable; it does not block the page. */}
|
|
74
|
+
<div className="pointer-events-auto absolute inset-0">
|
|
75
|
+
<SpotlightCanvas
|
|
76
|
+
rects={rects}
|
|
77
|
+
pulseRing
|
|
78
|
+
onClick={() => onDismiss?.()}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Captions beside each highlighted element. */}
|
|
83
|
+
{targets.map((t, i) =>
|
|
84
|
+
t.label ? (
|
|
85
|
+
<div
|
|
86
|
+
key={i}
|
|
87
|
+
className="bg-primary text-primary-foreground absolute rounded-md px-2 py-1 text-xs shadow-md"
|
|
88
|
+
style={{
|
|
89
|
+
left: t.rect.x,
|
|
90
|
+
top: t.rect.y + t.rect.height + HIGHLIGHT_PADDING + 6,
|
|
91
|
+
maxWidth: 240,
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
{t.label}
|
|
95
|
+
</div>
|
|
96
|
+
) : null,
|
|
97
|
+
)}
|
|
98
|
+
</div>,
|
|
99
|
+
document.body,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Chat highlight — AI-driven `point` directives
|
|
2
|
+
|
|
3
|
+
When the assistant answers a question about the page, it can also
|
|
4
|
+
**point at the screen** — highlight and optionally focus the relevant
|
|
5
|
+
elements. This module renders that.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The idea
|
|
10
|
+
|
|
11
|
+
The chat sends the backend a snapshot of the user's page (the
|
|
12
|
+
`page-snapshot` engine). Every interactive element in that snapshot got
|
|
13
|
+
a short ref id — `@e4`. The assistant, knowing those refs, can reply:
|
|
14
|
+
|
|
15
|
+
> "Click **Save** to apply your changes."
|
|
16
|
+
|
|
17
|
+
…and attach a directive: `point at @e4`. This module resolves `@e4`
|
|
18
|
+
back to the live DOM element and draws a spotlight on it.
|
|
19
|
+
|
|
20
|
+
This is the intelligent successor to the old scripted product Tour —
|
|
21
|
+
the spotlight is the same, but *what* it points at is decided by the AI
|
|
22
|
+
in context, not a hard-coded step list. The SVG renderer here is in
|
|
23
|
+
fact adapted from that deleted Tour component.
|
|
24
|
+
|
|
25
|
+
**Read-only.** A `point` directive highlights / focuses an element. It
|
|
26
|
+
never types, clicks, or changes the user's data.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## The round trip
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
capture: page-snapshot engine assigns @e1, @e2, … to interactive nodes
|
|
34
|
+
and keeps a ref → element registry
|
|
35
|
+
│
|
|
36
|
+
▼ snapshot sent with the chat message (request body metadata)
|
|
37
|
+
backend: the LLM, given the snapshot, may append a directive marker:
|
|
38
|
+
<<directives>[{"type":"point","ref":"@e4","focus":true}]</directives>>
|
|
39
|
+
the marker is parsed + validated, stripped from the reply,
|
|
40
|
+
and emitted as a `directive` SSE event
|
|
41
|
+
│
|
|
42
|
+
▼ directive SSE event arrives on the client
|
|
43
|
+
this module: resolve @e4 → element via the snapshot registry,
|
|
44
|
+
draw the spotlight, optionally focus
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Files
|
|
50
|
+
|
|
51
|
+
| File | Responsibility |
|
|
52
|
+
|------|----------------|
|
|
53
|
+
| `types.ts` | `PointDirective`, `HighlightTarget`, `SpotlightRect`, `CSTRefId`. |
|
|
54
|
+
| `resolveRef.ts` | `resolveRefs()` — CST ref → live DOM element via a `RefResolver`. Drops stale / detached refs. |
|
|
55
|
+
| `useHighlightTargets.ts` | Hook: directives → geometry-tracked targets. Re-measures on scroll/resize, scrolls the first off-screen target in, focuses on request, drops a target when its element leaves the DOM. |
|
|
56
|
+
| `SpotlightCanvas.tsx` | Pure SVG-mask renderer — dimmed scrim with rounded cut-outs, border, pulse ring. Takes geometry, not elements. |
|
|
57
|
+
| `HighlightOverlay.tsx` | The component: hook + canvas + captions, portalled to `body`, auto-dismiss (TTL / Esc / scrim click). |
|
|
58
|
+
| `index.ts` | Public barrel. |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { HighlightOverlay } from '@djangocfg/ui-tools/.../Chat/highlight';
|
|
66
|
+
|
|
67
|
+
<HighlightOverlay
|
|
68
|
+
directives={directivesFromLastDirectiveEvent}
|
|
69
|
+
resolver={snapshotRefRegistry} // the page-snapshot ref registry
|
|
70
|
+
ttlMs={6000}
|
|
71
|
+
onDismiss={() => clearDirectives()}
|
|
72
|
+
/>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `directives` — the `directives` array from the latest `directive` SSE
|
|
76
|
+
event. Empty → the overlay renders nothing.
|
|
77
|
+
- `resolver` — anything with `resolve(ref) => HTMLElement | null`. The
|
|
78
|
+
`page-snapshot` engine's `RefRegistry` satisfies this. Structurally
|
|
79
|
+
typed on purpose, so this module does not import the capture engine.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Staleness
|
|
84
|
+
|
|
85
|
+
A ref resolves against the snapshot that produced it. If the user has
|
|
86
|
+
since navigated or the element was removed, `resolve()` returns null (or
|
|
87
|
+
the element is detached) — that target is silently dropped. The overlay
|
|
88
|
+
only ever shows live elements.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Decoupling notes
|
|
93
|
+
|
|
94
|
+
- This module lives under `Chat/` because `point` is a chat feature and
|
|
95
|
+
it has exactly one consumer. If a second consumer ever appears,
|
|
96
|
+
promote it to a shared location — not before.
|
|
97
|
+
- It does not import the `page-snapshot` capture engine. It depends only
|
|
98
|
+
on the structural `RefResolver` interface, so the two stay
|
|
99
|
+
independent.
|
|
100
|
+
- The directive type is `point` only. The shape is a discriminated
|
|
101
|
+
union on `type`, so a future write-style directive could be added —
|
|
102
|
+
but that is a different risk class (it would change user data) and is
|
|
103
|
+
deliberately out of scope here.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
6
|
+
|
|
7
|
+
import type { SpotlightRect } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* SVG-mask spotlight renderer — adapted from the former Tour component.
|
|
11
|
+
*
|
|
12
|
+
* Draws a dimmed full-screen overlay with rounded cut-outs over the
|
|
13
|
+
* target rects, plus a brand-coloured border and an attention pulse.
|
|
14
|
+
* Pure presentation: it takes geometry, not DOM elements.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Injected once — keyframes + transition classes for the SVG rects.
|
|
18
|
+
const SPOTLIGHT_STYLES = `
|
|
19
|
+
@keyframes chat-spotlight-pulse {
|
|
20
|
+
0%, 100% { opacity: 1; }
|
|
21
|
+
50% { opacity: 0.5; }
|
|
22
|
+
}
|
|
23
|
+
@keyframes chat-spotlight-ring {
|
|
24
|
+
0% { transform: scale(1); opacity: 0.8; }
|
|
25
|
+
100% { transform: scale(1.15); opacity: 0; }
|
|
26
|
+
}
|
|
27
|
+
.chat-spotlight-rect {
|
|
28
|
+
transition: x 300ms ease-out, y 300ms ease-out,
|
|
29
|
+
width 300ms ease-out, height 300ms ease-out;
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
/** Props for the spotlight canvas. */
|
|
34
|
+
export interface SpotlightCanvasProps {
|
|
35
|
+
/** Geometry of every element to spotlight. */
|
|
36
|
+
rects: SpotlightRect[];
|
|
37
|
+
/** Dim-overlay opacity, 0–1 (default: 0.5). */
|
|
38
|
+
opacity?: number;
|
|
39
|
+
/** Show an expanding attention ring (default: true). */
|
|
40
|
+
pulseRing?: boolean;
|
|
41
|
+
/** Dismiss the overlay when the scrim is clicked. */
|
|
42
|
+
onClick?: () => void;
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function SpotlightCanvas({
|
|
47
|
+
rects,
|
|
48
|
+
opacity = 0.5,
|
|
49
|
+
pulseRing = true,
|
|
50
|
+
onClick,
|
|
51
|
+
className,
|
|
52
|
+
}: SpotlightCanvasProps) {
|
|
53
|
+
// Expand each rect by its padding and pre-compute the rounded box.
|
|
54
|
+
// Hooks run unconditionally — no early return before this.
|
|
55
|
+
const boxes = useMemo(
|
|
56
|
+
() =>
|
|
57
|
+
rects.map((target, index) => {
|
|
58
|
+
const x = target.rect.x - target.padding;
|
|
59
|
+
const y = target.rect.y - target.padding;
|
|
60
|
+
const width = target.rect.width + target.padding * 2;
|
|
61
|
+
const height = target.rect.height + target.padding * 2;
|
|
62
|
+
const rx = target.radius + 2;
|
|
63
|
+
const centerX = target.rect.x + target.rect.width / 2;
|
|
64
|
+
const centerY = target.rect.y + target.rect.height / 2;
|
|
65
|
+
return {
|
|
66
|
+
key: index,
|
|
67
|
+
x,
|
|
68
|
+
y,
|
|
69
|
+
width,
|
|
70
|
+
height,
|
|
71
|
+
rx,
|
|
72
|
+
transformOrigin: `${centerX}px ${centerY}px`,
|
|
73
|
+
};
|
|
74
|
+
}),
|
|
75
|
+
[rects],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (rects.length === 0) return null;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<style dangerouslySetInnerHTML={{ __html: SPOTLIGHT_STYLES }} />
|
|
83
|
+
<svg
|
|
84
|
+
className={cn('fixed inset-0 h-full w-full', className)}
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
onClick={onClick}
|
|
87
|
+
>
|
|
88
|
+
<defs>
|
|
89
|
+
<mask id="chat-spotlight-mask">
|
|
90
|
+
{/* White = visible dim; black cut-outs = transparent targets. */}
|
|
91
|
+
<rect fill="white" width="100%" height="100%" />
|
|
92
|
+
{boxes.map((b) => (
|
|
93
|
+
<rect
|
|
94
|
+
key={b.key}
|
|
95
|
+
fill="black"
|
|
96
|
+
className="chat-spotlight-rect"
|
|
97
|
+
x={b.x}
|
|
98
|
+
y={b.y}
|
|
99
|
+
width={b.width}
|
|
100
|
+
height={b.height}
|
|
101
|
+
rx={b.rx}
|
|
102
|
+
ry={b.rx}
|
|
103
|
+
/>
|
|
104
|
+
))}
|
|
105
|
+
</mask>
|
|
106
|
+
</defs>
|
|
107
|
+
|
|
108
|
+
{/* Dimmed scrim with the target cut-outs. */}
|
|
109
|
+
<rect
|
|
110
|
+
fill="black"
|
|
111
|
+
fillOpacity={opacity}
|
|
112
|
+
width="100%"
|
|
113
|
+
height="100%"
|
|
114
|
+
mask="url(#chat-spotlight-mask)"
|
|
115
|
+
className="transition-opacity duration-300"
|
|
116
|
+
/>
|
|
117
|
+
|
|
118
|
+
{/* Attention pulse. */}
|
|
119
|
+
{pulseRing &&
|
|
120
|
+
boxes.map((b) => (
|
|
121
|
+
<rect
|
|
122
|
+
key={`pulse-${b.key}`}
|
|
123
|
+
className="fill-none stroke-primary stroke-2"
|
|
124
|
+
style={{
|
|
125
|
+
transformOrigin: b.transformOrigin,
|
|
126
|
+
animation: 'chat-spotlight-ring 1.5s ease-out infinite',
|
|
127
|
+
}}
|
|
128
|
+
x={b.x}
|
|
129
|
+
y={b.y}
|
|
130
|
+
width={b.width}
|
|
131
|
+
height={b.height}
|
|
132
|
+
rx={b.rx}
|
|
133
|
+
ry={b.rx}
|
|
134
|
+
/>
|
|
135
|
+
))}
|
|
136
|
+
|
|
137
|
+
{/* Solid border around each target. */}
|
|
138
|
+
{boxes.map((b) => (
|
|
139
|
+
<rect
|
|
140
|
+
key={`border-${b.key}`}
|
|
141
|
+
className="chat-spotlight-rect fill-none stroke-primary stroke-2"
|
|
142
|
+
x={b.x}
|
|
143
|
+
y={b.y}
|
|
144
|
+
width={b.width}
|
|
145
|
+
height={b.height}
|
|
146
|
+
rx={b.rx}
|
|
147
|
+
ry={b.rx}
|
|
148
|
+
/>
|
|
149
|
+
))}
|
|
150
|
+
</svg>
|
|
151
|
+
</>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, createElement } from 'react';
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { HighlightOverlay } from '../HighlightOverlay';
|
|
7
|
+
import type { RefResolver } from '../resolveRef';
|
|
8
|
+
import type { CSTRefId, PointDirective } from '../types';
|
|
9
|
+
|
|
10
|
+
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
|
|
12
|
+
// jsdom has no ResizeObserver — the overlay observes its targets, so
|
|
13
|
+
// provide a no-op stub for the test environment.
|
|
14
|
+
if (typeof globalThis.ResizeObserver === 'undefined') {
|
|
15
|
+
(globalThis as Record<string, unknown>).ResizeObserver = class {
|
|
16
|
+
observe() {}
|
|
17
|
+
unobserve() {}
|
|
18
|
+
disconnect() {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let container: HTMLDivElement;
|
|
23
|
+
let root: Root;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
container = document.createElement('div');
|
|
27
|
+
document.body.appendChild(container);
|
|
28
|
+
root = createRoot(container);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
act(() => root.unmount());
|
|
33
|
+
container.remove();
|
|
34
|
+
document.body.innerHTML = '';
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** Resolver over a fixed map. */
|
|
38
|
+
function makeResolver(map: Record<string, HTMLElement | null>): RefResolver {
|
|
39
|
+
return { resolve: (ref: CSTRefId) => map[ref] ?? null };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function render(
|
|
43
|
+
directives: PointDirective[],
|
|
44
|
+
resolver: RefResolver | null,
|
|
45
|
+
) {
|
|
46
|
+
await act(async () => {
|
|
47
|
+
root.render(
|
|
48
|
+
createElement(HighlightOverlay, { directives, resolver, ttlMs: 0 }),
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('HighlightOverlay', () => {
|
|
54
|
+
it('renders nothing with no directives', async () => {
|
|
55
|
+
await render([], null);
|
|
56
|
+
expect(document.querySelector('[data-chat-highlight-overlay]')).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('renders nothing when refs do not resolve', async () => {
|
|
60
|
+
await render([{ type: 'point', ref: '@e9' }], makeResolver({}));
|
|
61
|
+
expect(document.querySelector('[data-chat-highlight-overlay]')).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('draws the overlay for a resolved ref', async () => {
|
|
65
|
+
const el = document.createElement('button');
|
|
66
|
+
el.textContent = 'Save';
|
|
67
|
+
document.body.appendChild(el);
|
|
68
|
+
|
|
69
|
+
await render(
|
|
70
|
+
[{ type: 'point', ref: '@e1' }],
|
|
71
|
+
makeResolver({ '@e1': el }),
|
|
72
|
+
);
|
|
73
|
+
expect(
|
|
74
|
+
document.querySelector('[data-chat-highlight-overlay]'),
|
|
75
|
+
).not.toBeNull();
|
|
76
|
+
// The SVG spotlight canvas is mounted.
|
|
77
|
+
expect(document.querySelector('svg')).not.toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('renders a caption when the directive has a label', async () => {
|
|
81
|
+
const el = document.createElement('input');
|
|
82
|
+
document.body.appendChild(el);
|
|
83
|
+
|
|
84
|
+
await render(
|
|
85
|
+
[{ type: 'point', ref: '@e1', label: 'Type your key here' }],
|
|
86
|
+
makeResolver({ '@e1': el }),
|
|
87
|
+
);
|
|
88
|
+
expect(document.body.textContent).toContain('Type your key here');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('focuses the element when the directive asks for focus', async () => {
|
|
92
|
+
const el = document.createElement('input');
|
|
93
|
+
document.body.appendChild(el);
|
|
94
|
+
|
|
95
|
+
await render(
|
|
96
|
+
[{ type: 'point', ref: '@e1', focus: true }],
|
|
97
|
+
makeResolver({ '@e1': el }),
|
|
98
|
+
);
|
|
99
|
+
expect(document.activeElement).toBe(el);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('does not focus when focus is not requested', async () => {
|
|
103
|
+
const el = document.createElement('input');
|
|
104
|
+
document.body.appendChild(el);
|
|
105
|
+
|
|
106
|
+
await render(
|
|
107
|
+
[{ type: 'point', ref: '@e1' }],
|
|
108
|
+
makeResolver({ '@e1': el }),
|
|
109
|
+
);
|
|
110
|
+
expect(document.activeElement).not.toBe(el);
|
|
111
|
+
});
|
|
112
|
+
});
|