@djangocfg/ui-nextjs 2.1.75 → 2.1.76
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.76",
|
|
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.76",
|
|
62
|
+
"@djangocfg/ui-core": "^2.1.76",
|
|
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.76",
|
|
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 |
|
|
@@ -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
|
}
|
|
@@ -207,6 +213,8 @@ export function ImageViewer({ file, content, inDialog = false }: ImageViewerProp
|
|
|
207
213
|
opacity: useProgressiveLoading && !isFullyLoaded ? 0 : 1,
|
|
208
214
|
}}
|
|
209
215
|
draggable={false}
|
|
216
|
+
crossOrigin="anonymous"
|
|
217
|
+
onError={() => setLoadError(true)}
|
|
210
218
|
/>
|
|
211
219
|
)}
|
|
212
220
|
</div>
|
|
@@ -223,7 +231,7 @@ export function ImageViewer({ file, content, inDialog = false }: ImageViewerProp
|
|
|
223
231
|
</div>
|
|
224
232
|
<div className="flex-1 min-h-0 relative">
|
|
225
233
|
<div className="absolute inset-0">
|
|
226
|
-
<ImageViewer file={file} content={content} inDialog />
|
|
234
|
+
<ImageViewer file={file} content={content} src={directSrc} inDialog />
|
|
227
235
|
</div>
|
|
228
236
|
</div>
|
|
229
237
|
</div>
|
|
@@ -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
|
}
|