@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.
- package/README.md +30 -0
- package/dist/docs/DocsRenderer.d.ts.map +1 -1
- package/dist/docs/DocsRenderer.js +12 -1
- package/dist/docs/DocsRenderer.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/stl/StlViewer.d.ts +13 -0
- package/dist/stl/StlViewer.d.ts.map +1 -0
- package/dist/stl/StlViewer.js +277 -0
- package/dist/stl/StlViewer.js.map +1 -0
- package/dist/stl/index.d.ts +5 -0
- package/dist/stl/index.d.ts.map +1 -0
- package/dist/stl/index.js +3 -0
- package/dist/stl/index.js.map +1 -0
- package/dist/stl/parse.d.ts +13 -0
- package/dist/stl/parse.d.ts.map +1 -0
- package/dist/stl/parse.js +69 -0
- package/dist/stl/parse.js.map +1 -0
- package/dist/strudel/StrudelPlayer.d.ts +15 -0
- package/dist/strudel/StrudelPlayer.d.ts.map +1 -0
- package/dist/strudel/StrudelPlayer.js +178 -0
- package/dist/strudel/StrudelPlayer.js.map +1 -0
- package/dist/strudel/engine.d.ts +38 -0
- package/dist/strudel/engine.d.ts.map +1 -0
- package/dist/strudel/engine.js +65 -0
- package/dist/strudel/engine.js.map +1 -0
- package/dist/strudel/index.d.ts +5 -0
- package/dist/strudel/index.d.ts.map +1 -0
- package/dist/strudel/index.js +4 -0
- package/dist/strudel/index.js.map +1 -0
- package/dist/strudel/url.d.ts +5 -0
- package/dist/strudel/url.d.ts.map +1 -0
- package/dist/strudel/url.js +9 -0
- package/dist/strudel/url.js.map +1 -0
- package/dist/styles.css +194 -0
- package/package.json +14 -1
- package/src/docs/DocsRenderer.tsx +12 -1
- package/src/env.d.ts +3 -0
- package/src/index.ts +4 -0
- package/src/stl/StlViewer.tsx +334 -0
- package/src/stl/index.ts +4 -0
- package/src/stl/parse.ts +72 -0
- package/src/strudel/StrudelPlayer.tsx +246 -0
- package/src/strudel/engine.ts +98 -0
- package/src/strudel/index.ts +4 -0
- package/src/strudel/url.ts +9 -0
- 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,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;
|