@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,225 +0,0 @@
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, useMemo } 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 { useAppT } from '@djangocfg/i18n';
31
- import { Button, Slider, cn } from '../../_shared';
32
- import { useHybridAudioContext } from '../context/HybridAudioProvider';
33
- import { HybridWaveform } from './HybridWaveform';
34
- import { formatTime } from '../utils';
35
-
36
- // =============================================================================
37
- // TYPES
38
- // =============================================================================
39
-
40
- export interface HybridAudioPlayerProps {
41
- /** Show playback controls */
42
- showControls?: boolean;
43
- /** Show frequency waveform */
44
- showWaveform?: boolean;
45
- /** Waveform visualization mode */
46
- waveformMode?: 'frequency' | 'static';
47
- /** Waveform height in pixels */
48
- waveformHeight?: number;
49
- /** Show time display */
50
- showTimer?: boolean;
51
- /** Show volume control */
52
- showVolume?: boolean;
53
- /** Show loop button */
54
- showLoop?: boolean;
55
- /** Additional CSS class */
56
- className?: string;
57
- /** Inline styles */
58
- style?: React.CSSProperties;
59
- }
60
-
61
- // =============================================================================
62
- // COMPONENT
63
- // =============================================================================
64
-
65
- export const HybridAudioPlayer = memo(function HybridAudioPlayer({
66
- showControls = true,
67
- showWaveform = true,
68
- waveformMode = 'frequency',
69
- waveformHeight = 64,
70
- showTimer = true,
71
- showVolume = true,
72
- showLoop = true,
73
- className,
74
- style,
75
- }: HybridAudioPlayerProps) {
76
- const t = useAppT();
77
- const { state, controls } = useHybridAudioContext();
78
-
79
- const labels = useMemo(() => ({
80
- restart: t('tools.audio.restart'),
81
- back: t('tools.audio.back'),
82
- forward: t('tools.audio.forward'),
83
- volume: t('tools.audio.volume'),
84
- }), [t]);
85
-
86
- const isLoading = !state.isReady;
87
-
88
- const handleVolumeChange = (value: number[]) => {
89
- controls.setVolume(value[0] / 100);
90
- };
91
-
92
- return (
93
- <div
94
- className={cn('flex flex-col gap-3 p-4 rounded-lg bg-card border', className)}
95
- style={style}
96
- >
97
- {/* Frequency Waveform */}
98
- {showWaveform && (
99
- <div className="relative">
100
- <HybridWaveform
101
- mode={waveformMode}
102
- height={waveformHeight}
103
- className={cn(isLoading && 'opacity-50')}
104
- />
105
- {isLoading && (
106
- <div className="absolute inset-0 flex items-center justify-center">
107
- <Loader2 className="h-6 w-6 animate-spin text-primary" />
108
- </div>
109
- )}
110
- </div>
111
- )}
112
-
113
- {/* Timer */}
114
- {showTimer && (
115
- <div className="flex justify-between text-xs text-muted-foreground tabular-nums px-1">
116
- <span>{formatTime(state.currentTime)}</span>
117
- <span>{formatTime(state.duration)}</span>
118
- </div>
119
- )}
120
-
121
- {/* Controls */}
122
- {showControls && (
123
- <div className="flex items-center justify-center gap-1">
124
- {/* Restart */}
125
- <Button
126
- variant="ghost"
127
- size="icon"
128
- className="h-9 w-9"
129
- onClick={controls.restart}
130
- disabled={!state.isReady}
131
- title={labels.restart}
132
- >
133
- <RotateCcw className="h-4 w-4" />
134
- </Button>
135
-
136
- {/* Skip back 5s */}
137
- <Button
138
- variant="ghost"
139
- size="icon"
140
- className="h-9 w-9"
141
- onClick={() => controls.skip(-5)}
142
- disabled={!state.isReady}
143
- title={labels.back}
144
- >
145
- <SkipBack className="h-4 w-4" />
146
- </Button>
147
-
148
- {/* Play/Pause */}
149
- <Button
150
- variant="default"
151
- size="icon"
152
- className="h-12 w-12 rounded-full"
153
- onClick={controls.togglePlay}
154
- disabled={!state.isReady && !isLoading}
155
- title={state.isPlaying ? 'Pause' : 'Play'}
156
- >
157
- {isLoading ? (
158
- <Loader2 className="h-5 w-5 animate-spin" />
159
- ) : state.isPlaying ? (
160
- <Pause className="h-5 w-5" />
161
- ) : (
162
- <Play className="h-5 w-5 ml-0.5" />
163
- )}
164
- </Button>
165
-
166
- {/* Skip forward 5s */}
167
- <Button
168
- variant="ghost"
169
- size="icon"
170
- className="h-9 w-9"
171
- onClick={() => controls.skip(5)}
172
- disabled={!state.isReady}
173
- title={labels.forward}
174
- >
175
- <SkipForward className="h-4 w-4" />
176
- </Button>
177
-
178
- {/* Volume */}
179
- {showVolume && (
180
- <>
181
- <Button
182
- variant="ghost"
183
- size="icon"
184
- className="h-9 w-9"
185
- onClick={controls.toggleMute}
186
- title={state.isMuted ? 'Unmute' : 'Mute'}
187
- >
188
- {state.isMuted || state.volume === 0 ? (
189
- <VolumeX className="h-4 w-4" />
190
- ) : (
191
- <Volume2 className="h-4 w-4" />
192
- )}
193
- </Button>
194
-
195
- <Slider
196
- value={[state.isMuted ? 0 : state.volume * 100]}
197
- max={100}
198
- step={1}
199
- onValueChange={handleVolumeChange}
200
- className="w-20"
201
- aria-label={labels.volume}
202
- />
203
- </>
204
- )}
205
-
206
- {/* Loop/Repeat */}
207
- {showLoop && (
208
- <Button
209
- variant="ghost"
210
- size="icon"
211
- className={cn('h-9 w-9', state.isLooping && 'text-primary')}
212
- onClick={controls.toggleLoop}
213
- disabled={!state.isReady}
214
- title={state.isLooping ? 'Disable loop' : 'Enable loop'}
215
- >
216
- <Repeat className="h-4 w-4" />
217
- </Button>
218
- )}
219
- </div>
220
- )}
221
- </div>
222
- );
223
- });
224
-
225
- export default HybridAudioPlayer;
@@ -1,163 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * HybridCompactPlayer - Single-row audio player
5
- *
6
- * Designed for tight spaces: play/pause + waveform + timer.
7
- * No cover art, no volume slider, no skip buttons.
8
- *
9
- * @example
10
- * <HybridCompactPlayer src="https://example.com/audio.mp3" title="Rain & Thunder" />
11
- *
12
- * @example
13
- * // Lazy-loaded (preferred in app)
14
- * <LazyHybridCompactPlayer src={url} autoPlay />
15
- */
16
-
17
- import { type ReactNode } from 'react';
18
- import { Play, Pause, Loader2 } from 'lucide-react';
19
- import { cn } from '@djangocfg/ui-core';
20
- import { Button } from '../../_shared';
21
-
22
- import { HybridAudioProvider } from '../context/HybridAudioProvider';
23
- import { HybridWaveform } from './HybridWaveform';
24
- import { useHybridAudioContext } from '../context/HybridAudioProvider';
25
- import { formatTime } from '../utils';
26
-
27
- // =============================================================================
28
- // TYPES
29
- // =============================================================================
30
-
31
- export interface HybridCompactPlayerProps {
32
- /** Audio source URL */
33
- src: string;
34
- /** Track title (shown as tooltip / aria-label) */
35
- title?: string;
36
- /** Auto-play on load */
37
- autoPlay?: boolean;
38
- /** Loop playback */
39
- loop?: boolean;
40
- /** Initial volume (0-1) */
41
- initialVolume?: number;
42
- /** Waveform visualization mode */
43
- waveformMode?: 'frequency' | 'static';
44
- /** Show timer */
45
- showTimer?: boolean;
46
- /** Button size — 'sm' (h-6 w-6) | 'md' (h-8 w-8, default) */
47
- buttonSize?: 'sm' | 'md';
48
- /** Additional class name */
49
- className?: string;
50
- /** Callbacks */
51
- onPlay?: () => void;
52
- onPause?: () => void;
53
- onEnded?: () => void;
54
- onError?: (error: Error) => void;
55
- }
56
-
57
- // =============================================================================
58
- // COMPONENT
59
- // =============================================================================
60
-
61
- export function HybridCompactPlayer({
62
- src,
63
- title,
64
- autoPlay = false,
65
- loop = false,
66
- initialVolume = 1,
67
- waveformMode = 'frequency',
68
- showTimer = true,
69
- buttonSize = 'md',
70
- className,
71
- onPlay,
72
- onPause,
73
- onEnded,
74
- onError,
75
- }: HybridCompactPlayerProps): ReactNode {
76
- return (
77
- <HybridAudioProvider
78
- src={src}
79
- autoPlay={autoPlay}
80
- loop={loop}
81
- initialVolume={initialVolume}
82
- onPlay={onPlay}
83
- onPause={onPause}
84
- onEnded={onEnded}
85
- onError={onError}
86
- >
87
- <HybridCompactPlayerInner
88
- title={title}
89
- waveformMode={waveformMode}
90
- showTimer={showTimer}
91
- buttonSize={buttonSize}
92
- className={className}
93
- />
94
- </HybridAudioProvider>
95
- );
96
- }
97
-
98
- // =============================================================================
99
- // INNER (needs context)
100
- // =============================================================================
101
-
102
- const BUTTON_SIZE = {
103
- sm: 'h-6 w-6',
104
- md: 'h-8 w-8',
105
- } as const;
106
-
107
- interface InnerProps {
108
- title?: string;
109
- waveformMode: 'frequency' | 'static';
110
- showTimer: boolean;
111
- buttonSize: 'sm' | 'md';
112
- className?: string;
113
- }
114
-
115
- function HybridCompactPlayerInner({ title, waveformMode, showTimer, buttonSize, className }: InnerProps) {
116
- const { state, controls } = useHybridAudioContext();
117
- const isLoading = !state.isReady;
118
-
119
- return (
120
- <div className={cn('flex items-center gap-2 w-full', className)}>
121
- {/* Play / Pause */}
122
- <Button
123
- variant="ghost"
124
- size="icon"
125
- className={cn(BUTTON_SIZE[buttonSize], 'flex-shrink-0')}
126
- onClick={controls.togglePlay}
127
- disabled={!state.isReady && !isLoading}
128
- title={title ?? (state.isPlaying ? 'Pause' : 'Play')}
129
- aria-label={state.isPlaying ? 'Pause' : 'Play'}
130
- >
131
- {isLoading ? (
132
- <Loader2 className="h-3.5 w-3.5 animate-spin" />
133
- ) : state.isPlaying ? (
134
- <Pause className="h-3.5 w-3.5" />
135
- ) : (
136
- <Play className="h-3.5 w-3.5 ml-0.5" />
137
- )}
138
- </Button>
139
-
140
- {/* Waveform */}
141
- <div className="flex-1 min-w-0">
142
- <HybridWaveform
143
- mode={waveformMode}
144
- height={32}
145
- barWidth={2}
146
- barGap={1}
147
- className={cn(isLoading && 'opacity-40')}
148
- />
149
- </div>
150
-
151
- {/* Timer */}
152
- {showTimer && (
153
- <span className="text-[11px] text-muted-foreground tabular-nums flex-shrink-0">
154
- {formatTime(state.currentTime)}
155
- <span className="text-muted-foreground/50"> / </span>
156
- {formatTime(state.duration)}
157
- </span>
158
- )}
159
- </div>
160
- );
161
- }
162
-
163
- export default HybridCompactPlayer;
@@ -1,284 +0,0 @@
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 { EffectIntensity, EffectColorScheme } from '../effects';
37
- import type { VisualizationVariant } from '../hooks';
38
-
39
- // =============================================================================
40
- // TYPES
41
- // =============================================================================
42
-
43
- export interface HybridSimplePlayerProps {
44
- /** Audio source URL */
45
- src: string;
46
-
47
- /** Track title */
48
- title?: string;
49
-
50
- /** Artist name */
51
- artist?: string;
52
-
53
- /** Cover art URL or ReactNode */
54
- coverArt?: string | ReactNode;
55
-
56
- /** Cover art size */
57
- coverSize?: 'sm' | 'md' | 'lg';
58
-
59
- /** Show frequency waveform */
60
- showWaveform?: boolean;
61
-
62
- /** Waveform visualization mode */
63
- waveformMode?: 'frequency' | 'static';
64
-
65
- /** Waveform height in pixels */
66
- waveformHeight?: number;
67
-
68
- /** Show timer */
69
- showTimer?: boolean;
70
-
71
- /** Show volume control */
72
- showVolume?: boolean;
73
-
74
- /** Show loop/repeat button */
75
- showLoop?: boolean;
76
-
77
- /** Enable audio-reactive cover effects */
78
- reactiveCover?: boolean;
79
-
80
- /** Reactive effect variant */
81
- variant?: VisualizationVariant;
82
-
83
- /** Reactive effect intensity */
84
- intensity?: EffectIntensity;
85
-
86
- /** Reactive effect color scheme */
87
- colorScheme?: EffectColorScheme;
88
-
89
- /** Auto-play on load */
90
- autoPlay?: boolean;
91
-
92
- /** Loop playback */
93
- loop?: boolean;
94
-
95
- /** Initial volume (0-1) */
96
- initialVolume?: number;
97
-
98
- /** Layout direction */
99
- layout?: 'vertical' | 'horizontal';
100
-
101
- /** Additional class name */
102
- className?: string;
103
-
104
- /** Callbacks */
105
- onPlay?: () => void;
106
- onPause?: () => void;
107
- onEnded?: () => void;
108
- onError?: (error: Error) => void;
109
- }
110
-
111
- // =============================================================================
112
- // CONSTANTS
113
- // =============================================================================
114
-
115
- const COVER_SIZES = {
116
- sm: 'w-24 h-24',
117
- md: 'w-32 h-32',
118
- lg: 'w-48 h-48',
119
- };
120
-
121
- // In horizontal layout cover stays fixed-size; in vertical it fills the full width
122
- const COVER_SIZE_FOR_LAYOUT = (coverSize: 'sm' | 'md' | 'lg', isHorizontal: boolean) =>
123
- isHorizontal ? coverSize : 'full';
124
-
125
- // =============================================================================
126
- // COMPONENT
127
- // =============================================================================
128
-
129
- export function HybridSimplePlayer(props: HybridSimplePlayerProps) {
130
- return (
131
- <VisualizationProvider>
132
- <HybridSimplePlayerContent {...props} />
133
- </VisualizationProvider>
134
- );
135
- }
136
-
137
- function HybridSimplePlayerContent({
138
- src,
139
- title,
140
- artist,
141
- coverArt,
142
- coverSize = 'md',
143
- showWaveform = true,
144
- waveformMode = 'frequency',
145
- waveformHeight = 64,
146
- showTimer = true,
147
- showVolume = true,
148
- showLoop = true,
149
- reactiveCover = true,
150
- variant,
151
- intensity,
152
- colorScheme,
153
- autoPlay = false,
154
- loop = false,
155
- initialVolume = 1,
156
- layout = 'vertical',
157
- className,
158
- onPlay,
159
- onPause,
160
- onEnded,
161
- onError,
162
- }: HybridSimplePlayerProps) {
163
- const { settings: vizSettings, nextVariant } = useVisualization();
164
-
165
- // Determine effective variant (from props or localStorage settings)
166
- const effectiveVariant =
167
- variant ?? (vizSettings.variant !== 'none' ? vizSettings.variant : 'spotlight');
168
- const effectiveIntensity = intensity ?? vizSettings.intensity;
169
- const effectiveColorScheme = colorScheme ?? vizSettings.colorScheme;
170
-
171
- // Show reactive cover if enabled and variant is not 'none'
172
- const showReactiveCover = reactiveCover && effectiveVariant !== 'none';
173
-
174
- // Render cover art content
175
- const renderCoverContent = () => {
176
- if (typeof coverArt === 'string') {
177
- return (
178
- <img src={coverArt} alt={title || 'Album cover'} className="w-full h-full object-cover" />
179
- );
180
- }
181
-
182
- if (coverArt) {
183
- return coverArt;
184
- }
185
-
186
- // Default placeholder
187
- return (
188
- <div className="w-full h-full bg-muted/30 flex items-center justify-center">
189
- <Music className="w-1/3 h-1/3 text-muted-foreground/50" />
190
- </div>
191
- );
192
- };
193
-
194
- const isHorizontal = layout === 'horizontal';
195
-
196
- return (
197
- <HybridAudioProvider
198
- src={src}
199
- autoPlay={autoPlay}
200
- loop={loop}
201
- initialVolume={initialVolume}
202
- onPlay={onPlay}
203
- onPause={onPause}
204
- onEnded={onEnded}
205
- onError={onError}
206
- >
207
- <div
208
- className={cn(
209
- 'flex gap-4',
210
- isHorizontal ? 'flex-row items-center' : 'flex-col items-center',
211
- className
212
- )}
213
- >
214
- {/* Cover Art */}
215
- {(coverArt || reactiveCover) && (
216
- <div className={cn('flex flex-col items-center gap-2', isHorizontal ? 'shrink-0' : 'w-full')}>
217
- {showReactiveCover ? (
218
- <AudioReactiveCover
219
- size={COVER_SIZE_FOR_LAYOUT(coverSize, isHorizontal)}
220
- variant={effectiveVariant as 'glow' | 'orbs' | 'spotlight' | 'mesh'}
221
- intensity={effectiveIntensity}
222
- colorScheme={effectiveColorScheme}
223
- onClick={nextVariant}
224
- >
225
- <div className={cn('rounded-lg overflow-hidden', isHorizontal ? COVER_SIZES[coverSize] : 'w-full h-full')}>
226
- {renderCoverContent()}
227
- </div>
228
- </AudioReactiveCover>
229
- ) : (
230
- <div
231
- className={cn(
232
- 'rounded-lg overflow-hidden shadow-lg cursor-pointer',
233
- isHorizontal ? COVER_SIZES[coverSize] : 'w-full sm:max-w-xs aspect-square mx-auto'
234
- )}
235
- onClick={nextVariant}
236
- role="button"
237
- tabIndex={0}
238
- onKeyDown={(e) => e.key === 'Enter' && nextVariant()}
239
- >
240
- {renderCoverContent()}
241
- </div>
242
- )}
243
-
244
- {/* Effect indicator */}
245
- {reactiveCover && (
246
- <span className="text-[10px] uppercase tracking-wider text-muted-foreground/50 select-none">
247
- {vizSettings.variant === 'none' ? 'off' : vizSettings.variant}
248
- </span>
249
- )}
250
- </div>
251
- )}
252
-
253
- {/* Track Info + Player */}
254
- <div
255
- className={cn('flex flex-col gap-3', isHorizontal ? 'flex-1 min-w-0' : 'w-full')}
256
- >
257
- {/* Track Info */}
258
- {(title || artist) && (
259
- <div className={cn('text-center', isHorizontal && 'text-left')}>
260
- {title && (
261
- <h3 className="text-base font-medium text-foreground truncate">{title}</h3>
262
- )}
263
- {artist && <p className="text-sm text-muted-foreground truncate">{artist}</p>}
264
- </div>
265
- )}
266
-
267
- {/* Audio Player */}
268
- <HybridAudioPlayer
269
- showControls
270
- showWaveform={showWaveform}
271
- waveformMode={waveformMode}
272
- waveformHeight={waveformHeight}
273
- showTimer={showTimer}
274
- showVolume={showVolume}
275
- showLoop={showLoop}
276
- className="border-0 bg-transparent"
277
- />
278
- </div>
279
- </div>
280
- </HybridAudioProvider>
281
- );
282
- }
283
-
284
- export default HybridSimplePlayer;