@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.
Files changed (297) hide show
  1. package/README.md +9 -10
  2. package/dist/file-icon/index.cjs +449 -61
  3. package/dist/file-icon/index.cjs.map +1 -1
  4. package/dist/file-icon/index.d.cts +56 -18
  5. package/dist/file-icon/index.d.ts +56 -18
  6. package/dist/file-icon/index.mjs +448 -62
  7. package/dist/file-icon/index.mjs.map +1 -1
  8. package/dist/tree/index.cjs +49 -22
  9. package/dist/tree/index.cjs.map +1 -1
  10. package/dist/tree/index.d.cts +9 -3
  11. package/dist/tree/index.d.ts +9 -3
  12. package/dist/tree/index.mjs +49 -22
  13. package/dist/tree/index.mjs.map +1 -1
  14. package/dist/{types-B_zhyAqR.d.cts → types-eEu8SeiQ.d.cts} +4 -0
  15. package/dist/{types-B_zhyAqR.d.ts → types-eEu8SeiQ.d.ts} +4 -0
  16. package/package.json +8 -13
  17. package/src/components/FloatingToolbar/index.tsx +37 -3
  18. package/src/lib/page-snapshot/__tests__/capture-integration.test.ts +85 -0
  19. package/src/lib/page-snapshot/__tests__/engine.test.ts +36 -0
  20. package/src/lib/page-snapshot/__tests__/redaction-integration.test.ts +99 -0
  21. package/src/lib/page-snapshot/__tests__/tokens.test.ts +17 -0
  22. package/src/lib/page-snapshot/capture/__tests__/budget.test.ts +49 -0
  23. package/src/lib/page-snapshot/capture/__tests__/chrome-filter.test.ts +47 -0
  24. package/src/lib/page-snapshot/capture/__tests__/fold.test.ts +66 -0
  25. package/src/lib/page-snapshot/capture/__tests__/scope.test.ts +74 -0
  26. package/src/lib/page-snapshot/capture/__tests__/walk.test.ts +129 -0
  27. package/src/lib/page-snapshot/capture/accessible-name.ts +73 -0
  28. package/src/lib/page-snapshot/capture/budget.ts +95 -0
  29. package/src/lib/page-snapshot/capture/chrome-filter.ts +81 -0
  30. package/src/lib/page-snapshot/capture/classify.ts +111 -0
  31. package/src/lib/page-snapshot/capture/dom-utils.ts +111 -0
  32. package/src/lib/page-snapshot/capture/fold.ts +96 -0
  33. package/src/lib/page-snapshot/capture/scope.ts +169 -0
  34. package/src/lib/page-snapshot/capture/walk.ts +250 -0
  35. package/src/lib/page-snapshot/cst/__tests__/serialize.test.ts +50 -0
  36. package/src/lib/page-snapshot/cst/directives.ts +47 -0
  37. package/src/lib/page-snapshot/cst/payload.ts +50 -0
  38. package/src/lib/page-snapshot/cst/serialize.ts +84 -0
  39. package/src/lib/page-snapshot/cst/types.ts +115 -0
  40. package/src/lib/page-snapshot/engine.ts +176 -0
  41. package/src/lib/page-snapshot/index.ts +93 -0
  42. package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +72 -0
  43. package/src/lib/page-snapshot/react/PageSnapshotPreview.tsx +78 -0
  44. package/src/lib/page-snapshot/react/__tests__/PageSnapshotChip.test.tsx +54 -0
  45. package/src/lib/page-snapshot/react/__tests__/provider.test.tsx +103 -0
  46. package/src/lib/page-snapshot/react/__tests__/use-page-snapshot-toggle.test.tsx +62 -0
  47. package/src/lib/page-snapshot/react/provider.tsx +162 -0
  48. package/src/lib/page-snapshot/react/use-page-snapshot-toggle.ts +47 -0
  49. package/src/lib/page-snapshot/react/use-page-snapshot.ts +67 -0
  50. package/src/lib/page-snapshot/redaction/__tests__/audit.test.ts +25 -0
  51. package/src/lib/page-snapshot/redaction/__tests__/heuristics.test.ts +73 -0
  52. package/src/lib/page-snapshot/redaction/__tests__/luhn.test.ts +26 -0
  53. package/src/lib/page-snapshot/redaction/__tests__/patterns.test.ts +60 -0
  54. package/src/lib/page-snapshot/redaction/audit.ts +58 -0
  55. package/src/lib/page-snapshot/redaction/heuristics.ts +75 -0
  56. package/src/lib/page-snapshot/redaction/index.ts +75 -0
  57. package/src/lib/page-snapshot/redaction/luhn.ts +25 -0
  58. package/src/lib/page-snapshot/redaction/patterns.ts +111 -0
  59. package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +24 -0
  60. package/src/lib/page-snapshot/refs/registry.ts +46 -0
  61. package/src/lib/page-snapshot/staleness/__tests__/hash.test.ts +34 -0
  62. package/src/lib/page-snapshot/staleness/hash.ts +20 -0
  63. package/src/lib/page-snapshot/tokens.ts +15 -0
  64. package/src/tools/AudioPlayer/context/PlayerProvider.tsx +13 -14
  65. package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +55 -6
  66. package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +2 -5
  67. package/src/tools/Chat/README.md +277 -39
  68. package/src/tools/Chat/composer/Composer.tsx +471 -0
  69. package/src/tools/Chat/composer/ComposerActionBar.tsx +65 -0
  70. package/src/tools/Chat/composer/ComposerBanner.tsx +128 -0
  71. package/src/tools/Chat/composer/ComposerButton.tsx +64 -0
  72. package/src/tools/Chat/composer/ComposerFooter.tsx +90 -0
  73. package/src/tools/Chat/composer/ComposerMenuButton.tsx +62 -0
  74. package/src/tools/Chat/composer/ComposerModelPicker.tsx +104 -0
  75. package/src/tools/Chat/composer/ComposerRichTextarea.tsx +88 -0
  76. package/src/tools/Chat/composer/ComposerToolPill.tsx +95 -0
  77. package/src/tools/Chat/composer/index.ts +45 -0
  78. package/src/tools/Chat/composer/size-context.tsx +26 -0
  79. package/src/tools/Chat/composer/types.ts +143 -0
  80. package/src/tools/Chat/composer/useComposerActions.tsx +164 -0
  81. package/src/tools/Chat/context/ChatProvider.tsx +54 -3
  82. package/src/tools/Chat/core/__tests__/metadata.test.ts +69 -0
  83. package/src/tools/Chat/core/index.ts +23 -1
  84. package/src/tools/Chat/core/markdown.ts +1 -1
  85. package/src/tools/Chat/core/metadata.ts +47 -0
  86. package/src/tools/Chat/core/payload-dispatch.ts +1 -1
  87. package/src/tools/Chat/core/transport/http.ts +71 -32
  88. package/src/tools/Chat/core/transport/sse.ts +18 -10
  89. package/src/tools/Chat/highlight/HighlightOverlay.tsx +101 -0
  90. package/src/tools/Chat/highlight/README.md +103 -0
  91. package/src/tools/Chat/highlight/SpotlightCanvas.tsx +153 -0
  92. package/src/tools/Chat/highlight/__tests__/HighlightOverlay.test.tsx +112 -0
  93. package/src/tools/Chat/highlight/__tests__/resolveRef.test.ts +55 -0
  94. package/src/tools/Chat/highlight/index.ts +21 -0
  95. package/src/tools/Chat/highlight/resolveRef.ts +42 -0
  96. package/src/tools/Chat/highlight/types.ts +49 -0
  97. package/src/tools/Chat/highlight/useHighlightTargets.ts +128 -0
  98. package/src/tools/Chat/hooks/index.ts +0 -5
  99. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +28 -47
  100. package/src/tools/Chat/hooks/useChat.ts +47 -14
  101. package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
  102. package/src/tools/Chat/hooks/useChatLayout.ts +1 -1
  103. package/src/tools/Chat/hooks/useStreamEndFocus.ts +54 -0
  104. package/src/tools/Chat/index.ts +25 -219
  105. package/src/tools/Chat/launcher/ChatDock.tsx +1 -1
  106. package/src/tools/Chat/launcher/ChatLauncher.tsx +1 -1
  107. package/src/tools/Chat/launcher/{ChatHeader.tsx → header/ChatHeader.tsx} +24 -11
  108. package/src/tools/Chat/launcher/{ChatHeaderActionButton.tsx → header/ChatHeaderActionButton.tsx} +34 -3
  109. package/src/tools/Chat/launcher/{ChatHeaderLanguageButton.tsx → header/ChatHeaderLanguageButton.tsx} +2 -2
  110. package/src/tools/Chat/launcher/{ChatHeaderModeToggle.tsx → header/ChatHeaderModeToggle.tsx} +1 -1
  111. package/src/tools/Chat/launcher/{ChatHeaderResetButton.tsx → header/ChatHeaderResetButton.tsx} +2 -1
  112. package/src/tools/Chat/launcher/{HeaderSlots.tsx → header/HeaderSlots.tsx} +3 -3
  113. package/src/tools/Chat/launcher/header/index.ts +26 -0
  114. package/src/tools/Chat/launcher/index.ts +3 -10
  115. package/src/tools/Chat/lazy.tsx +38 -284
  116. package/src/tools/Chat/{components → messages}/MessageBubble.tsx +58 -5
  117. package/src/tools/Chat/{components → messages}/MessageList.tsx +8 -25
  118. package/src/tools/Chat/messages/blocks/MessageBlocks.tsx +131 -0
  119. package/src/tools/Chat/messages/blocks/builtin.tsx +91 -0
  120. package/src/tools/Chat/messages/blocks/index.ts +12 -0
  121. package/src/tools/Chat/messages/blocks/registry.tsx +42 -0
  122. package/src/tools/Chat/messages/blocks/renderers/AudioBlock.tsx +20 -0
  123. package/src/tools/Chat/messages/blocks/renderers/CodeBlock.tsx +19 -0
  124. package/src/tools/Chat/messages/blocks/renderers/GalleryBlock.tsx +26 -0
  125. package/src/tools/Chat/messages/blocks/renderers/ImageBlock.tsx +27 -0
  126. package/src/tools/Chat/messages/blocks/renderers/JsonBlock.tsx +12 -0
  127. package/src/tools/Chat/messages/blocks/renderers/LottieBlock.tsx +11 -0
  128. package/src/tools/Chat/messages/blocks/renderers/MapBlock.tsx +36 -0
  129. package/src/tools/Chat/messages/blocks/renderers/MermaidBlock.tsx +11 -0
  130. package/src/tools/Chat/messages/blocks/renderers/VideoBlock.tsx +24 -0
  131. package/src/tools/Chat/messages/blocks/renderers/types.ts +8 -0
  132. package/src/tools/Chat/{components → messages}/index.ts +11 -5
  133. package/src/tools/Chat/public.ts +212 -0
  134. package/src/tools/Chat/shell/ChatRoot.tsx +345 -0
  135. package/src/tools/Chat/{components → shell}/EmptyState.tsx +4 -2
  136. package/src/tools/Chat/shell/index.ts +15 -0
  137. package/src/tools/Chat/types/block.ts +120 -0
  138. package/src/tools/Chat/types/config.ts +0 -5
  139. package/src/tools/Chat/types/index.ts +17 -0
  140. package/src/tools/Chat/types/message.ts +3 -0
  141. package/src/tools/Chat/utils/index.ts +4 -0
  142. package/src/tools/CodeEditor/README.md +4 -6
  143. package/src/tools/CodeEditor/components/DiffEditor.tsx +48 -13
  144. package/src/tools/CodeEditor/components/Editor.tsx +96 -44
  145. package/src/tools/CodeEditor/context/EditorProvider.tsx +34 -17
  146. package/src/tools/CodeEditor/hooks/useEditorTheme.ts +92 -99
  147. package/src/tools/CodeEditor/hooks/useMonaco.ts +37 -22
  148. package/src/tools/CodeEditor/lazy.tsx +6 -0
  149. package/src/tools/CodeEditor/lib/index.ts +1 -1
  150. package/src/tools/CodeEditor/lib/themes.ts +3 -39
  151. package/src/tools/CronScheduler/CronScheduler.client.tsx +230 -61
  152. package/src/tools/CronScheduler/components/CustomInput.tsx +21 -4
  153. package/src/tools/CronScheduler/components/DayChips.tsx +13 -11
  154. package/src/tools/CronScheduler/components/MonthDayGrid.tsx +4 -4
  155. package/src/tools/CronScheduler/components/SchedulePreview.tsx +7 -3
  156. package/src/tools/CronScheduler/components/TimeSelector.tsx +1 -1
  157. package/src/tools/CronScheduler/index.tsx +1 -1
  158. package/src/tools/CronScheduler/types/index.ts +8 -3
  159. package/src/tools/CronScheduler/utils/cron-humanize.ts +61 -16
  160. package/src/tools/CronScheduler/utils/cron-parser.ts +13 -4
  161. package/src/tools/FileIcon/FileIcon.tsx +24 -39
  162. package/src/tools/FileIcon/get-file-icon.ts +73 -0
  163. package/src/tools/FileIcon/icons/icon-data.ts +399 -0
  164. package/src/tools/FileIcon/index.ts +4 -0
  165. package/src/tools/FileIcon/loader.ts +17 -35
  166. package/src/tools/FileIcon/specialFolders.ts +18 -0
  167. package/src/tools/Gallery/components/lightbox/GalleryLightbox.tsx +112 -35
  168. package/src/tools/Gallery/components/media/GalleryVideo.tsx +21 -2
  169. package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +11 -1
  170. package/src/tools/Gallery/hooks/usePreloadImages.ts +54 -7
  171. package/src/tools/ImageViewer/components/ImageInfo.tsx +12 -1
  172. package/src/tools/ImageViewer/components/ImageToolbar.tsx +51 -43
  173. package/src/tools/ImageViewer/components/ImageViewer.tsx +96 -24
  174. package/src/tools/ImageViewer/hooks/useImageLoading.ts +13 -0
  175. package/src/tools/ImageViewer/utils/constants.ts +3 -0
  176. package/src/tools/ImageViewer/utils/index.ts +1 -0
  177. package/src/tools/JsonForm/JsonSchemaForm.tsx +4 -1
  178. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +5 -3
  179. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +7 -4
  180. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +3 -1
  181. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +23 -3
  182. package/src/tools/JsonForm/widgets/ColorWidget.tsx +20 -12
  183. package/src/tools/JsonForm/widgets/NumberWidget.tsx +14 -9
  184. package/src/tools/JsonForm/widgets/RadioWidget.tsx +78 -0
  185. package/src/tools/JsonForm/widgets/SelectWidget.tsx +1 -0
  186. package/src/tools/JsonForm/widgets/SliderWidget.tsx +7 -4
  187. package/src/tools/JsonForm/widgets/TextWidget.tsx +41 -17
  188. package/src/tools/JsonForm/widgets/index.ts +1 -0
  189. package/src/tools/JsonTree/components/JsonContent.tsx +115 -40
  190. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +177 -72
  191. package/src/tools/LottiePlayer/index.tsx +14 -4
  192. package/src/tools/LottiePlayer/lazy.tsx +11 -3
  193. package/src/tools/LottiePlayer/types.ts +31 -1
  194. package/src/tools/LottiePlayer/useLottie.ts +32 -9
  195. package/src/tools/LottiePlayer/usePrefersReducedMotion.ts +46 -0
  196. package/src/tools/Map/components/LayerSwitcher.tsx +54 -21
  197. package/src/tools/Map/components/MapCluster.tsx +28 -21
  198. package/src/tools/Map/components/MapContainer.tsx +11 -4
  199. package/src/tools/Map/components/MapLegend.tsx +46 -15
  200. package/src/tools/Map/components/MapMarker.tsx +31 -2
  201. package/src/tools/Map/hooks/useMapEvents.ts +64 -105
  202. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +61 -6
  203. package/src/tools/MarkdownEditor/MentionList.tsx +37 -4
  204. package/src/tools/MarkdownEditor/createMentionSuggestion.ts +11 -0
  205. package/src/tools/MarkdownEditor/lazy.tsx +32 -7
  206. package/src/tools/MarkdownEditor/styles.css +13 -0
  207. package/src/tools/MarkdownMessage/CodeBlock.tsx +40 -17
  208. package/src/tools/MarkdownMessage/MarkdownMessage.tsx +26 -6
  209. package/src/tools/MarkdownMessage/components.tsx +22 -9
  210. package/src/tools/MarkdownMessage/types.ts +24 -1
  211. package/src/tools/Mermaid/Mermaid.client.tsx +27 -5
  212. package/src/tools/Mermaid/components/MermaidErrorPanel.tsx +31 -0
  213. package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +14 -17
  214. package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +264 -168
  215. package/src/tools/Mermaid/hooks/useMermaidValidation.ts +76 -10
  216. package/src/tools/Mermaid/index.tsx +6 -0
  217. package/src/tools/Mermaid/utils/mermaid-helpers.ts +141 -18
  218. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +11 -1
  219. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +49 -20
  220. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +7 -0
  221. package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +7 -4
  222. package/src/tools/OpenapiViewer/constants.ts +3 -0
  223. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +73 -11
  224. package/src/tools/OpenapiViewer/utils/schemaExport.ts +26 -6
  225. package/src/tools/PrettyCode/PrettyCode.client.tsx +23 -16
  226. package/src/tools/PrettyCode/lazy.tsx +1 -1
  227. package/src/tools/SpeechRecognition/README.md +1 -1
  228. package/src/tools/SpeechRecognition/__tests__/language.test.ts +9 -3
  229. package/src/tools/SpeechRecognition/components/RecordingPulse.tsx +59 -0
  230. package/src/tools/SpeechRecognition/components/index.ts +2 -0
  231. package/src/tools/SpeechRecognition/core/engine/external.ts +24 -7
  232. package/src/tools/SpeechRecognition/core/language.ts +23 -6
  233. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +36 -5
  234. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +18 -11
  235. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +94 -26
  236. package/src/tools/SpeechRecognition/widgets/index.ts +1 -1
  237. package/src/tools/Tree/README.md +4 -8
  238. package/src/tools/Tree/TreeRoot.tsx +22 -10
  239. package/src/tools/Tree/components/TreeContent.tsx +24 -4
  240. package/src/tools/Tree/components/TreeLabel.tsx +8 -2
  241. package/src/tools/Tree/components/TreeRow.tsx +16 -6
  242. package/src/tools/Tree/data/flatten.ts +10 -4
  243. package/src/tools/Tree/types.ts +4 -0
  244. package/src/tools/Uploader/components/UploadAddButton.tsx +29 -6
  245. package/src/tools/Uploader/components/UploadDropzone.tsx +63 -7
  246. package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +19 -5
  247. package/src/tools/Uploader/components/UploadPreviewItem.tsx +47 -17
  248. package/src/tools/Uploader/components/UploadPreviewList.tsx +24 -12
  249. package/src/tools/Uploader/utils/formatters.ts +8 -3
  250. package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +1 -0
  251. package/src/tools/VideoPlayer/canvas/{jsx.d.ts → jsx-augmentation.ts} +12 -19
  252. package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +1 -0
  253. package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +1 -0
  254. package/src/tools/VideoPlayer/parts/fullscreen.tsx +1 -1
  255. package/src/tools/VideoPlayer/parts/pip.tsx +1 -1
  256. package/src/tools/VideoPlayer/parts/playback-rate.tsx +1 -1
  257. package/src/tools/VideoPlayer/parts/seek-bar.tsx +2 -2
  258. package/src/tools/VideoPlayer/parts/volume.tsx +2 -2
  259. package/src/tools/index.ts +2 -1
  260. package/src/tools/Chat/components/AudioToggle.tsx +0 -78
  261. package/src/tools/Chat/components/ChatRoot.tsx +0 -305
  262. package/src/tools/Chat/components/Composer.tsx +0 -216
  263. package/src/tools/Chat/hooks/useChatScroll.ts +0 -145
  264. package/src/tools/Chat/types.ts +0 -9
  265. package/src/tools/JsonTree/components/JsonToolbar.tsx +0 -95
  266. package/src/tools/JsonTree/hooks/useElementCorner.ts +0 -84
  267. package/src/tools/JsonTree/hooks/useNavbarHeight.ts +0 -83
  268. package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +0 -121
  269. package/src/tools/Tour/README.md +0 -373
  270. package/src/tools/Tour/components/Tour.tsx +0 -12
  271. package/src/tools/Tour/components/TourContent.tsx +0 -171
  272. package/src/tools/Tour/components/TourNavigation.tsx +0 -77
  273. package/src/tools/Tour/components/TourProgress.tsx +0 -88
  274. package/src/tools/Tour/components/TourSpotlight.tsx +0 -199
  275. package/src/tools/Tour/components/index.ts +0 -5
  276. package/src/tools/Tour/context/TourContext.ts +0 -19
  277. package/src/tools/Tour/context/TourProvider.tsx +0 -292
  278. package/src/tools/Tour/context/index.ts +0 -2
  279. package/src/tools/Tour/hooks/index.ts +0 -3
  280. package/src/tools/Tour/hooks/useKeyboardNavigation.ts +0 -59
  281. package/src/tools/Tour/hooks/useStepTarget.ts +0 -121
  282. package/src/tools/Tour/hooks/useTour.ts +0 -42
  283. package/src/tools/Tour/index.ts +0 -38
  284. package/src/tools/Tour/types/index.ts +0 -224
  285. package/src/tools/Tour/utils/dom.ts +0 -98
  286. package/src/tools/Tour/utils/index.ts +0 -3
  287. package/src/tools/Tour/utils/logger.ts +0 -3
  288. package/src/tools/Tour/utils/scrollIntoView.ts +0 -24
  289. /package/src/tools/Chat/{config.ts → constants.ts} +0 -0
  290. /package/src/tools/Chat/launcher/{ChatHeaderAudioToggle.tsx → header/ChatHeaderAudioToggle.tsx} +0 -0
  291. /package/src/tools/Chat/{components → messages}/Attachments.tsx +0 -0
  292. /package/src/tools/Chat/{components → messages}/JumpToLatest.tsx +0 -0
  293. /package/src/tools/Chat/{components → messages}/MessageActions.tsx +0 -0
  294. /package/src/tools/Chat/{components → messages}/Sources.tsx +0 -0
  295. /package/src/tools/Chat/{components → messages}/StreamingIndicator.tsx +0 -0
  296. /package/src/tools/Chat/{components → messages}/ToolCalls.tsx +0 -0
  297. /package/src/tools/Chat/{components → shell}/ErrorBanner.tsx +0 -0
@@ -0,0 +1,62 @@
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 { PageSnapshotProvider } from '../provider';
7
+ import {
8
+ usePageSnapshotToggle,
9
+ type PageSnapshotToggle,
10
+ } from '../use-page-snapshot-toggle';
11
+
12
+ (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
13
+
14
+ let container: HTMLDivElement;
15
+ let root: Root;
16
+ let toggle: PageSnapshotToggle | null = null;
17
+
18
+ function Probe() {
19
+ toggle = usePageSnapshotToggle();
20
+ return null;
21
+ }
22
+
23
+ beforeEach(() => {
24
+ container = document.createElement('div');
25
+ document.body.appendChild(container);
26
+ root = createRoot(container);
27
+ });
28
+
29
+ afterEach(() => {
30
+ act(() => root.unmount());
31
+ container.remove();
32
+ toggle = null;
33
+ });
34
+
35
+ async function render() {
36
+ await act(async () => {
37
+ root.render(
38
+ createElement(PageSnapshotProvider, {
39
+ children: createElement(Probe),
40
+ }),
41
+ );
42
+ });
43
+ }
44
+
45
+ describe('usePageSnapshotToggle', () => {
46
+ it('starts not pressed', async () => {
47
+ await render();
48
+ expect(toggle!.pressed).toBe(false);
49
+ expect(toggle!.label).toMatch(/share/i);
50
+ });
51
+
52
+ it('toggles the opt-in on and off', async () => {
53
+ await render();
54
+
55
+ await act(async () => toggle!.toggle());
56
+ expect(toggle!.pressed).toBe(true);
57
+ expect(toggle!.label).toMatch(/stop sharing/i);
58
+
59
+ await act(async () => toggle!.toggle());
60
+ expect(toggle!.pressed).toBe(false);
61
+ });
62
+ });
@@ -0,0 +1,162 @@
1
+ 'use client';
2
+
3
+ import {
4
+ type ReactNode,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
11
+
12
+ import { useLocationProperty } from '@djangocfg/ui-core/hooks';
13
+
14
+ import type { CaptureEngineOptions, CaptureResult } from '../engine';
15
+ import { PageSnapshotEngine } from '../engine';
16
+ import {
17
+ PageSnapshotContext,
18
+ type PageSnapshotContextValue,
19
+ } from './use-page-snapshot';
20
+
21
+ /** Props for `PageSnapshotProvider`. */
22
+ export interface PageSnapshotProviderProps {
23
+ /** Engine configuration. */
24
+ config?: CaptureEngineOptions;
25
+ /**
26
+ * Start opted-in. Default false — the user must explicitly enable
27
+ * sharing page context.
28
+ */
29
+ defaultLinked?: boolean;
30
+ children: ReactNode;
31
+ }
32
+
33
+ /**
34
+ * Provides the page-snapshot capability to the chat.
35
+ *
36
+ * - Constructs the engine lazily, browser-only (hydration-safe).
37
+ * - Holds the opt-in state.
38
+ * - `getChatMetadata` is the contributor handed to
39
+ * `ChatProvider.getDynamicMetadata`: it captures a fresh snapshot at
40
+ * send time (capture-on-submit) and returns `{ pageContext }`.
41
+ * - Auto-captures on route change; tracks staleness via content hash.
42
+ */
43
+ export function PageSnapshotProvider({
44
+ config,
45
+ defaultLinked = false,
46
+ children,
47
+ }: PageSnapshotProviderProps) {
48
+ const [isLinked, setIsLinked] = useState(defaultLinked);
49
+ const [isStale, setIsStale] = useState(false);
50
+ const [lastSnapshot, setLastSnapshot] = useState<CaptureResult | null>(null);
51
+
52
+ // Engine is browser-only. Construct it once, lazily, after mount —
53
+ // a ref (not state) since it never needs to trigger a re-render.
54
+ const engineRef = useRef<PageSnapshotEngine | null>(null);
55
+ const getEngine = useCallback((): PageSnapshotEngine | null => {
56
+ if (typeof window === 'undefined') return null;
57
+ if (!engineRef.current) {
58
+ engineRef.current = new PageSnapshotEngine(config);
59
+ }
60
+ return engineRef.current;
61
+ }, [config]);
62
+
63
+ // The latest content hash — used to decide if the page changed.
64
+ const lastHashRef = useRef<string | null>(null);
65
+
66
+ /**
67
+ * Run a capture; on success update lastSnapshot + hash + staleness.
68
+ *
69
+ * Capture is synchronous (a DOM walk, single-digit ms) so there is no
70
+ * `isProcessing` flag to toggle around it — that would only add two
71
+ * needless re-renders.
72
+ */
73
+ const runCapture = useCallback(
74
+ (track: boolean): CaptureResult | null => {
75
+ const engine = getEngine();
76
+ if (!engine) return null;
77
+ try {
78
+ const result = engine.capture();
79
+ if (track) {
80
+ setLastSnapshot(result);
81
+ lastHashRef.current = result.payload.metadata.contentHash;
82
+ setIsStale(false);
83
+ }
84
+ return result;
85
+ } catch {
86
+ // Capture must never break the chat — fail soft.
87
+ return null;
88
+ }
89
+ },
90
+ [getEngine],
91
+ );
92
+
93
+ const capture = useCallback((): CaptureResult | null => {
94
+ if (!isLinked) return null;
95
+ return runCapture(true);
96
+ }, [isLinked, runCapture]);
97
+
98
+ const generatePreview = useCallback(
99
+ (): CaptureResult | null => runCapture(false),
100
+ [runCapture],
101
+ );
102
+
103
+ const refresh = useCallback(() => {
104
+ runCapture(true);
105
+ }, [runCapture]);
106
+
107
+ /**
108
+ * The chat metadata contributor. Captures fresh at call time — the
109
+ * chat invokes this synchronously at send (capture-on-submit).
110
+ */
111
+ const getChatMetadata = useCallback((): Record<string, unknown> | undefined => {
112
+ if (!isLinked) return undefined;
113
+ const result = capture();
114
+ if (!result) return undefined;
115
+ return { pageContext: result.payload };
116
+ }, [isLinked, capture]);
117
+
118
+ // Reactive pathname from ui-core's router abstraction. Framework-
119
+ // agnostic (History API under the hood) — no next/navigation
120
+ // dependency, works in Next / Vite / CRA / Electron alike.
121
+ const pathname = useLocationProperty(
122
+ () => window.location.pathname,
123
+ () => '/',
124
+ );
125
+
126
+ // Route change → auto-capture a fresh snapshot (capture, not send).
127
+ // Also covers the initial opt-in: when `isLinked` flips true this
128
+ // effect runs and populates `lastSnapshot`.
129
+ useEffect(() => {
130
+ if (!isLinked) return;
131
+ runCapture(true);
132
+ // `pathname` in deps: a new route re-captures automatically.
133
+ }, [isLinked, pathname, runCapture]);
134
+
135
+ const value = useMemo<PageSnapshotContextValue>(
136
+ () => ({
137
+ isLinked,
138
+ setIsLinked,
139
+ capture,
140
+ generatePreview,
141
+ getChatMetadata,
142
+ lastSnapshot,
143
+ isStale,
144
+ refresh,
145
+ }),
146
+ [
147
+ isLinked,
148
+ capture,
149
+ generatePreview,
150
+ getChatMetadata,
151
+ lastSnapshot,
152
+ isStale,
153
+ refresh,
154
+ ],
155
+ );
156
+
157
+ return (
158
+ <PageSnapshotContext.Provider value={value}>
159
+ {children}
160
+ </PageSnapshotContext.Provider>
161
+ );
162
+ }
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+
5
+ import { usePageSnapshot } from './use-page-snapshot';
6
+
7
+ /**
8
+ * The opt-in toggle's current state and click handler.
9
+ *
10
+ * Deliberately framework-/component-agnostic: it returns plain values,
11
+ * not a `ComposerAction`. The host wires these into whatever toggle
12
+ * control its composer uses — this keeps `page-snapshot` decoupled from
13
+ * the chat's component types.
14
+ */
15
+ export interface PageSnapshotToggle {
16
+ /** Whether page-context sharing is currently on. */
17
+ pressed: boolean;
18
+ /** Flip the opt-in. */
19
+ toggle: () => void;
20
+ /** Suggested control label / tooltip — reflects the current state. */
21
+ label: string;
22
+ }
23
+
24
+ /**
25
+ * Drive an opt-in toggle for page-context sharing.
26
+ *
27
+ * @example
28
+ * const snap = usePageSnapshotToggle();
29
+ * // feed into a composer action:
30
+ * { id: 'page-context', icon: <MonitorUp />, label: snap.label,
31
+ * pressed: snap.pressed, onClick: snap.toggle }
32
+ */
33
+ export function usePageSnapshotToggle(): PageSnapshotToggle {
34
+ const { isLinked, setIsLinked } = usePageSnapshot();
35
+
36
+ const toggle = useCallback(() => {
37
+ setIsLinked(!isLinked);
38
+ }, [isLinked, setIsLinked]);
39
+
40
+ return {
41
+ pressed: isLinked,
42
+ toggle,
43
+ label: isLinked
44
+ ? 'Stop sharing page context'
45
+ : 'Share page context with the assistant',
46
+ };
47
+ }
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext } from 'react';
4
+
5
+ import type { CaptureResult } from '../engine';
6
+
7
+ /**
8
+ * The page-snapshot context value.
9
+ *
10
+ * Exposes the opt-in state, capture, staleness, and a `getChatMetadata`
11
+ * contributor the host passes to `ChatProvider.getDynamicMetadata`.
12
+ */
13
+ export interface PageSnapshotContextValue {
14
+ /** Whether the user has opted in to sharing page context. */
15
+ isLinked: boolean;
16
+ /** Toggle the opt-in. */
17
+ setIsLinked: (linked: boolean) => void;
18
+
19
+ /**
20
+ * Capture a snapshot for sending. Returns null when not linked, when
21
+ * not in a browser, or when capture fails. Updates `lastSnapshot` and
22
+ * clears the stale flag.
23
+ */
24
+ capture: () => CaptureResult | null;
25
+
26
+ /**
27
+ * Capture a snapshot for preview only — ignores the opt-in gate and
28
+ * does not update staleness state. Drives the preview drawer.
29
+ */
30
+ generatePreview: () => CaptureResult | null;
31
+
32
+ /**
33
+ * The chat metadata contributor. Returns `{ pageContext }` when
34
+ * linked, `undefined` otherwise. Passed to
35
+ * `ChatProvider.getDynamicMetadata` — invoked at send time, so it
36
+ * captures a fresh snapshot per message (capture-on-submit).
37
+ */
38
+ getChatMetadata: () => Record<string, unknown> | undefined;
39
+
40
+ /** The most recent captured snapshot, if any. */
41
+ lastSnapshot: CaptureResult | null;
42
+
43
+ /** Whether the captured context is stale vs the current page. */
44
+ isStale: boolean;
45
+
46
+ /** Force a fresh capture and clear staleness (e.g. chip "refresh"). */
47
+ refresh: () => void;
48
+ }
49
+
50
+ /** Internal context — consumed via `usePageSnapshot`. */
51
+ export const PageSnapshotContext =
52
+ createContext<PageSnapshotContextValue | null>(null);
53
+
54
+ /**
55
+ * Access the page-snapshot context.
56
+ *
57
+ * Must be used within a `PageSnapshotProvider`.
58
+ */
59
+ export function usePageSnapshot(): PageSnapshotContextValue {
60
+ const ctx = useContext(PageSnapshotContext);
61
+ if (!ctx) {
62
+ throw new Error(
63
+ 'usePageSnapshot must be used within a PageSnapshotProvider.',
64
+ );
65
+ }
66
+ return ctx;
67
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { RedactionAuditor } from '../audit';
4
+
5
+ describe('RedactionAuditor', () => {
6
+ it('counts scanned elements and redactions', () => {
7
+ const auditor = new RedactionAuditor();
8
+ auditor.markScanned();
9
+ auditor.markScanned();
10
+ auditor.log({ elementRole: 'input', triggerReason: 'heuristic' });
11
+
12
+ const report = auditor.report();
13
+ expect(report.totalElementsScanned).toBe(2);
14
+ expect(report.totalRedactedCount).toBe(1);
15
+ expect(report.entries[0].triggerReason).toBe('heuristic');
16
+ expect(auditor.redactedCount).toBe(1);
17
+ });
18
+
19
+ it('starts empty', () => {
20
+ const report = new RedactionAuditor().report();
21
+ expect(report.totalElementsScanned).toBe(0);
22
+ expect(report.totalRedactedCount).toBe(0);
23
+ expect(report.entries).toEqual([]);
24
+ });
25
+ });
@@ -0,0 +1,73 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
+
4
+ import {
5
+ hasIncludeAnnotation,
6
+ hasRedactAnnotation,
7
+ heuristicRedactKind,
8
+ } from '../heuristics';
9
+
10
+ afterEach(() => {
11
+ document.body.innerHTML = '';
12
+ });
13
+
14
+ function el(html: string): HTMLElement {
15
+ document.body.innerHTML = html;
16
+ return document.body.firstElementChild as HTMLElement;
17
+ }
18
+
19
+ describe('heuristicRedactKind', () => {
20
+ it('flags a password input', () => {
21
+ expect(heuristicRedactKind(el('<input type="password" />'))).toBe(
22
+ 'password',
23
+ );
24
+ });
25
+
26
+ it('flags fields named like secrets', () => {
27
+ expect(heuristicRedactKind(el('<input name="api_key" />'))).toBe(
28
+ 'sensitive-name',
29
+ );
30
+ expect(heuristicRedactKind(el('<input id="stripe-secret" />'))).toBe(
31
+ 'sensitive-name',
32
+ );
33
+ expect(heuristicRedactKind(el('<input class="cvv-field" />'))).toBe(
34
+ 'sensitive-name',
35
+ );
36
+ });
37
+
38
+ it('flags sensitive autocomplete values', () => {
39
+ expect(
40
+ heuristicRedactKind(el('<input autocomplete="current-password" />')),
41
+ ).toBe('autocomplete');
42
+ expect(
43
+ heuristicRedactKind(el('<input autocomplete="cc-number" />')),
44
+ ).toBe('autocomplete');
45
+ });
46
+
47
+ it('does not flag a normal text field', () => {
48
+ expect(heuristicRedactKind(el('<input name="company" />'))).toBeNull();
49
+ expect(heuristicRedactKind(el('<input id="search" />'))).toBeNull();
50
+ });
51
+ });
52
+
53
+ describe('hasRedactAnnotation', () => {
54
+ it('detects data-ai-redact on the element', () => {
55
+ expect(hasRedactAnnotation(el('<div data-ai-redact></div>'))).toBe(true);
56
+ });
57
+
58
+ it('detects data-ai-redact on an ancestor', () => {
59
+ document.body.innerHTML = `<div data-ai-redact><span id="c">x</span></div>`;
60
+ expect(hasRedactAnnotation(document.getElementById('c')!)).toBe(true);
61
+ });
62
+
63
+ it('is false on an un-annotated element', () => {
64
+ expect(hasRedactAnnotation(el('<div></div>'))).toBe(false);
65
+ });
66
+ });
67
+
68
+ describe('hasIncludeAnnotation', () => {
69
+ it('detects data-ai-include', () => {
70
+ expect(hasIncludeAnnotation(el('<input data-ai-include />'))).toBe(true);
71
+ expect(hasIncludeAnnotation(el('<input />'))).toBe(false);
72
+ });
73
+ });
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { validateLuhn } from '../luhn';
4
+
5
+ describe('validateLuhn', () => {
6
+ it('accepts known-valid card numbers', () => {
7
+ expect(validateLuhn('4242424242424242')).toBe(true); // Visa test
8
+ expect(validateLuhn('4111111111111111')).toBe(true); // Visa test
9
+ expect(validateLuhn('5555555555554444')).toBe(true); // Mastercard test
10
+ });
11
+
12
+ it('accepts valid numbers with separators', () => {
13
+ expect(validateLuhn('4242 4242 4242 4242')).toBe(true);
14
+ expect(validateLuhn('4242-4242-4242-4242')).toBe(true);
15
+ });
16
+
17
+ it('rejects checksum-invalid numbers', () => {
18
+ expect(validateLuhn('4242424242424243')).toBe(false);
19
+ expect(validateLuhn('1234567812345678')).toBe(false);
20
+ });
21
+
22
+ it('rejects too-short / too-long digit runs', () => {
23
+ expect(validateLuhn('1234')).toBe(false);
24
+ expect(validateLuhn('12345678901234567890')).toBe(false);
25
+ });
26
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { redactPatterns } from '../patterns';
4
+
5
+ describe('redactPatterns', () => {
6
+ it('redacts a JWT', () => {
7
+ const jwt =
8
+ 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHDpoEx';
9
+ const { value, matched } = redactPatterns(`token is ${jwt} ok`);
10
+ expect(matched).toContain('jwt');
11
+ expect(value).not.toContain('eyJhbGci');
12
+ expect(value).toContain('‹redacted:jwt›');
13
+ });
14
+
15
+ it('redacts a bearer token', () => {
16
+ const { value, matched } = redactPatterns(
17
+ 'Authorization: Bearer abcdef1234567890ABCDEF',
18
+ );
19
+ expect(matched).toContain('bearer-token');
20
+ expect(value).not.toContain('abcdef1234567890');
21
+ });
22
+
23
+ it('redacts API-key shapes', () => {
24
+ const { matched } = redactPatterns('key sk-abcdefghijklmnop1234 end');
25
+ expect(matched).toContain('api-key');
26
+ });
27
+
28
+ it('redacts an email address', () => {
29
+ const { value, matched } = redactPatterns('mail me at jane@acme.com now');
30
+ expect(matched).toContain('email');
31
+ expect(value).not.toContain('jane@acme.com');
32
+ });
33
+
34
+ it('redacts a Luhn-valid card number', () => {
35
+ const { value, matched } = redactPatterns('card 4242424242424242 charged');
36
+ expect(matched).toContain('credit-card');
37
+ expect(value).not.toContain('4242424242424242');
38
+ });
39
+
40
+ it('does NOT redact a Luhn-invalid long number (order id)', () => {
41
+ // 16 digits but not a valid card — must survive (false-positive guard).
42
+ const orderId = '1234567890123456';
43
+ const { value, matched } = redactPatterns(`order ${orderId} shipped`);
44
+ expect(matched).not.toContain('credit-card');
45
+ expect(value).toContain(orderId);
46
+ });
47
+
48
+ it('redacts a phone number with separators or +', () => {
49
+ expect(redactPatterns('call +1 (415) 555-2671 today').matched).toContain(
50
+ 'phone',
51
+ );
52
+ expect(redactPatterns('ph: 415-555-2671').matched).toContain('phone');
53
+ });
54
+
55
+ it('leaves clean text untouched', () => {
56
+ const { value, matched } = redactPatterns('just a normal sentence');
57
+ expect(matched).toEqual([]);
58
+ expect(value).toBe('just a normal sentence');
59
+ });
60
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Redaction audit — a record of what was scrubbed and why.
3
+ *
4
+ * Records only that a redaction happened — never the redacted value
5
+ * itself, so the audit trail is safe to log.
6
+ */
7
+
8
+ /** Why a value was redacted. */
9
+ export type RedactionReason = 'heuristic' | 'regex' | 'attribute';
10
+
11
+ /** One redaction event. */
12
+ export interface RedactionEntry {
13
+ /** Role/tag of the element involved. */
14
+ elementRole: string;
15
+ /** What triggered the redaction. */
16
+ triggerReason: RedactionReason;
17
+ /** Ref id, if the element had one. */
18
+ assignedRef?: string;
19
+ }
20
+
21
+ /** A full audit report for one capture. */
22
+ export interface RedactionAuditReport {
23
+ timestamp: number;
24
+ totalElementsScanned: number;
25
+ totalRedactedCount: number;
26
+ entries: RedactionEntry[];
27
+ }
28
+
29
+ /** Collects redaction events during a capture. */
30
+ export class RedactionAuditor {
31
+ private readonly entries: RedactionEntry[] = [];
32
+ private scanned = 0;
33
+
34
+ /** Record one scanned element. */
35
+ markScanned(): void {
36
+ this.scanned++;
37
+ }
38
+
39
+ /** Record one redaction. */
40
+ log(entry: RedactionEntry): void {
41
+ this.entries.push(entry);
42
+ }
43
+
44
+ /** How many redactions so far. */
45
+ get redactedCount(): number {
46
+ return this.entries.length;
47
+ }
48
+
49
+ /** Produce the final report. */
50
+ report(): RedactionAuditReport {
51
+ return {
52
+ timestamp: Date.now(),
53
+ totalElementsScanned: this.scanned,
54
+ totalRedactedCount: this.entries.length,
55
+ entries: [...this.entries],
56
+ };
57
+ }
58
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Heuristic redaction — drop a whole field's value by element shape.
3
+ *
4
+ * Where patterns.ts scrubs sensitive substrings, this decides that an
5
+ * entire field is sensitive (a password input, an element named
6
+ * `*token*`/`*secret*`) and its value must never be captured at all.
7
+ *
8
+ * Safe-by-default: an un-annotated page is still covered — heuristics
9
+ * do not depend on developers adding `data-ai-redact`.
10
+ *
11
+ */
12
+
13
+ /**
14
+ * Attribute-name / token pattern that marks a field as sensitive.
15
+ * Tested against id, name, class, autocomplete.
16
+ */
17
+ const SENSITIVE_NAME_RE =
18
+ /(^|[\s_-])(password|passwd|pwd|secret|token|api[-_]?key|apikey|auth|bearer|cvv|cvc|card[-_]?number|cc[-_]?num|ssn|sin|tax[-_]?id|private[-_]?key|stripe|client[-_]?secret|access[-_]?key)([\s_-]|$)/i;
19
+
20
+ /** autocomplete values that mark a sensitive field. */
21
+ const SENSITIVE_AUTOCOMPLETE: ReadonlySet<string> = new Set([
22
+ 'current-password',
23
+ 'new-password',
24
+ 'cc-number',
25
+ 'cc-csc',
26
+ 'one-time-code',
27
+ ]);
28
+
29
+ /** Why a heuristic fired (used for the audit reason / token kind). */
30
+ export type HeuristicKind = 'password' | 'sensitive-name' | 'autocomplete';
31
+
32
+ /**
33
+ * Decide whether an element's value must be fully redacted by shape.
34
+ * Returns the heuristic kind, or null if the element is not sensitive.
35
+ */
36
+ export function heuristicRedactKind(el: Element): HeuristicKind | null {
37
+ // A password input — never capture its value.
38
+ if (el instanceof HTMLInputElement && el.type === 'password') {
39
+ return 'password';
40
+ }
41
+
42
+ // autocomplete hint.
43
+ const autocomplete = el.getAttribute('autocomplete')?.toLowerCase();
44
+ if (autocomplete && SENSITIVE_AUTOCOMPLETE.has(autocomplete)) {
45
+ return 'autocomplete';
46
+ }
47
+
48
+ // id / name / class token pattern.
49
+ const tokens = [
50
+ el.id,
51
+ el.getAttribute('name') ?? '',
52
+ typeof el.className === 'string' ? el.className : '',
53
+ ].join(' ');
54
+ if (tokens.trim() && SENSITIVE_NAME_RE.test(tokens)) {
55
+ return 'sensitive-name';
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * Does this element (or an ancestor) carry `data-ai-redact` — an
63
+ * explicit developer instruction to drop the whole subtree.
64
+ */
65
+ export function hasRedactAnnotation(el: Element): boolean {
66
+ return el.closest('[data-ai-redact]') !== null;
67
+ }
68
+
69
+ /**
70
+ * Does this element carry `data-ai-include` — an explicit override that
71
+ * forces a heuristically-blocked but known-safe value to be kept.
72
+ */
73
+ export function hasIncludeAnnotation(el: Element): boolean {
74
+ return el.hasAttribute('data-ai-include');
75
+ }