@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.
Files changed (174) hide show
  1. package/dist/LottiePlayer.client-LBEC2JKY.mjs +161 -0
  2. package/dist/LottiePlayer.client-LBEC2JKY.mjs.map +1 -0
  3. package/dist/LottiePlayer.client-WFMG2OOW.cjs +168 -0
  4. package/dist/LottiePlayer.client-WFMG2OOW.cjs.map +1 -0
  5. package/dist/Mermaid.client-4TU2TSH3.mjs +477 -0
  6. package/dist/Mermaid.client-4TU2TSH3.mjs.map +1 -0
  7. package/dist/Mermaid.client-SBYY364Q.cjs +483 -0
  8. package/dist/Mermaid.client-SBYY364Q.cjs.map +1 -0
  9. package/dist/PlaygroundLayout-3YVSAEAF.cjs +1003 -0
  10. package/dist/PlaygroundLayout-3YVSAEAF.cjs.map +1 -0
  11. package/dist/PlaygroundLayout-4DYBORAS.mjs +996 -0
  12. package/dist/PlaygroundLayout-4DYBORAS.mjs.map +1 -0
  13. package/dist/PrettyCode.client-LCBPPTIX.mjs +152 -0
  14. package/dist/PrettyCode.client-LCBPPTIX.mjs.map +1 -0
  15. package/dist/PrettyCode.client-PNPLXRH6.cjs +154 -0
  16. package/dist/PrettyCode.client-PNPLXRH6.cjs.map +1 -0
  17. package/dist/chunk-37ZI6VD4.mjs +12 -0
  18. package/dist/chunk-37ZI6VD4.mjs.map +1 -0
  19. package/dist/chunk-3HK2OE62.cjs +81 -0
  20. package/dist/chunk-3HK2OE62.cjs.map +1 -0
  21. package/dist/chunk-7DGDQVQW.cjs +591 -0
  22. package/dist/chunk-7DGDQVQW.cjs.map +1 -0
  23. package/dist/chunk-M6P2FU7L.mjs +572 -0
  24. package/dist/chunk-M6P2FU7L.mjs.map +1 -0
  25. package/dist/chunk-UQ3XI5MY.cjs +15 -0
  26. package/dist/chunk-UQ3XI5MY.cjs.map +1 -0
  27. package/dist/chunk-YFRNE2IR.mjs +79 -0
  28. package/dist/chunk-YFRNE2IR.mjs.map +1 -0
  29. package/dist/index.cjs +5042 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.cts +1591 -0
  32. package/dist/index.d.ts +1591 -0
  33. package/dist/index.mjs +4941 -0
  34. package/dist/index.mjs.map +1 -0
  35. package/package.json +86 -0
  36. package/src/components/markdown/MarkdownMessage.tsx +340 -0
  37. package/src/components/markdown/index.ts +5 -0
  38. package/src/index.ts +26 -0
  39. package/src/stores/index.ts +9 -0
  40. package/src/stores/mediaCache.ts +534 -0
  41. package/src/tools/AudioPlayer/README.md +206 -0
  42. package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +216 -0
  43. package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +280 -0
  44. package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
  45. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +149 -0
  46. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
  47. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
  48. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
  49. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
  50. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
  51. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
  52. package/src/tools/AudioPlayer/components/index.ts +22 -0
  53. package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +158 -0
  54. package/src/tools/AudioPlayer/context/index.ts +16 -0
  55. package/src/tools/AudioPlayer/effects/index.ts +412 -0
  56. package/src/tools/AudioPlayer/hooks/index.ts +35 -0
  57. package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
  58. package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
  59. package/src/tools/AudioPlayer/hooks/useVisualization.tsx +207 -0
  60. package/src/tools/AudioPlayer/index.ts +133 -0
  61. package/src/tools/AudioPlayer/types/effects.ts +73 -0
  62. package/src/tools/AudioPlayer/types/index.ts +27 -0
  63. package/src/tools/AudioPlayer/utils/debug.ts +14 -0
  64. package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
  65. package/src/tools/AudioPlayer/utils/index.ts +6 -0
  66. package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
  67. package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
  68. package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
  69. package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
  70. package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
  71. package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
  72. package/src/tools/ImageViewer/README.md +200 -0
  73. package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
  74. package/src/tools/ImageViewer/components/ImageToolbar.tsx +145 -0
  75. package/src/tools/ImageViewer/components/ImageViewer.tsx +241 -0
  76. package/src/tools/ImageViewer/components/index.ts +7 -0
  77. package/src/tools/ImageViewer/hooks/index.ts +9 -0
  78. package/src/tools/ImageViewer/hooks/useImageLoading.ts +204 -0
  79. package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
  80. package/src/tools/ImageViewer/index.ts +60 -0
  81. package/src/tools/ImageViewer/types.ts +81 -0
  82. package/src/tools/ImageViewer/utils/constants.ts +59 -0
  83. package/src/tools/ImageViewer/utils/debug.ts +14 -0
  84. package/src/tools/ImageViewer/utils/index.ts +17 -0
  85. package/src/tools/ImageViewer/utils/lqip.ts +47 -0
  86. package/src/tools/JsonForm/JsonSchemaForm.tsx +197 -0
  87. package/src/tools/JsonForm/examples/BotConfigExample.tsx +249 -0
  88. package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +161 -0
  89. package/src/tools/JsonForm/index.ts +46 -0
  90. package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +47 -0
  91. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +74 -0
  92. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +107 -0
  93. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +35 -0
  94. package/src/tools/JsonForm/templates/FieldTemplate.tsx +62 -0
  95. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +116 -0
  96. package/src/tools/JsonForm/templates/index.ts +12 -0
  97. package/src/tools/JsonForm/types.ts +83 -0
  98. package/src/tools/JsonForm/utils.ts +213 -0
  99. package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +37 -0
  100. package/src/tools/JsonForm/widgets/ColorWidget.tsx +219 -0
  101. package/src/tools/JsonForm/widgets/NumberWidget.tsx +89 -0
  102. package/src/tools/JsonForm/widgets/SelectWidget.tsx +97 -0
  103. package/src/tools/JsonForm/widgets/SliderWidget.tsx +148 -0
  104. package/src/tools/JsonForm/widgets/SwitchWidget.tsx +35 -0
  105. package/src/tools/JsonForm/widgets/TextWidget.tsx +96 -0
  106. package/src/tools/JsonForm/widgets/index.ts +14 -0
  107. package/src/tools/JsonTree/index.tsx +243 -0
  108. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +213 -0
  109. package/src/tools/LottiePlayer/index.tsx +56 -0
  110. package/src/tools/LottiePlayer/types.ts +108 -0
  111. package/src/tools/LottiePlayer/useLottie.ts +164 -0
  112. package/src/tools/Mermaid/Mermaid.client.tsx +82 -0
  113. package/src/tools/Mermaid/components/MermaidCodeViewer.tsx +95 -0
  114. package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +103 -0
  115. package/src/tools/Mermaid/hooks/index.ts +4 -0
  116. package/src/tools/Mermaid/hooks/useMermaidCleanup.ts +73 -0
  117. package/src/tools/Mermaid/hooks/useMermaidFullscreen.ts +46 -0
  118. package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +226 -0
  119. package/src/tools/Mermaid/hooks/useMermaidValidation.ts +29 -0
  120. package/src/tools/Mermaid/index.tsx +44 -0
  121. package/src/tools/Mermaid/utils/mermaid-helpers.ts +33 -0
  122. package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +149 -0
  123. package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +263 -0
  124. package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +125 -0
  125. package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +100 -0
  126. package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +157 -0
  127. package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
  128. package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +173 -0
  129. package/src/tools/OpenapiViewer/components/VersionSelector.tsx +68 -0
  130. package/src/tools/OpenapiViewer/components/index.ts +14 -0
  131. package/src/tools/OpenapiViewer/constants.ts +39 -0
  132. package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +337 -0
  133. package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
  134. package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
  135. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +199 -0
  136. package/src/tools/OpenapiViewer/index.tsx +37 -0
  137. package/src/tools/OpenapiViewer/types.ts +151 -0
  138. package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
  139. package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
  140. package/src/tools/OpenapiViewer/utils/index.ts +9 -0
  141. package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
  142. package/src/tools/PrettyCode/PrettyCode.client.tsx +208 -0
  143. package/src/tools/PrettyCode/index.tsx +47 -0
  144. package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
  145. package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
  146. package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
  147. package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
  148. package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
  149. package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
  150. package/src/tools/VideoPlayer/README.md +264 -0
  151. package/src/tools/VideoPlayer/components/VideoControls.tsx +138 -0
  152. package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +172 -0
  153. package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
  154. package/src/tools/VideoPlayer/components/index.ts +14 -0
  155. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
  156. package/src/tools/VideoPlayer/context/index.ts +8 -0
  157. package/src/tools/VideoPlayer/hooks/index.ts +12 -0
  158. package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +70 -0
  159. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +116 -0
  160. package/src/tools/VideoPlayer/index.ts +77 -0
  161. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +284 -0
  162. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +505 -0
  163. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +400 -0
  164. package/src/tools/VideoPlayer/providers/index.ts +8 -0
  165. package/src/tools/VideoPlayer/types/index.ts +38 -0
  166. package/src/tools/VideoPlayer/types/player.ts +116 -0
  167. package/src/tools/VideoPlayer/types/provider.ts +93 -0
  168. package/src/tools/VideoPlayer/types/sources.ts +97 -0
  169. package/src/tools/VideoPlayer/utils/debug.ts +14 -0
  170. package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
  171. package/src/tools/VideoPlayer/utils/index.ts +12 -0
  172. package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
  173. package/src/tools/_shared.ts +29 -0
  174. package/src/tools/index.ts +172 -0
@@ -0,0 +1,264 @@
1
+ # VideoPlayer
2
+
3
+ Unified video player component supporting multiple modes and source types.
4
+
5
+ ## Modes
6
+
7
+ | Mode | Source Types | Use Case |
8
+ |------|-------------|----------|
9
+ | `vidstack` | YouTube, Vimeo, HLS, DASH | Full-featured player with themes and controls |
10
+ | `native` | URL, data-url | Lightweight HTML5 player |
11
+ | `streaming` | stream, blob | HTTP Range streaming with auth, binary data |
12
+
13
+ Mode is auto-detected from source type, or can be forced via `mode` prop.
14
+
15
+ ## Installation
16
+
17
+ ```tsx
18
+ import {
19
+ VideoPlayer,
20
+ VideoPlayerProvider,
21
+ VideoErrorFallback,
22
+ resolveFileSource
23
+ } from '@djangocfg/ui-nextjs';
24
+ ```
25
+
26
+ ## Basic Usage
27
+
28
+ ### YouTube / Vimeo
29
+
30
+ ```tsx
31
+ <VideoPlayer source={{ type: 'youtube', id: 'dQw4w9WgXcQ' }} />
32
+ <VideoPlayer source={{ type: 'vimeo', id: '123456789' }} />
33
+ ```
34
+
35
+ ### HLS / DASH Streaming
36
+
37
+ ```tsx
38
+ <VideoPlayer source={{ type: 'hls', url: 'https://example.com/video.m3u8' }} />
39
+ <VideoPlayer source={{ type: 'dash', url: 'https://example.com/video.mpd' }} />
40
+ ```
41
+
42
+ ### Direct URL
43
+
44
+ ```tsx
45
+ <VideoPlayer source={{ type: 'url', url: 'https://example.com/video.mp4' }} />
46
+ ```
47
+
48
+ ### HTTP Range Streaming (with auth)
49
+
50
+ ```tsx
51
+ // Full source (standalone)
52
+ <VideoPlayer
53
+ source={{
54
+ type: 'stream',
55
+ sessionId: 'abc123',
56
+ path: '/videos/movie.mp4',
57
+ getStreamUrl: (id, path) => `/api/stream/${id}?path=${path}&token=${token}`
58
+ }}
59
+ />
60
+
61
+ // Simplified source (with context)
62
+ <VideoPlayerProvider sessionId={sessionId} getStreamUrl={getStreamUrl}>
63
+ <VideoPlayer source={{ type: 'stream', path: '/videos/movie.mp4' }} />
64
+ </VideoPlayerProvider>
65
+ ```
66
+
67
+ ### Blob / ArrayBuffer
68
+
69
+ ```tsx
70
+ <VideoPlayer
71
+ source={{
72
+ type: 'blob',
73
+ data: arrayBuffer,
74
+ mimeType: 'video/mp4'
75
+ }}
76
+ />
77
+ ```
78
+
79
+ ## Props
80
+
81
+ | Prop | Type | Default | Description |
82
+ |------|------|---------|-------------|
83
+ | `source` | `VideoSourceUnion` | required | Video source configuration |
84
+ | `mode` | `'auto' \| 'vidstack' \| 'native' \| 'streaming'` | `'auto'` | Force specific player mode |
85
+ | `aspectRatio` | `number \| 'auto' \| 'fill'` | `16/9` | Aspect ratio or fill parent |
86
+ | `autoPlay` | `boolean` | `false` | Auto-play on load |
87
+ | `muted` | `boolean` | `false` | Muted by default |
88
+ | `loop` | `boolean` | `false` | Loop playback |
89
+ | `controls` | `boolean` | `true` | Show player controls |
90
+ | `playsInline` | `boolean` | `true` | Inline playback on mobile |
91
+ | `preload` | `'none' \| 'metadata' \| 'auto'` | `'metadata'` | Preload strategy |
92
+ | `theme` | `'default' \| 'minimal' \| 'modern'` | `'default'` | Vidstack theme |
93
+ | `errorFallback` | `ReactNode \| (props) => ReactNode` | - | Custom error UI |
94
+ | `className` | `string` | - | Container className |
95
+ | `videoClassName` | `string` | - | Video element className |
96
+
97
+ ## Events
98
+
99
+ | Event | Type | Description |
100
+ |-------|------|-------------|
101
+ | `onPlay` | `() => void` | Playback started |
102
+ | `onPause` | `() => void` | Playback paused |
103
+ | `onEnded` | `() => void` | Playback ended |
104
+ | `onError` | `(error: string) => void` | Error occurred |
105
+ | `onLoadStart` | `() => void` | Loading started |
106
+ | `onCanPlay` | `() => void` | Ready to play |
107
+ | `onTimeUpdate` | `(time: number, duration: number) => void` | Time updated |
108
+
109
+ ## Ref API
110
+
111
+ ```tsx
112
+ const playerRef = useRef<VideoPlayerRef>(null);
113
+
114
+ <VideoPlayer ref={playerRef} source={source} />
115
+
116
+ // Control methods
117
+ playerRef.current?.play();
118
+ playerRef.current?.pause();
119
+ playerRef.current?.seek(30); // Seek to 30 seconds
120
+ playerRef.current?.setVolume(0.5); // 0-1
121
+ playerRef.current?.setMuted(true);
122
+ playerRef.current?.reload();
123
+ ```
124
+
125
+ ## Error Handling
126
+
127
+ ### Custom Error Fallback
128
+
129
+ ```tsx
130
+ <VideoPlayer
131
+ source={source}
132
+ errorFallback={(props) => (
133
+ <div>
134
+ <p>Error: {props.error}</p>
135
+ <button onClick={props.retry}>Retry</button>
136
+ </div>
137
+ )}
138
+ />
139
+ ```
140
+
141
+ ### Pre-built Error Fallback with Download
142
+
143
+ ```tsx
144
+ import { VideoErrorFallback } from '@djangocfg/ui-nextjs';
145
+
146
+ <VideoPlayer
147
+ source={source}
148
+ errorFallback={(props) => (
149
+ <VideoErrorFallback
150
+ {...props}
151
+ downloadUrl={getDownloadUrl()}
152
+ downloadFilename="video.mp4"
153
+ fileSize="125 MB"
154
+ />
155
+ )}
156
+ />
157
+ ```
158
+
159
+ ## Fill Mode
160
+
161
+ Use `aspectRatio="fill"` to fill the parent container:
162
+
163
+ ```tsx
164
+ <div className="absolute inset-0">
165
+ <VideoPlayer source={source} aspectRatio="fill" />
166
+ </div>
167
+ ```
168
+
169
+ ## Context Provider
170
+
171
+ For apps with multiple streaming videos, use the context to avoid repetition:
172
+
173
+ ```tsx
174
+ // In layout or parent component
175
+ <VideoPlayerProvider
176
+ sessionId={sessionId}
177
+ getStreamUrl={(id, path) => `/api/stream/${id}?path=${path}`}
178
+ >
179
+ {/* All nested VideoPlayers use this config */}
180
+ <VideoPlayer source={{ type: 'stream', path: '/video1.mp4' }} />
181
+ <VideoPlayer source={{ type: 'stream', path: '/video2.mp4' }} />
182
+ </VideoPlayerProvider>
183
+ ```
184
+
185
+ ## File Source Helper
186
+
187
+ For file browser integration:
188
+
189
+ ```tsx
190
+ import { resolveFileSource } from '@djangocfg/ui-nextjs';
191
+
192
+ const source = resolveFileSource({
193
+ file: { name: 'movie.mp4', path: '/videos/movie.mp4' },
194
+ sessionId: 'abc123',
195
+ getStreamUrl: (id, path) => `/api/stream/${id}?path=${path}`,
196
+ });
197
+
198
+ if (source) {
199
+ return <VideoPlayer source={source} />;
200
+ }
201
+ ```
202
+
203
+ ## Source Types Reference
204
+
205
+ ```typescript
206
+ type VideoSourceUnion =
207
+ | { type: 'url'; url: string; title?: string; poster?: string }
208
+ | { type: 'youtube'; id: string; title?: string; poster?: string }
209
+ | { type: 'vimeo'; id: string; title?: string; poster?: string }
210
+ | { type: 'hls'; url: string; title?: string; poster?: string }
211
+ | { type: 'dash'; url: string; title?: string; poster?: string }
212
+ | { type: 'stream'; sessionId: string; path: string; getStreamUrl: fn; mimeType?: string }
213
+ | { type: 'blob'; data: ArrayBuffer | Blob; mimeType?: string }
214
+ | { type: 'data-url'; data: string; mimeType?: string }
215
+ ```
216
+
217
+ ## Architecture
218
+
219
+ ```
220
+ VideoPlayer/
221
+ ├── index.ts # Public API exports
222
+ ├── types/
223
+ │ ├── index.ts # Type re-exports
224
+ │ ├── sources.ts # Source types (Url, YouTube, HLS, etc.)
225
+ │ ├── player.ts # Player props, ref, events
226
+ │ └── provider.ts # Provider & context types
227
+ ├── hooks/
228
+ │ ├── index.ts
229
+ │ └── useVideoPositionCache.ts # Playback position caching
230
+ ├── utils/
231
+ │ ├── index.ts
232
+ │ ├── resolvers.ts # resolvePlayerMode, isSimpleStreamSource
233
+ │ └── fileSource.ts # resolveFileSource helper
234
+ ├── components/
235
+ │ ├── index.ts
236
+ │ ├── VideoPlayer.tsx # Main orchestrator
237
+ │ ├── VideoControls.tsx # Standalone Vidstack controls
238
+ │ └── VideoErrorFallback.tsx # Pre-built error UI
239
+ ├── context/
240
+ │ ├── index.ts
241
+ │ └── VideoPlayerContext.tsx # Streaming config provider
242
+ └── providers/
243
+ ├── index.ts
244
+ ├── VidstackProvider.tsx # YouTube, Vimeo, HLS, DASH
245
+ ├── NativeProvider.tsx # HTML5 <video>
246
+ └── StreamProvider.tsx # HTTP Range, Blob
247
+ ```
248
+
249
+ ## Accessibility
250
+
251
+ Full accessibility support:
252
+ - Keyboard navigation (Space, Arrow keys, F for fullscreen)
253
+ - Screen reader announcements
254
+ - Focus indicators
255
+ - ARIA labels and roles
256
+
257
+ ## Browser Support
258
+
259
+ - Chrome 63+
260
+ - Firefox 67+
261
+ - Safari 12+
262
+ - Edge 79+
263
+ - iOS Safari 12+
264
+ - Chrome Android 63+
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Custom Video Controls for Vidstack Player
3
+ */
4
+
5
+ 'use client';
6
+
7
+ import { Maximize, Minimize, Pause, Play, Volume2, VolumeX } from 'lucide-react';
8
+ import React from 'react';
9
+
10
+ import { cn } from '@djangocfg/ui-core/lib';
11
+ import { useMediaRemote, useMediaStore } from '@vidstack/react';
12
+
13
+ import type { MediaPlayerInstance } from '@vidstack/react';
14
+ interface VideoControlsProps {
15
+ player: React.RefObject<MediaPlayerInstance | null>;
16
+ className?: string;
17
+ }
18
+
19
+ export function VideoControls({ player, className }: VideoControlsProps) {
20
+ const store = useMediaStore(player);
21
+ const remote = useMediaRemote();
22
+
23
+ const isPaused = store.paused;
24
+ const isMuted = store.muted;
25
+ const isFullscreen = store.fullscreen;
26
+ const currentTime = store.currentTime;
27
+ const duration = store.duration;
28
+ const volume = store.volume;
29
+
30
+ const formatTime = (seconds: number): string => {
31
+ if (!seconds || seconds < 0) return '0:00';
32
+ const minutes = Math.floor(seconds / 60);
33
+ const secs = Math.floor(seconds % 60);
34
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
35
+ };
36
+
37
+ const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
38
+ if (!duration) return;
39
+ const rect = e.currentTarget.getBoundingClientRect();
40
+ const clickX = e.clientX - rect.left;
41
+ const percentage = clickX / rect.width;
42
+ const newTime = percentage * duration;
43
+ remote.seek(newTime);
44
+ };
45
+
46
+ const handleVolumeChange = (e: React.MouseEvent<HTMLDivElement>) => {
47
+ const rect = e.currentTarget.getBoundingClientRect();
48
+ const clickX = e.clientX - rect.left;
49
+ const percentage = Math.max(0, Math.min(1, clickX / rect.width));
50
+ remote.changeVolume(percentage);
51
+ if (percentage > 0 && isMuted) {
52
+ remote.toggleMuted();
53
+ }
54
+ };
55
+
56
+ const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
57
+
58
+ return (
59
+ <div
60
+ className={cn(
61
+ "absolute inset-0 flex flex-col justify-end transition-opacity duration-300",
62
+ "bg-gradient-to-t from-black/80 via-black/20 to-transparent",
63
+ "opacity-0 group-hover:opacity-100 focus-within:opacity-100",
64
+ "pointer-events-none group-hover:pointer-events-auto",
65
+ className
66
+ )}
67
+ >
68
+ {/* Progress Bar */}
69
+ <div className="px-4 pb-2 pointer-events-auto">
70
+ <div
71
+ className="h-1.5 bg-white/20 rounded-full cursor-pointer hover:h-2 transition-all group"
72
+ onClick={handleProgressClick}
73
+ >
74
+ <div
75
+ className="h-full bg-primary rounded-full transition-all relative group-hover:bg-primary/90"
76
+ style={{ width: `${progress}%` }}
77
+ >
78
+ <div className="absolute right-0 top-1/2 -translate-y-1/2 w-3 h-3 bg-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity" />
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ {/* Control Bar */}
84
+ <div className="flex items-center gap-4 px-4 pb-4 pointer-events-auto">
85
+ {/* Play/Pause */}
86
+ <button
87
+ onClick={() => remote.togglePaused()}
88
+ className="text-white hover:text-primary transition-colors p-1.5 hover:bg-white/10 rounded-full"
89
+ >
90
+ {isPaused ? <Play className="h-6 w-6" /> : <Pause className="h-6 w-6" />}
91
+ </button>
92
+
93
+ {/* Time */}
94
+ <div className="text-white text-sm font-medium">
95
+ {formatTime(currentTime)} / {formatTime(duration)}
96
+ </div>
97
+
98
+ <div className="flex-1" />
99
+
100
+ {/* Volume Control */}
101
+ <div className="flex items-center gap-2 group/volume">
102
+ <button
103
+ onClick={() => remote.toggleMuted()}
104
+ className="text-white hover:text-primary transition-colors p-1.5 hover:bg-white/10 rounded-full"
105
+ >
106
+ {isMuted || volume === 0 ? (
107
+ <VolumeX className="h-5 w-5" />
108
+ ) : (
109
+ <Volume2 className="h-5 w-5" />
110
+ )}
111
+ </button>
112
+
113
+ <div
114
+ className="w-0 group-hover/volume:w-20 transition-all overflow-hidden"
115
+ >
116
+ <div
117
+ className="h-1.5 bg-white/20 rounded-full cursor-pointer hover:h-2 transition-all"
118
+ onClick={handleVolumeChange}
119
+ >
120
+ <div
121
+ className="h-full bg-white rounded-full transition-all"
122
+ style={{ width: `${volume * 100}%` }}
123
+ />
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ {/* Fullscreen */}
129
+ <button
130
+ onClick={() => isFullscreen ? remote.exitFullscreen() : remote.enterFullscreen()}
131
+ className="text-white hover:text-primary transition-colors p-1.5 hover:bg-white/10 rounded-full"
132
+ >
133
+ {isFullscreen ? <Minimize className="h-5 w-5" /> : <Maximize className="h-5 w-5" />}
134
+ </button>
135
+ </div>
136
+ </div>
137
+ );
138
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * VideoErrorFallback - Pre-built error fallback with download button
3
+ * For use with VideoPlayer errorFallback prop
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React from 'react';
9
+ import { FileVideo, RefreshCw } from 'lucide-react';
10
+
11
+ import { cn, Button, DownloadButton } from '../../_shared';
12
+
13
+ import type { ErrorFallbackProps } from '../types';
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ export interface VideoErrorFallbackProps extends ErrorFallbackProps {
20
+ /** URL for download button (if provided, shows download button) */
21
+ downloadUrl?: string;
22
+ /** Filename for download */
23
+ downloadFilename?: string;
24
+ /** File size to display */
25
+ fileSize?: string;
26
+ /** Show retry button */
27
+ showRetry?: boolean;
28
+ /** Custom className */
29
+ className?: string;
30
+ /** Custom icon */
31
+ icon?: React.ReactNode;
32
+ /** Custom title (defaults to error message) */
33
+ title?: string;
34
+ /** Custom description */
35
+ description?: string;
36
+ }
37
+
38
+ // ============================================================================
39
+ // Component
40
+ // ============================================================================
41
+
42
+ /**
43
+ * Pre-built error fallback component for VideoPlayer
44
+ *
45
+ * @example
46
+ * // Basic usage
47
+ * <VideoPlayer
48
+ * source={source}
49
+ * errorFallback={(props) => (
50
+ * <VideoErrorFallback
51
+ * {...props}
52
+ * downloadUrl={getDownloadUrl()}
53
+ * downloadFilename="video.mp4"
54
+ * />
55
+ * )}
56
+ * />
57
+ *
58
+ * @example
59
+ * // With file size
60
+ * <VideoErrorFallback
61
+ * error="Failed to load video"
62
+ * downloadUrl="/api/download/video.mp4"
63
+ * fileSize="125 MB"
64
+ * showRetry
65
+ * retry={() => reloadVideo()}
66
+ * />
67
+ */
68
+ export function VideoErrorFallback({
69
+ error,
70
+ retry,
71
+ downloadUrl,
72
+ downloadFilename,
73
+ fileSize,
74
+ showRetry = true,
75
+ className,
76
+ icon,
77
+ title,
78
+ description,
79
+ }: VideoErrorFallbackProps) {
80
+ const displayTitle = title || error || 'Video cannot be previewed';
81
+
82
+ return (
83
+ <div
84
+ className={cn(
85
+ 'absolute inset-0 flex flex-col items-center justify-center gap-4 bg-black/90 text-white p-6',
86
+ className
87
+ )}
88
+ >
89
+ {/* Icon */}
90
+ {icon || <FileVideo className="w-16 h-16 text-muted-foreground" />}
91
+
92
+ {/* Title */}
93
+ <p className="text-lg font-medium text-center">{displayTitle}</p>
94
+
95
+ {/* Description / File size */}
96
+ {(description || fileSize) && (
97
+ <p className="text-sm text-muted-foreground text-center">
98
+ {description || fileSize}
99
+ </p>
100
+ )}
101
+
102
+ {/* Actions */}
103
+ <div className="flex items-center gap-3 mt-2">
104
+ {/* Retry button */}
105
+ {showRetry && retry && (
106
+ <Button
107
+ variant="outline"
108
+ size="sm"
109
+ onClick={retry}
110
+ className="gap-2"
111
+ >
112
+ <RefreshCw className="w-4 h-4" />
113
+ Retry
114
+ </Button>
115
+ )}
116
+
117
+ {/* Download button */}
118
+ {downloadUrl && (
119
+ <DownloadButton
120
+ url={downloadUrl}
121
+ filename={downloadFilename}
122
+ variant="default"
123
+ size="sm"
124
+ >
125
+ Download to view
126
+ </DownloadButton>
127
+ )}
128
+ </div>
129
+ </div>
130
+ );
131
+ }
132
+
133
+ // ============================================================================
134
+ // Factory for common use cases
135
+ // ============================================================================
136
+
137
+ export interface CreateVideoErrorFallbackOptions {
138
+ /** Function to get download URL from source */
139
+ getDownloadUrl?: (source: unknown) => string | undefined;
140
+ /** Function to get filename from source */
141
+ getFilename?: (source: unknown) => string | undefined;
142
+ /** Function to get file size from source */
143
+ getFileSize?: (source: unknown) => string | undefined;
144
+ /** Show retry button */
145
+ showRetry?: boolean;
146
+ }
147
+
148
+ /**
149
+ * Factory to create error fallback function for VideoPlayer
150
+ *
151
+ * @example
152
+ * const errorFallback = createVideoErrorFallback({
153
+ * getDownloadUrl: (source) => source.downloadUrl,
154
+ * getFilename: (source) => source.filename,
155
+ * showRetry: true,
156
+ * });
157
+ *
158
+ * <VideoPlayer source={source} errorFallback={errorFallback} />
159
+ */
160
+ export function createVideoErrorFallback(
161
+ options: CreateVideoErrorFallbackOptions
162
+ ): (props: ErrorFallbackProps, source?: unknown) => React.ReactNode {
163
+ return (props: ErrorFallbackProps, source?: unknown) => (
164
+ <VideoErrorFallback
165
+ {...props}
166
+ downloadUrl={options.getDownloadUrl?.(source)}
167
+ downloadFilename={options.getFilename?.(source)}
168
+ fileSize={options.getFileSize?.(source)}
169
+ showRetry={options.showRetry}
170
+ />
171
+ );
172
+ }