@djangocfg/ui-tools 2.1.91
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/dist/LottiePlayer.client-LBEC2JKY.mjs +161 -0
- package/dist/LottiePlayer.client-LBEC2JKY.mjs.map +1 -0
- package/dist/LottiePlayer.client-WFMG2OOW.cjs +168 -0
- package/dist/LottiePlayer.client-WFMG2OOW.cjs.map +1 -0
- package/dist/Mermaid.client-4TU2TSH3.mjs +477 -0
- package/dist/Mermaid.client-4TU2TSH3.mjs.map +1 -0
- package/dist/Mermaid.client-SBYY364Q.cjs +483 -0
- package/dist/Mermaid.client-SBYY364Q.cjs.map +1 -0
- package/dist/PlaygroundLayout-3YVSAEAF.cjs +1003 -0
- package/dist/PlaygroundLayout-3YVSAEAF.cjs.map +1 -0
- package/dist/PlaygroundLayout-4DYBORAS.mjs +996 -0
- package/dist/PlaygroundLayout-4DYBORAS.mjs.map +1 -0
- package/dist/PrettyCode.client-LCBPPTIX.mjs +152 -0
- package/dist/PrettyCode.client-LCBPPTIX.mjs.map +1 -0
- package/dist/PrettyCode.client-PNPLXRH6.cjs +154 -0
- package/dist/PrettyCode.client-PNPLXRH6.cjs.map +1 -0
- package/dist/chunk-37ZI6VD4.mjs +12 -0
- package/dist/chunk-37ZI6VD4.mjs.map +1 -0
- package/dist/chunk-3HK2OE62.cjs +81 -0
- package/dist/chunk-3HK2OE62.cjs.map +1 -0
- package/dist/chunk-7DGDQVQW.cjs +591 -0
- package/dist/chunk-7DGDQVQW.cjs.map +1 -0
- package/dist/chunk-M6P2FU7L.mjs +572 -0
- package/dist/chunk-M6P2FU7L.mjs.map +1 -0
- package/dist/chunk-UQ3XI5MY.cjs +15 -0
- package/dist/chunk-UQ3XI5MY.cjs.map +1 -0
- package/dist/chunk-YFRNE2IR.mjs +79 -0
- package/dist/chunk-YFRNE2IR.mjs.map +1 -0
- package/dist/index.cjs +5042 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1591 -0
- package/dist/index.d.ts +1591 -0
- package/dist/index.mjs +4941 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +86 -0
- package/src/components/markdown/MarkdownMessage.tsx +340 -0
- package/src/components/markdown/index.ts +5 -0
- package/src/index.ts +26 -0
- package/src/stores/index.ts +9 -0
- package/src/stores/mediaCache.ts +534 -0
- package/src/tools/AudioPlayer/README.md +206 -0
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +216 -0
- package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +280 -0
- package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +149 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
- package/src/tools/AudioPlayer/components/index.ts +22 -0
- package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +158 -0
- package/src/tools/AudioPlayer/context/index.ts +16 -0
- package/src/tools/AudioPlayer/effects/index.ts +412 -0
- package/src/tools/AudioPlayer/hooks/index.ts +35 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
- package/src/tools/AudioPlayer/hooks/useVisualization.tsx +207 -0
- package/src/tools/AudioPlayer/index.ts +133 -0
- package/src/tools/AudioPlayer/types/effects.ts +73 -0
- package/src/tools/AudioPlayer/types/index.ts +27 -0
- package/src/tools/AudioPlayer/utils/debug.ts +14 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
- package/src/tools/AudioPlayer/utils/index.ts +6 -0
- package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
- package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
- package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
- package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
- package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
- package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
- package/src/tools/ImageViewer/README.md +200 -0
- package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +145 -0
- package/src/tools/ImageViewer/components/ImageViewer.tsx +241 -0
- package/src/tools/ImageViewer/components/index.ts +7 -0
- package/src/tools/ImageViewer/hooks/index.ts +9 -0
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +204 -0
- package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
- package/src/tools/ImageViewer/index.ts +60 -0
- package/src/tools/ImageViewer/types.ts +81 -0
- package/src/tools/ImageViewer/utils/constants.ts +59 -0
- package/src/tools/ImageViewer/utils/debug.ts +14 -0
- package/src/tools/ImageViewer/utils/index.ts +17 -0
- package/src/tools/ImageViewer/utils/lqip.ts +47 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +197 -0
- package/src/tools/JsonForm/examples/BotConfigExample.tsx +249 -0
- package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +161 -0
- package/src/tools/JsonForm/index.ts +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +47 -0
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +74 -0
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +107 -0
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +35 -0
- package/src/tools/JsonForm/templates/FieldTemplate.tsx +62 -0
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +116 -0
- package/src/tools/JsonForm/templates/index.ts +12 -0
- package/src/tools/JsonForm/types.ts +83 -0
- package/src/tools/JsonForm/utils.ts +213 -0
- package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +37 -0
- package/src/tools/JsonForm/widgets/ColorWidget.tsx +219 -0
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +89 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +97 -0
- package/src/tools/JsonForm/widgets/SliderWidget.tsx +148 -0
- package/src/tools/JsonForm/widgets/SwitchWidget.tsx +35 -0
- package/src/tools/JsonForm/widgets/TextWidget.tsx +96 -0
- package/src/tools/JsonForm/widgets/index.ts +14 -0
- package/src/tools/JsonTree/index.tsx +243 -0
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +213 -0
- package/src/tools/LottiePlayer/index.tsx +56 -0
- package/src/tools/LottiePlayer/types.ts +108 -0
- package/src/tools/LottiePlayer/useLottie.ts +164 -0
- package/src/tools/Mermaid/Mermaid.client.tsx +82 -0
- package/src/tools/Mermaid/components/MermaidCodeViewer.tsx +95 -0
- package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +103 -0
- package/src/tools/Mermaid/hooks/index.ts +4 -0
- package/src/tools/Mermaid/hooks/useMermaidCleanup.ts +73 -0
- package/src/tools/Mermaid/hooks/useMermaidFullscreen.ts +46 -0
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +226 -0
- package/src/tools/Mermaid/hooks/useMermaidValidation.ts +29 -0
- package/src/tools/Mermaid/index.tsx +44 -0
- package/src/tools/Mermaid/utils/mermaid-helpers.ts +33 -0
- package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +149 -0
- package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +263 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +125 -0
- package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +100 -0
- package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +157 -0
- package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
- package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +173 -0
- package/src/tools/OpenapiViewer/components/VersionSelector.tsx +68 -0
- package/src/tools/OpenapiViewer/components/index.ts +14 -0
- package/src/tools/OpenapiViewer/constants.ts +39 -0
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +337 -0
- package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
- package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +199 -0
- package/src/tools/OpenapiViewer/index.tsx +37 -0
- package/src/tools/OpenapiViewer/types.ts +151 -0
- package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
- package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
- package/src/tools/OpenapiViewer/utils/index.ts +9 -0
- package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +208 -0
- package/src/tools/PrettyCode/index.tsx +47 -0
- package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
- package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
- package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
- package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
- package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
- package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
- package/src/tools/VideoPlayer/README.md +264 -0
- package/src/tools/VideoPlayer/components/VideoControls.tsx +138 -0
- package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +172 -0
- package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
- package/src/tools/VideoPlayer/components/index.ts +14 -0
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
- package/src/tools/VideoPlayer/context/index.ts +8 -0
- package/src/tools/VideoPlayer/hooks/index.ts +12 -0
- package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +70 -0
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +116 -0
- package/src/tools/VideoPlayer/index.ts +77 -0
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +284 -0
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +505 -0
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +400 -0
- package/src/tools/VideoPlayer/providers/index.ts +8 -0
- package/src/tools/VideoPlayer/types/index.ts +38 -0
- package/src/tools/VideoPlayer/types/player.ts +116 -0
- package/src/tools/VideoPlayer/types/provider.ts +93 -0
- package/src/tools/VideoPlayer/types/sources.ts +97 -0
- package/src/tools/VideoPlayer/utils/debug.ts +14 -0
- package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
- package/src/tools/VideoPlayer/utils/index.ts +12 -0
- package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
- package/src/tools/_shared.ts +29 -0
- package/src/tools/index.ts +172 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useImageLoading - Manages image loading state with LQIP
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useEffect, useRef } from 'react';
|
|
8
|
+
import { useMediaCacheStore, generateContentKey } from '../../../stores/mediaCache';
|
|
9
|
+
import { createLQIP, MAX_IMAGE_SIZE, WARNING_IMAGE_SIZE, PROGRESSIVE_LOADING_THRESHOLD, imageDebug } from '../utils';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// TYPES
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export interface UseImageLoadingOptions {
|
|
16
|
+
/** Image content (ArrayBuffer or string) */
|
|
17
|
+
content: string | ArrayBuffer;
|
|
18
|
+
/** MIME type for blob creation */
|
|
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;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UseImageLoadingReturn {
|
|
28
|
+
/** Blob URL source for the image */
|
|
29
|
+
src: string | null;
|
|
30
|
+
/** Low-quality placeholder URL */
|
|
31
|
+
lqip: string | null;
|
|
32
|
+
/** Whether full image is loaded */
|
|
33
|
+
isFullyLoaded: boolean;
|
|
34
|
+
/** Whether to use progressive loading */
|
|
35
|
+
useProgressiveLoading: boolean;
|
|
36
|
+
/** Error message if any */
|
|
37
|
+
error: string | null;
|
|
38
|
+
/** Content key for caching */
|
|
39
|
+
contentKey: string | null;
|
|
40
|
+
/** Image size in bytes */
|
|
41
|
+
size: number;
|
|
42
|
+
/** Whether content exists */
|
|
43
|
+
hasContent: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// HOOK
|
|
48
|
+
// =============================================================================
|
|
49
|
+
|
|
50
|
+
export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadingReturn {
|
|
51
|
+
const { content, mimeType, src: directSrc } = options;
|
|
52
|
+
|
|
53
|
+
// Get stable function references from store (not from hook to avoid re-renders)
|
|
54
|
+
const getOrCreateBlobUrl = useMediaCacheStore.getState().getOrCreateBlobUrl;
|
|
55
|
+
const releaseBlobUrl = useMediaCacheStore.getState().releaseBlobUrl;
|
|
56
|
+
|
|
57
|
+
const [src, setSrc] = useState<string | null>(null);
|
|
58
|
+
const [lqip, setLqip] = useState<string | null>(null);
|
|
59
|
+
const [isFullyLoaded, setIsFullyLoaded] = useState(false);
|
|
60
|
+
const [error, setError] = useState<string | null>(null);
|
|
61
|
+
|
|
62
|
+
const contentKeyRef = useRef<string | null>(null);
|
|
63
|
+
const isMountedRef = useRef(true);
|
|
64
|
+
|
|
65
|
+
// Calculate size and flags
|
|
66
|
+
const size = content ? (typeof content === 'string' ? content.length : content.byteLength) : 0;
|
|
67
|
+
// When directSrc is provided, we have content (the URL itself)
|
|
68
|
+
const hasContent = directSrc ? true : size > 0;
|
|
69
|
+
const useProgressiveLoading = directSrc ? false : size > PROGRESSIVE_LOADING_THRESHOLD;
|
|
70
|
+
|
|
71
|
+
// Track unmount for cleanup
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
isMountedRef.current = true;
|
|
74
|
+
return () => {
|
|
75
|
+
isMountedRef.current = false;
|
|
76
|
+
// Release blob URL only on actual unmount
|
|
77
|
+
if (contentKeyRef.current) {
|
|
78
|
+
useMediaCacheStore.getState().releaseBlobUrl(contentKeyRef.current);
|
|
79
|
+
contentKeyRef.current = null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
// Create blob URL with caching and size validation
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
// Reset error state
|
|
87
|
+
setError(null);
|
|
88
|
+
|
|
89
|
+
// Direct URL mode - use as-is without blob conversion
|
|
90
|
+
if (directSrc) {
|
|
91
|
+
imageDebug.load(directSrc, 'url');
|
|
92
|
+
setSrc(directSrc);
|
|
93
|
+
setIsFullyLoaded(true);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!hasContent) {
|
|
98
|
+
setSrc(null);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Size validation - reject oversized images
|
|
103
|
+
if (size > MAX_IMAGE_SIZE) {
|
|
104
|
+
const sizeMB = (size / 1024 / 1024).toFixed(1);
|
|
105
|
+
const errorMsg = `Image too large: ${sizeMB}MB (maximum: 50MB)`;
|
|
106
|
+
imageDebug.error(errorMsg, { size, sizeMB, maxSize: MAX_IMAGE_SIZE });
|
|
107
|
+
setError(errorMsg);
|
|
108
|
+
setSrc(null);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Warn about large images
|
|
113
|
+
if (size > WARNING_IMAGE_SIZE) {
|
|
114
|
+
const sizeMB = (size / 1024 / 1024).toFixed(1);
|
|
115
|
+
imageDebug.warn(`Large image: ${sizeMB}MB - may impact performance`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Handle string content (data URLs or binary strings)
|
|
119
|
+
if (typeof content === 'string') {
|
|
120
|
+
// Pass through data URLs directly
|
|
121
|
+
if (content.startsWith('data:')) {
|
|
122
|
+
imageDebug.load(content.slice(0, 50) + '...', 'data-url');
|
|
123
|
+
setSrc(content);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Convert binary string to ArrayBuffer and use Blob URL
|
|
128
|
+
const encoder = new TextEncoder();
|
|
129
|
+
const buffer = encoder.encode(content).buffer;
|
|
130
|
+
const contentKey = generateContentKey(buffer);
|
|
131
|
+
|
|
132
|
+
// Release previous blob URL if content changed
|
|
133
|
+
if (contentKeyRef.current && contentKeyRef.current !== contentKey) {
|
|
134
|
+
releaseBlobUrl(contentKeyRef.current);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
contentKeyRef.current = contentKey;
|
|
138
|
+
const url = getOrCreateBlobUrl(contentKey, buffer, mimeType || 'image/png');
|
|
139
|
+
imageDebug.load(url, 'blob');
|
|
140
|
+
imageDebug.state('loaded', { size, mimeType, contentKey });
|
|
141
|
+
setSrc(url);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle ArrayBuffer with cached blob URL
|
|
146
|
+
const contentKey = generateContentKey(content);
|
|
147
|
+
|
|
148
|
+
// Release previous blob URL if content changed
|
|
149
|
+
if (contentKeyRef.current && contentKeyRef.current !== contentKey) {
|
|
150
|
+
releaseBlobUrl(contentKeyRef.current);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
contentKeyRef.current = contentKey;
|
|
154
|
+
const url = getOrCreateBlobUrl(contentKey, content, mimeType || 'image/png');
|
|
155
|
+
imageDebug.load(url, 'blob');
|
|
156
|
+
imageDebug.state('loaded', { size, mimeType, contentKey });
|
|
157
|
+
setSrc(url);
|
|
158
|
+
|
|
159
|
+
// No cleanup here - cleanup happens in unmount effect above
|
|
160
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
161
|
+
}, [content, mimeType, hasContent, size, directSrc]);
|
|
162
|
+
|
|
163
|
+
// Create LQIP for progressive loading
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (!src || !useProgressiveLoading) {
|
|
166
|
+
setLqip(null);
|
|
167
|
+
setIsFullyLoaded(true);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setIsFullyLoaded(false);
|
|
172
|
+
imageDebug.state('progressive loading', { size });
|
|
173
|
+
|
|
174
|
+
// Create low-quality placeholder
|
|
175
|
+
createLQIP(src).then((placeholder) => {
|
|
176
|
+
if (placeholder) {
|
|
177
|
+
imageDebug.debug('LQIP created');
|
|
178
|
+
setLqip(placeholder);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Pre-load full image
|
|
183
|
+
const img = new Image();
|
|
184
|
+
img.onload = () => {
|
|
185
|
+
imageDebug.state('fully loaded');
|
|
186
|
+
setIsFullyLoaded(true);
|
|
187
|
+
};
|
|
188
|
+
img.onerror = () => {
|
|
189
|
+
imageDebug.error('Failed to load full image');
|
|
190
|
+
};
|
|
191
|
+
img.src = src;
|
|
192
|
+
}, [src, useProgressiveLoading, size]);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
src,
|
|
196
|
+
lqip,
|
|
197
|
+
isFullyLoaded,
|
|
198
|
+
useProgressiveLoading,
|
|
199
|
+
error,
|
|
200
|
+
contentKey: contentKeyRef.current,
|
|
201
|
+
size,
|
|
202
|
+
hasContent,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useImageTransform - Manages image rotation and flip state
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
8
|
+
import type { ImageTransform } from '../types';
|
|
9
|
+
import { DEFAULT_TRANSFORM } from '../utils';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// TYPES
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export interface UseImageTransformOptions {
|
|
16
|
+
/** Reset transform when this key changes */
|
|
17
|
+
resetKey?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UseImageTransformReturn {
|
|
21
|
+
/** Current transform state */
|
|
22
|
+
transform: ImageTransform;
|
|
23
|
+
/** Rotate 90 degrees clockwise */
|
|
24
|
+
rotate: () => void;
|
|
25
|
+
/** Toggle horizontal flip */
|
|
26
|
+
flipH: () => void;
|
|
27
|
+
/** Toggle vertical flip */
|
|
28
|
+
flipV: () => void;
|
|
29
|
+
/** Reset all transforms */
|
|
30
|
+
reset: () => void;
|
|
31
|
+
/** CSS transform string for applying to image */
|
|
32
|
+
transformStyle: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// HOOK
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
export function useImageTransform(
|
|
40
|
+
options: UseImageTransformOptions = {}
|
|
41
|
+
): UseImageTransformReturn {
|
|
42
|
+
const { resetKey } = options;
|
|
43
|
+
|
|
44
|
+
const [transform, setTransform] = useState<ImageTransform>(DEFAULT_TRANSFORM);
|
|
45
|
+
|
|
46
|
+
// Reset transform when key changes
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
setTransform(DEFAULT_TRANSFORM);
|
|
49
|
+
}, [resetKey]);
|
|
50
|
+
|
|
51
|
+
const rotate = useCallback(() => {
|
|
52
|
+
setTransform((prev) => ({
|
|
53
|
+
...prev,
|
|
54
|
+
rotation: (prev.rotation + 90) % 360,
|
|
55
|
+
}));
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const flipH = useCallback(() => {
|
|
59
|
+
setTransform((prev) => ({
|
|
60
|
+
...prev,
|
|
61
|
+
flipH: !prev.flipH,
|
|
62
|
+
}));
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const flipV = useCallback(() => {
|
|
66
|
+
setTransform((prev) => ({
|
|
67
|
+
...prev,
|
|
68
|
+
flipV: !prev.flipV,
|
|
69
|
+
}));
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const reset = useCallback(() => {
|
|
73
|
+
setTransform(DEFAULT_TRANSFORM);
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
// Build CSS transform string
|
|
77
|
+
const transformStyle = useMemo(() => {
|
|
78
|
+
const transforms: string[] = [];
|
|
79
|
+
|
|
80
|
+
if (transform.rotation !== 0) {
|
|
81
|
+
transforms.push(`rotate(${transform.rotation}deg)`);
|
|
82
|
+
}
|
|
83
|
+
if (transform.flipH) {
|
|
84
|
+
transforms.push('scaleX(-1)');
|
|
85
|
+
}
|
|
86
|
+
if (transform.flipV) {
|
|
87
|
+
transforms.push('scaleY(-1)');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return transforms.join(' ') || 'none';
|
|
91
|
+
}, [transform]);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
transform,
|
|
95
|
+
rotate,
|
|
96
|
+
flipH,
|
|
97
|
+
flipV,
|
|
98
|
+
reset,
|
|
99
|
+
transformStyle,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageViewer - Image viewer with zoom, pan, rotate, flip
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Zoom with mouse wheel and presets (25%, 50%, 100%, 200%, 400%)
|
|
6
|
+
* - Pan with drag
|
|
7
|
+
* - Rotate 90°
|
|
8
|
+
* - Flip horizontal/vertical
|
|
9
|
+
* - Fullscreen dialog mode
|
|
10
|
+
* - Keyboard shortcuts (+/-, 0 for reset, r for rotate)
|
|
11
|
+
* - Progressive loading with LQIP for large images
|
|
12
|
+
* - Checkerboard background for transparency
|
|
13
|
+
* - Image dimensions display with caching
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// COMPONENTS
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
export { ImageViewer, ImageToolbar, ImageInfo } from './components';
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// HOOKS
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
export { useImageTransform, useImageLoading } from './hooks';
|
|
27
|
+
export type {
|
|
28
|
+
UseImageTransformOptions,
|
|
29
|
+
UseImageTransformReturn,
|
|
30
|
+
UseImageLoadingOptions,
|
|
31
|
+
UseImageLoadingReturn,
|
|
32
|
+
} from './hooks';
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// TYPES
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
export type {
|
|
39
|
+
ImageFile,
|
|
40
|
+
ImageViewerProps,
|
|
41
|
+
ImageToolbarProps,
|
|
42
|
+
ImageInfoProps,
|
|
43
|
+
ImageTransform,
|
|
44
|
+
ZoomPreset,
|
|
45
|
+
} from './types';
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// UTILS
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
createLQIP,
|
|
53
|
+
MAX_IMAGE_SIZE,
|
|
54
|
+
WARNING_IMAGE_SIZE,
|
|
55
|
+
PROGRESSIVE_LOADING_THRESHOLD,
|
|
56
|
+
MIN_ZOOM,
|
|
57
|
+
MAX_ZOOM,
|
|
58
|
+
ZOOM_PRESETS,
|
|
59
|
+
DEFAULT_TRANSFORM,
|
|
60
|
+
} from './utils';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageViewer type definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// FILE TYPES
|
|
7
|
+
// =============================================================================
|
|
8
|
+
|
|
9
|
+
export interface ImageFile {
|
|
10
|
+
/** Display name for the image */
|
|
11
|
+
name: string;
|
|
12
|
+
/** File path used for change detection and caching */
|
|
13
|
+
path: string;
|
|
14
|
+
/** Optional MIME type */
|
|
15
|
+
mimeType?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// COMPONENT PROPS
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
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
|
+
/**
|
|
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;
|
|
33
|
+
/** Hide expand button when already in dialog */
|
|
34
|
+
inDialog?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ImageToolbarProps {
|
|
38
|
+
/** Current zoom scale */
|
|
39
|
+
scale: number;
|
|
40
|
+
/** Current transform state */
|
|
41
|
+
transform: ImageTransform;
|
|
42
|
+
/** Rotate image callback */
|
|
43
|
+
onRotate: () => void;
|
|
44
|
+
/** Flip horizontal callback */
|
|
45
|
+
onFlipH: () => void;
|
|
46
|
+
/** Flip vertical callback */
|
|
47
|
+
onFlipV: () => void;
|
|
48
|
+
/** Zoom preset selection callback */
|
|
49
|
+
onZoomPreset: (value: number | 'fit') => void;
|
|
50
|
+
/** Expand to fullscreen callback (undefined hides button) */
|
|
51
|
+
onExpand?: () => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ImageInfoProps {
|
|
55
|
+
/** Image source URL */
|
|
56
|
+
src: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// =============================================================================
|
|
60
|
+
// STATE TYPES
|
|
61
|
+
// =============================================================================
|
|
62
|
+
|
|
63
|
+
export interface ImageTransform {
|
|
64
|
+
/** Rotation angle: 0, 90, 180, or 270 degrees */
|
|
65
|
+
rotation: number;
|
|
66
|
+
/** Horizontal flip state */
|
|
67
|
+
flipH: boolean;
|
|
68
|
+
/** Vertical flip state */
|
|
69
|
+
flipV: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// UI TYPES
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
export interface ZoomPreset {
|
|
77
|
+
/** Display label (e.g., "100%", "Fit") */
|
|
78
|
+
label: string;
|
|
79
|
+
/** Zoom value or 'fit' for fit-to-view */
|
|
80
|
+
value: number | 'fit';
|
|
81
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageViewer constants
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ZoomPreset, ImageTransform } from '../types';
|
|
6
|
+
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// SIZE LIMITS
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
/** Maximum image size before blocking (50MB) */
|
|
12
|
+
export const MAX_IMAGE_SIZE = 50 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
/** Image size threshold for warning (10MB) */
|
|
15
|
+
export const WARNING_IMAGE_SIZE = 10 * 1024 * 1024;
|
|
16
|
+
|
|
17
|
+
/** Progressive loading threshold - use LQIP for images > 500KB */
|
|
18
|
+
export const PROGRESSIVE_LOADING_THRESHOLD = 500 * 1024;
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// LQIP CONFIGURATION
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/** Low-quality placeholder size (32x32) */
|
|
25
|
+
export const LQIP_SIZE = 32;
|
|
26
|
+
|
|
27
|
+
/** LQIP JPEG quality */
|
|
28
|
+
export const LQIP_QUALITY = 0.5;
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// ZOOM CONFIGURATION
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
/** Minimum zoom level */
|
|
35
|
+
export const MIN_ZOOM = 0.1;
|
|
36
|
+
|
|
37
|
+
/** Maximum zoom level */
|
|
38
|
+
export const MAX_ZOOM = 8;
|
|
39
|
+
|
|
40
|
+
/** Available zoom presets */
|
|
41
|
+
export const ZOOM_PRESETS: readonly ZoomPreset[] = [
|
|
42
|
+
{ label: 'Fit', value: 'fit' },
|
|
43
|
+
{ label: '25%', value: 0.25 },
|
|
44
|
+
{ label: '50%', value: 0.5 },
|
|
45
|
+
{ label: '100%', value: 1 },
|
|
46
|
+
{ label: '200%', value: 2 },
|
|
47
|
+
{ label: '400%', value: 4 },
|
|
48
|
+
] as const;
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// DEFAULT VALUES
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
/** Default transform state */
|
|
55
|
+
export const DEFAULT_TRANSFORM: ImageTransform = {
|
|
56
|
+
rotation: 0,
|
|
57
|
+
flipH: false,
|
|
58
|
+
flipV: false,
|
|
59
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ImageViewer Debug Logger
|
|
5
|
+
*
|
|
6
|
+
* Uses universal logger with media-specific helpers.
|
|
7
|
+
* Logs go to both console (dev) and zustand store (for Console panel).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createMediaLogger } from '@djangocfg/ui-core/lib';
|
|
11
|
+
|
|
12
|
+
export const imageDebug = createMediaLogger('ImageViewer');
|
|
13
|
+
|
|
14
|
+
export default imageDebug;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageViewer utilities - Public API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { createLQIP } from './lqip';
|
|
6
|
+
export {
|
|
7
|
+
MAX_IMAGE_SIZE,
|
|
8
|
+
WARNING_IMAGE_SIZE,
|
|
9
|
+
PROGRESSIVE_LOADING_THRESHOLD,
|
|
10
|
+
LQIP_SIZE,
|
|
11
|
+
LQIP_QUALITY,
|
|
12
|
+
MIN_ZOOM,
|
|
13
|
+
MAX_ZOOM,
|
|
14
|
+
ZOOM_PRESETS,
|
|
15
|
+
DEFAULT_TRANSFORM,
|
|
16
|
+
} from './constants';
|
|
17
|
+
export { imageDebug } from './debug';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LQIP (Low-Quality Image Placeholder) generator
|
|
3
|
+
*
|
|
4
|
+
* Creates a tiny blurred preview image for progressive loading.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LQIP_SIZE, LQIP_QUALITY } from './constants';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a low-quality image placeholder from source URL
|
|
11
|
+
*
|
|
12
|
+
* @param imageSrc - Full quality image URL
|
|
13
|
+
* @returns Data URL of tiny preview, or null on error
|
|
14
|
+
*/
|
|
15
|
+
export async function createLQIP(imageSrc: string): Promise<string | null> {
|
|
16
|
+
try {
|
|
17
|
+
// Load the full image
|
|
18
|
+
const img = new Image();
|
|
19
|
+
img.crossOrigin = 'anonymous';
|
|
20
|
+
|
|
21
|
+
await new Promise<void>((resolve, reject) => {
|
|
22
|
+
img.onload = () => resolve();
|
|
23
|
+
img.onerror = () => reject(new Error('Failed to load image for LQIP'));
|
|
24
|
+
img.src = imageSrc;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Calculate aspect ratio preserving dimensions
|
|
28
|
+
const aspect = img.naturalWidth / img.naturalHeight;
|
|
29
|
+
const width = aspect >= 1 ? LQIP_SIZE : Math.round(LQIP_SIZE * aspect);
|
|
30
|
+
const height = aspect >= 1 ? Math.round(LQIP_SIZE / aspect) : LQIP_SIZE;
|
|
31
|
+
|
|
32
|
+
// Create canvas for downscaling
|
|
33
|
+
const canvas = document.createElement('canvas');
|
|
34
|
+
canvas.width = width;
|
|
35
|
+
canvas.height = height;
|
|
36
|
+
const ctx = canvas.getContext('2d');
|
|
37
|
+
if (!ctx) return null;
|
|
38
|
+
|
|
39
|
+
// Draw scaled down image
|
|
40
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
41
|
+
|
|
42
|
+
// Return as data URL (very small, ~1-2KB)
|
|
43
|
+
return canvas.toDataURL('image/jpeg', LQIP_QUALITY);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|