@djangocfg/ui-nextjs 2.1.65 → 2.1.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -8
- package/src/blocks/SplitHero/SplitHeroMedia.tsx +2 -1
- package/src/stores/index.ts +8 -0
- package/src/stores/mediaCache.ts +464 -0
- package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
- package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
- package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
- package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
- package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
- package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
- package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
- package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
- package/src/tools/AudioPlayer/README.md +325 -0
- package/src/tools/AudioPlayer/components/AudioEqualizer.tsx +200 -0
- package/src/tools/AudioPlayer/components/AudioPlayer.tsx +231 -0
- package/src/tools/AudioPlayer/components/AudioShortcutsPopover.tsx +99 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
- package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +280 -0
- package/src/tools/AudioPlayer/components/VisualizationToggle.tsx +64 -0
- package/src/tools/AudioPlayer/components/index.ts +21 -0
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
- package/src/tools/AudioPlayer/context/index.ts +11 -0
- package/src/tools/AudioPlayer/context/selectors.ts +96 -0
- package/src/tools/AudioPlayer/effects/index.ts +412 -0
- package/src/tools/AudioPlayer/hooks/index.ts +29 -0
- package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
- package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +149 -0
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
- package/src/tools/AudioPlayer/hooks/useVisualization.tsx +201 -0
- package/src/tools/AudioPlayer/index.ts +139 -0
- package/src/tools/AudioPlayer/types/audio.ts +107 -0
- package/src/tools/AudioPlayer/types/components.ts +98 -0
- package/src/tools/AudioPlayer/types/effects.ts +73 -0
- package/src/tools/AudioPlayer/types/index.ts +35 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
- package/src/tools/AudioPlayer/utils/index.ts +5 -0
- package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
- package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
- package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
- package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
- package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
- package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
- package/src/tools/ImageViewer/README.md +174 -0
- package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
- package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
- package/src/tools/ImageViewer/components/index.ts +7 -0
- package/src/tools/ImageViewer/hooks/index.ts +9 -0
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
- package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
- package/src/tools/ImageViewer/index.ts +60 -0
- package/src/tools/ImageViewer/types.ts +75 -0
- package/src/tools/ImageViewer/utils/constants.ts +59 -0
- package/src/tools/ImageViewer/utils/index.ts +16 -0
- package/src/tools/ImageViewer/utils/lqip.ts +47 -0
- package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
- package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
- package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
- package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
- package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
- package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
- package/src/tools/VideoPlayer/README.md +212 -187
- package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
- package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +174 -0
- package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
- package/src/tools/VideoPlayer/components/index.ts +14 -0
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
- package/src/tools/VideoPlayer/context/index.ts +8 -0
- package/src/tools/VideoPlayer/hooks/index.ts +9 -0
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
- package/src/tools/VideoPlayer/index.ts +70 -9
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +206 -0
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +401 -0
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +332 -0
- package/src/tools/VideoPlayer/providers/index.ts +8 -0
- package/src/tools/VideoPlayer/types/index.ts +38 -0
- package/src/tools/VideoPlayer/types/player.ts +116 -0
- package/src/tools/VideoPlayer/types/provider.ts +93 -0
- package/src/tools/VideoPlayer/types/sources.ts +97 -0
- package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
- package/src/tools/VideoPlayer/utils/index.ts +11 -0
- package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
- package/src/tools/index.ts +92 -4
- package/src/tools/VideoPlayer/NativePlayer.tsx +0 -141
- package/src/tools/VideoPlayer/VideoPlayer.tsx +0 -231
- package/src/tools/VideoPlayer/types.ts +0 -118
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AudioPlayer - Audio playback controls and waveform display
|
|
5
|
+
*
|
|
6
|
+
* Uses AudioContext for state management
|
|
7
|
+
* Renders waveform (via container ref), controls, timer, and equalizer
|
|
8
|
+
* Supports keyboard shortcuts for playback control
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { forwardRef } from 'react';
|
|
12
|
+
import {
|
|
13
|
+
Play,
|
|
14
|
+
Pause,
|
|
15
|
+
RotateCcw,
|
|
16
|
+
SkipBack,
|
|
17
|
+
SkipForward,
|
|
18
|
+
Volume2,
|
|
19
|
+
VolumeX,
|
|
20
|
+
Loader2,
|
|
21
|
+
Repeat,
|
|
22
|
+
} from 'lucide-react';
|
|
23
|
+
import { Button, Slider, cn } from '@djangocfg/ui-nextjs';
|
|
24
|
+
import { useAudio } from '../context';
|
|
25
|
+
import { AudioEqualizer } from './AudioEqualizer';
|
|
26
|
+
import { useAudioHotkeys } from '../hooks';
|
|
27
|
+
import { AudioShortcutsPopover } from './AudioShortcutsPopover';
|
|
28
|
+
import { formatTime } from '../utils';
|
|
29
|
+
import type { AudioPlayerProps } from '../types';
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// COMPONENT
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
export const AudioPlayer = forwardRef<HTMLDivElement, AudioPlayerProps>(
|
|
36
|
+
function AudioPlayer(
|
|
37
|
+
{
|
|
38
|
+
showControls = true,
|
|
39
|
+
showWaveform = true,
|
|
40
|
+
showEqualizer = false,
|
|
41
|
+
showTimer = true,
|
|
42
|
+
showVolume = true,
|
|
43
|
+
showLoop = true,
|
|
44
|
+
equalizerOptions = {},
|
|
45
|
+
className,
|
|
46
|
+
style,
|
|
47
|
+
},
|
|
48
|
+
ref
|
|
49
|
+
) {
|
|
50
|
+
// Get all state and controls from context
|
|
51
|
+
const {
|
|
52
|
+
isReady,
|
|
53
|
+
isPlaying,
|
|
54
|
+
currentTime,
|
|
55
|
+
duration,
|
|
56
|
+
volume,
|
|
57
|
+
isMuted,
|
|
58
|
+
isLooping,
|
|
59
|
+
togglePlay,
|
|
60
|
+
restart,
|
|
61
|
+
skip,
|
|
62
|
+
setVolume,
|
|
63
|
+
toggleMute,
|
|
64
|
+
toggleLoop,
|
|
65
|
+
} = useAudio();
|
|
66
|
+
|
|
67
|
+
// Enable keyboard shortcuts
|
|
68
|
+
useAudioHotkeys({ enabled: isReady });
|
|
69
|
+
|
|
70
|
+
const isLoading = !isReady;
|
|
71
|
+
|
|
72
|
+
const handleVolumeChange = (value: number[]) => {
|
|
73
|
+
setVolume(value[0] / 100);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
className={cn(
|
|
79
|
+
'flex flex-col gap-3 p-4 rounded-lg bg-card border',
|
|
80
|
+
className
|
|
81
|
+
)}
|
|
82
|
+
style={style}
|
|
83
|
+
>
|
|
84
|
+
{/* Waveform container - rendered by WaveSurfer via context */}
|
|
85
|
+
{showWaveform && (
|
|
86
|
+
<div className="relative">
|
|
87
|
+
<div
|
|
88
|
+
ref={ref}
|
|
89
|
+
className={cn(
|
|
90
|
+
'w-full rounded transition-opacity',
|
|
91
|
+
isLoading && 'opacity-50'
|
|
92
|
+
)}
|
|
93
|
+
/>
|
|
94
|
+
{isLoading && (
|
|
95
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
96
|
+
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{/* Equalizer animation */}
|
|
103
|
+
{showEqualizer && (
|
|
104
|
+
<AudioEqualizer
|
|
105
|
+
barCount={equalizerOptions.barCount}
|
|
106
|
+
height={equalizerOptions.height}
|
|
107
|
+
gap={equalizerOptions.gap}
|
|
108
|
+
showPeaks={equalizerOptions.showPeaks}
|
|
109
|
+
barColor={equalizerOptions.barColor}
|
|
110
|
+
peakColor={equalizerOptions.peakColor}
|
|
111
|
+
className="px-1"
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{/* Timer */}
|
|
116
|
+
{showTimer && (
|
|
117
|
+
<div className="flex justify-between text-xs text-muted-foreground tabular-nums px-1">
|
|
118
|
+
<span>{formatTime(currentTime)}</span>
|
|
119
|
+
<span>{formatTime(duration)}</span>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
{/* Controls */}
|
|
124
|
+
{showControls && (
|
|
125
|
+
<div className="flex items-center justify-center gap-1">
|
|
126
|
+
{/* Restart */}
|
|
127
|
+
<Button
|
|
128
|
+
variant="ghost"
|
|
129
|
+
size="icon"
|
|
130
|
+
className="h-9 w-9"
|
|
131
|
+
onClick={restart}
|
|
132
|
+
disabled={!isReady}
|
|
133
|
+
title="Restart"
|
|
134
|
+
>
|
|
135
|
+
<RotateCcw className="h-4 w-4" />
|
|
136
|
+
</Button>
|
|
137
|
+
|
|
138
|
+
{/* Skip back 5s */}
|
|
139
|
+
<Button
|
|
140
|
+
variant="ghost"
|
|
141
|
+
size="icon"
|
|
142
|
+
className="h-9 w-9"
|
|
143
|
+
onClick={() => skip(-5)}
|
|
144
|
+
disabled={!isReady}
|
|
145
|
+
title="Back 5 seconds"
|
|
146
|
+
>
|
|
147
|
+
<SkipBack className="h-4 w-4" />
|
|
148
|
+
</Button>
|
|
149
|
+
|
|
150
|
+
{/* Play/Pause */}
|
|
151
|
+
<Button
|
|
152
|
+
variant="default"
|
|
153
|
+
size="icon"
|
|
154
|
+
className="h-12 w-12 rounded-full"
|
|
155
|
+
onClick={togglePlay}
|
|
156
|
+
disabled={!isReady && !isLoading}
|
|
157
|
+
title={isPlaying ? 'Pause' : 'Play'}
|
|
158
|
+
>
|
|
159
|
+
{isLoading ? (
|
|
160
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
161
|
+
) : isPlaying ? (
|
|
162
|
+
<Pause className="h-5 w-5" />
|
|
163
|
+
) : (
|
|
164
|
+
<Play className="h-5 w-5 ml-0.5" />
|
|
165
|
+
)}
|
|
166
|
+
</Button>
|
|
167
|
+
|
|
168
|
+
{/* Skip forward 5s */}
|
|
169
|
+
<Button
|
|
170
|
+
variant="ghost"
|
|
171
|
+
size="icon"
|
|
172
|
+
className="h-9 w-9"
|
|
173
|
+
onClick={() => skip(5)}
|
|
174
|
+
disabled={!isReady}
|
|
175
|
+
title="Forward 5 seconds"
|
|
176
|
+
>
|
|
177
|
+
<SkipForward className="h-4 w-4" />
|
|
178
|
+
</Button>
|
|
179
|
+
|
|
180
|
+
{/* Volume */}
|
|
181
|
+
{showVolume && (
|
|
182
|
+
<>
|
|
183
|
+
<Button
|
|
184
|
+
variant="ghost"
|
|
185
|
+
size="icon"
|
|
186
|
+
className="h-9 w-9"
|
|
187
|
+
onClick={toggleMute}
|
|
188
|
+
title={isMuted ? 'Unmute' : 'Mute'}
|
|
189
|
+
>
|
|
190
|
+
{isMuted || volume === 0 ? (
|
|
191
|
+
<VolumeX className="h-4 w-4" />
|
|
192
|
+
) : (
|
|
193
|
+
<Volume2 className="h-4 w-4" />
|
|
194
|
+
)}
|
|
195
|
+
</Button>
|
|
196
|
+
|
|
197
|
+
<Slider
|
|
198
|
+
value={[isMuted ? 0 : volume * 100]}
|
|
199
|
+
max={100}
|
|
200
|
+
step={1}
|
|
201
|
+
onValueChange={handleVolumeChange}
|
|
202
|
+
className="w-20"
|
|
203
|
+
aria-label="Volume"
|
|
204
|
+
/>
|
|
205
|
+
</>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{/* Loop/Repeat */}
|
|
209
|
+
{showLoop && (
|
|
210
|
+
<Button
|
|
211
|
+
variant="ghost"
|
|
212
|
+
size="icon"
|
|
213
|
+
className={cn('h-9 w-9', isLooping && 'text-primary')}
|
|
214
|
+
onClick={toggleLoop}
|
|
215
|
+
disabled={!isReady}
|
|
216
|
+
title={isLooping ? 'Disable loop' : 'Enable loop'}
|
|
217
|
+
>
|
|
218
|
+
<Repeat className="h-4 w-4" />
|
|
219
|
+
</Button>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{/* Shortcuts help */}
|
|
223
|
+
<AudioShortcutsPopover compact />
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
export default AudioPlayer;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AudioShortcutsPopover
|
|
5
|
+
*
|
|
6
|
+
* Displays keyboard shortcuts for audio player controls.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Popover,
|
|
11
|
+
PopoverContent,
|
|
12
|
+
PopoverTrigger,
|
|
13
|
+
Button,
|
|
14
|
+
Kbd,
|
|
15
|
+
KbdGroup,
|
|
16
|
+
Separator,
|
|
17
|
+
Tooltip,
|
|
18
|
+
TooltipTrigger,
|
|
19
|
+
TooltipContent,
|
|
20
|
+
} from '@djangocfg/ui-nextjs';
|
|
21
|
+
import { Keyboard } from 'lucide-react';
|
|
22
|
+
import { AUDIO_SHORTCUTS } from '../hooks';
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// TYPES
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
interface AudioShortcutsPopoverProps {
|
|
29
|
+
/** Compact mode for smaller displays */
|
|
30
|
+
compact?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// COMPONENT
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
export function AudioShortcutsPopover({ compact = false }: AudioShortcutsPopoverProps) {
|
|
38
|
+
const trigger = (
|
|
39
|
+
<PopoverTrigger asChild>
|
|
40
|
+
<Button
|
|
41
|
+
variant="ghost"
|
|
42
|
+
size="icon"
|
|
43
|
+
className={compact ? 'h-6 w-6' : 'size-7 text-muted-foreground hover:text-foreground'}
|
|
44
|
+
title={compact ? undefined : 'Keyboard shortcuts'}
|
|
45
|
+
>
|
|
46
|
+
<Keyboard className={compact ? 'h-3.5 w-3.5' : 'size-4'} />
|
|
47
|
+
</Button>
|
|
48
|
+
</PopoverTrigger>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Popover>
|
|
53
|
+
{compact ? (
|
|
54
|
+
<Tooltip>
|
|
55
|
+
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
|
|
56
|
+
<TooltipContent side="bottom">Shortcuts</TooltipContent>
|
|
57
|
+
</Tooltip>
|
|
58
|
+
) : (
|
|
59
|
+
trigger
|
|
60
|
+
)}
|
|
61
|
+
<PopoverContent className="w-56 p-0" align="end">
|
|
62
|
+
<div className="px-3 py-2 border-b">
|
|
63
|
+
<h4 className="font-medium text-sm">Player Shortcuts</h4>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="p-2 space-y-3 max-h-72 overflow-y-auto">
|
|
66
|
+
{AUDIO_SHORTCUTS.map((group, groupIndex) => (
|
|
67
|
+
<div key={group.title}>
|
|
68
|
+
{groupIndex > 0 && <Separator className="my-2" />}
|
|
69
|
+
<div className="text-xs font-medium text-muted-foreground mb-1.5 px-1">
|
|
70
|
+
{group.title}
|
|
71
|
+
</div>
|
|
72
|
+
<div className="space-y-1">
|
|
73
|
+
{group.shortcuts.map((shortcut) => (
|
|
74
|
+
<div
|
|
75
|
+
key={shortcut.label}
|
|
76
|
+
className="flex items-center justify-between px-1 py-0.5"
|
|
77
|
+
>
|
|
78
|
+
<span className="text-sm text-foreground">
|
|
79
|
+
{shortcut.label}
|
|
80
|
+
</span>
|
|
81
|
+
<KbdGroup>
|
|
82
|
+
{shortcut.keys.map((key, i) => (
|
|
83
|
+
<Kbd key={i} size="sm">
|
|
84
|
+
{key}
|
|
85
|
+
</Kbd>
|
|
86
|
+
))}
|
|
87
|
+
</KbdGroup>
|
|
88
|
+
</div>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
</PopoverContent>
|
|
95
|
+
</Popover>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default AudioShortcutsPopover;
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
* Audio analysis is performed in the AudioProvider context to persist
|
|
10
|
+
* across variant changes (including switching to 'none' and back).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { type ReactNode } from 'react';
|
|
14
|
+
import { cn } from '@djangocfg/ui-nextjs';
|
|
15
|
+
import { useAudioElement } from '../../context';
|
|
16
|
+
import {
|
|
17
|
+
type EffectVariant,
|
|
18
|
+
type EffectIntensity,
|
|
19
|
+
type EffectColorScheme,
|
|
20
|
+
getEffectConfig,
|
|
21
|
+
prepareEffectColors,
|
|
22
|
+
calculateGlowLayers,
|
|
23
|
+
calculateOrbs,
|
|
24
|
+
calculateMeshGradients,
|
|
25
|
+
calculateSpotlight,
|
|
26
|
+
EFFECT_ANIMATIONS,
|
|
27
|
+
} from '../../effects';
|
|
28
|
+
import { GlowEffect, OrbsEffect, SpotlightEffect, MeshEffect, type GlowEffectData } from './effects';
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// TYPES
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
export interface AudioReactiveCoverProps {
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
size?: 'sm' | 'md' | 'lg';
|
|
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
|
+
};
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// COMPONENT
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
export function AudioReactiveCover({
|
|
59
|
+
children,
|
|
60
|
+
size = 'lg',
|
|
61
|
+
variant = 'spotlight',
|
|
62
|
+
intensity = 'medium',
|
|
63
|
+
colorScheme = 'primary',
|
|
64
|
+
onClick,
|
|
65
|
+
className,
|
|
66
|
+
}: AudioReactiveCoverProps) {
|
|
67
|
+
// Get audio levels from context (persists across variant changes)
|
|
68
|
+
const { isPlaying, audioLevels: levels } = useAudioElement();
|
|
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 */}
|
|
114
|
+
{glowData && (
|
|
115
|
+
<GlowEffect data={glowData} colors={colors} isPlaying={isPlaying} />
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{orbsData && (
|
|
119
|
+
<OrbsEffect orbs={orbsData} blur={effectConfig.blur} isPlaying={isPlaying} />
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{spotlightData && (
|
|
123
|
+
<SpotlightEffect data={spotlightData} colors={colors} blur={effectConfig.blur} isPlaying={isPlaying} />
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{meshData && (
|
|
127
|
+
<MeshEffect gradients={meshData} blur={effectConfig.blur} isPlaying={isPlaying} />
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{/* Content (cover art) */}
|
|
131
|
+
<div
|
|
132
|
+
className="relative w-full h-full rounded-lg overflow-hidden shadow-2xl z-10 bg-background cursor-pointer"
|
|
133
|
+
onClick={onClick}
|
|
134
|
+
role={onClick ? 'button' : undefined}
|
|
135
|
+
tabIndex={onClick ? 0 : undefined}
|
|
136
|
+
onKeyDown={onClick ? (e) => e.key === 'Enter' && onClick() : undefined}
|
|
137
|
+
>
|
|
138
|
+
{children}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Inject animations once */}
|
|
142
|
+
<style dangerouslySetInnerHTML={{ __html: EFFECT_ANIMATIONS }} />
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
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 '@djangocfg/ui-nextjs';
|
|
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 '@djangocfg/ui-nextjs';
|
|
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 '@djangocfg/ui-nextjs';
|
|
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
|
+
}
|