@djangocfg/ui-tools 2.1.407 → 2.1.409

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/README.md +9 -10
  2. package/dist/file-icon/index.cjs +449 -61
  3. package/dist/file-icon/index.cjs.map +1 -1
  4. package/dist/file-icon/index.d.cts +56 -18
  5. package/dist/file-icon/index.d.ts +56 -18
  6. package/dist/file-icon/index.mjs +448 -62
  7. package/dist/file-icon/index.mjs.map +1 -1
  8. package/dist/tree/index.cjs +49 -22
  9. package/dist/tree/index.cjs.map +1 -1
  10. package/dist/tree/index.d.cts +9 -3
  11. package/dist/tree/index.d.ts +9 -3
  12. package/dist/tree/index.mjs +49 -22
  13. package/dist/tree/index.mjs.map +1 -1
  14. package/dist/{types-B_zhyAqR.d.cts → types-eEu8SeiQ.d.cts} +4 -0
  15. package/dist/{types-B_zhyAqR.d.ts → types-eEu8SeiQ.d.ts} +4 -0
  16. package/package.json +8 -13
  17. package/src/components/FloatingToolbar/index.tsx +37 -3
  18. package/src/lib/page-snapshot/__tests__/capture-integration.test.ts +85 -0
  19. package/src/lib/page-snapshot/__tests__/engine.test.ts +36 -0
  20. package/src/lib/page-snapshot/__tests__/redaction-integration.test.ts +99 -0
  21. package/src/lib/page-snapshot/__tests__/tokens.test.ts +17 -0
  22. package/src/lib/page-snapshot/capture/__tests__/budget.test.ts +49 -0
  23. package/src/lib/page-snapshot/capture/__tests__/chrome-filter.test.ts +47 -0
  24. package/src/lib/page-snapshot/capture/__tests__/fold.test.ts +66 -0
  25. package/src/lib/page-snapshot/capture/__tests__/scope.test.ts +74 -0
  26. package/src/lib/page-snapshot/capture/__tests__/walk.test.ts +129 -0
  27. package/src/lib/page-snapshot/capture/accessible-name.ts +73 -0
  28. package/src/lib/page-snapshot/capture/budget.ts +95 -0
  29. package/src/lib/page-snapshot/capture/chrome-filter.ts +81 -0
  30. package/src/lib/page-snapshot/capture/classify.ts +111 -0
  31. package/src/lib/page-snapshot/capture/dom-utils.ts +111 -0
  32. package/src/lib/page-snapshot/capture/fold.ts +96 -0
  33. package/src/lib/page-snapshot/capture/scope.ts +169 -0
  34. package/src/lib/page-snapshot/capture/walk.ts +250 -0
  35. package/src/lib/page-snapshot/cst/__tests__/serialize.test.ts +50 -0
  36. package/src/lib/page-snapshot/cst/directives.ts +47 -0
  37. package/src/lib/page-snapshot/cst/payload.ts +50 -0
  38. package/src/lib/page-snapshot/cst/serialize.ts +84 -0
  39. package/src/lib/page-snapshot/cst/types.ts +115 -0
  40. package/src/lib/page-snapshot/engine.ts +176 -0
  41. package/src/lib/page-snapshot/index.ts +93 -0
  42. package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +72 -0
  43. package/src/lib/page-snapshot/react/PageSnapshotPreview.tsx +78 -0
  44. package/src/lib/page-snapshot/react/__tests__/PageSnapshotChip.test.tsx +54 -0
  45. package/src/lib/page-snapshot/react/__tests__/provider.test.tsx +103 -0
  46. package/src/lib/page-snapshot/react/__tests__/use-page-snapshot-toggle.test.tsx +62 -0
  47. package/src/lib/page-snapshot/react/provider.tsx +162 -0
  48. package/src/lib/page-snapshot/react/use-page-snapshot-toggle.ts +47 -0
  49. package/src/lib/page-snapshot/react/use-page-snapshot.ts +67 -0
  50. package/src/lib/page-snapshot/redaction/__tests__/audit.test.ts +25 -0
  51. package/src/lib/page-snapshot/redaction/__tests__/heuristics.test.ts +73 -0
  52. package/src/lib/page-snapshot/redaction/__tests__/luhn.test.ts +26 -0
  53. package/src/lib/page-snapshot/redaction/__tests__/patterns.test.ts +60 -0
  54. package/src/lib/page-snapshot/redaction/audit.ts +58 -0
  55. package/src/lib/page-snapshot/redaction/heuristics.ts +75 -0
  56. package/src/lib/page-snapshot/redaction/index.ts +75 -0
  57. package/src/lib/page-snapshot/redaction/luhn.ts +25 -0
  58. package/src/lib/page-snapshot/redaction/patterns.ts +111 -0
  59. package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +24 -0
  60. package/src/lib/page-snapshot/refs/registry.ts +46 -0
  61. package/src/lib/page-snapshot/staleness/__tests__/hash.test.ts +34 -0
  62. package/src/lib/page-snapshot/staleness/hash.ts +20 -0
  63. package/src/lib/page-snapshot/tokens.ts +15 -0
  64. package/src/tools/AudioPlayer/context/PlayerProvider.tsx +13 -14
  65. package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +55 -6
  66. package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +2 -5
  67. package/src/tools/Chat/README.md +277 -39
  68. package/src/tools/Chat/composer/Composer.tsx +471 -0
  69. package/src/tools/Chat/composer/ComposerActionBar.tsx +65 -0
  70. package/src/tools/Chat/composer/ComposerBanner.tsx +128 -0
  71. package/src/tools/Chat/composer/ComposerButton.tsx +64 -0
  72. package/src/tools/Chat/composer/ComposerFooter.tsx +90 -0
  73. package/src/tools/Chat/composer/ComposerMenuButton.tsx +62 -0
  74. package/src/tools/Chat/composer/ComposerModelPicker.tsx +104 -0
  75. package/src/tools/Chat/composer/ComposerRichTextarea.tsx +88 -0
  76. package/src/tools/Chat/composer/ComposerToolPill.tsx +95 -0
  77. package/src/tools/Chat/composer/index.ts +45 -0
  78. package/src/tools/Chat/composer/size-context.tsx +26 -0
  79. package/src/tools/Chat/composer/types.ts +143 -0
  80. package/src/tools/Chat/composer/useComposerActions.tsx +164 -0
  81. package/src/tools/Chat/context/ChatProvider.tsx +54 -3
  82. package/src/tools/Chat/core/__tests__/metadata.test.ts +69 -0
  83. package/src/tools/Chat/core/index.ts +23 -1
  84. package/src/tools/Chat/core/markdown.ts +1 -1
  85. package/src/tools/Chat/core/metadata.ts +47 -0
  86. package/src/tools/Chat/core/payload-dispatch.ts +1 -1
  87. package/src/tools/Chat/core/transport/http.ts +71 -32
  88. package/src/tools/Chat/core/transport/sse.ts +18 -10
  89. package/src/tools/Chat/highlight/HighlightOverlay.tsx +101 -0
  90. package/src/tools/Chat/highlight/README.md +103 -0
  91. package/src/tools/Chat/highlight/SpotlightCanvas.tsx +153 -0
  92. package/src/tools/Chat/highlight/__tests__/HighlightOverlay.test.tsx +112 -0
  93. package/src/tools/Chat/highlight/__tests__/resolveRef.test.ts +55 -0
  94. package/src/tools/Chat/highlight/index.ts +21 -0
  95. package/src/tools/Chat/highlight/resolveRef.ts +42 -0
  96. package/src/tools/Chat/highlight/types.ts +49 -0
  97. package/src/tools/Chat/highlight/useHighlightTargets.ts +128 -0
  98. package/src/tools/Chat/hooks/index.ts +0 -5
  99. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +28 -47
  100. package/src/tools/Chat/hooks/useChat.ts +47 -14
  101. package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
  102. package/src/tools/Chat/hooks/useChatLayout.ts +1 -1
  103. package/src/tools/Chat/hooks/useStreamEndFocus.ts +54 -0
  104. package/src/tools/Chat/index.ts +25 -219
  105. package/src/tools/Chat/launcher/ChatDock.tsx +1 -1
  106. package/src/tools/Chat/launcher/ChatLauncher.tsx +1 -1
  107. package/src/tools/Chat/launcher/{ChatHeader.tsx → header/ChatHeader.tsx} +24 -11
  108. package/src/tools/Chat/launcher/{ChatHeaderActionButton.tsx → header/ChatHeaderActionButton.tsx} +34 -3
  109. package/src/tools/Chat/launcher/{ChatHeaderLanguageButton.tsx → header/ChatHeaderLanguageButton.tsx} +2 -2
  110. package/src/tools/Chat/launcher/{ChatHeaderModeToggle.tsx → header/ChatHeaderModeToggle.tsx} +1 -1
  111. package/src/tools/Chat/launcher/{ChatHeaderResetButton.tsx → header/ChatHeaderResetButton.tsx} +2 -1
  112. package/src/tools/Chat/launcher/{HeaderSlots.tsx → header/HeaderSlots.tsx} +3 -3
  113. package/src/tools/Chat/launcher/header/index.ts +26 -0
  114. package/src/tools/Chat/launcher/index.ts +3 -10
  115. package/src/tools/Chat/lazy.tsx +38 -284
  116. package/src/tools/Chat/{components → messages}/MessageBubble.tsx +58 -5
  117. package/src/tools/Chat/{components → messages}/MessageList.tsx +8 -25
  118. package/src/tools/Chat/messages/blocks/MessageBlocks.tsx +131 -0
  119. package/src/tools/Chat/messages/blocks/builtin.tsx +91 -0
  120. package/src/tools/Chat/messages/blocks/index.ts +12 -0
  121. package/src/tools/Chat/messages/blocks/registry.tsx +42 -0
  122. package/src/tools/Chat/messages/blocks/renderers/AudioBlock.tsx +20 -0
  123. package/src/tools/Chat/messages/blocks/renderers/CodeBlock.tsx +19 -0
  124. package/src/tools/Chat/messages/blocks/renderers/GalleryBlock.tsx +26 -0
  125. package/src/tools/Chat/messages/blocks/renderers/ImageBlock.tsx +27 -0
  126. package/src/tools/Chat/messages/blocks/renderers/JsonBlock.tsx +12 -0
  127. package/src/tools/Chat/messages/blocks/renderers/LottieBlock.tsx +11 -0
  128. package/src/tools/Chat/messages/blocks/renderers/MapBlock.tsx +36 -0
  129. package/src/tools/Chat/messages/blocks/renderers/MermaidBlock.tsx +11 -0
  130. package/src/tools/Chat/messages/blocks/renderers/VideoBlock.tsx +24 -0
  131. package/src/tools/Chat/messages/blocks/renderers/types.ts +8 -0
  132. package/src/tools/Chat/{components → messages}/index.ts +11 -5
  133. package/src/tools/Chat/public.ts +212 -0
  134. package/src/tools/Chat/shell/ChatRoot.tsx +345 -0
  135. package/src/tools/Chat/{components → shell}/EmptyState.tsx +4 -2
  136. package/src/tools/Chat/shell/index.ts +15 -0
  137. package/src/tools/Chat/types/block.ts +120 -0
  138. package/src/tools/Chat/types/config.ts +0 -5
  139. package/src/tools/Chat/types/index.ts +17 -0
  140. package/src/tools/Chat/types/message.ts +3 -0
  141. package/src/tools/Chat/utils/index.ts +4 -0
  142. package/src/tools/CodeEditor/README.md +4 -6
  143. package/src/tools/CodeEditor/components/DiffEditor.tsx +48 -13
  144. package/src/tools/CodeEditor/components/Editor.tsx +96 -44
  145. package/src/tools/CodeEditor/context/EditorProvider.tsx +34 -17
  146. package/src/tools/CodeEditor/hooks/useEditorTheme.ts +92 -99
  147. package/src/tools/CodeEditor/hooks/useMonaco.ts +37 -22
  148. package/src/tools/CodeEditor/lazy.tsx +6 -0
  149. package/src/tools/CodeEditor/lib/index.ts +1 -1
  150. package/src/tools/CodeEditor/lib/themes.ts +3 -39
  151. package/src/tools/CronScheduler/CronScheduler.client.tsx +230 -61
  152. package/src/tools/CronScheduler/components/CustomInput.tsx +21 -4
  153. package/src/tools/CronScheduler/components/DayChips.tsx +13 -11
  154. package/src/tools/CronScheduler/components/MonthDayGrid.tsx +4 -4
  155. package/src/tools/CronScheduler/components/SchedulePreview.tsx +7 -3
  156. package/src/tools/CronScheduler/components/TimeSelector.tsx +1 -1
  157. package/src/tools/CronScheduler/index.tsx +1 -1
  158. package/src/tools/CronScheduler/types/index.ts +8 -3
  159. package/src/tools/CronScheduler/utils/cron-humanize.ts +61 -16
  160. package/src/tools/CronScheduler/utils/cron-parser.ts +13 -4
  161. package/src/tools/FileIcon/FileIcon.tsx +24 -39
  162. package/src/tools/FileIcon/get-file-icon.ts +73 -0
  163. package/src/tools/FileIcon/icons/icon-data.ts +399 -0
  164. package/src/tools/FileIcon/index.ts +4 -0
  165. package/src/tools/FileIcon/loader.ts +17 -35
  166. package/src/tools/FileIcon/specialFolders.ts +18 -0
  167. package/src/tools/Gallery/components/lightbox/GalleryLightbox.tsx +112 -35
  168. package/src/tools/Gallery/components/media/GalleryVideo.tsx +21 -2
  169. package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +11 -1
  170. package/src/tools/Gallery/hooks/usePreloadImages.ts +54 -7
  171. package/src/tools/ImageViewer/components/ImageInfo.tsx +12 -1
  172. package/src/tools/ImageViewer/components/ImageToolbar.tsx +51 -43
  173. package/src/tools/ImageViewer/components/ImageViewer.tsx +96 -24
  174. package/src/tools/ImageViewer/hooks/useImageLoading.ts +13 -0
  175. package/src/tools/ImageViewer/utils/constants.ts +3 -0
  176. package/src/tools/ImageViewer/utils/index.ts +1 -0
  177. package/src/tools/JsonForm/JsonSchemaForm.tsx +4 -1
  178. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +5 -3
  179. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +7 -4
  180. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +3 -1
  181. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +23 -3
  182. package/src/tools/JsonForm/widgets/ColorWidget.tsx +20 -12
  183. package/src/tools/JsonForm/widgets/NumberWidget.tsx +14 -9
  184. package/src/tools/JsonForm/widgets/RadioWidget.tsx +78 -0
  185. package/src/tools/JsonForm/widgets/SelectWidget.tsx +1 -0
  186. package/src/tools/JsonForm/widgets/SliderWidget.tsx +7 -4
  187. package/src/tools/JsonForm/widgets/TextWidget.tsx +41 -17
  188. package/src/tools/JsonForm/widgets/index.ts +1 -0
  189. package/src/tools/JsonTree/components/JsonContent.tsx +115 -40
  190. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +177 -72
  191. package/src/tools/LottiePlayer/index.tsx +14 -4
  192. package/src/tools/LottiePlayer/lazy.tsx +11 -3
  193. package/src/tools/LottiePlayer/types.ts +31 -1
  194. package/src/tools/LottiePlayer/useLottie.ts +32 -9
  195. package/src/tools/LottiePlayer/usePrefersReducedMotion.ts +46 -0
  196. package/src/tools/Map/components/LayerSwitcher.tsx +54 -21
  197. package/src/tools/Map/components/MapCluster.tsx +28 -21
  198. package/src/tools/Map/components/MapContainer.tsx +11 -4
  199. package/src/tools/Map/components/MapLegend.tsx +46 -15
  200. package/src/tools/Map/components/MapMarker.tsx +31 -2
  201. package/src/tools/Map/hooks/useMapEvents.ts +64 -105
  202. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +61 -6
  203. package/src/tools/MarkdownEditor/MentionList.tsx +37 -4
  204. package/src/tools/MarkdownEditor/createMentionSuggestion.ts +11 -0
  205. package/src/tools/MarkdownEditor/lazy.tsx +32 -7
  206. package/src/tools/MarkdownEditor/styles.css +13 -0
  207. package/src/tools/MarkdownMessage/CodeBlock.tsx +40 -17
  208. package/src/tools/MarkdownMessage/MarkdownMessage.tsx +26 -6
  209. package/src/tools/MarkdownMessage/components.tsx +22 -9
  210. package/src/tools/MarkdownMessage/types.ts +24 -1
  211. package/src/tools/Mermaid/Mermaid.client.tsx +27 -5
  212. package/src/tools/Mermaid/components/MermaidErrorPanel.tsx +31 -0
  213. package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +14 -17
  214. package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +264 -168
  215. package/src/tools/Mermaid/hooks/useMermaidValidation.ts +76 -10
  216. package/src/tools/Mermaid/index.tsx +6 -0
  217. package/src/tools/Mermaid/utils/mermaid-helpers.ts +141 -18
  218. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +11 -1
  219. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +49 -20
  220. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +7 -0
  221. package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +7 -4
  222. package/src/tools/OpenapiViewer/constants.ts +3 -0
  223. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +73 -11
  224. package/src/tools/OpenapiViewer/utils/schemaExport.ts +26 -6
  225. package/src/tools/PrettyCode/PrettyCode.client.tsx +23 -16
  226. package/src/tools/PrettyCode/lazy.tsx +1 -1
  227. package/src/tools/SpeechRecognition/README.md +1 -1
  228. package/src/tools/SpeechRecognition/__tests__/language.test.ts +9 -3
  229. package/src/tools/SpeechRecognition/components/RecordingPulse.tsx +59 -0
  230. package/src/tools/SpeechRecognition/components/index.ts +2 -0
  231. package/src/tools/SpeechRecognition/core/engine/external.ts +24 -7
  232. package/src/tools/SpeechRecognition/core/language.ts +23 -6
  233. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +36 -5
  234. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +18 -11
  235. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +94 -26
  236. package/src/tools/SpeechRecognition/widgets/index.ts +1 -1
  237. package/src/tools/Tree/README.md +4 -8
  238. package/src/tools/Tree/TreeRoot.tsx +22 -10
  239. package/src/tools/Tree/components/TreeContent.tsx +24 -4
  240. package/src/tools/Tree/components/TreeLabel.tsx +8 -2
  241. package/src/tools/Tree/components/TreeRow.tsx +16 -6
  242. package/src/tools/Tree/data/flatten.ts +10 -4
  243. package/src/tools/Tree/types.ts +4 -0
  244. package/src/tools/Uploader/components/UploadAddButton.tsx +29 -6
  245. package/src/tools/Uploader/components/UploadDropzone.tsx +63 -7
  246. package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +19 -5
  247. package/src/tools/Uploader/components/UploadPreviewItem.tsx +47 -17
  248. package/src/tools/Uploader/components/UploadPreviewList.tsx +24 -12
  249. package/src/tools/Uploader/utils/formatters.ts +8 -3
  250. package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +1 -0
  251. package/src/tools/VideoPlayer/canvas/{jsx.d.ts → jsx-augmentation.ts} +12 -19
  252. package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +1 -0
  253. package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +1 -0
  254. package/src/tools/VideoPlayer/parts/fullscreen.tsx +1 -1
  255. package/src/tools/VideoPlayer/parts/pip.tsx +1 -1
  256. package/src/tools/VideoPlayer/parts/playback-rate.tsx +1 -1
  257. package/src/tools/VideoPlayer/parts/seek-bar.tsx +2 -2
  258. package/src/tools/VideoPlayer/parts/volume.tsx +2 -2
  259. package/src/tools/index.ts +2 -1
  260. package/src/tools/Chat/components/AudioToggle.tsx +0 -78
  261. package/src/tools/Chat/components/ChatRoot.tsx +0 -305
  262. package/src/tools/Chat/components/Composer.tsx +0 -216
  263. package/src/tools/Chat/hooks/useChatScroll.ts +0 -145
  264. package/src/tools/Chat/types.ts +0 -9
  265. package/src/tools/JsonTree/components/JsonToolbar.tsx +0 -95
  266. package/src/tools/JsonTree/hooks/useElementCorner.ts +0 -84
  267. package/src/tools/JsonTree/hooks/useNavbarHeight.ts +0 -83
  268. package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +0 -121
  269. package/src/tools/Tour/README.md +0 -373
  270. package/src/tools/Tour/components/Tour.tsx +0 -12
  271. package/src/tools/Tour/components/TourContent.tsx +0 -171
  272. package/src/tools/Tour/components/TourNavigation.tsx +0 -77
  273. package/src/tools/Tour/components/TourProgress.tsx +0 -88
  274. package/src/tools/Tour/components/TourSpotlight.tsx +0 -199
  275. package/src/tools/Tour/components/index.ts +0 -5
  276. package/src/tools/Tour/context/TourContext.ts +0 -19
  277. package/src/tools/Tour/context/TourProvider.tsx +0 -292
  278. package/src/tools/Tour/context/index.ts +0 -2
  279. package/src/tools/Tour/hooks/index.ts +0 -3
  280. package/src/tools/Tour/hooks/useKeyboardNavigation.ts +0 -59
  281. package/src/tools/Tour/hooks/useStepTarget.ts +0 -121
  282. package/src/tools/Tour/hooks/useTour.ts +0 -42
  283. package/src/tools/Tour/index.ts +0 -38
  284. package/src/tools/Tour/types/index.ts +0 -224
  285. package/src/tools/Tour/utils/dom.ts +0 -98
  286. package/src/tools/Tour/utils/index.ts +0 -3
  287. package/src/tools/Tour/utils/logger.ts +0 -3
  288. package/src/tools/Tour/utils/scrollIntoView.ts +0 -24
  289. /package/src/tools/Chat/{config.ts → constants.ts} +0 -0
  290. /package/src/tools/Chat/launcher/{ChatHeaderAudioToggle.tsx → header/ChatHeaderAudioToggle.tsx} +0 -0
  291. /package/src/tools/Chat/{components → messages}/Attachments.tsx +0 -0
  292. /package/src/tools/Chat/{components → messages}/JumpToLatest.tsx +0 -0
  293. /package/src/tools/Chat/{components → messages}/MessageActions.tsx +0 -0
  294. /package/src/tools/Chat/{components → messages}/Sources.tsx +0 -0
  295. /package/src/tools/Chat/{components → messages}/StreamingIndicator.tsx +0 -0
  296. /package/src/tools/Chat/{components → messages}/ToolCalls.tsx +0 -0
  297. /package/src/tools/Chat/{components → shell}/ErrorBanner.tsx +0 -0
@@ -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 }}
@@ -222,11 +289,12 @@ export function ImageViewer({
222
289
  className="max-w-full max-h-full object-contain select-none"
223
290
  style={{
224
291
  transform: transformStyle,
225
- transition: useProgressiveLoading ? 'transform 0.15s ease-out, opacity 0.3s ease-out' : 'transform 0.15s ease-out',
226
- 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,
227
295
  }}
228
296
  draggable={false}
229
- crossOrigin="anonymous"
297
+ onLoad={() => setImgDecoded(true)}
230
298
  onError={() => setLoadError(true)}
231
299
  />
232
300
  )}
@@ -240,18 +308,22 @@ export function ImageViewer({
240
308
  <button
241
309
  type="button"
242
310
  onClick={prev}
243
- 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"
244
314
  >
245
315
  <ChevronLeft className="h-5 w-5" />
246
316
  </button>
247
317
  <button
248
318
  type="button"
249
319
  onClick={next}
250
- 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"
251
323
  >
252
324
  <ChevronRight className="h-5 w-5" />
253
325
  </button>
254
- <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">
255
327
  {currentIndex + 1} / {images.length}
256
328
  </div>
257
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';