@djangocfg/ui-tools 2.1.207 → 2.1.208
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 +47 -1
- package/dist/{chunk-MADKYFI3.mjs → chunk-BEURMR25.mjs} +73 -27
- package/dist/chunk-BEURMR25.mjs.map +1 -0
- package/dist/{chunk-6RG7HOVF.cjs → chunk-IPRNIM7L.cjs} +72 -26
- package/dist/chunk-IPRNIM7L.cjs.map +1 -0
- package/dist/components-3DASJBTX.mjs +5 -0
- package/dist/{components-GGZJFEII.mjs.map → components-3DASJBTX.mjs.map} +1 -1
- package/dist/components-NW2ZF6TG.cjs +22 -0
- package/dist/{components-YC32T5D7.cjs.map → components-NW2ZF6TG.cjs.map} +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +19 -9
- package/dist/index.d.ts +19 -9
- package/dist/index.mjs +2 -2
- package/package.json +6 -6
- package/src/tools/ImageViewer/ImageViewer.story.tsx +85 -0
- package/src/tools/ImageViewer/README.md +73 -93
- package/src/tools/ImageViewer/components/ImageViewer.tsx +94 -75
- package/src/tools/ImageViewer/types.ts +19 -8
- package/dist/chunk-6RG7HOVF.cjs.map +0 -1
- package/dist/chunk-MADKYFI3.mjs.map +0 -1
- package/dist/components-GGZJFEII.mjs +0 -5
- package/dist/components-YC32T5D7.cjs +0 -22
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ This package contains heavy components that are loaded lazily to keep your initi
|
|
|
36
36
|
| `AudioPlayer` | ~200KB | Audio player with WaveSurfer.js |
|
|
37
37
|
| `VideoPlayer` | ~150KB | Professional video player with Vidstack |
|
|
38
38
|
| `JsonTree` | ~100KB | JSON visualization with modes (full/compact/inline) |
|
|
39
|
-
| `ImageViewer` | ~50KB | Image viewer with zoom/pan/rotate |
|
|
39
|
+
| `ImageViewer` | ~50KB | Image viewer with zoom/pan/rotate/flip and gallery navigation |
|
|
40
40
|
| `CronScheduler` | ~15KB | Cron expression builder with intuitive UI |
|
|
41
41
|
|
|
42
42
|
## Tree-Shakeable Imports
|
|
@@ -559,6 +559,52 @@ import {
|
|
|
559
559
|
} from '@djangocfg/ui-tools';
|
|
560
560
|
```
|
|
561
561
|
|
|
562
|
+
## Image Viewer
|
|
563
|
+
|
|
564
|
+
Image viewer with zoom, pan, rotate, flip and gallery navigation.
|
|
565
|
+
|
|
566
|
+
```tsx
|
|
567
|
+
import { ImageViewer } from '@djangocfg/ui-tools';
|
|
568
|
+
|
|
569
|
+
// Single image
|
|
570
|
+
<div className="w-full h-[500px]">
|
|
571
|
+
<ImageViewer
|
|
572
|
+
images={[{ file: { name: 'photo.jpg', path: '/images/photo.jpg' }, src: '/images/photo.jpg' }]}
|
|
573
|
+
/>
|
|
574
|
+
</div>
|
|
575
|
+
|
|
576
|
+
// Gallery — pass multiple images, keyboard ←/→ navigation enabled automatically
|
|
577
|
+
<div className="w-full h-[500px]">
|
|
578
|
+
<ImageViewer
|
|
579
|
+
images={[
|
|
580
|
+
{ file: { name: 'Photo 1', path: 'p1' }, src: 'https://example.com/1.jpg' },
|
|
581
|
+
{ file: { name: 'Photo 2', path: 'p2' }, src: 'https://example.com/2.jpg' },
|
|
582
|
+
{ file: { name: 'Photo 3', path: 'p3' }, src: 'https://example.com/3.jpg' },
|
|
583
|
+
]}
|
|
584
|
+
initialIndex={0}
|
|
585
|
+
/>
|
|
586
|
+
</div>
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Props
|
|
590
|
+
|
|
591
|
+
| Prop | Type | Default | Description |
|
|
592
|
+
|------|------|---------|-------------|
|
|
593
|
+
| `images` | `ImageItem[]` | required | Array of images to display |
|
|
594
|
+
| `initialIndex` | `number` | `0` | Index of the image to show first |
|
|
595
|
+
| `inDialog` | `boolean` | `false` | Hide expand button (for nested usage) |
|
|
596
|
+
|
|
597
|
+
### Keyboard Shortcuts
|
|
598
|
+
|
|
599
|
+
| Key | Action |
|
|
600
|
+
|-----|--------|
|
|
601
|
+
| `+` / `=` | Zoom in |
|
|
602
|
+
| `-` | Zoom out |
|
|
603
|
+
| `0` | Reset to fit |
|
|
604
|
+
| `R` | Rotate 90° |
|
|
605
|
+
| `←` | Previous image (gallery) |
|
|
606
|
+
| `→` | Next image (gallery) |
|
|
607
|
+
|
|
562
608
|
## JSON Tree
|
|
563
609
|
|
|
564
610
|
JSON visualization with three display modes:
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useImageCache, useMediaCacheStore, generateContentKey } from './chunk-5LBDYFWH.mjs';
|
|
2
2
|
import { __name } from './chunk-CGILA3WO.mjs';
|
|
3
3
|
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
-
import { ZoomOut, ZoomIn, Maximize2, FlipHorizontal, FlipVertical, RotateCw, Expand, AlertCircle,
|
|
4
|
+
import { ZoomOut, ZoomIn, Maximize2, FlipHorizontal, FlipVertical, RotateCw, Expand, ImageIcon, AlertCircle, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
5
5
|
import { useControls, TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch';
|
|
6
6
|
import { Alert, AlertDescription, cn as cn$1, Dialog, DialogContent, DialogTitle } from '@djangocfg/ui-core';
|
|
7
7
|
import { useTypedT } from '@djangocfg/i18n';
|
|
8
|
+
import { useHotkey } from '@djangocfg/ui-core/hooks';
|
|
8
9
|
import { Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@djangocfg/ui-core/components';
|
|
9
10
|
import { createMediaLogger, cn } from '@djangocfg/ui-core/lib';
|
|
10
11
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
@@ -368,8 +369,20 @@ function useImageLoading(options) {
|
|
|
368
369
|
};
|
|
369
370
|
}
|
|
370
371
|
__name(useImageLoading, "useImageLoading");
|
|
371
|
-
function ImageViewer({
|
|
372
|
+
function ImageViewer({
|
|
373
|
+
images,
|
|
374
|
+
initialIndex = 0,
|
|
375
|
+
inDialog = false
|
|
376
|
+
}) {
|
|
372
377
|
const t = useTypedT();
|
|
378
|
+
const [currentIndex, setCurrentIndex] = useState(
|
|
379
|
+
() => Math.max(0, Math.min(initialIndex, images.length - 1))
|
|
380
|
+
);
|
|
381
|
+
useEffect(() => {
|
|
382
|
+
setCurrentIndex(Math.max(0, Math.min(initialIndex, images.length - 1)));
|
|
383
|
+
}, [initialIndex, images.length]);
|
|
384
|
+
const current = images[currentIndex];
|
|
385
|
+
const hasMultiple = images.length > 1;
|
|
373
386
|
const [scale, setScale] = useState(1);
|
|
374
387
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
375
388
|
const [loadError, setLoadError] = useState(false);
|
|
@@ -387,29 +400,34 @@ function ImageViewer({ file, content, src: directSrc, inDialog = false }) {
|
|
|
387
400
|
useProgressiveLoading,
|
|
388
401
|
error,
|
|
389
402
|
hasContent
|
|
390
|
-
} = useImageLoading({
|
|
403
|
+
} = useImageLoading({
|
|
404
|
+
content: current?.content ?? "",
|
|
405
|
+
mimeType: current?.file.mimeType,
|
|
406
|
+
src: current?.src
|
|
407
|
+
});
|
|
391
408
|
useEffect(() => {
|
|
392
409
|
setLoadError(false);
|
|
393
410
|
}, [src]);
|
|
394
411
|
const { transform, rotate, flipH, flipV, transformStyle } = useImageTransform({
|
|
395
|
-
resetKey: file.path
|
|
412
|
+
resetKey: current?.file.path ?? ""
|
|
396
413
|
});
|
|
397
414
|
const handleZoomPreset = useCallback((value) => {
|
|
398
415
|
if (!controlsRef.current) return;
|
|
399
|
-
if (value === "fit")
|
|
400
|
-
|
|
401
|
-
} else {
|
|
402
|
-
controlsRef.current.setTransform(0, 0, value);
|
|
403
|
-
}
|
|
404
|
-
}, []);
|
|
405
|
-
const handleExpand = useCallback(() => {
|
|
406
|
-
setDialogOpen(true);
|
|
416
|
+
if (value === "fit") controlsRef.current.resetTransform();
|
|
417
|
+
else controlsRef.current.setTransform(0, 0, value);
|
|
407
418
|
}, []);
|
|
419
|
+
const handleExpand = useCallback(() => setDialogOpen(true), []);
|
|
420
|
+
const prev = useCallback(
|
|
421
|
+
() => setCurrentIndex((i) => (i - 1 + images.length) % images.length),
|
|
422
|
+
[images.length]
|
|
423
|
+
);
|
|
424
|
+
const next = useCallback(
|
|
425
|
+
() => setCurrentIndex((i) => (i + 1) % images.length),
|
|
426
|
+
[images.length]
|
|
427
|
+
);
|
|
408
428
|
useEffect(() => {
|
|
409
429
|
const handleKeyDown = /* @__PURE__ */ __name((e) => {
|
|
410
|
-
if (!containerRef.current?.contains(document.activeElement) && document.activeElement !== containerRef.current)
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
430
|
+
if (!containerRef.current?.contains(document.activeElement) && document.activeElement !== containerRef.current) return;
|
|
413
431
|
const controls = controlsRef.current;
|
|
414
432
|
if (!controls) return;
|
|
415
433
|
switch (e.key) {
|
|
@@ -437,6 +455,14 @@ function ImageViewer({ file, content, src: directSrc, inDialog = false }) {
|
|
|
437
455
|
window.addEventListener("keydown", handleKeyDown);
|
|
438
456
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
439
457
|
}, [rotate]);
|
|
458
|
+
useHotkey("ArrowLeft", prev, { enabled: hasMultiple, preventDefault: true });
|
|
459
|
+
useHotkey("ArrowRight", next, { enabled: hasMultiple, preventDefault: true });
|
|
460
|
+
if (!current) {
|
|
461
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-2 bg-muted/30", children: [
|
|
462
|
+
/* @__PURE__ */ jsx(ImageIcon, { className: "w-12 h-12 text-muted-foreground/50" }),
|
|
463
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: labels.noImage })
|
|
464
|
+
] });
|
|
465
|
+
}
|
|
440
466
|
if (error || loadError) {
|
|
441
467
|
return /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-3 bg-muted/30 p-4", children: [
|
|
442
468
|
/* @__PURE__ */ jsx(AlertCircle, { className: "w-12 h-12 text-destructive/70" }),
|
|
@@ -514,12 +540,7 @@ function ImageViewer({ file, content, src: directSrc, inDialog = false }) {
|
|
|
514
540
|
alt: "",
|
|
515
541
|
"aria-hidden": "true",
|
|
516
542
|
className: "absolute inset-0 max-w-full max-h-full object-contain select-none",
|
|
517
|
-
style: {
|
|
518
|
-
transform: transformStyle,
|
|
519
|
-
filter: "blur(20px)",
|
|
520
|
-
transition: "opacity 0.3s ease-out",
|
|
521
|
-
opacity: isFullyLoaded ? 0 : 1
|
|
522
|
-
},
|
|
543
|
+
style: { transform: transformStyle, filter: "blur(20px)", transition: "opacity 0.3s ease-out", opacity: isFullyLoaded ? 0 : 1 },
|
|
523
544
|
draggable: false
|
|
524
545
|
}
|
|
525
546
|
),
|
|
@@ -527,7 +548,7 @@ function ImageViewer({ file, content, src: directSrc, inDialog = false }) {
|
|
|
527
548
|
"img",
|
|
528
549
|
{
|
|
529
550
|
src,
|
|
530
|
-
alt: file.name,
|
|
551
|
+
alt: current.file.name,
|
|
531
552
|
className: "max-w-full max-h-full object-contain select-none",
|
|
532
553
|
style: {
|
|
533
554
|
transform: transformStyle,
|
|
@@ -545,10 +566,35 @@ function ImageViewer({ file, content, src: directSrc, inDialog = false }) {
|
|
|
545
566
|
]
|
|
546
567
|
}
|
|
547
568
|
),
|
|
569
|
+
hasMultiple && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
570
|
+
/* @__PURE__ */ jsx(
|
|
571
|
+
"button",
|
|
572
|
+
{
|
|
573
|
+
type: "button",
|
|
574
|
+
onClick: prev,
|
|
575
|
+
className: "absolute left-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-1.5 transition-colors",
|
|
576
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-5 w-5" })
|
|
577
|
+
}
|
|
578
|
+
),
|
|
579
|
+
/* @__PURE__ */ jsx(
|
|
580
|
+
"button",
|
|
581
|
+
{
|
|
582
|
+
type: "button",
|
|
583
|
+
onClick: next,
|
|
584
|
+
className: "absolute right-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-1.5 transition-colors",
|
|
585
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-5 w-5" })
|
|
586
|
+
}
|
|
587
|
+
),
|
|
588
|
+
/* @__PURE__ */ jsxs("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 z-10 bg-black/50 text-white text-xs px-2 py-0.5 rounded-full pointer-events-none", children: [
|
|
589
|
+
currentIndex + 1,
|
|
590
|
+
" / ",
|
|
591
|
+
images.length
|
|
592
|
+
] })
|
|
593
|
+
] }),
|
|
548
594
|
!inDialog && /* @__PURE__ */ jsx(Dialog, { open: dialogOpen, onOpenChange: setDialogOpen, children: /* @__PURE__ */ jsxs(DialogContent, { className: "max-w-[95vw] max-h-[95vh] w-[95vw] h-[95vh] p-0 overflow-hidden [&>button]:hidden flex flex-col", children: [
|
|
549
|
-
/* @__PURE__ */ jsx(DialogTitle, { className: "sr-only", children: file.name }),
|
|
550
|
-
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-4 py-2 border-b shrink-0", children: /* @__PURE__ */ jsx("span", { className: "text-sm font-medium truncate", children: file.name }) }),
|
|
551
|
-
/* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsx(ImageViewer, {
|
|
595
|
+
/* @__PURE__ */ jsx(DialogTitle, { className: "sr-only", children: current.file.name }),
|
|
596
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-4 py-2 border-b shrink-0", children: /* @__PURE__ */ jsx("span", { className: "text-sm font-medium truncate", children: current.file.name }) }),
|
|
597
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsx(ImageViewer, { images, initialIndex: currentIndex, inDialog: true }) })
|
|
552
598
|
] }) })
|
|
553
599
|
]
|
|
554
600
|
}
|
|
@@ -557,5 +603,5 @@ function ImageViewer({ file, content, src: directSrc, inDialog = false }) {
|
|
|
557
603
|
__name(ImageViewer, "ImageViewer");
|
|
558
604
|
|
|
559
605
|
export { ImageInfo, ImageToolbar, ImageViewer };
|
|
560
|
-
//# sourceMappingURL=chunk-
|
|
561
|
-
//# sourceMappingURL=chunk-
|
|
606
|
+
//# sourceMappingURL=chunk-BEURMR25.mjs.map
|
|
607
|
+
//# sourceMappingURL=chunk-BEURMR25.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tools/ImageViewer/utils/constants.ts","../src/tools/ImageViewer/utils/lqip.ts","../src/tools/ImageViewer/utils/debug.ts","../src/tools/ImageViewer/components/ImageToolbar.tsx","../src/tools/ImageViewer/components/ImageInfo.tsx","../src/tools/ImageViewer/hooks/useImageTransform.ts","../src/tools/ImageViewer/hooks/useImageLoading.ts","../src/tools/ImageViewer/components/ImageViewer.tsx"],"names":["jsxs","useState","useEffect","useMemo","contentKey","url","useTypedT","useRef","useCallback","jsx","cn","Fragment"],"mappings":"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","file":"chunk-BEURMR25.mjs","sourcesContent":["/**\n * ImageViewer constants\n */\n\nimport type { ZoomPreset, ImageTransform } from '../types';\n\n// =============================================================================\n// SIZE LIMITS\n// =============================================================================\n\n/** Maximum image size before blocking (50MB) */\nexport const MAX_IMAGE_SIZE = 50 * 1024 * 1024;\n\n/** Image size threshold for warning (10MB) */\nexport const WARNING_IMAGE_SIZE = 10 * 1024 * 1024;\n\n/** Progressive loading threshold - use LQIP for images > 500KB */\nexport const PROGRESSIVE_LOADING_THRESHOLD = 500 * 1024;\n\n// =============================================================================\n// LQIP CONFIGURATION\n// =============================================================================\n\n/** Low-quality placeholder size (32x32) */\nexport const LQIP_SIZE = 32;\n\n/** LQIP JPEG quality */\nexport const LQIP_QUALITY = 0.5;\n\n// =============================================================================\n// ZOOM CONFIGURATION\n// =============================================================================\n\n/** Minimum zoom level */\nexport const MIN_ZOOM = 0.1;\n\n/** Maximum zoom level */\nexport const MAX_ZOOM = 8;\n\n/** Available zoom presets */\nexport const ZOOM_PRESETS: readonly ZoomPreset[] = [\n { label: 'Fit', value: 'fit' },\n { label: '25%', value: 0.25 },\n { label: '50%', value: 0.5 },\n { label: '100%', value: 1 },\n { label: '200%', value: 2 },\n { label: '400%', value: 4 },\n] as const;\n\n// =============================================================================\n// DEFAULT VALUES\n// =============================================================================\n\n/** Default transform state */\nexport const DEFAULT_TRANSFORM: ImageTransform = {\n rotation: 0,\n flipH: false,\n flipV: false,\n};\n","/**\n * LQIP (Low-Quality Image Placeholder) generator\n *\n * Creates a tiny blurred preview image for progressive loading.\n */\n\nimport { LQIP_QUALITY, LQIP_SIZE } from './constants';\n\n/**\n * Create a low-quality image placeholder from source URL\n *\n * @param imageSrc - Full quality image URL\n * @returns Data URL of tiny preview, or null on error\n */\nexport async function createLQIP(imageSrc: string): Promise<string | null> {\n try {\n // Load the full image\n const img = new Image();\n img.crossOrigin = 'anonymous';\n\n await new Promise<void>((resolve, reject) => {\n img.onload = () => resolve();\n img.onerror = () => reject(new Error('Failed to load image for LQIP'));\n img.src = imageSrc;\n });\n\n // Calculate aspect ratio preserving dimensions\n const aspect = img.naturalWidth / img.naturalHeight;\n const width = aspect >= 1 ? LQIP_SIZE : Math.round(LQIP_SIZE * aspect);\n const height = aspect >= 1 ? Math.round(LQIP_SIZE / aspect) : LQIP_SIZE;\n\n // Create canvas for downscaling\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n\n // Draw scaled down image\n ctx.drawImage(img, 0, 0, width, height);\n\n // Return as data URL (very small, ~1-2KB)\n return canvas.toDataURL('image/jpeg', LQIP_QUALITY);\n } catch {\n return null;\n }\n}\n","'use client';\n\n/**\n * ImageViewer Debug Logger\n *\n * Uses universal logger with media-specific helpers.\n * Logs go to both console (dev) and zustand store (for Console panel).\n */\n\nimport { createMediaLogger } from '@djangocfg/ui-core/lib';\n\nexport const imageDebug = createMediaLogger('ImageViewer');\n\nexport default imageDebug;\n","'use client';\n\n/**\n * ImageToolbar - Floating toolbar for image controls\n */\n\nimport { useMemo } from 'react';\nimport { ZoomIn, ZoomOut, RotateCw, FlipHorizontal, FlipVertical, Maximize2, Expand } from 'lucide-react';\nimport { useControls } from 'react-zoom-pan-pinch';\nimport { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@djangocfg/ui-core/components';\nimport { useTypedT, type I18nTranslations } from '@djangocfg/i18n';\nimport { cn } from '@djangocfg/ui-core/lib';\nimport { ZOOM_PRESETS } from '../utils';\nimport type { ImageToolbarProps } from '../types';\n\n// =============================================================================\n// COMPONENT\n// =============================================================================\n\nexport function ImageToolbar({\n scale,\n transform,\n onRotate,\n onFlipH,\n onFlipV,\n onZoomPreset,\n onExpand,\n}: ImageToolbarProps) {\n const t = useTypedT<I18nTranslations>();\n const { zoomIn, zoomOut, resetTransform } = useControls();\n\n const labels = useMemo(() => ({\n zoomIn: t('tools.image.zoomIn'),\n zoomOut: t('tools.image.zoomOut'),\n fitToView: t('tools.image.fitToView'),\n flipHorizontal: t('tools.image.flipHorizontal'),\n flipVertical: t('tools.image.flipVertical'),\n rotate: t('tools.image.rotate'),\n fullscreen: t('tools.image.fullscreen'),\n }), [t]);\n\n const zoomLabel = useMemo(() => {\n const percent = Math.round(scale * 100);\n return `${percent}%`;\n }, [scale]);\n\n return (\n <div className=\"absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex items-center gap-0.5 bg-background/90 backdrop-blur-sm border rounded-lg p-1 shadow-lg\">\n {/* Zoom controls */}\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-7 w-7\"\n onClick={() => zoomOut()}\n title={labels.zoomOut}\n >\n <ZoomOut className=\"h-3.5 w-3.5\" />\n </Button>\n\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\" className=\"h-7 px-2 min-w-[52px] font-mono text-xs\">\n {zoomLabel}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"center\" className=\"min-w-[80px]\">\n {ZOOM_PRESETS.map((preset) => (\n <DropdownMenuItem\n key={preset.label}\n onClick={() => onZoomPreset(preset.value)}\n className=\"text-xs justify-center\"\n >\n {preset.label}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-7 w-7\"\n onClick={() => zoomIn()}\n title={labels.zoomIn}\n >\n <ZoomIn className=\"h-3.5 w-3.5\" />\n </Button>\n\n <div className=\"w-px h-4 bg-border mx-1\" />\n\n {/* Fit to view */}\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-7 w-7\"\n onClick={() => resetTransform()}\n title={labels.fitToView}\n >\n <Maximize2 className=\"h-3.5 w-3.5\" />\n </Button>\n\n <div className=\"w-px h-4 bg-border mx-1\" />\n\n {/* Transform controls */}\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className={cn('h-7 w-7', transform.flipH && 'bg-accent')}\n onClick={onFlipH}\n title={labels.flipHorizontal}\n >\n <FlipHorizontal className=\"h-3.5 w-3.5\" />\n </Button>\n\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className={cn('h-7 w-7', transform.flipV && 'bg-accent')}\n onClick={onFlipV}\n title={labels.flipVertical}\n >\n <FlipVertical className=\"h-3.5 w-3.5\" />\n </Button>\n\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-7 w-7\"\n onClick={onRotate}\n title={labels.rotate}\n >\n <RotateCw className=\"h-3.5 w-3.5\" />\n </Button>\n\n {transform.rotation !== 0 && (\n <span className=\"text-[10px] text-muted-foreground font-mono pl-1\">\n {transform.rotation}°\n </span>\n )}\n\n {onExpand && (\n <>\n <div className=\"w-px h-4 bg-border mx-1\" />\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-7 w-7\"\n onClick={onExpand}\n title={labels.fullscreen}\n >\n <Expand className=\"h-3.5 w-3.5\" />\n </Button>\n </>\n )}\n </div>\n );\n}\n","'use client';\n\n/**\n * ImageInfo - Displays image dimensions badge\n */\n\nimport { useEffect, useState } from 'react';\nimport { useImageCache } from '../../../stores/mediaCache';\nimport type { ImageInfoProps } from '../types';\n\n// =============================================================================\n// COMPONENT\n// =============================================================================\n\nexport function ImageInfo({ src }: ImageInfoProps) {\n const { getDimensions, cacheDimensions } = useImageCache();\n const [dimensions, setDimensions] = useState<{ w: number; h: number } | null>(null);\n\n useEffect(() => {\n // Check cache first\n const cached = getDimensions(src);\n if (cached) {\n setDimensions({ w: cached.width, h: cached.height });\n return;\n }\n\n // Load and cache dimensions\n const img = new Image();\n img.onload = () => {\n const dims = { w: img.naturalWidth, h: img.naturalHeight };\n setDimensions(dims);\n cacheDimensions(src, { width: dims.w, height: dims.h });\n };\n img.src = src;\n }, [src, getDimensions, cacheDimensions]);\n\n if (!dimensions) return null;\n\n return (\n <div className=\"absolute top-3 right-3 z-10 px-2 py-1 bg-background/80 backdrop-blur-sm border rounded text-[10px] text-muted-foreground font-mono\">\n {dimensions.w} × {dimensions.h}\n </div>\n );\n}\n","'use client';\n\n/**\n * useImageTransform - Manages image rotation and flip state\n */\n\nimport { useCallback, useEffect, useMemo, useState } from 'react';\n\nimport { DEFAULT_TRANSFORM } from '../utils';\n\nimport type { ImageTransform } from '../types';\n// =============================================================================\n// TYPES\n// =============================================================================\n\nexport interface UseImageTransformOptions {\n /** Reset transform when this key changes */\n resetKey?: string;\n}\n\nexport interface UseImageTransformReturn {\n /** Current transform state */\n transform: ImageTransform;\n /** Rotate 90 degrees clockwise */\n rotate: () => void;\n /** Toggle horizontal flip */\n flipH: () => void;\n /** Toggle vertical flip */\n flipV: () => void;\n /** Reset all transforms */\n reset: () => void;\n /** CSS transform string for applying to image */\n transformStyle: string;\n}\n\n// =============================================================================\n// HOOK\n// =============================================================================\n\nexport function useImageTransform(\n options: UseImageTransformOptions = {}\n): UseImageTransformReturn {\n const { resetKey } = options;\n\n const [transform, setTransform] = useState<ImageTransform>(DEFAULT_TRANSFORM);\n\n // Reset transform when key changes\n useEffect(() => {\n setTransform(DEFAULT_TRANSFORM);\n }, [resetKey]);\n\n const rotate = useCallback(() => {\n setTransform((prev) => ({\n ...prev,\n rotation: (prev.rotation + 90) % 360,\n }));\n }, []);\n\n const flipH = useCallback(() => {\n setTransform((prev) => ({\n ...prev,\n flipH: !prev.flipH,\n }));\n }, []);\n\n const flipV = useCallback(() => {\n setTransform((prev) => ({\n ...prev,\n flipV: !prev.flipV,\n }));\n }, []);\n\n const reset = useCallback(() => {\n setTransform(DEFAULT_TRANSFORM);\n }, []);\n\n // Build CSS transform string\n const transformStyle = useMemo(() => {\n const transforms: string[] = [];\n\n if (transform.rotation !== 0) {\n transforms.push(`rotate(${transform.rotation}deg)`);\n }\n if (transform.flipH) {\n transforms.push('scaleX(-1)');\n }\n if (transform.flipV) {\n transforms.push('scaleY(-1)');\n }\n\n return transforms.join(' ') || 'none';\n }, [transform]);\n\n return {\n transform,\n rotate,\n flipH,\n flipV,\n reset,\n transformStyle,\n };\n}\n","'use client';\n\n/**\n * useImageLoading - Manages image loading state with LQIP\n */\n\nimport { useEffect, useRef, useState } from 'react';\n\nimport { generateContentKey, useMediaCacheStore } from '../../../stores/mediaCache';\nimport {\n createLQIP, imageDebug, MAX_IMAGE_SIZE, PROGRESSIVE_LOADING_THRESHOLD, WARNING_IMAGE_SIZE\n} from '../utils';\n\n// =============================================================================\n// TYPES\n// =============================================================================\n\nexport interface UseImageLoadingOptions {\n /** Image content (ArrayBuffer or string) */\n content: string | ArrayBuffer;\n /** MIME type for blob creation */\n mimeType?: string;\n /**\n * Direct image URL (bypasses content→blob conversion).\n * When provided, content is ignored and URL is used directly.\n */\n src?: string;\n}\n\nexport interface UseImageLoadingReturn {\n /** Blob URL source for the image */\n src: string | null;\n /** Low-quality placeholder URL */\n lqip: string | null;\n /** Whether full image is loaded */\n isFullyLoaded: boolean;\n /** Whether to use progressive loading */\n useProgressiveLoading: boolean;\n /** Error message if any */\n error: string | null;\n /** Content key for caching */\n contentKey: string | null;\n /** Image size in bytes */\n size: number;\n /** Whether content exists */\n hasContent: boolean;\n}\n\n// =============================================================================\n// HOOK\n// =============================================================================\n\nexport function useImageLoading(options: UseImageLoadingOptions): UseImageLoadingReturn {\n const { content, mimeType, src: directSrc } = options;\n\n // Get stable function references from store (not from hook to avoid re-renders)\n const getOrCreateBlobUrl = useMediaCacheStore.getState().getOrCreateBlobUrl;\n const releaseBlobUrl = useMediaCacheStore.getState().releaseBlobUrl;\n\n const [src, setSrc] = useState<string | null>(null);\n const [lqip, setLqip] = useState<string | null>(null);\n const [isFullyLoaded, setIsFullyLoaded] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const contentKeyRef = useRef<string | null>(null);\n const isMountedRef = useRef(true);\n\n // Calculate size and flags\n const size = content ? (typeof content === 'string' ? content.length : content.byteLength) : 0;\n // When directSrc is provided, we have content (the URL itself)\n const hasContent = directSrc ? true : size > 0;\n const useProgressiveLoading = directSrc ? false : size > PROGRESSIVE_LOADING_THRESHOLD;\n\n // Track unmount for cleanup\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n // Release blob URL only on actual unmount\n if (contentKeyRef.current) {\n useMediaCacheStore.getState().releaseBlobUrl(contentKeyRef.current);\n contentKeyRef.current = null;\n }\n };\n }, []);\n\n // Create blob URL with caching and size validation\n useEffect(() => {\n // Reset error state\n setError(null);\n\n // Direct URL mode - use as-is without blob conversion\n if (directSrc) {\n imageDebug.load(directSrc, 'url');\n setSrc(directSrc);\n setIsFullyLoaded(true);\n return;\n }\n\n if (!hasContent) {\n setSrc(null);\n return;\n }\n\n // Size validation - reject oversized images\n if (size > MAX_IMAGE_SIZE) {\n const sizeMB = (size / 1024 / 1024).toFixed(1);\n const errorMsg = `Image too large: ${sizeMB}MB (maximum: 50MB)`;\n imageDebug.error(errorMsg, { size, sizeMB, maxSize: MAX_IMAGE_SIZE });\n setError(errorMsg);\n setSrc(null);\n return;\n }\n\n // Warn about large images\n if (size > WARNING_IMAGE_SIZE) {\n const sizeMB = (size / 1024 / 1024).toFixed(1);\n imageDebug.warn(`Large image: ${sizeMB}MB - may impact performance`);\n }\n\n // Handle string content (data URLs or binary strings)\n if (typeof content === 'string') {\n // Pass through data URLs directly\n if (content.startsWith('data:')) {\n imageDebug.load(content.slice(0, 50) + '...', 'data-url');\n setSrc(content);\n return;\n }\n\n // Convert binary string to ArrayBuffer and use Blob URL\n const encoder = new TextEncoder();\n const buffer = encoder.encode(content).buffer;\n const contentKey = generateContentKey(buffer);\n\n // Release previous blob URL if content changed\n if (contentKeyRef.current && contentKeyRef.current !== contentKey) {\n releaseBlobUrl(contentKeyRef.current);\n }\n\n contentKeyRef.current = contentKey;\n const url = getOrCreateBlobUrl(contentKey, buffer, mimeType || 'image/png');\n imageDebug.load(url, 'blob');\n imageDebug.state('loaded', { size, mimeType, contentKey });\n setSrc(url);\n return;\n }\n\n // Handle ArrayBuffer with cached blob URL\n const contentKey = generateContentKey(content);\n\n // Release previous blob URL if content changed\n if (contentKeyRef.current && contentKeyRef.current !== contentKey) {\n releaseBlobUrl(contentKeyRef.current);\n }\n\n contentKeyRef.current = contentKey;\n const url = getOrCreateBlobUrl(contentKey, content, mimeType || 'image/png');\n imageDebug.load(url, 'blob');\n imageDebug.state('loaded', { size, mimeType, contentKey });\n setSrc(url);\n\n // No cleanup here - cleanup happens in unmount effect above\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [content, mimeType, hasContent, size, directSrc]);\n\n // Create LQIP for progressive loading\n useEffect(() => {\n if (!src || !useProgressiveLoading) {\n setLqip(null);\n setIsFullyLoaded(true);\n return;\n }\n\n setIsFullyLoaded(false);\n imageDebug.state('progressive loading', { size });\n\n // Create low-quality placeholder\n createLQIP(src).then((placeholder) => {\n if (placeholder) {\n imageDebug.debug('LQIP created');\n setLqip(placeholder);\n }\n });\n\n // Pre-load full image\n const img = new Image();\n img.onload = () => {\n imageDebug.state('fully loaded');\n setIsFullyLoaded(true);\n };\n img.onerror = () => {\n imageDebug.error('Failed to load full image');\n };\n img.src = src;\n }, [src, useProgressiveLoading, size]);\n\n return {\n src,\n lqip,\n isFullyLoaded,\n useProgressiveLoading,\n error,\n contentKey: contentKeyRef.current,\n size,\n hasContent,\n };\n}\n","'use client';\n\n/**\n * ImageViewer - Image viewer with zoom, pan, rotate, flip, gallery navigation\n *\n * Features:\n * - Zoom with mouse wheel and presets\n * - Pan with drag\n * - Rotate 90°\n * - Flip horizontal/vertical\n * - Fullscreen dialog\n * - Keyboard shortcuts (+/-, 0, r, ←/→ for gallery)\n * - Gallery mode: pass images[] with multiple items\n */\n\nimport { useEffect, useState, useRef, useCallback, useMemo } from 'react';\nimport { ImageIcon, AlertCircle, ChevronLeft, ChevronRight } from 'lucide-react';\nimport { TransformWrapper, TransformComponent, useControls } from 'react-zoom-pan-pinch';\nimport { cn, Dialog, DialogContent, DialogTitle, Alert, AlertDescription } from '@djangocfg/ui-core';\nimport { useTypedT, type I18nTranslations } from '@djangocfg/i18n';\nimport { useHotkey } from '@djangocfg/ui-core/hooks';\n\nimport { ImageToolbar } from './ImageToolbar';\nimport { ImageInfo } from './ImageInfo';\nimport { useImageTransform, useImageLoading } from '../hooks';\nimport type { ImageViewerProps, ImageItem } from '../types';\n\n// =============================================================================\n// COMPONENT\n// =============================================================================\n\nexport function ImageViewer({\n images,\n initialIndex = 0,\n inDialog = false,\n}: ImageViewerProps) {\n const t = useTypedT<I18nTranslations>();\n\n const [currentIndex, setCurrentIndex] = useState(() =>\n Math.max(0, Math.min(initialIndex, images.length - 1))\n );\n\n // Reset index when initialIndex changes (e.g. opening different image)\n useEffect(() => {\n setCurrentIndex(Math.max(0, Math.min(initialIndex, images.length - 1)));\n }, [initialIndex, images.length]);\n\n const current = images[currentIndex];\n const hasMultiple = images.length > 1;\n\n const [scale, setScale] = useState(1);\n const [dialogOpen, setDialogOpen] = useState(false);\n const [loadError, setLoadError] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const controlsRef = useRef<ReturnType<typeof useControls> | null>(null);\n\n const labels = useMemo(() => ({\n noImage: t('tools.image.noImage'),\n failedToLoad: t('tools.image.failedToLoad'),\n loading: t('ui.form.loading'),\n }), [t]);\n\n const {\n src,\n lqip,\n isFullyLoaded,\n useProgressiveLoading,\n error,\n hasContent,\n } = useImageLoading({\n content: current?.content ?? '',\n mimeType: current?.file.mimeType,\n src: current?.src,\n });\n\n useEffect(() => { setLoadError(false); }, [src]);\n\n const { transform, rotate, flipH, flipV, transformStyle } = useImageTransform({\n resetKey: current?.file.path ?? '',\n });\n\n const handleZoomPreset = useCallback((value: number | 'fit') => {\n if (!controlsRef.current) return;\n if (value === 'fit') controlsRef.current.resetTransform();\n else controlsRef.current.setTransform(0, 0, value);\n }, []);\n\n const handleExpand = useCallback(() => setDialogOpen(true), []);\n\n const prev = useCallback(() =>\n setCurrentIndex((i) => (i - 1 + images.length) % images.length),\n [images.length]\n );\n\n const next = useCallback(() =>\n setCurrentIndex((i) => (i + 1) % images.length),\n [images.length]\n );\n\n // Keyboard: zoom/rotate (only when container focused)\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (!containerRef.current?.contains(document.activeElement) &&\n document.activeElement !== containerRef.current) return;\n const controls = controlsRef.current;\n if (!controls) return;\n switch (e.key) {\n case '+': case '=': e.preventDefault(); controls.zoomIn(); break;\n case '-': e.preventDefault(); controls.zoomOut(); break;\n case '0': e.preventDefault(); controls.resetTransform(); break;\n case 'r': if (!e.metaKey && !e.ctrlKey) { e.preventDefault(); rotate(); } break;\n }\n };\n window.addEventListener('keydown', handleKeyDown);\n return () => window.removeEventListener('keydown', handleKeyDown);\n }, [rotate]);\n\n // Keyboard: gallery navigation (global when open)\n useHotkey('ArrowLeft', prev, { enabled: hasMultiple, preventDefault: true });\n useHotkey('ArrowRight', next, { enabled: hasMultiple, preventDefault: true });\n\n if (!current) {\n return (\n <div className=\"flex-1 flex flex-col items-center justify-center gap-2 bg-muted/30\">\n <ImageIcon className=\"w-12 h-12 text-muted-foreground/50\" />\n <p className=\"text-sm text-muted-foreground\">{labels.noImage}</p>\n </div>\n );\n }\n\n if (error || loadError) {\n return (\n <div className=\"flex-1 flex flex-col items-center justify-center gap-3 bg-muted/30 p-4\">\n <AlertCircle className=\"w-12 h-12 text-destructive/70\" />\n <Alert variant=\"destructive\" className=\"max-w-md\">\n <AlertCircle className=\"h-4 w-4\" />\n <AlertDescription>{error || labels.failedToLoad}</AlertDescription>\n </Alert>\n </div>\n );\n }\n\n if (!hasContent) {\n return (\n <div className=\"flex-1 flex flex-col items-center justify-center gap-2 bg-muted/30\">\n <ImageIcon className=\"w-12 h-12 text-muted-foreground/50\" />\n <p className=\"text-sm text-muted-foreground\">{labels.noImage}</p>\n </div>\n );\n }\n\n return (\n <div\n ref={containerRef}\n tabIndex={0}\n className={cn(\n 'flex-1 h-full relative overflow-hidden outline-none',\n 'bg-[length:16px_16px]',\n '[background-color:hsl(var(--muted)/0.2)]',\n '[background-image:linear-gradient(45deg,hsl(var(--muted)/0.4)_25%,transparent_25%),linear-gradient(-45deg,hsl(var(--muted)/0.4)_25%,transparent_25%),linear-gradient(45deg,transparent_75%,hsl(var(--muted)/0.4)_75%),linear-gradient(-45deg,transparent_75%,hsl(var(--muted)/0.4)_75%)]',\n '[background-position:0_0,0_8px,8px_-8px,-8px_0px]'\n )}\n >\n {src && <ImageInfo src={src} />}\n\n {useProgressiveLoading && !isFullyLoaded && (\n <div className=\"absolute top-3 left-3 z-10 px-2 py-1 bg-background/80 backdrop-blur-sm border rounded text-[10px] text-muted-foreground font-mono flex items-center gap-1.5\">\n <div className=\"w-2 h-2 bg-blue-500 rounded-full animate-pulse\" />\n {labels.loading}\n </div>\n )}\n\n <TransformWrapper\n initialScale={1}\n minScale={0.1}\n maxScale={8}\n centerOnInit\n centerZoomedOut\n onTransformed={(ref, state) => { setScale(state.scale); controlsRef.current = ref; }}\n onInit={(ref) => { controlsRef.current = ref; }}\n wheel={{ step: 0.1 }}\n doubleClick={{ mode: 'toggle', step: 2 }}\n panning={{ velocityDisabled: false }}\n >\n <ImageToolbar\n scale={scale}\n transform={transform}\n onRotate={rotate}\n onFlipH={flipH}\n onFlipV={flipV}\n onZoomPreset={handleZoomPreset}\n onExpand={!inDialog ? handleExpand : undefined}\n />\n\n <TransformComponent\n wrapperClass=\"!w-full !h-full cursor-grab active:cursor-grabbing\"\n contentClass=\"!w-full !h-full flex items-center justify-center\"\n >\n <div className=\"relative\">\n {useProgressiveLoading && lqip && !isFullyLoaded && (\n <img\n src={lqip}\n alt=\"\"\n aria-hidden=\"true\"\n className=\"absolute inset-0 max-w-full max-h-full object-contain select-none\"\n style={{ transform: transformStyle, filter: 'blur(20px)', transition: 'opacity 0.3s ease-out', opacity: isFullyLoaded ? 0 : 1 }}\n draggable={false}\n />\n )}\n {src && (\n <img\n src={src}\n alt={current.file.name}\n className=\"max-w-full max-h-full object-contain select-none\"\n style={{\n transform: transformStyle,\n transition: useProgressiveLoading ? 'transform 0.15s ease-out, opacity 0.3s ease-out' : 'transform 0.15s ease-out',\n opacity: useProgressiveLoading && !isFullyLoaded ? 0 : 1,\n }}\n draggable={false}\n crossOrigin=\"anonymous\"\n onError={() => setLoadError(true)}\n />\n )}\n </div>\n </TransformComponent>\n </TransformWrapper>\n\n {/* Gallery navigation */}\n {hasMultiple && (\n <>\n <button\n type=\"button\"\n onClick={prev}\n className=\"absolute left-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-1.5 transition-colors\"\n >\n <ChevronLeft className=\"h-5 w-5\" />\n </button>\n <button\n type=\"button\"\n onClick={next}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-1.5 transition-colors\"\n >\n <ChevronRight className=\"h-5 w-5\" />\n </button>\n <div className=\"absolute bottom-2 left-1/2 -translate-x-1/2 z-10 bg-black/50 text-white text-xs px-2 py-0.5 rounded-full pointer-events-none\">\n {currentIndex + 1} / {images.length}\n </div>\n </>\n )}\n\n {/* Fullscreen dialog */}\n {!inDialog && (\n <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n <DialogContent className=\"max-w-[95vw] max-h-[95vh] w-[95vw] h-[95vh] p-0 overflow-hidden [&>button]:hidden flex flex-col\">\n <DialogTitle className=\"sr-only\">{current.file.name}</DialogTitle>\n <div className=\"flex items-center justify-between px-4 py-2 border-b shrink-0\">\n <span className=\"text-sm font-medium truncate\">{current.file.name}</span>\n </div>\n <div className=\"flex-1 min-h-0\">\n <ImageViewer images={images} initialIndex={currentIndex} inDialog />\n </div>\n </DialogContent>\n </Dialog>\n )}\n </div>\n );\n}\n"]}
|
|
@@ -7,6 +7,7 @@ var lucideReact = require('lucide-react');
|
|
|
7
7
|
var reactZoomPanPinch = require('react-zoom-pan-pinch');
|
|
8
8
|
var uiCore = require('@djangocfg/ui-core');
|
|
9
9
|
var i18n = require('@djangocfg/i18n');
|
|
10
|
+
var hooks = require('@djangocfg/ui-core/hooks');
|
|
10
11
|
var components = require('@djangocfg/ui-core/components');
|
|
11
12
|
var lib = require('@djangocfg/ui-core/lib');
|
|
12
13
|
var jsxRuntime = require('react/jsx-runtime');
|
|
@@ -370,8 +371,20 @@ function useImageLoading(options) {
|
|
|
370
371
|
};
|
|
371
372
|
}
|
|
372
373
|
chunkWGEGR3DF_cjs.__name(useImageLoading, "useImageLoading");
|
|
373
|
-
function ImageViewer({
|
|
374
|
+
function ImageViewer({
|
|
375
|
+
images,
|
|
376
|
+
initialIndex = 0,
|
|
377
|
+
inDialog = false
|
|
378
|
+
}) {
|
|
374
379
|
const t = i18n.useTypedT();
|
|
380
|
+
const [currentIndex, setCurrentIndex] = react.useState(
|
|
381
|
+
() => Math.max(0, Math.min(initialIndex, images.length - 1))
|
|
382
|
+
);
|
|
383
|
+
react.useEffect(() => {
|
|
384
|
+
setCurrentIndex(Math.max(0, Math.min(initialIndex, images.length - 1)));
|
|
385
|
+
}, [initialIndex, images.length]);
|
|
386
|
+
const current = images[currentIndex];
|
|
387
|
+
const hasMultiple = images.length > 1;
|
|
375
388
|
const [scale, setScale] = react.useState(1);
|
|
376
389
|
const [dialogOpen, setDialogOpen] = react.useState(false);
|
|
377
390
|
const [loadError, setLoadError] = react.useState(false);
|
|
@@ -389,29 +402,34 @@ function ImageViewer({ file, content, src: directSrc, inDialog = false }) {
|
|
|
389
402
|
useProgressiveLoading,
|
|
390
403
|
error,
|
|
391
404
|
hasContent
|
|
392
|
-
} = useImageLoading({
|
|
405
|
+
} = useImageLoading({
|
|
406
|
+
content: current?.content ?? "",
|
|
407
|
+
mimeType: current?.file.mimeType,
|
|
408
|
+
src: current?.src
|
|
409
|
+
});
|
|
393
410
|
react.useEffect(() => {
|
|
394
411
|
setLoadError(false);
|
|
395
412
|
}, [src]);
|
|
396
413
|
const { transform, rotate, flipH, flipV, transformStyle } = useImageTransform({
|
|
397
|
-
resetKey: file.path
|
|
414
|
+
resetKey: current?.file.path ?? ""
|
|
398
415
|
});
|
|
399
416
|
const handleZoomPreset = react.useCallback((value) => {
|
|
400
417
|
if (!controlsRef.current) return;
|
|
401
|
-
if (value === "fit")
|
|
402
|
-
|
|
403
|
-
} else {
|
|
404
|
-
controlsRef.current.setTransform(0, 0, value);
|
|
405
|
-
}
|
|
406
|
-
}, []);
|
|
407
|
-
const handleExpand = react.useCallback(() => {
|
|
408
|
-
setDialogOpen(true);
|
|
418
|
+
if (value === "fit") controlsRef.current.resetTransform();
|
|
419
|
+
else controlsRef.current.setTransform(0, 0, value);
|
|
409
420
|
}, []);
|
|
421
|
+
const handleExpand = react.useCallback(() => setDialogOpen(true), []);
|
|
422
|
+
const prev = react.useCallback(
|
|
423
|
+
() => setCurrentIndex((i) => (i - 1 + images.length) % images.length),
|
|
424
|
+
[images.length]
|
|
425
|
+
);
|
|
426
|
+
const next = react.useCallback(
|
|
427
|
+
() => setCurrentIndex((i) => (i + 1) % images.length),
|
|
428
|
+
[images.length]
|
|
429
|
+
);
|
|
410
430
|
react.useEffect(() => {
|
|
411
431
|
const handleKeyDown = /* @__PURE__ */ chunkWGEGR3DF_cjs.__name((e) => {
|
|
412
|
-
if (!containerRef.current?.contains(document.activeElement) && document.activeElement !== containerRef.current)
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
432
|
+
if (!containerRef.current?.contains(document.activeElement) && document.activeElement !== containerRef.current) return;
|
|
415
433
|
const controls = controlsRef.current;
|
|
416
434
|
if (!controls) return;
|
|
417
435
|
switch (e.key) {
|
|
@@ -439,6 +457,14 @@ function ImageViewer({ file, content, src: directSrc, inDialog = false }) {
|
|
|
439
457
|
window.addEventListener("keydown", handleKeyDown);
|
|
440
458
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
441
459
|
}, [rotate]);
|
|
460
|
+
hooks.useHotkey("ArrowLeft", prev, { enabled: hasMultiple, preventDefault: true });
|
|
461
|
+
hooks.useHotkey("ArrowRight", next, { enabled: hasMultiple, preventDefault: true });
|
|
462
|
+
if (!current) {
|
|
463
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-2 bg-muted/30", children: [
|
|
464
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ImageIcon, { className: "w-12 h-12 text-muted-foreground/50" }),
|
|
465
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: labels.noImage })
|
|
466
|
+
] });
|
|
467
|
+
}
|
|
442
468
|
if (error || loadError) {
|
|
443
469
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-3 bg-muted/30 p-4", children: [
|
|
444
470
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "w-12 h-12 text-destructive/70" }),
|
|
@@ -516,12 +542,7 @@ function ImageViewer({ file, content, src: directSrc, inDialog = false }) {
|
|
|
516
542
|
alt: "",
|
|
517
543
|
"aria-hidden": "true",
|
|
518
544
|
className: "absolute inset-0 max-w-full max-h-full object-contain select-none",
|
|
519
|
-
style: {
|
|
520
|
-
transform: transformStyle,
|
|
521
|
-
filter: "blur(20px)",
|
|
522
|
-
transition: "opacity 0.3s ease-out",
|
|
523
|
-
opacity: isFullyLoaded ? 0 : 1
|
|
524
|
-
},
|
|
545
|
+
style: { transform: transformStyle, filter: "blur(20px)", transition: "opacity 0.3s ease-out", opacity: isFullyLoaded ? 0 : 1 },
|
|
525
546
|
draggable: false
|
|
526
547
|
}
|
|
527
548
|
),
|
|
@@ -529,7 +550,7 @@ function ImageViewer({ file, content, src: directSrc, inDialog = false }) {
|
|
|
529
550
|
"img",
|
|
530
551
|
{
|
|
531
552
|
src,
|
|
532
|
-
alt: file.name,
|
|
553
|
+
alt: current.file.name,
|
|
533
554
|
className: "max-w-full max-h-full object-contain select-none",
|
|
534
555
|
style: {
|
|
535
556
|
transform: transformStyle,
|
|
@@ -547,10 +568,35 @@ function ImageViewer({ file, content, src: directSrc, inDialog = false }) {
|
|
|
547
568
|
]
|
|
548
569
|
}
|
|
549
570
|
),
|
|
571
|
+
hasMultiple && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
572
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
573
|
+
"button",
|
|
574
|
+
{
|
|
575
|
+
type: "button",
|
|
576
|
+
onClick: prev,
|
|
577
|
+
className: "absolute left-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-1.5 transition-colors",
|
|
578
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "h-5 w-5" })
|
|
579
|
+
}
|
|
580
|
+
),
|
|
581
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
582
|
+
"button",
|
|
583
|
+
{
|
|
584
|
+
type: "button",
|
|
585
|
+
onClick: next,
|
|
586
|
+
className: "absolute right-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-1.5 transition-colors",
|
|
587
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-5 w-5" })
|
|
588
|
+
}
|
|
589
|
+
),
|
|
590
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 z-10 bg-black/50 text-white text-xs px-2 py-0.5 rounded-full pointer-events-none", children: [
|
|
591
|
+
currentIndex + 1,
|
|
592
|
+
" / ",
|
|
593
|
+
images.length
|
|
594
|
+
] })
|
|
595
|
+
] }),
|
|
550
596
|
!inDialog && /* @__PURE__ */ jsxRuntime.jsx(uiCore.Dialog, { open: dialogOpen, onOpenChange: setDialogOpen, children: /* @__PURE__ */ jsxRuntime.jsxs(uiCore.DialogContent, { className: "max-w-[95vw] max-h-[95vh] w-[95vw] h-[95vh] p-0 overflow-hidden [&>button]:hidden flex flex-col", children: [
|
|
551
|
-
/* @__PURE__ */ jsxRuntime.jsx(uiCore.DialogTitle, { className: "sr-only", children: file.name }),
|
|
552
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-between px-4 py-2 border-b shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium truncate", children: file.name }) }),
|
|
553
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsxRuntime.jsx(ImageViewer, {
|
|
597
|
+
/* @__PURE__ */ jsxRuntime.jsx(uiCore.DialogTitle, { className: "sr-only", children: current.file.name }),
|
|
598
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-between px-4 py-2 border-b shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium truncate", children: current.file.name }) }),
|
|
599
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsxRuntime.jsx(ImageViewer, { images, initialIndex: currentIndex, inDialog: true }) })
|
|
554
600
|
] }) })
|
|
555
601
|
]
|
|
556
602
|
}
|
|
@@ -561,5 +607,5 @@ chunkWGEGR3DF_cjs.__name(ImageViewer, "ImageViewer");
|
|
|
561
607
|
exports.ImageInfo = ImageInfo;
|
|
562
608
|
exports.ImageToolbar = ImageToolbar;
|
|
563
609
|
exports.ImageViewer = ImageViewer;
|
|
564
|
-
//# sourceMappingURL=chunk-
|
|
565
|
-
//# sourceMappingURL=chunk-
|
|
610
|
+
//# sourceMappingURL=chunk-IPRNIM7L.cjs.map
|
|
611
|
+
//# sourceMappingURL=chunk-IPRNIM7L.cjs.map
|