@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.56",
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.56",
62
- "@djangocfg/ui-core": "^2.1.56",
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.56",
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
+ );
@@ -1,2 +1,5 @@
1
1
  export { AnimatedBackground } from './AnimatedBackground';
2
2
  export type { BackgroundVariant } from './AnimatedBackground';
3
+
4
+ export { MouseFollower } from './MouseFollower';
5
+ export type { MouseFollowerVariant } from './MouseFollower';
@@ -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 { VideoSource, VideoPlayerProps, VideoPlayerRef } from './types';
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
+
@@ -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 { VideoSource, VideoPlayerProps, VideoPlayerRef } from './VideoPlayer';
42
+ export { VideoPlayer, VideoUrlError, VideoControls, NativePlayer } from './VideoPlayer';
43
+ export type {
44
+ VideoSource,
45
+ VideoPlayerProps,
46
+ VideoPlayerRef,
47
+ NativePlayerProps,
48
+ NativePlayerRef,
49
+ } from './VideoPlayer';