@djangocfg/ui-tools 2.1.207 → 2.1.209

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.
@@ -1,6 +1,6 @@
1
1
  # ImageViewer
2
2
 
3
- Image viewer with zoom, pan, rotate, and flip functionality.
3
+ Image viewer with zoom, pan, rotate, flip and gallery navigation.
4
4
 
5
5
  ## Features
6
6
 
@@ -10,26 +10,44 @@ Image viewer with zoom, pan, rotate, and flip functionality.
10
10
  - Rotate 90°
11
11
  - Flip horizontal/vertical
12
12
  - Fullscreen dialog mode
13
- - Keyboard shortcuts
13
+ - **Gallery mode** — multiple images with prev/next buttons
14
+ - **Keyboard shortcuts** — `+/-/0/r` for zoom/rotate, `←/→` for gallery navigation
14
15
  - Checkerboard background for transparency
15
16
  - Image dimensions display
16
17
 
17
18
  ## Installation
18
19
 
19
20
  ```tsx
20
- import { ImageViewer } from '@djangocfg/ui-nextjs';
21
+ import { ImageViewer } from '@djangocfg/ui-tools';
21
22
  ```
22
23
 
23
24
  ## Basic Usage
24
25
 
25
26
  ```tsx
27
+ // Single image — URL
26
28
  <ImageViewer
27
- file={{
28
- name: 'photo.jpg',
29
- path: '/images/photo.jpg',
30
- mimeType: 'image/jpeg',
31
- }}
32
- content={imageArrayBuffer}
29
+ images={[{
30
+ file: { name: 'photo.jpg', path: '/images/photo.jpg' },
31
+ src: 'https://example.com/photo.jpg',
32
+ }]}
33
+ />
34
+
35
+ // Single image — raw bytes / base64
36
+ <ImageViewer
37
+ images={[{
38
+ file: { name: 'photo.png', path: 'unique-key', mimeType: 'image/png' },
39
+ content: arrayBuffer, // ArrayBuffer or base64 string
40
+ }]}
41
+ />
42
+
43
+ // Gallery
44
+ <ImageViewer
45
+ images={[
46
+ { file: { name: 'Photo 1', path: 'p1' }, src: 'https://example.com/1.jpg' },
47
+ { file: { name: 'Photo 2', path: 'p2' }, src: 'https://example.com/2.jpg' },
48
+ { file: { name: 'Photo 3', path: 'p3' }, src: 'https://example.com/3.jpg' },
49
+ ]}
50
+ initialIndex={0}
33
51
  />
34
52
  ```
35
53
 
@@ -37,66 +55,51 @@ import { ImageViewer } from '@djangocfg/ui-nextjs';
37
55
 
38
56
  | Prop | Type | Default | Description |
39
57
  |------|------|---------|-------------|
40
- | `file` | `ImageFile` | required | File info (name, path, mimeType) |
41
- | `content` | `string \| ArrayBuffer` | required | Image data |
42
- | `src` | `string` | - | Direct URL (bypasses content→blob conversion) |
58
+ | `images` | `ImageItem[]` | required | Array of images to display |
59
+ | `initialIndex` | `number` | `0` | Index of the image to show first |
43
60
  | `inDialog` | `boolean` | `false` | Hide expand button (for nested usage) |
44
61
 
45
- ## ImageFile Type
62
+ ## Types
46
63
 
47
64
  ```typescript
65
+ interface ImageItem {
66
+ file: ImageFile;
67
+ src?: string; // URL (CDN / server)
68
+ content?: string | ArrayBuffer; // Raw bytes or base64 (memory / blob)
69
+ }
70
+
48
71
  interface ImageFile {
49
72
  name: string; // Display name
50
- path: string; // File path (for state tracking)
73
+ path: string; // File path (for state tracking / cache key)
51
74
  mimeType?: string; // MIME type (e.g., 'image/png')
52
75
  }
53
76
  ```
54
77
 
55
- ## Content Formats
78
+ Both `src` and `content` are optional — supply whichever you have. When both are provided, `src` takes precedence (passed directly to `useImageLoading` as the direct URL).
56
79
 
57
- The `content` prop accepts:
80
+ ### content support
58
81
 
59
- - **ArrayBuffer**: Binary image data (creates blob URL)
60
- - **Data URL**: Base64 encoded string starting with `data:`
61
- - **Base64 string**: Raw base64 (auto-prefixed with data URL)
62
-
63
- ```tsx
64
- // ArrayBuffer (from fetch or file read)
65
- const response = await fetch('/image.png');
66
- const buffer = await response.arrayBuffer();
67
- <ImageViewer file={file} content={buffer} />
68
-
69
- // Data URL
70
- <ImageViewer file={file} content="data:image/png;base64,iVBORw0KGgo..." />
71
-
72
- // Base64 string (auto-converted to data URL)
73
- <ImageViewer file={file} content="iVBORw0KGgo..." />
74
-
75
- // Direct URL (HTTP streaming for large files)
76
- <ImageViewer
77
- file={file}
78
- content=""
79
- src="https://api.example.com/images/large-photo.jpg"
80
- />
81
- ```
82
+ | Value | Behaviour |
83
+ |-------|-----------|
84
+ | `data:image/...` string | used directly as `<img src>` |
85
+ | binary string | encoded → Blob URL via `TextEncoder` |
86
+ | `ArrayBuffer` | converted to Blob URL with caching |
82
87
 
83
- ## HTTP Streaming
88
+ ## Gallery Navigation
84
89
 
85
- For large images, use the `src` prop to stream directly from a URL instead of loading the entire file into memory:
90
+ When `images` has more than one item, prev/next buttons appear and keyboard navigation is enabled:
86
91
 
87
92
  ```tsx
88
- // When src is provided, content is ignored
89
93
  <ImageViewer
90
- file={{ name: 'large-photo.jpg', path: '/photos/large.jpg' }}
91
- content="" // Empty - not used when src is provided
92
- src={streamingUrl}
94
+ images={photos}
95
+ initialIndex={2} // open at 3rd photo
93
96
  />
94
97
  ```
95
98
 
96
- This is useful for:
97
- - Large images that shouldn't be loaded into memory
98
- - HTTP Range request streaming
99
- - Pre-signed URLs with authentication tokens
99
+ | Key | Action |
100
+ |-----|--------|
101
+ | `←` | Previous image |
102
+ | `→` | Next image |
100
103
 
101
104
  ## Keyboard Shortcuts
102
105
 
@@ -106,10 +109,12 @@ This is useful for:
106
109
  | `-` | Zoom out |
107
110
  | `0` | Reset to fit |
108
111
  | `R` | Rotate 90° |
112
+ | `←` | Previous image (gallery) |
113
+ | `→` | Next image (gallery) |
109
114
 
110
115
  ## Toolbar Controls
111
116
 
112
- The floating toolbar at the bottom provides:
117
+ The floating toolbar provides:
113
118
 
114
119
  - **Zoom out** button
115
120
  - **Zoom level** dropdown with presets
@@ -122,63 +127,38 @@ The floating toolbar at the bottom provides:
122
127
 
123
128
  ## Fullscreen Mode
124
129
 
125
- Click the expand button to open the image in a fullscreen dialog. The dialog includes the same toolbar and supports all interactions.
130
+ Click the expand button to open in a fullscreen dialog. Gallery navigation works inside the dialog too.
126
131
 
127
132
  ```tsx
128
- // Fullscreen is automatically available unless inDialog is true
129
- <ImageViewer file={file} content={content} />
133
+ // Fullscreen available by default
134
+ <ImageViewer images={images} />
130
135
 
131
- // When embedding in your own dialog, disable the expand button
136
+ // Disable expand button when embedding in your own dialog
132
137
  <Dialog>
133
- <ImageViewer file={file} content={content} inDialog />
138
+ <ImageViewer images={images} inDialog />
134
139
  </Dialog>
135
140
  ```
136
141
 
137
142
  ## Styling
138
143
 
139
- The component fills its container and displays a checkerboard pattern behind transparent images.
144
+ The component fills its container. Wrap in a sized element:
140
145
 
141
146
  ```tsx
142
147
  <div className="w-full h-[500px]">
143
- <ImageViewer file={file} content={content} />
148
+ <ImageViewer images={images} />
144
149
  </div>
145
150
  ```
146
151
 
147
- ## Error State
148
-
149
- When content is empty or invalid, displays an error placeholder:
150
-
151
- ```tsx
152
- // Shows "Failed to load image" with icon
153
- <ImageViewer file={file} content="" />
154
- ```
155
-
156
- ## Example: File Browser Integration
157
-
158
- ```tsx
159
- function FilePreview({ file, content }: { file: OpenFile; content: ArrayBuffer }) {
160
- const imageFile: ImageFile = {
161
- name: file.name,
162
- path: file.path,
163
- mimeType: file.mimeType,
164
- };
165
-
166
- return (
167
- <div className="h-full">
168
- <ImageViewer file={imageFile} content={content} />
169
- </div>
170
- );
171
- }
172
- ```
173
-
174
152
  ## Architecture
175
153
 
176
154
  ```
177
155
  ImageViewer/
178
156
  ├── index.ts # Public API exports
179
157
  ├── types.ts # Type definitions
158
+ ├── ImageViewer.story.tsx # Playground stories
159
+ ├── README.md
180
160
  ├── components/
181
- │ ├── index.ts # Component exports
161
+ │ ├── index.ts
182
162
  │ ├── ImageViewer.tsx # Main viewer component
183
163
  │ ├── ImageToolbar.tsx # Zoom/rotate/flip controls
184
164
  │ └── ImageInfo.tsx # Dimensions display
@@ -186,15 +166,15 @@ ImageViewer/
186
166
  │ ├── index.ts
187
167
  │ ├── useImageLoading.ts # Blob URL & LQIP management
188
168
  │ └── useImageTransform.ts # Rotation/flip state
189
- ├── utils/
190
- ├── index.ts
191
- ├── constants.ts # Size limits, zoom presets
192
- └── lqip.ts # Low-Quality Image Placeholder
193
- └── README.md
169
+ └── utils/
170
+ ├── index.ts
171
+ ├── constants.ts # Size limits, zoom presets
172
+ └── lqip.ts # Low-Quality Image Placeholder
194
173
  ```
195
174
 
196
175
  ## Dependencies
197
176
 
198
- - `react-zoom-pan-pinch` - Zoom and pan functionality
199
- - `lucide-react` - Icons
200
- - `@djangocfg/ui-core` - UI components (Button, Dialog, etc.)
177
+ - `react-zoom-pan-pinch` zoom and pan
178
+ - `lucide-react` icons
179
+ - `@djangocfg/ui-core` UI components (Button, Dialog, etc.)
180
+ - `@djangocfg/ui-core/hooks` — `useHotkey` for keyboard navigation
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  /**
4
- * ImageViewer - Image viewer with zoom, pan, rotate, flip
4
+ * ImageViewer - Image viewer with zoom, pan, rotate, flip, gallery navigation
5
5
  *
6
6
  * Features:
7
7
  * - Zoom with mouse wheel and presets
@@ -9,26 +9,45 @@
9
9
  * - Rotate 90°
10
10
  * - Flip horizontal/vertical
11
11
  * - Fullscreen dialog
12
- * - Keyboard shortcuts (+/-, 0, r)
12
+ * - Keyboard shortcuts (+/-, 0, r, ←/→ for gallery)
13
+ * - Gallery mode: pass images[] with multiple items
13
14
  */
14
15
 
15
16
  import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
16
- import { ImageIcon, AlertCircle } from 'lucide-react';
17
+ import { ImageIcon, AlertCircle, ChevronLeft, ChevronRight } from 'lucide-react';
17
18
  import { TransformWrapper, TransformComponent, useControls } from 'react-zoom-pan-pinch';
18
19
  import { cn, Dialog, DialogContent, DialogTitle, Alert, AlertDescription } from '@djangocfg/ui-core';
19
20
  import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
21
+ import { useHotkey } from '@djangocfg/ui-core/hooks';
20
22
 
21
23
  import { ImageToolbar } from './ImageToolbar';
22
24
  import { ImageInfo } from './ImageInfo';
23
25
  import { useImageTransform, useImageLoading } from '../hooks';
24
- import type { ImageViewerProps } from '../types';
26
+ import type { ImageViewerProps, ImageItem } from '../types';
25
27
 
26
28
  // =============================================================================
27
29
  // COMPONENT
28
30
  // =============================================================================
29
31
 
30
- export function ImageViewer({ file, content, src: directSrc, inDialog = false }: ImageViewerProps) {
32
+ export function ImageViewer({
33
+ images,
34
+ initialIndex = 0,
35
+ inDialog = false,
36
+ }: ImageViewerProps) {
31
37
  const t = useTypedT<I18nTranslations>();
38
+
39
+ const [currentIndex, setCurrentIndex] = useState(() =>
40
+ Math.max(0, Math.min(initialIndex, images.length - 1))
41
+ );
42
+
43
+ // Reset index when initialIndex changes (e.g. opening different image)
44
+ useEffect(() => {
45
+ setCurrentIndex(Math.max(0, Math.min(initialIndex, images.length - 1)));
46
+ }, [initialIndex, images.length]);
47
+
48
+ const current = images[currentIndex];
49
+ const hasMultiple = images.length > 1;
50
+
32
51
  const [scale, setScale] = useState(1);
33
52
  const [dialogOpen, setDialogOpen] = useState(false);
34
53
  const [loadError, setLoadError] = useState(false);
@@ -41,7 +60,6 @@ export function ImageViewer({ file, content, src: directSrc, inDialog = false }:
41
60
  loading: t('ui.form.loading'),
42
61
  }), [t]);
43
62
 
44
- // Loading state
45
63
  const {
46
64
  src,
47
65
  lqip,
@@ -49,72 +67,67 @@ export function ImageViewer({ file, content, src: directSrc, inDialog = false }:
49
67
  useProgressiveLoading,
50
68
  error,
51
69
  hasContent,
52
- } = useImageLoading({ content, mimeType: file.mimeType, src: directSrc });
70
+ } = useImageLoading({
71
+ content: current?.content ?? '',
72
+ mimeType: current?.file.mimeType,
73
+ src: current?.src,
74
+ });
53
75
 
54
- // Reset load error when src changes
55
- useEffect(() => {
56
- setLoadError(false);
57
- }, [src]);
76
+ useEffect(() => { setLoadError(false); }, [src]);
58
77
 
59
- // Transform state
60
78
  const { transform, rotate, flipH, flipV, transformStyle } = useImageTransform({
61
- resetKey: file.path,
79
+ resetKey: current?.file.path ?? '',
62
80
  });
63
81
 
64
- // Zoom preset handler
65
82
  const handleZoomPreset = useCallback((value: number | 'fit') => {
66
83
  if (!controlsRef.current) return;
67
- if (value === 'fit') {
68
- controlsRef.current.resetTransform();
69
- } else {
70
- controlsRef.current.setTransform(0, 0, value);
71
- }
84
+ if (value === 'fit') controlsRef.current.resetTransform();
85
+ else controlsRef.current.setTransform(0, 0, value);
72
86
  }, []);
73
87
 
74
- // Expand to fullscreen
75
- const handleExpand = useCallback(() => {
76
- setDialogOpen(true);
77
- }, []);
88
+ const handleExpand = useCallback(() => setDialogOpen(true), []);
78
89
 
79
- // Keyboard shortcuts
90
+ const prev = useCallback(() =>
91
+ setCurrentIndex((i) => (i - 1 + images.length) % images.length),
92
+ [images.length]
93
+ );
94
+
95
+ const next = useCallback(() =>
96
+ setCurrentIndex((i) => (i + 1) % images.length),
97
+ [images.length]
98
+ );
99
+
100
+ // Keyboard: zoom/rotate (only when container focused)
80
101
  useEffect(() => {
81
102
  const handleKeyDown = (e: KeyboardEvent) => {
82
103
  if (!containerRef.current?.contains(document.activeElement) &&
83
- document.activeElement !== containerRef.current) {
84
- return;
85
- }
86
-
104
+ document.activeElement !== containerRef.current) return;
87
105
  const controls = controlsRef.current;
88
106
  if (!controls) return;
89
-
90
107
  switch (e.key) {
91
- case '+':
92
- case '=':
93
- e.preventDefault();
94
- controls.zoomIn();
95
- break;
96
- case '-':
97
- e.preventDefault();
98
- controls.zoomOut();
99
- break;
100
- case '0':
101
- e.preventDefault();
102
- controls.resetTransform();
103
- break;
104
- case 'r':
105
- if (!e.metaKey && !e.ctrlKey) {
106
- e.preventDefault();
107
- rotate();
108
- }
109
- break;
108
+ case '+': case '=': e.preventDefault(); controls.zoomIn(); break;
109
+ case '-': e.preventDefault(); controls.zoomOut(); break;
110
+ case '0': e.preventDefault(); controls.resetTransform(); break;
111
+ case 'r': if (!e.metaKey && !e.ctrlKey) { e.preventDefault(); rotate(); } break;
110
112
  }
111
113
  };
112
-
113
114
  window.addEventListener('keydown', handleKeyDown);
114
115
  return () => window.removeEventListener('keydown', handleKeyDown);
115
116
  }, [rotate]);
116
117
 
117
- // Show error for oversized images or load errors
118
+ // Keyboard: gallery navigation (global when open)
119
+ useHotkey('ArrowLeft', prev, { enabled: hasMultiple, preventDefault: true });
120
+ useHotkey('ArrowRight', next, { enabled: hasMultiple, preventDefault: true });
121
+
122
+ if (!current) {
123
+ return (
124
+ <div className="flex-1 flex flex-col items-center justify-center gap-2 bg-muted/30">
125
+ <ImageIcon className="w-12 h-12 text-muted-foreground/50" />
126
+ <p className="text-sm text-muted-foreground">{labels.noImage}</p>
127
+ </div>
128
+ );
129
+ }
130
+
118
131
  if (error || loadError) {
119
132
  return (
120
133
  <div className="flex-1 flex flex-col items-center justify-center gap-3 bg-muted/30 p-4">
@@ -127,7 +140,6 @@ export function ImageViewer({ file, content, src: directSrc, inDialog = false }:
127
140
  );
128
141
  }
129
142
 
130
- // No content
131
143
  if (!hasContent) {
132
144
  return (
133
145
  <div className="flex-1 flex flex-col items-center justify-center gap-2 bg-muted/30">
@@ -151,7 +163,6 @@ export function ImageViewer({ file, content, src: directSrc, inDialog = false }:
151
163
  >
152
164
  {src && <ImageInfo src={src} />}
153
165
 
154
- {/* Progressive loading indicator */}
155
166
  {useProgressiveLoading && !isFullyLoaded && (
156
167
  <div className="absolute top-3 left-3 z-10 px-2 py-1 bg-background/80 backdrop-blur-sm border rounded text-[10px] text-muted-foreground font-mono flex items-center gap-1.5">
157
168
  <div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
@@ -165,13 +176,8 @@ export function ImageViewer({ file, content, src: directSrc, inDialog = false }:
165
176
  maxScale={8}
166
177
  centerOnInit
167
178
  centerZoomedOut
168
- onTransformed={(ref, state) => {
169
- setScale(state.scale);
170
- controlsRef.current = ref;
171
- }}
172
- onInit={(ref) => {
173
- controlsRef.current = ref;
174
- }}
179
+ onTransformed={(ref, state) => { setScale(state.scale); controlsRef.current = ref; }}
180
+ onInit={(ref) => { controlsRef.current = ref; }}
175
181
  wheel={{ step: 0.1 }}
176
182
  doubleClick={{ mode: 'toggle', step: 2 }}
177
183
  panning={{ velocityDisabled: false }}
@@ -191,34 +197,24 @@ export function ImageViewer({ file, content, src: directSrc, inDialog = false }:
191
197
  contentClass="!w-full !h-full flex items-center justify-center"
192
198
  >
193
199
  <div className="relative">
194
- {/* LQIP Placeholder (blurred, shown while loading) */}
195
200
  {useProgressiveLoading && lqip && !isFullyLoaded && (
196
201
  <img
197
202
  src={lqip}
198
203
  alt=""
199
204
  aria-hidden="true"
200
205
  className="absolute inset-0 max-w-full max-h-full object-contain select-none"
201
- style={{
202
- transform: transformStyle,
203
- filter: 'blur(20px)',
204
- transition: 'opacity 0.3s ease-out',
205
- opacity: isFullyLoaded ? 0 : 1,
206
- }}
206
+ style={{ transform: transformStyle, filter: 'blur(20px)', transition: 'opacity 0.3s ease-out', opacity: isFullyLoaded ? 0 : 1 }}
207
207
  draggable={false}
208
208
  />
209
209
  )}
210
-
211
- {/* Full Image */}
212
210
  {src && (
213
211
  <img
214
212
  src={src}
215
- alt={file.name}
213
+ alt={current.file.name}
216
214
  className="max-w-full max-h-full object-contain select-none"
217
215
  style={{
218
216
  transform: transformStyle,
219
- transition: useProgressiveLoading
220
- ? 'transform 0.15s ease-out, opacity 0.3s ease-out'
221
- : 'transform 0.15s ease-out',
217
+ transition: useProgressiveLoading ? 'transform 0.15s ease-out, opacity 0.3s ease-out' : 'transform 0.15s ease-out',
222
218
  opacity: useProgressiveLoading && !isFullyLoaded ? 0 : 1,
223
219
  }}
224
220
  draggable={false}
@@ -230,16 +226,39 @@ export function ImageViewer({ file, content, src: directSrc, inDialog = false }:
230
226
  </TransformComponent>
231
227
  </TransformWrapper>
232
228
 
229
+ {/* Gallery navigation */}
230
+ {hasMultiple && (
231
+ <>
232
+ <button
233
+ type="button"
234
+ onClick={prev}
235
+ 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"
236
+ >
237
+ <ChevronLeft className="h-5 w-5" />
238
+ </button>
239
+ <button
240
+ type="button"
241
+ onClick={next}
242
+ 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"
243
+ >
244
+ <ChevronRight className="h-5 w-5" />
245
+ </button>
246
+ <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">
247
+ {currentIndex + 1} / {images.length}
248
+ </div>
249
+ </>
250
+ )}
251
+
233
252
  {/* Fullscreen dialog */}
234
253
  {!inDialog && (
235
254
  <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
236
255
  <DialogContent className="max-w-[95vw] max-h-[95vh] w-[95vw] h-[95vh] p-0 overflow-hidden [&>button]:hidden flex flex-col">
237
- <DialogTitle className="sr-only">{file.name}</DialogTitle>
256
+ <DialogTitle className="sr-only">{current.file.name}</DialogTitle>
238
257
  <div className="flex items-center justify-between px-4 py-2 border-b shrink-0">
239
- <span className="text-sm font-medium truncate">{file.name}</span>
258
+ <span className="text-sm font-medium truncate">{current.file.name}</span>
240
259
  </div>
241
260
  <div className="flex-1 min-h-0">
242
- <ImageViewer file={file} content={content} src={directSrc} inDialog />
261
+ <ImageViewer images={images} initialIndex={currentIndex} inDialog />
243
262
  </div>
244
263
  </DialogContent>
245
264
  </Dialog>
@@ -15,21 +15,32 @@ export interface ImageFile {
15
15
  mimeType?: string;
16
16
  }
17
17
 
18
+ export interface ImageItem {
19
+ /** File info for this image */
20
+ file: ImageFile;
21
+ /** Direct image URL (use when serving from CDN/server) */
22
+ src?: string;
23
+ /** Raw image content — ArrayBuffer or base64/binary string (use when serving from memory) */
24
+ content?: string | ArrayBuffer;
25
+ }
26
+
18
27
  // =============================================================================
19
28
  // COMPONENT PROPS
20
29
  // =============================================================================
21
30
 
22
31
  export interface ImageViewerProps {
23
- /** File info (name, path, mimeType) */
24
- file: ImageFile;
25
- /** Image content as string (data URL or base64) or ArrayBuffer */
26
- content: string | ArrayBuffer;
27
32
  /**
28
- * Direct image URL for HTTP streaming.
29
- * When provided, bypasses content→blob conversion and uses URL directly.
30
- * Useful for large files loaded via HTTP Range requests.
33
+ * Gallery images array.
34
+ * When provided, enables gallery mode with prev/next navigation.
35
+ * Single-image mode: pass one item.
36
+ * Each item can have `src` (URL) or `content` (ArrayBuffer/base64) or both.
31
37
  */
32
- src?: string;
38
+ images: ImageItem[];
39
+ /**
40
+ * Initial index to show (default: 0).
41
+ * Useful when opening a specific image from a grid.
42
+ */
43
+ initialIndex?: number;
33
44
  /** Hide expand button when already in dialog */
34
45
  inDialog?: boolean;
35
46
  }