@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.
Files changed (336) hide show
  1. package/README.md +9 -11
  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 +13 -16
  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/lazy.tsx +13 -27
  67. package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +2 -5
  68. package/src/tools/Chat/README.md +267 -39
  69. package/src/tools/Chat/composer/Composer.tsx +471 -0
  70. package/src/tools/Chat/composer/ComposerActionBar.tsx +65 -0
  71. package/src/tools/Chat/composer/ComposerBanner.tsx +128 -0
  72. package/src/tools/Chat/composer/ComposerButton.tsx +64 -0
  73. package/src/tools/Chat/composer/ComposerFooter.tsx +90 -0
  74. package/src/tools/Chat/composer/ComposerMenuButton.tsx +62 -0
  75. package/src/tools/Chat/composer/ComposerModelPicker.tsx +104 -0
  76. package/src/tools/Chat/composer/ComposerRichTextarea.tsx +88 -0
  77. package/src/tools/Chat/composer/ComposerToolPill.tsx +95 -0
  78. package/src/tools/Chat/composer/index.ts +45 -0
  79. package/src/tools/Chat/composer/size-context.tsx +26 -0
  80. package/src/tools/Chat/composer/types.ts +143 -0
  81. package/src/tools/Chat/composer/useComposerActions.tsx +164 -0
  82. package/src/tools/Chat/context/ChatProvider.tsx +54 -3
  83. package/src/tools/Chat/core/__tests__/metadata.test.ts +69 -0
  84. package/src/tools/Chat/core/index.ts +23 -1
  85. package/src/tools/Chat/core/markdown.ts +1 -1
  86. package/src/tools/Chat/core/metadata.ts +47 -0
  87. package/src/tools/Chat/core/payload-dispatch.ts +1 -1
  88. package/src/tools/Chat/core/transport/http.ts +71 -32
  89. package/src/tools/Chat/core/transport/sse.ts +18 -10
  90. package/src/tools/Chat/highlight/HighlightOverlay.tsx +101 -0
  91. package/src/tools/Chat/highlight/README.md +103 -0
  92. package/src/tools/Chat/highlight/SpotlightCanvas.tsx +153 -0
  93. package/src/tools/Chat/highlight/__tests__/HighlightOverlay.test.tsx +112 -0
  94. package/src/tools/Chat/highlight/__tests__/resolveRef.test.ts +55 -0
  95. package/src/tools/Chat/highlight/index.ts +21 -0
  96. package/src/tools/Chat/highlight/resolveRef.ts +42 -0
  97. package/src/tools/Chat/highlight/types.ts +49 -0
  98. package/src/tools/Chat/highlight/useHighlightTargets.ts +128 -0
  99. package/src/tools/Chat/hooks/index.ts +0 -5
  100. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +28 -47
  101. package/src/tools/Chat/hooks/useChat.ts +47 -14
  102. package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
  103. package/src/tools/Chat/hooks/useChatLayout.ts +1 -1
  104. package/src/tools/Chat/hooks/useStreamEndFocus.ts +54 -0
  105. package/src/tools/Chat/index.ts +25 -219
  106. package/src/tools/Chat/launcher/ChatDock.tsx +1 -1
  107. package/src/tools/Chat/launcher/ChatLauncher.tsx +1 -1
  108. package/src/tools/Chat/launcher/{ChatHeader.tsx → header/ChatHeader.tsx} +24 -11
  109. package/src/tools/Chat/launcher/{ChatHeaderActionButton.tsx → header/ChatHeaderActionButton.tsx} +34 -3
  110. package/src/tools/Chat/launcher/{ChatHeaderLanguageButton.tsx → header/ChatHeaderLanguageButton.tsx} +2 -2
  111. package/src/tools/Chat/launcher/{ChatHeaderModeToggle.tsx → header/ChatHeaderModeToggle.tsx} +1 -1
  112. package/src/tools/Chat/launcher/{ChatHeaderResetButton.tsx → header/ChatHeaderResetButton.tsx} +2 -1
  113. package/src/tools/Chat/launcher/{HeaderSlots.tsx → header/HeaderSlots.tsx} +3 -3
  114. package/src/tools/Chat/launcher/header/index.ts +26 -0
  115. package/src/tools/Chat/launcher/index.ts +3 -10
  116. package/src/tools/Chat/lazy.tsx +38 -284
  117. package/src/tools/Chat/{components → messages}/MessageBubble.tsx +58 -5
  118. package/src/tools/Chat/{components → messages}/MessageList.tsx +8 -25
  119. package/src/tools/Chat/messages/blocks/MessageBlocks.tsx +131 -0
  120. package/src/tools/Chat/messages/blocks/builtin.tsx +91 -0
  121. package/src/tools/Chat/messages/blocks/index.ts +12 -0
  122. package/src/tools/Chat/messages/blocks/registry.tsx +42 -0
  123. package/src/tools/Chat/messages/blocks/renderers/AudioBlock.tsx +20 -0
  124. package/src/tools/Chat/messages/blocks/renderers/CodeBlock.tsx +19 -0
  125. package/src/tools/Chat/messages/blocks/renderers/GalleryBlock.tsx +26 -0
  126. package/src/tools/Chat/messages/blocks/renderers/ImageBlock.tsx +27 -0
  127. package/src/tools/Chat/messages/blocks/renderers/JsonBlock.tsx +12 -0
  128. package/src/tools/Chat/messages/blocks/renderers/LottieBlock.tsx +11 -0
  129. package/src/tools/Chat/messages/blocks/renderers/MapBlock.tsx +36 -0
  130. package/src/tools/Chat/messages/blocks/renderers/MermaidBlock.tsx +11 -0
  131. package/src/tools/Chat/messages/blocks/renderers/VideoBlock.tsx +24 -0
  132. package/src/tools/Chat/messages/blocks/renderers/types.ts +8 -0
  133. package/src/tools/Chat/{components → messages}/index.ts +11 -5
  134. package/src/tools/Chat/public.ts +212 -0
  135. package/src/tools/Chat/shell/ChatRoot.tsx +326 -0
  136. package/src/tools/Chat/{components → shell}/EmptyState.tsx +4 -2
  137. package/src/tools/Chat/shell/index.ts +15 -0
  138. package/src/tools/Chat/types/block.ts +120 -0
  139. package/src/tools/Chat/types/config.ts +0 -5
  140. package/src/tools/Chat/types/index.ts +17 -0
  141. package/src/tools/Chat/types/message.ts +3 -0
  142. package/src/tools/Chat/utils/index.ts +4 -0
  143. package/src/tools/CodeEditor/README.md +4 -6
  144. package/src/tools/CodeEditor/components/DiffEditor.tsx +48 -13
  145. package/src/tools/CodeEditor/components/Editor.tsx +96 -44
  146. package/src/tools/CodeEditor/context/EditorProvider.tsx +34 -17
  147. package/src/tools/CodeEditor/hooks/useEditorTheme.ts +92 -99
  148. package/src/tools/CodeEditor/hooks/useMonaco.ts +37 -22
  149. package/src/tools/CodeEditor/lazy.tsx +6 -0
  150. package/src/tools/CodeEditor/lib/index.ts +1 -1
  151. package/src/tools/CodeEditor/lib/themes.ts +3 -39
  152. package/src/tools/CronScheduler/CronScheduler.client.tsx +230 -61
  153. package/src/tools/CronScheduler/components/CustomInput.tsx +21 -4
  154. package/src/tools/CronScheduler/components/DayChips.tsx +13 -11
  155. package/src/tools/CronScheduler/components/MonthDayGrid.tsx +4 -4
  156. package/src/tools/CronScheduler/components/SchedulePreview.tsx +7 -3
  157. package/src/tools/CronScheduler/components/TimeSelector.tsx +1 -1
  158. package/src/tools/CronScheduler/index.tsx +1 -1
  159. package/src/tools/CronScheduler/types/index.ts +8 -3
  160. package/src/tools/CronScheduler/utils/cron-humanize.ts +61 -16
  161. package/src/tools/CronScheduler/utils/cron-parser.ts +13 -4
  162. package/src/tools/FileIcon/FileIcon.tsx +24 -39
  163. package/src/tools/FileIcon/get-file-icon.ts +73 -0
  164. package/src/tools/FileIcon/icons/icon-data.ts +399 -0
  165. package/src/tools/FileIcon/index.ts +4 -0
  166. package/src/tools/FileIcon/loader.ts +17 -35
  167. package/src/tools/FileIcon/specialFolders.ts +18 -0
  168. package/src/tools/Gallery/components/lightbox/GalleryLightbox.tsx +112 -35
  169. package/src/tools/Gallery/components/media/GalleryVideo.tsx +21 -2
  170. package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +11 -1
  171. package/src/tools/Gallery/hooks/usePreloadImages.ts +54 -7
  172. package/src/tools/ImageViewer/components/ImageInfo.tsx +12 -1
  173. package/src/tools/ImageViewer/components/ImageToolbar.tsx +51 -43
  174. package/src/tools/ImageViewer/components/ImageViewer.tsx +106 -26
  175. package/src/tools/ImageViewer/hooks/useImageLoading.ts +13 -0
  176. package/src/tools/ImageViewer/utils/constants.ts +3 -0
  177. package/src/tools/ImageViewer/utils/index.ts +1 -0
  178. package/src/tools/JsonForm/JsonSchemaForm.tsx +4 -1
  179. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +5 -3
  180. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +7 -4
  181. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +3 -1
  182. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +23 -3
  183. package/src/tools/JsonForm/widgets/ColorWidget.tsx +20 -12
  184. package/src/tools/JsonForm/widgets/NumberWidget.tsx +14 -9
  185. package/src/tools/JsonForm/widgets/RadioWidget.tsx +78 -0
  186. package/src/tools/JsonForm/widgets/SelectWidget.tsx +1 -0
  187. package/src/tools/JsonForm/widgets/SliderWidget.tsx +7 -4
  188. package/src/tools/JsonForm/widgets/TextWidget.tsx +41 -17
  189. package/src/tools/JsonForm/widgets/index.ts +1 -0
  190. package/src/tools/JsonTree/components/JsonContent.tsx +115 -40
  191. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +177 -72
  192. package/src/tools/LottiePlayer/index.tsx +14 -4
  193. package/src/tools/LottiePlayer/lazy.tsx +11 -3
  194. package/src/tools/LottiePlayer/types.ts +31 -1
  195. package/src/tools/LottiePlayer/useLottie.ts +32 -9
  196. package/src/tools/LottiePlayer/usePrefersReducedMotion.ts +46 -0
  197. package/src/tools/Map/components/LayerSwitcher.tsx +54 -21
  198. package/src/tools/Map/components/MapCluster.tsx +28 -21
  199. package/src/tools/Map/components/MapContainer.tsx +11 -4
  200. package/src/tools/Map/components/MapLegend.tsx +46 -15
  201. package/src/tools/Map/components/MapMarker.tsx +31 -2
  202. package/src/tools/Map/hooks/useMapEvents.ts +64 -105
  203. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +61 -6
  204. package/src/tools/MarkdownEditor/MentionList.tsx +37 -4
  205. package/src/tools/MarkdownEditor/createMentionSuggestion.ts +11 -0
  206. package/src/tools/MarkdownEditor/lazy.tsx +32 -7
  207. package/src/tools/MarkdownEditor/styles.css +13 -0
  208. package/src/tools/MarkdownMessage/CodeBlock.tsx +40 -17
  209. package/src/tools/MarkdownMessage/MarkdownMessage.tsx +26 -6
  210. package/src/tools/MarkdownMessage/components.tsx +22 -9
  211. package/src/tools/MarkdownMessage/types.ts +24 -1
  212. package/src/tools/Mermaid/Mermaid.client.tsx +27 -5
  213. package/src/tools/Mermaid/components/MermaidErrorPanel.tsx +31 -0
  214. package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +14 -17
  215. package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +264 -168
  216. package/src/tools/Mermaid/hooks/useMermaidValidation.ts +76 -10
  217. package/src/tools/Mermaid/index.tsx +6 -0
  218. package/src/tools/Mermaid/utils/mermaid-helpers.ts +141 -18
  219. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +11 -1
  220. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +49 -20
  221. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +7 -0
  222. package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +7 -4
  223. package/src/tools/OpenapiViewer/constants.ts +3 -0
  224. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +73 -11
  225. package/src/tools/OpenapiViewer/utils/schemaExport.ts +26 -6
  226. package/src/tools/PrettyCode/PrettyCode.client.tsx +23 -16
  227. package/src/tools/PrettyCode/lazy.tsx +1 -1
  228. package/src/tools/SpeechRecognition/README.md +1 -1
  229. package/src/tools/SpeechRecognition/__tests__/language.test.ts +9 -3
  230. package/src/tools/SpeechRecognition/components/RecordingPulse.tsx +59 -0
  231. package/src/tools/SpeechRecognition/components/index.ts +2 -0
  232. package/src/tools/SpeechRecognition/core/engine/external.ts +24 -7
  233. package/src/tools/SpeechRecognition/core/language.ts +23 -6
  234. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +36 -5
  235. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +18 -11
  236. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +94 -26
  237. package/src/tools/SpeechRecognition/widgets/index.ts +1 -1
  238. package/src/tools/Tree/README.md +4 -8
  239. package/src/tools/Tree/TreeRoot.tsx +22 -10
  240. package/src/tools/Tree/components/TreeContent.tsx +24 -4
  241. package/src/tools/Tree/components/TreeLabel.tsx +8 -2
  242. package/src/tools/Tree/components/TreeRow.tsx +16 -6
  243. package/src/tools/Tree/data/flatten.ts +10 -4
  244. package/src/tools/Tree/types.ts +4 -0
  245. package/src/tools/Uploader/components/UploadAddButton.tsx +29 -6
  246. package/src/tools/Uploader/components/UploadDropzone.tsx +63 -7
  247. package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +19 -5
  248. package/src/tools/Uploader/components/UploadPreviewItem.tsx +47 -17
  249. package/src/tools/Uploader/components/UploadPreviewList.tsx +24 -12
  250. package/src/tools/Uploader/utils/formatters.ts +8 -3
  251. package/src/tools/VideoPlayer/README.md +87 -230
  252. package/src/tools/VideoPlayer/VideoPlayer.tsx +82 -0
  253. package/src/tools/VideoPlayer/canvas/canvas-dispatcher.tsx +34 -0
  254. package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +39 -0
  255. package/src/tools/VideoPlayer/canvas/iframe-canvas.tsx +33 -0
  256. package/src/tools/VideoPlayer/canvas/index.ts +12 -0
  257. package/src/tools/VideoPlayer/canvas/jsx-augmentation.ts +47 -0
  258. package/src/tools/VideoPlayer/canvas/native-canvas.tsx +38 -0
  259. package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +40 -0
  260. package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +78 -0
  261. package/src/tools/VideoPlayer/index.ts +51 -65
  262. package/src/tools/VideoPlayer/lazy.tsx +11 -54
  263. package/src/tools/VideoPlayer/parts/controls-bar.tsx +35 -0
  264. package/src/tools/VideoPlayer/parts/fullscreen.tsx +19 -0
  265. package/src/tools/VideoPlayer/parts/index.ts +15 -0
  266. package/src/tools/VideoPlayer/parts/pip.tsx +19 -0
  267. package/src/tools/VideoPlayer/parts/play-button.tsx +19 -0
  268. package/src/tools/VideoPlayer/parts/playback-rate.tsx +31 -0
  269. package/src/tools/VideoPlayer/parts/poster.tsx +3 -0
  270. package/src/tools/VideoPlayer/parts/seek-bar.tsx +26 -0
  271. package/src/tools/VideoPlayer/parts/volume.tsx +32 -0
  272. package/src/tools/VideoPlayer/styles/video-player.css +141 -0
  273. package/src/tools/VideoPlayer/types.ts +82 -0
  274. package/src/tools/VideoPlayer/utils/parse-embed-url.ts +70 -0
  275. package/src/tools/VideoPlayer/utils/vimeo-id.ts +24 -0
  276. package/src/tools/VideoPlayer/utils/youtube-id.ts +64 -0
  277. package/src/tools/index.ts +37 -29
  278. package/src/tools/Chat/components/AudioToggle.tsx +0 -78
  279. package/src/tools/Chat/components/ChatRoot.tsx +0 -305
  280. package/src/tools/Chat/components/Composer.tsx +0 -216
  281. package/src/tools/Chat/hooks/useChatScroll.ts +0 -145
  282. package/src/tools/Chat/types.ts +0 -9
  283. package/src/tools/JsonTree/components/JsonToolbar.tsx +0 -95
  284. package/src/tools/JsonTree/hooks/useElementCorner.ts +0 -84
  285. package/src/tools/JsonTree/hooks/useNavbarHeight.ts +0 -83
  286. package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +0 -121
  287. package/src/tools/Tour/README.md +0 -373
  288. package/src/tools/Tour/components/Tour.tsx +0 -12
  289. package/src/tools/Tour/components/TourContent.tsx +0 -171
  290. package/src/tools/Tour/components/TourNavigation.tsx +0 -77
  291. package/src/tools/Tour/components/TourProgress.tsx +0 -88
  292. package/src/tools/Tour/components/TourSpotlight.tsx +0 -199
  293. package/src/tools/Tour/components/index.ts +0 -5
  294. package/src/tools/Tour/context/TourContext.ts +0 -19
  295. package/src/tools/Tour/context/TourProvider.tsx +0 -292
  296. package/src/tools/Tour/context/index.ts +0 -2
  297. package/src/tools/Tour/hooks/index.ts +0 -3
  298. package/src/tools/Tour/hooks/useKeyboardNavigation.ts +0 -59
  299. package/src/tools/Tour/hooks/useStepTarget.ts +0 -121
  300. package/src/tools/Tour/hooks/useTour.ts +0 -42
  301. package/src/tools/Tour/index.ts +0 -38
  302. package/src/tools/Tour/types/index.ts +0 -224
  303. package/src/tools/Tour/utils/dom.ts +0 -98
  304. package/src/tools/Tour/utils/index.ts +0 -3
  305. package/src/tools/Tour/utils/logger.ts +0 -3
  306. package/src/tools/Tour/utils/scrollIntoView.ts +0 -24
  307. package/src/tools/VideoPlayer/components/VideoControls.tsx +0 -138
  308. package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +0 -172
  309. package/src/tools/VideoPlayer/components/VideoPlayer.tsx +0 -201
  310. package/src/tools/VideoPlayer/components/index.ts +0 -14
  311. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +0 -52
  312. package/src/tools/VideoPlayer/context/index.ts +0 -8
  313. package/src/tools/VideoPlayer/hooks/index.ts +0 -12
  314. package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +0 -71
  315. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +0 -117
  316. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +0 -284
  317. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +0 -505
  318. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +0 -397
  319. package/src/tools/VideoPlayer/providers/index.ts +0 -8
  320. package/src/tools/VideoPlayer/types/index.ts +0 -38
  321. package/src/tools/VideoPlayer/types/player.ts +0 -116
  322. package/src/tools/VideoPlayer/types/provider.ts +0 -93
  323. package/src/tools/VideoPlayer/types/sources.ts +0 -97
  324. package/src/tools/VideoPlayer/utils/debug.ts +0 -14
  325. package/src/tools/VideoPlayer/utils/fileSource.ts +0 -78
  326. package/src/tools/VideoPlayer/utils/index.ts +0 -12
  327. package/src/tools/VideoPlayer/utils/resolvers.ts +0 -75
  328. /package/src/tools/Chat/{config.ts → constants.ts} +0 -0
  329. /package/src/tools/Chat/launcher/{ChatHeaderAudioToggle.tsx → header/ChatHeaderAudioToggle.tsx} +0 -0
  330. /package/src/tools/Chat/{components → messages}/Attachments.tsx +0 -0
  331. /package/src/tools/Chat/{components → messages}/JumpToLatest.tsx +0 -0
  332. /package/src/tools/Chat/{components → messages}/MessageActions.tsx +0 -0
  333. /package/src/tools/Chat/{components → messages}/Sources.tsx +0 -0
  334. /package/src/tools/Chat/{components → messages}/StreamingIndicator.tsx +0 -0
  335. /package/src/tools/Chat/{components → messages}/ToolCalls.tsx +0 -0
  336. /package/src/tools/Chat/{components → shell}/ErrorBanner.tsx +0 -0
@@ -12,4 +12,8 @@ export type {
12
12
  FolderIconOverrides,
13
13
  } from './specialFolders';
14
14
 
15
+ export { getFileIcon, DEFAULT_FILE_ICON } from './get-file-icon';
16
+ export type { FileIconDef } from './get-file-icon';
17
+
18
+ /** @deprecated Use `getFileIcon` — resolution is now synchronous. */
15
19
  export { loadMaterialIcons, getMaterialIconsSync } from './loader';
@@ -1,47 +1,29 @@
1
1
  'use client';
2
2
 
3
3
  /**
4
- * Lazy loader for `material-file-icons`. The package is an optional dependency:
5
- * if it's installed in the consumer's tree, we render its rich VSCode-style
6
- * SVGs; if not, the loader resolves to `null` and `<FileIcon>` falls back to a
7
- * Lucide icon. Either way, ui-tools never throws or warns about a missing
8
- * package.
4
+ * Back-compat shim for the former `material-file-icons` lazy loader.
9
5
  *
10
- * Resolution is cached per-process so we don't re-import on every render.
6
+ * File icons are now resolved synchronously from a vendored static data
7
+ * module — see `./get-file-icon`. The async API is kept so existing callers
8
+ * of `loadMaterialIcons` / `getMaterialIconsSync` keep working; both resolve
9
+ * immediately to the local resolver.
10
+ *
11
+ * @deprecated Import {@link getFileIcon} from `./get-file-icon` instead.
11
12
  */
12
13
 
13
- type GetIconFn = (name: string) => { svg: string } | undefined;
14
-
15
- let cached: GetIconFn | null | undefined;
16
- let inflight: Promise<GetIconFn | null> | null = null;
14
+ import { getFileIcon } from './get-file-icon';
17
15
 
18
- export async function loadMaterialIcons(): Promise<GetIconFn | null> {
19
- if (cached !== undefined) return cached;
20
- if (inflight) return inflight;
16
+ /** Returns an inline SVG string for a file name, or `undefined`. */
17
+ type GetIconFn = (name: string) => { svg?: string } | undefined;
21
18
 
22
- inflight = (async () => {
23
- try {
24
- // Computed specifier so bundlers don't try to eagerly resolve it.
25
- const specifier = 'material-file-icons';
26
- const mod = (await import(/* @vite-ignore */ specifier)) as {
27
- getIcon?: GetIconFn;
28
- default?: { getIcon?: GetIconFn };
29
- };
30
- const fn = mod.getIcon ?? mod.default?.getIcon ?? null;
31
- cached = fn;
32
- return fn;
33
- } catch {
34
- cached = null;
35
- return null;
36
- } finally {
37
- inflight = null;
38
- }
39
- })();
19
+ const localGetIcon: GetIconFn = (name) => ({ svg: getFileIcon(name).svg });
40
20
 
41
- return inflight;
21
+ /** @deprecated Use {@link getFileIcon} — resolution is now synchronous. */
22
+ export function loadMaterialIcons(): Promise<GetIconFn> {
23
+ return Promise.resolve(localGetIcon);
42
24
  }
43
25
 
44
- /** Synchronous accessorreturns null until `loadMaterialIcons` resolves. */
45
- export function getMaterialIconsSync(): GetIconFn | null {
46
- return cached ?? null;
26
+ /** @deprecated Use {@link getFileIcon} resolution is now synchronous. */
27
+ export function getMaterialIconsSync(): GetIconFn {
28
+ return localGetIcon;
47
29
  }
@@ -2,6 +2,8 @@
2
2
 
3
3
  import {
4
4
  BookOpen,
5
+ Component,
6
+ Database,
5
7
  FlaskConical,
6
8
  Folder,
7
9
  FolderCode,
@@ -11,9 +13,11 @@ import {
11
13
  FolderOutput,
12
14
  Github,
13
15
  Image as ImageIcon,
16
+ Languages,
14
17
  Package,
15
18
  Settings,
16
19
  Terminal,
20
+ Workflow,
17
21
  type LucideIcon,
18
22
  } from 'lucide-react';
19
23
 
@@ -50,6 +54,7 @@ const SPECIAL_FOLDERS: Record<string, LucideIcon> = {
50
54
  test: FlaskConical,
51
55
  __tests__: FlaskConical,
52
56
  __test__: FlaskConical,
57
+ spec: FlaskConical,
53
58
  scripts: Terminal,
54
59
  bin: Terminal,
55
60
  config: Settings,
@@ -60,6 +65,19 @@ const SPECIAL_FOLDERS: Record<string, LucideIcon> = {
60
65
  '.git': FolderGit2,
61
66
  '.github': Github,
62
67
  '.gitlab': FolderGit2,
68
+ '.husky': Workflow,
69
+ '.circleci': Workflow,
70
+ components: Component,
71
+ component: Component,
72
+ ui: Component,
73
+ i18n: Languages,
74
+ locale: Languages,
75
+ locales: Languages,
76
+ lang: Languages,
77
+ translations: Languages,
78
+ db: Database,
79
+ database: Database,
80
+ migrations: Database,
63
81
  };
64
82
 
65
83
  export interface ResolveFolderIconOptions {
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { memo, useEffect, useCallback, useState, useMemo } from 'react'
3
+ import { memo, useEffect, useCallback, useState, useMemo, useRef } from 'react'
4
4
  import { createPortal } from 'react-dom'
5
5
  import { cn } from '@djangocfg/ui-core/lib'
6
6
  import { useAppT } from '@djangocfg/i18n'
@@ -19,7 +19,8 @@ import type { GalleryLightboxProps } from '../../types'
19
19
  * - Preloads adjacent images for smooth navigation
20
20
  * - Loading skeleton while image loads
21
21
  * - Zoom with pan/drag support
22
- * - Keyboard and swipe navigation
22
+ * - Keyboard navigation (arrows, Home/End, Esc), focus trap, backdrop click
23
+ * - Swipe navigation
23
24
  */
24
25
  export const GalleryLightbox = memo(function GalleryLightbox({
25
26
  open,
@@ -35,6 +36,9 @@ export const GalleryLightbox = memo(function GalleryLightbox({
35
36
  }: GalleryLightboxProps) {
36
37
  const t = useAppT()
37
38
  const [mounted, setMounted] = useState(false)
39
+ const dialogRef = useRef<HTMLDivElement>(null)
40
+ // Element focused before the lightbox opened, restored on close
41
+ const previousFocusRef = useRef<HTMLElement | null>(null)
38
42
 
39
43
  // Translations
40
44
  const labels = useMemo(() => ({
@@ -58,7 +62,9 @@ export const GalleryLightbox = memo(function GalleryLightbox({
58
62
  // Prepare data
59
63
  const currentImage = useMemo(() => images[currentIndex], [images, currentIndex])
60
64
  const hasMultiple = useMemo(() => images.length > 1, [images.length])
65
+ const isVideo = currentImage?.type === 'video' && Boolean(currentImage.videoSrc)
61
66
  const { isZoomed, isDragging } = zoom
67
+ const resetZoom = zoom.reset
62
68
 
63
69
  // Preload adjacent images when lightbox is open
64
70
  usePreloadImages(open ? images : [], currentIndex, 2)
@@ -70,20 +76,33 @@ export const GalleryLightbox = memo(function GalleryLightbox({
70
76
 
71
77
  // Reset zoom when image changes
72
78
  useEffect(() => {
73
- zoom.reset()
74
- }, [currentIndex])
79
+ resetZoom()
80
+ }, [currentIndex, resetZoom])
75
81
 
76
82
  // Prevent body scroll when open
77
83
  useEffect(() => {
78
- if (open) {
79
- const originalStyle = window.getComputedStyle(document.body).overflow
80
- document.body.style.overflow = 'hidden'
81
- return () => {
82
- document.body.style.overflow = originalStyle
83
- }
84
+ if (!open) return
85
+ const originalStyle = document.body.style.overflow
86
+ document.body.style.overflow = 'hidden'
87
+ return () => {
88
+ document.body.style.overflow = originalStyle
84
89
  }
85
90
  }, [open])
86
91
 
92
+ // Focus management: move focus into the dialog on open, restore on close
93
+ useEffect(() => {
94
+ if (!open || !mounted) return
95
+ previousFocusRef.current = document.activeElement as HTMLElement | null
96
+ // Defer to allow the portal content to mount
97
+ const id = window.requestAnimationFrame(() => {
98
+ dialogRef.current?.focus()
99
+ })
100
+ return () => {
101
+ window.cancelAnimationFrame(id)
102
+ previousFocusRef.current?.focus?.()
103
+ }
104
+ }, [open, mounted])
105
+
87
106
  // Navigation handlers
88
107
  const goToPrev = useCallback(() => {
89
108
  if (hasMultiple) {
@@ -97,15 +116,16 @@ export const GalleryLightbox = memo(function GalleryLightbox({
97
116
  }
98
117
  }, [hasMultiple, currentIndex, images.length, onIndexChange])
99
118
 
100
- // Keyboard navigation
119
+ // Keyboard navigation + focus trap
101
120
  useEffect(() => {
102
121
  if (!open) return
103
122
 
104
123
  const handleKeyDown = (e: KeyboardEvent) => {
105
124
  switch (e.key) {
106
125
  case 'Escape':
126
+ e.preventDefault()
107
127
  if (isZoomed) {
108
- zoom.reset()
128
+ resetZoom()
109
129
  } else {
110
130
  onClose()
111
131
  }
@@ -116,12 +136,47 @@ export const GalleryLightbox = memo(function GalleryLightbox({
116
136
  case 'ArrowRight':
117
137
  if (!isZoomed) goToNext()
118
138
  break
139
+ case 'Home':
140
+ if (!isZoomed && hasMultiple) {
141
+ e.preventDefault()
142
+ onIndexChange(0)
143
+ }
144
+ break
145
+ case 'End':
146
+ if (!isZoomed && hasMultiple) {
147
+ e.preventDefault()
148
+ onIndexChange(images.length - 1)
149
+ }
150
+ break
151
+ case 'Tab': {
152
+ // Trap focus within the dialog
153
+ const dialog = dialogRef.current
154
+ if (!dialog) break
155
+ const focusable = dialog.querySelectorAll<HTMLElement>(
156
+ 'button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])'
157
+ )
158
+ if (focusable.length === 0) {
159
+ e.preventDefault()
160
+ break
161
+ }
162
+ const first = focusable[0]
163
+ const last = focusable[focusable.length - 1]
164
+ const active = document.activeElement
165
+ if (e.shiftKey && (active === first || active === dialog)) {
166
+ e.preventDefault()
167
+ last.focus()
168
+ } else if (!e.shiftKey && active === last) {
169
+ e.preventDefault()
170
+ first.focus()
171
+ }
172
+ break
173
+ }
119
174
  }
120
175
  }
121
176
 
122
177
  window.addEventListener('keydown', handleKeyDown)
123
178
  return () => window.removeEventListener('keydown', handleKeyDown)
124
- }, [open, isZoomed, goToPrev, goToNext, onClose, zoom])
179
+ }, [open, isZoomed, goToPrev, goToNext, onClose, resetZoom, hasMultiple, images.length, onIndexChange])
125
180
 
126
181
  // Swipe handlers (disabled when zoomed)
127
182
  const swipeHandlers = useSwipe({
@@ -132,13 +187,27 @@ export const GalleryLightbox = memo(function GalleryLightbox({
132
187
  // Action handlers
133
188
  const handleDownload = useCallback(() => {
134
189
  if (!currentImage) return
190
+ // For videos download the video file, otherwise the image
191
+ const downloadUrl = isVideo ? currentImage.videoSrc! : currentImage.src
192
+ // Derive a filename from the URL path, fall back to a numbered name
193
+ let filename = isVideo ? `video-${currentIndex + 1}.mp4` : `image-${currentIndex + 1}.jpg`
194
+ try {
195
+ const path = new URL(downloadUrl, window.location.href).pathname
196
+ const last = path.split('/').pop()
197
+ if (last && /\.[a-z0-9]+$/i.test(last)) {
198
+ filename = decodeURIComponent(last)
199
+ }
200
+ } catch {
201
+ // Keep fallback filename
202
+ }
135
203
  const link = document.createElement('a')
136
- link.href = currentImage.src
137
- link.download = currentImage.alt || `image-${currentIndex + 1}.jpg`
204
+ link.href = downloadUrl
205
+ link.download = filename
206
+ link.rel = 'noopener'
138
207
  document.body.appendChild(link)
139
208
  link.click()
140
209
  document.body.removeChild(link)
141
- }, [currentImage, currentIndex])
210
+ }, [currentImage, currentIndex, isVideo])
142
211
 
143
212
  const handleShare = useCallback(async () => {
144
213
  if (!currentImage) return
@@ -160,29 +229,31 @@ export const GalleryLightbox = memo(function GalleryLightbox({
160
229
  zoom.toggleZoom()
161
230
  }, [zoom])
162
231
 
163
- const handleContentClick = useCallback((e: React.MouseEvent) => {
164
- e.stopPropagation()
165
- }, [])
232
+ // Close only when the backdrop itself (not a child) is clicked
233
+ const handleBackdropClick = useCallback((e: React.MouseEvent) => {
234
+ if (e.target === e.currentTarget) onClose()
235
+ }, [onClose])
166
236
 
167
237
  if (!open || !mounted) return null
168
238
 
169
239
  const lightbox = (
170
240
  <div
241
+ ref={dialogRef}
171
242
  className={cn(
172
243
  'fixed inset-0 z-50',
173
244
  'backdrop-blur-sm',
174
- 'animate-in fade-in-0 duration-200'
245
+ 'animate-in fade-in-0 duration-200',
246
+ 'focus:outline-none'
175
247
  )}
176
248
  style={{ backgroundColor: 'rgba(0, 0, 0, 0.85)' }}
177
- onClick={onClose}
178
249
  role="dialog"
179
250
  aria-modal="true"
180
- aria-label={labels.lightbox}
251
+ aria-label={title || labels.lightbox}
252
+ tabIndex={-1}
181
253
  >
182
254
  {/* Content */}
183
255
  <div
184
256
  className="relative w-full h-full flex flex-col"
185
- onClick={handleContentClick}
186
257
  {...swipeHandlers}
187
258
  >
188
259
  {/* Header */}
@@ -191,7 +262,7 @@ export const GalleryLightbox = memo(function GalleryLightbox({
191
262
  <div className="text-white">
192
263
  {title && <div className="font-medium">{title}</div>}
193
264
  {hasMultiple && (
194
- <div className="text-sm text-white/70">
265
+ <div className="text-sm text-white/70" aria-live="polite">
195
266
  {currentIndex + 1} / {images.length}
196
267
  </div>
197
268
  )}
@@ -199,10 +270,10 @@ export const GalleryLightbox = memo(function GalleryLightbox({
199
270
 
200
271
  {/* Actions */}
201
272
  <div className="flex items-center gap-2">
202
- {enableZoom && (
273
+ {enableZoom && !isVideo && (
203
274
  <button
204
275
  type="button"
205
- className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors"
276
+ className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
206
277
  onClick={handleToggleZoom}
207
278
  aria-label={isZoomed ? labels.zoomOut : labels.zoomIn}
208
279
  >
@@ -217,7 +288,7 @@ export const GalleryLightbox = memo(function GalleryLightbox({
217
288
  {enableDownload && (
218
289
  <button
219
290
  type="button"
220
- className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors"
291
+ className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
221
292
  onClick={handleDownload}
222
293
  aria-label={labels.download}
223
294
  >
@@ -228,7 +299,7 @@ export const GalleryLightbox = memo(function GalleryLightbox({
228
299
  {enableShare && (
229
300
  <button
230
301
  type="button"
231
- className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors"
302
+ className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
232
303
  onClick={handleShare}
233
304
  aria-label={labels.share}
234
305
  >
@@ -238,7 +309,7 @@ export const GalleryLightbox = memo(function GalleryLightbox({
238
309
 
239
310
  <button
240
311
  type="button"
241
- className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors"
312
+ className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
242
313
  onClick={onClose}
243
314
  aria-label={labels.close}
244
315
  >
@@ -248,17 +319,21 @@ export const GalleryLightbox = memo(function GalleryLightbox({
248
319
  </div>
249
320
 
250
321
  {/* Main Image */}
251
- <div className="flex-1 flex items-center justify-center p-4 pt-16 pb-24 overflow-hidden">
322
+ <div
323
+ className="flex-1 flex items-center justify-center p-4 pt-16 pb-24 overflow-hidden"
324
+ onClick={handleBackdropClick}
325
+ >
252
326
  {currentImage && (
253
327
  <div
254
328
  className={cn(
255
329
  'relative w-full h-full select-none',
256
- isZoomed ? 'cursor-grab' : enableZoom ? 'cursor-zoom-in' : 'cursor-default',
257
- isDragging && 'cursor-grabbing'
330
+ // Zoom cursors only apply to images, not video (needs native controls)
331
+ !isVideo && (isZoomed ? 'cursor-grab' : enableZoom ? 'cursor-zoom-in' : 'cursor-default'),
332
+ !isVideo && isDragging && 'cursor-grabbing'
258
333
  )}
259
- {...(enableZoom ? zoom.handlers : {})}
334
+ {...(enableZoom && !isVideo ? zoom.handlers : {})}
260
335
  style={{
261
- transform: isZoomed
336
+ transform: isZoomed && !isVideo
262
337
  ? `scale(${zoom.state.scale}) translate(${zoom.state.x}px, ${zoom.state.y}px)`
263
338
  : 'scale(1)',
264
339
  transition: isDragging ? 'none' : 'transform 0.3s ease-out',
@@ -267,7 +342,9 @@ export const GalleryLightbox = memo(function GalleryLightbox({
267
342
  >
268
343
  <GalleryMedia
269
344
  media={currentImage}
270
- className="w-full h-full pointer-events-none"
345
+ // pointer-events disabled for images so the zoom wrapper gets clicks;
346
+ // video needs pointer events for its native/overlay controls
347
+ className={cn('w-full h-full', !isVideo && 'pointer-events-none')}
271
348
  objectFit="contain"
272
349
  showLoading
273
350
  priority
@@ -51,8 +51,13 @@ export const GalleryVideo = memo(function GalleryVideo({
51
51
  if (!video) return
52
52
 
53
53
  if (video.paused) {
54
- video.play()
55
- setIsPlaying(true)
54
+ // play() returns a promise that can reject (autoplay policy / no source)
55
+ const playPromise = video.play()
56
+ if (playPromise && typeof playPromise.then === 'function') {
57
+ playPromise.then(() => setIsPlaying(true)).catch(() => setIsPlaying(false))
58
+ } else {
59
+ setIsPlaying(true)
60
+ }
56
61
  } else {
57
62
  video.pause()
58
63
  setIsPlaying(false)
@@ -98,6 +103,14 @@ export const GalleryVideo = memo(function GalleryVideo({
98
103
  setIsPlaying(false)
99
104
  }, [])
100
105
 
106
+ // Keep local state in sync with native video events (native controls, end of playback)
107
+ const handlePlay = useCallback(() => setIsPlaying(true), [])
108
+ const handlePause = useCallback(() => setIsPlaying(false), [])
109
+ const handleEnded = useCallback(() => {
110
+ setIsPlaying(false)
111
+ setShowOverlay(true)
112
+ }, [])
113
+
101
114
  const handleContainerClick = useCallback((e: React.MouseEvent) => {
102
115
  e.stopPropagation()
103
116
  }, [])
@@ -110,6 +123,7 @@ export const GalleryVideo = memo(function GalleryVideo({
110
123
  if (hasError) {
111
124
  return (
112
125
  <div
126
+ data-nav
113
127
  className={cn('relative w-full h-full bg-black', className)}
114
128
  onClick={handleContainerClick}
115
129
  >
@@ -133,6 +147,7 @@ export const GalleryVideo = memo(function GalleryVideo({
133
147
 
134
148
  return (
135
149
  <div
150
+ data-nav
136
151
  className={cn('relative w-full h-full bg-black', className)}
137
152
  onMouseEnter={handleMouseEnter}
138
153
  onMouseLeave={handleMouseLeave}
@@ -143,12 +158,16 @@ export const GalleryVideo = memo(function GalleryVideo({
143
158
  className="w-full h-full object-contain"
144
159
  src={media.videoSrc}
145
160
  poster={media.src}
161
+ aria-label={media.alt || 'Video'}
146
162
  autoPlay={autoPlay}
147
163
  muted={muted}
148
164
  loop={loop}
149
165
  playsInline
150
166
  onClick={handleVideoClick}
151
167
  onError={handleError}
168
+ onPlay={handlePlay}
169
+ onPause={handlePause}
170
+ onEnded={handleEnded}
152
171
  >
153
172
  {media.videoType && (
154
173
  <source src={media.videoSrc} type={media.videoType} />
@@ -161,8 +161,18 @@ export const GalleryCarousel = memo(function GalleryCarousel({
161
161
  const dy = Math.abs(e.clientY - y)
162
162
  const dt = Date.now() - time
163
163
 
164
+ // Don't open lightbox when interacting with video controls
165
+ const target = e.target as HTMLElement
166
+ const isVideoInteraction =
167
+ target.tagName === 'VIDEO' || target.closest('video, [data-nav]') !== null
168
+
164
169
  // Only trigger click if pointer didn't move much and was quick
165
- if (dx < CLICK_THRESHOLD && dy < CLICK_THRESHOLD && dt < CLICK_TIME_THRESHOLD) {
170
+ if (
171
+ !isVideoInteraction &&
172
+ dx < CLICK_THRESHOLD &&
173
+ dy < CLICK_THRESHOLD &&
174
+ dt < CLICK_TIME_THRESHOLD
175
+ ) {
166
176
  onLightboxOpen()
167
177
  }
168
178
 
@@ -3,6 +3,10 @@
3
3
  import { useEffect, useRef } from 'react';
4
4
 
5
5
  import type { GalleryMediaItem } from '../types'
6
+ import { normalizeImageUrl } from '../utils'
7
+
8
+ /** Max number of URLs to remember as "already preloaded" before evicting oldest */
9
+ const MAX_PRELOAD_MEMORY = 200
6
10
 
7
11
  /**
8
12
  * Preload adjacent images for smoother navigation
@@ -13,6 +17,7 @@ export function usePreloadImages(
13
17
  currentIndex: number,
14
18
  preloadCount: number = 1
15
19
  ): void {
20
+ // Insertion-ordered Set, used as a bounded LRU to avoid unbounded growth
16
21
  const preloadedRef = useRef<Set<string>>(new Set())
17
22
 
18
23
  useEffect(() => {
@@ -26,18 +31,50 @@ export function usePreloadImages(
26
31
 
27
32
  const index = (currentIndex + offset + images.length) % images.length
28
33
  const image = images[index]
34
+ if (!image) continue
35
+
36
+ // Videos are not preloadable via Image(); skip them
37
+ if (image.type === 'video') continue
29
38
 
30
- if (image && !preloadedRef.current.has(image.src)) {
31
- toPreload.push(image.src)
39
+ const src = normalizeImageUrl(image.src)
40
+ if (src && !preloadedRef.current.has(src)) {
41
+ toPreload.push(src)
32
42
  }
33
43
  }
34
44
 
35
- // Preload images
45
+ if (toPreload.length === 0) return
46
+
47
+ const memory = preloadedRef.current
48
+ const loaders: HTMLImageElement[] = []
49
+
36
50
  toPreload.forEach((src) => {
37
51
  const img = new Image()
52
+ // Release the loader once finished so it can be GC'd
53
+ const cleanup = () => {
54
+ img.onload = null
55
+ img.onerror = null
56
+ }
57
+ img.onload = cleanup
58
+ img.onerror = cleanup
38
59
  img.src = src
39
- preloadedRef.current.add(src)
60
+ loaders.push(img)
61
+
62
+ memory.add(src)
63
+ // Evict oldest entries to keep the Set bounded
64
+ if (memory.size > MAX_PRELOAD_MEMORY) {
65
+ const oldest = memory.values().next().value
66
+ if (oldest !== undefined) memory.delete(oldest)
67
+ }
40
68
  })
69
+
70
+ // Cancel in-flight loaders if the effect re-runs before they settle
71
+ return () => {
72
+ loaders.forEach((img) => {
73
+ img.onload = null
74
+ img.onerror = null
75
+ img.src = ''
76
+ })
77
+ }
41
78
  }, [images, currentIndex, preloadCount])
42
79
  }
43
80
 
@@ -47,9 +84,19 @@ export function usePreloadImages(
47
84
  export function preloadImage(src: string): Promise<void> {
48
85
  return new Promise((resolve, reject) => {
49
86
  const img = new Image()
50
- img.onload = () => resolve()
51
- img.onerror = reject
52
- img.src = src
87
+ const cleanup = () => {
88
+ img.onload = null
89
+ img.onerror = null
90
+ }
91
+ img.onload = () => {
92
+ cleanup()
93
+ resolve()
94
+ }
95
+ img.onerror = () => {
96
+ cleanup()
97
+ reject(new Error(`Failed to preload image: ${src}`))
98
+ }
99
+ img.src = normalizeImageUrl(src)
53
100
  })
54
101
  }
55
102
 
@@ -24,20 +24,31 @@ export function ImageInfo({ src }: ImageInfoProps) {
24
24
  return;
25
25
  }
26
26
 
27
+ // Clear stale dimensions while the new source resolves
28
+ setDimensions(null);
29
+
27
30
  // Load and cache dimensions
31
+ let cancelled = false;
28
32
  const img = new Image();
29
33
  img.onload = () => {
34
+ if (cancelled) return;
30
35
  const dims = { w: img.naturalWidth, h: img.naturalHeight };
31
36
  setDimensions(dims);
32
37
  cacheDimensions(src, { width: dims.w, height: dims.h });
33
38
  };
34
39
  img.src = src;
40
+
41
+ return () => {
42
+ cancelled = true;
43
+ img.onload = null;
44
+ img.src = '';
45
+ };
35
46
  }, [src, getDimensions, cacheDimensions]);
36
47
 
37
48
  if (!dimensions) return null;
38
49
 
39
50
  return (
40
- <div className="absolute top-3 right-3 z-10 px-2 py-1 bg-background/80 backdrop-blur-sm border rounded text-[10px] text-muted-foreground font-mono">
51
+ <div className="absolute top-3 right-3 z-10 px-2 py-1 bg-black/60 backdrop-blur-sm border border-white/10 rounded text-[10px] text-white/80 font-mono">
41
52
  {dimensions.w} × {dimensions.h}
42
53
  </div>
43
54
  );