@djangocfg/ui-nextjs 2.1.65 → 2.1.66

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.
Files changed (30) hide show
  1. package/package.json +9 -6
  2. package/src/blocks/SplitHero/SplitHeroMedia.tsx +2 -1
  3. package/src/tools/AudioPlayer/AudioEqualizer.tsx +235 -0
  4. package/src/tools/AudioPlayer/AudioPlayer.tsx +223 -0
  5. package/src/tools/AudioPlayer/AudioReactiveCover.tsx +389 -0
  6. package/src/tools/AudioPlayer/AudioShortcutsPopover.tsx +95 -0
  7. package/src/tools/AudioPlayer/README.md +301 -0
  8. package/src/tools/AudioPlayer/SimpleAudioPlayer.tsx +275 -0
  9. package/src/tools/AudioPlayer/VisualizationToggle.tsx +68 -0
  10. package/src/tools/AudioPlayer/context.tsx +426 -0
  11. package/src/tools/AudioPlayer/effects/index.ts +412 -0
  12. package/src/tools/AudioPlayer/index.ts +84 -0
  13. package/src/tools/AudioPlayer/types.ts +162 -0
  14. package/src/tools/AudioPlayer/useAudioHotkeys.ts +142 -0
  15. package/src/tools/AudioPlayer/useAudioVisualization.tsx +195 -0
  16. package/src/tools/ImageViewer/ImageViewer.tsx +416 -0
  17. package/src/tools/ImageViewer/README.md +161 -0
  18. package/src/tools/ImageViewer/index.ts +16 -0
  19. package/src/tools/VideoPlayer/README.md +196 -187
  20. package/src/tools/VideoPlayer/VideoErrorFallback.tsx +174 -0
  21. package/src/tools/VideoPlayer/VideoPlayer.tsx +189 -218
  22. package/src/tools/VideoPlayer/VideoPlayerContext.tsx +125 -0
  23. package/src/tools/VideoPlayer/index.ts +59 -7
  24. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +206 -0
  25. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +311 -0
  26. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +254 -0
  27. package/src/tools/VideoPlayer/providers/index.ts +8 -0
  28. package/src/tools/VideoPlayer/types.ts +320 -71
  29. package/src/tools/index.ts +82 -4
  30. package/src/tools/VideoPlayer/NativePlayer.tsx +0 -141
@@ -0,0 +1,206 @@
1
+ /**
2
+ * NativeProvider - Lightweight native HTML5 video player
3
+ * For demo videos, background videos, autoplay loop muted scenarios
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
9
+
10
+ import { cn } from '@djangocfg/ui-core/lib';
11
+ import { Preloader, AspectRatio } from '@djangocfg/ui-core';
12
+
13
+ import type { NativeProviderProps, VideoPlayerRef } from '../types';
14
+
15
+ /**
16
+ * Get video URL from source
17
+ */
18
+ function getVideoUrl(source: NativeProviderProps['source']): string {
19
+ switch (source.type) {
20
+ case 'url':
21
+ return source.url;
22
+ case 'data-url':
23
+ return source.data;
24
+ default:
25
+ return '';
26
+ }
27
+ }
28
+
29
+ export const NativeProvider = forwardRef<VideoPlayerRef, NativeProviderProps>(
30
+ (
31
+ {
32
+ source,
33
+ aspectRatio = 16 / 9,
34
+ autoPlay = true,
35
+ muted = true,
36
+ loop = true,
37
+ playsInline = true,
38
+ preload = 'auto',
39
+ controls = false,
40
+ disableContextMenu = true,
41
+ showPreloader = true,
42
+ preloaderTimeout = 5000,
43
+ className,
44
+ videoClassName,
45
+ onPlay,
46
+ onPause,
47
+ onEnded,
48
+ onError,
49
+ onLoadStart,
50
+ onCanPlay,
51
+ onTimeUpdate,
52
+ },
53
+ ref
54
+ ) => {
55
+ const [isLoading, setIsLoading] = useState(showPreloader);
56
+ const videoRef = useRef<HTMLVideoElement>(null);
57
+
58
+ const videoUrl = getVideoUrl(source);
59
+
60
+ // Expose video element methods via ref
61
+ useImperativeHandle(
62
+ ref,
63
+ () => ({
64
+ play: () => videoRef.current?.play(),
65
+ pause: () => videoRef.current?.pause(),
66
+ togglePlay: () => {
67
+ const video = videoRef.current;
68
+ if (video) {
69
+ video.paused ? video.play() : video.pause();
70
+ }
71
+ },
72
+ seekTo: (time: number) => {
73
+ if (videoRef.current) videoRef.current.currentTime = time;
74
+ },
75
+ setVolume: (volume: number) => {
76
+ if (videoRef.current) videoRef.current.volume = Math.max(0, Math.min(1, volume));
77
+ },
78
+ toggleMute: () => {
79
+ if (videoRef.current) videoRef.current.muted = !videoRef.current.muted;
80
+ },
81
+ enterFullscreen: () => videoRef.current?.requestFullscreen(),
82
+ exitFullscreen: () => document.exitFullscreen(),
83
+ get currentTime() {
84
+ return videoRef.current?.currentTime ?? 0;
85
+ },
86
+ get duration() {
87
+ return videoRef.current?.duration ?? 0;
88
+ },
89
+ get paused() {
90
+ return videoRef.current?.paused ?? true;
91
+ },
92
+ }),
93
+ []
94
+ );
95
+
96
+ useEffect(() => {
97
+ if (!showPreloader) return;
98
+
99
+ const video = videoRef.current;
100
+ if (!video) return;
101
+
102
+ // Check if video is already loaded
103
+ if (video.readyState >= 3) {
104
+ setIsLoading(false);
105
+ return;
106
+ }
107
+
108
+ const hideLoader = () => setIsLoading(false);
109
+
110
+ video.addEventListener('canplay', hideLoader);
111
+ video.addEventListener('loadeddata', hideLoader);
112
+ video.addEventListener('playing', hideLoader);
113
+
114
+ // Fallback: hide loader after timeout
115
+ const timeout = setTimeout(hideLoader, preloaderTimeout);
116
+
117
+ return () => {
118
+ video.removeEventListener('canplay', hideLoader);
119
+ video.removeEventListener('loadeddata', hideLoader);
120
+ video.removeEventListener('playing', hideLoader);
121
+ clearTimeout(timeout);
122
+ };
123
+ }, [showPreloader, preloaderTimeout]);
124
+
125
+ const handleContextMenu = (e: React.MouseEvent) => {
126
+ if (disableContextMenu) {
127
+ e.preventDefault();
128
+ }
129
+ };
130
+
131
+ const handleError = (e: React.SyntheticEvent<HTMLVideoElement>) => {
132
+ setIsLoading(false);
133
+ onError?.(e.currentTarget.error?.message || 'Video playback error');
134
+ };
135
+
136
+ const handleTimeUpdate = () => {
137
+ const video = videoRef.current;
138
+ if (video && onTimeUpdate) {
139
+ onTimeUpdate(video.currentTime, video.duration);
140
+ }
141
+ };
142
+
143
+ // Determine if we should use AspectRatio wrapper or fill mode
144
+ const isFillMode = aspectRatio === 'fill';
145
+ const computedAspectRatio = aspectRatio === 'auto' || aspectRatio === 'fill' ? undefined : aspectRatio;
146
+
147
+ // Video content
148
+ const videoContent = (
149
+ <>
150
+ {/* Preloader */}
151
+ {showPreloader && isLoading && (
152
+ <div
153
+ className={cn(
154
+ 'absolute inset-0 flex items-center justify-center bg-muted/30 backdrop-blur-sm z-10'
155
+ )}
156
+ >
157
+ <Preloader size="lg" spinnerClassName="text-white" />
158
+ </div>
159
+ )}
160
+
161
+ {/* Video */}
162
+ <video
163
+ ref={videoRef}
164
+ className={cn('w-full h-full object-cover', videoClassName)}
165
+ src={videoUrl}
166
+ autoPlay={autoPlay}
167
+ muted={muted}
168
+ loop={loop}
169
+ playsInline={playsInline}
170
+ preload={preload}
171
+ controls={controls}
172
+ poster={source.poster}
173
+ onContextMenu={handleContextMenu}
174
+ onLoadStart={onLoadStart}
175
+ onCanPlay={onCanPlay}
176
+ onPlay={onPlay}
177
+ onPause={onPause}
178
+ onPlaying={() => setIsLoading(false)}
179
+ onEnded={onEnded}
180
+ onError={handleError}
181
+ onTimeUpdate={handleTimeUpdate}
182
+ />
183
+ </>
184
+ );
185
+
186
+ // Fill mode - no AspectRatio wrapper
187
+ if (isFillMode) {
188
+ return (
189
+ <div className={cn('relative w-full h-full overflow-hidden', className)}>
190
+ {videoContent}
191
+ </div>
192
+ );
193
+ }
194
+
195
+ // Normal mode with AspectRatio
196
+ return (
197
+ <div className={cn('relative overflow-hidden', className)}>
198
+ <AspectRatio ratio={computedAspectRatio}>
199
+ {videoContent}
200
+ </AspectRatio>
201
+ </div>
202
+ );
203
+ }
204
+ );
205
+
206
+ NativeProvider.displayName = 'NativeProvider';
@@ -0,0 +1,311 @@
1
+ /**
2
+ * StreamProvider - HTTP Range streaming and Blob video player
3
+ * Supports:
4
+ * - HTTP Range requests with authorization (for large files)
5
+ * - Blob/ArrayBuffer sources
6
+ * - Data URL sources
7
+ * - Fill parent container mode
8
+ * - Custom error fallback
9
+ */
10
+
11
+ 'use client';
12
+
13
+ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
14
+
15
+ import { cn } from '@djangocfg/ui-core/lib';
16
+ import { Preloader, AspectRatio } from '@djangocfg/ui-core';
17
+
18
+ import type { StreamProviderProps, VideoPlayerRef, StreamSource, BlobSource, DataUrlSource, ErrorFallbackProps } from '../types';
19
+
20
+ /** Default error fallback UI */
21
+ function DefaultErrorFallback({ error }: ErrorFallbackProps) {
22
+ return (
23
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-4 text-white">
24
+ <svg
25
+ className="w-16 h-16 text-muted-foreground"
26
+ fill="none"
27
+ stroke="currentColor"
28
+ viewBox="0 0 24 24"
29
+ >
30
+ <path
31
+ strokeLinecap="round"
32
+ strokeLinejoin="round"
33
+ strokeWidth={2}
34
+ d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
35
+ />
36
+ </svg>
37
+ <p className="text-lg">{error || 'Video cannot be previewed'}</p>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
43
+ (
44
+ {
45
+ source,
46
+ aspectRatio = 16 / 9,
47
+ autoPlay = false,
48
+ muted = false,
49
+ loop = false,
50
+ playsInline = true,
51
+ preload = 'metadata',
52
+ controls = true,
53
+ disableContextMenu = false,
54
+ showPreloader = true,
55
+ preloaderTimeout = 10000,
56
+ className,
57
+ videoClassName,
58
+ errorFallback,
59
+ onPlay,
60
+ onPause,
61
+ onEnded,
62
+ onError,
63
+ onLoadStart,
64
+ onCanPlay,
65
+ onTimeUpdate,
66
+ },
67
+ ref
68
+ ) => {
69
+ const [videoUrl, setVideoUrl] = useState<string | null>(null);
70
+ const [isLoading, setIsLoading] = useState(true);
71
+ const [hasError, setHasError] = useState(false);
72
+ const [errorMessage, setErrorMessage] = useState<string>('Video cannot be previewed');
73
+ const blobUrlRef = useRef<string | null>(null);
74
+ const videoRef = useRef<HTMLVideoElement>(null);
75
+
76
+ // Retry function for error fallback
77
+ const retry = useCallback(() => {
78
+ setHasError(false);
79
+ setIsLoading(true);
80
+ // Re-trigger source effect by forcing state update
81
+ const video = videoRef.current;
82
+ if (video && videoUrl) {
83
+ video.load();
84
+ }
85
+ }, [videoUrl]);
86
+
87
+ // Expose video element methods via ref
88
+ useImperativeHandle(
89
+ ref,
90
+ () => ({
91
+ play: () => videoRef.current?.play(),
92
+ pause: () => videoRef.current?.pause(),
93
+ togglePlay: () => {
94
+ const video = videoRef.current;
95
+ if (video) {
96
+ video.paused ? video.play() : video.pause();
97
+ }
98
+ },
99
+ seekTo: (time: number) => {
100
+ if (videoRef.current) videoRef.current.currentTime = time;
101
+ },
102
+ setVolume: (volume: number) => {
103
+ if (videoRef.current) videoRef.current.volume = Math.max(0, Math.min(1, volume));
104
+ },
105
+ toggleMute: () => {
106
+ if (videoRef.current) videoRef.current.muted = !videoRef.current.muted;
107
+ },
108
+ enterFullscreen: () => videoRef.current?.requestFullscreen(),
109
+ exitFullscreen: () => document.exitFullscreen(),
110
+ get currentTime() {
111
+ return videoRef.current?.currentTime ?? 0;
112
+ },
113
+ get duration() {
114
+ return videoRef.current?.duration ?? 0;
115
+ },
116
+ get paused() {
117
+ return videoRef.current?.paused ?? true;
118
+ },
119
+ }),
120
+ []
121
+ );
122
+
123
+ // Create video URL based on source type
124
+ useEffect(() => {
125
+ // Cleanup previous blob URL
126
+ if (blobUrlRef.current) {
127
+ URL.revokeObjectURL(blobUrlRef.current);
128
+ blobUrlRef.current = null;
129
+ }
130
+
131
+ setHasError(false);
132
+ setIsLoading(true);
133
+
134
+ switch (source.type) {
135
+ case 'stream': {
136
+ const streamSource = source as StreamSource;
137
+ const url = streamSource.getStreamUrl(streamSource.sessionId, streamSource.path);
138
+ setVideoUrl(url);
139
+ break;
140
+ }
141
+
142
+ case 'blob': {
143
+ const blobSource = source as BlobSource;
144
+ const blob = new Blob([blobSource.data], {
145
+ type: blobSource.mimeType || 'video/mp4'
146
+ });
147
+ const url = URL.createObjectURL(blob);
148
+ blobUrlRef.current = url;
149
+ setVideoUrl(url);
150
+ break;
151
+ }
152
+
153
+ case 'data-url': {
154
+ const dataUrlSource = source as DataUrlSource;
155
+ setVideoUrl(dataUrlSource.data);
156
+ break;
157
+ }
158
+
159
+ default:
160
+ setVideoUrl(null);
161
+ setHasError(true);
162
+ setErrorMessage('Invalid video source');
163
+ }
164
+
165
+ return () => {
166
+ if (blobUrlRef.current) {
167
+ URL.revokeObjectURL(blobUrlRef.current);
168
+ blobUrlRef.current = null;
169
+ }
170
+ };
171
+ }, [source]);
172
+
173
+ // Preloader timeout
174
+ useEffect(() => {
175
+ if (!showPreloader || !isLoading) return;
176
+
177
+ const timeout = setTimeout(() => {
178
+ setIsLoading(false);
179
+ }, preloaderTimeout);
180
+
181
+ return () => clearTimeout(timeout);
182
+ }, [showPreloader, isLoading, preloaderTimeout]);
183
+
184
+ const handleContextMenu = (e: React.MouseEvent) => {
185
+ if (disableContextMenu) {
186
+ e.preventDefault();
187
+ }
188
+ };
189
+
190
+ const handleCanPlay = () => {
191
+ setIsLoading(false);
192
+ onCanPlay?.();
193
+ };
194
+
195
+ const handleLoadedData = () => {
196
+ setIsLoading(false);
197
+ };
198
+
199
+ const handleError = () => {
200
+ setIsLoading(false);
201
+ setHasError(true);
202
+ setErrorMessage('Failed to load video');
203
+ onError?.('Video playback error');
204
+ };
205
+
206
+ const handleTimeUpdate = () => {
207
+ const video = videoRef.current;
208
+ if (video && onTimeUpdate) {
209
+ onTimeUpdate(video.currentTime, video.duration);
210
+ }
211
+ };
212
+
213
+ // Determine if we should use AspectRatio wrapper or fill mode
214
+ const isFillMode = aspectRatio === 'fill';
215
+ const computedAspectRatio = aspectRatio === 'auto' || aspectRatio === 'fill' ? undefined : aspectRatio;
216
+
217
+ // Render error fallback
218
+ const renderErrorFallback = () => {
219
+ const fallbackProps: ErrorFallbackProps = { error: errorMessage, retry };
220
+
221
+ if (typeof errorFallback === 'function') {
222
+ return errorFallback(fallbackProps);
223
+ }
224
+
225
+ if (errorFallback) {
226
+ return errorFallback;
227
+ }
228
+
229
+ return <DefaultErrorFallback {...fallbackProps} />;
230
+ };
231
+
232
+ // Error state
233
+ if (!videoUrl || hasError) {
234
+ if (isFillMode) {
235
+ return (
236
+ <div className={cn('relative w-full h-full overflow-hidden bg-black', className)}>
237
+ {renderErrorFallback()}
238
+ </div>
239
+ );
240
+ }
241
+
242
+ return (
243
+ <div className={cn('relative overflow-hidden bg-black', className)}>
244
+ <AspectRatio ratio={computedAspectRatio}>
245
+ {renderErrorFallback()}
246
+ </AspectRatio>
247
+ </div>
248
+ );
249
+ }
250
+
251
+ // Video content
252
+ const videoContent = (
253
+ <>
254
+ {/* Loading indicator */}
255
+ {showPreloader && isLoading && (
256
+ <div className="absolute inset-0 flex items-center justify-center bg-black/50 z-10">
257
+ <Preloader size="lg" spinnerClassName="text-white" />
258
+ </div>
259
+ )}
260
+
261
+ {/* Video element */}
262
+ <video
263
+ ref={videoRef}
264
+ src={videoUrl}
265
+ className={cn(
266
+ 'w-full h-full object-contain',
267
+ isLoading && 'opacity-0',
268
+ videoClassName
269
+ )}
270
+ autoPlay={autoPlay}
271
+ muted={muted}
272
+ loop={loop}
273
+ playsInline={playsInline}
274
+ preload={preload}
275
+ controls={controls}
276
+ crossOrigin="anonymous"
277
+ poster={source.poster}
278
+ onContextMenu={handleContextMenu}
279
+ onLoadStart={onLoadStart}
280
+ onCanPlay={handleCanPlay}
281
+ onLoadedData={handleLoadedData}
282
+ onPlay={onPlay}
283
+ onPause={onPause}
284
+ onEnded={onEnded}
285
+ onError={handleError}
286
+ onTimeUpdate={handleTimeUpdate}
287
+ />
288
+ </>
289
+ );
290
+
291
+ // Fill mode - no AspectRatio wrapper
292
+ if (isFillMode) {
293
+ return (
294
+ <div className={cn('relative w-full h-full overflow-hidden bg-black', className)}>
295
+ {videoContent}
296
+ </div>
297
+ );
298
+ }
299
+
300
+ // Normal mode with AspectRatio
301
+ return (
302
+ <div className={cn('relative overflow-hidden bg-black', className)}>
303
+ <AspectRatio ratio={computedAspectRatio}>
304
+ {videoContent}
305
+ </AspectRatio>
306
+ </div>
307
+ );
308
+ }
309
+ );
310
+
311
+ StreamProvider.displayName = 'StreamProvider';