@brett_lamy/docstream 1.9.1 → 1.10.0

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 (49) hide show
  1. package/README.md +30 -0
  2. package/dist/docs/DocsRenderer.d.ts.map +1 -1
  3. package/dist/docs/DocsRenderer.js +12 -1
  4. package/dist/docs/DocsRenderer.js.map +1 -1
  5. package/dist/index.d.ts +4 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/stl/StlViewer.d.ts +13 -0
  10. package/dist/stl/StlViewer.d.ts.map +1 -0
  11. package/dist/stl/StlViewer.js +277 -0
  12. package/dist/stl/StlViewer.js.map +1 -0
  13. package/dist/stl/index.d.ts +5 -0
  14. package/dist/stl/index.d.ts.map +1 -0
  15. package/dist/stl/index.js +3 -0
  16. package/dist/stl/index.js.map +1 -0
  17. package/dist/stl/parse.d.ts +13 -0
  18. package/dist/stl/parse.d.ts.map +1 -0
  19. package/dist/stl/parse.js +69 -0
  20. package/dist/stl/parse.js.map +1 -0
  21. package/dist/strudel/StrudelPlayer.d.ts +15 -0
  22. package/dist/strudel/StrudelPlayer.d.ts.map +1 -0
  23. package/dist/strudel/StrudelPlayer.js +178 -0
  24. package/dist/strudel/StrudelPlayer.js.map +1 -0
  25. package/dist/strudel/engine.d.ts +38 -0
  26. package/dist/strudel/engine.d.ts.map +1 -0
  27. package/dist/strudel/engine.js +65 -0
  28. package/dist/strudel/engine.js.map +1 -0
  29. package/dist/strudel/index.d.ts +5 -0
  30. package/dist/strudel/index.d.ts.map +1 -0
  31. package/dist/strudel/index.js +4 -0
  32. package/dist/strudel/index.js.map +1 -0
  33. package/dist/strudel/url.d.ts +5 -0
  34. package/dist/strudel/url.d.ts.map +1 -0
  35. package/dist/strudel/url.js +9 -0
  36. package/dist/strudel/url.js.map +1 -0
  37. package/dist/styles.css +194 -0
  38. package/package.json +14 -1
  39. package/src/docs/DocsRenderer.tsx +12 -1
  40. package/src/env.d.ts +3 -0
  41. package/src/index.ts +4 -0
  42. package/src/stl/StlViewer.tsx +334 -0
  43. package/src/stl/index.ts +4 -0
  44. package/src/stl/parse.ts +72 -0
  45. package/src/strudel/StrudelPlayer.tsx +246 -0
  46. package/src/strudel/engine.ts +98 -0
  47. package/src/strudel/index.ts +4 -0
  48. package/src/strudel/url.ts +9 -0
  49. package/src/styles.css +194 -0
@@ -0,0 +1,246 @@
1
+ import { useEffect, useId, useRef, useState, useSyncExternalStore } from "react"
2
+ import { Code2, Loader2, Pause, Play } from "lucide-react"
3
+ import { HighlightedCode } from "../docs/HighlightedCode"
4
+ import {
5
+ DEFAULT_SAMPLES,
6
+ getStrudelSnapshot,
7
+ playStrudel,
8
+ stopStrudel,
9
+ strudelNow,
10
+ subscribeStrudel,
11
+ type StrudelHap,
12
+ type StrudelPattern,
13
+ } from "./engine"
14
+
15
+ export interface StrudelPlayerProps {
16
+ /** Strudel pattern code (what you would type at strudel.cc). */
17
+ code?: string
18
+ /** URL of a `.strudel` file to load when `code` is absent. */
19
+ src?: string
20
+ title?: string
21
+ className?: string
22
+ /** Show the code under the player from the start. Defaults to false. */
23
+ showCode?: boolean
24
+ /** Sample banks to load before the first play. Defaults to the Tidal dirt-samples. */
25
+ samples?: readonly string[]
26
+ }
27
+
28
+ type Source = { kind: "loading" } | { kind: "error"; message: string } | { kind: "ready"; code: string }
29
+
30
+ const NOTE = /^([a-g])([#bs]*)(-?\d+)?$/i
31
+ const STEPS: Record<string, number> = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }
32
+
33
+ // A vertical position for an event: its pitch when it has one, else a stable lane per sample name.
34
+ function pitch(value: unknown): number | null {
35
+ if (typeof value === "number") return value
36
+ if (!value || typeof value !== "object") return null
37
+ const v = value as Record<string, unknown>
38
+ if (typeof v.note === "number") return v.note
39
+ if (typeof v.note === "string") {
40
+ const m = NOTE.exec(v.note)
41
+ if (m) {
42
+ const accidental = [...m[2]!].reduce((n, c) => n + (c === "b" ? -1 : 1), 0)
43
+ return STEPS[m[1]!.toLowerCase()]! + accidental + (Number(m[3] ?? 3) + 1) * 12
44
+ }
45
+ }
46
+ if (typeof v.freq === "number") return 69 + 12 * Math.log2(v.freq / 440)
47
+ if (typeof v.n === "number" && typeof v.s !== "string") return v.n
48
+ if (typeof v.s === "string") {
49
+ let h = 0
50
+ for (const c of v.s) h = (h * 31 + c.charCodeAt(0)) | 0
51
+ return (Math.abs(h) % 24) + (typeof v.n === "number" ? v.n : 0)
52
+ }
53
+ return null
54
+ }
55
+
56
+ const num = (t: number | { valueOf(): number }) => Number(t.valueOf())
57
+
58
+ // Scrolling piano roll: two cycles centered on the playhead, onsets only. Idle: quiet bars.
59
+ function drawRoll(canvas: HTMLCanvasElement, pattern: StrudelPattern | null, now: number | null) {
60
+ const ctx = canvas.getContext("2d")
61
+ if (!ctx) return
62
+ const dpr = Math.min(window.devicePixelRatio || 1, 2)
63
+ const w = Math.max(1, Math.round(canvas.clientWidth * dpr))
64
+ const h = Math.max(1, Math.round(canvas.clientHeight * dpr))
65
+ if (canvas.width !== w || canvas.height !== h) {
66
+ canvas.width = w
67
+ canvas.height = h
68
+ }
69
+ ctx.clearRect(0, 0, w, h)
70
+ ctx.fillStyle = getComputedStyle(canvas).color
71
+ if (!pattern || now === null) {
72
+ ctx.globalAlpha = 0.18
73
+ const bars = 48
74
+ for (let i = 0; i < bars; i++) {
75
+ const bh = (0.2 + 0.6 * Math.abs(Math.sin(i * 1.7) * Math.cos(i * 0.6))) * h
76
+ ctx.fillRect((i / bars) * w + 1, (h - bh) / 2, w / bars - 3 * dpr, bh)
77
+ }
78
+ ctx.globalAlpha = 1
79
+ return
80
+ }
81
+ const begin = now - 1, end = now + 1
82
+ let haps: StrudelHap[]
83
+ try {
84
+ haps = pattern.queryArc(begin, end).filter((hap) => hap.whole && num(hap.whole.begin) === num(hap.part.begin))
85
+ } catch {
86
+ return
87
+ }
88
+ const pitches = haps.map((hap) => pitch(hap.value))
89
+ const known = pitches.filter((p): p is number => p !== null)
90
+ const lo = known.length ? Math.min(...known) : 0
91
+ const hi = known.length ? Math.max(...known) : 1
92
+ const range = Math.max(hi - lo, 11)
93
+ const pad = 4 * dpr
94
+ const lane = Math.max(3 * dpr, Math.min(10 * dpr, (h - 2 * pad) / (range + 1)))
95
+ const x = (t: number) => ((t - begin) / (end - begin)) * w
96
+ haps.forEach((hap, i) => {
97
+ const p = pitches[i] ?? lo
98
+ const s = num(hap.whole!.begin), e = num(hap.whole!.end)
99
+ const y = h - pad - lane - ((p - lo + (range - (hi - lo)) / 2) / range) * (h - 2 * pad - lane)
100
+ ctx.globalAlpha = s <= now && now < e ? 1 : s > now ? 0.45 : 0.25
101
+ ctx.fillRect(x(s), y, Math.max(2 * dpr, x(e) - x(s) - 1.5 * dpr), lane)
102
+ })
103
+ ctx.globalAlpha = 0.6
104
+ ctx.fillRect(w / 2 - dpr / 2, 0, dpr, h)
105
+ ctx.globalAlpha = 1
106
+ }
107
+
108
+ /** A live-coded music player: Strudel (strudel.cc) runs the pattern in this page, no iframe. */
109
+ export function StrudelPlayer({ code, src, title, className, showCode = false, samples = DEFAULT_SAMPLES }: StrudelPlayerProps) {
110
+ const id = useId()
111
+ const [source, setSource] = useState<Source>(() => (code !== undefined ? { kind: "ready", code } : { kind: "loading" }))
112
+ const [codeOpen, setCodeOpen] = useState(showCode)
113
+ const [error, setError] = useState<string | null>(null)
114
+ const [cycle, setCycle] = useState(0)
115
+ const pattern = useRef<StrudelPattern | null>(null)
116
+ const canvasRef = useRef<HTMLCanvasElement>(null)
117
+ const snapshot = useSyncExternalStore(subscribeStrudel, getStrudelSnapshot, getStrudelSnapshot)
118
+ const status = snapshot.owner === id ? snapshot.status : "idle"
119
+
120
+ useEffect(() => {
121
+ if (code !== undefined) {
122
+ setSource({ kind: "ready", code })
123
+ return
124
+ }
125
+ if (!src) {
126
+ setSource({ kind: "error", message: "no pattern" })
127
+ return
128
+ }
129
+ let live = true
130
+ setSource({ kind: "loading" })
131
+ fetch(src)
132
+ .then((res) => {
133
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim())
134
+ return res.text()
135
+ })
136
+ .then((text) => live && setSource({ kind: "ready", code: text }))
137
+ .catch((err: unknown) => live && setSource({ kind: "error", message: err instanceof Error ? err.message : String(err) }))
138
+ return () => {
139
+ live = false
140
+ }
141
+ }, [code, src])
142
+
143
+ // Stop our pattern when the player unmounts or its code changes underneath it.
144
+ const ready = source.kind === "ready" ? source.code : null
145
+ useEffect(() => () => void stopStrudel(id), [id, ready])
146
+
147
+ useEffect(() => {
148
+ const canvas = canvasRef.current
149
+ if (!canvas) return
150
+ if (status !== "playing") {
151
+ drawRoll(canvas, null, null)
152
+ const resize = new ResizeObserver(() => drawRoll(canvas, null, null))
153
+ resize.observe(canvas)
154
+ return () => resize.disconnect()
155
+ }
156
+ let frame = 0
157
+ let shown = -1
158
+ const tick = () => {
159
+ const now = strudelNow()
160
+ drawRoll(canvas, pattern.current, now)
161
+ const whole = now === null ? 0 : Math.floor(now) + 1
162
+ if (whole !== shown) setCycle((shown = whole))
163
+ frame = requestAnimationFrame(tick)
164
+ }
165
+ tick()
166
+ return () => cancelAnimationFrame(frame)
167
+ }, [status])
168
+
169
+ const toggle = async () => {
170
+ if (source.kind !== "ready") return
171
+ if (status !== "idle") {
172
+ await stopStrudel(id)
173
+ return
174
+ }
175
+ setError(null)
176
+ try {
177
+ pattern.current = await playStrudel(id, source.code, samples)
178
+ } catch (err) {
179
+ if (err instanceof Error && err.message === "superseded") return
180
+ setError(err instanceof Error ? err.message : String(err))
181
+ }
182
+ }
183
+
184
+ const label = title ?? (src ? src.split(/[?#]/)[0]!.split("/").pop()! : "Strudel pattern")
185
+ const codeId = `${id}-code`
186
+ const sub =
187
+ source.kind === "error"
188
+ ? `Couldn't load: ${source.message}`
189
+ : source.kind === "loading"
190
+ ? "Loading…"
191
+ : status === "loading"
192
+ ? "Loading sounds…"
193
+ : status === "playing"
194
+ ? `Cycle ${cycle}`
195
+ : "Strudel"
196
+ return (
197
+ <figure className={`docstream-strudel ${className ?? ""}`.trim()} data-state={status}>
198
+ <div className="docstream-strudel__bar">
199
+ <button
200
+ type="button"
201
+ className="docstream-strudel__play"
202
+ onClick={toggle}
203
+ disabled={source.kind !== "ready"}
204
+ aria-label={status === "idle" ? `Play ${label}` : `Stop ${label}`}
205
+ >
206
+ {status === "loading" ? (
207
+ <Loader2 className="docstream-strudel__spin" size={18} />
208
+ ) : status === "playing" ? (
209
+ <Pause size={18} />
210
+ ) : (
211
+ <Play size={18} />
212
+ )}
213
+ </button>
214
+ <div className="docstream-strudel__meta">
215
+ <span className="docstream-strudel__title">{label}</span>
216
+ <span className="docstream-strudel__sub" aria-live="polite">
217
+ {sub}
218
+ </span>
219
+ </div>
220
+ <canvas ref={canvasRef} className="docstream-strudel__roll" aria-hidden="true" />
221
+ {source.kind === "ready" && (
222
+ <button
223
+ type="button"
224
+ className="docstream-strudel__toggle"
225
+ aria-expanded={codeOpen}
226
+ aria-controls={codeId}
227
+ aria-label={codeOpen ? "Hide code" : "Show code"}
228
+ onClick={() => setCodeOpen((open) => !open)}
229
+ >
230
+ <Code2 size={16} />
231
+ </button>
232
+ )}
233
+ </div>
234
+ {error && (
235
+ <div className="docstream-strudel__error" role="alert">
236
+ {error}
237
+ </div>
238
+ )}
239
+ {source.kind === "ready" && codeOpen && (
240
+ <div id={codeId} className="docstream-strudel__code">
241
+ <HighlightedCode code={source.code.trim()} language="javascript" />
242
+ </div>
243
+ )}
244
+ </figure>
245
+ )
246
+ }
@@ -0,0 +1,98 @@
1
+ // One Strudel runtime per page, loaded on the first play (@strudel/web is ~850 KB and touches
2
+ // `window` at import). Players share it, so starting one stops whichever was playing.
3
+
4
+ /** A scheduled event, as `Pattern.queryArc` returns it. */
5
+ export interface StrudelHap {
6
+ whole?: { begin: number | { valueOf(): number }; end: number | { valueOf(): number } } | null
7
+ part: { begin: number | { valueOf(): number }; end: number | { valueOf(): number } }
8
+ value: unknown
9
+ }
10
+
11
+ export interface StrudelPattern {
12
+ queryArc(begin: number, end: number): StrudelHap[]
13
+ }
14
+
15
+ interface StrudelRepl {
16
+ evaluate(code: string, autostart?: boolean): Promise<StrudelPattern | undefined>
17
+ stop(): void
18
+ scheduler: { now(): number; started: boolean }
19
+ state: { evalError?: unknown }
20
+ }
21
+
22
+ interface StrudelWeb {
23
+ initStrudel(options?: { prebake?: () => unknown; onToggle?: (started: boolean) => void }): Promise<StrudelRepl>
24
+ samples(source: string): Promise<unknown>
25
+ }
26
+
27
+ /** Sample banks loaded before the first pattern, matching what strudel.cc code expects (`s("bd sd")`). */
28
+ export const DEFAULT_SAMPLES = ["github:tidalcycles/dirt-samples"]
29
+
30
+ export interface StrudelSnapshot {
31
+ /** The player id that owns the runtime right now, or null. */
32
+ owner: string | null
33
+ status: "idle" | "loading" | "playing"
34
+ }
35
+
36
+ let runtime: Promise<StrudelRepl> | null = null
37
+ let loaded: StrudelRepl | null = null
38
+ let snapshot: StrudelSnapshot = { owner: null, status: "idle" }
39
+ const listeners = new Set<() => void>()
40
+
41
+ function set(next: StrudelSnapshot) {
42
+ snapshot = next
43
+ listeners.forEach((fn) => fn())
44
+ }
45
+
46
+ export function subscribeStrudel(fn: () => void): () => void {
47
+ listeners.add(fn)
48
+ return () => listeners.delete(fn)
49
+ }
50
+
51
+ export function getStrudelSnapshot(): StrudelSnapshot {
52
+ return snapshot
53
+ }
54
+
55
+ function load(samples: readonly string[]): Promise<StrudelRepl> {
56
+ runtime ??= import("@strudel/web").then((mod: StrudelWeb) =>
57
+ mod.initStrudel({
58
+ prebake: () => Promise.all(samples.map((s) => mod.samples(s))),
59
+ onToggle: (started) => {
60
+ if (!started && snapshot.status === "playing") set({ owner: null, status: "idle" })
61
+ },
62
+ }),
63
+ ).then((repl) => (loaded = repl))
64
+ runtime.catch(() => {
65
+ runtime = null
66
+ })
67
+ return runtime
68
+ }
69
+
70
+ /** Evaluate `code` and start it, taking the runtime from any other player. Must run from a user gesture. */
71
+ export async function playStrudel(owner: string, code: string, samples: readonly string[] = DEFAULT_SAMPLES): Promise<StrudelPattern> {
72
+ set({ owner, status: "loading" })
73
+ try {
74
+ const repl = await load(samples)
75
+ if (snapshot.owner !== owner) throw new Error("superseded")
76
+ const pattern = await repl.evaluate(code, true)
77
+ if (!pattern || repl.state.evalError) {
78
+ const err = repl.state.evalError
79
+ throw err instanceof Error ? err : new Error(String(err ?? "Strudel could not evaluate this pattern"))
80
+ }
81
+ if (snapshot.owner === owner) set({ owner, status: "playing" })
82
+ return pattern
83
+ } catch (err) {
84
+ if (snapshot.owner === owner) set({ owner: null, status: "idle" })
85
+ throw err
86
+ }
87
+ }
88
+
89
+ export async function stopStrudel(owner: string): Promise<void> {
90
+ if (snapshot.owner !== owner) return
91
+ set({ owner: null, status: "idle" })
92
+ ;(await runtime)?.stop()
93
+ }
94
+
95
+ /** The scheduler's position in cycles, for animation frames; null while stopped or not loaded. */
96
+ export function strudelNow(): number | null {
97
+ return loaded?.scheduler.started ? loaded.scheduler.now() : null
98
+ }
@@ -0,0 +1,4 @@
1
+ export { StrudelPlayer } from "./StrudelPlayer"
2
+ export type { StrudelPlayerProps } from "./StrudelPlayer"
3
+ export { DEFAULT_SAMPLES, playStrudel, stopStrudel } from "./engine"
4
+ export { isStrudelFile, isStrudelLanguage } from "./url"
@@ -0,0 +1,9 @@
1
+ /** True for a URL whose path ends in `.strudel` (query string and hash ignored). */
2
+ export function isStrudelFile(url: string): boolean {
3
+ return /\.strudel(?:[?#]|$)/i.test(url.trim())
4
+ }
5
+
6
+ /** Fence languages that render as a Strudel player. */
7
+ export function isStrudelLanguage(language: string | null | undefined): boolean {
8
+ return !!language && /^strudel$/i.test(language)
9
+ }
package/src/styles.css CHANGED
@@ -450,6 +450,200 @@
450
450
  border-radius: 12px;
451
451
  }
452
452
 
453
+ /* StlViewer: a WebGL preview of an .stl model ({% embed url="part.stl" %}). The model takes
454
+ its color from `color`, so it follows the theme's primary. */
455
+ .docstream-stl,
456
+ .docstream-strudel {
457
+ margin: 0;
458
+ display: flex;
459
+ flex-direction: column;
460
+ gap: 6px;
461
+ }
462
+
463
+ .docstream-stl__stage {
464
+ position: relative;
465
+ aspect-ratio: 16 / 9;
466
+ border: 1px solid var(--gb-border);
467
+ border-radius: 12px;
468
+ overflow: hidden;
469
+ color: var(--gb-primary, #6366f1);
470
+ background:
471
+ radial-gradient(circle at 50% 40%, color-mix(in srgb, var(--gb-muted, #f4f4f5) 40%, transparent), transparent 70%),
472
+ var(--gb-muted, #f4f4f5);
473
+ }
474
+
475
+ .docstream-stl__canvas {
476
+ display: block;
477
+ width: 100%;
478
+ height: 100%;
479
+ cursor: grab;
480
+ touch-action: none;
481
+ }
482
+
483
+ .docstream-stl__canvas:active {
484
+ cursor: grabbing;
485
+ }
486
+
487
+ .docstream-stl__canvas:focus-visible {
488
+ outline: 2px solid var(--gb-primary, #6366f1);
489
+ outline-offset: -2px;
490
+ border-radius: 12px;
491
+ }
492
+
493
+ .docstream-stl__status {
494
+ display: grid;
495
+ place-items: center;
496
+ height: 100%;
497
+ padding: 16px;
498
+ text-align: center;
499
+ font-size: 13px;
500
+ color: var(--gb-muted-foreground);
501
+ }
502
+
503
+ .docstream-stl__reset {
504
+ position: absolute;
505
+ top: 8px;
506
+ right: 8px;
507
+ padding: 3px 10px;
508
+ border: 1px solid var(--gb-border);
509
+ border-radius: 999px;
510
+ background: var(--gb-panel, #fff);
511
+ color: var(--gb-panel-foreground, inherit);
512
+ font: inherit;
513
+ font-size: 12px;
514
+ cursor: pointer;
515
+ opacity: 0.85;
516
+ }
517
+
518
+ .docstream-stl__reset:hover {
519
+ opacity: 1;
520
+ background: var(--gb-hover, var(--gb-accent));
521
+ }
522
+
523
+ .docstream-stl__caption {
524
+ display: flex;
525
+ flex-wrap: wrap;
526
+ justify-content: space-between;
527
+ gap: 4px 12px;
528
+ font-size: 12px;
529
+ color: var(--gb-muted-foreground);
530
+ }
531
+
532
+ .docstream-stl__caption a {
533
+ color: inherit;
534
+ font-weight: 500;
535
+ }
536
+
537
+ /* StrudelPlayer: a Strudel pattern as an audio player (```strudel fences, .strudel embeds). */
538
+ .docstream-strudel__bar {
539
+ display: flex;
540
+ align-items: center;
541
+ gap: 12px;
542
+ padding: 10px 12px;
543
+ border: 1px solid var(--gb-border);
544
+ border-radius: 12px;
545
+ background: var(--gb-panel, #fff);
546
+ color: var(--gb-panel-foreground, inherit);
547
+ }
548
+
549
+ .docstream-strudel__play {
550
+ flex: none;
551
+ display: grid;
552
+ place-items: center;
553
+ width: 40px;
554
+ height: 40px;
555
+ border: 0;
556
+ border-radius: 999px;
557
+ background: var(--gb-primary, #6366f1);
558
+ color: var(--gb-primary-foreground, #fff);
559
+ cursor: pointer;
560
+ }
561
+
562
+ .docstream-strudel__play:disabled {
563
+ opacity: 0.5;
564
+ cursor: default;
565
+ }
566
+
567
+ .docstream-strudel__play:focus-visible,
568
+ .docstream-strudel__toggle:focus-visible {
569
+ outline: 2px solid var(--gb-primary, #6366f1);
570
+ outline-offset: 2px;
571
+ }
572
+
573
+ .docstream-strudel__spin {
574
+ animation: docstream-strudel-spin 0.9s linear infinite;
575
+ }
576
+
577
+ @keyframes docstream-strudel-spin {
578
+ to {
579
+ transform: rotate(360deg);
580
+ }
581
+ }
582
+
583
+ .docstream-strudel__meta {
584
+ display: flex;
585
+ flex-direction: column;
586
+ min-width: 0;
587
+ flex: 0 1 auto;
588
+ max-width: 40%;
589
+ }
590
+
591
+ .docstream-strudel__title {
592
+ font-weight: 600;
593
+ font-size: 14px;
594
+ overflow: hidden;
595
+ text-overflow: ellipsis;
596
+ white-space: nowrap;
597
+ }
598
+
599
+ .docstream-strudel__sub {
600
+ font-size: 12px;
601
+ color: var(--gb-muted-foreground);
602
+ font-variant-numeric: tabular-nums;
603
+ }
604
+
605
+ .docstream-strudel__roll {
606
+ flex: 1 1 auto;
607
+ min-width: 60px;
608
+ height: 40px;
609
+ color: var(--gb-primary, #6366f1);
610
+ }
611
+
612
+ .docstream-strudel__toggle {
613
+ flex: none;
614
+ display: grid;
615
+ place-items: center;
616
+ width: 32px;
617
+ height: 32px;
618
+ border: 1px solid var(--gb-border);
619
+ border-radius: 8px;
620
+ background: transparent;
621
+ color: var(--gb-muted-foreground);
622
+ cursor: pointer;
623
+ }
624
+
625
+ .docstream-strudel__toggle[aria-expanded="true"],
626
+ .docstream-strudel__toggle:hover {
627
+ background: var(--gb-hover, var(--gb-accent));
628
+ color: inherit;
629
+ }
630
+
631
+ .docstream-strudel__error {
632
+ font-size: 12px;
633
+ color: var(--gb-danger, #dc2626);
634
+ white-space: pre-wrap;
635
+ }
636
+
637
+ .docstream-strudel__code {
638
+ border: 1px solid var(--gb-border);
639
+ border-radius: 12px;
640
+ background: var(--gb-code-bg);
641
+ color: var(--gb-code-fg);
642
+ padding: 12px 14px;
643
+ overflow-x: auto;
644
+ font-size: 13px;
645
+ }
646
+
453
647
  .docs-content-ref {
454
648
  display: flex !important;
455
649
  align-items: center;