@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
@@ -7,7 +7,7 @@
7
7
  import { useMemo } from 'react';
8
8
  import { ZoomIn, ZoomOut, RotateCw, FlipHorizontal, FlipVertical, Maximize2, Expand } from 'lucide-react';
9
9
  import { useControls } from 'react-zoom-pan-pinch';
10
- import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@djangocfg/ui-core/components';
10
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@djangocfg/ui-core/components';
11
11
  import { useAppT } from '@djangocfg/i18n';
12
12
  import { cn } from '@djangocfg/ui-core/lib';
13
13
  import { ZOOM_PRESETS } from '../utils';
@@ -17,6 +17,18 @@ import type { ImageToolbarProps } from '../types';
17
17
  // COMPONENT
18
18
  // =============================================================================
19
19
 
20
+ // The toolbar floats over the image, whose brightness is unknown and
21
+ // unrelated to the host theme. A theme-bound `bg-background` surface
22
+ // would clash with a dark photo (white slab) or a light photo in dark
23
+ // mode. A fixed dark-glass overlay with light icons stays readable on
24
+ // any content — the same model the gallery nav arrows and media
25
+ // players use.
26
+ const TB_BUTTON =
27
+ 'inline-flex items-center justify-center rounded-md text-white/90 ' +
28
+ 'transition-colors hover:bg-white/15 hover:text-white ' +
29
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 ' +
30
+ 'disabled:opacity-40 disabled:pointer-events-none';
31
+
20
32
  export function ImageToolbar({
21
33
  scale,
22
34
  transform,
@@ -45,23 +57,25 @@ export function ImageToolbar({
45
57
  }, [scale]);
46
58
 
47
59
  return (
48
- <div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex items-center gap-0.5 bg-background/90 backdrop-blur-sm border rounded-lg p-1 shadow-lg">
60
+ <div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex items-center gap-0.5 bg-black/60 backdrop-blur-sm border border-white/10 rounded-lg p-1 shadow-lg">
49
61
  {/* Zoom controls */}
50
- <Button
51
- variant="ghost"
52
- size="icon"
53
- className="h-7 w-7"
62
+ <button
63
+ type="button"
64
+ className={cn(TB_BUTTON, 'h-7 w-7')}
54
65
  onClick={() => zoomOut()}
55
66
  title={labels.zoomOut}
56
67
  >
57
68
  <ZoomOut className="h-3.5 w-3.5" />
58
- </Button>
69
+ </button>
59
70
 
60
71
  <DropdownMenu>
61
72
  <DropdownMenuTrigger asChild>
62
- <Button variant="ghost" size="sm" className="h-7 px-2 min-w-[52px] font-mono text-xs">
73
+ <button
74
+ type="button"
75
+ className={cn(TB_BUTTON, 'h-7 px-2 min-w-[52px] font-mono text-xs')}
76
+ >
63
77
  {zoomLabel}
64
- </Button>
78
+ </button>
65
79
  </DropdownMenuTrigger>
66
80
  <DropdownMenuContent align="center" className="min-w-[80px]">
67
81
  {ZOOM_PRESETS.map((preset) => (
@@ -76,80 +90,74 @@ export function ImageToolbar({
76
90
  </DropdownMenuContent>
77
91
  </DropdownMenu>
78
92
 
79
- <Button
80
- variant="ghost"
81
- size="icon"
82
- className="h-7 w-7"
93
+ <button
94
+ type="button"
95
+ className={cn(TB_BUTTON, 'h-7 w-7')}
83
96
  onClick={() => zoomIn()}
84
97
  title={labels.zoomIn}
85
98
  >
86
99
  <ZoomIn className="h-3.5 w-3.5" />
87
- </Button>
100
+ </button>
88
101
 
89
- <div className="w-px h-4 bg-border mx-1" />
102
+ <div className="w-px h-4 bg-white/20 mx-1" />
90
103
 
91
104
  {/* Fit to view */}
92
- <Button
93
- variant="ghost"
94
- size="icon"
95
- className="h-7 w-7"
105
+ <button
106
+ type="button"
107
+ className={cn(TB_BUTTON, 'h-7 w-7')}
96
108
  onClick={() => resetTransform()}
97
109
  title={labels.fitToView}
98
110
  >
99
111
  <Maximize2 className="h-3.5 w-3.5" />
100
- </Button>
112
+ </button>
101
113
 
102
- <div className="w-px h-4 bg-border mx-1" />
114
+ <div className="w-px h-4 bg-white/20 mx-1" />
103
115
 
104
116
  {/* Transform controls */}
105
- <Button
106
- variant="ghost"
107
- size="icon"
108
- className={cn('h-7 w-7', transform.flipH && 'bg-accent')}
117
+ <button
118
+ type="button"
119
+ className={cn(TB_BUTTON, 'h-7 w-7', transform.flipH && 'bg-white/20 text-white')}
109
120
  onClick={onFlipH}
110
121
  title={labels.flipHorizontal}
111
122
  >
112
123
  <FlipHorizontal className="h-3.5 w-3.5" />
113
- </Button>
124
+ </button>
114
125
 
115
- <Button
116
- variant="ghost"
117
- size="icon"
118
- className={cn('h-7 w-7', transform.flipV && 'bg-accent')}
126
+ <button
127
+ type="button"
128
+ className={cn(TB_BUTTON, 'h-7 w-7', transform.flipV && 'bg-white/20 text-white')}
119
129
  onClick={onFlipV}
120
130
  title={labels.flipVertical}
121
131
  >
122
132
  <FlipVertical className="h-3.5 w-3.5" />
123
- </Button>
133
+ </button>
124
134
 
125
- <Button
126
- variant="ghost"
127
- size="icon"
128
- className="h-7 w-7"
135
+ <button
136
+ type="button"
137
+ className={cn(TB_BUTTON, 'h-7 w-7')}
129
138
  onClick={onRotate}
130
139
  title={labels.rotate}
131
140
  >
132
141
  <RotateCw className="h-3.5 w-3.5" />
133
- </Button>
142
+ </button>
134
143
 
135
144
  {transform.rotation !== 0 && (
136
- <span className="text-[10px] text-muted-foreground font-mono pl-1">
145
+ <span className="text-[10px] text-white/60 font-mono pl-1">
137
146
  {transform.rotation}°
138
147
  </span>
139
148
  )}
140
149
 
141
150
  {onExpand && (
142
151
  <>
143
- <div className="w-px h-4 bg-border mx-1" />
144
- <Button
145
- variant="ghost"
146
- size="icon"
147
- className="h-7 w-7"
152
+ <div className="w-px h-4 bg-white/20 mx-1" />
153
+ <button
154
+ type="button"
155
+ className={cn(TB_BUTTON, 'h-7 w-7')}
148
156
  onClick={onExpand}
149
157
  title={labels.fullscreen}
150
158
  >
151
159
  <Expand className="h-3.5 w-3.5" />
152
- </Button>
160
+ </button>
153
161
  </>
154
162
  )}
155
163
  </div>
@@ -14,8 +14,12 @@
14
14
  */
15
15
 
16
16
  import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
17
- import { ImageIcon, AlertCircle, ChevronLeft, ChevronRight } from 'lucide-react';
18
- import { TransformWrapper, TransformComponent, useControls } from 'react-zoom-pan-pinch';
17
+ import { ImageIcon, AlertCircle, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
18
+ import {
19
+ TransformWrapper,
20
+ TransformComponent,
21
+ type ReactZoomPanPinchRef,
22
+ } from 'react-zoom-pan-pinch';
19
23
  import { cn, Dialog, DialogContent, DialogTitle, Alert, AlertDescription } from '@djangocfg/ui-core';
20
24
  import { useAppT } from '@djangocfg/i18n';
21
25
  import { useHotkey } from '@djangocfg/ui-core/hooks';
@@ -23,7 +27,8 @@ import { useHotkey } from '@djangocfg/ui-core/hooks';
23
27
  import { ImageToolbar } from './ImageToolbar';
24
28
  import { ImageInfo } from './ImageInfo';
25
29
  import { useImageTransform, useImageLoading } from '../hooks';
26
- import type { ImageViewerProps, ImageItem } from '../types';
30
+ import { KEYBOARD_PAN_STEP } from '../utils';
31
+ import type { ImageViewerProps } from '../types';
27
32
 
28
33
  // =============================================================================
29
34
  // COMPONENT
@@ -51,13 +56,16 @@ export function ImageViewer({
51
56
  const [scale, setScale] = useState(1);
52
57
  const [dialogOpen, setDialogOpen] = useState(false);
53
58
  const [loadError, setLoadError] = useState(false);
59
+ const [imgDecoded, setImgDecoded] = useState(false);
54
60
  const containerRef = useRef<HTMLDivElement>(null);
55
- const controlsRef = useRef<ReturnType<typeof useControls> | null>(null);
61
+ const controlsRef = useRef<ReactZoomPanPinchRef | null>(null);
56
62
 
57
63
  const labels = useMemo(() => ({
58
64
  noImage: t('tools.image.noImage'),
59
65
  failedToLoad: t('tools.image.failedToLoad'),
60
66
  loading: t('ui.form.loading'),
67
+ prev: t('tools.gallery.previous'),
68
+ next: t('tools.gallery.next'),
61
69
  }), [t]);
62
70
 
63
71
  const {
@@ -73,16 +81,34 @@ export function ImageViewer({
73
81
  src: current?.src,
74
82
  });
75
83
 
76
- useEffect(() => { setLoadError(false); }, [src]);
84
+ // Reset per-source load flags whenever the source changes
85
+ useEffect(() => {
86
+ setLoadError(false);
87
+ setImgDecoded(false);
88
+ }, [src]);
77
89
 
78
90
  const { transform, rotate, flipH, flipV, transformStyle } = useImageTransform({
79
91
  resetKey: current?.file.path ?? '',
80
92
  });
81
93
 
82
94
  const handleZoomPreset = useCallback((value: number | 'fit') => {
83
- if (!controlsRef.current) return;
84
- if (value === 'fit') controlsRef.current.resetTransform();
85
- else controlsRef.current.setTransform(0, 0, value);
95
+ const controls = controlsRef.current;
96
+ if (!controls) return;
97
+ if (value === 'fit') {
98
+ controls.resetTransform();
99
+ return;
100
+ }
101
+ // Anchor preset zoom to the canvas center instead of the top-left origin
102
+ const el = controls.instance.wrapperComponent;
103
+ if (el) {
104
+ const cx = el.offsetWidth / 2;
105
+ const cy = el.offsetHeight / 2;
106
+ const x = cx - cx * value;
107
+ const y = cy - cy * value;
108
+ controls.setTransform(x, y, value);
109
+ } else {
110
+ controls.setTransform(0, 0, value);
111
+ }
86
112
  }, []);
87
113
 
88
114
  const handleExpand = useCallback(() => setDialogOpen(true), []);
@@ -97,7 +123,18 @@ export function ImageViewer({
97
123
  [images.length]
98
124
  );
99
125
 
100
- // Keyboard: zoom/rotate (only when container focused)
126
+ // Pan the image by a fixed offset (arrow keys)
127
+ const panBy = useCallback((dx: number, dy: number) => {
128
+ const controls = controlsRef.current;
129
+ if (!controls) return false;
130
+ const { positionX, positionY, scale: s } = controls.instance.transformState;
131
+ // Only pan when the image is zoomed in — otherwise it cannot move
132
+ if (s <= 1) return false;
133
+ controls.setTransform(positionX + dx, positionY + dy, s);
134
+ return true;
135
+ }, []);
136
+
137
+ // Keyboard: zoom / rotate / pan (only when container focused)
101
138
  useEffect(() => {
102
139
  const handleKeyDown = (e: KeyboardEvent) => {
103
140
  if (!containerRef.current?.contains(document.activeElement) &&
@@ -106,18 +143,33 @@ export function ImageViewer({
106
143
  if (!controls) return;
107
144
  switch (e.key) {
108
145
  case '+': case '=': e.preventDefault(); controls.zoomIn(); break;
109
- case '-': e.preventDefault(); controls.zoomOut(); break;
146
+ case '-': case '_': e.preventDefault(); controls.zoomOut(); break;
110
147
  case '0': e.preventDefault(); controls.resetTransform(); break;
111
- case 'r': if (!e.metaKey && !e.ctrlKey) { e.preventDefault(); rotate(); } break;
148
+ case 'r': case 'R':
149
+ if (!e.metaKey && !e.ctrlKey) { e.preventDefault(); rotate(); }
150
+ break;
151
+ // Arrow keys pan only when zoomed in. While at fit scale they
152
+ // fall through to gallery navigation (handled by useHotkey).
153
+ case 'ArrowUp': if (panBy(0, KEYBOARD_PAN_STEP)) e.preventDefault(); break;
154
+ case 'ArrowDown': if (panBy(0, -KEYBOARD_PAN_STEP)) e.preventDefault(); break;
155
+ case 'ArrowLeft': if (panBy(KEYBOARD_PAN_STEP, 0)) e.preventDefault(); break;
156
+ case 'ArrowRight': if (panBy(-KEYBOARD_PAN_STEP, 0)) e.preventDefault(); break;
112
157
  }
113
158
  };
114
159
  window.addEventListener('keydown', handleKeyDown);
115
160
  return () => window.removeEventListener('keydown', handleKeyDown);
116
- }, [rotate]);
161
+ }, [rotate, panBy]);
117
162
 
118
- // Keyboard: gallery navigation (global when open)
119
- useHotkey('ArrowLeft', prev, { enabled: hasMultiple, preventDefault: true });
120
- useHotkey('ArrowRight', next, { enabled: hasMultiple, preventDefault: true });
163
+ // Keyboard: gallery navigation (global, only while not zoomed in so it
164
+ // never competes with arrow-key panning)
165
+ useHotkey('ArrowLeft', prev, {
166
+ enabled: hasMultiple && scale <= 1,
167
+ preventDefault: true,
168
+ });
169
+ useHotkey('ArrowRight', next, {
170
+ enabled: hasMultiple && scale <= 1,
171
+ preventDefault: true,
172
+ });
121
173
 
122
174
  if (!current) {
123
175
  return (
@@ -153,8 +205,11 @@ export function ImageViewer({
153
205
  <div
154
206
  ref={containerRef}
155
207
  tabIndex={0}
208
+ role="img"
209
+ aria-label={current.file.name}
156
210
  className={cn(
157
211
  'flex-1 h-full relative overflow-hidden outline-none',
212
+ 'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',
158
213
  'bg-[length:16px_16px]',
159
214
  '[background-color:color-mix(in_oklab,var(--muted)_20%,transparent)]',
160
215
  '[background-image:linear-gradient(45deg,color-mix(in_oklab,var(--muted)_40%,transparent)_25%,transparent_25%),linear-gradient(-45deg,color-mix(in_oklab,var(--muted)_40%,transparent)_25%,transparent_25%),linear-gradient(45deg,transparent_75%,color-mix(in_oklab,var(--muted)_40%,transparent)_75%),linear-gradient(-45deg,transparent_75%,color-mix(in_oklab,var(--muted)_40%,transparent)_75%)]',
@@ -164,19 +219,31 @@ export function ImageViewer({
164
219
  {src && <ImageInfo src={src} />}
165
220
 
166
221
  {useProgressiveLoading && !isFullyLoaded && (
167
- <div className="absolute top-3 left-3 z-10 px-2 py-1 bg-background/80 backdrop-blur-sm border rounded text-[10px] text-muted-foreground font-mono flex items-center gap-1.5">
168
- <div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
222
+ <div className="absolute top-3 left-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 flex items-center gap-1.5">
223
+ <div className="w-2 h-2 bg-blue-400 rounded-full animate-pulse" />
169
224
  {labels.loading}
170
225
  </div>
171
226
  )}
172
227
 
228
+ {/* Spinner while a non-progressive image is still decoding */}
229
+ {!useProgressiveLoading && !imgDecoded && !loadError && (
230
+ <div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 pointer-events-none">
231
+ <Loader2 className="w-8 h-8 text-muted-foreground/60 animate-spin" />
232
+ <span className="text-xs text-muted-foreground">{labels.loading}</span>
233
+ </div>
234
+ )}
235
+
173
236
  <TransformWrapper
174
237
  initialScale={1}
175
238
  minScale={0.1}
176
239
  maxScale={8}
177
240
  centerOnInit
178
241
  centerZoomedOut
179
- onTransformed={(ref, state) => { setScale(state.scale); controlsRef.current = ref; }}
242
+ onTransformed={(ref, state) => {
243
+ controlsRef.current = ref;
244
+ // Avoid a re-render on every pan frame — only when zoom changes
245
+ setScale((prev) => (prev === state.scale ? prev : state.scale));
246
+ }}
180
247
  onInit={(ref) => { controlsRef.current = ref; }}
181
248
  wheel={{ step: 0.1 }}
182
249
  doubleClick={{ mode: 'toggle', step: 2 }}
@@ -196,13 +263,21 @@ export function ImageViewer({
196
263
  wrapperClass="!w-full !h-full cursor-grab active:cursor-grabbing"
197
264
  contentClass="!w-full !h-full flex items-center justify-center"
198
265
  >
199
- <div className="relative">
266
+ {/*
267
+ Fill the TransformComponent content box so the children's
268
+ `max-w-full / max-h-full` resolve against the actual viewport
269
+ instead of the image's natural box. Without `w-full h-full`
270
+ this wrapper shrink-fits the image, which collapses the
271
+ max-* constraints and renders the image at intrinsic size —
272
+ visible as cropping / half-height in tall containers.
273
+ */}
274
+ <div className="relative w-full h-full flex items-center justify-center">
200
275
  {useProgressiveLoading && lqip && !isFullyLoaded && (
201
276
  <img
202
277
  src={lqip}
203
278
  alt=""
204
279
  aria-hidden="true"
205
- className="absolute inset-0 max-w-full max-h-full object-contain select-none"
280
+ className="absolute max-w-full max-h-full object-contain select-none"
206
281
  style={{ transform: transformStyle, filter: 'blur(20px)', transition: 'opacity 0.3s ease-out', opacity: isFullyLoaded ? 0 : 1 }}
207
282
  draggable={false}
208
283
  />
@@ -214,11 +289,12 @@ export function ImageViewer({
214
289
  className="max-w-full max-h-full object-contain select-none"
215
290
  style={{
216
291
  transform: transformStyle,
217
- transition: useProgressiveLoading ? 'transform 0.15s ease-out, opacity 0.3s ease-out' : 'transform 0.15s ease-out',
218
- opacity: useProgressiveLoading && !isFullyLoaded ? 0 : 1,
292
+ transition: 'transform 0.15s ease-out, opacity 0.3s ease-out',
293
+ opacity:
294
+ (useProgressiveLoading && !isFullyLoaded) || !imgDecoded ? 0 : 1,
219
295
  }}
220
296
  draggable={false}
221
- crossOrigin="anonymous"
297
+ onLoad={() => setImgDecoded(true)}
222
298
  onError={() => setLoadError(true)}
223
299
  />
224
300
  )}
@@ -232,18 +308,22 @@ export function ImageViewer({
232
308
  <button
233
309
  type="button"
234
310
  onClick={prev}
235
- className="absolute left-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-1.5 transition-colors"
311
+ aria-label={labels.prev}
312
+ title={labels.prev}
313
+ className="absolute left-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-1.5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"
236
314
  >
237
315
  <ChevronLeft className="h-5 w-5" />
238
316
  </button>
239
317
  <button
240
318
  type="button"
241
319
  onClick={next}
242
- className="absolute right-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-1.5 transition-colors"
320
+ aria-label={labels.next}
321
+ title={labels.next}
322
+ className="absolute right-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-1.5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"
243
323
  >
244
324
  <ChevronRight className="h-5 w-5" />
245
325
  </button>
246
- <div className="absolute bottom-2 left-1/2 -translate-x-1/2 z-10 bg-black/50 text-white text-xs px-2 py-0.5 rounded-full pointer-events-none">
326
+ <div className="absolute top-3 left-1/2 -translate-x-1/2 z-10 bg-black/60 backdrop-blur-sm border border-white/10 text-white/80 text-xs font-mono px-2 py-0.5 rounded-full pointer-events-none">
247
327
  {currentIndex + 1} / {images.length}
248
328
  </div>
249
329
  </>
@@ -171,11 +171,16 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
171
171
  return;
172
172
  }
173
173
 
174
+ setLqip(null);
174
175
  setIsFullyLoaded(false);
175
176
  imageDebug.state('progressive loading', { size });
176
177
 
178
+ // Guard against state updates after src change / unmount
179
+ let cancelled = false;
180
+
177
181
  // Create low-quality placeholder
178
182
  createLQIP(src).then((placeholder) => {
183
+ if (cancelled || !isMountedRef.current) return;
179
184
  if (placeholder) {
180
185
  imageDebug.debug('LQIP created');
181
186
  setLqip(placeholder);
@@ -185,6 +190,7 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
185
190
  // Pre-load full image
186
191
  const img = new Image();
187
192
  img.onload = () => {
193
+ if (cancelled || !isMountedRef.current) return;
188
194
  imageDebug.state('fully loaded');
189
195
  setIsFullyLoaded(true);
190
196
  };
@@ -192,6 +198,13 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
192
198
  imageDebug.error('Failed to load full image');
193
199
  };
194
200
  img.src = src;
201
+
202
+ return () => {
203
+ cancelled = true;
204
+ img.onload = null;
205
+ img.onerror = null;
206
+ img.src = '';
207
+ };
195
208
  }, [src, useProgressiveLoading, size]);
196
209
 
197
210
  return {
@@ -37,6 +37,9 @@ export const MIN_ZOOM = 0.1;
37
37
  /** Maximum zoom level */
38
38
  export const MAX_ZOOM = 8;
39
39
 
40
+ /** Pixels to pan per arrow-key press */
41
+ export const KEYBOARD_PAN_STEP = 60;
42
+
40
43
  /** Available zoom presets */
41
44
  export const ZOOM_PRESETS: readonly ZoomPreset[] = [
42
45
  { label: 'Fit', value: 'fit' },
@@ -11,6 +11,7 @@ export {
11
11
  LQIP_QUALITY,
12
12
  MIN_ZOOM,
13
13
  MAX_ZOOM,
14
+ KEYBOARD_PAN_STEP,
14
15
  ZOOM_PRESETS,
15
16
  DEFAULT_TRANSFORM,
16
17
  } from './constants';
@@ -17,7 +17,8 @@ import {
17
17
  import { JsonFormContext, JsonSchemaFormProps } from './types';
18
18
  import { normalizeFormData, validateSchema } from './utils';
19
19
  import {
20
- CheckboxWidget, ColorWidget, NumberWidget, SelectWidget, SliderWidget, SwitchWidget, TextareaWidget, TextWidget
20
+ CheckboxWidget, ColorWidget, NumberWidget, RadioWidget, SelectWidget, SliderWidget, SwitchWidget,
21
+ TextareaWidget, TextWidget
21
22
  } from './widgets';
22
23
 
23
24
  /**
@@ -108,6 +109,7 @@ export function JsonSchemaForm<T = any>(props: JsonSchemaFormProps<T>) {
108
109
  NumberWidget,
109
110
  CheckboxWidget,
110
111
  SelectWidget,
112
+ RadioWidget,
111
113
  SwitchWidget,
112
114
  ColorWidget,
113
115
  SliderWidget,
@@ -117,6 +119,7 @@ export function JsonSchemaForm<T = any>(props: JsonSchemaFormProps<T>) {
117
119
  number: NumberWidget,
118
120
  checkbox: CheckboxWidget,
119
121
  select: SelectWidget,
122
+ radio: RadioWidget,
120
123
  switch: SwitchWidget,
121
124
  color: ColorWidget,
122
125
  slider: SliderWidget,
@@ -4,6 +4,7 @@ import { Plus } from 'lucide-react';
4
4
  import React from 'react';
5
5
 
6
6
  import { Button } from '@djangocfg/ui-core/components';
7
+ import { useAppT } from '@djangocfg/i18n';
7
8
  import { ArrayFieldTemplateProps } from '@rjsf/utils';
8
9
 
9
10
  /**
@@ -23,6 +24,7 @@ export function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
23
24
  onAddClick,
24
25
  required,
25
26
  } = props;
27
+ const t = useAppT();
26
28
 
27
29
  return (
28
30
  <div className="space-y-4">
@@ -41,7 +43,7 @@ export function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
41
43
  className="gap-2"
42
44
  >
43
45
  <Plus className="h-4 w-4" />
44
- Add Item
46
+ {t('tools.jsonForm.addItem')}
45
47
  </Button>
46
48
  )}
47
49
  </div>
@@ -54,7 +56,7 @@ export function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
54
56
 
55
57
  {items.length === 0 && (
56
58
  <div className="text-center py-8 text-muted-foreground border-2 border-dashed rounded-md">
57
- No items added yet.
59
+ {t('tools.jsonForm.noItems')}
58
60
  {canAdd && (
59
61
  <Button
60
62
  type="button"
@@ -64,7 +66,7 @@ export function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
64
66
  className="mt-2 gap-2"
65
67
  >
66
68
  <Plus className="h-4 w-4" />
67
- Add First Item
69
+ {t('tools.jsonForm.addFirstItem')}
68
70
  </Button>
69
71
  )}
70
72
  </div>
@@ -3,8 +3,11 @@
3
3
  import React, { ChangeEvent, FocusEvent, useCallback, useMemo } from 'react';
4
4
 
5
5
  import { Input } from '@djangocfg/ui-core/components';
6
+ import { cn } from '@djangocfg/ui-core/lib';
6
7
  import { getInputProps, WidgetProps } from '@rjsf/utils';
7
8
 
9
+ import { useWidgetEnv } from '../widgets/_useWidgetEnv';
10
+
8
11
  /**
9
12
  * Base input template for JSON Schema Form
10
13
  *
@@ -24,8 +27,6 @@ export function BaseInputTemplate(props: WidgetProps) {
24
27
  id,
25
28
  type,
26
29
  value,
27
- readonly,
28
- disabled,
29
30
  autofocus,
30
31
  onBlur,
31
32
  onFocus,
@@ -35,6 +36,7 @@ export function BaseInputTemplate(props: WidgetProps) {
35
36
  rawErrors,
36
37
  placeholder,
37
38
  } = props;
39
+ const { disabled, compact, tooltipText } = useWidgetEnv(props);
38
40
 
39
41
  // Get input props from RJSF utils (handles step, min, max, etc.)
40
42
  const inputProps = useMemo(() => {
@@ -92,13 +94,14 @@ export function BaseInputTemplate(props: WidgetProps) {
92
94
  type={inputType}
93
95
  value={safeValue}
94
96
  disabled={disabled}
95
- readOnly={readonly}
96
97
  autoFocus={autofocus}
97
98
  onChange={handleChange}
98
99
  onBlur={handleBlur}
99
100
  onFocus={handleFocus}
100
101
  placeholder={placeholder}
101
- className={hasError ? 'border-destructive' : ''}
102
+ title={tooltipText}
103
+ aria-invalid={hasError || undefined}
104
+ className={cn(hasError && 'border-destructive', compact && 'h-7 text-xs')}
102
105
  step={inputProps.step}
103
106
  min={inputProps.min}
104
107
  max={inputProps.max}
@@ -4,6 +4,7 @@ import { AlertCircle } from 'lucide-react';
4
4
  import React from 'react';
5
5
 
6
6
  import { Alert, AlertDescription, AlertTitle } from '@djangocfg/ui-core/components';
7
+ import { useAppT } from '@djangocfg/i18n';
7
8
  import { ErrorListProps } from '@rjsf/utils';
8
9
 
9
10
  /**
@@ -12,6 +13,7 @@ import { ErrorListProps } from '@rjsf/utils';
12
13
  */
13
14
  export function ErrorListTemplate(props: ErrorListProps) {
14
15
  const { errors } = props;
16
+ const t = useAppT();
15
17
 
16
18
  if (!errors || errors.length === 0) {
17
19
  return null;
@@ -20,7 +22,7 @@ export function ErrorListTemplate(props: ErrorListProps) {
20
22
  return (
21
23
  <Alert variant="destructive" className="mb-6">
22
24
  <AlertCircle className="h-4 w-4" />
23
- <AlertTitle>Validation Errors</AlertTitle>
25
+ <AlertTitle>{t('tools.jsonForm.validationErrors')}</AlertTitle>
24
26
  <AlertDescription>
25
27
  <ul className="list-disc list-inside space-y-1 mt-2">
26
28
  {errors.map((error, index) => (
@@ -9,6 +9,21 @@ import { ObjectFieldTemplateProps } from '@rjsf/utils';
9
9
 
10
10
  import type { JsonFormDensity, UiGroup } from '../types';
11
11
 
12
+ /**
13
+ * Static map of literal grid-column classes.
14
+ *
15
+ * Tailwind JIT only compiles classes it can see at build time, so a
16
+ * runtime-built `grid-cols-${n}` string is never emitted. Listing the
17
+ * full literal classes here keeps `ui:grid` working. Columns collapse
18
+ * to a single column on small screens for responsive behaviour.
19
+ */
20
+ const GRID_COL_CLASS: Record<number, string> = {
21
+ 1: 'grid-cols-1',
22
+ 2: 'grid-cols-1 sm:grid-cols-2',
23
+ 3: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',
24
+ 4: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-4',
25
+ };
26
+
12
27
  /**
13
28
  * Object field template for JSON Schema Form
14
29
  *
@@ -53,9 +68,14 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
53
68
  // Check if this is root object (no title usually means root)
54
69
  const isRoot = !title;
55
70
 
56
- // Grid class based on columns
57
- const gridClass = gridCols
58
- ? `grid gap-4 grid-cols-${gridCols}`
71
+ // Grid class based on columns. `ui:grid` is clamped to the 1-4 range
72
+ // covered by GRID_COL_CLASS so Tailwind always has a literal class.
73
+ const colClass =
74
+ typeof gridCols === 'number'
75
+ ? GRID_COL_CLASS[Math.min(4, Math.max(1, Math.round(gridCols)))]
76
+ : undefined;
77
+ const gridClass = colClass
78
+ ? cn('grid', compact ? 'gap-2' : 'gap-4', colClass)
59
79
  : compact
60
80
  ? 'space-y-2'
61
81
  : 'space-y-4';