@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
@@ -15,19 +15,39 @@ export interface TreeContentProps<T> {
15
15
  className?: string;
16
16
  /** Override aria-label for the container. */
17
17
  ariaLabel?: string;
18
+ /**
19
+ * Container ARIA role. Defaults to `'tree'` for standalone composition
20
+ * use. `<TreeRoot>` passes `'group'` because its own outer element
21
+ * already carries `role="tree"` + `aria-activedescendant`.
22
+ */
23
+ role?: 'tree' | 'group' | 'presentation';
18
24
  }
19
25
 
20
- export function TreeContent<T>({ children, className, ariaLabel }: TreeContentProps<T>) {
21
- const { flatRows, labels, selected, focused, matchingIds, appearance } = useTreeContext<T>();
26
+ /** DOM id for a tree row referenced by `aria-activedescendant`. */
27
+ export const treeRowDomId = (id: string) => `tree-row-${id}`;
28
+
29
+ export function TreeContent<T>({
30
+ children,
31
+ className,
32
+ ariaLabel,
33
+ role = 'tree',
34
+ }: TreeContentProps<T>) {
35
+ const { flatRows, labels, selected, focused, matchingIds, appearance, selectionMode } =
36
+ useTreeContext<T>();
22
37
 
23
38
  if (flatRows.length === 0) {
24
39
  return <TreeEmpty>{labels.empty}</TreeEmpty>;
25
40
  }
26
41
 
42
+ const isTree = role === 'tree';
43
+
27
44
  return (
28
45
  <div
29
- role="tree"
30
- aria-label={ariaLabel ?? labels.ariaLabel}
46
+ role={role}
47
+ aria-label={isTree ? (ariaLabel ?? labels.ariaLabel) : undefined}
48
+ aria-multiselectable={
49
+ isTree && selectionMode === 'multiple' ? true : undefined
50
+ }
31
51
  className={cn('relative flex flex-col py-1', className)}
32
52
  style={appearanceToStyle(appearance)}
33
53
  >
@@ -18,10 +18,16 @@ export interface TreeLabelProps {
18
18
  function TreeLabelRaw({ children, isMatchingSearch, className }: TreeLabelProps) {
19
19
  return (
20
20
  <span
21
- style={{ fontSize: 'var(--tree-font-size)' }}
21
+ // Colour is set inline via the semantic token rather than the
22
+ // `text-foreground` Tailwind utility: that class isn't always
23
+ // emitted into a consuming app's stylesheet (the bundler only
24
+ // generates utilities it sees referenced in scanned source), so
25
+ // relying on it left the label rendering as UA-default black on
26
+ // a dark surface. `var(--foreground)` always resolves.
27
+ style={{ fontSize: 'var(--tree-font-size)', color: 'var(--foreground)' }}
22
28
  className={cn(
23
29
  'truncate leading-tight tracking-[-0.005em]',
24
- isMatchingSearch && 'font-medium text-foreground',
30
+ isMatchingSearch && 'font-medium',
25
31
  className,
26
32
  )}
27
33
  >
@@ -8,6 +8,7 @@ import { useTreeContext } from '../context/TreeContext';
8
8
  import { radiusClass, rowStateClasses } from '../data/appearance';
9
9
  import type { FlatRow, TreeRowRenderProps } from '../types';
10
10
  import { TreeChevron } from './TreeChevron';
11
+ import { treeRowDomId } from './TreeContent';
11
12
  import { TreeIcon } from './TreeIcon';
12
13
  import { TreeIndentGuides } from './TreeIndentGuides';
13
14
  import { TreeLabel } from './TreeLabel';
@@ -34,6 +35,7 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
34
35
  focused,
35
36
  matchingIds,
36
37
  select,
38
+ setSelectedIds,
37
39
  toggle,
38
40
  setFocus,
39
41
  activate,
@@ -44,10 +46,11 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
44
46
  renderContextMenu,
45
47
  } = ctx;
46
48
 
47
- const { node, level, isFolder, isExpanded, isLoading } = row;
49
+ const { node, level, isFolder, isExpanded, isLoading, posInSet, setSize } = row;
48
50
  const isSelected = selected.has(node.id);
49
51
  const isFocused = focused === node.id;
50
52
  const isMatchingSearch = matchingIds.has(node.id);
53
+ const isMultiSelect = ctx.selectionMode === 'multiple';
51
54
 
52
55
  const slot: TreeRowRenderProps<T> = {
53
56
  node,
@@ -68,7 +71,13 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
68
71
  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
69
72
  if (node.disabled) return;
70
73
  setFocus(node.id);
71
- select(node.id);
74
+ // Multi-select: a plain click replaces the selection; ⌘/Ctrl-click
75
+ // toggles the clicked row (classic file-manager / VSCode behaviour).
76
+ if (isMultiSelect && !(e.metaKey || e.ctrlKey)) {
77
+ setSelectedIds([node.id]);
78
+ } else {
79
+ select(node.id);
80
+ }
72
81
  if (isFolder) {
73
82
  toggle(node.id);
74
83
  } else if (activationMode === 'single-click') {
@@ -76,7 +85,6 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
76
85
  } else if (activationMode === 'single-click-preview') {
77
86
  activate(node, { preview: true });
78
87
  }
79
- e.currentTarget.scrollIntoView?.({ block: 'nearest' });
80
88
  };
81
89
 
82
90
  const handleDoubleClick = () => {
@@ -87,11 +95,13 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
87
95
 
88
96
  const trigger = (
89
97
  <div
98
+ id={treeRowDomId(node.id)}
90
99
  role="treeitem"
91
100
  aria-level={level + 1}
101
+ aria-posinset={posInSet}
102
+ aria-setsize={setSize}
92
103
  aria-expanded={isFolder ? isExpanded : undefined}
93
- aria-selected={isSelected || undefined}
94
- aria-current={isSelected ? 'true' : undefined}
104
+ aria-selected={ctx.selectionMode === 'none' ? undefined : isSelected}
95
105
  aria-disabled={node.disabled || undefined}
96
106
  data-tree-row=""
97
107
  data-id={node.id}
@@ -100,7 +110,7 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
100
110
  data-focused={isFocused && !isSelected ? 'true' : undefined}
101
111
  data-folder={isFolder || undefined}
102
112
  data-expanded={isExpanded || undefined}
103
- tabIndex={isFocused ? 0 : -1}
113
+ tabIndex={-1}
104
114
  style={{
105
115
  paddingLeft: 6 + level * appearance.indent,
106
116
  height: 'var(--tree-row-height)',
@@ -32,12 +32,16 @@ export function flattenTree<T>({
32
32
  const out: FlatRow<T>[] = [];
33
33
 
34
34
  const walk = (nodes: TreeNode<T>[], level: number, parentId: TreeItemId | null) => {
35
- for (const node of nodes) {
36
- if (filterNode && !filterNode(node)) continue;
35
+ // Visible siblings only — `aria-setsize`/`posinset` must ignore filtered nodes.
36
+ const visible = filterNode ? nodes.filter(filterNode) : nodes;
37
+ const setSize = visible.length;
37
38
 
39
+ visible.forEach((node, index) => {
38
40
  const isFolder = isNodeFolder(node);
39
41
  const isExpanded = expandedIds.has(node.id);
40
- const resolved = isFolder ? resolveChildren(cache, node) : { children: [], status: 'loaded' as const };
42
+ const resolved = isFolder
43
+ ? resolveChildren(cache, node)
44
+ : { children: [], status: 'loaded' as const };
41
45
 
42
46
  out.push({
43
47
  node,
@@ -47,12 +51,14 @@ export function flattenTree<T>({
47
51
  isExpanded,
48
52
  isLoading: resolved.status === 'loading',
49
53
  hasError: resolved.status === 'error',
54
+ posInSet: index + 1,
55
+ setSize,
50
56
  });
51
57
 
52
58
  if (isFolder && isExpanded && resolved.children) {
53
59
  walk(resolved.children, level + 1, node.id);
54
60
  }
55
- }
61
+ });
56
62
  };
57
63
 
58
64
  walk(roots, 0, null);
@@ -174,4 +174,8 @@ export interface FlatRow<T> {
174
174
  isExpanded: boolean;
175
175
  isLoading: boolean;
176
176
  hasError: boolean;
177
+ /** 1-based position among visible siblings (for `aria-posinset`). */
178
+ posInSet: number;
179
+ /** Count of visible siblings sharing this row's parent (for `aria-setsize`). */
180
+ setSize: number;
177
181
  }
@@ -5,7 +5,8 @@ import { useUploady } from '@rpldy/uploady';
5
5
  import { Plus } from 'lucide-react';
6
6
  import { Button } from '@djangocfg/ui-core/components';
7
7
  import { cn } from '@djangocfg/ui-core/lib';
8
- import { buildAcceptString, logger } from '../utils';
8
+ import { useT } from '@djangocfg/i18n';
9
+ import { buildAcceptString, getAssetTypeFromMime, logger } from '../utils';
9
10
  import type { AssetType } from '../types';
10
11
 
11
12
  interface UploadAddButtonProps {
@@ -19,6 +20,16 @@ interface UploadAddButtonProps {
19
20
  children?: React.ReactNode;
20
21
  }
21
22
 
23
+ /** Returns true if a file's MIME type is allowed by the accepted asset types. */
24
+ function isAcceptedType(file: File, accept: AssetType[]): boolean {
25
+ if (file.type) {
26
+ if (file.type.startsWith('image/')) return accept.includes('image');
27
+ if (file.type.startsWith('audio/')) return accept.includes('audio');
28
+ if (file.type.startsWith('video/')) return accept.includes('video');
29
+ }
30
+ return accept.includes(file.type ? getAssetTypeFromMime(file.type) : 'document');
31
+ }
32
+
22
33
  export function UploadAddButton({
23
34
  accept = ['image', 'audio', 'video', 'document'],
24
35
  multiple = true,
@@ -29,6 +40,7 @@ export function UploadAddButton({
29
40
  size = 'default',
30
41
  children,
31
42
  }: UploadAddButtonProps) {
43
+ const t = useT();
32
44
  const { upload } = useUploady();
33
45
  const inputRef = useRef<HTMLInputElement>(null);
34
46
 
@@ -38,18 +50,27 @@ export function UploadAddButton({
38
50
  const fileArray = Array.from(files);
39
51
  const maxBytes = maxSizeMB * 1024 * 1024;
40
52
 
41
- const validFiles = fileArray.filter(file => {
53
+ let validFiles = fileArray.filter(file => {
42
54
  if (file.size > maxBytes) {
43
- logger.warn(`File ${file.name} exceeds max size of ${maxSizeMB}MB`);
55
+ logger.warn(`File "${file.name}" exceeds max size of ${maxSizeMB}MB`);
56
+ return false;
57
+ }
58
+ if (!isAcceptedType(file, accept)) {
59
+ logger.warn(`File "${file.name}" (${file.type || 'unknown'}) is not an accepted type`);
44
60
  return false;
45
61
  }
46
62
  return true;
47
63
  });
48
64
 
65
+ // Enforce single-file selection when multiple is disabled.
66
+ if (!multiple && validFiles.length > 1) {
67
+ validFiles = validFiles.slice(0, 1);
68
+ }
69
+
49
70
  if (validFiles.length > 0) {
50
71
  upload(validFiles);
51
72
  }
52
- }, [upload, maxSizeMB]);
73
+ }, [upload, maxSizeMB, accept, multiple]);
53
74
 
54
75
  const handleClick = useCallback(() => {
55
76
  inputRef.current?.click();
@@ -71,6 +92,8 @@ export function UploadAddButton({
71
92
  multiple={multiple}
72
93
  onChange={handleInputChange}
73
94
  className="hidden"
95
+ tabIndex={-1}
96
+ aria-hidden="true"
74
97
  disabled={disabled}
75
98
  />
76
99
  <Button
@@ -82,8 +105,8 @@ export function UploadAddButton({
82
105
  >
83
106
  {children || (
84
107
  <>
85
- <Plus className="h-4 w-4 mr-2" />
86
- Add Files
108
+ <Plus className="h-4 w-4 mr-2" aria-hidden="true" />
109
+ {t('tools.upload.upload')}
87
110
  </>
88
111
  )}
89
112
  </Button>
@@ -5,9 +5,21 @@ import { useUploady } from '@rpldy/uploady';
5
5
  import { Upload } from 'lucide-react';
6
6
  import { cn } from '@djangocfg/ui-core/lib';
7
7
  import { useT } from '@djangocfg/i18n';
8
- import { buildAcceptString, logger } from '../utils';
8
+ import { buildAcceptString, getAssetTypeFromMime, logger } from '../utils';
9
9
  import { useClipboardPaste } from '../hooks/useClipboardPaste';
10
- import type { UploadDropzoneProps } from '../types';
10
+ import type { AssetType, UploadDropzoneProps } from '../types';
11
+
12
+ /** Returns true if a file's MIME type is allowed by the accepted asset types. */
13
+ function isAcceptedType(file: File, accept: AssetType[]): boolean {
14
+ // image/audio/video are matched by prefix so unknown subtypes still pass.
15
+ if (file.type) {
16
+ if (file.type.startsWith('image/')) return accept.includes('image');
17
+ if (file.type.startsWith('audio/')) return accept.includes('audio');
18
+ if (file.type.startsWith('video/')) return accept.includes('video');
19
+ }
20
+ // No/other MIME type — fall back to asset detection (defaults to document).
21
+ return accept.includes(file.type ? getAssetTypeFromMime(file.type) : 'document');
22
+ }
11
23
 
12
24
  function useOptionalUploady(uploadFn?: (files: File[]) => void) {
13
25
  try {
@@ -37,6 +49,7 @@ export function UploadDropzone({
37
49
  const upload = useOptionalUploady(uploadFn);
38
50
  const inputRef = useRef<HTMLInputElement>(null);
39
51
  const [isDragging, setIsDragging] = useState(false);
52
+ const [rejectedCount, setRejectedCount] = useState(0);
40
53
  const dragCounter = useRef(0);
41
54
 
42
55
  // Prepare data
@@ -55,11 +68,12 @@ export function UploadDropzone({
55
68
  'relative flex flex-col items-center justify-center',
56
69
  'border-2 border-dashed rounded-lg cursor-pointer',
57
70
  'transition-colors duration-200',
71
+ 'outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
58
72
  compact ? 'p-4' : 'p-8',
59
73
  isDragging
60
74
  ? 'border-primary bg-primary/5'
61
75
  : 'border-muted-foreground/25 hover:border-muted-foreground/50',
62
- disabled && 'opacity-50 cursor-not-allowed',
76
+ disabled && 'opacity-50 cursor-not-allowed pointer-events-none',
63
77
  className
64
78
  ), [compact, isDragging, disabled, className]);
65
79
 
@@ -70,19 +84,30 @@ export function UploadDropzone({
70
84
  const handleFiles = useCallback((files: FileList | File[]) => {
71
85
  const fileArray = Array.from(files);
72
86
 
73
- const validFiles = fileArray.filter(file => {
87
+ let validFiles = fileArray.filter(file => {
74
88
  if (file.size > maxBytes) {
75
- logger.warn(`File ${file.name} exceeds max size of ${maxSizeMB}MB`);
89
+ logger.warn(`File "${file.name}" exceeds max size of ${maxSizeMB}MB`);
90
+ return false;
91
+ }
92
+ if (!isAcceptedType(file, accept)) {
93
+ logger.warn(`File "${file.name}" (${file.type || 'unknown'}) is not an accepted type`);
76
94
  return false;
77
95
  }
78
96
  return true;
79
97
  });
80
98
 
99
+ // Enforce single-file selection when multiple is disabled.
100
+ if (!multiple && validFiles.length > 1) {
101
+ validFiles = validFiles.slice(0, 1);
102
+ }
103
+
104
+ setRejectedCount(fileArray.length - validFiles.length);
105
+
81
106
  if (validFiles.length > 0) {
82
107
  onFilesSelected?.(validFiles);
83
108
  upload(validFiles);
84
109
  }
85
- }, [upload, maxBytes, maxSizeMB, onFilesSelected]);
110
+ }, [upload, maxBytes, maxSizeMB, accept, multiple, onFilesSelected]);
86
111
 
87
112
  // Build accept MIME types for clipboard paste (e.g. ['image', 'video'] → ['image', 'video'])
88
113
  const pasteAcceptTypes = useMemo(
@@ -142,6 +167,14 @@ export function UploadDropzone({
142
167
  }
143
168
  }, [disabled]);
144
169
 
170
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
171
+ if (disabled) return;
172
+ if (e.key === 'Enter' || e.key === ' ') {
173
+ e.preventDefault();
174
+ inputRef.current?.click();
175
+ }
176
+ }, [disabled]);
177
+
145
178
  const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
146
179
  if (e.target.files?.length) {
147
180
  handleFiles(e.target.files);
@@ -153,9 +186,21 @@ export function UploadDropzone({
153
186
  const displayText = isDragging ? labels.dropHere : labels.dragDrop;
154
187
  const showMaxSize = !compact;
155
188
 
189
+ // Announce drag/rejection state to assistive tech.
190
+ const liveMessage = isDragging
191
+ ? labels.dropHere
192
+ : rejectedCount > 0
193
+ ? `${rejectedCount} file(s) were not accepted`
194
+ : '';
195
+
156
196
  return (
157
197
  <div
198
+ role="button"
199
+ tabIndex={disabled ? -1 : 0}
200
+ aria-disabled={disabled || undefined}
201
+ aria-label={labels.dragDrop}
158
202
  onClick={handleClick}
203
+ onKeyDown={handleKeyDown}
159
204
  onDragEnter={handleDragEnter}
160
205
  onDragLeave={handleDragLeave}
161
206
  onDragOver={handleDragOver}
@@ -169,18 +214,29 @@ export function UploadDropzone({
169
214
  multiple={multiple}
170
215
  onChange={handleInputChange}
171
216
  className="hidden"
217
+ tabIndex={-1}
218
+ aria-hidden="true"
172
219
  disabled={disabled}
173
220
  />
174
221
 
175
222
  {children || (
176
223
  <>
177
- <Upload className={iconClassName} />
224
+ <Upload className={iconClassName} aria-hidden="true" />
178
225
  <p className={textClassName}>{displayText}</p>
179
226
  {showMaxSize && (
180
227
  <p className="text-xs text-muted-foreground/60 mt-1">{labels.maxSize}</p>
181
228
  )}
229
+ {rejectedCount > 0 && !isDragging && (
230
+ <p className="text-xs text-destructive mt-1">
231
+ {rejectedCount} file(s) skipped (size or type)
232
+ </p>
233
+ )}
182
234
  </>
183
235
  )}
236
+
237
+ <span className="sr-only" role="status" aria-live="polite">
238
+ {liveMessage}
239
+ </span>
184
240
  </div>
185
241
  );
186
242
  }
@@ -5,7 +5,7 @@ import { useUploady } from '@rpldy/uploady';
5
5
  import { Upload } from 'lucide-react';
6
6
  import { cn } from '@djangocfg/ui-core/lib';
7
7
  import { useT } from '@djangocfg/i18n';
8
- import { logger } from '../utils';
8
+ import { getAssetTypeFromMime, logger } from '../utils';
9
9
  import type { AssetType } from '../types';
10
10
 
11
11
  export interface UploadPageDropOverlayProps {
@@ -21,6 +21,16 @@ export interface UploadPageDropOverlayProps {
21
21
  onFilesDropped?: (files: File[]) => void;
22
22
  }
23
23
 
24
+ /** Returns true if a file's MIME type is allowed by the accepted asset types. */
25
+ function isAcceptedType(file: File, accept: AssetType[]): boolean {
26
+ if (file.type) {
27
+ if (file.type.startsWith('image/')) return accept.includes('image');
28
+ if (file.type.startsWith('audio/')) return accept.includes('audio');
29
+ if (file.type.startsWith('video/')) return accept.includes('video');
30
+ }
31
+ return accept.includes(file.type ? getAssetTypeFromMime(file.type) : 'document');
32
+ }
33
+
24
34
  export function UploadPageDropOverlay({
25
35
  accept = ['image', 'audio', 'video', 'document'],
26
36
  maxSizeMB = 100,
@@ -57,7 +67,11 @@ export function UploadPageDropOverlay({
57
67
 
58
68
  const validFiles = fileArray.filter(file => {
59
69
  if (file.size > maxBytes) {
60
- logger.warn(`File ${file.name} exceeds max size of ${maxSizeMB}MB`);
70
+ logger.warn(`File "${file.name}" exceeds max size of ${maxSizeMB}MB`);
71
+ return false;
72
+ }
73
+ if (!isAcceptedType(file, accept)) {
74
+ logger.warn(`File "${file.name}" (${file.type || 'unknown'}) is not an accepted type`);
61
75
  return false;
62
76
  }
63
77
  return true;
@@ -68,7 +82,7 @@ export function UploadPageDropOverlay({
68
82
  onFilesDropped?.(validFiles);
69
83
  upload(validFiles);
70
84
  }
71
- }, [upload, maxBytes, maxSizeMB, onFilesDropped]);
85
+ }, [upload, maxBytes, maxSizeMB, accept, onFilesDropped]);
72
86
 
73
87
  const handleDragEnter = useCallback((e: DragEvent) => {
74
88
  e.preventDefault();
@@ -129,7 +143,7 @@ export function UploadPageDropOverlay({
129
143
  // Prepare default content
130
144
  const defaultContent = (
131
145
  <div className="flex flex-col items-center gap-4 p-8 rounded-xl border-2 border-dashed border-primary bg-background/90">
132
- <Upload className="h-16 w-16 text-primary" />
146
+ <Upload className="h-16 w-16 text-primary" aria-hidden="true" />
133
147
  <div className="text-center">
134
148
  <p className="text-lg font-medium">{labels.dropHere}</p>
135
149
  <p className="text-sm text-muted-foreground">{labels.uploading}</p>
@@ -138,7 +152,7 @@ export function UploadPageDropOverlay({
138
152
  );
139
153
 
140
154
  return (
141
- <div className={overlayClassName}>
155
+ <div className={overlayClassName} role="status" aria-live="polite">
142
156
  {children || defaultContent}
143
157
  </div>
144
158
  );
@@ -58,7 +58,8 @@ export function UploadPreviewItem({
58
58
 
59
59
  // Prepare visibility flags
60
60
  const Icon = ASSET_ICONS[assetType];
61
- const canShowPreview = showThumbnail && !!previewUrl && assetType === 'image';
61
+ const canShowImagePreview = showThumbnail && !!previewUrl && assetType === 'image';
62
+ const canShowVideoPreview = showThumbnail && !!previewUrl && assetType === 'video';
62
63
  const canRetry = (status === 'error' || status === 'aborted') && !!onRetry;
63
64
  const canRemove = REMOVABLE_STATUSES.includes(status) && !!onRemove;
64
65
  const isUploading = status === 'uploading';
@@ -89,21 +90,21 @@ export function UploadPreviewItem({
89
90
  case 'uploading':
90
91
  return (
91
92
  <Badge variant="secondary" className="gap-1 tabular-nums">
92
- <Loader2 className="h-3 w-3 animate-spin" />
93
+ <Loader2 className="h-3 w-3 animate-spin" aria-hidden="true" />
93
94
  {progressPercent}%
94
95
  </Badge>
95
96
  );
96
97
  case 'complete':
97
98
  return (
98
- <Badge variant="default" className="gap-1 bg-green-500">
99
- <CheckCircle className="h-3 w-3" />
99
+ <Badge variant="default" className="gap-1 bg-success text-success-foreground border-transparent">
100
+ <CheckCircle className="h-3 w-3" aria-hidden="true" />
100
101
  {labels.uploaded}
101
102
  </Badge>
102
103
  );
103
104
  case 'error':
104
105
  return (
105
106
  <Badge variant="destructive" className="gap-1">
106
- <AlertCircle className="h-3 w-3" />
107
+ <AlertCircle className="h-3 w-3" aria-hidden="true" />
107
108
  {labels.failed}
108
109
  </Badge>
109
110
  );
@@ -124,15 +125,26 @@ export function UploadPreviewItem({
124
125
 
125
126
  // Prepare thumbnail content
126
127
  const thumbnailContent = useMemo(() => {
127
- if (canShowPreview) {
128
- return previewUrl ? (
129
- <img src={previewUrl} alt={file.name} className="w-full h-full object-cover" />
130
- ) : (
131
- <Skeleton className="w-full h-full" />
128
+ if (canShowImagePreview && previewUrl) {
129
+ return <img src={previewUrl} alt={file.name} className="w-full h-full object-cover" />;
130
+ }
131
+ if (canShowVideoPreview && previewUrl) {
132
+ return (
133
+ <video
134
+ src={previewUrl}
135
+ className="w-full h-full object-cover"
136
+ muted
137
+ playsInline
138
+ preload="metadata"
139
+ aria-label={file.name}
140
+ />
132
141
  );
133
142
  }
134
- return <Icon className="h-6 w-6 text-muted-foreground" />;
135
- }, [canShowPreview, previewUrl, file.name, Icon]);
143
+ if (showThumbnail && !previewUrl && (assetType === 'image' || assetType === 'video')) {
144
+ return <Skeleton className="w-full h-full" />;
145
+ }
146
+ return <Icon className="h-6 w-6 text-muted-foreground" aria-hidden="true" />;
147
+ }, [canShowImagePreview, canShowVideoPreview, showThumbnail, previewUrl, assetType, file.name, Icon]);
136
148
 
137
149
  return (
138
150
  <div className={containerClassName}>
@@ -158,7 +170,13 @@ export function UploadPreviewItem({
158
170
 
159
171
  <p className="text-xs text-muted-foreground tabular-nums">{fileSize}</p>
160
172
 
161
- {isUploading && <Progress value={progressPercent} className="h-1 mt-1" />}
173
+ {isUploading && (
174
+ <Progress
175
+ value={progressPercent}
176
+ className="h-1 mt-1"
177
+ aria-label={`${progressPercent}%`}
178
+ />
179
+ )}
162
180
 
163
181
  {hasError && (
164
182
  <Tooltip>
@@ -179,8 +197,14 @@ export function UploadPreviewItem({
179
197
  {canRetry && (
180
198
  <Tooltip>
181
199
  <TooltipTrigger asChild>
182
- <Button variant="ghost" size="icon" className="h-8 w-8" onClick={handleRetry}>
183
- <RotateCcw className="h-4 w-4" />
200
+ <Button
201
+ variant="ghost"
202
+ size="icon"
203
+ className="h-8 w-8"
204
+ onClick={handleRetry}
205
+ aria-label={labels.retry}
206
+ >
207
+ <RotateCcw className="h-4 w-4" aria-hidden="true" />
184
208
  </Button>
185
209
  </TooltipTrigger>
186
210
  <TooltipContent>{labels.retry}</TooltipContent>
@@ -190,8 +214,14 @@ export function UploadPreviewItem({
190
214
  {canRemove && (
191
215
  <Tooltip>
192
216
  <TooltipTrigger asChild>
193
- <Button variant="ghost" size="icon" className="h-8 w-8" onClick={handleRemove}>
194
- <X className="h-4 w-4" />
217
+ <Button
218
+ variant="ghost"
219
+ size="icon"
220
+ className="h-8 w-8"
221
+ onClick={handleRemove}
222
+ aria-label={removeTooltip}
223
+ >
224
+ <X className="h-4 w-4" aria-hidden="true" />
195
225
  </Button>
196
226
  </TooltipTrigger>
197
227
  <TooltipContent>{removeTooltip}</TooltipContent>