@djangocfg/ui-nextjs 2.1.82 → 2.1.83

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 (33) hide show
  1. package/package.json +4 -4
  2. package/src/tools/AudioPlayer/@refactoring3/00-IMPLEMENTATION-ROADMAP.md +1146 -0
  3. package/src/tools/AudioPlayer/@refactoring3/01-WAVESURFER-STREAMING-ANALYSIS.md +611 -0
  4. package/src/tools/AudioPlayer/@refactoring3/02-MEDIA-VIEWER-ANALYSIS.md +560 -0
  5. package/src/tools/AudioPlayer/@refactoring3/03-HYBRID-ARCHITECTURE-PROPOSAL.md +769 -0
  6. package/src/tools/AudioPlayer/@refactoring3/04-CRACKLING-ISSUE-DIAGNOSIS.md +373 -0
  7. package/src/tools/AudioPlayer/README.md +177 -205
  8. package/src/tools/AudioPlayer/components/AudioPlayer.tsx +9 -4
  9. package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +251 -0
  10. package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +291 -0
  11. package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
  12. package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +16 -26
  13. package/src/tools/AudioPlayer/components/index.ts +6 -1
  14. package/src/tools/AudioPlayer/context/AudioProvider.tsx +8 -3
  15. package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +121 -0
  16. package/src/tools/AudioPlayer/context/index.ts +14 -2
  17. package/src/tools/AudioPlayer/hooks/index.ts +11 -0
  18. package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
  19. package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
  20. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +6 -3
  21. package/src/tools/AudioPlayer/index.ts +31 -0
  22. package/src/tools/AudioPlayer/progressive/ProgressiveAudioPlayer.tsx +8 -0
  23. package/src/tools/index.ts +22 -0
  24. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +0 -148
  25. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +0 -301
  26. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +0 -281
  27. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +0 -328
  28. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +0 -251
  29. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +0 -427
  30. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +0 -193
  31. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +0 -146
  32. package/src/tools/AudioPlayer/@refactoring2/ISSUE_ANALYSIS.md +0 -187
  33. package/src/tools/AudioPlayer/@refactoring2/PLAN.md +0 -372
@@ -0,0 +1,251 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * HybridAudioPlayer - Audio playback controls with frequency visualization
5
+ *
6
+ * Uses HybridAudioContext for state management.
7
+ * Native HTML5 audio for playback, Web Audio API for visualization only.
8
+ *
9
+ * Features:
10
+ * - Real-time frequency visualization
11
+ * - Playback controls (play, pause, skip, restart)
12
+ * - Volume control with mute
13
+ * - Loop toggle
14
+ * - Keyboard shortcuts
15
+ * - Timer display
16
+ */
17
+
18
+ import { memo } from 'react';
19
+ import {
20
+ Play,
21
+ Pause,
22
+ RotateCcw,
23
+ SkipBack,
24
+ SkipForward,
25
+ Volume2,
26
+ VolumeX,
27
+ Loader2,
28
+ Repeat,
29
+ } from 'lucide-react';
30
+ import { Button, Slider, cn } from '@djangocfg/ui-nextjs';
31
+ import { useHybridAudioContext } from '../context/HybridAudioProvider';
32
+ import { HybridWaveform } from './HybridWaveform';
33
+ import { AudioEqualizer } from './AudioEqualizer';
34
+ import { AudioShortcutsPopover } from './AudioShortcutsPopover';
35
+ import { useAudioHotkeys } from '../hooks';
36
+ import { formatTime } from '../utils';
37
+
38
+ // =============================================================================
39
+ // TYPES
40
+ // =============================================================================
41
+
42
+ export interface HybridAudioPlayerProps {
43
+ /** Show playback controls */
44
+ showControls?: boolean;
45
+ /** Show frequency waveform */
46
+ showWaveform?: boolean;
47
+ /** Waveform visualization mode */
48
+ waveformMode?: 'frequency' | 'static';
49
+ /** Waveform height in pixels */
50
+ waveformHeight?: number;
51
+ /** Show equalizer bars */
52
+ showEqualizer?: boolean;
53
+ /** Show time display */
54
+ showTimer?: boolean;
55
+ /** Show volume control */
56
+ showVolume?: boolean;
57
+ /** Show loop button */
58
+ showLoop?: boolean;
59
+ /** Equalizer options */
60
+ equalizerOptions?: {
61
+ barCount?: number;
62
+ height?: number;
63
+ gap?: number;
64
+ showPeaks?: boolean;
65
+ barColor?: string;
66
+ peakColor?: string;
67
+ };
68
+ /** Additional CSS class */
69
+ className?: string;
70
+ /** Inline styles */
71
+ style?: React.CSSProperties;
72
+ }
73
+
74
+ // =============================================================================
75
+ // COMPONENT
76
+ // =============================================================================
77
+
78
+ export const HybridAudioPlayer = memo(function HybridAudioPlayer({
79
+ showControls = true,
80
+ showWaveform = true,
81
+ waveformMode = 'frequency',
82
+ waveformHeight = 64,
83
+ showEqualizer = false,
84
+ showTimer = true,
85
+ showVolume = true,
86
+ showLoop = true,
87
+ equalizerOptions = {},
88
+ className,
89
+ style,
90
+ }: HybridAudioPlayerProps) {
91
+ const { state, controls } = useHybridAudioContext();
92
+
93
+ // Enable keyboard shortcuts
94
+ useAudioHotkeys({ enabled: state.isReady });
95
+
96
+ const isLoading = !state.isReady;
97
+
98
+ const handleVolumeChange = (value: number[]) => {
99
+ controls.setVolume(value[0] / 100);
100
+ };
101
+
102
+ return (
103
+ <div
104
+ className={cn('flex flex-col gap-3 p-4 rounded-lg bg-card border', className)}
105
+ style={style}
106
+ >
107
+ {/* Frequency Waveform */}
108
+ {showWaveform && (
109
+ <div className="relative">
110
+ <HybridWaveform
111
+ mode={waveformMode}
112
+ height={waveformHeight}
113
+ className={cn(isLoading && 'opacity-50')}
114
+ />
115
+ {isLoading && (
116
+ <div className="absolute inset-0 flex items-center justify-center">
117
+ <Loader2 className="h-6 w-6 animate-spin text-primary" />
118
+ </div>
119
+ )}
120
+ </div>
121
+ )}
122
+
123
+ {/* Equalizer animation */}
124
+ {showEqualizer && (
125
+ <AudioEqualizer
126
+ barCount={equalizerOptions.barCount}
127
+ height={equalizerOptions.height}
128
+ gap={equalizerOptions.gap}
129
+ showPeaks={equalizerOptions.showPeaks}
130
+ barColor={equalizerOptions.barColor}
131
+ peakColor={equalizerOptions.peakColor}
132
+ className="px-1"
133
+ />
134
+ )}
135
+
136
+ {/* Timer */}
137
+ {showTimer && (
138
+ <div className="flex justify-between text-xs text-muted-foreground tabular-nums px-1">
139
+ <span>{formatTime(state.currentTime)}</span>
140
+ <span>{formatTime(state.duration)}</span>
141
+ </div>
142
+ )}
143
+
144
+ {/* Controls */}
145
+ {showControls && (
146
+ <div className="flex items-center justify-center gap-1">
147
+ {/* Restart */}
148
+ <Button
149
+ variant="ghost"
150
+ size="icon"
151
+ className="h-9 w-9"
152
+ onClick={controls.restart}
153
+ disabled={!state.isReady}
154
+ title="Restart"
155
+ >
156
+ <RotateCcw className="h-4 w-4" />
157
+ </Button>
158
+
159
+ {/* Skip back 5s */}
160
+ <Button
161
+ variant="ghost"
162
+ size="icon"
163
+ className="h-9 w-9"
164
+ onClick={() => controls.skip(-5)}
165
+ disabled={!state.isReady}
166
+ title="Back 5 seconds"
167
+ >
168
+ <SkipBack className="h-4 w-4" />
169
+ </Button>
170
+
171
+ {/* Play/Pause */}
172
+ <Button
173
+ variant="default"
174
+ size="icon"
175
+ className="h-12 w-12 rounded-full"
176
+ onClick={controls.togglePlay}
177
+ disabled={!state.isReady && !isLoading}
178
+ title={state.isPlaying ? 'Pause' : 'Play'}
179
+ >
180
+ {isLoading ? (
181
+ <Loader2 className="h-5 w-5 animate-spin" />
182
+ ) : state.isPlaying ? (
183
+ <Pause className="h-5 w-5" />
184
+ ) : (
185
+ <Play className="h-5 w-5 ml-0.5" />
186
+ )}
187
+ </Button>
188
+
189
+ {/* Skip forward 5s */}
190
+ <Button
191
+ variant="ghost"
192
+ size="icon"
193
+ className="h-9 w-9"
194
+ onClick={() => controls.skip(5)}
195
+ disabled={!state.isReady}
196
+ title="Forward 5 seconds"
197
+ >
198
+ <SkipForward className="h-4 w-4" />
199
+ </Button>
200
+
201
+ {/* Volume */}
202
+ {showVolume && (
203
+ <>
204
+ <Button
205
+ variant="ghost"
206
+ size="icon"
207
+ className="h-9 w-9"
208
+ onClick={controls.toggleMute}
209
+ title={state.isMuted ? 'Unmute' : 'Mute'}
210
+ >
211
+ {state.isMuted || state.volume === 0 ? (
212
+ <VolumeX className="h-4 w-4" />
213
+ ) : (
214
+ <Volume2 className="h-4 w-4" />
215
+ )}
216
+ </Button>
217
+
218
+ <Slider
219
+ value={[state.isMuted ? 0 : state.volume * 100]}
220
+ max={100}
221
+ step={1}
222
+ onValueChange={handleVolumeChange}
223
+ className="w-20"
224
+ aria-label="Volume"
225
+ />
226
+ </>
227
+ )}
228
+
229
+ {/* Loop/Repeat */}
230
+ {showLoop && (
231
+ <Button
232
+ variant="ghost"
233
+ size="icon"
234
+ className={cn('h-9 w-9', state.isLooping && 'text-primary')}
235
+ onClick={controls.toggleLoop}
236
+ disabled={!state.isReady}
237
+ title={state.isLooping ? 'Disable loop' : 'Enable loop'}
238
+ >
239
+ <Repeat className="h-4 w-4" />
240
+ </Button>
241
+ )}
242
+
243
+ {/* Shortcuts help */}
244
+ <AudioShortcutsPopover compact />
245
+ </div>
246
+ )}
247
+ </div>
248
+ );
249
+ });
250
+
251
+ export default HybridAudioPlayer;
@@ -0,0 +1,291 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * HybridSimplePlayer - Easy-to-use hybrid audio player wrapper
5
+ *
6
+ * Combines HybridAudioProvider + HybridAudioPlayer + optional reactive cover
7
+ * in a single component with sensible defaults.
8
+ *
9
+ * Uses native HTML5 audio for playback (no crackling) with Web Audio API
10
+ * for visualization only.
11
+ *
12
+ * @example
13
+ * // Minimal usage
14
+ * <HybridSimplePlayer src="https://example.com/audio.mp3" />
15
+ *
16
+ * @example
17
+ * // With cover art and reactive effects
18
+ * <HybridSimplePlayer
19
+ * src={audioUrl}
20
+ * title="Track Title"
21
+ * artist="Artist Name"
22
+ * coverArt="/path/to/cover.jpg"
23
+ * reactiveCover
24
+ * variant="spotlight"
25
+ * />
26
+ */
27
+
28
+ import { type ReactNode } from 'react';
29
+ import { Music } from 'lucide-react';
30
+ import { cn } from '@djangocfg/ui-core';
31
+
32
+ import { HybridAudioProvider } from '../context/HybridAudioProvider';
33
+ import { HybridAudioPlayer } from './HybridAudioPlayer';
34
+ import { AudioReactiveCover } from './ReactiveCover';
35
+ import { VisualizationProvider, useVisualization } from '../hooks';
36
+ import type { EqualizerOptions } from '../types';
37
+ import type { EffectIntensity, EffectColorScheme } from '../effects';
38
+ import type { VisualizationVariant } from '../hooks';
39
+
40
+ // =============================================================================
41
+ // TYPES
42
+ // =============================================================================
43
+
44
+ export interface HybridSimplePlayerProps {
45
+ /** Audio source URL */
46
+ src: string;
47
+
48
+ /** Track title */
49
+ title?: string;
50
+
51
+ /** Artist name */
52
+ artist?: string;
53
+
54
+ /** Cover art URL or ReactNode */
55
+ coverArt?: string | ReactNode;
56
+
57
+ /** Cover art size */
58
+ coverSize?: 'sm' | 'md' | 'lg';
59
+
60
+ /** Show frequency waveform */
61
+ showWaveform?: boolean;
62
+
63
+ /** Waveform visualization mode */
64
+ waveformMode?: 'frequency' | 'static';
65
+
66
+ /** Waveform height in pixels */
67
+ waveformHeight?: number;
68
+
69
+ /** Show equalizer bars */
70
+ showEqualizer?: boolean;
71
+
72
+ /** Show timer */
73
+ showTimer?: boolean;
74
+
75
+ /** Show volume control */
76
+ showVolume?: boolean;
77
+
78
+ /** Show loop/repeat button */
79
+ showLoop?: boolean;
80
+
81
+ /** Enable audio-reactive cover effects */
82
+ reactiveCover?: boolean;
83
+
84
+ /** Reactive effect variant */
85
+ variant?: VisualizationVariant;
86
+
87
+ /** Reactive effect intensity */
88
+ intensity?: EffectIntensity;
89
+
90
+ /** Reactive effect color scheme */
91
+ colorScheme?: EffectColorScheme;
92
+
93
+ /** Auto-play on load */
94
+ autoPlay?: boolean;
95
+
96
+ /** Loop playback */
97
+ loop?: boolean;
98
+
99
+ /** Initial volume (0-1) */
100
+ initialVolume?: number;
101
+
102
+ /** Equalizer customization */
103
+ equalizerOptions?: EqualizerOptions;
104
+
105
+ /** Layout direction */
106
+ layout?: 'vertical' | 'horizontal';
107
+
108
+ /** Additional class name */
109
+ className?: string;
110
+
111
+ /** Callbacks */
112
+ onPlay?: () => void;
113
+ onPause?: () => void;
114
+ onEnded?: () => void;
115
+ onError?: (error: Error) => void;
116
+ }
117
+
118
+ // =============================================================================
119
+ // CONSTANTS
120
+ // =============================================================================
121
+
122
+ const COVER_SIZES = {
123
+ sm: 'w-24 h-24',
124
+ md: 'w-32 h-32',
125
+ lg: 'w-48 h-48',
126
+ };
127
+
128
+ // =============================================================================
129
+ // COMPONENT
130
+ // =============================================================================
131
+
132
+ export function HybridSimplePlayer(props: HybridSimplePlayerProps) {
133
+ return (
134
+ <VisualizationProvider>
135
+ <HybridSimplePlayerContent {...props} />
136
+ </VisualizationProvider>
137
+ );
138
+ }
139
+
140
+ function HybridSimplePlayerContent({
141
+ src,
142
+ title,
143
+ artist,
144
+ coverArt,
145
+ coverSize = 'md',
146
+ showWaveform = true,
147
+ waveformMode = 'frequency',
148
+ waveformHeight = 64,
149
+ showEqualizer = false,
150
+ showTimer = true,
151
+ showVolume = true,
152
+ showLoop = true,
153
+ reactiveCover = true,
154
+ variant,
155
+ intensity,
156
+ colorScheme,
157
+ autoPlay = false,
158
+ loop = false,
159
+ initialVolume = 1,
160
+ equalizerOptions,
161
+ layout = 'vertical',
162
+ className,
163
+ onPlay,
164
+ onPause,
165
+ onEnded,
166
+ onError,
167
+ }: HybridSimplePlayerProps) {
168
+ const { settings: vizSettings, nextVariant } = useVisualization();
169
+
170
+ // Determine effective variant (from props or localStorage settings)
171
+ const effectiveVariant =
172
+ variant ?? (vizSettings.variant !== 'none' ? vizSettings.variant : 'spotlight');
173
+ const effectiveIntensity = intensity ?? vizSettings.intensity;
174
+ const effectiveColorScheme = colorScheme ?? vizSettings.colorScheme;
175
+
176
+ // Show reactive cover if enabled and variant is not 'none'
177
+ const showReactiveCover = reactiveCover && effectiveVariant !== 'none';
178
+
179
+ // Render cover art content
180
+ const renderCoverContent = () => {
181
+ if (typeof coverArt === 'string') {
182
+ return (
183
+ <img src={coverArt} alt={title || 'Album cover'} className="w-full h-full object-cover" />
184
+ );
185
+ }
186
+
187
+ if (coverArt) {
188
+ return coverArt;
189
+ }
190
+
191
+ // Default placeholder
192
+ return (
193
+ <div className="w-full h-full bg-muted/30 flex items-center justify-center">
194
+ <Music className="w-1/3 h-1/3 text-muted-foreground/50" />
195
+ </div>
196
+ );
197
+ };
198
+
199
+ const isHorizontal = layout === 'horizontal';
200
+
201
+ return (
202
+ <HybridAudioProvider
203
+ src={src}
204
+ autoPlay={autoPlay}
205
+ loop={loop}
206
+ initialVolume={initialVolume}
207
+ onPlay={onPlay}
208
+ onPause={onPause}
209
+ onEnded={onEnded}
210
+ onError={onError}
211
+ >
212
+ <div
213
+ className={cn(
214
+ 'flex gap-4',
215
+ isHorizontal ? 'flex-row items-center' : 'flex-col items-center',
216
+ className
217
+ )}
218
+ >
219
+ {/* Cover Art */}
220
+ {(coverArt || reactiveCover) && (
221
+ <div className="flex flex-col items-center gap-2 shrink-0">
222
+ {showReactiveCover ? (
223
+ <AudioReactiveCover
224
+ size={coverSize}
225
+ variant={effectiveVariant as 'glow' | 'orbs' | 'spotlight' | 'mesh'}
226
+ intensity={effectiveIntensity}
227
+ colorScheme={effectiveColorScheme}
228
+ onClick={nextVariant}
229
+ >
230
+ <div className={cn('rounded-lg overflow-hidden', COVER_SIZES[coverSize])}>
231
+ {renderCoverContent()}
232
+ </div>
233
+ </AudioReactiveCover>
234
+ ) : (
235
+ <div
236
+ className={cn(
237
+ 'rounded-lg overflow-hidden shadow-lg cursor-pointer',
238
+ COVER_SIZES[coverSize]
239
+ )}
240
+ onClick={nextVariant}
241
+ role="button"
242
+ tabIndex={0}
243
+ onKeyDown={(e) => e.key === 'Enter' && nextVariant()}
244
+ >
245
+ {renderCoverContent()}
246
+ </div>
247
+ )}
248
+
249
+ {/* Effect indicator */}
250
+ {reactiveCover && (
251
+ <span className="text-[10px] uppercase tracking-wider text-muted-foreground/50 select-none">
252
+ {vizSettings.variant === 'none' ? 'off' : vizSettings.variant}
253
+ </span>
254
+ )}
255
+ </div>
256
+ )}
257
+
258
+ {/* Track Info + Player */}
259
+ <div
260
+ className={cn('flex flex-col gap-3', isHorizontal ? 'flex-1 min-w-0' : 'w-full max-w-md')}
261
+ >
262
+ {/* Track Info */}
263
+ {(title || artist) && (
264
+ <div className={cn('text-center', isHorizontal && 'text-left')}>
265
+ {title && (
266
+ <h3 className="text-base font-medium text-foreground truncate">{title}</h3>
267
+ )}
268
+ {artist && <p className="text-sm text-muted-foreground truncate">{artist}</p>}
269
+ </div>
270
+ )}
271
+
272
+ {/* Audio Player */}
273
+ <HybridAudioPlayer
274
+ showControls
275
+ showWaveform={showWaveform}
276
+ waveformMode={waveformMode}
277
+ waveformHeight={waveformHeight}
278
+ showEqualizer={showEqualizer}
279
+ showTimer={showTimer}
280
+ showVolume={showVolume}
281
+ showLoop={showLoop}
282
+ equalizerOptions={equalizerOptions}
283
+ className="border-0 bg-transparent"
284
+ />
285
+ </div>
286
+ </div>
287
+ </HybridAudioProvider>
288
+ );
289
+ }
290
+
291
+ export default HybridSimplePlayer;