@djangocfg/ui-tools 2.1.310 → 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
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+
3
+ // Player keyboard shortcuts via ui-core's useHotkey (react-hotkeys-hook).
4
+ //
5
+ // Idiomatic pattern: each useHotkey call returns a callback ref that scopes
6
+ // the binding to the element it's attached to. We compose all of them into
7
+ // one ref and hand it to the player container. Library handles focus gating;
8
+ // no manual `document.activeElement` polling, no `data-scope` juggling.
9
+
10
+ import { useCallback } from 'react';
11
+ import { useHotkey } from '@djangocfg/ui-core/hooks';
12
+ import { clamp } from '../utils/clamp';
13
+ import type { PlayerControls } from '../types';
14
+
15
+ export type HotkeyBinding = {
16
+ /** Human label for help dialogs and tooltips. */
17
+ label: string;
18
+ /** Pretty hint shown to users (e.g. "Space", "←", "M"). */
19
+ hint: string;
20
+ };
21
+
22
+ export type UseKeyboardShortcutsOptions = {
23
+ audio: HTMLAudioElement;
24
+ controls: PlayerControls;
25
+ enabled?: boolean;
26
+ };
27
+
28
+ export type UseKeyboardShortcutsReturn = {
29
+ /** Attach to the player container — react-hotkeys-hook only fires when
30
+ * focus is within this element. Composes with other refs via callback. */
31
+ ref: (instance: HTMLElement | null) => void;
32
+ /** All registered bindings — useful for rendering a help dialog. */
33
+ bindings: ReadonlyArray<HotkeyBinding>;
34
+ };
35
+
36
+ const OPTS = { preventDefault: true } as const;
37
+
38
+ const BINDINGS: ReadonlyArray<HotkeyBinding> = [
39
+ { label: 'Play / pause', hint: 'Space' },
40
+ { label: 'Seek +5s', hint: '→' },
41
+ { label: 'Seek −5s', hint: '←' },
42
+ { label: 'Volume up', hint: '↑' },
43
+ { label: 'Volume down', hint: '↓' },
44
+ { label: 'Toggle mute', hint: 'M' },
45
+ { label: 'Toggle loop', hint: 'L' },
46
+ ];
47
+
48
+ export function useKeyboardShortcuts({
49
+ audio,
50
+ controls,
51
+ enabled = true,
52
+ }: UseKeyboardShortcutsOptions): UseKeyboardShortcutsReturn {
53
+ // Each useHotkey returns a callback ref that scopes the binding to its
54
+ // element. We must call them at the top level (rules of hooks).
55
+ const refToggle = useHotkey(['space', 'k'], () => void controls.toggle(), {
56
+ ...OPTS, enabled, description: 'Play / pause',
57
+ });
58
+ const refForward = useHotkey('right', () => controls.seek(audio.currentTime + 5), {
59
+ ...OPTS, enabled, description: 'Seek +5s',
60
+ });
61
+ const refBackward = useHotkey('left', () => controls.seek(audio.currentTime - 5), {
62
+ ...OPTS, enabled, description: 'Seek −5s',
63
+ });
64
+ const refVolUp = useHotkey('up', () => controls.setVolume(clamp(audio.volume + 0.05, 0, 1)), {
65
+ ...OPTS, enabled, description: 'Volume up',
66
+ });
67
+ const refVolDown = useHotkey('down', () => controls.setVolume(clamp(audio.volume - 0.05, 0, 1)), {
68
+ ...OPTS, enabled, description: 'Volume down',
69
+ });
70
+ const refMute = useHotkey('m', () => controls.toggleMute(), {
71
+ ...OPTS, enabled, description: 'Toggle mute',
72
+ });
73
+ const refLoop = useHotkey('l', () => controls.toggleLoop(), {
74
+ ...OPTS, enabled, description: 'Toggle loop',
75
+ });
76
+
77
+ const ref = useCallback(
78
+ (instance: HTMLElement | null) => {
79
+ refToggle(instance);
80
+ refForward(instance);
81
+ refBackward(instance);
82
+ refVolUp(instance);
83
+ refVolDown(instance);
84
+ refMute(instance);
85
+ refLoop(instance);
86
+ },
87
+ [refToggle, refForward, refBackward, refVolUp, refVolDown, refMute, refLoop],
88
+ );
89
+
90
+ return { ref, bindings: BINDINGS };
91
+ }
@@ -0,0 +1,74 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import type { PlayerControls } from '../types';
5
+ import type { PlayerMeta } from '../context/MetaContext';
6
+
7
+ const ACTIONS: MediaSessionAction[] = [
8
+ 'play',
9
+ 'pause',
10
+ 'previoustrack',
11
+ 'nexttrack',
12
+ 'seekbackward',
13
+ 'seekforward',
14
+ ];
15
+
16
+ export function useMediaSession(
17
+ audio: HTMLAudioElement,
18
+ meta: PlayerMeta,
19
+ controls: PlayerControls,
20
+ onPrev?: () => void,
21
+ onNext?: () => void,
22
+ ): void {
23
+ useEffect(() => {
24
+ if (typeof navigator === 'undefined' || !('mediaSession' in navigator)) return;
25
+ const ms = navigator.mediaSession;
26
+ try {
27
+ ms.metadata = new MediaMetadata({
28
+ title: meta.title ?? '',
29
+ artist: meta.artist ?? '',
30
+ album: meta.album ?? '',
31
+ artwork: meta.cover
32
+ ? [{ src: meta.cover, sizes: '512x512', type: 'image/jpeg' }]
33
+ : [],
34
+ });
35
+ } catch {
36
+ // ignore
37
+ }
38
+
39
+ const handlers: Partial<Record<MediaSessionAction, MediaSessionActionHandler | null>> = {
40
+ play: () => {
41
+ void controls.play();
42
+ },
43
+ pause: () => controls.pause(),
44
+ previoustrack: onPrev ?? null,
45
+ nexttrack: onNext ?? null,
46
+ seekbackward: (details) => {
47
+ const offset = details.seekOffset ?? 10;
48
+ controls.seek(audio.currentTime - offset);
49
+ },
50
+ seekforward: (details) => {
51
+ const offset = details.seekOffset ?? 10;
52
+ controls.seek(audio.currentTime + offset);
53
+ },
54
+ };
55
+
56
+ for (const a of ACTIONS) {
57
+ try {
58
+ ms.setActionHandler(a, handlers[a] ?? null);
59
+ } catch {
60
+ // ignore unsupported actions
61
+ }
62
+ }
63
+
64
+ return () => {
65
+ for (const a of ACTIONS) {
66
+ try {
67
+ ms.setActionHandler(a, null);
68
+ } catch {
69
+ // ignore
70
+ }
71
+ }
72
+ };
73
+ }, [audio, controls, meta.title, meta.artist, meta.album, meta.cover, onPrev, onNext]);
74
+ }
@@ -0,0 +1,83 @@
1
+ 'use client';
2
+
3
+ // Lazy peaks fetch with module-cache + in-flight dedupe. Triggers on first
4
+ // `play()`, IntersectionObserver visibility, or explicit `decodeOnMount` flag.
5
+
6
+ import { useEffect, useState } from 'react';
7
+ import { getPeaks, getPeaksFromCache } from '../audio/peaksCache';
8
+
9
+ export type UsePeaksOptions = {
10
+ src: string;
11
+ enabled?: boolean;
12
+ triggerRef?: React.RefObject<Element | null>;
13
+ decodeOnMount?: boolean;
14
+ };
15
+
16
+ export function usePeaks(opts: UsePeaksOptions): {
17
+ peaks: Float32Array | null;
18
+ loading: boolean;
19
+ error: unknown;
20
+ } {
21
+ const { src, enabled = true, triggerRef, decodeOnMount = false } = opts;
22
+ const cached = getPeaksFromCache(src) ?? null;
23
+ const [peaks, setLocal] = useState<Float32Array | null>(cached);
24
+ const [loading, setLoading] = useState<boolean>(!cached && enabled);
25
+ const [error, setError] = useState<unknown>(null);
26
+
27
+ useEffect(() => {
28
+ if (!enabled) return;
29
+ const hit = getPeaksFromCache(src);
30
+ if (hit) {
31
+ setLocal(hit);
32
+ setLoading(false);
33
+ return;
34
+ }
35
+ setLocal(null);
36
+ setLoading(true);
37
+ let cancelled = false;
38
+ let started = false;
39
+
40
+ const startDecode = () => {
41
+ if (started) return;
42
+ started = true;
43
+ getPeaks(src)
44
+ .then((p) => {
45
+ if (cancelled) return;
46
+ setLocal(p);
47
+ setLoading(false);
48
+ })
49
+ .catch((e) => {
50
+ if (cancelled) return;
51
+ setError(e);
52
+ setLoading(false);
53
+ });
54
+ };
55
+
56
+ if (decodeOnMount || !triggerRef?.current || typeof IntersectionObserver === 'undefined') {
57
+ startDecode();
58
+ return () => {
59
+ cancelled = true;
60
+ };
61
+ }
62
+
63
+ const obs = new IntersectionObserver(
64
+ (entries) => {
65
+ for (const entry of entries) {
66
+ if (entry.isIntersecting) {
67
+ startDecode();
68
+ obs.disconnect();
69
+ break;
70
+ }
71
+ }
72
+ },
73
+ { rootMargin: '200px' },
74
+ );
75
+ obs.observe(triggerRef.current);
76
+ return () => {
77
+ cancelled = true;
78
+ obs.disconnect();
79
+ };
80
+ }, [src, enabled, decodeOnMount, triggerRef]);
81
+
82
+ return { peaks, loading, error };
83
+ }
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+
3
+ // Read the persistent player preferences (volume, muted). useSyncExternalStore
4
+ // over preferencesStore — re-renders when localStorage changes (same tab or
5
+ // other tab via the native `storage` event).
6
+
7
+ import { useSyncExternalStore } from 'react';
8
+ import {
9
+ getPreferences,
10
+ subscribePreferences,
11
+ type PlayerPreferences,
12
+ } from '../store/preferencesStore';
13
+
14
+ const SSR: PlayerPreferences = { volume: 1, muted: false };
15
+ function getServerSnapshot(): PlayerPreferences {
16
+ return SSR;
17
+ }
18
+
19
+ export function usePlayerPreferences(): PlayerPreferences {
20
+ return useSyncExternalStore(subscribePreferences, getPreferences, getServerSnapshot);
21
+ }
@@ -0,0 +1,77 @@
1
+ 'use client';
2
+
3
+ // One rAF loop reads audio.currentTime and writes a single CSS variable.
4
+ // Pauses on document.hidden and when the audio is paused (with a final
5
+ // flush to keep the bar in sync after the seek that triggered the pause).
6
+
7
+ import { useEffect } from 'react';
8
+
9
+ const VAR = '--p';
10
+
11
+ export function usePlayheadLoop(
12
+ audio: HTMLAudioElement,
13
+ el: HTMLElement | null,
14
+ enabled = true,
15
+ ): void {
16
+ useEffect(() => {
17
+ if (!enabled || !el) return;
18
+ let raf = 0;
19
+ let lastPct = -1;
20
+
21
+ const writePct = () => {
22
+ const dur = audio.duration;
23
+ if (!Number.isFinite(dur) || dur <= 0) return;
24
+ const pct = Math.max(0, Math.min(100, (audio.currentTime / dur) * 100));
25
+ if (Math.abs(pct - lastPct) < 0.01) return;
26
+ lastPct = pct;
27
+ el.style.setProperty(VAR, `${pct.toFixed(2)}%`);
28
+ };
29
+
30
+ const tick = () => {
31
+ if (typeof document !== 'undefined' && document.hidden) {
32
+ raf = 0;
33
+ return;
34
+ }
35
+ writePct();
36
+ raf = requestAnimationFrame(tick);
37
+ };
38
+
39
+ const start = () => {
40
+ if (raf) return;
41
+ raf = requestAnimationFrame(tick);
42
+ };
43
+ const stop = () => {
44
+ if (!raf) return;
45
+ cancelAnimationFrame(raf);
46
+ raf = 0;
47
+ writePct();
48
+ };
49
+
50
+ if (!audio.paused) start();
51
+
52
+ const onPlay = () => start();
53
+ const onPauseOrEnd = () => stop();
54
+ const onSeek = () => writePct();
55
+ const onVisibility = () => {
56
+ if (document.hidden) stop();
57
+ else if (!audio.paused) start();
58
+ };
59
+
60
+ audio.addEventListener('play', onPlay);
61
+ audio.addEventListener('pause', onPauseOrEnd);
62
+ audio.addEventListener('ended', onPauseOrEnd);
63
+ audio.addEventListener('seeked', onSeek);
64
+ audio.addEventListener('timeupdate', writePct);
65
+ document.addEventListener('visibilitychange', onVisibility);
66
+
67
+ return () => {
68
+ stop();
69
+ audio.removeEventListener('play', onPlay);
70
+ audio.removeEventListener('pause', onPauseOrEnd);
71
+ audio.removeEventListener('ended', onPauseOrEnd);
72
+ audio.removeEventListener('seeked', onSeek);
73
+ audio.removeEventListener('timeupdate', writePct);
74
+ document.removeEventListener('visibilitychange', onVisibility);
75
+ };
76
+ }, [audio, el, enabled]);
77
+ }
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ export function useElementWidth(el: HTMLElement | null): number {
6
+ const [w, setW] = useState(0);
7
+ useEffect(() => {
8
+ if (!el || typeof ResizeObserver === 'undefined') return;
9
+ setW(el.clientWidth);
10
+ const obs = new ResizeObserver((entries) => {
11
+ for (const entry of entries) {
12
+ const next = entry.contentRect.width;
13
+ setW((prev) => (Math.abs(prev - next) < 0.5 ? prev : next));
14
+ }
15
+ });
16
+ obs.observe(el);
17
+ return () => obs.disconnect();
18
+ }, [el]);
19
+ return w;
20
+ }
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+
3
+ // Fires `cb` whenever <html> class changes (typical light/dark toggle convention)
4
+ // or the user's color-scheme media preference flips. Used to repaint canvases
5
+ // after a theme switch — see ADR-003.
6
+
7
+ import { useEffect } from 'react';
8
+
9
+ export function useThemeWatcher(cb: () => void): void {
10
+ useEffect(() => {
11
+ const root = document.documentElement;
12
+ const obs = new MutationObserver(cb);
13
+ obs.observe(root, { attributes: true, attributeFilter: ['class', 'data-theme'] });
14
+ const mq = window.matchMedia?.('(prefers-color-scheme: dark)');
15
+ const onMq = () => cb();
16
+ mq?.addEventListener?.('change', onMq);
17
+ return () => {
18
+ obs.disconnect();
19
+ mq?.removeEventListener?.('change', onMq);
20
+ };
21
+ }, [cb]);
22
+ }
@@ -1,139 +1,68 @@
1
- /**
2
- * AudioPlayer - Complete audio playback solution with reactive visualizations
3
- *
4
- * Uses HTML5 audio for playback + Web Audio API for visualization.
5
- * No crackling, native streaming support.
6
- *
7
- * @example
8
- * // Simple usage
9
- * import { HybridSimplePlayer } from '../../_shared';
10
- * <HybridSimplePlayer src="/audio.mp3" title="Track Title" />
11
- *
12
- * @example
13
- * // Custom setup with context
14
- * import { HybridAudioProvider, HybridAudioPlayer } from '../../_shared';
15
- * <HybridAudioProvider src={audioUrl}>
16
- * <HybridAudioPlayer showWaveform showControls />
17
- * </HybridAudioProvider>
18
- */
19
-
20
- // =============================================================================
21
- // COMPONENTS
22
- // =============================================================================
23
-
24
- export {
25
- HybridAudioPlayer,
26
- HybridSimplePlayer,
27
- HybridCompactPlayer,
28
- HybridWaveform,
29
- AudioReactiveCover,
30
- // Effect components (for custom implementations)
31
- GlowEffect,
32
- OrbsEffect,
33
- SpotlightEffect,
34
- MeshEffect,
35
- } from './components';
36
-
1
+ export { Player } from './Player';
2
+ export { LazyPlayer } from './lazy';
37
3
  export type {
38
- HybridAudioPlayerProps,
39
- HybridSimplePlayerProps,
40
- HybridCompactPlayerProps,
41
- HybridWaveformProps,
42
- AudioReactiveCoverProps,
43
- GlowEffectData,
44
- } from './components';
45
-
46
- // =============================================================================
47
- // CONTEXT
48
- // =============================================================================
49
-
4
+ PlayerProps,
5
+ PlayerState,
6
+ PlayerStateKind,
7
+ PlayerControls,
8
+ PlayerHandle,
9
+ WaveformConfig,
10
+ WaveformMode,
11
+ ReactiveCoverMode,
12
+ PlayerVariant,
13
+ PlayerErrorReason,
14
+ } from './types';
15
+
16
+ // Selector hooks for advanced consumers (rare).
50
17
  export {
51
- HybridAudioProvider,
52
- useHybridAudioContext,
53
- useHybridAudioState,
54
- useHybridAudioControls,
55
- useHybridAudioLevels,
56
- useHybridWebAudio,
18
+ PlayerProvider,
19
+ usePlayerAudio,
20
+ usePlayerControls,
21
+ usePlayerDuration,
22
+ usePlayerLevels,
23
+ usePlayerMeta,
24
+ usePlayerPaused,
25
+ usePlayerState,
57
26
  } from './context';
58
-
59
- export type {
60
- HybridAudioContextValue,
61
- HybridAudioProviderProps,
62
- } from './context';
63
-
64
- // =============================================================================
65
- // HOOKS
66
- // =============================================================================
67
-
68
27
  export {
69
- // Audio bus
70
- useAudioBus,
71
- useAudioBusStore,
72
- // Core hooks
73
- useHybridAudio,
74
- useHybridAudioAnalysis,
75
- // Visualization settings
76
- useVisualization,
77
- useAudioVisualization,
78
- VisualizationProvider,
79
- VARIANT_INFO,
80
- INTENSITY_INFO,
81
- COLOR_SCHEME_INFO,
82
- } from './hooks';
83
-
84
- export type {
85
- UseAudioBusReturn,
86
- UseHybridAudioOptions,
87
- HybridAudioState,
88
- HybridAudioControls,
89
- HybridWebAudioAPI,
90
- UseHybridAudioReturn,
91
- VisualizationSettings,
92
- VisualizationVariant,
93
- VisualizationIntensity,
94
- VisualizationColorScheme,
95
- UseVisualizationReturn,
96
- UseAudioVisualizationReturn,
97
- VisualizationProviderProps,
98
- } from './hooks';
99
-
100
- // =============================================================================
101
- // TYPES
102
- // =============================================================================
103
-
104
- export type { EqualizerOptions } from './types';
105
-
106
- // =============================================================================
107
- // EFFECTS
108
- // =============================================================================
109
-
28
+ setActivePlayer,
29
+ getActivePlayer,
30
+ getLastActivePlayer,
31
+ subscribeActivePlayer,
32
+ getPreferences,
33
+ setStoredVolume,
34
+ setStoredMuted,
35
+ subscribePreferences,
36
+ } from './store';
37
+ export type { PlayerPreferences } from './store';
110
38
  export {
111
- // Utilities
112
- getEffectConfig,
113
- getColors,
114
- prepareEffectColors,
115
- calculateGlowLayers,
116
- calculateOrbs,
117
- calculateMeshGradients,
118
- calculateSpotlight,
119
- // Constants
120
- INTENSITY_CONFIG,
121
- COLOR_SCHEMES,
122
- EFFECT_ANIMATIONS,
123
- } from './effects';
124
-
125
- export type {
126
- EffectVariant,
127
- EffectIntensity,
128
- EffectColorScheme,
129
- AudioLevels,
130
- EffectConfig,
131
- EffectColors,
132
- EffectLayer,
133
- } from './effects';
134
-
135
- // =============================================================================
136
- // UTILITIES
137
- // =============================================================================
138
-
139
- export { formatTime } from './utils';
39
+ useActivePlayer,
40
+ useLastActivePlayer,
41
+ useIsActivePlayer,
42
+ } from './hooks/useActivePlayer';
43
+ export { usePlayerPreferences } from './hooks/usePlayerPreferences';
44
+ export { clearPeaksCache, setPeaks } from './audio';
45
+
46
+ // Slot components — for custom layouts inside <PlayerProvider>.
47
+ // Each one is presentational: reads from the player context, renders one
48
+ // piece of UI. Compose them in any grid you want.
49
+ export { Cover, CoverPlaceholder, ReactivePulse } from './parts/Cover';
50
+ export { Title, Artist, TimeDisplay } from './parts/Meta';
51
+ export {
52
+ PlayButton,
53
+ SkipButton,
54
+ VolumeControl,
55
+ LoopButton,
56
+ ControlsRow,
57
+ IconButton,
58
+ } from './parts/Controls';
59
+ export {
60
+ Waveform,
61
+ PeaksWaveform,
62
+ LiveWaveform,
63
+ BarsWaveform,
64
+ ProgressBar,
65
+ WaveformSkeleton,
66
+ } from './parts/Waveform';
67
+ export { ErrorState } from './parts/ErrorState';
68
+ export { DefaultLayout, CompactLayout } from './parts/Layout';