@djangocfg/ui-tools 2.1.381 → 2.1.383

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 (178) hide show
  1. package/README.md +132 -899
  2. package/dist/ChatRoot-6IZFM5HM.mjs +5 -0
  3. package/dist/{ChatRoot-EJC5Y2YM.cjs.map → ChatRoot-6IZFM5HM.mjs.map} +1 -1
  4. package/dist/ChatRoot-LW4XNIKP.cjs +14 -0
  5. package/dist/{ChatRoot-QOSKJPM6.mjs.map → ChatRoot-LW4XNIKP.cjs.map} +1 -1
  6. package/dist/DictationField-U25MEYAL.mjs +4 -0
  7. package/dist/DictationField-U25MEYAL.mjs.map +1 -0
  8. package/dist/DictationField-XWR5VOID.cjs +13 -0
  9. package/dist/DictationField-XWR5VOID.cjs.map +1 -0
  10. package/dist/{DocsLayout-2YKPXZYO.mjs → DocsLayout-2P3ONDWJ.mjs} +3 -3
  11. package/dist/{DocsLayout-2YKPXZYO.mjs.map → DocsLayout-2P3ONDWJ.mjs.map} +1 -1
  12. package/dist/{DocsLayout-Q4KS3QWW.cjs → DocsLayout-2YZNS5VK.cjs} +8 -8
  13. package/dist/{DocsLayout-Q4KS3QWW.cjs.map → DocsLayout-2YZNS5VK.cjs.map} +1 -1
  14. package/dist/chunk-4PFW7MIJ.cjs +837 -0
  15. package/dist/chunk-4PFW7MIJ.cjs.map +1 -0
  16. package/dist/chunk-C2YN6WEO.mjs +833 -0
  17. package/dist/chunk-C2YN6WEO.mjs.map +1 -0
  18. package/dist/{chunk-XACCHZH2.cjs → chunk-FIRK5CEH.cjs} +42 -4
  19. package/dist/chunk-FIRK5CEH.cjs.map +1 -0
  20. package/dist/{chunk-NWUT327A.mjs → chunk-HIK6BPL7.mjs} +38 -5
  21. package/dist/chunk-HIK6BPL7.mjs.map +1 -0
  22. package/dist/chunk-OZAU3QWD.cjs +2493 -0
  23. package/dist/chunk-OZAU3QWD.cjs.map +1 -0
  24. package/dist/chunk-UWVP6LCW.mjs +2447 -0
  25. package/dist/chunk-UWVP6LCW.mjs.map +1 -0
  26. package/dist/index.cjs +1668 -99
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +1215 -107
  29. package/dist/index.d.ts +1215 -107
  30. package/dist/index.mjs +1555 -50
  31. package/dist/index.mjs.map +1 -1
  32. package/package.json +16 -15
  33. package/src/audio-assets.d.ts +8 -0
  34. package/src/components/markdown/MarkdownMessage/CollapseToggle.tsx +3 -1
  35. package/src/components/markdown/MarkdownMessage/components.tsx +2 -5
  36. package/src/tools/Chat/README.md +347 -530
  37. package/src/tools/Chat/components/Attachments.tsx +6 -1
  38. package/src/tools/Chat/components/ChatRoot.tsx +30 -2
  39. package/src/tools/Chat/components/Composer.tsx +20 -3
  40. package/src/tools/Chat/components/ErrorBanner.tsx +7 -3
  41. package/src/tools/Chat/components/MessageActions.tsx +3 -1
  42. package/src/tools/Chat/components/MessageBubble.tsx +6 -5
  43. package/src/tools/Chat/components/MessageList.tsx +87 -1
  44. package/src/tools/Chat/components/ToolCalls.tsx +21 -3
  45. package/src/tools/Chat/context/ChatProvider.tsx +21 -3
  46. package/src/tools/Chat/core/audio/audioBus.ts +10 -163
  47. package/src/tools/Chat/core/audio/defaults.ts +43 -0
  48. package/src/tools/Chat/core/audio/index.ts +1 -0
  49. package/src/tools/Chat/core/audio/preferences.ts +5 -59
  50. package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
  51. package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
  52. package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
  53. package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
  54. package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
  55. package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
  56. package/src/tools/Chat/core/audio/types.ts +28 -0
  57. package/src/tools/Chat/core/reducer.ts +33 -0
  58. package/src/tools/Chat/core/transport/index.ts +13 -0
  59. package/src/tools/Chat/core/transport/mappers/index.ts +6 -0
  60. package/src/tools/Chat/core/transport/mappers/pydantic-ai.ts +142 -0
  61. package/src/tools/Chat/core/transport/pydantic-ai-transport.ts +208 -0
  62. package/src/tools/Chat/core/transport/sse.ts +18 -5
  63. package/src/tools/Chat/hooks/index.ts +25 -0
  64. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +5 -3
  65. package/src/tools/Chat/hooks/useChat.ts +28 -0
  66. package/src/tools/Chat/hooks/useChatAudio.ts +59 -180
  67. package/src/tools/Chat/hooks/useChatDockPrefs.ts +74 -0
  68. package/src/tools/Chat/hooks/useChatReset.ts +70 -0
  69. package/src/tools/Chat/hooks/useChatUnread.ts +87 -0
  70. package/src/tools/Chat/hooks/useFocusOnEmptyClick.ts +111 -0
  71. package/src/tools/Chat/hooks/useVisitorFingerprint.ts +48 -0
  72. package/src/tools/Chat/index.ts +84 -1
  73. package/src/tools/Chat/launcher/ChatDock.tsx +263 -0
  74. package/src/tools/Chat/launcher/ChatFAB.tsx +349 -0
  75. package/src/tools/Chat/launcher/ChatGreeting.tsx +200 -0
  76. package/src/tools/Chat/launcher/ChatHeader.tsx +76 -0
  77. package/src/tools/Chat/launcher/ChatHeaderActionButton.tsx +87 -0
  78. package/src/tools/Chat/launcher/ChatHeaderAudioToggle.tsx +47 -0
  79. package/src/tools/Chat/launcher/ChatHeaderLanguageButton.tsx +179 -0
  80. package/src/tools/Chat/launcher/ChatHeaderModeToggle.tsx +57 -0
  81. package/src/tools/Chat/launcher/ChatHeaderResetButton.tsx +93 -0
  82. package/src/tools/Chat/launcher/ChatLauncher.tsx +321 -0
  83. package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +197 -0
  84. package/src/tools/Chat/launcher/index.ts +46 -0
  85. package/src/tools/Chat/launcher/useChatPresence.ts +44 -0
  86. package/src/tools/Chat/styles/bubbleTokens.ts +71 -0
  87. package/src/tools/Chat/styles/index.ts +16 -0
  88. package/src/tools/Chat/styles/useChatStyles.ts +101 -0
  89. package/src/tools/Chat/types/attachment.ts +25 -0
  90. package/src/tools/Chat/types/config.ts +48 -0
  91. package/src/tools/Chat/types/events.ts +35 -0
  92. package/src/tools/Chat/types/index.ts +34 -0
  93. package/src/tools/Chat/types/labels.ts +38 -0
  94. package/src/tools/Chat/types/message.ts +32 -0
  95. package/src/tools/Chat/types/persona.ts +31 -0
  96. package/src/tools/Chat/types/session.ts +43 -0
  97. package/src/tools/Chat/types/tool-call.ts +17 -0
  98. package/src/tools/Chat/types/transport.ts +28 -0
  99. package/src/tools/Chat/types.ts +5 -240
  100. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +50 -14
  101. package/src/tools/MarkdownEditor/index.ts +1 -1
  102. package/src/tools/SpeechRecognition/README.md +336 -0
  103. package/src/tools/SpeechRecognition/__tests__/ids.test.ts +15 -0
  104. package/src/tools/SpeechRecognition/__tests__/language.test.ts +59 -0
  105. package/src/tools/SpeechRecognition/__tests__/reducer.test.ts +71 -0
  106. package/src/tools/SpeechRecognition/__tests__/transcript.test.ts +52 -0
  107. package/src/tools/SpeechRecognition/components/DevicePicker.tsx +49 -0
  108. package/src/tools/SpeechRecognition/components/DictationButton.tsx +93 -0
  109. package/src/tools/SpeechRecognition/components/EngineBadge.tsx +30 -0
  110. package/src/tools/SpeechRecognition/components/ErrorBanner.tsx +52 -0
  111. package/src/tools/SpeechRecognition/components/LanguagePicker.tsx +63 -0
  112. package/src/tools/SpeechRecognition/components/MicMeter.tsx +63 -0
  113. package/src/tools/SpeechRecognition/components/PushToTalkHint.tsx +51 -0
  114. package/src/tools/SpeechRecognition/components/TranscriptView.tsx +55 -0
  115. package/src/tools/SpeechRecognition/components/index.ts +16 -0
  116. package/src/tools/SpeechRecognition/context/SpeechRecognitionProvider.tsx +47 -0
  117. package/src/tools/SpeechRecognition/context/index.ts +6 -0
  118. package/src/tools/SpeechRecognition/core/audio/defaults.ts +24 -0
  119. package/src/tools/SpeechRecognition/core/engine/external.ts +222 -0
  120. package/src/tools/SpeechRecognition/core/engine/http.ts +147 -0
  121. package/src/tools/SpeechRecognition/core/engine/index.ts +52 -0
  122. package/src/tools/SpeechRecognition/core/engine/mediarecorder.ts +105 -0
  123. package/src/tools/SpeechRecognition/core/engine/websocket.ts +211 -0
  124. package/src/tools/SpeechRecognition/core/engine/webspeech.ts +188 -0
  125. package/src/tools/SpeechRecognition/core/ids.ts +11 -0
  126. package/src/tools/SpeechRecognition/core/index.ts +14 -0
  127. package/src/tools/SpeechRecognition/core/language.ts +78 -0
  128. package/src/tools/SpeechRecognition/core/languages-catalog.ts +229 -0
  129. package/src/tools/SpeechRecognition/core/logger.ts +3 -0
  130. package/src/tools/SpeechRecognition/core/reducer.ts +105 -0
  131. package/src/tools/SpeechRecognition/core/transcript.ts +36 -0
  132. package/src/tools/SpeechRecognition/hooks/index.ts +14 -0
  133. package/src/tools/SpeechRecognition/hooks/useDictation.ts +59 -0
  134. package/src/tools/SpeechRecognition/hooks/useEnginePrefs.ts +15 -0
  135. package/src/tools/SpeechRecognition/hooks/useMicDevices.ts +57 -0
  136. package/src/tools/SpeechRecognition/hooks/useMicLevel.ts +52 -0
  137. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +85 -0
  138. package/src/tools/SpeechRecognition/hooks/useResolvedLanguage.ts +28 -0
  139. package/src/tools/SpeechRecognition/hooks/useSpeechLanguageInfo.ts +108 -0
  140. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +188 -0
  141. package/src/tools/SpeechRecognition/hooks/useVoiceSupport.ts +78 -0
  142. package/src/tools/SpeechRecognition/index.ts +82 -0
  143. package/src/tools/SpeechRecognition/lazy.tsx +19 -0
  144. package/src/tools/SpeechRecognition/store/index.ts +2 -0
  145. package/src/tools/SpeechRecognition/store/prefsStore.ts +54 -0
  146. package/src/tools/SpeechRecognition/types.ts +133 -0
  147. package/src/tools/SpeechRecognition/widgets/DictationField.tsx +105 -0
  148. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +305 -0
  149. package/src/tools/SpeechRecognition/widgets/VoiceMessageRecorder.tsx +88 -0
  150. package/src/tools/SpeechRecognition/widgets/index.ts +6 -0
  151. package/dist/ChatRoot-EJC5Y2YM.cjs +0 -14
  152. package/dist/ChatRoot-QOSKJPM6.mjs +0 -5
  153. package/dist/chunk-NWUT327A.mjs.map +0 -1
  154. package/dist/chunk-QLMKCSR6.mjs +0 -2420
  155. package/dist/chunk-QLMKCSR6.mjs.map +0 -1
  156. package/dist/chunk-SI5RD2GD.cjs +0 -2460
  157. package/dist/chunk-SI5RD2GD.cjs.map +0 -1
  158. package/dist/chunk-XACCHZH2.cjs.map +0 -1
  159. package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +0 -771
  160. package/src/stories/index.ts +0 -33
  161. package/src/tools/AudioPlayer/AudioPlayer.story.tsx +0 -481
  162. package/src/tools/Chat/Chat.story.tsx +0 -1457
  163. package/src/tools/CodeEditor/CodeEditor.story.tsx +0 -202
  164. package/src/tools/CronScheduler/CronScheduler.story.tsx +0 -300
  165. package/src/tools/Gallery/Gallery.story.tsx +0 -237
  166. package/src/tools/ImageViewer/ImageViewer.story.tsx +0 -85
  167. package/src/tools/JsonForm/JsonForm.story.tsx +0 -350
  168. package/src/tools/JsonTree/JsonTree.story.tsx +0 -141
  169. package/src/tools/LottiePlayer/LottiePlayer.story.tsx +0 -95
  170. package/src/tools/Map/Map.story.tsx +0 -458
  171. package/src/tools/MarkdownEditor/MarkdownEditor.story.tsx +0 -225
  172. package/src/tools/Mermaid/Mermaid.story.tsx +0 -251
  173. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +0 -230
  174. package/src/tools/PrettyCode/PrettyCode.story.tsx +0 -304
  175. package/src/tools/Tour/Tour.story.tsx +0 -279
  176. package/src/tools/Tree/Tree.story.tsx +0 -620
  177. package/src/tools/Uploader/Uploader.story.tsx +0 -415
  178. package/src/tools/VideoPlayer/VideoPlayer.story.tsx +0 -87
@@ -1,33 +0,0 @@
1
- /**
2
- * UI-Tools story modules registry.
3
- *
4
- * Exporting stories via an explicit package subpath keeps Next.js consumers stable:
5
- * no file-system scanning, no deep-imports that violate package `exports`.
6
- */
7
-
8
- import type { StoryModule } from '@djangocfg/playground';
9
-
10
- import * as JsonForm from '../tools/JsonForm/JsonForm.story';
11
- import * as JsonTree from '../tools/JsonTree/JsonTree.story';
12
- import * as OpenapiViewer from '../tools/OpenapiViewer/OpenapiViewer.story';
13
- import * as Mermaid from '../tools/Mermaid/Mermaid.story';
14
- import * as Map from '../tools/Map/Map.story';
15
- import * as Tour from '../tools/Tour/Tour.story';
16
- import * as CodeEditor from '../tools/CodeEditor/CodeEditor.story';
17
- import * as PrettyCode from '../tools/PrettyCode/PrettyCode.story';
18
- import * as MarkdownEditor from '../tools/MarkdownEditor/MarkdownEditor.story';
19
- import * as Chat from '../tools/Chat/Chat.story';
20
-
21
- export const uiToolsStoryModules: Record<string, StoryModule> = {
22
- 'JsonForm.story.tsx': JsonForm,
23
- 'JsonTree.story.tsx': JsonTree,
24
- 'OpenapiViewer.story.tsx': OpenapiViewer,
25
- 'Mermaid.story.tsx': Mermaid,
26
- 'Map.story.tsx': Map,
27
- 'Tour.story.tsx': Tour,
28
- 'CodeEditor.story.tsx': CodeEditor,
29
- 'PrettyCode.story.tsx': PrettyCode,
30
- 'MarkdownEditor.story.tsx': MarkdownEditor,
31
- 'Chat.story.tsx': Chat,
32
- };
33
-
@@ -1,481 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
- import { defineStory, useBoolean, useSelect } from '@djangocfg/playground';
3
- import { TooltipProvider } from '@djangocfg/ui-core/components';
4
- import { decodePeaks } from './audio/decodePeaks';
5
- import { PlayerProvider } from './context/PlayerProvider';
6
- import { Player } from './Player';
7
- import {
8
- useActivePlayer,
9
- useIsActivePlayer,
10
- useLastActivePlayer,
11
- } from './hooks/useActivePlayer';
12
- import { usePlayerPreferences } from './hooks/usePlayerPreferences';
13
- import { Cover } from './parts/Cover/Cover';
14
- import { LoopButton } from './parts/Controls/LoopButton';
15
- import { PlayButton } from './parts/Controls/PlayButton';
16
- import { VolumeControl } from './parts/Controls/VolumeControl';
17
- import { Artist } from './parts/Meta/Artist';
18
- import { TimeDisplay } from './parts/Meta/TimeDisplay';
19
- import { Title } from './parts/Meta/Title';
20
- import { Waveform } from './parts/Waveform/Waveform';
21
- import type { WaveformMode } from './types';
22
-
23
- export default defineStory({
24
- title: 'Tools/Audio Player',
25
- component: Player,
26
- description:
27
- 'WebView-safe audio player. Static peaks waveform by default; clip-path playhead; one accent.',
28
- });
29
-
30
- // Local samples copied from @sources/. Vite serves them same-origin so
31
- // crossOrigin="anonymous" + decodeAudioData work without CORS friction.
32
- const SAMPLES = {
33
- short: '/audio/short.mp3',
34
- voice: '/audio/voice.mp3',
35
- long: '/audio/long.mp3',
36
- } as const;
37
-
38
- const COVER = 'data:image/svg+xml;utf8,' + encodeURIComponent(`
39
- <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
40
- <rect width="200" height="200" fill="#0f172a"/>
41
- <circle cx="100" cy="100" r="56" fill="none" stroke="#94a3b8" stroke-width="2"/>
42
- <circle cx="100" cy="100" r="6" fill="#94a3b8"/>
43
- </svg>
44
- `);
45
-
46
- const Frame = ({ children, max = 'max-w-xl' }: { children: React.ReactNode; max?: string }) => (
47
- <div className={`mx-auto w-full ${max} p-6`}>{children}</div>
48
- );
49
-
50
- function Section({ title, hint, children }: { title: string; hint?: string; children: React.ReactNode }) {
51
- return (
52
- <section className="space-y-2">
53
- <header>
54
- <h3 className="text-sm font-medium text-foreground">{title}</h3>
55
- {hint && <p className="text-xs text-muted-foreground">{hint}</p>}
56
- </header>
57
- {children}
58
- </section>
59
- );
60
- }
61
-
62
- export const Default = () => (
63
- <Frame>
64
- <Player src={SAMPLES.short} title="Stereo demo" artist="wavesurfer samples" />
65
- </Frame>
66
- );
67
-
68
- export const WithCover = () => (
69
- <Frame>
70
- <Player src={SAMPLES.short} title="With cover" artist="wavesurfer samples" cover={COVER} />
71
- </Frame>
72
- );
73
-
74
- export const Compact = () => (
75
- <Frame max="max-w-md">
76
- <Player src={SAMPLES.short} title="Compact" variant="compact" />
77
- </Frame>
78
- );
79
-
80
- export const Bars = () => (
81
- <Frame>
82
- <Player
83
- src={SAMPLES.short}
84
- title="Bars decoration"
85
- artist="No audio coupling"
86
- waveform={{ mode: 'bars', height: 28 }}
87
- />
88
- </Frame>
89
- );
90
-
91
- export const Live = () => (
92
- <Frame>
93
- <Player
94
- src={SAMPLES.voice}
95
- title="Live analyser"
96
- artist="Reads from AnalyserNode at 30 Hz"
97
- waveform={{ mode: 'live', height: 48 }}
98
- />
99
- </Frame>
100
- );
101
-
102
- export const NoWaveform = () => (
103
- <Frame>
104
- <Player src={SAMPLES.short} title="Minimal" artist="No waveform" waveform={{ mode: 'none' }} />
105
- </Frame>
106
- );
107
-
108
- export const CustomLayout = () => (
109
- <Frame max="max-w-2xl">
110
- <p className="mb-4 text-xs text-muted-foreground">
111
- Slot composition: drop <code className="rounded bg-muted px-1">PlayerProvider</code> and
112
- arrange the parts you want yourself. Big cover left (spans two rows), meta + timer right,
113
- waveform + controls full-width below.
114
- </p>
115
- <CustomLayoutDemo />
116
- </Frame>
117
- );
118
-
119
- export const ReactiveCover = () => (
120
- <Frame>
121
- <Player
122
- src={SAMPLES.voice}
123
- title="Subtle reactive cover"
124
- artist="Tiny scale on the cover"
125
- cover={COVER}
126
- reactiveCover="subtle"
127
- waveform={{ mode: 'live' }}
128
- />
129
- </Frame>
130
- );
131
-
132
- export const ErrorState = () => (
133
- <Frame>
134
- <Player
135
- src="/audio/this-does-not-exist.mp3"
136
- title="Broken source"
137
- artist="Demonstrates error state"
138
- />
139
- </Frame>
140
- );
141
-
142
- export const Exclusive = () => {
143
- const active = useActivePlayer();
144
- return (
145
- <Frame max="max-w-2xl">
146
- <p className="mb-4 text-xs text-muted-foreground">
147
- Three players, <code className="rounded bg-muted px-1">exclusive</code> on (default).
148
- Press play on one — the others pause automatically (same tab + cross-tab via{' '}
149
- <code className="rounded bg-muted px-1">BroadcastChannel</code>).
150
- <br />
151
- Active id: <code className="rounded bg-muted px-1">{active ?? '∅'}</code>
152
- </p>
153
- <div className="space-y-3">
154
- <Player src={SAMPLES.short} title="Track A · stereo" artist="wavesurfer" />
155
- <Player src={SAMPLES.voice} title="Track B · librivox" artist="voice" />
156
- <Player src={SAMPLES.long} title="Track C · phonograph" artist="deepnote" />
157
- </div>
158
- </Frame>
159
- );
160
- };
161
-
162
- // One-stop demo of every prop / mode / hook. Long but scrollable; each section
163
- // is independent so you can grab a single example as a reference.
164
- export const Showcase = () => {
165
- return (
166
- <div className="mx-auto w-full max-w-3xl space-y-8 p-6">
167
- <Section title="1 · Default" hint="Static peaks waveform, container query auto-switches to compact below 480 px / on phones.">
168
- <Player src={SAMPLES.short} title="Stereo demo" artist="wavesurfer" />
169
- </Section>
170
-
171
- <Section title="2 · With cover" hint="Cover uses <img loading='lazy' decoding='async'>; placeholder otherwise.">
172
- <Player src={SAMPLES.short} title="With cover" artist="wavesurfer" cover={COVER} />
173
- </Section>
174
-
175
- <Section title="3 · Compact (forced)" hint="Single-row layout. Always picked when variant='compact'.">
176
- <Player src={SAMPLES.short} title="Compact" variant="compact" />
177
- </Section>
178
-
179
- <Section title="4 · Skip controls" hint="onPrev / onNext render the SkipBack / SkipForward buttons (and wire MediaSession).">
180
- <Player
181
- src={SAMPLES.short}
182
- title="With prev / next"
183
- artist="onPrev / onNext"
184
- cover={COVER}
185
- onPrev={() => console.log('prev')}
186
- onNext={() => console.log('next')}
187
- />
188
- </Section>
189
-
190
- <Section title="5 · Live analyser" hint="mode='live' — AnalyserNode @ 30 Hz pushed into LevelsStore; canvas paints from rAF.">
191
- <Player
192
- src={SAMPLES.voice}
193
- title="Live waveform"
194
- artist="AnalyserNode at 30 Hz"
195
- waveform={{ mode: 'live', height: 48 }}
196
- />
197
- </Section>
198
-
199
- <Section title="6 · Bars (decoration)" hint="mode='bars' — CSS-only equalizer animation, no audio coupling.">
200
- <Player
201
- src={SAMPLES.short}
202
- title="Bars"
203
- artist="No audio coupling"
204
- waveform={{ mode: 'bars', height: 28 }}
205
- />
206
- </Section>
207
-
208
- <Section title="7a · Progress bar (no animation)" hint="mode='progress' — thin scrubber, no waveform/animation. Same click + drag + hover-tip + playhead pipeline.">
209
- <Player
210
- src={SAMPLES.short}
211
- title="Plain progress"
212
- artist="No waveform animation"
213
- waveform={{ mode: 'progress', height: 4 }}
214
- />
215
- </Section>
216
-
217
- <Section title="7b · No waveform" hint="mode='none' — bare meta + controls. Use when even the scrubber gets in the way.">
218
- <Player src={SAMPLES.short} title="Minimal" waveform={{ mode: 'none' }} />
219
- </Section>
220
-
221
- <Section title="8 · Subtle reactive cover" hint="reactiveCover='subtle' — compositor-only scale tied to a low-pass envelope.">
222
- <Player
223
- src={SAMPLES.voice}
224
- title="Reactive cover"
225
- artist="scale(1.00 — 1.03)"
226
- cover={COVER}
227
- reactiveCover="subtle"
228
- waveform={{ mode: 'live' }}
229
- />
230
- </Section>
231
-
232
- <Section title="9 · Pre-computed peaks" hint="peaks prop seeds the cache — skips the OfflineAudioContext decode entirely.">
233
- <PrecomputedPeaksDemo />
234
- </Section>
235
-
236
- <Section title="10 · seekStartsPlayback={false}" hint="Click on the waveform seeks but does not start playback. Useful in embeds.">
237
- <Player
238
- src={SAMPLES.short}
239
- title="Click only seeks"
240
- artist="No autoplay on click"
241
- seekStartsPlayback={false}
242
- />
243
- </Section>
244
-
245
- <Section title="11 · Persistent volume sync" hint="Two uncontrolled players read / write the same persisted prefs (also synced across tabs).">
246
- <PreferencesSyncDemo />
247
- </Section>
248
-
249
- <Section title="12 · Active-player coordination" hint="exclusive (default) pauses sibling players. Active id + 'last active' badges from useActivePlayer / useLastActivePlayer.">
250
- <ActivePlayerDemo />
251
- </Section>
252
-
253
- <Section title="13 · Custom toolbar via useIsActivePlayer" hint="Uses Player's id+`useIsActivePlayer` to outline the playing card.">
254
- <ActiveOutlineDemo />
255
- </Section>
256
-
257
- <Section title="14 · Custom layout (slot composition)" hint="Drop <PlayerProvider> + import the parts you want. Build any grid.">
258
- <CustomLayoutDemo />
259
- </Section>
260
-
261
- <Section title="15 · Error state" hint="Bad src triggers MediaError → ErrorState with retry.">
262
- <Player src="/audio/this-does-not-exist.mp3" title="Broken source" artist="See error pane" />
263
- </Section>
264
- </div>
265
- );
266
- };
267
-
268
- function PrecomputedPeaksDemo() {
269
- const [peaks, setPeaks] = useState<Float32Array | null>(null);
270
- useEffect(() => {
271
- let alive = true;
272
- decodePeaks(SAMPLES.short).then((p) => alive && setPeaks(p)).catch(() => undefined);
273
- return () => {
274
- alive = false;
275
- };
276
- }, []);
277
- if (!peaks) {
278
- return (
279
- <div className="rounded-md border border-border/60 bg-muted/30 px-3 py-4 text-xs text-muted-foreground">
280
- Decoding peaks once (so the player below mounts with them already in hand)…
281
- </div>
282
- );
283
- }
284
- return (
285
- <Player
286
- src={SAMPLES.short}
287
- title="Pre-computed peaks"
288
- artist={`Float32Array · ${peaks.length} buckets`}
289
- peaks={peaks}
290
- />
291
- );
292
- }
293
-
294
- function PreferencesSyncDemo() {
295
- const prefs = usePlayerPreferences();
296
- return (
297
- <div className="space-y-3">
298
- <div className="rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs">
299
- Stored prefs:{' '}
300
- <code className="text-foreground">
301
- volume {Math.round(prefs.volume * 100)}% · {prefs.muted ? 'muted' : 'unmuted'}
302
- </code>
303
- </div>
304
- <Player src={SAMPLES.short} title="Player A" artist="uncontrolled volume" />
305
- <Player src={SAMPLES.voice} title="Player B" artist="uncontrolled volume" />
306
- </div>
307
- );
308
- }
309
-
310
- function ActivePlayerDemo() {
311
- const active = useActivePlayer();
312
- const last = useLastActivePlayer();
313
- return (
314
- <div className="space-y-3">
315
- <div className="grid grid-cols-2 gap-2 text-xs">
316
- <div className="rounded border border-border/60 bg-card px-2 py-1.5">
317
- Active: <code className="text-foreground">{active ?? '∅'}</code>
318
- </div>
319
- <div className="rounded border border-border/60 bg-card px-2 py-1.5">
320
- Last: <code className="text-foreground">{last ?? '∅'}</code>
321
- </div>
322
- </div>
323
- <Player src={SAMPLES.short} title="Track A" artist="exclusive default" />
324
- <Player src={SAMPLES.voice} title="Track B" artist="exclusive default" />
325
- <Player src={SAMPLES.long} title="Track C" artist="exclusive default" />
326
- </div>
327
- );
328
- }
329
-
330
- function ActiveOutlineCard({
331
- id,
332
- src,
333
- title,
334
- }: {
335
- id: string;
336
- src: string;
337
- title: string;
338
- }) {
339
- const isActive = useIsActivePlayer(id);
340
- return (
341
- <div
342
- className={`rounded-lg p-px transition-colors ${
343
- isActive ? 'bg-primary/60' : 'bg-transparent'
344
- }`}
345
- >
346
- <Player src={src} title={title} artist={isActive ? 'now playing' : 'idle'} ariaLabel={id} />
347
- </div>
348
- );
349
- }
350
-
351
- function ActiveOutlineDemo() {
352
- // We pass a stable `ariaLabel` as the id we read back from the bus.
353
- // useActivePlayer returns React's useId-generated value, so a real consumer
354
- // would key off that instead — this is just to demonstrate the pattern.
355
- return (
356
- <div className="space-y-3">
357
- <p className="text-xs text-muted-foreground">
358
- The currently playing card gets a primary ring. Implementation reads
359
- <code className="rounded bg-muted px-1">useIsActivePlayer(id)</code>.
360
- </p>
361
- <ActiveOutlineCard id="card-a" src={SAMPLES.short} title="Card A" />
362
- <ActiveOutlineCard id="card-b" src={SAMPLES.voice} title="Card B" />
363
- </div>
364
- );
365
- }
366
-
367
- function CustomLayoutDemo() {
368
- return (
369
- <TooltipProvider delayDuration={400}>
370
- <PlayerProvider src={SAMPLES.long} title="Custom layout demo" artist="phonograph" cover={COVER}>
371
- <div className="grid grid-cols-[96px_1fr_auto] gap-4 rounded-lg border border-border/60 bg-card p-4">
372
- {/* Left: large cover spanning two rows */}
373
- <div className="row-span-2">
374
- <Cover size={96} alt="Custom layout cover" />
375
- </div>
376
-
377
- {/* Top right: meta + timer */}
378
- <div className="min-w-0">
379
- <Title />
380
- <Artist />
381
- </div>
382
- <TimeDisplay />
383
-
384
- {/* Bottom: full-width waveform + controls */}
385
- <div className="col-span-2 space-y-3">
386
- <Waveform height={48} />
387
- <div className="flex items-center justify-between gap-2">
388
- <PlayButton />
389
- <div className="flex items-center gap-1">
390
- <VolumeControl />
391
- <LoopButton />
392
- </div>
393
- </div>
394
- </div>
395
- </div>
396
- </PlayerProvider>
397
- </TooltipProvider>
398
- );
399
- }
400
-
401
- export const Interactive = () => {
402
- const [sampleKey] = useSelect('sample', {
403
- options: ['short', 'voice', 'long'] as const,
404
- defaultValue: 'short',
405
- label: 'Sample',
406
- });
407
- const [mode] = useSelect('waveformMode', {
408
- options: ['peaks', 'live', 'bars', 'progress', 'none'] as const,
409
- defaultValue: 'peaks',
410
- label: 'Waveform mode',
411
- });
412
- const [variant] = useSelect('variant', {
413
- options: ['auto', 'default', 'compact'] as const,
414
- defaultValue: 'default',
415
- label: 'Variant',
416
- });
417
- const [withCover] = useBoolean('withCover', { defaultValue: true, label: 'Show cover' });
418
- const [reactive] = useBoolean('reactive', {
419
- defaultValue: false,
420
- label: 'Reactive cover (subtle)',
421
- });
422
-
423
- const [userSrc, setUserSrc] = useState<string | null>(null);
424
- const [userName, setUserName] = useState<string>('');
425
-
426
- useEffect(() => {
427
- return () => {
428
- if (userSrc?.startsWith('blob:')) URL.revokeObjectURL(userSrc);
429
- };
430
- }, [userSrc]);
431
-
432
- const src = userSrc ?? SAMPLES[sampleKey];
433
-
434
- return (
435
- <Frame>
436
- <div className="mb-4 flex flex-wrap items-center gap-3">
437
- <label className="inline-flex cursor-pointer items-center gap-2 rounded-md border border-border/60 bg-card px-3 py-1.5 text-xs hover:bg-accent">
438
- <span>Drop / pick a file</span>
439
- <input
440
- type="file"
441
- accept="audio/*"
442
- className="hidden"
443
- onChange={(e) => {
444
- const file = e.target.files?.[0];
445
- if (!file) return;
446
- if (userSrc?.startsWith('blob:')) URL.revokeObjectURL(userSrc);
447
- setUserSrc(URL.createObjectURL(file));
448
- setUserName(file.name);
449
- }}
450
- />
451
- </label>
452
- {userSrc && (
453
- <button
454
- type="button"
455
- onClick={() => {
456
- if (userSrc.startsWith('blob:')) URL.revokeObjectURL(userSrc);
457
- setUserSrc(null);
458
- setUserName('');
459
- }}
460
- className="rounded-md border border-border/60 px-2 py-1 text-xs text-muted-foreground hover:bg-accent"
461
- >
462
- Reset to sample
463
- </button>
464
- )}
465
- <span className="text-xs text-muted-foreground">
466
- {userSrc ? userName : `sample: ${sampleKey}`}
467
- </span>
468
- </div>
469
- <Player
470
- key={src}
471
- src={src}
472
- title={userSrc ? userName : 'Local sample'}
473
- artist={userSrc ? 'Your file (blob URL)' : 'wavesurfer / phonograph'}
474
- cover={withCover ? COVER : undefined}
475
- variant={variant}
476
- waveform={{ mode: mode as WaveformMode }}
477
- reactiveCover={reactive ? 'subtle' : false}
478
- />
479
- </Frame>
480
- );
481
- };