@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.75",
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.75",
62
- "@djangocfg/ui-core": "^2.1.75",
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.75",
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">Failed to load image</p>
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
- const hasContent = size > 0;
60
- const useProgressiveLoading = size > PROGRESSIVE_LOADING_THRESHOLD;
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
  }