@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.409",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -51,11 +51,6 @@
|
|
|
51
51
|
"import": "./src/tools/Uploader/index.ts",
|
|
52
52
|
"require": "./src/tools/Uploader/index.ts"
|
|
53
53
|
},
|
|
54
|
-
"./tour": {
|
|
55
|
-
"types": "./src/tools/Tour/index.ts",
|
|
56
|
-
"import": "./src/tools/Tour/index.ts",
|
|
57
|
-
"require": "./src/tools/Tour/index.ts"
|
|
58
|
-
},
|
|
59
54
|
"./code-editor": {
|
|
60
55
|
"types": "./src/tools/CodeEditor/lazy.tsx",
|
|
61
56
|
"import": "./src/tools/CodeEditor/lazy.tsx",
|
|
@@ -159,8 +154,8 @@
|
|
|
159
154
|
"test:watch": "vitest"
|
|
160
155
|
},
|
|
161
156
|
"peerDependencies": {
|
|
162
|
-
"@djangocfg/i18n": "^2.1.
|
|
163
|
-
"@djangocfg/ui-core": "^2.1.
|
|
157
|
+
"@djangocfg/i18n": "^2.1.409",
|
|
158
|
+
"@djangocfg/ui-core": "^2.1.409",
|
|
164
159
|
"consola": "^3.4.2",
|
|
165
160
|
"lodash-es": "^4.18.1",
|
|
166
161
|
"lucide-react": "^0.545.0",
|
|
@@ -212,18 +207,18 @@
|
|
|
212
207
|
},
|
|
213
208
|
"optionalDependencies": {
|
|
214
209
|
"@mapbox/mapbox-gl-draw": "^1.4.3",
|
|
215
|
-
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
216
|
-
"material-file-icons": "^2.4.0"
|
|
210
|
+
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
217
211
|
},
|
|
218
212
|
"devDependencies": {
|
|
219
|
-
"@djangocfg/i18n": "^2.1.
|
|
220
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
221
|
-
"@djangocfg/ui-core": "^2.1.
|
|
213
|
+
"@djangocfg/i18n": "^2.1.409",
|
|
214
|
+
"@djangocfg/typescript-config": "^2.1.409",
|
|
215
|
+
"@djangocfg/ui-core": "^2.1.409",
|
|
222
216
|
"@types/lodash-es": "^4.17.12",
|
|
223
217
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
224
218
|
"@types/node": "^24.7.2",
|
|
225
219
|
"@types/react": "^19.1.0",
|
|
226
220
|
"@types/react-dom": "^19.1.0",
|
|
221
|
+
"jsdom": "^29.1.1",
|
|
227
222
|
"lodash-es": "^4.18.1",
|
|
228
223
|
"lucide-react": "^0.545.0",
|
|
229
224
|
"react": "^19.1.0",
|
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState } from 'react';
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
import './FloatingToolbar.css';
|
|
6
6
|
import { useScrollIsolation } from './hooks/useScrollIsolation';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Track whether the container actually overflows. Scroll isolation
|
|
10
|
+
* (and its "Click to scroll" overlay) only makes sense when there is
|
|
11
|
+
* something to scroll — a fully-visible block must stay interactive.
|
|
12
|
+
*/
|
|
13
|
+
function useIsScrollable(ref: React.RefObject<HTMLElement | null>): boolean {
|
|
14
|
+
const [scrollable, setScrollable] = useState(false);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const el = ref.current;
|
|
18
|
+
if (!el) return;
|
|
19
|
+
|
|
20
|
+
const measure = () => {
|
|
21
|
+
// 1px tolerance for sub-pixel rounding.
|
|
22
|
+
setScrollable(
|
|
23
|
+
el.scrollHeight - el.clientHeight > 1 ||
|
|
24
|
+
el.scrollWidth - el.clientWidth > 1,
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
measure();
|
|
29
|
+
const observer = new ResizeObserver(measure);
|
|
30
|
+
observer.observe(el);
|
|
31
|
+
return () => observer.disconnect();
|
|
32
|
+
}, [ref]);
|
|
33
|
+
|
|
34
|
+
return scrollable;
|
|
35
|
+
}
|
|
36
|
+
|
|
8
37
|
export interface FloatingToolbarProps {
|
|
9
38
|
/** Ref to the container element the toolbar anchors to */
|
|
10
39
|
containerRef: React.RefObject<HTMLElement | null>;
|
|
@@ -39,11 +68,16 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|
|
39
68
|
zIndex = 30,
|
|
40
69
|
scrollIsolation = true,
|
|
41
70
|
}) => {
|
|
42
|
-
|
|
71
|
+
// Isolation only engages when the container can actually scroll —
|
|
72
|
+
// a block that fully fits never shows the "Click to scroll" overlay.
|
|
73
|
+
const isScrollable = useIsScrollable(containerRef);
|
|
74
|
+
const isolationActive = scrollIsolation && isScrollable;
|
|
75
|
+
|
|
76
|
+
const { locked, unlock } = useScrollIsolation(containerRef, isolationActive);
|
|
43
77
|
const [overlayHovered, setOverlayHovered] = useState(false);
|
|
44
78
|
|
|
45
79
|
const overlay =
|
|
46
|
-
|
|
80
|
+
isolationActive && locked ? (
|
|
47
81
|
<div
|
|
48
82
|
onClick={unlock}
|
|
49
83
|
onMouseEnter={() => setOverlayHovered(true)}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { PageSnapshotEngine } from '../engine';
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
document.body.innerHTML = '';
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Integration: capture realistic pages end-to-end through the engine
|
|
12
|
+
* (scope → walk → fold → budget). Verifies the P1 success criteria —
|
|
13
|
+
* a 100-row data table, with folding, stays within budget.
|
|
14
|
+
*/
|
|
15
|
+
describe('capture integration', () => {
|
|
16
|
+
it('captures a settings form sensibly and well under budget', () => {
|
|
17
|
+
document.body.innerHTML = `
|
|
18
|
+
<nav>global nav</nav>
|
|
19
|
+
<main>
|
|
20
|
+
<h1>Billing Settings</h1>
|
|
21
|
+
<form>
|
|
22
|
+
<label for="company">Company</label>
|
|
23
|
+
<input id="company" type="text" value="Acme Inc" />
|
|
24
|
+
<label for="plan">Plan</label>
|
|
25
|
+
<select id="plan"><option selected>Pro</option></select>
|
|
26
|
+
<button type="submit">Save</button>
|
|
27
|
+
</form>
|
|
28
|
+
</main>`;
|
|
29
|
+
|
|
30
|
+
const { payload, telemetry } = new PageSnapshotEngine().capture();
|
|
31
|
+
const json = JSON.stringify(payload.snapshot);
|
|
32
|
+
|
|
33
|
+
// Content is present, chrome is not.
|
|
34
|
+
expect(json).toContain('Billing Settings');
|
|
35
|
+
expect(json).toContain('Company');
|
|
36
|
+
expect(json).not.toContain('global nav');
|
|
37
|
+
|
|
38
|
+
// Comfortably small.
|
|
39
|
+
expect(payload.metadata.tokenEstimate).toBeLessThan(500);
|
|
40
|
+
expect(telemetry.degradation).toBe('none');
|
|
41
|
+
expect(telemetry.scopeTier).toBe('tier2-main');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('captures a 100-row table within budget thanks to folding', () => {
|
|
45
|
+
const rows = Array.from(
|
|
46
|
+
{ length: 100 },
|
|
47
|
+
(_, i) => `
|
|
48
|
+
<tr>
|
|
49
|
+
<td>Client ${i}</td>
|
|
50
|
+
<td>client${i}@example.com</td>
|
|
51
|
+
<td><button>Edit</button></td>
|
|
52
|
+
</tr>`,
|
|
53
|
+
).join('');
|
|
54
|
+
document.body.innerHTML = `
|
|
55
|
+
<main>
|
|
56
|
+
<table><tbody>${rows}</tbody></table>
|
|
57
|
+
</main>`;
|
|
58
|
+
|
|
59
|
+
const { payload, telemetry } = new PageSnapshotEngine().capture();
|
|
60
|
+
|
|
61
|
+
// Folding fired — the 100 identical rows collapsed.
|
|
62
|
+
expect(telemetry.foldedCount).toBeGreaterThan(0);
|
|
63
|
+
|
|
64
|
+
// The whole snapshot stays within the default 8k budget.
|
|
65
|
+
expect(payload.metadata.tokenEstimate).toBeLessThan(8_000);
|
|
66
|
+
|
|
67
|
+
// The collapse marker is present.
|
|
68
|
+
expect(JSON.stringify(payload.snapshot)).toContain('collapsed');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('a 100-row table WITHOUT folding would be far larger (sanity)', () => {
|
|
72
|
+
// Same table; confirm the folded result is dramatically smaller
|
|
73
|
+
// than the raw row count would imply (~100 rows × 3 cells).
|
|
74
|
+
const rows = Array.from(
|
|
75
|
+
{ length: 100 },
|
|
76
|
+
(_, i) => `<tr><td>Client ${i}</td><td>x${i}</td></tr>`,
|
|
77
|
+
).join('');
|
|
78
|
+
document.body.innerHTML = `<main><table><tbody>${rows}</tbody></table></main>`;
|
|
79
|
+
|
|
80
|
+
const { payload } = new PageSnapshotEngine().capture();
|
|
81
|
+
const interactiveAndText = JSON.stringify(payload.snapshot).length;
|
|
82
|
+
// Folded snapshot is small — nowhere near 100 rows of serialized CST.
|
|
83
|
+
expect(interactiveAndText).toBeLessThan(4_000);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { PageSnapshotEngine } from '../engine';
|
|
5
|
+
import { CST_SCHEMA_VERSION } from '../cst/payload';
|
|
6
|
+
|
|
7
|
+
describe('PageSnapshotEngine (P0 skeleton)', () => {
|
|
8
|
+
it('returns a schema-valid empty snapshot', () => {
|
|
9
|
+
const { payload, telemetry } = new PageSnapshotEngine({
|
|
10
|
+
scope: 'full',
|
|
11
|
+
}).capture();
|
|
12
|
+
|
|
13
|
+
expect(payload.snapshot.type).toBe('root');
|
|
14
|
+
expect(payload.snapshot.children).toEqual([]);
|
|
15
|
+
expect(payload.title).toBe(document.title);
|
|
16
|
+
expect(payload.url).toBe(window.location.href);
|
|
17
|
+
expect(payload.route).toBe(window.location.pathname);
|
|
18
|
+
|
|
19
|
+
expect(payload.metadata.schemaVersion).toBe(CST_SCHEMA_VERSION);
|
|
20
|
+
expect(payload.metadata.representation).toBe('CST');
|
|
21
|
+
expect(payload.metadata.contentHash).toMatch(/^[0-9a-f]{8}$/);
|
|
22
|
+
expect(payload.metadata.tokenEstimate).toBeGreaterThan(0);
|
|
23
|
+
expect(payload.metadata.redactedCount).toBe(0);
|
|
24
|
+
expect(payload.metadata.foldedCount).toBe(0);
|
|
25
|
+
|
|
26
|
+
expect(telemetry.nodesVisited).toBe(0);
|
|
27
|
+
expect(telemetry.executionTimeMs).toBeGreaterThanOrEqual(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('produces a stable content hash across repeated captures', () => {
|
|
31
|
+
const engine = new PageSnapshotEngine();
|
|
32
|
+
const a = engine.capture().payload.metadata.contentHash;
|
|
33
|
+
const b = engine.capture().payload.metadata.contentHash;
|
|
34
|
+
expect(a).toBe(b);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { PageSnapshotEngine } from '../engine';
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
document.body.innerHTML = '';
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/** Capture the page and return the serialized snapshot string. */
|
|
11
|
+
function snapshotJson(): string {
|
|
12
|
+
return JSON.stringify(new PageSnapshotEngine().capture().payload.snapshot);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Integration: redaction runs inside the engine's capture pipeline, in
|
|
17
|
+
* the browser, before any snapshot would leave the device. Verifies the
|
|
18
|
+
* P2 success criteria — no secret/PII reaches the serialized snapshot.
|
|
19
|
+
*/
|
|
20
|
+
describe('redaction integration', () => {
|
|
21
|
+
it('redacts a password field value (safe by default, no annotation)', () => {
|
|
22
|
+
document.body.innerHTML = `
|
|
23
|
+
<main>
|
|
24
|
+
<form>
|
|
25
|
+
<label for="p">Password</label>
|
|
26
|
+
<input id="p" type="password" value="hunter2-secret" />
|
|
27
|
+
</form>
|
|
28
|
+
</main>`;
|
|
29
|
+
const json = snapshotJson();
|
|
30
|
+
expect(json).not.toContain('hunter2-secret');
|
|
31
|
+
expect(json).toContain('‹redacted:password›');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('redacts a secret-named field', () => {
|
|
35
|
+
document.body.innerHTML = `
|
|
36
|
+
<main><input name="api_key" type="text" value="raw-key-value-123" /></main>`;
|
|
37
|
+
const json = snapshotJson();
|
|
38
|
+
expect(json).not.toContain('raw-key-value-123');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('redacts PII patterns inside static text', () => {
|
|
42
|
+
document.body.innerHTML = `
|
|
43
|
+
<main><p>Contact: jane.doe@example.com or call later</p></main>`;
|
|
44
|
+
const json = snapshotJson();
|
|
45
|
+
expect(json).not.toContain('jane.doe@example.com');
|
|
46
|
+
expect(json).toContain('‹redacted:email›');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('drops a data-ai-redact subtree entirely', () => {
|
|
50
|
+
document.body.innerHTML = `
|
|
51
|
+
<main>
|
|
52
|
+
<div data-ai-redact>
|
|
53
|
+
<input type="text" value="confidential-block-value" />
|
|
54
|
+
<p>secret paragraph</p>
|
|
55
|
+
</div>
|
|
56
|
+
<p>public paragraph</p>
|
|
57
|
+
</main>`;
|
|
58
|
+
const json = snapshotJson();
|
|
59
|
+
expect(json).not.toContain('confidential-block-value');
|
|
60
|
+
expect(json).not.toContain('secret paragraph');
|
|
61
|
+
expect(json).toContain('[redacted]');
|
|
62
|
+
expect(json).toContain('public paragraph');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('data-ai-include overrides a heuristic block for a safe value', () => {
|
|
66
|
+
// A field named "token" would normally be redacted; data-ai-include
|
|
67
|
+
// forces the (known-safe, public) value through.
|
|
68
|
+
document.body.innerHTML = `
|
|
69
|
+
<main>
|
|
70
|
+
<input name="public_token_name" data-ai-include
|
|
71
|
+
type="text" value="PUBLIC-PROJECT-ALPHA" />
|
|
72
|
+
</main>`;
|
|
73
|
+
const json = snapshotJson();
|
|
74
|
+
expect(json).toContain('PUBLIC-PROJECT-ALPHA');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('reports redactions in the capture telemetry', () => {
|
|
78
|
+
document.body.innerHTML = `
|
|
79
|
+
<main>
|
|
80
|
+
<input type="password" value="p1" />
|
|
81
|
+
<input name="secret_key" value="s1" />
|
|
82
|
+
</main>`;
|
|
83
|
+
const { telemetry } = new PageSnapshotEngine().capture();
|
|
84
|
+
expect(telemetry.redactedCount).toBeGreaterThanOrEqual(2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('leaves a normal page completely untouched', () => {
|
|
88
|
+
document.body.innerHTML = `
|
|
89
|
+
<main>
|
|
90
|
+
<input name="company" type="text" value="Acme Inc" />
|
|
91
|
+
<p>Welcome to the dashboard</p>
|
|
92
|
+
</main>`;
|
|
93
|
+
const { payload, telemetry } = new PageSnapshotEngine().capture();
|
|
94
|
+
const json = JSON.stringify(payload.snapshot);
|
|
95
|
+
expect(json).toContain('Acme Inc');
|
|
96
|
+
expect(json).toContain('Welcome to the dashboard');
|
|
97
|
+
expect(telemetry.redactedCount).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { estimateTokens } from '../tokens';
|
|
4
|
+
|
|
5
|
+
describe('estimateTokens', () => {
|
|
6
|
+
it('returns 0 for an empty string', () => {
|
|
7
|
+
expect(estimateTokens('')).toBe(0);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('scales with length (~chars / 4.2)', () => {
|
|
11
|
+
expect(estimateTokens('a'.repeat(42))).toBe(10);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('rounds up partial tokens', () => {
|
|
15
|
+
expect(estimateTokens('abc')).toBe(1);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { enforceBudget } from '../budget';
|
|
4
|
+
import type { CSTNode } from '../../cst/types';
|
|
5
|
+
|
|
6
|
+
describe('enforceBudget', () => {
|
|
7
|
+
it('leaves a small snapshot untouched', () => {
|
|
8
|
+
const children: CSTNode[] = [
|
|
9
|
+
{ type: 'text', content: 'short' },
|
|
10
|
+
{ type: 'interactive', role: 'button', ref: '@e1', name: 'Go' },
|
|
11
|
+
];
|
|
12
|
+
const result = enforceBudget(children, 8_000);
|
|
13
|
+
expect(result.degradation).toBe('none');
|
|
14
|
+
expect(result.children).toEqual(children);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('truncates long text when over budget', () => {
|
|
18
|
+
const children: CSTNode[] = [
|
|
19
|
+
{ type: 'text', content: 'x'.repeat(5_000) },
|
|
20
|
+
];
|
|
21
|
+
const result = enforceBudget(children, 100);
|
|
22
|
+
expect(result.degradation).toBe('truncated');
|
|
23
|
+
const text = result.children[0];
|
|
24
|
+
expect(text.type).toBe('text');
|
|
25
|
+
if (text.type === 'text') {
|
|
26
|
+
expect(text.content).toContain('(truncated)');
|
|
27
|
+
expect(text.content.length).toBeLessThan(200);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('falls back to interactive-only when truncation is not enough', () => {
|
|
32
|
+
// Many text nodes — truncation alone still over a tiny budget.
|
|
33
|
+
const children: CSTNode[] = Array.from({ length: 200 }, (_, i) => ({
|
|
34
|
+
type: 'text' as const,
|
|
35
|
+
content: `paragraph ${i} `.repeat(20),
|
|
36
|
+
}));
|
|
37
|
+
children.push({
|
|
38
|
+
type: 'interactive',
|
|
39
|
+
role: 'button',
|
|
40
|
+
ref: '@e1',
|
|
41
|
+
name: 'Submit',
|
|
42
|
+
});
|
|
43
|
+
const result = enforceBudget(children, 50);
|
|
44
|
+
expect(result.degradation).toBe('interactive-only');
|
|
45
|
+
// Only the interactive node survives.
|
|
46
|
+
expect(result.children.every((n) => n.type === 'interactive')).toBe(true);
|
|
47
|
+
expect(result.children).toHaveLength(1);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { isChrome } from '../chrome-filter';
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
document.body.innerHTML = '';
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function el(html: string): HTMLElement {
|
|
11
|
+
document.body.innerHTML = html;
|
|
12
|
+
return document.body.firstElementChild as HTMLElement;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('isChrome', () => {
|
|
16
|
+
it('flags landmark chrome tags', () => {
|
|
17
|
+
expect(isChrome(el('<nav>links</nav>'))).toBe(true);
|
|
18
|
+
expect(isChrome(el('<header>top</header>'))).toBe(true);
|
|
19
|
+
expect(isChrome(el('<footer>bottom</footer>'))).toBe(true);
|
|
20
|
+
expect(isChrome(el('<aside>side</aside>'))).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('flags landmark chrome roles', () => {
|
|
24
|
+
expect(isChrome(el('<div role="navigation">x</div>'))).toBe(true);
|
|
25
|
+
expect(isChrome(el('<div role="banner">x</div>'))).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('flags chrome by class/id name pattern', () => {
|
|
29
|
+
expect(isChrome(el('<div class="sidebar">x</div>'))).toBe(true);
|
|
30
|
+
expect(isChrome(el('<div id="app-navbar">x</div>'))).toBe(true);
|
|
31
|
+
expect(isChrome(el('<div class="breadcrumb">x</div>'))).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('never flags content landmarks', () => {
|
|
35
|
+
expect(isChrome(el('<main>content</main>'))).toBe(false);
|
|
36
|
+
expect(isChrome(el('<div role="main">content</div>'))).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('does not flag a plain content div', () => {
|
|
40
|
+
expect(isChrome(el('<div class="card-body">data</div>'))).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('content-named element wins over an unlikely token', () => {
|
|
44
|
+
// "main-content" contains "content" → not chrome despite layout-ish name
|
|
45
|
+
expect(isChrome(el('<div class="main-content">x</div>'))).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { foldSiblings } from '../fold';
|
|
4
|
+
import type { CSTNode } from '../../cst/types';
|
|
5
|
+
|
|
6
|
+
/** Build N identically-shaped interactive rows. */
|
|
7
|
+
function rows(n: number, role: 'button' = 'button'): CSTNode[] {
|
|
8
|
+
return Array.from({ length: n }, (_, i) => ({
|
|
9
|
+
type: 'container' as const,
|
|
10
|
+
role: 'section' as const,
|
|
11
|
+
children: [
|
|
12
|
+
{ type: 'interactive' as const, role, ref: `@e${i + 1}` as const, name: `Row ${i}` },
|
|
13
|
+
],
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('foldSiblings', () => {
|
|
18
|
+
it('does not fold short chains (< 5)', () => {
|
|
19
|
+
const { children, foldedCount } = foldSiblings(rows(4));
|
|
20
|
+
expect(foldedCount).toBe(0);
|
|
21
|
+
expect(children).toHaveLength(4);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('folds a long chain of identical siblings', () => {
|
|
25
|
+
const { children, foldedCount } = foldSiblings(rows(100));
|
|
26
|
+
expect(foldedCount).toBe(1);
|
|
27
|
+
// 3 kept + 1 collapse marker.
|
|
28
|
+
expect(children).toHaveLength(4);
|
|
29
|
+
expect(children[3]).toEqual({
|
|
30
|
+
type: 'text',
|
|
31
|
+
content: '[… 97 more similar items collapsed]',
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('keeps the first 3 instances', () => {
|
|
36
|
+
const { children } = foldSiblings(rows(20));
|
|
37
|
+
expect(children.slice(0, 3).every((n) => n.type === 'container')).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('does not fold structurally different siblings', () => {
|
|
41
|
+
const mixed: CSTNode[] = [
|
|
42
|
+
{ type: 'interactive', role: 'button', ref: '@e1', name: 'A' },
|
|
43
|
+
{ type: 'interactive', role: 'textbox', ref: '@e2', name: 'B' },
|
|
44
|
+
{ type: 'interactive', role: 'checkbox', ref: '@e3', name: 'C' },
|
|
45
|
+
{ type: 'interactive', role: 'link', ref: '@e4', name: 'D' },
|
|
46
|
+
{ type: 'interactive', role: 'select', ref: '@e5', name: 'E' },
|
|
47
|
+
{ type: 'interactive', role: 'tab', ref: '@e6', name: 'F' },
|
|
48
|
+
];
|
|
49
|
+
const { foldedCount, children } = foldSiblings(mixed);
|
|
50
|
+
expect(foldedCount).toBe(0);
|
|
51
|
+
expect(children).toHaveLength(6);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('folds nested repetitive lists too', () => {
|
|
55
|
+
const table: CSTNode[] = [
|
|
56
|
+
{ type: 'container', role: 'table', children: rows(50) },
|
|
57
|
+
];
|
|
58
|
+
const { children, foldedCount } = foldSiblings(table);
|
|
59
|
+
expect(foldedCount).toBe(1);
|
|
60
|
+
const tableNode = children[0];
|
|
61
|
+
expect(tableNode.type).toBe('container');
|
|
62
|
+
if (tableNode.type === 'container') {
|
|
63
|
+
expect(tableNode.children).toHaveLength(4); // 3 + marker
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { resolveScope } from '../scope';
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
document.body.innerHTML = '';
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* jsdom does not lay out elements, so geometry-based tiers (3 dialog
|
|
12
|
+
* sizing, heuristic scoring) can only be smoke-tested. These tests
|
|
13
|
+
* cover the structural tier selection — which is the part that matters
|
|
14
|
+
* for ladder ordering.
|
|
15
|
+
*/
|
|
16
|
+
describe('resolveScope — tier ladder', () => {
|
|
17
|
+
it('honors an explicit target selector first', () => {
|
|
18
|
+
document.body.innerHTML = `<main id="m">content</main><div id="t">x</div>`;
|
|
19
|
+
const scope = resolveScope({ strategy: 'container', targetSelector: '#t' });
|
|
20
|
+
expect(scope.tier).toBe('explicit-selector');
|
|
21
|
+
expect(scope.roots[0].id).toBe('t');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("'full' strategy forces body", () => {
|
|
25
|
+
document.body.innerHTML = `<main>content</main>`;
|
|
26
|
+
const scope = resolveScope({ strategy: 'full' });
|
|
27
|
+
expect(scope.tier).toBe('tier4-body');
|
|
28
|
+
expect(scope.roots[0]).toBe(document.body);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('Tier 1 — picks [data-ai-context] over <main>', () => {
|
|
32
|
+
document.body.innerHTML = `
|
|
33
|
+
<main>main content</main>
|
|
34
|
+
<div data-ai-context>annotated region</div>
|
|
35
|
+
`;
|
|
36
|
+
const scope = resolveScope({ strategy: 'container' });
|
|
37
|
+
expect(scope.tier).toBe('tier1-annotation');
|
|
38
|
+
expect(scope.roots[0].dataset.aiContext).toBe('');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('Tier 1 — ignores data-ai-context="exclude"', () => {
|
|
42
|
+
document.body.innerHTML = `
|
|
43
|
+
<main>main content</main>
|
|
44
|
+
<div data-ai-context="exclude">excluded</div>
|
|
45
|
+
`;
|
|
46
|
+
const scope = resolveScope({ strategy: 'container' });
|
|
47
|
+
// Annotation tier yields nothing usable → falls to <main>.
|
|
48
|
+
expect(scope.tier).toBe('tier2-main');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('Tier 2 — picks <main> when no annotation', () => {
|
|
52
|
+
document.body.innerHTML = `
|
|
53
|
+
<nav>navigation</nav>
|
|
54
|
+
<main id="m">main content here</main>
|
|
55
|
+
`;
|
|
56
|
+
const scope = resolveScope({ strategy: 'container' });
|
|
57
|
+
expect(scope.tier).toBe('tier2-main');
|
|
58
|
+
expect(scope.roots[0].id).toBe('m');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('Tier 2 — role="main" also counts', () => {
|
|
62
|
+
document.body.innerHTML = `<div role="main" id="m">content</div>`;
|
|
63
|
+
const scope = resolveScope({ strategy: 'container' });
|
|
64
|
+
expect(scope.tier).toBe('tier2-main');
|
|
65
|
+
expect(scope.roots[0].id).toBe('m');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('Tier 4 — falls back to body when nothing else matches', () => {
|
|
69
|
+
// No main, no annotation, no sized blocks (jsdom = zero geometry).
|
|
70
|
+
document.body.innerHTML = `<span>just text</span>`;
|
|
71
|
+
const scope = resolveScope({ strategy: 'container' });
|
|
72
|
+
expect(scope.tier).toBe('tier4-body');
|
|
73
|
+
});
|
|
74
|
+
});
|