@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.
- package/README.md +9 -10
- package/dist/file-icon/index.cjs +449 -61
- package/dist/file-icon/index.cjs.map +1 -1
- package/dist/file-icon/index.d.cts +56 -18
- package/dist/file-icon/index.d.ts +56 -18
- package/dist/file-icon/index.mjs +448 -62
- package/dist/file-icon/index.mjs.map +1 -1
- package/dist/tree/index.cjs +49 -22
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +9 -3
- package/dist/tree/index.d.ts +9 -3
- package/dist/tree/index.mjs +49 -22
- package/dist/tree/index.mjs.map +1 -1
- package/dist/{types-B_zhyAqR.d.cts → types-eEu8SeiQ.d.cts} +4 -0
- package/dist/{types-B_zhyAqR.d.ts → types-eEu8SeiQ.d.ts} +4 -0
- package/package.json +8 -13
- package/src/components/FloatingToolbar/index.tsx +37 -3
- package/src/lib/page-snapshot/__tests__/capture-integration.test.ts +85 -0
- package/src/lib/page-snapshot/__tests__/engine.test.ts +36 -0
- package/src/lib/page-snapshot/__tests__/redaction-integration.test.ts +99 -0
- package/src/lib/page-snapshot/__tests__/tokens.test.ts +17 -0
- package/src/lib/page-snapshot/capture/__tests__/budget.test.ts +49 -0
- package/src/lib/page-snapshot/capture/__tests__/chrome-filter.test.ts +47 -0
- package/src/lib/page-snapshot/capture/__tests__/fold.test.ts +66 -0
- package/src/lib/page-snapshot/capture/__tests__/scope.test.ts +74 -0
- package/src/lib/page-snapshot/capture/__tests__/walk.test.ts +129 -0
- package/src/lib/page-snapshot/capture/accessible-name.ts +73 -0
- package/src/lib/page-snapshot/capture/budget.ts +95 -0
- package/src/lib/page-snapshot/capture/chrome-filter.ts +81 -0
- package/src/lib/page-snapshot/capture/classify.ts +111 -0
- package/src/lib/page-snapshot/capture/dom-utils.ts +111 -0
- package/src/lib/page-snapshot/capture/fold.ts +96 -0
- package/src/lib/page-snapshot/capture/scope.ts +169 -0
- package/src/lib/page-snapshot/capture/walk.ts +250 -0
- package/src/lib/page-snapshot/cst/__tests__/serialize.test.ts +50 -0
- package/src/lib/page-snapshot/cst/directives.ts +47 -0
- package/src/lib/page-snapshot/cst/payload.ts +50 -0
- package/src/lib/page-snapshot/cst/serialize.ts +84 -0
- package/src/lib/page-snapshot/cst/types.ts +115 -0
- package/src/lib/page-snapshot/engine.ts +176 -0
- package/src/lib/page-snapshot/index.ts +93 -0
- package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +72 -0
- package/src/lib/page-snapshot/react/PageSnapshotPreview.tsx +78 -0
- package/src/lib/page-snapshot/react/__tests__/PageSnapshotChip.test.tsx +54 -0
- package/src/lib/page-snapshot/react/__tests__/provider.test.tsx +103 -0
- package/src/lib/page-snapshot/react/__tests__/use-page-snapshot-toggle.test.tsx +62 -0
- package/src/lib/page-snapshot/react/provider.tsx +162 -0
- package/src/lib/page-snapshot/react/use-page-snapshot-toggle.ts +47 -0
- package/src/lib/page-snapshot/react/use-page-snapshot.ts +67 -0
- package/src/lib/page-snapshot/redaction/__tests__/audit.test.ts +25 -0
- package/src/lib/page-snapshot/redaction/__tests__/heuristics.test.ts +73 -0
- package/src/lib/page-snapshot/redaction/__tests__/luhn.test.ts +26 -0
- package/src/lib/page-snapshot/redaction/__tests__/patterns.test.ts +60 -0
- package/src/lib/page-snapshot/redaction/audit.ts +58 -0
- package/src/lib/page-snapshot/redaction/heuristics.ts +75 -0
- package/src/lib/page-snapshot/redaction/index.ts +75 -0
- package/src/lib/page-snapshot/redaction/luhn.ts +25 -0
- package/src/lib/page-snapshot/redaction/patterns.ts +111 -0
- package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +24 -0
- package/src/lib/page-snapshot/refs/registry.ts +46 -0
- package/src/lib/page-snapshot/staleness/__tests__/hash.test.ts +34 -0
- package/src/lib/page-snapshot/staleness/hash.ts +20 -0
- package/src/lib/page-snapshot/tokens.ts +15 -0
- package/src/tools/AudioPlayer/context/PlayerProvider.tsx +13 -14
- package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +55 -6
- package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +2 -5
- package/src/tools/Chat/README.md +277 -39
- package/src/tools/Chat/composer/Composer.tsx +471 -0
- package/src/tools/Chat/composer/ComposerActionBar.tsx +65 -0
- package/src/tools/Chat/composer/ComposerBanner.tsx +128 -0
- package/src/tools/Chat/composer/ComposerButton.tsx +64 -0
- package/src/tools/Chat/composer/ComposerFooter.tsx +90 -0
- package/src/tools/Chat/composer/ComposerMenuButton.tsx +62 -0
- package/src/tools/Chat/composer/ComposerModelPicker.tsx +104 -0
- package/src/tools/Chat/composer/ComposerRichTextarea.tsx +88 -0
- package/src/tools/Chat/composer/ComposerToolPill.tsx +95 -0
- package/src/tools/Chat/composer/index.ts +45 -0
- package/src/tools/Chat/composer/size-context.tsx +26 -0
- package/src/tools/Chat/composer/types.ts +143 -0
- package/src/tools/Chat/composer/useComposerActions.tsx +164 -0
- package/src/tools/Chat/context/ChatProvider.tsx +54 -3
- package/src/tools/Chat/core/__tests__/metadata.test.ts +69 -0
- package/src/tools/Chat/core/index.ts +23 -1
- package/src/tools/Chat/core/markdown.ts +1 -1
- package/src/tools/Chat/core/metadata.ts +47 -0
- package/src/tools/Chat/core/payload-dispatch.ts +1 -1
- package/src/tools/Chat/core/transport/http.ts +71 -32
- package/src/tools/Chat/core/transport/sse.ts +18 -10
- package/src/tools/Chat/highlight/HighlightOverlay.tsx +101 -0
- package/src/tools/Chat/highlight/README.md +103 -0
- package/src/tools/Chat/highlight/SpotlightCanvas.tsx +153 -0
- package/src/tools/Chat/highlight/__tests__/HighlightOverlay.test.tsx +112 -0
- package/src/tools/Chat/highlight/__tests__/resolveRef.test.ts +55 -0
- package/src/tools/Chat/highlight/index.ts +21 -0
- package/src/tools/Chat/highlight/resolveRef.ts +42 -0
- package/src/tools/Chat/highlight/types.ts +49 -0
- package/src/tools/Chat/highlight/useHighlightTargets.ts +128 -0
- package/src/tools/Chat/hooks/index.ts +0 -5
- package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +28 -47
- package/src/tools/Chat/hooks/useChat.ts +47 -14
- package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
- package/src/tools/Chat/hooks/useChatLayout.ts +1 -1
- package/src/tools/Chat/hooks/useStreamEndFocus.ts +54 -0
- package/src/tools/Chat/index.ts +25 -219
- package/src/tools/Chat/launcher/ChatDock.tsx +1 -1
- package/src/tools/Chat/launcher/ChatLauncher.tsx +1 -1
- package/src/tools/Chat/launcher/{ChatHeader.tsx → header/ChatHeader.tsx} +24 -11
- package/src/tools/Chat/launcher/{ChatHeaderActionButton.tsx → header/ChatHeaderActionButton.tsx} +34 -3
- package/src/tools/Chat/launcher/{ChatHeaderLanguageButton.tsx → header/ChatHeaderLanguageButton.tsx} +2 -2
- package/src/tools/Chat/launcher/{ChatHeaderModeToggle.tsx → header/ChatHeaderModeToggle.tsx} +1 -1
- package/src/tools/Chat/launcher/{ChatHeaderResetButton.tsx → header/ChatHeaderResetButton.tsx} +2 -1
- package/src/tools/Chat/launcher/{HeaderSlots.tsx → header/HeaderSlots.tsx} +3 -3
- package/src/tools/Chat/launcher/header/index.ts +26 -0
- package/src/tools/Chat/launcher/index.ts +3 -10
- package/src/tools/Chat/lazy.tsx +38 -284
- package/src/tools/Chat/{components → messages}/MessageBubble.tsx +58 -5
- package/src/tools/Chat/{components → messages}/MessageList.tsx +8 -25
- package/src/tools/Chat/messages/blocks/MessageBlocks.tsx +131 -0
- package/src/tools/Chat/messages/blocks/builtin.tsx +91 -0
- package/src/tools/Chat/messages/blocks/index.ts +12 -0
- package/src/tools/Chat/messages/blocks/registry.tsx +42 -0
- package/src/tools/Chat/messages/blocks/renderers/AudioBlock.tsx +20 -0
- package/src/tools/Chat/messages/blocks/renderers/CodeBlock.tsx +19 -0
- package/src/tools/Chat/messages/blocks/renderers/GalleryBlock.tsx +26 -0
- package/src/tools/Chat/messages/blocks/renderers/ImageBlock.tsx +27 -0
- package/src/tools/Chat/messages/blocks/renderers/JsonBlock.tsx +12 -0
- package/src/tools/Chat/messages/blocks/renderers/LottieBlock.tsx +11 -0
- package/src/tools/Chat/messages/blocks/renderers/MapBlock.tsx +36 -0
- package/src/tools/Chat/messages/blocks/renderers/MermaidBlock.tsx +11 -0
- package/src/tools/Chat/messages/blocks/renderers/VideoBlock.tsx +24 -0
- package/src/tools/Chat/messages/blocks/renderers/types.ts +8 -0
- package/src/tools/Chat/{components → messages}/index.ts +11 -5
- package/src/tools/Chat/public.ts +212 -0
- package/src/tools/Chat/shell/ChatRoot.tsx +345 -0
- package/src/tools/Chat/{components → shell}/EmptyState.tsx +4 -2
- package/src/tools/Chat/shell/index.ts +15 -0
- package/src/tools/Chat/types/block.ts +120 -0
- package/src/tools/Chat/types/config.ts +0 -5
- package/src/tools/Chat/types/index.ts +17 -0
- package/src/tools/Chat/types/message.ts +3 -0
- package/src/tools/Chat/utils/index.ts +4 -0
- package/src/tools/CodeEditor/README.md +4 -6
- package/src/tools/CodeEditor/components/DiffEditor.tsx +48 -13
- package/src/tools/CodeEditor/components/Editor.tsx +96 -44
- package/src/tools/CodeEditor/context/EditorProvider.tsx +34 -17
- package/src/tools/CodeEditor/hooks/useEditorTheme.ts +92 -99
- package/src/tools/CodeEditor/hooks/useMonaco.ts +37 -22
- package/src/tools/CodeEditor/lazy.tsx +6 -0
- package/src/tools/CodeEditor/lib/index.ts +1 -1
- package/src/tools/CodeEditor/lib/themes.ts +3 -39
- package/src/tools/CronScheduler/CronScheduler.client.tsx +230 -61
- package/src/tools/CronScheduler/components/CustomInput.tsx +21 -4
- package/src/tools/CronScheduler/components/DayChips.tsx +13 -11
- package/src/tools/CronScheduler/components/MonthDayGrid.tsx +4 -4
- package/src/tools/CronScheduler/components/SchedulePreview.tsx +7 -3
- package/src/tools/CronScheduler/components/TimeSelector.tsx +1 -1
- package/src/tools/CronScheduler/index.tsx +1 -1
- package/src/tools/CronScheduler/types/index.ts +8 -3
- package/src/tools/CronScheduler/utils/cron-humanize.ts +61 -16
- package/src/tools/CronScheduler/utils/cron-parser.ts +13 -4
- package/src/tools/FileIcon/FileIcon.tsx +24 -39
- package/src/tools/FileIcon/get-file-icon.ts +73 -0
- package/src/tools/FileIcon/icons/icon-data.ts +399 -0
- package/src/tools/FileIcon/index.ts +4 -0
- package/src/tools/FileIcon/loader.ts +17 -35
- package/src/tools/FileIcon/specialFolders.ts +18 -0
- package/src/tools/Gallery/components/lightbox/GalleryLightbox.tsx +112 -35
- package/src/tools/Gallery/components/media/GalleryVideo.tsx +21 -2
- package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +11 -1
- package/src/tools/Gallery/hooks/usePreloadImages.ts +54 -7
- package/src/tools/ImageViewer/components/ImageInfo.tsx +12 -1
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +51 -43
- package/src/tools/ImageViewer/components/ImageViewer.tsx +96 -24
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +13 -0
- package/src/tools/ImageViewer/utils/constants.ts +3 -0
- package/src/tools/ImageViewer/utils/index.ts +1 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +4 -1
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +5 -3
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +7 -4
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +3 -1
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +23 -3
- package/src/tools/JsonForm/widgets/ColorWidget.tsx +20 -12
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +14 -9
- package/src/tools/JsonForm/widgets/RadioWidget.tsx +78 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +1 -0
- package/src/tools/JsonForm/widgets/SliderWidget.tsx +7 -4
- package/src/tools/JsonForm/widgets/TextWidget.tsx +41 -17
- package/src/tools/JsonForm/widgets/index.ts +1 -0
- package/src/tools/JsonTree/components/JsonContent.tsx +115 -40
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +177 -72
- package/src/tools/LottiePlayer/index.tsx +14 -4
- package/src/tools/LottiePlayer/lazy.tsx +11 -3
- package/src/tools/LottiePlayer/types.ts +31 -1
- package/src/tools/LottiePlayer/useLottie.ts +32 -9
- package/src/tools/LottiePlayer/usePrefersReducedMotion.ts +46 -0
- package/src/tools/Map/components/LayerSwitcher.tsx +54 -21
- package/src/tools/Map/components/MapCluster.tsx +28 -21
- package/src/tools/Map/components/MapContainer.tsx +11 -4
- package/src/tools/Map/components/MapLegend.tsx +46 -15
- package/src/tools/Map/components/MapMarker.tsx +31 -2
- package/src/tools/Map/hooks/useMapEvents.ts +64 -105
- package/src/tools/MarkdownEditor/MarkdownEditor.tsx +61 -6
- package/src/tools/MarkdownEditor/MentionList.tsx +37 -4
- package/src/tools/MarkdownEditor/createMentionSuggestion.ts +11 -0
- package/src/tools/MarkdownEditor/lazy.tsx +32 -7
- package/src/tools/MarkdownEditor/styles.css +13 -0
- package/src/tools/MarkdownMessage/CodeBlock.tsx +40 -17
- package/src/tools/MarkdownMessage/MarkdownMessage.tsx +26 -6
- package/src/tools/MarkdownMessage/components.tsx +22 -9
- package/src/tools/MarkdownMessage/types.ts +24 -1
- package/src/tools/Mermaid/Mermaid.client.tsx +27 -5
- package/src/tools/Mermaid/components/MermaidErrorPanel.tsx +31 -0
- package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +14 -17
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +264 -168
- package/src/tools/Mermaid/hooks/useMermaidValidation.ts +76 -10
- package/src/tools/Mermaid/index.tsx +6 -0
- package/src/tools/Mermaid/utils/mermaid-helpers.ts +141 -18
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +11 -1
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +49 -20
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +7 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +7 -4
- package/src/tools/OpenapiViewer/constants.ts +3 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +73 -11
- package/src/tools/OpenapiViewer/utils/schemaExport.ts +26 -6
- package/src/tools/PrettyCode/PrettyCode.client.tsx +23 -16
- package/src/tools/PrettyCode/lazy.tsx +1 -1
- package/src/tools/SpeechRecognition/README.md +1 -1
- package/src/tools/SpeechRecognition/__tests__/language.test.ts +9 -3
- package/src/tools/SpeechRecognition/components/RecordingPulse.tsx +59 -0
- package/src/tools/SpeechRecognition/components/index.ts +2 -0
- package/src/tools/SpeechRecognition/core/engine/external.ts +24 -7
- package/src/tools/SpeechRecognition/core/language.ts +23 -6
- package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +36 -5
- package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +18 -11
- package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +94 -26
- package/src/tools/SpeechRecognition/widgets/index.ts +1 -1
- package/src/tools/Tree/README.md +4 -8
- package/src/tools/Tree/TreeRoot.tsx +22 -10
- package/src/tools/Tree/components/TreeContent.tsx +24 -4
- package/src/tools/Tree/components/TreeLabel.tsx +8 -2
- package/src/tools/Tree/components/TreeRow.tsx +16 -6
- package/src/tools/Tree/data/flatten.ts +10 -4
- package/src/tools/Tree/types.ts +4 -0
- package/src/tools/Uploader/components/UploadAddButton.tsx +29 -6
- package/src/tools/Uploader/components/UploadDropzone.tsx +63 -7
- package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +19 -5
- package/src/tools/Uploader/components/UploadPreviewItem.tsx +47 -17
- package/src/tools/Uploader/components/UploadPreviewList.tsx +24 -12
- package/src/tools/Uploader/utils/formatters.ts +8 -3
- package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +1 -0
- package/src/tools/VideoPlayer/canvas/{jsx.d.ts → jsx-augmentation.ts} +12 -19
- package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +1 -0
- package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +1 -0
- package/src/tools/VideoPlayer/parts/fullscreen.tsx +1 -1
- package/src/tools/VideoPlayer/parts/pip.tsx +1 -1
- package/src/tools/VideoPlayer/parts/playback-rate.tsx +1 -1
- package/src/tools/VideoPlayer/parts/seek-bar.tsx +2 -2
- package/src/tools/VideoPlayer/parts/volume.tsx +2 -2
- package/src/tools/index.ts +2 -1
- package/src/tools/Chat/components/AudioToggle.tsx +0 -78
- package/src/tools/Chat/components/ChatRoot.tsx +0 -305
- package/src/tools/Chat/components/Composer.tsx +0 -216
- package/src/tools/Chat/hooks/useChatScroll.ts +0 -145
- package/src/tools/Chat/types.ts +0 -9
- package/src/tools/JsonTree/components/JsonToolbar.tsx +0 -95
- package/src/tools/JsonTree/hooks/useElementCorner.ts +0 -84
- package/src/tools/JsonTree/hooks/useNavbarHeight.ts +0 -83
- package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +0 -121
- package/src/tools/Tour/README.md +0 -373
- package/src/tools/Tour/components/Tour.tsx +0 -12
- package/src/tools/Tour/components/TourContent.tsx +0 -171
- package/src/tools/Tour/components/TourNavigation.tsx +0 -77
- package/src/tools/Tour/components/TourProgress.tsx +0 -88
- package/src/tools/Tour/components/TourSpotlight.tsx +0 -199
- package/src/tools/Tour/components/index.ts +0 -5
- package/src/tools/Tour/context/TourContext.ts +0 -19
- package/src/tools/Tour/context/TourProvider.tsx +0 -292
- package/src/tools/Tour/context/index.ts +0 -2
- package/src/tools/Tour/hooks/index.ts +0 -3
- package/src/tools/Tour/hooks/useKeyboardNavigation.ts +0 -59
- package/src/tools/Tour/hooks/useStepTarget.ts +0 -121
- package/src/tools/Tour/hooks/useTour.ts +0 -42
- package/src/tools/Tour/index.ts +0 -38
- package/src/tools/Tour/types/index.ts +0 -224
- package/src/tools/Tour/utils/dom.ts +0 -98
- package/src/tools/Tour/utils/index.ts +0 -3
- package/src/tools/Tour/utils/logger.ts +0 -3
- package/src/tools/Tour/utils/scrollIntoView.ts +0 -24
- /package/src/tools/Chat/{config.ts → constants.ts} +0 -0
- /package/src/tools/Chat/launcher/{ChatHeaderAudioToggle.tsx → header/ChatHeaderAudioToggle.tsx} +0 -0
- /package/src/tools/Chat/{components → messages}/Attachments.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/JumpToLatest.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/MessageActions.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/Sources.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/StreamingIndicator.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/ToolCalls.tsx +0 -0
- /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
|
-
|
|
21
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
|
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
|
|
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={
|
|
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={
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
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);
|
package/src/tools/Tree/types.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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 (
|
|
128
|
-
return previewUrl
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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 &&
|
|
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
|
|
183
|
-
|
|
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
|
|
194
|
-
|
|
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>
|