@djangocfg/ui-nextjs 2.1.56 → 2.1.58
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.58",
|
|
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.58",
|
|
62
|
+
"@djangocfg/ui-core": "^2.1.58",
|
|
63
63
|
"@types/react": "^19.1.0",
|
|
64
64
|
"@types/react-dom": "^19.1.0",
|
|
65
65
|
"consola": "^3.4.2",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"vidstack": "next"
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
109
|
+
"@djangocfg/typescript-config": "^2.1.58",
|
|
110
110
|
"@types/node": "^24.7.2",
|
|
111
111
|
"eslint": "^9.37.0",
|
|
112
112
|
"tailwindcss-animate": "1.0.7",
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
6
|
+
|
|
7
|
+
export type MouseFollowerVariant =
|
|
8
|
+
| 'glow'
|
|
9
|
+
| 'spotlight'
|
|
10
|
+
| 'gradient-blob'
|
|
11
|
+
| 'ring'
|
|
12
|
+
| 'trail';
|
|
13
|
+
|
|
14
|
+
interface MouseFollowerProps {
|
|
15
|
+
/** Visual style of the follower */
|
|
16
|
+
variant?: MouseFollowerVariant;
|
|
17
|
+
/** Size of the effect in pixels */
|
|
18
|
+
size?: number;
|
|
19
|
+
/** Color - can be CSS color or 'primary' to use theme */
|
|
20
|
+
color?: string;
|
|
21
|
+
/** Smoothness of following (0.05 = very smooth, 0.3 = snappy) */
|
|
22
|
+
smoothness?: number;
|
|
23
|
+
/** Opacity of the effect (0-1) */
|
|
24
|
+
opacity?: number;
|
|
25
|
+
/** Blur amount for glow effects */
|
|
26
|
+
blur?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
|
|
27
|
+
/** Additional className for the container */
|
|
28
|
+
className?: string;
|
|
29
|
+
/** Whether to show on mobile (touch devices) */
|
|
30
|
+
showOnMobile?: boolean;
|
|
31
|
+
/** Disable the effect */
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Position {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const blurMap = {
|
|
41
|
+
sm: 'blur-sm',
|
|
42
|
+
md: 'blur-md',
|
|
43
|
+
lg: 'blur-lg',
|
|
44
|
+
xl: 'blur-xl',
|
|
45
|
+
'2xl': 'blur-2xl',
|
|
46
|
+
'3xl': 'blur-3xl',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const MouseFollower: React.FC<MouseFollowerProps> = ({
|
|
50
|
+
variant = 'glow',
|
|
51
|
+
size = 300,
|
|
52
|
+
color = 'primary',
|
|
53
|
+
smoothness = 0.1,
|
|
54
|
+
opacity = 0.3,
|
|
55
|
+
blur = '2xl',
|
|
56
|
+
className,
|
|
57
|
+
showOnMobile = false,
|
|
58
|
+
disabled = false,
|
|
59
|
+
}) => {
|
|
60
|
+
const [mounted, setMounted] = useState(false);
|
|
61
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
62
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
63
|
+
const positionRef = useRef<Position>({ x: 0, y: 0 });
|
|
64
|
+
const targetRef = useRef<Position>({ x: 0, y: 0 });
|
|
65
|
+
const elementRef = useRef<HTMLDivElement>(null);
|
|
66
|
+
const rafRef = useRef<number>(0);
|
|
67
|
+
|
|
68
|
+
// Resolve color
|
|
69
|
+
const resolvedColor = useMemo(() => {
|
|
70
|
+
if (color === 'primary') return 'hsl(var(--primary))';
|
|
71
|
+
if (color === 'secondary') return 'hsl(var(--secondary))';
|
|
72
|
+
if (color === 'accent') return 'hsl(var(--accent))';
|
|
73
|
+
return color;
|
|
74
|
+
}, [color]);
|
|
75
|
+
|
|
76
|
+
// Check if mobile
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
setMounted(true);
|
|
79
|
+
const checkMobile = () => {
|
|
80
|
+
setIsMobile('ontouchstart' in window || navigator.maxTouchPoints > 0);
|
|
81
|
+
};
|
|
82
|
+
checkMobile();
|
|
83
|
+
window.addEventListener('resize', checkMobile);
|
|
84
|
+
return () => window.removeEventListener('resize', checkMobile);
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
// Animation loop with lerp
|
|
88
|
+
const animate = useCallback(() => {
|
|
89
|
+
const dx = targetRef.current.x - positionRef.current.x;
|
|
90
|
+
const dy = targetRef.current.y - positionRef.current.y;
|
|
91
|
+
|
|
92
|
+
positionRef.current.x += dx * smoothness;
|
|
93
|
+
positionRef.current.y += dy * smoothness;
|
|
94
|
+
|
|
95
|
+
if (elementRef.current) {
|
|
96
|
+
elementRef.current.style.transform = `translate(${positionRef.current.x - size / 2}px, ${positionRef.current.y - size / 2}px)`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
rafRef.current = requestAnimationFrame(animate);
|
|
100
|
+
}, [smoothness, size]);
|
|
101
|
+
|
|
102
|
+
// Mouse move handler
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (disabled || !mounted) return;
|
|
105
|
+
if (isMobile && !showOnMobile) return;
|
|
106
|
+
|
|
107
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
108
|
+
targetRef.current = { x: e.clientX, y: e.clientY };
|
|
109
|
+
if (!isVisible) setIsVisible(true);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleMouseLeave = () => {
|
|
113
|
+
setIsVisible(false);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleMouseEnter = () => {
|
|
117
|
+
setIsVisible(true);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
121
|
+
document.addEventListener('mouseleave', handleMouseLeave);
|
|
122
|
+
document.addEventListener('mouseenter', handleMouseEnter);
|
|
123
|
+
|
|
124
|
+
rafRef.current = requestAnimationFrame(animate);
|
|
125
|
+
|
|
126
|
+
return () => {
|
|
127
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
128
|
+
document.removeEventListener('mouseleave', handleMouseLeave);
|
|
129
|
+
document.removeEventListener('mouseenter', handleMouseEnter);
|
|
130
|
+
cancelAnimationFrame(rafRef.current);
|
|
131
|
+
};
|
|
132
|
+
}, [disabled, mounted, isMobile, showOnMobile, animate, isVisible]);
|
|
133
|
+
|
|
134
|
+
if (!mounted || disabled) return null;
|
|
135
|
+
if (isMobile && !showOnMobile) return null;
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
className={cn(
|
|
140
|
+
'fixed inset-0 pointer-events-none overflow-hidden z-0',
|
|
141
|
+
className
|
|
142
|
+
)}
|
|
143
|
+
>
|
|
144
|
+
<div
|
|
145
|
+
ref={elementRef}
|
|
146
|
+
className={cn(
|
|
147
|
+
'absolute top-0 left-0 transition-opacity duration-300',
|
|
148
|
+
isVisible ? 'opacity-100' : 'opacity-0'
|
|
149
|
+
)}
|
|
150
|
+
style={{ width: size, height: size }}
|
|
151
|
+
>
|
|
152
|
+
{variant === 'glow' && (
|
|
153
|
+
<GlowEffect
|
|
154
|
+
size={size}
|
|
155
|
+
color={resolvedColor}
|
|
156
|
+
opacity={opacity}
|
|
157
|
+
blur={blurMap[blur]}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{variant === 'spotlight' && (
|
|
162
|
+
<SpotlightEffect
|
|
163
|
+
size={size}
|
|
164
|
+
color={resolvedColor}
|
|
165
|
+
opacity={opacity}
|
|
166
|
+
blur={blurMap[blur]}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
{variant === 'gradient-blob' && (
|
|
171
|
+
<GradientBlobEffect
|
|
172
|
+
size={size}
|
|
173
|
+
color={resolvedColor}
|
|
174
|
+
opacity={opacity}
|
|
175
|
+
blur={blurMap[blur]}
|
|
176
|
+
/>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{variant === 'ring' && (
|
|
180
|
+
<RingEffect
|
|
181
|
+
size={size}
|
|
182
|
+
color={resolvedColor}
|
|
183
|
+
opacity={opacity}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{variant === 'trail' && (
|
|
188
|
+
<TrailEffect
|
|
189
|
+
size={size}
|
|
190
|
+
color={resolvedColor}
|
|
191
|
+
opacity={opacity}
|
|
192
|
+
blur={blurMap[blur]}
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// =============================================================================
|
|
201
|
+
// Effect Variants
|
|
202
|
+
// =============================================================================
|
|
203
|
+
|
|
204
|
+
interface EffectProps {
|
|
205
|
+
size: number;
|
|
206
|
+
color: string;
|
|
207
|
+
opacity: number;
|
|
208
|
+
blur?: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const GlowEffect: React.FC<EffectProps> = ({ size, color, opacity, blur }) => (
|
|
212
|
+
<div
|
|
213
|
+
className={cn('w-full h-full rounded-full', blur)}
|
|
214
|
+
style={{
|
|
215
|
+
background: `radial-gradient(circle, ${color} 0%, transparent 70%)`,
|
|
216
|
+
opacity,
|
|
217
|
+
}}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const SpotlightEffect: React.FC<EffectProps> = ({ size, color, opacity, blur }) => (
|
|
222
|
+
<>
|
|
223
|
+
{/* Main spotlight cone */}
|
|
224
|
+
<div
|
|
225
|
+
className={cn('absolute inset-0', blur)}
|
|
226
|
+
style={{
|
|
227
|
+
background: `conic-gradient(from 180deg at 50% 50%,
|
|
228
|
+
transparent 0deg,
|
|
229
|
+
${color} 150deg,
|
|
230
|
+
${color} 210deg,
|
|
231
|
+
transparent 360deg
|
|
232
|
+
)`,
|
|
233
|
+
opacity: opacity * 0.6,
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
{/* Center glow */}
|
|
237
|
+
<div
|
|
238
|
+
className={cn('absolute rounded-full', blur)}
|
|
239
|
+
style={{
|
|
240
|
+
width: '40%',
|
|
241
|
+
height: '40%',
|
|
242
|
+
top: '30%',
|
|
243
|
+
left: '30%',
|
|
244
|
+
background: `radial-gradient(circle, ${color} 0%, transparent 70%)`,
|
|
245
|
+
opacity,
|
|
246
|
+
}}
|
|
247
|
+
/>
|
|
248
|
+
</>
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const GradientBlobEffect: React.FC<EffectProps> = ({ size, color, opacity, blur }) => (
|
|
252
|
+
<>
|
|
253
|
+
{/* Animated blob shape */}
|
|
254
|
+
<div
|
|
255
|
+
className={cn('w-full h-full', blur)}
|
|
256
|
+
style={{
|
|
257
|
+
background: `radial-gradient(ellipse 60% 40% at 50% 50%, ${color} 0%, transparent 70%)`,
|
|
258
|
+
opacity,
|
|
259
|
+
animation: 'blob-morph 8s ease-in-out infinite',
|
|
260
|
+
}}
|
|
261
|
+
/>
|
|
262
|
+
{/* Secondary blob */}
|
|
263
|
+
<div
|
|
264
|
+
className={cn('absolute inset-0', blur)}
|
|
265
|
+
style={{
|
|
266
|
+
background: `radial-gradient(ellipse 40% 60% at 50% 50%, ${color} 0%, transparent 60%)`,
|
|
267
|
+
opacity: opacity * 0.5,
|
|
268
|
+
animation: 'blob-morph 8s ease-in-out infinite reverse',
|
|
269
|
+
animationDelay: '-4s',
|
|
270
|
+
}}
|
|
271
|
+
/>
|
|
272
|
+
<style>{`
|
|
273
|
+
@keyframes blob-morph {
|
|
274
|
+
0%, 100% { transform: scale(1) rotate(0deg); border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; }
|
|
275
|
+
25% { transform: scale(1.1) rotate(90deg); border-radius: 30% 60% 70% 40% / 50% 60% 30% 60%; }
|
|
276
|
+
50% { transform: scale(0.9) rotate(180deg); border-radius: 50% 60% 30% 60% / 30% 60% 70% 40%; }
|
|
277
|
+
75% { transform: scale(1.05) rotate(270deg); border-radius: 60% 40% 50% 40% / 70% 30% 50% 60%; }
|
|
278
|
+
}
|
|
279
|
+
`}</style>
|
|
280
|
+
</>
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const RingEffect: React.FC<EffectProps> = ({ size, color, opacity }) => (
|
|
284
|
+
<>
|
|
285
|
+
{/* Outer ring */}
|
|
286
|
+
<div
|
|
287
|
+
className="absolute inset-0 rounded-full"
|
|
288
|
+
style={{
|
|
289
|
+
border: `2px solid ${color}`,
|
|
290
|
+
opacity: opacity * 0.3,
|
|
291
|
+
}}
|
|
292
|
+
/>
|
|
293
|
+
{/* Middle ring */}
|
|
294
|
+
<div
|
|
295
|
+
className="absolute rounded-full"
|
|
296
|
+
style={{
|
|
297
|
+
inset: '15%',
|
|
298
|
+
border: `2px solid ${color}`,
|
|
299
|
+
opacity: opacity * 0.5,
|
|
300
|
+
animation: 'ring-pulse 2s ease-in-out infinite',
|
|
301
|
+
}}
|
|
302
|
+
/>
|
|
303
|
+
{/* Inner ring */}
|
|
304
|
+
<div
|
|
305
|
+
className="absolute rounded-full"
|
|
306
|
+
style={{
|
|
307
|
+
inset: '30%',
|
|
308
|
+
border: `2px solid ${color}`,
|
|
309
|
+
opacity: opacity * 0.7,
|
|
310
|
+
animation: 'ring-pulse 2s ease-in-out infinite',
|
|
311
|
+
animationDelay: '-1s',
|
|
312
|
+
}}
|
|
313
|
+
/>
|
|
314
|
+
{/* Center dot */}
|
|
315
|
+
<div
|
|
316
|
+
className="absolute rounded-full"
|
|
317
|
+
style={{
|
|
318
|
+
width: '8px',
|
|
319
|
+
height: '8px',
|
|
320
|
+
top: '50%',
|
|
321
|
+
left: '50%',
|
|
322
|
+
transform: 'translate(-50%, -50%)',
|
|
323
|
+
background: color,
|
|
324
|
+
opacity,
|
|
325
|
+
}}
|
|
326
|
+
/>
|
|
327
|
+
<style>{`
|
|
328
|
+
@keyframes ring-pulse {
|
|
329
|
+
0%, 100% { opacity: 0.3; transform: scale(1); }
|
|
330
|
+
50% { opacity: 0.7; transform: scale(1.05); }
|
|
331
|
+
}
|
|
332
|
+
`}</style>
|
|
333
|
+
</>
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const TrailEffect: React.FC<EffectProps> = ({ size, color, opacity, blur }) => (
|
|
337
|
+
<>
|
|
338
|
+
{/* Multiple trailing circles */}
|
|
339
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
340
|
+
<div
|
|
341
|
+
key={i}
|
|
342
|
+
className={cn('absolute rounded-full', blur)}
|
|
343
|
+
style={{
|
|
344
|
+
width: `${100 - i * 15}%`,
|
|
345
|
+
height: `${100 - i * 15}%`,
|
|
346
|
+
top: `${i * 7.5}%`,
|
|
347
|
+
left: `${i * 7.5}%`,
|
|
348
|
+
background: `radial-gradient(circle, ${color} 0%, transparent 70%)`,
|
|
349
|
+
opacity: opacity * (1 - i * 0.15),
|
|
350
|
+
transition: `transform ${0.1 + i * 0.05}s ease-out`,
|
|
351
|
+
}}
|
|
352
|
+
/>
|
|
353
|
+
))}
|
|
354
|
+
</>
|
|
355
|
+
);
|
package/src/animations/index.ts
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativePlayer - Lightweight native HTML5 video player
|
|
3
|
+
* For demo videos, background videos, autoplay loop muted scenarios
|
|
4
|
+
* Use VideoPlayer (Vidstack) for full-featured player with controls
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
|
|
10
|
+
|
|
11
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
12
|
+
import { Preloader, AspectRatio } from '@djangocfg/ui-core';
|
|
13
|
+
|
|
14
|
+
import type { NativePlayerProps, NativePlayerRef } from './types';
|
|
15
|
+
|
|
16
|
+
export const NativePlayer = forwardRef<NativePlayerRef, NativePlayerProps>(
|
|
17
|
+
(
|
|
18
|
+
{
|
|
19
|
+
src,
|
|
20
|
+
poster,
|
|
21
|
+
aspectRatio = 16 / 9,
|
|
22
|
+
autoPlay = true,
|
|
23
|
+
muted = true,
|
|
24
|
+
loop = true,
|
|
25
|
+
playsInline = true,
|
|
26
|
+
preload = 'auto',
|
|
27
|
+
controls = false,
|
|
28
|
+
disableContextMenu = true,
|
|
29
|
+
showPreloader = true,
|
|
30
|
+
preloaderTimeout = 5000,
|
|
31
|
+
className,
|
|
32
|
+
videoClassName,
|
|
33
|
+
preloaderClassName,
|
|
34
|
+
onLoadStart,
|
|
35
|
+
onCanPlay,
|
|
36
|
+
onPlaying,
|
|
37
|
+
onEnded,
|
|
38
|
+
onError,
|
|
39
|
+
},
|
|
40
|
+
ref
|
|
41
|
+
) => {
|
|
42
|
+
const [isLoading, setIsLoading] = useState(showPreloader);
|
|
43
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
44
|
+
|
|
45
|
+
// Expose video element methods via ref
|
|
46
|
+
useImperativeHandle(ref, () => ({
|
|
47
|
+
play: () => videoRef.current?.play(),
|
|
48
|
+
pause: () => videoRef.current?.pause(),
|
|
49
|
+
get currentTime() {
|
|
50
|
+
return videoRef.current?.currentTime ?? 0;
|
|
51
|
+
},
|
|
52
|
+
set currentTime(time: number) {
|
|
53
|
+
if (videoRef.current) videoRef.current.currentTime = time;
|
|
54
|
+
},
|
|
55
|
+
get paused() {
|
|
56
|
+
return videoRef.current?.paused ?? true;
|
|
57
|
+
},
|
|
58
|
+
get element() {
|
|
59
|
+
return videoRef.current;
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!showPreloader) return;
|
|
65
|
+
|
|
66
|
+
const video = videoRef.current;
|
|
67
|
+
if (!video) return;
|
|
68
|
+
|
|
69
|
+
// Check if video is already loaded (readyState >= HAVE_FUTURE_DATA)
|
|
70
|
+
if (video.readyState >= 3) {
|
|
71
|
+
setIsLoading(false);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const hideLoader = () => setIsLoading(false);
|
|
76
|
+
|
|
77
|
+
// Listen to multiple events for better browser support
|
|
78
|
+
video.addEventListener('canplay', hideLoader);
|
|
79
|
+
video.addEventListener('loadeddata', hideLoader);
|
|
80
|
+
video.addEventListener('playing', hideLoader);
|
|
81
|
+
|
|
82
|
+
// Fallback: hide loader after timeout even if video fails (shows poster instead)
|
|
83
|
+
const timeout = setTimeout(hideLoader, preloaderTimeout);
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
video.removeEventListener('canplay', hideLoader);
|
|
87
|
+
video.removeEventListener('loadeddata', hideLoader);
|
|
88
|
+
video.removeEventListener('playing', hideLoader);
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
};
|
|
91
|
+
}, [showPreloader, preloaderTimeout]);
|
|
92
|
+
|
|
93
|
+
const handleContextMenu = (e: React.MouseEvent) => {
|
|
94
|
+
if (disableContextMenu) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className={cn('relative overflow-hidden', className)}>
|
|
101
|
+
<AspectRatio ratio={aspectRatio}>
|
|
102
|
+
{/* Preloader */}
|
|
103
|
+
{showPreloader && isLoading && (
|
|
104
|
+
<div
|
|
105
|
+
className={cn(
|
|
106
|
+
'absolute inset-0 flex items-center justify-center bg-muted/30 backdrop-blur-sm z-10',
|
|
107
|
+
preloaderClassName
|
|
108
|
+
)}
|
|
109
|
+
>
|
|
110
|
+
<Preloader size="lg" spinnerClassName="text-white" />
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
{/* Video */}
|
|
115
|
+
<video
|
|
116
|
+
ref={videoRef}
|
|
117
|
+
className={cn('w-full h-full object-cover', videoClassName)}
|
|
118
|
+
autoPlay={autoPlay}
|
|
119
|
+
muted={muted}
|
|
120
|
+
loop={loop}
|
|
121
|
+
playsInline={playsInline}
|
|
122
|
+
preload={preload}
|
|
123
|
+
controls={controls}
|
|
124
|
+
poster={poster}
|
|
125
|
+
onContextMenu={handleContextMenu}
|
|
126
|
+
onLoadStart={onLoadStart}
|
|
127
|
+
onCanPlay={onCanPlay}
|
|
128
|
+
onPlaying={onPlaying}
|
|
129
|
+
onEnded={onEnded}
|
|
130
|
+
onError={onError}
|
|
131
|
+
>
|
|
132
|
+
<source src={src} type="video/mp4" />
|
|
133
|
+
Your browser does not support the video tag.
|
|
134
|
+
</video>
|
|
135
|
+
</AspectRatio>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
NativePlayer.displayName = 'NativePlayer';
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export { VideoPlayer, VideoUrlError } from './VideoPlayer';
|
|
7
|
+
export { NativePlayer } from './NativePlayer';
|
|
7
8
|
export { VideoControls } from './VideoControls';
|
|
8
|
-
export type {
|
|
9
|
+
export type {
|
|
10
|
+
VideoSource,
|
|
11
|
+
VideoPlayerProps,
|
|
12
|
+
VideoPlayerRef,
|
|
13
|
+
NativePlayerProps,
|
|
14
|
+
NativePlayerRef,
|
|
15
|
+
} from './types';
|
|
9
16
|
|
|
@@ -60,3 +60,59 @@ export interface VideoPlayerRef {
|
|
|
60
60
|
exitFullscreen: () => void;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* NativePlayer Types - Lightweight HTML5 video player
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
export interface NativePlayerProps {
|
|
68
|
+
/** Video source URL (MP4, WebM) */
|
|
69
|
+
src: string;
|
|
70
|
+
/** Poster/thumbnail image URL */
|
|
71
|
+
poster?: string;
|
|
72
|
+
/** Aspect ratio (default: 16/9) */
|
|
73
|
+
aspectRatio?: number;
|
|
74
|
+
/** Auto-play video (default: true) */
|
|
75
|
+
autoPlay?: boolean;
|
|
76
|
+
/** Mute video (default: true) */
|
|
77
|
+
muted?: boolean;
|
|
78
|
+
/** Loop video (default: true) */
|
|
79
|
+
loop?: boolean;
|
|
80
|
+
/** Play video inline on mobile (default: true) */
|
|
81
|
+
playsInline?: boolean;
|
|
82
|
+
/** Preload strategy (default: 'auto') */
|
|
83
|
+
preload?: 'auto' | 'metadata' | 'none';
|
|
84
|
+
/** Show native browser controls (default: false) */
|
|
85
|
+
controls?: boolean;
|
|
86
|
+
/** Disable right-click context menu (default: true) */
|
|
87
|
+
disableContextMenu?: boolean;
|
|
88
|
+
/** Show preloader while video loads (default: true) */
|
|
89
|
+
showPreloader?: boolean;
|
|
90
|
+
/** Preloader timeout in ms - hide even if not loaded (default: 5000) */
|
|
91
|
+
preloaderTimeout?: number;
|
|
92
|
+
/** Container className */
|
|
93
|
+
className?: string;
|
|
94
|
+
/** Video element className */
|
|
95
|
+
videoClassName?: string;
|
|
96
|
+
/** Preloader overlay className */
|
|
97
|
+
preloaderClassName?: string;
|
|
98
|
+
/** Event callbacks */
|
|
99
|
+
onLoadStart?: () => void;
|
|
100
|
+
onCanPlay?: () => void;
|
|
101
|
+
onPlaying?: () => void;
|
|
102
|
+
onEnded?: () => void;
|
|
103
|
+
onError?: (e: React.SyntheticEvent<HTMLVideoElement, Event>) => void;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface NativePlayerRef {
|
|
107
|
+
/** Play video */
|
|
108
|
+
play: () => Promise<void> | undefined;
|
|
109
|
+
/** Pause video */
|
|
110
|
+
pause: () => void;
|
|
111
|
+
/** Current playback time in seconds */
|
|
112
|
+
currentTime: number;
|
|
113
|
+
/** Whether video is paused */
|
|
114
|
+
paused: boolean;
|
|
115
|
+
/** Direct access to video element */
|
|
116
|
+
element: HTMLVideoElement | null;
|
|
117
|
+
}
|
|
118
|
+
|
package/src/tools/index.ts
CHANGED
|
@@ -39,5 +39,11 @@ export { default as OpenapiViewer } from './OpenapiViewer';
|
|
|
39
39
|
export type { PlaygroundConfig, SchemaSource, PlaygroundProps } from './OpenapiViewer';
|
|
40
40
|
|
|
41
41
|
// Export VideoPlayer
|
|
42
|
-
export { VideoPlayer, VideoUrlError, VideoControls } from './VideoPlayer';
|
|
43
|
-
export type {
|
|
42
|
+
export { VideoPlayer, VideoUrlError, VideoControls, NativePlayer } from './VideoPlayer';
|
|
43
|
+
export type {
|
|
44
|
+
VideoSource,
|
|
45
|
+
VideoPlayerProps,
|
|
46
|
+
VideoPlayerRef,
|
|
47
|
+
NativePlayerProps,
|
|
48
|
+
NativePlayerRef,
|
|
49
|
+
} from './VideoPlayer';
|