@djangocfg/ui-tools 2.1.312 → 2.1.313

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 (161) hide show
  1. package/README.md +38 -22
  2. package/dist/{DocsLayout-W5JLRNSZ.mjs → DocsLayout-ESVQZO3V.mjs} +3 -3
  3. package/dist/{DocsLayout-W5JLRNSZ.mjs.map → DocsLayout-ESVQZO3V.mjs.map} +1 -1
  4. package/dist/{DocsLayout-ZXD2CUOH.cjs → DocsLayout-KUPDWJ3G.cjs} +48 -48
  5. package/dist/{DocsLayout-ZXD2CUOH.cjs.map → DocsLayout-KUPDWJ3G.cjs.map} +1 -1
  6. package/dist/Player-M3GC3VPE.mjs +4 -0
  7. package/dist/Player-M3GC3VPE.mjs.map +1 -0
  8. package/dist/Player-ZGQKKOWI.css +65 -0
  9. package/dist/Player-ZGQKKOWI.css.map +1 -0
  10. package/dist/Player-ZL2X5LGG.cjs +13 -0
  11. package/dist/Player-ZL2X5LGG.cjs.map +1 -0
  12. package/dist/{chunk-CXVGN6ZW.cjs → chunk-DFTVB66S.cjs} +7 -6
  13. package/dist/chunk-DFTVB66S.cjs.map +1 -0
  14. package/dist/{chunk-2QY3LJR6.mjs → chunk-EUADAUBQ.mjs} +5 -4
  15. package/dist/chunk-EUADAUBQ.mjs.map +1 -0
  16. package/dist/chunk-FX2QFYWF.mjs +2059 -0
  17. package/dist/chunk-FX2QFYWF.mjs.map +1 -0
  18. package/dist/{chunk-6HNAPVZ2.mjs → chunk-GBLQTHWT.mjs} +11 -13
  19. package/dist/chunk-GBLQTHWT.mjs.map +1 -0
  20. package/dist/{chunk-FYLR232K.cjs → chunk-S44PW6NK.cjs} +11 -13
  21. package/dist/chunk-S44PW6NK.cjs.map +1 -0
  22. package/dist/chunk-ZLQHUZDU.cjs +2061 -0
  23. package/dist/chunk-ZLQHUZDU.cjs.map +1 -0
  24. package/dist/components-WYEZL5TE.cjs +26 -0
  25. package/dist/{components-3RTH76CV.cjs.map → components-WYEZL5TE.cjs.map} +1 -1
  26. package/dist/components-ZAGG2PBO.mjs +5 -0
  27. package/dist/{components-5GVVL2Q6.mjs.map → components-ZAGG2PBO.mjs.map} +1 -1
  28. package/dist/index.cjs +36 -220
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.css +65 -0
  31. package/dist/index.css.map +1 -1
  32. package/dist/index.d.cts +44 -500
  33. package/dist/index.d.ts +44 -500
  34. package/dist/index.mjs +16 -62
  35. package/dist/index.mjs.map +1 -1
  36. package/package.json +6 -6
  37. package/src/components/markdown/MarkdownMessage/ActionRow.tsx +48 -0
  38. package/src/components/markdown/MarkdownMessage/ChatMessageRow.tsx +97 -0
  39. package/src/components/markdown/MarkdownMessage/CodeBlock.tsx +9 -13
  40. package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +77 -2
  41. package/src/components/markdown/MarkdownMessage/MarkdownMessage.tsx +2 -3
  42. package/src/components/markdown/MarkdownMessage/README.md +72 -0
  43. package/src/components/markdown/MarkdownMessage/components.tsx +3 -3
  44. package/src/components/markdown/MarkdownMessage/index.ts +6 -0
  45. package/src/index.ts +2 -11
  46. package/src/tools/AudioPlayer/AudioPlayer.story.tsx +454 -107
  47. package/src/tools/AudioPlayer/Player.tsx +80 -0
  48. package/src/tools/AudioPlayer/PlayerShell.tsx +122 -0
  49. package/src/tools/AudioPlayer/README.md +139 -204
  50. package/src/tools/AudioPlayer/audio/audioContext.ts +39 -0
  51. package/src/tools/AudioPlayer/audio/decodePeaks.ts +36 -0
  52. package/src/tools/AudioPlayer/audio/index.ts +4 -0
  53. package/src/tools/AudioPlayer/audio/mediaElementSourceCache.ts +20 -0
  54. package/src/tools/AudioPlayer/audio/peaksCache.ts +37 -0
  55. package/src/tools/AudioPlayer/context/AudioRefContext.tsx +9 -0
  56. package/src/tools/AudioPlayer/context/ControlsContext.tsx +7 -0
  57. package/src/tools/AudioPlayer/context/LevelsContext.tsx +7 -0
  58. package/src/tools/AudioPlayer/context/MetaContext.tsx +16 -0
  59. package/src/tools/AudioPlayer/context/PlayerProvider.tsx +314 -0
  60. package/src/tools/AudioPlayer/context/StateContext.tsx +7 -0
  61. package/src/tools/AudioPlayer/context/index.ts +16 -15
  62. package/src/tools/AudioPlayer/context/selectors.ts +36 -0
  63. package/src/tools/AudioPlayer/hooks/index.ts +12 -39
  64. package/src/tools/AudioPlayer/hooks/useActivePlayer.ts +31 -0
  65. package/src/tools/AudioPlayer/hooks/useAnalyser.ts +62 -0
  66. package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +102 -0
  67. package/src/tools/AudioPlayer/hooks/useKeyboardShortcuts.ts +91 -0
  68. package/src/tools/AudioPlayer/hooks/useMediaSession.ts +74 -0
  69. package/src/tools/AudioPlayer/hooks/usePeaks.ts +83 -0
  70. package/src/tools/AudioPlayer/hooks/usePlayerPreferences.ts +21 -0
  71. package/src/tools/AudioPlayer/hooks/usePlayheadLoop.ts +77 -0
  72. package/src/tools/AudioPlayer/hooks/useResizeObserver.ts +20 -0
  73. package/src/tools/AudioPlayer/hooks/useThemeWatcher.ts +22 -0
  74. package/src/tools/AudioPlayer/index.ts +63 -134
  75. package/src/tools/AudioPlayer/lazy.tsx +8 -97
  76. package/src/tools/AudioPlayer/parts/Controls/ControlsRow.tsx +30 -0
  77. package/src/tools/AudioPlayer/parts/Controls/IconButton.tsx +62 -0
  78. package/src/tools/AudioPlayer/parts/Controls/LoopButton.tsx +33 -0
  79. package/src/tools/AudioPlayer/parts/Controls/PlayButton.tsx +86 -0
  80. package/src/tools/AudioPlayer/parts/Controls/SkipButton.tsx +17 -0
  81. package/src/tools/AudioPlayer/parts/Controls/VolumeControl.tsx +171 -0
  82. package/src/tools/AudioPlayer/parts/Controls/index.ts +6 -0
  83. package/src/tools/AudioPlayer/parts/Cover/Cover.tsx +24 -0
  84. package/src/tools/AudioPlayer/parts/Cover/CoverPlaceholder.tsx +27 -0
  85. package/src/tools/AudioPlayer/parts/Cover/ReactivePulse.tsx +66 -0
  86. package/src/tools/AudioPlayer/parts/Cover/index.ts +3 -0
  87. package/src/tools/AudioPlayer/parts/ErrorState/ErrorState.tsx +35 -0
  88. package/src/tools/AudioPlayer/parts/ErrorState/index.ts +1 -0
  89. package/src/tools/AudioPlayer/parts/Layout/CompactLayout.tsx +25 -0
  90. package/src/tools/AudioPlayer/parts/Layout/DefaultLayout.tsx +48 -0
  91. package/src/tools/AudioPlayer/parts/Layout/index.ts +2 -0
  92. package/src/tools/AudioPlayer/parts/Meta/Artist.tsx +14 -0
  93. package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +49 -0
  94. package/src/tools/AudioPlayer/parts/Meta/Title.tsx +13 -0
  95. package/src/tools/AudioPlayer/parts/Meta/index.ts +3 -0
  96. package/src/tools/AudioPlayer/parts/Skeleton/CoverSkeleton.tsx +13 -0
  97. package/src/tools/AudioPlayer/parts/Skeleton/MetaSkeleton.tsx +10 -0
  98. package/src/tools/AudioPlayer/parts/Skeleton/index.ts +2 -0
  99. package/src/tools/AudioPlayer/parts/Waveform/BarsWaveform.tsx +48 -0
  100. package/src/tools/AudioPlayer/parts/Waveform/LiveWaveform.tsx +95 -0
  101. package/src/tools/AudioPlayer/parts/Waveform/PeaksWaveform.tsx +100 -0
  102. package/src/tools/AudioPlayer/parts/Waveform/ProgressBar.tsx +76 -0
  103. package/src/tools/AudioPlayer/parts/Waveform/Waveform.tsx +74 -0
  104. package/src/tools/AudioPlayer/parts/Waveform/WaveformSkeleton.tsx +16 -0
  105. package/src/tools/AudioPlayer/parts/Waveform/index.ts +8 -0
  106. package/src/tools/AudioPlayer/parts/Waveform/waveformInteraction.ts +106 -0
  107. package/src/tools/AudioPlayer/parts/Waveform/waveformRenderer.ts +91 -0
  108. package/src/tools/AudioPlayer/parts/index.ts +1 -0
  109. package/src/tools/AudioPlayer/store/activePlayerBus.ts +63 -0
  110. package/src/tools/AudioPlayer/store/createLevelsStore.ts +37 -0
  111. package/src/tools/AudioPlayer/store/index.ts +16 -0
  112. package/src/tools/AudioPlayer/store/preferencesStore.ts +104 -0
  113. package/src/tools/AudioPlayer/styles/webview-safe.css +77 -0
  114. package/src/tools/AudioPlayer/types.ts +95 -0
  115. package/src/tools/AudioPlayer/utils/bucketize.ts +27 -0
  116. package/src/tools/AudioPlayer/utils/clamp.ts +5 -0
  117. package/src/tools/AudioPlayer/utils/dpr.ts +19 -0
  118. package/src/tools/AudioPlayer/utils/formatTime.ts +12 -8
  119. package/src/tools/AudioPlayer/utils/index.ts +4 -5
  120. package/src/tools/AudioPlayer/utils/readCssVar.ts +7 -0
  121. package/src/tools/AudioPlayer/utils/resolveCanvasColor.ts +28 -0
  122. package/src/tools/index.ts +5 -75
  123. package/dist/chunk-2QY3LJR6.mjs.map +0 -1
  124. package/dist/chunk-6HNAPVZ2.mjs.map +0 -1
  125. package/dist/chunk-CXVGN6ZW.cjs.map +0 -1
  126. package/dist/chunk-F2N7P5XU.cjs +0 -30
  127. package/dist/chunk-F2N7P5XU.cjs.map +0 -1
  128. package/dist/chunk-FYLR232K.cjs.map +0 -1
  129. package/dist/chunk-HMHIVEMS.mjs +0 -1619
  130. package/dist/chunk-HMHIVEMS.mjs.map +0 -1
  131. package/dist/chunk-JWB2EWQO.mjs +0 -5
  132. package/dist/chunk-JWB2EWQO.mjs.map +0 -1
  133. package/dist/chunk-YZX6FH3H.cjs +0 -1656
  134. package/dist/chunk-YZX6FH3H.cjs.map +0 -1
  135. package/dist/components-3RTH76CV.cjs +0 -27
  136. package/dist/components-5GVVL2Q6.mjs +0 -5
  137. package/dist/components-CPHOUQ5F.cjs +0 -46
  138. package/dist/components-CPHOUQ5F.cjs.map +0 -1
  139. package/dist/components-OTK43IMD.mjs +0 -6
  140. package/dist/components-OTK43IMD.mjs.map +0 -1
  141. package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +0 -225
  142. package/src/tools/AudioPlayer/components/HybridCompactPlayer.tsx +0 -163
  143. package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +0 -284
  144. package/src/tools/AudioPlayer/components/HybridWaveform.tsx +0 -286
  145. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +0 -151
  146. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +0 -110
  147. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +0 -58
  148. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +0 -45
  149. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +0 -82
  150. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +0 -8
  151. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +0 -6
  152. package/src/tools/AudioPlayer/components/index.ts +0 -23
  153. package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +0 -158
  154. package/src/tools/AudioPlayer/effects/index.ts +0 -412
  155. package/src/tools/AudioPlayer/hooks/useAudioBus.ts +0 -76
  156. package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +0 -403
  157. package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +0 -96
  158. package/src/tools/AudioPlayer/hooks/useVisualization.tsx +0 -207
  159. package/src/tools/AudioPlayer/types/effects.ts +0 -73
  160. package/src/tools/AudioPlayer/types/index.ts +0 -27
  161. package/src/tools/AudioPlayer/utils/debug.ts +0 -14
@@ -1,286 +0,0 @@
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
- import { useThemeColor } from '@djangocfg/ui-core/styles/palette';
20
-
21
- // =============================================================================
22
- // TYPES
23
- // =============================================================================
24
-
25
- export interface HybridWaveformProps {
26
- /** Visualization mode */
27
- mode?: 'frequency' | 'static';
28
- /** Canvas height in pixels */
29
- height?: number;
30
- /** Bar width in pixels */
31
- barWidth?: number;
32
- /** Gap between bars in pixels */
33
- barGap?: number;
34
- /** Bar border radius */
35
- barRadius?: number;
36
- /** Color for played portion */
37
- progressColor?: string;
38
- /** Color for unplayed portion */
39
- waveColor?: string;
40
- /** Color for buffered regions indicator */
41
- bufferedColor?: string;
42
- /** Additional CSS class */
43
- className?: string;
44
- /** Callback when user seeks */
45
- onSeek?: (time: number) => void;
46
- }
47
-
48
- // =============================================================================
49
- // COMPONENT
50
- // =============================================================================
51
-
52
- export const HybridWaveform = memo(function HybridWaveform({
53
- mode = 'frequency',
54
- height = 64,
55
- barWidth = 3,
56
- barGap = 2,
57
- barRadius = 2,
58
- progressColor,
59
- waveColor,
60
- bufferedColor,
61
- className,
62
- onSeek,
63
- }: HybridWaveformProps) {
64
- const themePrimary = useThemeColor('primary');
65
- const themePrimaryWave = useThemeColor('primary', 0.3);
66
- const themePrimaryBuffered = useThemeColor('primary', 0.15);
67
- const resolvedProgressColor = progressColor ?? themePrimary;
68
- const resolvedWaveColor = waveColor ?? themePrimaryWave;
69
- const resolvedBufferedColor = bufferedColor ?? themePrimaryBuffered;
70
- const canvasRef = useRef<HTMLCanvasElement>(null);
71
- const containerRef = useRef<HTMLDivElement>(null);
72
- const animationRef = useRef<number | null>(null);
73
- const { state, controls, webAudio } = useHybridAudioContext();
74
-
75
- // Handle click to seek
76
- const handleClick = useCallback(
77
- (e: React.MouseEvent<HTMLCanvasElement>) => {
78
- const canvas = canvasRef.current;
79
- if (!canvas || !state.duration) return;
80
-
81
- const rect = canvas.getBoundingClientRect();
82
- const x = e.clientX - rect.left;
83
- const progress = x / rect.width;
84
- const time = state.duration * progress;
85
-
86
- controls.seek(time);
87
- onSeek?.(time);
88
- },
89
- [state.duration, controls, onSeek]
90
- );
91
-
92
- // Render frequency bars (real-time visualization)
93
- const renderFrequency = useCallback(() => {
94
- const canvas = canvasRef.current;
95
- const analyser = webAudio.analyser;
96
- if (!canvas) return;
97
-
98
- const ctx = canvas.getContext('2d');
99
- if (!ctx) return;
100
-
101
- const { width, height: canvasHeight } = canvas;
102
- const dpr = window.devicePixelRatio || 1;
103
- const displayWidth = width / dpr;
104
-
105
- // Get frequency data if analyser is available
106
- let dataArray: Uint8Array<ArrayBuffer> | null = null;
107
- if (analyser) {
108
- dataArray = new Uint8Array(analyser.frequencyBinCount) as Uint8Array<ArrayBuffer>;
109
- analyser.getByteFrequencyData(dataArray);
110
- }
111
-
112
- ctx.clearRect(0, 0, width, canvasHeight);
113
-
114
- // Draw buffered regions at bottom
115
- if (state.buffered && state.duration > 0) {
116
- ctx.fillStyle = resolvedBufferedColor;
117
- for (let i = 0; i < state.buffered.length; i++) {
118
- const start = (state.buffered.start(i) / state.duration) * width;
119
- const end = (state.buffered.end(i) / state.duration) * width;
120
- ctx.fillRect(start, canvasHeight - 3 * dpr, end - start, 3 * dpr);
121
- }
122
- }
123
-
124
- // Calculate bar count based on available width
125
- const barCount = Math.floor(displayWidth / (barWidth + barGap));
126
- const progress = state.duration > 0 ? state.currentTime / state.duration : 0;
127
- const progressX = width * progress;
128
-
129
- // Draw bars
130
- for (let i = 0; i < barCount; i++) {
131
- let barHeight: number;
132
-
133
- if (dataArray && state.isPlaying) {
134
- // Real-time frequency data
135
- const step = Math.floor(dataArray.length / barCount);
136
- const value = dataArray[i * step] / 255;
137
- barHeight = Math.max(4 * dpr, value * (canvasHeight - 6 * dpr) * 0.9);
138
- } else {
139
- // Static fallback - small bars
140
- barHeight = 8 * dpr;
141
- }
142
-
143
- const x = i * (barWidth + barGap) * dpr;
144
- const y = (canvasHeight - barHeight) / 2;
145
-
146
- ctx.fillStyle = x < progressX ? resolvedProgressColor : resolvedWaveColor;
147
-
148
- // Draw rounded rect
149
- const radius = barRadius * dpr;
150
- const rectWidth = barWidth * dpr;
151
- ctx.beginPath();
152
- ctx.roundRect(x, y, rectWidth, barHeight, radius);
153
- ctx.fill();
154
- }
155
-
156
- // Continue animation if playing
157
- if (state.isPlaying) {
158
- animationRef.current = requestAnimationFrame(renderFrequency);
159
- }
160
- }, [
161
- webAudio.analyser,
162
- state.buffered,
163
- state.duration,
164
- state.currentTime,
165
- state.isPlaying,
166
- barWidth,
167
- barGap,
168
- barRadius,
169
- resolvedProgressColor,
170
- resolvedWaveColor,
171
- resolvedBufferedColor,
172
- ]);
173
-
174
- // Render static progress bar
175
- const renderStatic = useCallback(() => {
176
- const canvas = canvasRef.current;
177
- if (!canvas) return;
178
-
179
- const ctx = canvas.getContext('2d');
180
- if (!ctx) return;
181
-
182
- const { width, height: canvasHeight } = canvas;
183
- const dpr = window.devicePixelRatio || 1;
184
-
185
- ctx.clearRect(0, 0, width, canvasHeight);
186
-
187
- // Draw progress bar
188
- const progress = state.duration > 0 ? state.currentTime / state.duration : 0;
189
- const progressWidth = width * progress;
190
-
191
- // Background track
192
- ctx.fillStyle = resolvedWaveColor;
193
- ctx.fillRect(0, canvasHeight / 2 - 2 * dpr, width, 4 * dpr);
194
-
195
- // Buffered region (thin strip at bottom)
196
- if (state.buffered && state.duration > 0) {
197
- ctx.fillStyle = resolvedBufferedColor;
198
- for (let i = 0; i < state.buffered.length; i++) {
199
- const start = (state.buffered.start(i) / state.duration) * width;
200
- const end = (state.buffered.end(i) / state.duration) * width;
201
- ctx.fillRect(start, canvasHeight - 2 * dpr, end - start, 2 * dpr);
202
- }
203
- }
204
-
205
- // Progress
206
- ctx.fillStyle = resolvedProgressColor;
207
- ctx.fillRect(0, canvasHeight / 2 - 2 * dpr, progressWidth, 4 * dpr);
208
-
209
- // Handle
210
- if (progress > 0) {
211
- ctx.beginPath();
212
- ctx.arc(progressWidth, canvasHeight / 2, 6 * dpr, 0, Math.PI * 2);
213
- ctx.fill();
214
- }
215
- }, [state.buffered, state.duration, state.currentTime, resolvedProgressColor, resolvedWaveColor, resolvedBufferedColor]);
216
-
217
- // Resize canvas to match container
218
- useEffect(() => {
219
- const container = containerRef.current;
220
- const canvas = canvasRef.current;
221
- if (!container || !canvas) return;
222
-
223
- const resizeObserver = new ResizeObserver((entries) => {
224
- const entry = entries[0];
225
- if (!entry) return;
226
-
227
- const dpr = window.devicePixelRatio || 1;
228
- const displayWidth = entry.contentRect.width;
229
- const displayHeight = height;
230
-
231
- canvas.width = displayWidth * dpr;
232
- canvas.height = displayHeight * dpr;
233
- canvas.style.width = `${displayWidth}px`;
234
- canvas.style.height = `${displayHeight}px`;
235
-
236
- // Re-render after resize
237
- if (mode === 'frequency') {
238
- renderFrequency();
239
- } else {
240
- renderStatic();
241
- }
242
- });
243
-
244
- resizeObserver.observe(container);
245
- return () => resizeObserver.disconnect();
246
- }, [height, mode, renderFrequency, renderStatic]);
247
-
248
- // Animation loop for frequency mode
249
- useEffect(() => {
250
- if (mode === 'frequency') {
251
- renderFrequency();
252
- }
253
-
254
- return () => {
255
- if (animationRef.current) {
256
- cancelAnimationFrame(animationRef.current);
257
- }
258
- };
259
- }, [mode, renderFrequency]);
260
-
261
- // Re-render on time change for static mode
262
- useEffect(() => {
263
- if (mode === 'static') {
264
- renderStatic();
265
- }
266
- }, [mode, state.currentTime, renderStatic]);
267
-
268
- // Re-render frequency when playback state changes
269
- useEffect(() => {
270
- if (mode === 'frequency' && !state.isPlaying) {
271
- // One final render when stopped
272
- renderFrequency();
273
- }
274
- }, [mode, state.isPlaying, renderFrequency]);
275
-
276
- return (
277
- <div ref={containerRef} className={cn('w-full', className)}>
278
- <canvas
279
- ref={canvasRef}
280
- onClick={handleClick}
281
- className="cursor-pointer"
282
- style={{ width: '100%', height }}
283
- />
284
- </div>
285
- );
286
- });
@@ -1,151 +0,0 @@
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
- /** 'sm' | 'md' | 'lg' = fixed sizes; 'full' = stretch to container width (aspect-square) */
36
- size?: 'sm' | 'md' | 'lg' | 'full';
37
- variant?: EffectVariant;
38
- intensity?: EffectIntensity;
39
- colorScheme?: EffectColorScheme;
40
- onClick?: () => void;
41
- className?: string;
42
- }
43
-
44
- // =============================================================================
45
- // CONSTANTS
46
- // =============================================================================
47
-
48
- const SIZES = {
49
- sm: { container: 'w-32 h-32', orbBase: 40 },
50
- md: { container: 'w-40 h-40', orbBase: 50 },
51
- lg: { container: 'w-48 h-48', orbBase: 60 },
52
- full: { container: 'w-full sm:max-w-xs aspect-square mx-auto', orbBase: 60 },
53
- };
54
-
55
- // =============================================================================
56
- // COMPONENT
57
- // =============================================================================
58
-
59
- export function AudioReactiveCover({
60
- children,
61
- size = 'lg',
62
- variant = 'spotlight',
63
- intensity = 'medium',
64
- colorScheme = 'primary',
65
- onClick,
66
- className,
67
- }: AudioReactiveCoverProps) {
68
- // Get audio state from HybridAudioProvider context
69
- const { isPlaying } = useHybridAudioState();
70
- const levels = useHybridAudioLevels();
71
-
72
- // =========================================================================
73
- // PREPARE DATA BEFORE RENDER
74
- // =========================================================================
75
-
76
- const sizeConfig = SIZES[size];
77
- const effectConfig = getEffectConfig(intensity);
78
- const { colors, hueShift } = prepareEffectColors(colorScheme, levels);
79
-
80
- // Calculate scale based on overall level
81
- const containerScale = 1 + levels.overall * effectConfig.scale;
82
-
83
- // Prepare effect-specific data - NO memoization for real-time reactivity
84
- const glowData: GlowEffectData | null = variant === 'glow' ? {
85
- layers: calculateGlowLayers(levels, effectConfig, colors),
86
- hueShift,
87
- showPulseRings: levels.bass > 0.5,
88
- showSparkle: levels.high > 0.4,
89
- } : null;
90
-
91
- const orbsData = variant === 'orbs'
92
- ? calculateOrbs(levels, effectConfig, colors, sizeConfig.orbBase)
93
- : null;
94
-
95
- const meshData = variant === 'mesh'
96
- ? calculateMeshGradients(levels, effectConfig, colors)
97
- : null;
98
-
99
- const spotlightData = variant === 'spotlight'
100
- ? calculateSpotlight(levels, effectConfig, colors, levels.mid * 360)
101
- : null;
102
-
103
- // =========================================================================
104
- // RENDER
105
- // =========================================================================
106
-
107
- return (
108
- <div
109
- className={cn('relative', sizeConfig.container, className)}
110
- style={{
111
- transform: `scale(${containerScale})`,
112
- transition: 'transform 0.1s ease-out',
113
- }}
114
- >
115
- {/* Effect layers container - under cover, non-interactive */}
116
- <div className="absolute inset-0 z-0 pointer-events-none overflow-visible">
117
- {glowData && (
118
- <GlowEffect data={glowData} colors={colors} isPlaying={isPlaying} />
119
- )}
120
-
121
- {orbsData && (
122
- <OrbsEffect orbs={orbsData} blur={effectConfig.blur} isPlaying={isPlaying} />
123
- )}
124
-
125
- {spotlightData && (
126
- <SpotlightEffect data={spotlightData} colors={colors} blur={effectConfig.blur} isPlaying={isPlaying} />
127
- )}
128
-
129
- {meshData && (
130
- <MeshEffect gradients={meshData} blur={effectConfig.blur} isPlaying={isPlaying} />
131
- )}
132
- </div>
133
-
134
- {/* Content (cover art) */}
135
- <div
136
- className="relative w-full h-full rounded-lg overflow-hidden shadow-2xl z-10 bg-background cursor-pointer"
137
- onClick={onClick}
138
- role={onClick ? 'button' : undefined}
139
- tabIndex={onClick ? 0 : undefined}
140
- onKeyDown={onClick ? (e) => e.key === 'Enter' && onClick() : undefined}
141
- >
142
- {children}
143
- </div>
144
-
145
- {/* Inject animations once */}
146
- <style dangerouslySetInnerHTML={{ __html: EFFECT_ANIMATIONS }} />
147
- </div>
148
- );
149
- }
150
-
151
- export default AudioReactiveCover;
@@ -1,110 +0,0 @@
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
- }
@@ -1,58 +0,0 @@
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
- }
@@ -1,45 +0,0 @@
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
- }