@djangocfg/ui-tools 2.1.382 → 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 (62) hide show
  1. package/dist/DictationField-U25MEYAL.mjs +4 -0
  2. package/dist/{DictationField-2ZLQWLYV.mjs.map → DictationField-U25MEYAL.mjs.map} +1 -1
  3. package/dist/DictationField-XWR5VOID.cjs +13 -0
  4. package/dist/{DictationField-IPPJ54CU.cjs.map → DictationField-XWR5VOID.cjs.map} +1 -1
  5. package/dist/{chunk-KMSBGNVC.cjs → chunk-4PFW7MIJ.cjs} +4 -2
  6. package/dist/chunk-4PFW7MIJ.cjs.map +1 -0
  7. package/dist/{chunk-4LXG3NBV.mjs → chunk-C2YN6WEO.mjs} +3 -3
  8. package/dist/chunk-C2YN6WEO.mjs.map +1 -0
  9. package/dist/index.cjs +139 -2
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +68 -1
  12. package/dist/index.d.ts +68 -1
  13. package/dist/index.mjs +141 -6
  14. package/dist/index.mjs.map +1 -1
  15. package/package.json +6 -13
  16. package/src/tools/Chat/index.ts +15 -0
  17. package/dist/DictationField-2ZLQWLYV.mjs +0 -4
  18. package/dist/DictationField-IPPJ54CU.cjs +0 -13
  19. package/dist/chunk-4LXG3NBV.mjs.map +0 -1
  20. package/dist/chunk-KMSBGNVC.cjs.map +0 -1
  21. package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +0 -771
  22. package/src/stories/index.ts +0 -63
  23. package/src/tools/AudioPlayer/AudioPlayer.story.tsx +0 -481
  24. package/src/tools/Chat/stories/01-basic.story.tsx +0 -64
  25. package/src/tools/Chat/stories/02-bubbles.story.tsx +0 -21
  26. package/src/tools/Chat/stories/03-tool-calls.story.tsx +0 -59
  27. package/src/tools/Chat/stories/04-personas.story.tsx +0 -78
  28. package/src/tools/Chat/stories/05-launcher.story.tsx +0 -321
  29. package/src/tools/Chat/stories/06-header.story.tsx +0 -147
  30. package/src/tools/Chat/stories/07-audio-actions.story.tsx +0 -112
  31. package/src/tools/Chat/stories/shared/Frame.tsx +0 -21
  32. package/src/tools/Chat/stories/shared/index.ts +0 -5
  33. package/src/tools/Chat/stories/shared/messages.ts +0 -39
  34. package/src/tools/Chat/stories/shared/personas.ts +0 -13
  35. package/src/tools/Chat/stories/shared/seeds.ts +0 -92
  36. package/src/tools/Chat/stories/shared/transports.ts +0 -36
  37. package/src/tools/CodeEditor/CodeEditor.story.tsx +0 -202
  38. package/src/tools/CronScheduler/CronScheduler.story.tsx +0 -300
  39. package/src/tools/Gallery/Gallery.story.tsx +0 -237
  40. package/src/tools/ImageViewer/ImageViewer.story.tsx +0 -85
  41. package/src/tools/JsonForm/JsonForm.story.tsx +0 -350
  42. package/src/tools/JsonTree/JsonTree.story.tsx +0 -141
  43. package/src/tools/LottiePlayer/LottiePlayer.story.tsx +0 -95
  44. package/src/tools/Map/Map.story.tsx +0 -458
  45. package/src/tools/MarkdownEditor/MarkdownEditor.story.tsx +0 -225
  46. package/src/tools/Mermaid/Mermaid.story.tsx +0 -251
  47. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +0 -230
  48. package/src/tools/PrettyCode/PrettyCode.story.tsx +0 -304
  49. package/src/tools/SpeechRecognition/stories/01-basic.story.tsx +0 -32
  50. package/src/tools/SpeechRecognition/stories/02-dictation-field.story.tsx +0 -32
  51. package/src/tools/SpeechRecognition/stories/03-push-to-talk.story.tsx +0 -27
  52. package/src/tools/SpeechRecognition/stories/04-mic-meter.story.tsx +0 -35
  53. package/src/tools/SpeechRecognition/stories/05-custom-engine-http.story.tsx +0 -40
  54. package/src/tools/SpeechRecognition/stories/06-custom-engine-ws.story.tsx +0 -48
  55. package/src/tools/SpeechRecognition/stories/07-language-device.story.tsx +0 -57
  56. package/src/tools/SpeechRecognition/stories/08-errors-permissions.story.tsx +0 -25
  57. package/src/tools/SpeechRecognition/stories/09-chat-voice.story.tsx +0 -90
  58. package/src/tools/SpeechRecognition/stories/shared.tsx +0 -123
  59. package/src/tools/Tour/Tour.story.tsx +0 -279
  60. package/src/tools/Tree/Tree.story.tsx +0 -620
  61. package/src/tools/Uploader/Uploader.story.tsx +0 -415
  62. package/src/tools/VideoPlayer/VideoPlayer.story.tsx +0 -87
@@ -1,63 +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 ChatBasic from '../tools/Chat/stories/01-basic.story';
20
- import * as ChatBubbles from '../tools/Chat/stories/02-bubbles.story';
21
- import * as ChatToolCalls from '../tools/Chat/stories/03-tool-calls.story';
22
- import * as ChatPersonas from '../tools/Chat/stories/04-personas.story';
23
- import * as ChatLauncher from '../tools/Chat/stories/05-launcher.story';
24
- import * as ChatHeader from '../tools/Chat/stories/06-header.story';
25
- import * as ChatAudio from '../tools/Chat/stories/07-audio-actions.story';
26
- import * as STTBasic from '../tools/SpeechRecognition/stories/01-basic.story';
27
- import * as STTDictation from '../tools/SpeechRecognition/stories/02-dictation-field.story';
28
- import * as STTPushToTalk from '../tools/SpeechRecognition/stories/03-push-to-talk.story';
29
- import * as STTMicMeter from '../tools/SpeechRecognition/stories/04-mic-meter.story';
30
- import * as STTHttp from '../tools/SpeechRecognition/stories/05-custom-engine-http.story';
31
- import * as STTWs from '../tools/SpeechRecognition/stories/06-custom-engine-ws.story';
32
- import * as STTLangDevice from '../tools/SpeechRecognition/stories/07-language-device.story';
33
- import * as STTErrors from '../tools/SpeechRecognition/stories/08-errors-permissions.story';
34
- import * as STTChatVoice from '../tools/SpeechRecognition/stories/09-chat-voice.story';
35
-
36
- export const uiToolsStoryModules: Record<string, StoryModule> = {
37
- 'JsonForm.story.tsx': JsonForm,
38
- 'JsonTree.story.tsx': JsonTree,
39
- 'OpenapiViewer.story.tsx': OpenapiViewer,
40
- 'Mermaid.story.tsx': Mermaid,
41
- 'Map.story.tsx': Map,
42
- 'Tour.story.tsx': Tour,
43
- 'CodeEditor.story.tsx': CodeEditor,
44
- 'PrettyCode.story.tsx': PrettyCode,
45
- 'MarkdownEditor.story.tsx': MarkdownEditor,
46
- 'Chat/01-basic.story.tsx': ChatBasic,
47
- 'Chat/02-bubbles.story.tsx': ChatBubbles,
48
- 'Chat/03-tool-calls.story.tsx': ChatToolCalls,
49
- 'Chat/04-personas.story.tsx': ChatPersonas,
50
- 'Chat/05-launcher.story.tsx': ChatLauncher,
51
- 'Chat/06-header.story.tsx': ChatHeader,
52
- 'Chat/07-audio-actions.story.tsx': ChatAudio,
53
- 'SpeechRecognition/01-basic.story.tsx': STTBasic,
54
- 'SpeechRecognition/02-dictation-field.story.tsx': STTDictation,
55
- 'SpeechRecognition/03-push-to-talk.story.tsx': STTPushToTalk,
56
- 'SpeechRecognition/04-mic-meter.story.tsx': STTMicMeter,
57
- 'SpeechRecognition/05-custom-engine-http.story.tsx': STTHttp,
58
- 'SpeechRecognition/06-custom-engine-ws.story.tsx': STTWs,
59
- 'SpeechRecognition/07-language-device.story.tsx': STTLangDevice,
60
- 'SpeechRecognition/08-errors-permissions.story.tsx': STTErrors,
61
- 'Chat/08-voice-composer.story.tsx': STTChatVoice,
62
- };
63
-
@@ -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
- };
@@ -1,64 +0,0 @@
1
- import { useMemo } from 'react';
2
-
3
- import { defineStory } from '@djangocfg/playground';
4
-
5
- import { ChatRoot } from '../components/ChatRoot';
6
- import { Composer } from '../components/Composer';
7
- import { EmptyState } from '../components/EmptyState';
8
- import { MessageList } from '../components/MessageList';
9
- import { ChatProvider, useChatContext } from '../context';
10
- import { useChatComposer } from '../hooks/useChatComposer';
11
- import { Frame, SEED_BASIC, makeBasicTransport } from './shared';
12
-
13
- export default defineStory({
14
- title: 'Tools/Chat/Basic',
15
- component: ChatRoot,
16
- description:
17
- 'Default chat and the composition pattern. Start here. Mock transport, markdown replies, streaming.',
18
- });
19
-
20
- export const Default = () => {
21
- const transport = useMemo(() => makeBasicTransport(SEED_BASIC), []);
22
- return (
23
- <Frame>
24
- <ChatRoot
25
- transport={transport}
26
- config={{
27
- greeting: 'Hi there 👋',
28
- description: 'Try sending a message — replies are scripted by the mock transport.',
29
- placeholder: 'Ask anything…',
30
- suggestions: [
31
- { label: 'Show me markdown', prompt: 'Give me a markdown sample' },
32
- { label: 'Explain streaming', prompt: 'How does streaming work here?' },
33
- ],
34
- }}
35
- />
36
- </Frame>
37
- );
38
- };
39
-
40
- function ComposedLayout() {
41
- const ctx = useChatContext();
42
- const composer = useChatComposer({ onSubmit: ctx.sendMessage });
43
- return (
44
- <div className="flex h-full flex-col">
45
- <div className="border-b px-3 py-2 text-sm font-medium">My header</div>
46
- <MessageList
47
- className="flex-1"
48
- renderEmpty={() => <EmptyState greeting="Bring your own layout" />}
49
- />
50
- <Composer composer={composer} />
51
- </div>
52
- );
53
- }
54
-
55
- export const Composition = () => {
56
- const transport = useMemo(() => makeBasicTransport(SEED_BASIC), []);
57
- return (
58
- <Frame>
59
- <ChatProvider transport={transport} config={{ greeting: 'Composed chat' }}>
60
- <ComposedLayout />
61
- </ChatProvider>
62
- </Frame>
63
- );
64
- };
@@ -1,21 +0,0 @@
1
- import { defineStory } from '@djangocfg/playground';
2
-
3
- import { MessageBubble } from '../components/MessageBubble';
4
- import { BUBBLE_GALLERY, Frame } from './shared';
5
-
6
- export default defineStory({
7
- title: 'Tools/Chat/Bubbles',
8
- component: MessageBubble,
9
- description:
10
- 'All bubble states: user, assistant, error, streaming with tool-activity. Verifies role-aware token styling (anchor contrast, error tint, streaming chrome).',
11
- });
12
-
13
- export const Gallery = () => (
14
- <Frame h={600} w={520}>
15
- <div className="flex flex-col gap-2 overflow-y-auto p-3">
16
- {BUBBLE_GALLERY.map((m) => (
17
- <MessageBubble key={m.id} message={m} />
18
- ))}
19
- </div>
20
- </Frame>
21
- );
@@ -1,59 +0,0 @@
1
- import { useMemo } from 'react';
2
-
3
- import { defineStory } from '@djangocfg/playground';
4
-
5
- import { LazyJsonTree } from '../../JsonTree/lazy';
6
- import { ChatRoot } from '../components/ChatRoot';
7
- import { ToolCalls } from '../components/ToolCalls';
8
- import { createMockTransport } from '../core/transport/mock';
9
- import {
10
- Frame,
11
- SEED_JSON_PAYLOAD,
12
- SEED_TOOL_CALLS,
13
- makeToolCallsTransport,
14
- } from './shared';
15
-
16
- export default defineStory({
17
- title: 'Tools/Chat/ToolCalls',
18
- component: ToolCalls,
19
- description:
20
- 'Streaming tool-call panels (auto-open while running, auto-close on success) and the `renderInput`/`renderOutput` payload dispatcher.',
21
- });
22
-
23
- export const Default = () => {
24
- const transport = useMemo(() => makeToolCallsTransport(SEED_TOOL_CALLS), []);
25
- return (
26
- <Frame h={620}>
27
- <ChatRoot
28
- transport={transport}
29
- config={{ greeting: 'Tool-call panel — try sending a message to trigger another search.' }}
30
- />
31
- </Frame>
32
- );
33
- };
34
-
35
- export const WithJsonTreePayload = () => {
36
- const transport = useMemo(
37
- () =>
38
- createMockTransport({
39
- initialMessages: SEED_JSON_PAYLOAD,
40
- replies: ['Open the tool panel above to inspect the payload.'],
41
- latencyMs: 40,
42
- }),
43
- [],
44
- );
45
-
46
- return (
47
- <Frame h={620}>
48
- <ChatRoot
49
- transport={transport}
50
- config={{ greeting: 'JsonTree payload demo' }}
51
- toolCallsProps={{
52
- defaultExpanded: true,
53
- renderInput: (input) => <LazyJsonTree data={input} mode="compact" />,
54
- renderOutput: (output) => <LazyJsonTree data={output} mode="compact" />,
55
- }}
56
- />
57
- </Frame>
58
- );
59
- };