@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,279 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * HybridWaveform - Real-time frequency visualization for hybrid player.
5
+ *
6
+ * Two modes:
7
+ * - 'frequency': Real-time frequency bars (default, requires playing audio)
8
+ * - 'static': Static progress bar (for when no analyser is available)
9
+ *
10
+ * Features:
11
+ * - Shows buffered regions
12
+ * - Click to seek
13
+ * - Responsive width
14
+ */
15
+
16
+ import { useRef, useEffect, useCallback, memo } from 'react';
17
+ import { useHybridAudioContext } from '../context/HybridAudioProvider';
18
+ import { cn } from '@djangocfg/ui-core/lib';
19
+
20
+ // =============================================================================
21
+ // TYPES
22
+ // =============================================================================
23
+
24
+ export interface HybridWaveformProps {
25
+ /** Visualization mode */
26
+ mode?: 'frequency' | 'static';
27
+ /** Canvas height in pixels */
28
+ height?: number;
29
+ /** Bar width in pixels */
30
+ barWidth?: number;
31
+ /** Gap between bars in pixels */
32
+ barGap?: number;
33
+ /** Bar border radius */
34
+ barRadius?: number;
35
+ /** Color for played portion */
36
+ progressColor?: string;
37
+ /** Color for unplayed portion */
38
+ waveColor?: string;
39
+ /** Color for buffered regions indicator */
40
+ bufferedColor?: string;
41
+ /** Additional CSS class */
42
+ className?: string;
43
+ /** Callback when user seeks */
44
+ onSeek?: (time: number) => void;
45
+ }
46
+
47
+ // =============================================================================
48
+ // COMPONENT
49
+ // =============================================================================
50
+
51
+ export const HybridWaveform = memo(function HybridWaveform({
52
+ mode = 'frequency',
53
+ height = 64,
54
+ barWidth = 3,
55
+ barGap = 2,
56
+ barRadius = 2,
57
+ progressColor = 'hsl(217 91% 60%)',
58
+ waveColor = 'hsl(217 91% 60% / 0.3)',
59
+ bufferedColor = 'hsl(217 91% 60% / 0.15)',
60
+ className,
61
+ onSeek,
62
+ }: HybridWaveformProps) {
63
+ const canvasRef = useRef<HTMLCanvasElement>(null);
64
+ const containerRef = useRef<HTMLDivElement>(null);
65
+ const animationRef = useRef<number | null>(null);
66
+ const { state, controls, webAudio } = useHybridAudioContext();
67
+
68
+ // Handle click to seek
69
+ const handleClick = useCallback(
70
+ (e: React.MouseEvent<HTMLCanvasElement>) => {
71
+ const canvas = canvasRef.current;
72
+ if (!canvas || !state.duration) return;
73
+
74
+ const rect = canvas.getBoundingClientRect();
75
+ const x = e.clientX - rect.left;
76
+ const progress = x / rect.width;
77
+ const time = state.duration * progress;
78
+
79
+ controls.seek(time);
80
+ onSeek?.(time);
81
+ },
82
+ [state.duration, controls, onSeek]
83
+ );
84
+
85
+ // Render frequency bars (real-time visualization)
86
+ const renderFrequency = useCallback(() => {
87
+ const canvas = canvasRef.current;
88
+ const analyser = webAudio.analyser;
89
+ if (!canvas) return;
90
+
91
+ const ctx = canvas.getContext('2d');
92
+ if (!ctx) return;
93
+
94
+ const { width, height: canvasHeight } = canvas;
95
+ const dpr = window.devicePixelRatio || 1;
96
+ const displayWidth = width / dpr;
97
+
98
+ // Get frequency data if analyser is available
99
+ let dataArray: Uint8Array<ArrayBuffer> | null = null;
100
+ if (analyser) {
101
+ dataArray = new Uint8Array(analyser.frequencyBinCount) as Uint8Array<ArrayBuffer>;
102
+ analyser.getByteFrequencyData(dataArray);
103
+ }
104
+
105
+ ctx.clearRect(0, 0, width, canvasHeight);
106
+
107
+ // Draw buffered regions at bottom
108
+ if (state.buffered && state.duration > 0) {
109
+ ctx.fillStyle = bufferedColor;
110
+ for (let i = 0; i < state.buffered.length; i++) {
111
+ const start = (state.buffered.start(i) / state.duration) * width;
112
+ const end = (state.buffered.end(i) / state.duration) * width;
113
+ ctx.fillRect(start, canvasHeight - 3 * dpr, end - start, 3 * dpr);
114
+ }
115
+ }
116
+
117
+ // Calculate bar count based on available width
118
+ const barCount = Math.floor(displayWidth / (barWidth + barGap));
119
+ const progress = state.duration > 0 ? state.currentTime / state.duration : 0;
120
+ const progressX = width * progress;
121
+
122
+ // Draw bars
123
+ for (let i = 0; i < barCount; i++) {
124
+ let barHeight: number;
125
+
126
+ if (dataArray && state.isPlaying) {
127
+ // Real-time frequency data
128
+ const step = Math.floor(dataArray.length / barCount);
129
+ const value = dataArray[i * step] / 255;
130
+ barHeight = Math.max(4 * dpr, value * (canvasHeight - 6 * dpr) * 0.9);
131
+ } else {
132
+ // Static fallback - small bars
133
+ barHeight = 8 * dpr;
134
+ }
135
+
136
+ const x = i * (barWidth + barGap) * dpr;
137
+ const y = (canvasHeight - barHeight) / 2;
138
+
139
+ ctx.fillStyle = x < progressX ? progressColor : waveColor;
140
+
141
+ // Draw rounded rect
142
+ const radius = barRadius * dpr;
143
+ const rectWidth = barWidth * dpr;
144
+ ctx.beginPath();
145
+ ctx.roundRect(x, y, rectWidth, barHeight, radius);
146
+ ctx.fill();
147
+ }
148
+
149
+ // Continue animation if playing
150
+ if (state.isPlaying) {
151
+ animationRef.current = requestAnimationFrame(renderFrequency);
152
+ }
153
+ }, [
154
+ webAudio.analyser,
155
+ state.buffered,
156
+ state.duration,
157
+ state.currentTime,
158
+ state.isPlaying,
159
+ barWidth,
160
+ barGap,
161
+ barRadius,
162
+ progressColor,
163
+ waveColor,
164
+ bufferedColor,
165
+ ]);
166
+
167
+ // Render static progress bar
168
+ const renderStatic = useCallback(() => {
169
+ const canvas = canvasRef.current;
170
+ if (!canvas) return;
171
+
172
+ const ctx = canvas.getContext('2d');
173
+ if (!ctx) return;
174
+
175
+ const { width, height: canvasHeight } = canvas;
176
+ const dpr = window.devicePixelRatio || 1;
177
+
178
+ ctx.clearRect(0, 0, width, canvasHeight);
179
+
180
+ // Draw buffered regions
181
+ if (state.buffered && state.duration > 0) {
182
+ ctx.fillStyle = bufferedColor;
183
+ for (let i = 0; i < state.buffered.length; i++) {
184
+ const start = (state.buffered.start(i) / state.duration) * width;
185
+ const end = (state.buffered.end(i) / state.duration) * width;
186
+ ctx.fillRect(start, 0, end - start, canvasHeight);
187
+ }
188
+ }
189
+
190
+ // Draw progress bar
191
+ const progress = state.duration > 0 ? state.currentTime / state.duration : 0;
192
+ const progressWidth = width * progress;
193
+
194
+ // Background
195
+ ctx.fillStyle = waveColor;
196
+ ctx.fillRect(0, canvasHeight / 2 - 2 * dpr, width, 4 * dpr);
197
+
198
+ // Progress
199
+ ctx.fillStyle = progressColor;
200
+ ctx.fillRect(0, canvasHeight / 2 - 2 * dpr, progressWidth, 4 * dpr);
201
+
202
+ // Handle
203
+ if (progress > 0) {
204
+ ctx.beginPath();
205
+ ctx.arc(progressWidth, canvasHeight / 2, 6 * dpr, 0, Math.PI * 2);
206
+ ctx.fill();
207
+ }
208
+ }, [state.buffered, state.duration, state.currentTime, progressColor, waveColor, bufferedColor]);
209
+
210
+ // Resize canvas to match container
211
+ useEffect(() => {
212
+ const container = containerRef.current;
213
+ const canvas = canvasRef.current;
214
+ if (!container || !canvas) return;
215
+
216
+ const resizeObserver = new ResizeObserver((entries) => {
217
+ const entry = entries[0];
218
+ if (!entry) return;
219
+
220
+ const dpr = window.devicePixelRatio || 1;
221
+ const displayWidth = entry.contentRect.width;
222
+ const displayHeight = height;
223
+
224
+ canvas.width = displayWidth * dpr;
225
+ canvas.height = displayHeight * dpr;
226
+ canvas.style.width = `${displayWidth}px`;
227
+ canvas.style.height = `${displayHeight}px`;
228
+
229
+ // Re-render after resize
230
+ if (mode === 'frequency') {
231
+ renderFrequency();
232
+ } else {
233
+ renderStatic();
234
+ }
235
+ });
236
+
237
+ resizeObserver.observe(container);
238
+ return () => resizeObserver.disconnect();
239
+ }, [height, mode, renderFrequency, renderStatic]);
240
+
241
+ // Animation loop for frequency mode
242
+ useEffect(() => {
243
+ if (mode === 'frequency') {
244
+ renderFrequency();
245
+ }
246
+
247
+ return () => {
248
+ if (animationRef.current) {
249
+ cancelAnimationFrame(animationRef.current);
250
+ }
251
+ };
252
+ }, [mode, renderFrequency]);
253
+
254
+ // Re-render on time change for static mode
255
+ useEffect(() => {
256
+ if (mode === 'static') {
257
+ renderStatic();
258
+ }
259
+ }, [mode, state.currentTime, renderStatic]);
260
+
261
+ // Re-render frequency when playback state changes
262
+ useEffect(() => {
263
+ if (mode === 'frequency' && !state.isPlaying) {
264
+ // One final render when stopped
265
+ renderFrequency();
266
+ }
267
+ }, [mode, state.isPlaying, renderFrequency]);
268
+
269
+ return (
270
+ <div ref={containerRef} className={cn('w-full', className)}>
271
+ <canvas
272
+ ref={canvasRef}
273
+ onClick={handleClick}
274
+ className="cursor-pointer"
275
+ style={{ width: '100%', height }}
276
+ />
277
+ </div>
278
+ );
279
+ });
@@ -0,0 +1,149 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * AudioReactiveCover - Album art with audio-reactive animations
5
+ *
6
+ * Uses effects utilities for clean data preparation before render.
7
+ * Click on cover to switch between effect variants.
8
+ *
9
+ * Must be used within HybridAudioProvider context.
10
+ */
11
+
12
+ import { type ReactNode } from 'react';
13
+ import { cn } from '../../../_shared';
14
+ import { useHybridAudioLevels, useHybridAudioState } from '../../context/HybridAudioProvider';
15
+ import {
16
+ type EffectVariant,
17
+ type EffectIntensity,
18
+ type EffectColorScheme,
19
+ getEffectConfig,
20
+ prepareEffectColors,
21
+ calculateGlowLayers,
22
+ calculateOrbs,
23
+ calculateMeshGradients,
24
+ calculateSpotlight,
25
+ EFFECT_ANIMATIONS,
26
+ } from '../../effects';
27
+ import { GlowEffect, OrbsEffect, SpotlightEffect, MeshEffect, type GlowEffectData } from './effects';
28
+
29
+ // =============================================================================
30
+ // TYPES
31
+ // =============================================================================
32
+
33
+ export interface AudioReactiveCoverProps {
34
+ children: ReactNode;
35
+ size?: 'sm' | 'md' | 'lg';
36
+ variant?: EffectVariant;
37
+ intensity?: EffectIntensity;
38
+ colorScheme?: EffectColorScheme;
39
+ onClick?: () => void;
40
+ className?: string;
41
+ }
42
+
43
+ // =============================================================================
44
+ // CONSTANTS
45
+ // =============================================================================
46
+
47
+ const SIZES = {
48
+ sm: { container: 'w-32 h-32', orbBase: 40 },
49
+ md: { container: 'w-40 h-40', orbBase: 50 },
50
+ lg: { container: 'w-48 h-48', orbBase: 60 },
51
+ };
52
+
53
+ // =============================================================================
54
+ // COMPONENT
55
+ // =============================================================================
56
+
57
+ export function AudioReactiveCover({
58
+ children,
59
+ size = 'lg',
60
+ variant = 'spotlight',
61
+ intensity = 'medium',
62
+ colorScheme = 'primary',
63
+ onClick,
64
+ className,
65
+ }: AudioReactiveCoverProps) {
66
+ // Get audio state from HybridAudioProvider context
67
+ const { isPlaying } = useHybridAudioState();
68
+ const levels = useHybridAudioLevels();
69
+
70
+ // =========================================================================
71
+ // PREPARE DATA BEFORE RENDER
72
+ // =========================================================================
73
+
74
+ const sizeConfig = SIZES[size];
75
+ const effectConfig = getEffectConfig(intensity);
76
+ const { colors, hueShift } = prepareEffectColors(colorScheme, levels);
77
+
78
+ // Calculate scale based on overall level
79
+ const containerScale = 1 + levels.overall * effectConfig.scale;
80
+
81
+ // Prepare effect-specific data - NO memoization for real-time reactivity
82
+ const glowData: GlowEffectData | null = variant === 'glow' ? {
83
+ layers: calculateGlowLayers(levels, effectConfig, colors),
84
+ hueShift,
85
+ showPulseRings: levels.bass > 0.5,
86
+ showSparkle: levels.high > 0.4,
87
+ } : null;
88
+
89
+ const orbsData = variant === 'orbs'
90
+ ? calculateOrbs(levels, effectConfig, colors, sizeConfig.orbBase)
91
+ : null;
92
+
93
+ const meshData = variant === 'mesh'
94
+ ? calculateMeshGradients(levels, effectConfig, colors)
95
+ : null;
96
+
97
+ const spotlightData = variant === 'spotlight'
98
+ ? calculateSpotlight(levels, effectConfig, colors, levels.mid * 360)
99
+ : null;
100
+
101
+ // =========================================================================
102
+ // RENDER
103
+ // =========================================================================
104
+
105
+ return (
106
+ <div
107
+ className={cn('relative', sizeConfig.container, className)}
108
+ style={{
109
+ transform: `scale(${containerScale})`,
110
+ transition: 'transform 0.1s ease-out',
111
+ }}
112
+ >
113
+ {/* Effect layers container - under cover, non-interactive */}
114
+ <div className="absolute inset-0 z-0 pointer-events-none overflow-visible">
115
+ {glowData && (
116
+ <GlowEffect data={glowData} colors={colors} isPlaying={isPlaying} />
117
+ )}
118
+
119
+ {orbsData && (
120
+ <OrbsEffect orbs={orbsData} blur={effectConfig.blur} isPlaying={isPlaying} />
121
+ )}
122
+
123
+ {spotlightData && (
124
+ <SpotlightEffect data={spotlightData} colors={colors} blur={effectConfig.blur} isPlaying={isPlaying} />
125
+ )}
126
+
127
+ {meshData && (
128
+ <MeshEffect gradients={meshData} blur={effectConfig.blur} isPlaying={isPlaying} />
129
+ )}
130
+ </div>
131
+
132
+ {/* Content (cover art) */}
133
+ <div
134
+ className="relative w-full h-full rounded-lg overflow-hidden shadow-2xl z-10 bg-background cursor-pointer"
135
+ onClick={onClick}
136
+ role={onClick ? 'button' : undefined}
137
+ tabIndex={onClick ? 0 : undefined}
138
+ onKeyDown={onClick ? (e) => e.key === 'Enter' && onClick() : undefined}
139
+ >
140
+ {children}
141
+ </div>
142
+
143
+ {/* Inject animations once */}
144
+ <style dangerouslySetInnerHTML={{ __html: EFFECT_ANIMATIONS }} />
145
+ </div>
146
+ );
147
+ }
148
+
149
+ export default AudioReactiveCover;
@@ -0,0 +1,110 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * GlowEffect - Multi-layer glow effect with pulse rings
5
+ */
6
+
7
+ import { cn } from '../../../../_shared';
8
+ import type { calculateGlowLayers } from '../../../effects';
9
+
10
+ // =============================================================================
11
+ // TYPES
12
+ // =============================================================================
13
+
14
+ export interface GlowEffectData {
15
+ layers: ReturnType<typeof calculateGlowLayers>;
16
+ hueShift: number;
17
+ showPulseRings: boolean;
18
+ showSparkle: boolean;
19
+ }
20
+
21
+ interface GlowEffectProps {
22
+ data: GlowEffectData;
23
+ colors: string[];
24
+ isPlaying: boolean;
25
+ }
26
+
27
+ // =============================================================================
28
+ // COMPONENT
29
+ // =============================================================================
30
+
31
+ export function GlowEffect({ data, colors, isPlaying }: GlowEffectProps) {
32
+ const { layers, hueShift, showPulseRings, showSparkle } = data;
33
+
34
+ return (
35
+ <>
36
+ {/* Main glow layers */}
37
+ {layers.map((layer, i) => (
38
+ <div
39
+ key={i}
40
+ className={cn('absolute rounded-2xl -z-10', layer.blur)}
41
+ style={{
42
+ inset: `-${layer.inset}px`,
43
+ background: layer.background,
44
+ opacity: isPlaying ? layer.opacity : 0,
45
+ transform: i < 2 ? `scaleY(${layer.scale})` : `scale(${layer.scale})`,
46
+ animation: isPlaying && layer.animation ? layer.animation : 'none',
47
+ transition: 'opacity 0.3s',
48
+ }}
49
+ />
50
+ ))}
51
+
52
+ {/* Rotating color sweep */}
53
+ <div
54
+ className="absolute rounded-2xl blur-xl overflow-hidden -z-10"
55
+ style={{
56
+ inset: '-75px',
57
+ opacity: isPlaying ? 0.6 : 0,
58
+ transition: 'opacity 0.5s',
59
+ }}
60
+ >
61
+ <div
62
+ className="absolute inset-0"
63
+ style={{
64
+ background: `conic-gradient(
65
+ from ${hueShift}deg at 50% 50%,
66
+ hsl(${colors[0]} / 0.4) 0deg,
67
+ hsl(${colors[1] || colors[0]} / 0.3) 90deg,
68
+ hsl(${colors[2] || colors[0]} / 0.3) 180deg,
69
+ hsl(${colors[3] || colors[0]} / 0.3) 270deg,
70
+ hsl(${colors[0]} / 0.4) 360deg
71
+ )`,
72
+ animation: isPlaying ? 'glow-rotate 6s linear infinite' : 'none',
73
+ }}
74
+ />
75
+ </div>
76
+
77
+ {/* Pulse rings on bass hits */}
78
+ {showPulseRings && (
79
+ <>
80
+ <div
81
+ className="absolute -inset-6 rounded-xl border-2 animate-ping -z-10"
82
+ style={{
83
+ borderColor: `hsl(${colors[0]} / 0.4)`,
84
+ animationDuration: '1s',
85
+ }}
86
+ />
87
+ <div
88
+ className="absolute -inset-12 rounded-2xl border animate-ping -z-10"
89
+ style={{
90
+ borderColor: `hsl(${colors[1] || colors[0]} / 0.3)`,
91
+ animationDuration: '1.5s',
92
+ animationDelay: '0.2s',
93
+ }}
94
+ />
95
+ </>
96
+ )}
97
+
98
+ {/* Sparkle on high frequencies */}
99
+ {showSparkle && (
100
+ <div
101
+ className="absolute -inset-18 rounded-3xl -z-10"
102
+ style={{
103
+ background: `radial-gradient(circle at 50% 30%, hsl(${colors[2] || colors[0]} / 0.5) 0%, transparent 30%)`,
104
+ animation: 'sparkle-move 0.5s ease-out',
105
+ }}
106
+ />
107
+ )}
108
+ </>
109
+ );
110
+ }
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * MeshEffect - Mesh gradient blobs that react to audio
5
+ */
6
+
7
+ import { cn } from '../../../../_shared';
8
+ import type { calculateMeshGradients } from '../../../effects';
9
+
10
+ // =============================================================================
11
+ // TYPES
12
+ // =============================================================================
13
+
14
+ interface MeshEffectProps {
15
+ gradients: ReturnType<typeof calculateMeshGradients>;
16
+ blur: string;
17
+ isPlaying: boolean;
18
+ }
19
+
20
+ // =============================================================================
21
+ // COMPONENT
22
+ // =============================================================================
23
+
24
+ export function MeshEffect({ gradients, isPlaying }: MeshEffectProps) {
25
+ return (
26
+ <>
27
+ {gradients.map((g, i) => {
28
+ const isCenter = 'isCenter' in g && g.isCenter;
29
+ const scale = 'scale' in g ? g.scale : 1;
30
+ const rotation = 'rotation' in g ? g.rotation : 0;
31
+ const itemBlur = 'blur' in g ? g.blur : 'blur-2xl';
32
+
33
+ return (
34
+ <div
35
+ key={i}
36
+ className={cn('absolute rounded-full -z-10', itemBlur)}
37
+ style={{
38
+ width: g.width,
39
+ height: g.height,
40
+ top: 'top' in g ? g.top : undefined,
41
+ bottom: 'bottom' in g ? g.bottom : undefined,
42
+ left: 'left' in g ? g.left : undefined,
43
+ right: 'right' in g ? g.right : undefined,
44
+ background: isCenter
45
+ ? `radial-gradient(circle, hsl(${g.color} / 0.6) 0%, hsl(${g.color} / 0.3) 30%, transparent 60%)`
46
+ : `radial-gradient(circle, hsl(${g.color}) 0%, hsl(${g.color} / 0.5) 30%, transparent 65%)`,
47
+ opacity: isPlaying ? g.opacity : 0,
48
+ transform: isCenter
49
+ ? `translate(-50%, -50%) scale(${scale})`
50
+ : `scale(${scale}) rotate(${rotation}deg)`,
51
+ transition: 'all 0.08s ease-out',
52
+ }}
53
+ />
54
+ );
55
+ })}
56
+ </>
57
+ );
58
+ }
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * OrbsEffect - Floating orb particles that react to audio
5
+ */
6
+
7
+ import { cn } from '../../../../_shared';
8
+ import type { calculateOrbs } from '../../../effects';
9
+
10
+ // =============================================================================
11
+ // TYPES
12
+ // =============================================================================
13
+
14
+ interface OrbsEffectProps {
15
+ orbs: ReturnType<typeof calculateOrbs>;
16
+ blur: string;
17
+ isPlaying: boolean;
18
+ }
19
+
20
+ // =============================================================================
21
+ // COMPONENT
22
+ // =============================================================================
23
+
24
+ export function OrbsEffect({ orbs, blur, isPlaying }: OrbsEffectProps) {
25
+ return (
26
+ <>
27
+ {orbs.map((orb, i) => (
28
+ <div
29
+ key={i}
30
+ className={cn('absolute rounded-full -z-10', blur)}
31
+ style={{
32
+ width: orb.size,
33
+ height: orb.size,
34
+ left: `${orb.x}%`,
35
+ top: `${orb.y}%`,
36
+ background: `radial-gradient(circle at 30% 30%, hsl(${orb.color}) 0%, hsl(${orb.color} / 0.5) 40%, transparent 70%)`,
37
+ opacity: isPlaying ? orb.opacity : 0,
38
+ transform: `translate(-50%, -50%) scale(${orb.scale})`,
39
+ transition: 'all 0.08s ease-out',
40
+ }}
41
+ />
42
+ ))}
43
+ </>
44
+ );
45
+ }