@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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction entry point — scrub secrets / PII from a captured value.
|
|
3
|
+
*
|
|
4
|
+
* Two layers, applied at traverse time:
|
|
5
|
+
* 1. heuristic — a whole field redacted by element shape
|
|
6
|
+
* (password input, `*token*`-named field).
|
|
7
|
+
* 2. pattern — sensitive substrings scrubbed out of otherwise-kept
|
|
8
|
+
* text (JWT, API keys, emails, Luhn-validated cards, …).
|
|
9
|
+
*
|
|
10
|
+
* Safe-by-default: an un-annotated page is still covered. `data-ai-redact`
|
|
11
|
+
* (drop subtree) is handled in the walk; `data-ai-include` overrides the
|
|
12
|
+
* heuristic layer for a known-safe value.
|
|
13
|
+
*
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { RedactContext, RedactValue } from '../capture/walk';
|
|
17
|
+
import type { RedactionAuditor, RedactionReason } from './audit';
|
|
18
|
+
import {
|
|
19
|
+
hasIncludeAnnotation,
|
|
20
|
+
heuristicRedactKind,
|
|
21
|
+
} from './heuristics';
|
|
22
|
+
import { redactPatterns } from './patterns';
|
|
23
|
+
|
|
24
|
+
export type { RedactionAuditReport, RedactionReason } from './audit';
|
|
25
|
+
export { RedactionAuditor } from './audit';
|
|
26
|
+
|
|
27
|
+
/** Token substituted for a fully-redacted value. */
|
|
28
|
+
export function redactionToken(kind: string): string {
|
|
29
|
+
return `‹redacted:${kind}›`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build a redaction function bound to an auditor.
|
|
34
|
+
*
|
|
35
|
+
* The returned `RedactValue` is the hook the DOM walk calls for every
|
|
36
|
+
* captured input value and text run.
|
|
37
|
+
*/
|
|
38
|
+
export function createRedactor(auditor: RedactionAuditor): RedactValue {
|
|
39
|
+
return (value: string, context: RedactContext): string => {
|
|
40
|
+
auditor.markScanned();
|
|
41
|
+
if (!value) return value;
|
|
42
|
+
|
|
43
|
+
const { element, kind, ref } = context;
|
|
44
|
+
|
|
45
|
+
// Layer 1 — heuristic field-level redaction (input values only;
|
|
46
|
+
// static text has no single owning field shape).
|
|
47
|
+
if (kind === 'value') {
|
|
48
|
+
const heuristic = heuristicRedactKind(element);
|
|
49
|
+
if (heuristic && !hasIncludeAnnotation(element)) {
|
|
50
|
+
auditor.log({
|
|
51
|
+
elementRole: element.tagName.toLowerCase(),
|
|
52
|
+
triggerReason: 'heuristic',
|
|
53
|
+
assignedRef: ref,
|
|
54
|
+
});
|
|
55
|
+
return redactionToken(heuristic);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Layer 2 — pattern substring redaction.
|
|
60
|
+
const { value: scrubbed, matched } = redactPatterns(value);
|
|
61
|
+
if (matched.length > 0) {
|
|
62
|
+
const reason: RedactionReason = 'regex';
|
|
63
|
+
for (const _name of matched) {
|
|
64
|
+
auditor.log({
|
|
65
|
+
elementRole: element.tagName.toLowerCase(),
|
|
66
|
+
triggerReason: reason,
|
|
67
|
+
assignedRef: ref,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return scrubbed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return value;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Luhn checksum — validates credit-card-shaped digit runs so order
|
|
3
|
+
* numbers and IDs are not falsely redacted as cards.
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Validate a digit string with the Luhn algorithm. */
|
|
8
|
+
export function validateLuhn(digits: string): boolean {
|
|
9
|
+
const normalized = digits.replace(/\D/g, '');
|
|
10
|
+
if (normalized.length < 13 || normalized.length > 19) return false;
|
|
11
|
+
|
|
12
|
+
let sum = 0;
|
|
13
|
+
let double = false;
|
|
14
|
+
for (let i = normalized.length - 1; i >= 0; i--) {
|
|
15
|
+
let digit = normalized.charCodeAt(i) - 48; // '0' === 48
|
|
16
|
+
if (digit < 0 || digit > 9) return false;
|
|
17
|
+
if (double) {
|
|
18
|
+
digit *= 2;
|
|
19
|
+
if (digit > 9) digit -= 9;
|
|
20
|
+
}
|
|
21
|
+
sum += digit;
|
|
22
|
+
double = !double;
|
|
23
|
+
}
|
|
24
|
+
return sum % 10 === 0;
|
|
25
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction pattern catalog — secret / PII value patterns.
|
|
3
|
+
*
|
|
4
|
+
* Applied to string values (input values, static text) — these scrub
|
|
5
|
+
* sensitive substrings out of otherwise-keepable text. A separate
|
|
6
|
+
* heuristic layer (heuristics.ts) drops whole fields by element shape.
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { validateLuhn } from './luhn';
|
|
11
|
+
|
|
12
|
+
/** A named value-redaction rule. */
|
|
13
|
+
export interface RedactionPattern {
|
|
14
|
+
/** Identifier, surfaced in the audit report. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Matcher applied to string values (must be global for replaceAll). */
|
|
17
|
+
test: RegExp;
|
|
18
|
+
/**
|
|
19
|
+
* Optional extra check on a match before redacting — used to avoid
|
|
20
|
+
* false positives (e.g. Luhn for cards). Return true to redact.
|
|
21
|
+
*/
|
|
22
|
+
confirm?: (match: string) => boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Placeholder a matched secret/PII substring is replaced with. */
|
|
26
|
+
export function patternToken(name: string): string {
|
|
27
|
+
return `‹redacted:${name}›`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The catalog. Order matters — more specific patterns first so a token
|
|
32
|
+
* is not partially eaten by a broader rule.
|
|
33
|
+
*/
|
|
34
|
+
export const REDACTION_PATTERNS: RedactionPattern[] = [
|
|
35
|
+
{
|
|
36
|
+
// JWT — three base64url segments.
|
|
37
|
+
name: 'jwt',
|
|
38
|
+
test: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
// Bearer / authorization header value.
|
|
42
|
+
name: 'bearer-token',
|
|
43
|
+
test: /\bBearer\s+[A-Za-z0-9\-._~+/]{16,}=*/gi,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
// Common API-key prefixes (Stripe, OpenAI, GitHub, generic sk-/pk-).
|
|
47
|
+
name: 'api-key',
|
|
48
|
+
test: /\b(?:sk|pk|rk|ghp|gho|github_pat|xox[baprs])[-_][A-Za-z0-9-_]{16,}\b/g,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
// AWS access key id.
|
|
52
|
+
name: 'aws-key',
|
|
53
|
+
test: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
// Email address (RFC-5322-ish, pragmatic).
|
|
57
|
+
name: 'email',
|
|
58
|
+
test: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
// IBAN — country code + check digits + up to 30 alphanumerics.
|
|
62
|
+
name: 'iban',
|
|
63
|
+
test: /\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b/g,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
// Credit-card-shaped digit runs — Luhn-confirmed to avoid eating
|
|
67
|
+
// order numbers and other long IDs.
|
|
68
|
+
name: 'credit-card',
|
|
69
|
+
test: /\b(?:\d[ -]?){13,19}\b/g,
|
|
70
|
+
confirm: validateLuhn,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
// Phone number — must look like a phone, not just a long digit run.
|
|
74
|
+
// Requires either a leading "+" or separators (space/()/-), and a
|
|
75
|
+
// digit count in the 7–15 range. A bare 16-digit string is an ID,
|
|
76
|
+
// not a phone, and must survive.
|
|
77
|
+
name: 'phone',
|
|
78
|
+
test: /\+?\d[\d\s().-]{5,}\d/g,
|
|
79
|
+
confirm: (m) => {
|
|
80
|
+
const digits = m.replace(/\D/g, '');
|
|
81
|
+
if (digits.length < 7 || digits.length > 15) return false;
|
|
82
|
+
const hasPlus = m.trimStart().startsWith('+');
|
|
83
|
+
const hasSeparators = /[\s().-]/.test(m);
|
|
84
|
+
return hasPlus || hasSeparators;
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Redact every pattern match within a string.
|
|
91
|
+
* Returns the scrubbed string and the names of patterns that fired.
|
|
92
|
+
*/
|
|
93
|
+
export function redactPatterns(value: string): {
|
|
94
|
+
value: string;
|
|
95
|
+
matched: string[];
|
|
96
|
+
} {
|
|
97
|
+
let result = value;
|
|
98
|
+
const matched: string[] = [];
|
|
99
|
+
|
|
100
|
+
for (const pattern of REDACTION_PATTERNS) {
|
|
101
|
+
// Fresh lastIndex each pattern.
|
|
102
|
+
pattern.test.lastIndex = 0;
|
|
103
|
+
result = result.replace(pattern.test, (match) => {
|
|
104
|
+
if (pattern.confirm && !pattern.confirm(match)) return match;
|
|
105
|
+
if (!matched.includes(pattern.name)) matched.push(pattern.name);
|
|
106
|
+
return patternToken(pattern.name);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { value: result, matched };
|
|
111
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { RefRegistry } from '../registry';
|
|
5
|
+
|
|
6
|
+
describe('RefRegistry', () => {
|
|
7
|
+
it('reports unknown refs as unresolved', () => {
|
|
8
|
+
const reg = new RefRegistry('snap-1');
|
|
9
|
+
expect(reg.has('@e9')).toBe(false);
|
|
10
|
+
expect(reg.resolve('@e9')).toBeNull();
|
|
11
|
+
expect(reg.size).toBe(0);
|
|
12
|
+
expect(reg.snapshotId).toBe('snap-1');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('resolves a registered ref to its element', () => {
|
|
16
|
+
const reg = new RefRegistry('snap-1');
|
|
17
|
+
const el = document.createElement('button');
|
|
18
|
+
reg.set('@e1', el);
|
|
19
|
+
|
|
20
|
+
expect(reg.has('@e1')).toBe(true);
|
|
21
|
+
expect(reg.resolve('@e1')).toBe(el);
|
|
22
|
+
expect(reg.size).toBe(1);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ref registry — maps CST ref ids (`@e4`) back to DOM elements.
|
|
3
|
+
*
|
|
4
|
+
* Built during the DOM walk; kept after serialization so AI-driven
|
|
5
|
+
* highlight/focus directives can resolve a ref to a live element.
|
|
6
|
+
* Elements are held weakly so a removed node can be garbage-collected —
|
|
7
|
+
* an unresolvable ref is the "stale" case.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CSTRefId } from '../cst/types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A per-snapshot ref → element map. Elements are held weakly so a
|
|
14
|
+
* removed node can be garbage-collected — an unresolvable ref is the
|
|
15
|
+
* "stale" case.
|
|
16
|
+
*/
|
|
17
|
+
export class RefRegistry {
|
|
18
|
+
/** Unique id of the snapshot these refs belong to. */
|
|
19
|
+
readonly snapshotId: string;
|
|
20
|
+
|
|
21
|
+
private readonly map = new Map<CSTRefId, WeakRef<HTMLElement>>();
|
|
22
|
+
|
|
23
|
+
constructor(snapshotId: string) {
|
|
24
|
+
this.snapshotId = snapshotId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Register a ref → element pair (called during the walk). */
|
|
28
|
+
set(ref: CSTRefId, element: HTMLElement): void {
|
|
29
|
+
this.map.set(ref, new WeakRef(element));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Resolve a ref to its element, or null if gone / unknown (stale). */
|
|
33
|
+
resolve(ref: CSTRefId): HTMLElement | null {
|
|
34
|
+
return this.map.get(ref)?.deref() ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Whether a ref is known to this snapshot (regardless of liveness). */
|
|
38
|
+
has(ref: CSTRefId): boolean {
|
|
39
|
+
return this.map.has(ref);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Number of registered refs. */
|
|
43
|
+
get size(): number {
|
|
44
|
+
return this.map.size;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { serializeCST } from '../../cst/serialize';
|
|
4
|
+
import type { CSTRootNode } from '../../cst/types';
|
|
5
|
+
import { hashSnapshot } from '../hash';
|
|
6
|
+
|
|
7
|
+
const sampleRoot: CSTRootNode = {
|
|
8
|
+
type: 'root',
|
|
9
|
+
title: 'Billing Settings',
|
|
10
|
+
url: 'https://app.example.com/settings/billing',
|
|
11
|
+
children: [{ type: 'text', content: 'Configure your subscription.' }],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe('hashSnapshot', () => {
|
|
15
|
+
it('is stable for identical input', () => {
|
|
16
|
+
const s = serializeCST(sampleRoot);
|
|
17
|
+
expect(hashSnapshot(s)).toBe(hashSnapshot(s));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('changes when the snapshot changes', () => {
|
|
21
|
+
const a = hashSnapshot(serializeCST(sampleRoot));
|
|
22
|
+
const mutated: CSTRootNode = { ...sampleRoot, title: 'Other Page' };
|
|
23
|
+
const b = hashSnapshot(serializeCST(mutated));
|
|
24
|
+
expect(a).not.toBe(b);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns an 8-char hex string', () => {
|
|
28
|
+
expect(hashSnapshot('anything')).toMatch(/^[0-9a-f]{8}$/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('handles an empty string', () => {
|
|
32
|
+
expect(hashSnapshot('')).toMatch(/^[0-9a-f]{8}$/);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot content hash — staleness detection.
|
|
3
|
+
*
|
|
4
|
+
* A fast non-cryptographic hash (FNV-1a, 32-bit). Two captures of an
|
|
5
|
+
* unchanged page produce the same hash; a changed page produces a
|
|
6
|
+
* different one. Used to decide whether to send a fresh snapshot.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** FNV-1a 32-bit hash of a string, returned as a hex string. */
|
|
10
|
+
export function hashSnapshot(serialized: string): string {
|
|
11
|
+
// FNV-1a constants (32-bit).
|
|
12
|
+
let hash = 0x811c9dc5;
|
|
13
|
+
for (let i = 0; i < serialized.length; i++) {
|
|
14
|
+
hash ^= serialized.charCodeAt(i);
|
|
15
|
+
// hash *= 16777619, kept in 32-bit range via Math.imul.
|
|
16
|
+
hash = Math.imul(hash, 0x01000193);
|
|
17
|
+
}
|
|
18
|
+
// Unsigned, hex.
|
|
19
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token estimation.
|
|
3
|
+
*
|
|
4
|
+
* A cheap heuristic — chars / 4.2 — good enough to drive budget
|
|
5
|
+
* decisions client-side. The backend does the authoritative count.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Average characters per token, empirically ~4.2 for mixed UI text. */
|
|
9
|
+
const CHARS_PER_TOKEN = 4.2;
|
|
10
|
+
|
|
11
|
+
/** Estimate the token count of a string. */
|
|
12
|
+
export function estimateTokens(text: string): number {
|
|
13
|
+
if (!text) return 0;
|
|
14
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
15
|
+
}
|
|
@@ -89,12 +89,17 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|
|
89
89
|
const [source] = useState(() => createAudioSnapshotSource(audio));
|
|
90
90
|
const [levelsStore] = useState<LevelsStore>(() => createLevelsStore());
|
|
91
91
|
|
|
92
|
+
const [activePeaks, setActivePeaks] = useState<Float32Array | undefined>(
|
|
93
|
+
() => peaksProp ?? getPeaksFromCache(src),
|
|
94
|
+
);
|
|
95
|
+
|
|
92
96
|
// Seed peaks-from-prop into the module cache so other consumers benefit too.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
// Done in an effect (not render) to keep render pure under StrictMode.
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (peaksProp && getPeaksFromCache(src) === undefined) {
|
|
100
|
+
setPeaks(src, peaksProp);
|
|
101
|
+
}
|
|
102
|
+
}, [src, peaksProp]);
|
|
98
103
|
|
|
99
104
|
// Apply src / preload / loop / volume / muted imperatively.
|
|
100
105
|
useEffect(() => {
|
|
@@ -147,10 +152,10 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|
|
147
152
|
});
|
|
148
153
|
}, [audio, volumeIsControlled, mutedIsControlled]);
|
|
149
154
|
|
|
150
|
-
// When src changes, swap in
|
|
155
|
+
// When src changes, swap in peaks: prop wins, then module cache, else clear.
|
|
151
156
|
useEffect(() => {
|
|
152
|
-
setActivePeaks(getPeaksFromCache(src));
|
|
153
|
-
}, [src]);
|
|
157
|
+
setActivePeaks(peaksProp ?? getPeaksFromCache(src));
|
|
158
|
+
}, [src, peaksProp]);
|
|
154
159
|
|
|
155
160
|
// Lifecycle event hooks.
|
|
156
161
|
useEffect(() => {
|
|
@@ -306,9 +311,3 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|
|
306
311
|
</AudioRefCtx.Provider>
|
|
307
312
|
);
|
|
308
313
|
}
|
|
309
|
-
|
|
310
|
-
// Internal helper for parts that want to expose peaks-update from the lazy hook.
|
|
311
|
-
export function usePeaksSetter() {
|
|
312
|
-
// The provider seeds peaks via setPeaks() in the module cache; consumers
|
|
313
|
-
// re-read on src change. usePeaks hook re-uses the cache directly.
|
|
314
|
-
}
|
|
@@ -9,6 +9,7 @@ const TRACKED = [
|
|
|
9
9
|
'play',
|
|
10
10
|
'pause',
|
|
11
11
|
'ended',
|
|
12
|
+
'loadstart',
|
|
12
13
|
'loadedmetadata',
|
|
13
14
|
'durationchange',
|
|
14
15
|
'emptied',
|
|
@@ -17,6 +18,13 @@ const TRACKED = [
|
|
|
17
18
|
'error',
|
|
18
19
|
] as const;
|
|
19
20
|
|
|
21
|
+
// A detached <audio> whose `src` 404s (or points at a dead host) often never
|
|
22
|
+
// fires an `error` event — it stalls in NETWORK_LOADING indefinitely. After
|
|
23
|
+
// this long without metadata/canplay/error we synthesize a network error so
|
|
24
|
+
// the UI can surface it instead of showing a stuck skeleton forever.
|
|
25
|
+
const LOAD_WATCHDOG_MS = 12_000;
|
|
26
|
+
const SYNTHETIC_NETWORK_ERROR = 2;
|
|
27
|
+
|
|
20
28
|
type Snapshot = {
|
|
21
29
|
paused: boolean;
|
|
22
30
|
ended: boolean;
|
|
@@ -25,13 +33,14 @@ type Snapshot = {
|
|
|
25
33
|
errorCode: number | null;
|
|
26
34
|
};
|
|
27
35
|
|
|
28
|
-
function readSnapshot(audio: HTMLAudioElement): Snapshot {
|
|
36
|
+
function readSnapshot(audio: HTMLAudioElement, stalled: boolean): Snapshot {
|
|
37
|
+
const nativeCode = audio.error?.code ?? null;
|
|
29
38
|
return {
|
|
30
39
|
paused: audio.paused,
|
|
31
40
|
ended: audio.ended,
|
|
32
41
|
duration: Number.isFinite(audio.duration) ? audio.duration : 0,
|
|
33
42
|
ready: audio.readyState >= 2,
|
|
34
|
-
errorCode:
|
|
43
|
+
errorCode: nativeCode ?? (stalled ? SYNTHETIC_NETWORK_ERROR : null),
|
|
35
44
|
};
|
|
36
45
|
}
|
|
37
46
|
|
|
@@ -46,19 +55,59 @@ function snapshotsEqual(a: Snapshot, b: Snapshot): boolean {
|
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
export function createAudioSnapshotSource(audio: HTMLAudioElement) {
|
|
49
|
-
let
|
|
58
|
+
let stalled = false;
|
|
59
|
+
let cached: Snapshot = readSnapshot(audio, stalled);
|
|
50
60
|
return {
|
|
51
61
|
subscribe(cb: () => void): () => void {
|
|
62
|
+
let watchdog: ReturnType<typeof setTimeout> | null = null;
|
|
63
|
+
|
|
64
|
+
const clearWatchdog = () => {
|
|
65
|
+
if (watchdog !== null) {
|
|
66
|
+
clearTimeout(watchdog);
|
|
67
|
+
watchdog = null;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
52
70
|
const refresh = () => {
|
|
53
|
-
const next = readSnapshot(audio);
|
|
71
|
+
const next = readSnapshot(audio, stalled);
|
|
54
72
|
if (!snapshotsEqual(cached, next)) {
|
|
55
73
|
cached = next;
|
|
56
74
|
cb();
|
|
57
75
|
}
|
|
58
76
|
};
|
|
59
|
-
|
|
77
|
+
const armWatchdog = () => {
|
|
78
|
+
clearWatchdog();
|
|
79
|
+
if (audio.readyState >= 2 || audio.error) return;
|
|
80
|
+
watchdog = setTimeout(() => {
|
|
81
|
+
watchdog = null;
|
|
82
|
+
if (audio.readyState < 2 && !audio.error) {
|
|
83
|
+
stalled = true;
|
|
84
|
+
refresh();
|
|
85
|
+
}
|
|
86
|
+
}, LOAD_WATCHDOG_MS);
|
|
87
|
+
};
|
|
88
|
+
const onProgress = (e: Event) => {
|
|
89
|
+
if (e.type === 'loadstart' || e.type === 'emptied') {
|
|
90
|
+
// New load cycle — drop any prior stall and restart the watchdog.
|
|
91
|
+
stalled = false;
|
|
92
|
+
armWatchdog();
|
|
93
|
+
} else if (audio.readyState >= 2 || audio.error) {
|
|
94
|
+
// Real progress (or a native error) clears a pending stall timer.
|
|
95
|
+
clearWatchdog();
|
|
96
|
+
if (stalled) stalled = false;
|
|
97
|
+
}
|
|
98
|
+
refresh();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
for (const e of TRACKED) audio.addEventListener(e, onProgress);
|
|
102
|
+
// Re-sync immediately: events (error, loadedmetadata, …) may have fired
|
|
103
|
+
// between source creation and this subscription. Without this, a stale
|
|
104
|
+
// snapshot from construction time would never update — e.g. a `src` that
|
|
105
|
+
// already errored would stay stuck on `loading`.
|
|
106
|
+
armWatchdog();
|
|
107
|
+
refresh();
|
|
60
108
|
return () => {
|
|
61
|
-
|
|
109
|
+
clearWatchdog();
|
|
110
|
+
for (const e of TRACKED) audio.removeEventListener(e, onProgress);
|
|
62
111
|
};
|
|
63
112
|
},
|
|
64
113
|
getSnapshot(): Snapshot {
|
|
@@ -3,39 +3,25 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* `@djangocfg/ui-tools/audio-player` subpath entrypoint.
|
|
5
5
|
*
|
|
6
|
-
* `LazyPlayer` is
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* exists inside `LazyPlayer` once loaded).
|
|
6
|
+
* `LazyPlayer` is exported as a direct (synchronous) alias of `Player`. We
|
|
7
|
+
* intentionally avoid `React.lazy` + `import('./Player')` here: under bundlers
|
|
8
|
+
* that pre-bundle subpath entries (Vite optimizeDeps in Next.js/Vite/SB), the
|
|
9
|
+
* dynamic import creates a second chunk that re-instantiates the React
|
|
10
|
+
* Contexts (AudioRefCtx/ControlsCtx/MetaCtx/StateCtx/LevelsCtx). The slot
|
|
11
|
+
* components and selector hooks re-exported below would then read from a
|
|
12
|
+
* different context instance than `<PlayerProvider>` writes to, which made
|
|
13
|
+
* `usePlayerAudio` throw "must be used inside <PlayerProvider>".
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Heavy audio-decoding work (peaks, AudioContext) already happens lazily at
|
|
16
|
+
* runtime via effects inside `PlayerProvider`/`PlayerShell` — there is no
|
|
17
|
+
* benefit to splitting the React shell behind a second chunk.
|
|
19
18
|
*/
|
|
20
19
|
|
|
21
|
-
import { createLazyComponent } from '../../components';
|
|
22
|
-
import type { PlayerProps } from './types';
|
|
23
|
-
|
|
24
20
|
// ============================================================================
|
|
25
|
-
//
|
|
21
|
+
// Player component (synchronous; previously lazy — see note above)
|
|
26
22
|
// ============================================================================
|
|
27
23
|
|
|
28
|
-
export
|
|
29
|
-
() => import('./Player').then((mod) => ({ default: mod.Player })),
|
|
30
|
-
{
|
|
31
|
-
displayName: 'LazyAudioPlayer',
|
|
32
|
-
fallback: (
|
|
33
|
-
<div className="rounded-lg border border-border/60 bg-card px-4 py-6 text-sm text-muted-foreground">
|
|
34
|
-
Loading audio player…
|
|
35
|
-
</div>
|
|
36
|
-
),
|
|
37
|
-
},
|
|
38
|
-
);
|
|
24
|
+
export { Player, Player as LazyPlayer } from './Player';
|
|
39
25
|
|
|
40
26
|
// ============================================================================
|
|
41
27
|
// Light surface — types, store, context, slot components, hooks
|
|
@@ -14,9 +14,7 @@ export function TimeDisplay() {
|
|
|
14
14
|
useEffect(() => {
|
|
15
15
|
const el = currentRef.current;
|
|
16
16
|
if (!el) return;
|
|
17
|
-
let raf = 0;
|
|
18
17
|
let last = -1;
|
|
19
|
-
let timer: ReturnType<typeof setInterval> | null = null;
|
|
20
18
|
const write = () => {
|
|
21
19
|
const t = audio.currentTime;
|
|
22
20
|
if (Math.abs(t - last) < 0.5) return;
|
|
@@ -28,14 +26,13 @@ export function TimeDisplay() {
|
|
|
28
26
|
audio.addEventListener('seeked', onSeek);
|
|
29
27
|
audio.addEventListener('timeupdate', onSeek);
|
|
30
28
|
// Backup poll when timeupdate is throttled (e.g. background tab → visible).
|
|
31
|
-
timer = setInterval(() => {
|
|
29
|
+
const timer = setInterval(() => {
|
|
32
30
|
if (!audio.paused) write();
|
|
33
31
|
}, READ_INTERVAL_MS);
|
|
34
32
|
return () => {
|
|
35
|
-
cancelAnimationFrame(raf);
|
|
36
33
|
audio.removeEventListener('seeked', onSeek);
|
|
37
34
|
audio.removeEventListener('timeupdate', onSeek);
|
|
38
|
-
|
|
35
|
+
clearInterval(timer);
|
|
39
36
|
};
|
|
40
37
|
}, [audio]);
|
|
41
38
|
|