@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
|
@@ -12,4 +12,8 @@ export type {
|
|
|
12
12
|
FolderIconOverrides,
|
|
13
13
|
} from './specialFolders';
|
|
14
14
|
|
|
15
|
+
export { getFileIcon, DEFAULT_FILE_ICON } from './get-file-icon';
|
|
16
|
+
export type { FileIconDef } from './get-file-icon';
|
|
17
|
+
|
|
18
|
+
/** @deprecated Use `getFileIcon` — resolution is now synchronous. */
|
|
15
19
|
export { loadMaterialIcons, getMaterialIconsSync } from './loader';
|
|
@@ -1,47 +1,29 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* if it's installed in the consumer's tree, we render its rich VSCode-style
|
|
6
|
-
* SVGs; if not, the loader resolves to `null` and `<FileIcon>` falls back to a
|
|
7
|
-
* Lucide icon. Either way, ui-tools never throws or warns about a missing
|
|
8
|
-
* package.
|
|
4
|
+
* Back-compat shim for the former `material-file-icons` lazy loader.
|
|
9
5
|
*
|
|
10
|
-
*
|
|
6
|
+
* File icons are now resolved synchronously from a vendored static data
|
|
7
|
+
* module — see `./get-file-icon`. The async API is kept so existing callers
|
|
8
|
+
* of `loadMaterialIcons` / `getMaterialIconsSync` keep working; both resolve
|
|
9
|
+
* immediately to the local resolver.
|
|
10
|
+
*
|
|
11
|
+
* @deprecated Import {@link getFileIcon} from `./get-file-icon` instead.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
let cached: GetIconFn | null | undefined;
|
|
16
|
-
let inflight: Promise<GetIconFn | null> | null = null;
|
|
14
|
+
import { getFileIcon } from './get-file-icon';
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (inflight) return inflight;
|
|
16
|
+
/** Returns an inline SVG string for a file name, or `undefined`. */
|
|
17
|
+
type GetIconFn = (name: string) => { svg?: string } | undefined;
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
// Computed specifier so bundlers don't try to eagerly resolve it.
|
|
25
|
-
const specifier = 'material-file-icons';
|
|
26
|
-
const mod = (await import(/* @vite-ignore */ specifier)) as {
|
|
27
|
-
getIcon?: GetIconFn;
|
|
28
|
-
default?: { getIcon?: GetIconFn };
|
|
29
|
-
};
|
|
30
|
-
const fn = mod.getIcon ?? mod.default?.getIcon ?? null;
|
|
31
|
-
cached = fn;
|
|
32
|
-
return fn;
|
|
33
|
-
} catch {
|
|
34
|
-
cached = null;
|
|
35
|
-
return null;
|
|
36
|
-
} finally {
|
|
37
|
-
inflight = null;
|
|
38
|
-
}
|
|
39
|
-
})();
|
|
19
|
+
const localGetIcon: GetIconFn = (name) => ({ svg: getFileIcon(name).svg });
|
|
40
20
|
|
|
41
|
-
|
|
21
|
+
/** @deprecated Use {@link getFileIcon} — resolution is now synchronous. */
|
|
22
|
+
export function loadMaterialIcons(): Promise<GetIconFn> {
|
|
23
|
+
return Promise.resolve(localGetIcon);
|
|
42
24
|
}
|
|
43
25
|
|
|
44
|
-
/**
|
|
45
|
-
export function getMaterialIconsSync(): GetIconFn
|
|
46
|
-
return
|
|
26
|
+
/** @deprecated Use {@link getFileIcon} — resolution is now synchronous. */
|
|
27
|
+
export function getMaterialIconsSync(): GetIconFn {
|
|
28
|
+
return localGetIcon;
|
|
47
29
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
BookOpen,
|
|
5
|
+
Component,
|
|
6
|
+
Database,
|
|
5
7
|
FlaskConical,
|
|
6
8
|
Folder,
|
|
7
9
|
FolderCode,
|
|
@@ -11,9 +13,11 @@ import {
|
|
|
11
13
|
FolderOutput,
|
|
12
14
|
Github,
|
|
13
15
|
Image as ImageIcon,
|
|
16
|
+
Languages,
|
|
14
17
|
Package,
|
|
15
18
|
Settings,
|
|
16
19
|
Terminal,
|
|
20
|
+
Workflow,
|
|
17
21
|
type LucideIcon,
|
|
18
22
|
} from 'lucide-react';
|
|
19
23
|
|
|
@@ -50,6 +54,7 @@ const SPECIAL_FOLDERS: Record<string, LucideIcon> = {
|
|
|
50
54
|
test: FlaskConical,
|
|
51
55
|
__tests__: FlaskConical,
|
|
52
56
|
__test__: FlaskConical,
|
|
57
|
+
spec: FlaskConical,
|
|
53
58
|
scripts: Terminal,
|
|
54
59
|
bin: Terminal,
|
|
55
60
|
config: Settings,
|
|
@@ -60,6 +65,19 @@ const SPECIAL_FOLDERS: Record<string, LucideIcon> = {
|
|
|
60
65
|
'.git': FolderGit2,
|
|
61
66
|
'.github': Github,
|
|
62
67
|
'.gitlab': FolderGit2,
|
|
68
|
+
'.husky': Workflow,
|
|
69
|
+
'.circleci': Workflow,
|
|
70
|
+
components: Component,
|
|
71
|
+
component: Component,
|
|
72
|
+
ui: Component,
|
|
73
|
+
i18n: Languages,
|
|
74
|
+
locale: Languages,
|
|
75
|
+
locales: Languages,
|
|
76
|
+
lang: Languages,
|
|
77
|
+
translations: Languages,
|
|
78
|
+
db: Database,
|
|
79
|
+
database: Database,
|
|
80
|
+
migrations: Database,
|
|
63
81
|
};
|
|
64
82
|
|
|
65
83
|
export interface ResolveFolderIconOptions {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { memo, useEffect, useCallback, useState, useMemo } from 'react'
|
|
3
|
+
import { memo, useEffect, useCallback, useState, useMemo, useRef } from 'react'
|
|
4
4
|
import { createPortal } from 'react-dom'
|
|
5
5
|
import { cn } from '@djangocfg/ui-core/lib'
|
|
6
6
|
import { useAppT } from '@djangocfg/i18n'
|
|
@@ -19,7 +19,8 @@ import type { GalleryLightboxProps } from '../../types'
|
|
|
19
19
|
* - Preloads adjacent images for smooth navigation
|
|
20
20
|
* - Loading skeleton while image loads
|
|
21
21
|
* - Zoom with pan/drag support
|
|
22
|
-
* - Keyboard
|
|
22
|
+
* - Keyboard navigation (arrows, Home/End, Esc), focus trap, backdrop click
|
|
23
|
+
* - Swipe navigation
|
|
23
24
|
*/
|
|
24
25
|
export const GalleryLightbox = memo(function GalleryLightbox({
|
|
25
26
|
open,
|
|
@@ -35,6 +36,9 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
35
36
|
}: GalleryLightboxProps) {
|
|
36
37
|
const t = useAppT()
|
|
37
38
|
const [mounted, setMounted] = useState(false)
|
|
39
|
+
const dialogRef = useRef<HTMLDivElement>(null)
|
|
40
|
+
// Element focused before the lightbox opened, restored on close
|
|
41
|
+
const previousFocusRef = useRef<HTMLElement | null>(null)
|
|
38
42
|
|
|
39
43
|
// Translations
|
|
40
44
|
const labels = useMemo(() => ({
|
|
@@ -58,7 +62,9 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
58
62
|
// Prepare data
|
|
59
63
|
const currentImage = useMemo(() => images[currentIndex], [images, currentIndex])
|
|
60
64
|
const hasMultiple = useMemo(() => images.length > 1, [images.length])
|
|
65
|
+
const isVideo = currentImage?.type === 'video' && Boolean(currentImage.videoSrc)
|
|
61
66
|
const { isZoomed, isDragging } = zoom
|
|
67
|
+
const resetZoom = zoom.reset
|
|
62
68
|
|
|
63
69
|
// Preload adjacent images when lightbox is open
|
|
64
70
|
usePreloadImages(open ? images : [], currentIndex, 2)
|
|
@@ -70,20 +76,33 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
70
76
|
|
|
71
77
|
// Reset zoom when image changes
|
|
72
78
|
useEffect(() => {
|
|
73
|
-
|
|
74
|
-
}, [currentIndex])
|
|
79
|
+
resetZoom()
|
|
80
|
+
}, [currentIndex, resetZoom])
|
|
75
81
|
|
|
76
82
|
// Prevent body scroll when open
|
|
77
83
|
useEffect(() => {
|
|
78
|
-
if (open)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
+
if (!open) return
|
|
85
|
+
const originalStyle = document.body.style.overflow
|
|
86
|
+
document.body.style.overflow = 'hidden'
|
|
87
|
+
return () => {
|
|
88
|
+
document.body.style.overflow = originalStyle
|
|
84
89
|
}
|
|
85
90
|
}, [open])
|
|
86
91
|
|
|
92
|
+
// Focus management: move focus into the dialog on open, restore on close
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!open || !mounted) return
|
|
95
|
+
previousFocusRef.current = document.activeElement as HTMLElement | null
|
|
96
|
+
// Defer to allow the portal content to mount
|
|
97
|
+
const id = window.requestAnimationFrame(() => {
|
|
98
|
+
dialogRef.current?.focus()
|
|
99
|
+
})
|
|
100
|
+
return () => {
|
|
101
|
+
window.cancelAnimationFrame(id)
|
|
102
|
+
previousFocusRef.current?.focus?.()
|
|
103
|
+
}
|
|
104
|
+
}, [open, mounted])
|
|
105
|
+
|
|
87
106
|
// Navigation handlers
|
|
88
107
|
const goToPrev = useCallback(() => {
|
|
89
108
|
if (hasMultiple) {
|
|
@@ -97,15 +116,16 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
97
116
|
}
|
|
98
117
|
}, [hasMultiple, currentIndex, images.length, onIndexChange])
|
|
99
118
|
|
|
100
|
-
// Keyboard navigation
|
|
119
|
+
// Keyboard navigation + focus trap
|
|
101
120
|
useEffect(() => {
|
|
102
121
|
if (!open) return
|
|
103
122
|
|
|
104
123
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
105
124
|
switch (e.key) {
|
|
106
125
|
case 'Escape':
|
|
126
|
+
e.preventDefault()
|
|
107
127
|
if (isZoomed) {
|
|
108
|
-
|
|
128
|
+
resetZoom()
|
|
109
129
|
} else {
|
|
110
130
|
onClose()
|
|
111
131
|
}
|
|
@@ -116,12 +136,47 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
116
136
|
case 'ArrowRight':
|
|
117
137
|
if (!isZoomed) goToNext()
|
|
118
138
|
break
|
|
139
|
+
case 'Home':
|
|
140
|
+
if (!isZoomed && hasMultiple) {
|
|
141
|
+
e.preventDefault()
|
|
142
|
+
onIndexChange(0)
|
|
143
|
+
}
|
|
144
|
+
break
|
|
145
|
+
case 'End':
|
|
146
|
+
if (!isZoomed && hasMultiple) {
|
|
147
|
+
e.preventDefault()
|
|
148
|
+
onIndexChange(images.length - 1)
|
|
149
|
+
}
|
|
150
|
+
break
|
|
151
|
+
case 'Tab': {
|
|
152
|
+
// Trap focus within the dialog
|
|
153
|
+
const dialog = dialogRef.current
|
|
154
|
+
if (!dialog) break
|
|
155
|
+
const focusable = dialog.querySelectorAll<HTMLElement>(
|
|
156
|
+
'button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])'
|
|
157
|
+
)
|
|
158
|
+
if (focusable.length === 0) {
|
|
159
|
+
e.preventDefault()
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
const first = focusable[0]
|
|
163
|
+
const last = focusable[focusable.length - 1]
|
|
164
|
+
const active = document.activeElement
|
|
165
|
+
if (e.shiftKey && (active === first || active === dialog)) {
|
|
166
|
+
e.preventDefault()
|
|
167
|
+
last.focus()
|
|
168
|
+
} else if (!e.shiftKey && active === last) {
|
|
169
|
+
e.preventDefault()
|
|
170
|
+
first.focus()
|
|
171
|
+
}
|
|
172
|
+
break
|
|
173
|
+
}
|
|
119
174
|
}
|
|
120
175
|
}
|
|
121
176
|
|
|
122
177
|
window.addEventListener('keydown', handleKeyDown)
|
|
123
178
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
124
|
-
}, [open, isZoomed, goToPrev, goToNext, onClose,
|
|
179
|
+
}, [open, isZoomed, goToPrev, goToNext, onClose, resetZoom, hasMultiple, images.length, onIndexChange])
|
|
125
180
|
|
|
126
181
|
// Swipe handlers (disabled when zoomed)
|
|
127
182
|
const swipeHandlers = useSwipe({
|
|
@@ -132,13 +187,27 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
132
187
|
// Action handlers
|
|
133
188
|
const handleDownload = useCallback(() => {
|
|
134
189
|
if (!currentImage) return
|
|
190
|
+
// For videos download the video file, otherwise the image
|
|
191
|
+
const downloadUrl = isVideo ? currentImage.videoSrc! : currentImage.src
|
|
192
|
+
// Derive a filename from the URL path, fall back to a numbered name
|
|
193
|
+
let filename = isVideo ? `video-${currentIndex + 1}.mp4` : `image-${currentIndex + 1}.jpg`
|
|
194
|
+
try {
|
|
195
|
+
const path = new URL(downloadUrl, window.location.href).pathname
|
|
196
|
+
const last = path.split('/').pop()
|
|
197
|
+
if (last && /\.[a-z0-9]+$/i.test(last)) {
|
|
198
|
+
filename = decodeURIComponent(last)
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
// Keep fallback filename
|
|
202
|
+
}
|
|
135
203
|
const link = document.createElement('a')
|
|
136
|
-
link.href =
|
|
137
|
-
link.download =
|
|
204
|
+
link.href = downloadUrl
|
|
205
|
+
link.download = filename
|
|
206
|
+
link.rel = 'noopener'
|
|
138
207
|
document.body.appendChild(link)
|
|
139
208
|
link.click()
|
|
140
209
|
document.body.removeChild(link)
|
|
141
|
-
}, [currentImage, currentIndex])
|
|
210
|
+
}, [currentImage, currentIndex, isVideo])
|
|
142
211
|
|
|
143
212
|
const handleShare = useCallback(async () => {
|
|
144
213
|
if (!currentImage) return
|
|
@@ -160,29 +229,31 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
160
229
|
zoom.toggleZoom()
|
|
161
230
|
}, [zoom])
|
|
162
231
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
232
|
+
// Close only when the backdrop itself (not a child) is clicked
|
|
233
|
+
const handleBackdropClick = useCallback((e: React.MouseEvent) => {
|
|
234
|
+
if (e.target === e.currentTarget) onClose()
|
|
235
|
+
}, [onClose])
|
|
166
236
|
|
|
167
237
|
if (!open || !mounted) return null
|
|
168
238
|
|
|
169
239
|
const lightbox = (
|
|
170
240
|
<div
|
|
241
|
+
ref={dialogRef}
|
|
171
242
|
className={cn(
|
|
172
243
|
'fixed inset-0 z-50',
|
|
173
244
|
'backdrop-blur-sm',
|
|
174
|
-
'animate-in fade-in-0 duration-200'
|
|
245
|
+
'animate-in fade-in-0 duration-200',
|
|
246
|
+
'focus:outline-none'
|
|
175
247
|
)}
|
|
176
248
|
style={{ backgroundColor: 'rgba(0, 0, 0, 0.85)' }}
|
|
177
|
-
onClick={onClose}
|
|
178
249
|
role="dialog"
|
|
179
250
|
aria-modal="true"
|
|
180
|
-
aria-label={labels.lightbox}
|
|
251
|
+
aria-label={title || labels.lightbox}
|
|
252
|
+
tabIndex={-1}
|
|
181
253
|
>
|
|
182
254
|
{/* Content */}
|
|
183
255
|
<div
|
|
184
256
|
className="relative w-full h-full flex flex-col"
|
|
185
|
-
onClick={handleContentClick}
|
|
186
257
|
{...swipeHandlers}
|
|
187
258
|
>
|
|
188
259
|
{/* Header */}
|
|
@@ -191,7 +262,7 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
191
262
|
<div className="text-white">
|
|
192
263
|
{title && <div className="font-medium">{title}</div>}
|
|
193
264
|
{hasMultiple && (
|
|
194
|
-
<div className="text-sm text-white/70">
|
|
265
|
+
<div className="text-sm text-white/70" aria-live="polite">
|
|
195
266
|
{currentIndex + 1} / {images.length}
|
|
196
267
|
</div>
|
|
197
268
|
)}
|
|
@@ -199,10 +270,10 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
199
270
|
|
|
200
271
|
{/* Actions */}
|
|
201
272
|
<div className="flex items-center gap-2">
|
|
202
|
-
{enableZoom && (
|
|
273
|
+
{enableZoom && !isVideo && (
|
|
203
274
|
<button
|
|
204
275
|
type="button"
|
|
205
|
-
className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors"
|
|
276
|
+
className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
|
206
277
|
onClick={handleToggleZoom}
|
|
207
278
|
aria-label={isZoomed ? labels.zoomOut : labels.zoomIn}
|
|
208
279
|
>
|
|
@@ -217,7 +288,7 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
217
288
|
{enableDownload && (
|
|
218
289
|
<button
|
|
219
290
|
type="button"
|
|
220
|
-
className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors"
|
|
291
|
+
className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
|
221
292
|
onClick={handleDownload}
|
|
222
293
|
aria-label={labels.download}
|
|
223
294
|
>
|
|
@@ -228,7 +299,7 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
228
299
|
{enableShare && (
|
|
229
300
|
<button
|
|
230
301
|
type="button"
|
|
231
|
-
className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors"
|
|
302
|
+
className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
|
232
303
|
onClick={handleShare}
|
|
233
304
|
aria-label={labels.share}
|
|
234
305
|
>
|
|
@@ -238,7 +309,7 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
238
309
|
|
|
239
310
|
<button
|
|
240
311
|
type="button"
|
|
241
|
-
className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors"
|
|
312
|
+
className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
|
242
313
|
onClick={onClose}
|
|
243
314
|
aria-label={labels.close}
|
|
244
315
|
>
|
|
@@ -248,17 +319,21 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
248
319
|
</div>
|
|
249
320
|
|
|
250
321
|
{/* Main Image */}
|
|
251
|
-
<div
|
|
322
|
+
<div
|
|
323
|
+
className="flex-1 flex items-center justify-center p-4 pt-16 pb-24 overflow-hidden"
|
|
324
|
+
onClick={handleBackdropClick}
|
|
325
|
+
>
|
|
252
326
|
{currentImage && (
|
|
253
327
|
<div
|
|
254
328
|
className={cn(
|
|
255
329
|
'relative w-full h-full select-none',
|
|
256
|
-
|
|
257
|
-
|
|
330
|
+
// Zoom cursors only apply to images, not video (needs native controls)
|
|
331
|
+
!isVideo && (isZoomed ? 'cursor-grab' : enableZoom ? 'cursor-zoom-in' : 'cursor-default'),
|
|
332
|
+
!isVideo && isDragging && 'cursor-grabbing'
|
|
258
333
|
)}
|
|
259
|
-
{...(enableZoom ? zoom.handlers : {})}
|
|
334
|
+
{...(enableZoom && !isVideo ? zoom.handlers : {})}
|
|
260
335
|
style={{
|
|
261
|
-
transform: isZoomed
|
|
336
|
+
transform: isZoomed && !isVideo
|
|
262
337
|
? `scale(${zoom.state.scale}) translate(${zoom.state.x}px, ${zoom.state.y}px)`
|
|
263
338
|
: 'scale(1)',
|
|
264
339
|
transition: isDragging ? 'none' : 'transform 0.3s ease-out',
|
|
@@ -267,7 +342,9 @@ export const GalleryLightbox = memo(function GalleryLightbox({
|
|
|
267
342
|
>
|
|
268
343
|
<GalleryMedia
|
|
269
344
|
media={currentImage}
|
|
270
|
-
|
|
345
|
+
// pointer-events disabled for images so the zoom wrapper gets clicks;
|
|
346
|
+
// video needs pointer events for its native/overlay controls
|
|
347
|
+
className={cn('w-full h-full', !isVideo && 'pointer-events-none')}
|
|
271
348
|
objectFit="contain"
|
|
272
349
|
showLoading
|
|
273
350
|
priority
|
|
@@ -51,8 +51,13 @@ export const GalleryVideo = memo(function GalleryVideo({
|
|
|
51
51
|
if (!video) return
|
|
52
52
|
|
|
53
53
|
if (video.paused) {
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// play() returns a promise that can reject (autoplay policy / no source)
|
|
55
|
+
const playPromise = video.play()
|
|
56
|
+
if (playPromise && typeof playPromise.then === 'function') {
|
|
57
|
+
playPromise.then(() => setIsPlaying(true)).catch(() => setIsPlaying(false))
|
|
58
|
+
} else {
|
|
59
|
+
setIsPlaying(true)
|
|
60
|
+
}
|
|
56
61
|
} else {
|
|
57
62
|
video.pause()
|
|
58
63
|
setIsPlaying(false)
|
|
@@ -98,6 +103,14 @@ export const GalleryVideo = memo(function GalleryVideo({
|
|
|
98
103
|
setIsPlaying(false)
|
|
99
104
|
}, [])
|
|
100
105
|
|
|
106
|
+
// Keep local state in sync with native video events (native controls, end of playback)
|
|
107
|
+
const handlePlay = useCallback(() => setIsPlaying(true), [])
|
|
108
|
+
const handlePause = useCallback(() => setIsPlaying(false), [])
|
|
109
|
+
const handleEnded = useCallback(() => {
|
|
110
|
+
setIsPlaying(false)
|
|
111
|
+
setShowOverlay(true)
|
|
112
|
+
}, [])
|
|
113
|
+
|
|
101
114
|
const handleContainerClick = useCallback((e: React.MouseEvent) => {
|
|
102
115
|
e.stopPropagation()
|
|
103
116
|
}, [])
|
|
@@ -110,6 +123,7 @@ export const GalleryVideo = memo(function GalleryVideo({
|
|
|
110
123
|
if (hasError) {
|
|
111
124
|
return (
|
|
112
125
|
<div
|
|
126
|
+
data-nav
|
|
113
127
|
className={cn('relative w-full h-full bg-black', className)}
|
|
114
128
|
onClick={handleContainerClick}
|
|
115
129
|
>
|
|
@@ -133,6 +147,7 @@ export const GalleryVideo = memo(function GalleryVideo({
|
|
|
133
147
|
|
|
134
148
|
return (
|
|
135
149
|
<div
|
|
150
|
+
data-nav
|
|
136
151
|
className={cn('relative w-full h-full bg-black', className)}
|
|
137
152
|
onMouseEnter={handleMouseEnter}
|
|
138
153
|
onMouseLeave={handleMouseLeave}
|
|
@@ -143,12 +158,16 @@ export const GalleryVideo = memo(function GalleryVideo({
|
|
|
143
158
|
className="w-full h-full object-contain"
|
|
144
159
|
src={media.videoSrc}
|
|
145
160
|
poster={media.src}
|
|
161
|
+
aria-label={media.alt || 'Video'}
|
|
146
162
|
autoPlay={autoPlay}
|
|
147
163
|
muted={muted}
|
|
148
164
|
loop={loop}
|
|
149
165
|
playsInline
|
|
150
166
|
onClick={handleVideoClick}
|
|
151
167
|
onError={handleError}
|
|
168
|
+
onPlay={handlePlay}
|
|
169
|
+
onPause={handlePause}
|
|
170
|
+
onEnded={handleEnded}
|
|
152
171
|
>
|
|
153
172
|
{media.videoType && (
|
|
154
173
|
<source src={media.videoSrc} type={media.videoType} />
|
|
@@ -161,8 +161,18 @@ export const GalleryCarousel = memo(function GalleryCarousel({
|
|
|
161
161
|
const dy = Math.abs(e.clientY - y)
|
|
162
162
|
const dt = Date.now() - time
|
|
163
163
|
|
|
164
|
+
// Don't open lightbox when interacting with video controls
|
|
165
|
+
const target = e.target as HTMLElement
|
|
166
|
+
const isVideoInteraction =
|
|
167
|
+
target.tagName === 'VIDEO' || target.closest('video, [data-nav]') !== null
|
|
168
|
+
|
|
164
169
|
// Only trigger click if pointer didn't move much and was quick
|
|
165
|
-
if (
|
|
170
|
+
if (
|
|
171
|
+
!isVideoInteraction &&
|
|
172
|
+
dx < CLICK_THRESHOLD &&
|
|
173
|
+
dy < CLICK_THRESHOLD &&
|
|
174
|
+
dt < CLICK_TIME_THRESHOLD
|
|
175
|
+
) {
|
|
166
176
|
onLightboxOpen()
|
|
167
177
|
}
|
|
168
178
|
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
4
|
|
|
5
5
|
import type { GalleryMediaItem } from '../types'
|
|
6
|
+
import { normalizeImageUrl } from '../utils'
|
|
7
|
+
|
|
8
|
+
/** Max number of URLs to remember as "already preloaded" before evicting oldest */
|
|
9
|
+
const MAX_PRELOAD_MEMORY = 200
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* Preload adjacent images for smoother navigation
|
|
@@ -13,6 +17,7 @@ export function usePreloadImages(
|
|
|
13
17
|
currentIndex: number,
|
|
14
18
|
preloadCount: number = 1
|
|
15
19
|
): void {
|
|
20
|
+
// Insertion-ordered Set, used as a bounded LRU to avoid unbounded growth
|
|
16
21
|
const preloadedRef = useRef<Set<string>>(new Set())
|
|
17
22
|
|
|
18
23
|
useEffect(() => {
|
|
@@ -26,18 +31,50 @@ export function usePreloadImages(
|
|
|
26
31
|
|
|
27
32
|
const index = (currentIndex + offset + images.length) % images.length
|
|
28
33
|
const image = images[index]
|
|
34
|
+
if (!image) continue
|
|
35
|
+
|
|
36
|
+
// Videos are not preloadable via Image(); skip them
|
|
37
|
+
if (image.type === 'video') continue
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
const src = normalizeImageUrl(image.src)
|
|
40
|
+
if (src && !preloadedRef.current.has(src)) {
|
|
41
|
+
toPreload.push(src)
|
|
32
42
|
}
|
|
33
43
|
}
|
|
34
44
|
|
|
35
|
-
|
|
45
|
+
if (toPreload.length === 0) return
|
|
46
|
+
|
|
47
|
+
const memory = preloadedRef.current
|
|
48
|
+
const loaders: HTMLImageElement[] = []
|
|
49
|
+
|
|
36
50
|
toPreload.forEach((src) => {
|
|
37
51
|
const img = new Image()
|
|
52
|
+
// Release the loader once finished so it can be GC'd
|
|
53
|
+
const cleanup = () => {
|
|
54
|
+
img.onload = null
|
|
55
|
+
img.onerror = null
|
|
56
|
+
}
|
|
57
|
+
img.onload = cleanup
|
|
58
|
+
img.onerror = cleanup
|
|
38
59
|
img.src = src
|
|
39
|
-
|
|
60
|
+
loaders.push(img)
|
|
61
|
+
|
|
62
|
+
memory.add(src)
|
|
63
|
+
// Evict oldest entries to keep the Set bounded
|
|
64
|
+
if (memory.size > MAX_PRELOAD_MEMORY) {
|
|
65
|
+
const oldest = memory.values().next().value
|
|
66
|
+
if (oldest !== undefined) memory.delete(oldest)
|
|
67
|
+
}
|
|
40
68
|
})
|
|
69
|
+
|
|
70
|
+
// Cancel in-flight loaders if the effect re-runs before they settle
|
|
71
|
+
return () => {
|
|
72
|
+
loaders.forEach((img) => {
|
|
73
|
+
img.onload = null
|
|
74
|
+
img.onerror = null
|
|
75
|
+
img.src = ''
|
|
76
|
+
})
|
|
77
|
+
}
|
|
41
78
|
}, [images, currentIndex, preloadCount])
|
|
42
79
|
}
|
|
43
80
|
|
|
@@ -47,9 +84,19 @@ export function usePreloadImages(
|
|
|
47
84
|
export function preloadImage(src: string): Promise<void> {
|
|
48
85
|
return new Promise((resolve, reject) => {
|
|
49
86
|
const img = new Image()
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
87
|
+
const cleanup = () => {
|
|
88
|
+
img.onload = null
|
|
89
|
+
img.onerror = null
|
|
90
|
+
}
|
|
91
|
+
img.onload = () => {
|
|
92
|
+
cleanup()
|
|
93
|
+
resolve()
|
|
94
|
+
}
|
|
95
|
+
img.onerror = () => {
|
|
96
|
+
cleanup()
|
|
97
|
+
reject(new Error(`Failed to preload image: ${src}`))
|
|
98
|
+
}
|
|
99
|
+
img.src = normalizeImageUrl(src)
|
|
53
100
|
})
|
|
54
101
|
}
|
|
55
102
|
|
|
@@ -24,20 +24,31 @@ export function ImageInfo({ src }: ImageInfoProps) {
|
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Clear stale dimensions while the new source resolves
|
|
28
|
+
setDimensions(null);
|
|
29
|
+
|
|
27
30
|
// Load and cache dimensions
|
|
31
|
+
let cancelled = false;
|
|
28
32
|
const img = new Image();
|
|
29
33
|
img.onload = () => {
|
|
34
|
+
if (cancelled) return;
|
|
30
35
|
const dims = { w: img.naturalWidth, h: img.naturalHeight };
|
|
31
36
|
setDimensions(dims);
|
|
32
37
|
cacheDimensions(src, { width: dims.w, height: dims.h });
|
|
33
38
|
};
|
|
34
39
|
img.src = src;
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
cancelled = true;
|
|
43
|
+
img.onload = null;
|
|
44
|
+
img.src = '';
|
|
45
|
+
};
|
|
35
46
|
}, [src, getDimensions, cacheDimensions]);
|
|
36
47
|
|
|
37
48
|
if (!dimensions) return null;
|
|
38
49
|
|
|
39
50
|
return (
|
|
40
|
-
<div className="absolute top-3 right-3 z-10 px-2 py-1 bg-
|
|
51
|
+
<div className="absolute top-3 right-3 z-10 px-2 py-1 bg-black/60 backdrop-blur-sm border border-white/10 rounded text-[10px] text-white/80 font-mono">
|
|
41
52
|
{dimensions.w} × {dimensions.h}
|
|
42
53
|
</div>
|
|
43
54
|
);
|