@djangocfg/ui-nextjs 2.1.75 → 2.1.77
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-nextjs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.77",
|
|
4
4
|
"description": "Next.js UI component library with Radix UI primitives, Tailwind CSS styling, charts, and form components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"check": "tsc --noEmit"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@djangocfg/api": "^2.1.
|
|
62
|
-
"@djangocfg/ui-core": "^2.1.
|
|
61
|
+
"@djangocfg/api": "^2.1.77",
|
|
62
|
+
"@djangocfg/ui-core": "^2.1.77",
|
|
63
63
|
"@types/react": "^19.1.0",
|
|
64
64
|
"@types/react-dom": "^19.1.0",
|
|
65
65
|
"consola": "^3.4.2",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"wavesurfer.js": "^7.12.1"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
113
|
+
"@djangocfg/typescript-config": "^2.1.77",
|
|
114
114
|
"@types/node": "^24.7.2",
|
|
115
115
|
"eslint": "^9.37.0",
|
|
116
116
|
"tailwindcss-animate": "1.0.7",
|
|
@@ -39,6 +39,7 @@ import { ImageViewer } from '@djangocfg/ui-nextjs';
|
|
|
39
39
|
|------|------|---------|-------------|
|
|
40
40
|
| `file` | `ImageFile` | required | File info (name, path, mimeType) |
|
|
41
41
|
| `content` | `string \| ArrayBuffer` | required | Image data |
|
|
42
|
+
| `src` | `string` | - | Direct URL (bypasses content→blob conversion) |
|
|
42
43
|
| `inDialog` | `boolean` | `false` | Hide expand button (for nested usage) |
|
|
43
44
|
|
|
44
45
|
## ImageFile Type
|
|
@@ -70,8 +71,33 @@ const buffer = await response.arrayBuffer();
|
|
|
70
71
|
|
|
71
72
|
// Base64 string (auto-converted to data URL)
|
|
72
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
|
+
/>
|
|
73
81
|
```
|
|
74
82
|
|
|
83
|
+
## HTTP Streaming
|
|
84
|
+
|
|
85
|
+
For large images, use the `src` prop to stream directly from a URL instead of loading the entire file into memory:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
// When src is provided, content is ignored
|
|
89
|
+
<ImageViewer
|
|
90
|
+
file={{ name: 'large-photo.jpg', path: '/photos/large.jpg' }}
|
|
91
|
+
content="" // Empty - not used when src is provided
|
|
92
|
+
src={streamingUrl}
|
|
93
|
+
/>
|
|
94
|
+
```
|
|
95
|
+
|
|
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
|
|
100
|
+
|
|
75
101
|
## Keyboard Shortcuts
|
|
76
102
|
|
|
77
103
|
| Key | Action |
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
16
16
|
import { ImageIcon, AlertCircle } from 'lucide-react';
|
|
17
17
|
import { TransformWrapper, TransformComponent, useControls } from 'react-zoom-pan-pinch';
|
|
18
|
-
import { cn, Dialog, DialogContent, Alert, AlertDescription } from '@djangocfg/ui-core';
|
|
18
|
+
import { cn, Dialog, DialogContent, DialogTitle, Alert, AlertDescription } from '@djangocfg/ui-core';
|
|
19
19
|
|
|
20
20
|
import { ImageToolbar } from './ImageToolbar';
|
|
21
21
|
import { ImageInfo } from './ImageInfo';
|
|
@@ -26,9 +26,10 @@ import type { ImageViewerProps } from '../types';
|
|
|
26
26
|
// COMPONENT
|
|
27
27
|
// =============================================================================
|
|
28
28
|
|
|
29
|
-
export function ImageViewer({ file, content, inDialog = false }: ImageViewerProps) {
|
|
29
|
+
export function ImageViewer({ file, content, src: directSrc, inDialog = false }: ImageViewerProps) {
|
|
30
30
|
const [scale, setScale] = useState(1);
|
|
31
31
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
32
|
+
const [loadError, setLoadError] = useState(false);
|
|
32
33
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
33
34
|
const controlsRef = useRef<ReturnType<typeof useControls> | null>(null);
|
|
34
35
|
|
|
@@ -40,7 +41,12 @@ export function ImageViewer({ file, content, inDialog = false }: ImageViewerProp
|
|
|
40
41
|
useProgressiveLoading,
|
|
41
42
|
error,
|
|
42
43
|
hasContent,
|
|
43
|
-
} = useImageLoading({ content, mimeType: file.mimeType });
|
|
44
|
+
} = useImageLoading({ content, mimeType: file.mimeType, src: directSrc });
|
|
45
|
+
|
|
46
|
+
// Reset load error when src changes
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
setLoadError(false);
|
|
49
|
+
}, [src]);
|
|
44
50
|
|
|
45
51
|
// Transform state
|
|
46
52
|
const { transform, rotate, flipH, flipV, transformStyle } = useImageTransform({
|
|
@@ -100,14 +106,14 @@ export function ImageViewer({ file, content, inDialog = false }: ImageViewerProp
|
|
|
100
106
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
101
107
|
}, [rotate]);
|
|
102
108
|
|
|
103
|
-
// Show error for oversized images
|
|
104
|
-
if (error) {
|
|
109
|
+
// Show error for oversized images or load errors
|
|
110
|
+
if (error || loadError) {
|
|
105
111
|
return (
|
|
106
112
|
<div className="flex-1 flex flex-col items-center justify-center gap-3 bg-muted/30 p-4">
|
|
107
113
|
<AlertCircle className="w-12 h-12 text-destructive/70" />
|
|
108
114
|
<Alert variant="destructive" className="max-w-md">
|
|
109
115
|
<AlertCircle className="h-4 w-4" />
|
|
110
|
-
<AlertDescription>{error}</AlertDescription>
|
|
116
|
+
<AlertDescription>{error || 'Failed to load image'}</AlertDescription>
|
|
111
117
|
</Alert>
|
|
112
118
|
</div>
|
|
113
119
|
);
|
|
@@ -118,7 +124,7 @@ export function ImageViewer({ file, content, inDialog = false }: ImageViewerProp
|
|
|
118
124
|
return (
|
|
119
125
|
<div className="flex-1 flex flex-col items-center justify-center gap-2 bg-muted/30">
|
|
120
126
|
<ImageIcon className="w-12 h-12 text-muted-foreground/50" />
|
|
121
|
-
<p className="text-sm text-muted-foreground">
|
|
127
|
+
<p className="text-sm text-muted-foreground">No image content</p>
|
|
122
128
|
</div>
|
|
123
129
|
);
|
|
124
130
|
}
|
|
@@ -150,6 +156,7 @@ export function ImageViewer({ file, content, inDialog = false }: ImageViewerProp
|
|
|
150
156
|
minScale={0.1}
|
|
151
157
|
maxScale={8}
|
|
152
158
|
centerOnInit
|
|
159
|
+
centerZoomedOut
|
|
153
160
|
onTransformed={(ref, state) => {
|
|
154
161
|
setScale(state.scale);
|
|
155
162
|
controlsRef.current = ref;
|
|
@@ -207,6 +214,8 @@ export function ImageViewer({ file, content, inDialog = false }: ImageViewerProp
|
|
|
207
214
|
opacity: useProgressiveLoading && !isFullyLoaded ? 0 : 1,
|
|
208
215
|
}}
|
|
209
216
|
draggable={false}
|
|
217
|
+
crossOrigin="anonymous"
|
|
218
|
+
onError={() => setLoadError(true)}
|
|
210
219
|
/>
|
|
211
220
|
)}
|
|
212
221
|
</div>
|
|
@@ -216,16 +225,13 @@ export function ImageViewer({ file, content, inDialog = false }: ImageViewerProp
|
|
|
216
225
|
{/* Fullscreen dialog */}
|
|
217
226
|
{!inDialog && (
|
|
218
227
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
219
|
-
<DialogContent className="max-w-[95vw] max-h-[95vh] w-[95vw] h-[95vh] p-0 overflow-hidden [&>button]:hidden">
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
<ImageViewer file={file} content={content} inDialog />
|
|
227
|
-
</div>
|
|
228
|
-
</div>
|
|
228
|
+
<DialogContent className="max-w-[95vw] max-h-[95vh] w-[95vw] h-[95vh] p-0 overflow-hidden [&>button]:hidden flex flex-col">
|
|
229
|
+
<DialogTitle className="sr-only">{file.name}</DialogTitle>
|
|
230
|
+
<div className="flex items-center justify-between px-4 py-2 border-b shrink-0">
|
|
231
|
+
<span className="text-sm font-medium truncate">{file.name}</span>
|
|
232
|
+
</div>
|
|
233
|
+
<div className="flex-1 min-h-0">
|
|
234
|
+
<ImageViewer file={file} content={content} src={directSrc} inDialog />
|
|
229
235
|
</div>
|
|
230
236
|
</DialogContent>
|
|
231
237
|
</Dialog>
|
|
@@ -17,6 +17,11 @@ export interface UseImageLoadingOptions {
|
|
|
17
17
|
content: string | ArrayBuffer;
|
|
18
18
|
/** MIME type for blob creation */
|
|
19
19
|
mimeType?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Direct image URL (bypasses content→blob conversion).
|
|
22
|
+
* When provided, content is ignored and URL is used directly.
|
|
23
|
+
*/
|
|
24
|
+
src?: string;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export interface UseImageLoadingReturn {
|
|
@@ -43,7 +48,7 @@ export interface UseImageLoadingReturn {
|
|
|
43
48
|
// =============================================================================
|
|
44
49
|
|
|
45
50
|
export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadingReturn {
|
|
46
|
-
const { content, mimeType } = options;
|
|
51
|
+
const { content, mimeType, src: directSrc } = options;
|
|
47
52
|
|
|
48
53
|
const { getOrCreateBlobUrl, releaseBlobUrl } = useImageCache();
|
|
49
54
|
|
|
@@ -56,14 +61,22 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
|
|
|
56
61
|
|
|
57
62
|
// Calculate size and flags
|
|
58
63
|
const size = content ? (typeof content === 'string' ? content.length : content.byteLength) : 0;
|
|
59
|
-
|
|
60
|
-
const
|
|
64
|
+
// When directSrc is provided, we have content (the URL itself)
|
|
65
|
+
const hasContent = directSrc ? true : size > 0;
|
|
66
|
+
const useProgressiveLoading = directSrc ? false : size > PROGRESSIVE_LOADING_THRESHOLD;
|
|
61
67
|
|
|
62
68
|
// Create blob URL with caching and size validation
|
|
63
69
|
useEffect(() => {
|
|
64
70
|
// Reset error state
|
|
65
71
|
setError(null);
|
|
66
72
|
|
|
73
|
+
// Direct URL mode - use as-is without blob conversion
|
|
74
|
+
if (directSrc) {
|
|
75
|
+
setSrc(directSrc);
|
|
76
|
+
setIsFullyLoaded(true);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
67
80
|
if (!hasContent) {
|
|
68
81
|
setSrc(null);
|
|
69
82
|
return;
|
|
@@ -113,7 +126,7 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
|
|
|
113
126
|
contentKeyRef.current = null;
|
|
114
127
|
}
|
|
115
128
|
};
|
|
116
|
-
}, [content, mimeType, hasContent, size, getOrCreateBlobUrl, releaseBlobUrl]);
|
|
129
|
+
}, [content, mimeType, hasContent, size, directSrc, getOrCreateBlobUrl, releaseBlobUrl]);
|
|
117
130
|
|
|
118
131
|
// Create LQIP for progressive loading
|
|
119
132
|
useEffect(() => {
|
|
@@ -24,6 +24,12 @@ export interface ImageViewerProps {
|
|
|
24
24
|
file: ImageFile;
|
|
25
25
|
/** Image content as string (data URL or base64) or ArrayBuffer */
|
|
26
26
|
content: string | ArrayBuffer;
|
|
27
|
+
/**
|
|
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.
|
|
31
|
+
*/
|
|
32
|
+
src?: string;
|
|
27
33
|
/** Hide expand button when already in dialog */
|
|
28
34
|
inDialog?: boolean;
|
|
29
35
|
}
|