@brett_lamy/docstream 0.3.1 → 0.3.3
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 +43 -0
- package/package.json +19 -4
- package/src/docs/DocsRenderer.tsx +37 -27
- package/src/env.d.ts +4 -0
- package/src/gitbook/ast.ts +6 -0
- package/src/gitbook/parse.ts +23 -1
- package/src/gitbook/serialize.ts +12 -2
- package/src/index.ts +4 -2
- package/src/playground/PlaygroundStreamdown.tsx +43 -0
- package/src/styles.css +16 -0
- package/src/viz/VizEmbed.tsx +42 -0
- package/src/viz/index.ts +4 -0
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ GitBook-aware markdown rendering for React applications and AI streaming surface
|
|
|
12
12
|
- Syntax-highlighted code blocks through `lowlight`.
|
|
13
13
|
- GitBook block support for hints, tabs, expandables, steppers, embeds, content refs, columns, figures, tables, math, dividers, updates, and OpenAPI operations.
|
|
14
14
|
- CSS exported as a stable package entrypoint so host apps can theme with CSS variables or shadcn-style design tokens.
|
|
15
|
+
- Attribute-aware direct video embeds for muted, looping inline clips in long-form posts.
|
|
16
|
+
- Optional `VizEmbed` integration for mounting deterministic `@brett_lamy/viz-engine` scenes in a document.
|
|
15
17
|
|
|
16
18
|
## Installation
|
|
17
19
|
|
|
@@ -39,6 +41,13 @@ If your TypeScript app checks CSS side-effect imports, include Vite's standard e
|
|
|
39
41
|
|
|
40
42
|
Use `GitbookStreamdown` when markdown may arrive incrementally from an AI stream. The component accepts either `markdown` or string children.
|
|
41
43
|
|
|
44
|
+
Markdown-only applications can import the renderer from the `streamdown`
|
|
45
|
+
subpath. This keeps optional playground integrations out of the host bundle:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { GitbookStreamdown } from "@brett_lamy/docstream/streamdown"
|
|
49
|
+
```
|
|
50
|
+
|
|
42
51
|
```tsx
|
|
43
52
|
import { GitbookStreamdown } from "@brett_lamy/docstream"
|
|
44
53
|
import "@brett_lamy/docstream/styles.css"
|
|
@@ -139,6 +148,40 @@ Call the API.
|
|
|
139
148
|
|
|
140
149
|
When a spec cannot be resolved, the renderer displays a fallback asking for an OpenAPI spec URL.
|
|
141
150
|
|
|
151
|
+
### Inline video clips
|
|
152
|
+
|
|
153
|
+
Direct media embeds can opt into browser-safe inline playback. `muted` is
|
|
154
|
+
important when `autoplay` is enabled:
|
|
155
|
+
|
|
156
|
+
```md
|
|
157
|
+
{% embed url="/generated/example/clips/offsets.mp4"
|
|
158
|
+
title="A reader resumes from its bookmark"
|
|
159
|
+
autoplay="true" loop="true" muted="true" controls="false" %}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The attributes round-trip through `parseMarkdown` and `serializeMarkdown` and
|
|
163
|
+
are rendered as a native `<video playsInline>` element.
|
|
164
|
+
|
|
165
|
+
### VizEngine scenes
|
|
166
|
+
|
|
167
|
+
Install the optional peer dependency when a document needs a live, seekable
|
|
168
|
+
scene rather than a rendered clip:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
npm install @brett_lamy/docstream @brett_lamy/viz-engine
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { VizEmbed } from "@brett_lamy/docstream/viz"
|
|
176
|
+
import "@brett_lamy/docstream/styles.css"
|
|
177
|
+
|
|
178
|
+
<VizEmbed scene={scene} title="The same event, replayed into two projections" />
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`VizEmbed` keeps the scene's timeline deterministic and delegates the clock to
|
|
182
|
+
VizEngine, so a reader can pause or scrub the same explanation used to produce
|
|
183
|
+
the short clip.
|
|
184
|
+
|
|
142
185
|
## Assets and OpenAPI Specs
|
|
143
186
|
|
|
144
187
|
Relative image and OpenAPI spec paths can be resolved against an asset base:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brett_lamy/docstream",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "GitBook-aware readonly markdown and AI stream renderer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"types": "./src/gitbook/index.ts",
|
|
22
22
|
"import": "./src/gitbook/index.ts"
|
|
23
23
|
},
|
|
24
|
+
"./streamdown": {
|
|
25
|
+
"types": "./src/streamdown.tsx",
|
|
26
|
+
"import": "./src/streamdown.tsx"
|
|
27
|
+
},
|
|
24
28
|
"./assets": {
|
|
25
29
|
"types": "./src/assets.ts",
|
|
26
30
|
"import": "./src/assets.ts"
|
|
@@ -37,6 +41,10 @@
|
|
|
37
41
|
"types": "./src/video/index.ts",
|
|
38
42
|
"import": "./src/video/index.ts"
|
|
39
43
|
},
|
|
44
|
+
"./viz": {
|
|
45
|
+
"types": "./src/viz/index.ts",
|
|
46
|
+
"import": "./src/viz/index.ts"
|
|
47
|
+
},
|
|
40
48
|
"./playground": {
|
|
41
49
|
"types": "./src/playground/index.ts",
|
|
42
50
|
"import": "./src/playground/index.ts"
|
|
@@ -50,14 +58,21 @@
|
|
|
50
58
|
"mermaid": "^11.15.0",
|
|
51
59
|
"rrweb": "2.0.0-alpha.20",
|
|
52
60
|
"streamdown": "^2.5.0",
|
|
53
|
-
"yaml": "^2.9.0"
|
|
61
|
+
"yaml": "^2.9.0",
|
|
62
|
+
"@brett_lamy/viz-engine": "^0.2.0"
|
|
54
63
|
},
|
|
55
64
|
"peerDependencies": {
|
|
56
|
-
"
|
|
57
|
-
"@
|
|
65
|
+
"@agent-wasm/core": ">=0.4.0",
|
|
66
|
+
"@brett_lamy/viz-engine": ">=0.2.0",
|
|
67
|
+
"react": ">=18"
|
|
58
68
|
},
|
|
59
69
|
"repository": {
|
|
60
70
|
"type": "git",
|
|
61
71
|
"url": "git+https://github.com/BLamy/docstream.git"
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"@agent-wasm/core": "^0.4.0",
|
|
75
|
+
"@types/react": "^18.3.3",
|
|
76
|
+
"typescript": "^5.5.4"
|
|
62
77
|
}
|
|
63
78
|
}
|
|
@@ -16,7 +16,14 @@ import { VideoEmbed } from "../video"
|
|
|
16
16
|
import { OpenApiOperation } from "../openapi/OpenApiOperation"
|
|
17
17
|
import { Mermaid } from "./Mermaid"
|
|
18
18
|
import { HighlightedCode } from "./HighlightedCode"
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
export interface LivePreviewProps {
|
|
21
|
+
files: Record<string, string>
|
|
22
|
+
entry: string
|
|
23
|
+
title: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type LivePreviewRenderer = (props: LivePreviewProps) => ReactNode
|
|
20
27
|
|
|
21
28
|
function InlineText({ nodes }: { nodes: Inline[] }) {
|
|
22
29
|
return (
|
|
@@ -72,7 +79,7 @@ function isDirectVideo(url: string): boolean {
|
|
|
72
79
|
return /\.(?:mp4|webm|ogg)(?:[?#]|$)/i.test(url)
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
function Tabs({ block }: { block: Extract<Block, { type: "tabs" }
|
|
82
|
+
function Tabs({ block, liveRenderer }: { block: Extract<Block, { type: "tabs" }>; liveRenderer?: LivePreviewRenderer }) {
|
|
76
83
|
const [active, setActive] = useState(0)
|
|
77
84
|
return (
|
|
78
85
|
<div className="docs-tabs">
|
|
@@ -88,13 +95,13 @@ function Tabs({ block }: { block: Extract<Block, { type: "tabs" }> }) {
|
|
|
88
95
|
))}
|
|
89
96
|
</div>
|
|
90
97
|
<div className="docs-tabs-body">
|
|
91
|
-
<Blocks blocks={block.tabs[active]?.children ?? []} />
|
|
98
|
+
<Blocks blocks={block.tabs[active]?.children ?? []} liveRenderer={liveRenderer} />
|
|
92
99
|
</div>
|
|
93
100
|
</div>
|
|
94
101
|
)
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
function BlockView({ block }: { block: Block }) {
|
|
104
|
+
function BlockView({ block, liveRenderer }: { block: Block; liveRenderer?: LivePreviewRenderer }) {
|
|
98
105
|
switch (block.type) {
|
|
99
106
|
case "paragraph":
|
|
100
107
|
return (
|
|
@@ -112,15 +119,13 @@ function BlockView({ block }: { block: Block }) {
|
|
|
112
119
|
}
|
|
113
120
|
case "code":
|
|
114
121
|
if (block.language === "mermaid") return <Mermaid code={block.code} />
|
|
115
|
-
if (block.live && isReactLanguage(block.language)) {
|
|
122
|
+
if (block.live && isReactLanguage(block.language) && liveRenderer) {
|
|
116
123
|
const entry = block.entry ?? "/src/main.jsx"
|
|
117
|
-
return (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
/>
|
|
123
|
-
)
|
|
124
|
+
return liveRenderer({
|
|
125
|
+
files: { [entry]: block.code },
|
|
126
|
+
entry,
|
|
127
|
+
title: block.title ?? "Live React preview",
|
|
128
|
+
})
|
|
124
129
|
}
|
|
125
130
|
return (
|
|
126
131
|
<div className="docs-code">
|
|
@@ -145,19 +150,19 @@ function BlockView({ block }: { block: Block }) {
|
|
|
145
150
|
<div className={`docs-hint docs-hint-${block.style}`}>
|
|
146
151
|
<Icon className="docs-hint-icon" />
|
|
147
152
|
<div>
|
|
148
|
-
<Blocks blocks={block.children} />
|
|
153
|
+
<Blocks blocks={block.children} liveRenderer={liveRenderer} />
|
|
149
154
|
</div>
|
|
150
155
|
</div>
|
|
151
156
|
)
|
|
152
157
|
}
|
|
153
158
|
case "tabs":
|
|
154
|
-
return <Tabs block={block} />
|
|
159
|
+
return <Tabs block={block} liveRenderer={liveRenderer} />
|
|
155
160
|
case "expandable":
|
|
156
161
|
return (
|
|
157
162
|
<details className="docs-expandable">
|
|
158
163
|
<summary>{block.summary}</summary>
|
|
159
164
|
<div className="docs-expandable-body">
|
|
160
|
-
<Blocks blocks={block.children} />
|
|
165
|
+
<Blocks blocks={block.children} liveRenderer={liveRenderer} />
|
|
161
166
|
</div>
|
|
162
167
|
</details>
|
|
163
168
|
)
|
|
@@ -172,7 +177,7 @@ function BlockView({ block }: { block: Block }) {
|
|
|
172
177
|
</div>
|
|
173
178
|
<div className="docs-step-main">
|
|
174
179
|
{s.title && <div className="docs-step-title">{s.title}</div>}
|
|
175
|
-
<Blocks blocks={s.children} />
|
|
180
|
+
<Blocks blocks={s.children} liveRenderer={liveRenderer} />
|
|
176
181
|
</div>
|
|
177
182
|
</div>
|
|
178
183
|
))}
|
|
@@ -187,7 +192,12 @@ function BlockView({ block }: { block: Block }) {
|
|
|
187
192
|
<VideoEmbed
|
|
188
193
|
src={src ?? block.url}
|
|
189
194
|
className="docs-embed"
|
|
190
|
-
title={block.url}
|
|
195
|
+
title={block.title ?? block.url}
|
|
196
|
+
{...(block.autoplay === undefined ? {} : { autoplay: block.autoplay })}
|
|
197
|
+
{...(block.loop === undefined ? {} : { loop: block.loop })}
|
|
198
|
+
{...(block.muted === undefined ? {} : { muted: block.muted })}
|
|
199
|
+
{...(block.controls === undefined ? {} : { controls: block.controls })}
|
|
200
|
+
{...(block.poster === undefined ? {} : { poster: resolveAsset(block.poster) })}
|
|
191
201
|
/>
|
|
192
202
|
) : (
|
|
193
203
|
<a className="docs-content-ref" href={block.url} target="_blank" rel="noreferrer">
|
|
@@ -210,7 +220,7 @@ function BlockView({ block }: { block: Block }) {
|
|
|
210
220
|
<div className="docs-columns">
|
|
211
221
|
{block.columns.map((c, i) => (
|
|
212
222
|
<div key={i} className="docs-column">
|
|
213
|
-
<Blocks blocks={c.children} />
|
|
223
|
+
<Blocks blocks={c.children} liveRenderer={liveRenderer} />
|
|
214
224
|
</div>
|
|
215
225
|
))}
|
|
216
226
|
</div>
|
|
@@ -229,7 +239,7 @@ function BlockView({ block }: { block: Block }) {
|
|
|
229
239
|
{block.items.map((item, i) => (
|
|
230
240
|
<li key={i}>
|
|
231
241
|
{block.task && <input type="checkbox" checked={!!item.checked} readOnly />}
|
|
232
|
-
<Blocks blocks={item.children} inline />
|
|
242
|
+
<Blocks blocks={item.children} inline liveRenderer={liveRenderer} />
|
|
233
243
|
</li>
|
|
234
244
|
))}
|
|
235
245
|
</Tag>
|
|
@@ -238,7 +248,7 @@ function BlockView({ block }: { block: Block }) {
|
|
|
238
248
|
case "blockquote":
|
|
239
249
|
return (
|
|
240
250
|
<blockquote>
|
|
241
|
-
<Blocks blocks={block.children} />
|
|
251
|
+
<Blocks blocks={block.children} liveRenderer={liveRenderer} />
|
|
242
252
|
</blockquote>
|
|
243
253
|
)
|
|
244
254
|
case "divider":
|
|
@@ -293,7 +303,7 @@ function BlockView({ block }: { block: Block }) {
|
|
|
293
303
|
<div key={i} className="docs-update">
|
|
294
304
|
<div className="docs-update-date">{u.date}</div>
|
|
295
305
|
<div className="docs-update-body">
|
|
296
|
-
<Blocks blocks={u.children} />
|
|
306
|
+
<Blocks blocks={u.children} liveRenderer={liveRenderer} />
|
|
297
307
|
</div>
|
|
298
308
|
</div>
|
|
299
309
|
))}
|
|
@@ -314,11 +324,11 @@ function isReactLanguage(language: string | null): boolean {
|
|
|
314
324
|
return language === "jsx" || language === "tsx" || language === "react"
|
|
315
325
|
}
|
|
316
326
|
|
|
317
|
-
function Blocks({ blocks, inline }: { blocks: Block[]; inline?: boolean }) {
|
|
327
|
+
function Blocks({ blocks, inline, liveRenderer }: { blocks: Block[]; inline?: boolean; liveRenderer?: LivePreviewRenderer }) {
|
|
318
328
|
return (
|
|
319
329
|
<div className={inline ? "docs-blocks-inline" : undefined} style={inline ? { display: "inline" } : undefined}>
|
|
320
330
|
{blocks.map((b, i) => (
|
|
321
|
-
<BlockView key={i} block={b} />
|
|
331
|
+
<BlockView key={i} block={b} liveRenderer={liveRenderer} />
|
|
322
332
|
))}
|
|
323
333
|
</div>
|
|
324
334
|
)
|
|
@@ -326,14 +336,14 @@ function Blocks({ blocks, inline }: { blocks: Block[]; inline?: boolean }) {
|
|
|
326
336
|
|
|
327
337
|
// Renders a markdown string inline — used for embedded markdown like
|
|
328
338
|
// OpenAPI descriptions, which can themselves contain GitBook blocks.
|
|
329
|
-
export function MarkdownContent({ markdown }: { markdown: string }) {
|
|
330
|
-
return <Blocks blocks={parseMarkdown(markdown).children} />
|
|
339
|
+
export function MarkdownContent({ markdown, liveRenderer }: { markdown: string; liveRenderer?: LivePreviewRenderer }) {
|
|
340
|
+
return <Blocks blocks={parseMarkdown(markdown).children} liveRenderer={liveRenderer} />
|
|
331
341
|
}
|
|
332
342
|
|
|
333
|
-
export function DocsRenderer({ doc }: { doc: DocumentNode }) {
|
|
343
|
+
export function DocsRenderer({ doc, liveRenderer }: { doc: DocumentNode; liveRenderer?: LivePreviewRenderer }) {
|
|
334
344
|
return (
|
|
335
345
|
<article className="docs-article">
|
|
336
|
-
<Blocks blocks={doc.children} />
|
|
346
|
+
<Blocks blocks={doc.children} liveRenderer={liveRenderer} />
|
|
337
347
|
</article>
|
|
338
348
|
)
|
|
339
349
|
}
|
package/src/env.d.ts
ADDED
package/src/gitbook/ast.ts
CHANGED
|
@@ -85,6 +85,12 @@ export interface StepperNode {
|
|
|
85
85
|
export interface EmbedNode {
|
|
86
86
|
type: "embed"
|
|
87
87
|
url: string
|
|
88
|
+
title?: string
|
|
89
|
+
autoplay?: boolean
|
|
90
|
+
loop?: boolean
|
|
91
|
+
muted?: boolean
|
|
92
|
+
controls?: boolean
|
|
93
|
+
poster?: string
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
export interface ContentRefNode {
|
package/src/gitbook/parse.ts
CHANGED
|
@@ -66,6 +66,11 @@ function parseAttrs(raw: string | undefined): Record<string, string> {
|
|
|
66
66
|
return attrs
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function booleanAttr(attrs: Record<string, string>, key: string): boolean | undefined {
|
|
70
|
+
if (!(key in attrs)) return undefined
|
|
71
|
+
return attrs[key] !== "false"
|
|
72
|
+
}
|
|
73
|
+
|
|
69
74
|
interface TemplateTag {
|
|
70
75
|
name: string
|
|
71
76
|
attrs: Record<string, string>
|
|
@@ -197,7 +202,24 @@ export function parseBlocks(lines: string[]): Block[] {
|
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
if (tag.name === "embed") {
|
|
200
|
-
blocks.push({
|
|
205
|
+
blocks.push({
|
|
206
|
+
type: "embed",
|
|
207
|
+
url: tag.attrs.url ?? "",
|
|
208
|
+
...(tag.attrs.title ? { title: tag.attrs.title } : {}),
|
|
209
|
+
...(tag.attrs.poster ? { poster: tag.attrs.poster } : {}),
|
|
210
|
+
...(booleanAttr(tag.attrs, "autoplay") === undefined
|
|
211
|
+
? {}
|
|
212
|
+
: { autoplay: booleanAttr(tag.attrs, "autoplay") }),
|
|
213
|
+
...(booleanAttr(tag.attrs, "loop") === undefined
|
|
214
|
+
? {}
|
|
215
|
+
: { loop: booleanAttr(tag.attrs, "loop") }),
|
|
216
|
+
...(booleanAttr(tag.attrs, "muted") === undefined
|
|
217
|
+
? {}
|
|
218
|
+
: { muted: booleanAttr(tag.attrs, "muted") }),
|
|
219
|
+
...(booleanAttr(tag.attrs, "controls") === undefined
|
|
220
|
+
? {}
|
|
221
|
+
: { controls: booleanAttr(tag.attrs, "controls") }),
|
|
222
|
+
})
|
|
201
223
|
i++
|
|
202
224
|
// tolerate optional {% endembed %}
|
|
203
225
|
if (i < lines.length && templateTag(lines[i])?.name === "endembed") i++
|
package/src/gitbook/serialize.ts
CHANGED
|
@@ -60,8 +60,18 @@ function serializeBlock(b: Block): string {
|
|
|
60
60
|
})
|
|
61
61
|
.join("\n\n")}\n{% endstepper %}`
|
|
62
62
|
|
|
63
|
-
case "embed":
|
|
64
|
-
|
|
63
|
+
case "embed": {
|
|
64
|
+
const attrs = [
|
|
65
|
+
` url="${b.url}"`,
|
|
66
|
+
b.title ? ` title="${b.title}"` : "",
|
|
67
|
+
b.poster ? ` poster="${b.poster}"` : "",
|
|
68
|
+
b.autoplay === undefined ? "" : ` autoplay="${b.autoplay}"`,
|
|
69
|
+
b.loop === undefined ? "" : ` loop="${b.loop}"`,
|
|
70
|
+
b.muted === undefined ? "" : ` muted="${b.muted}"`,
|
|
71
|
+
b.controls === undefined ? "" : ` controls="${b.controls}"`,
|
|
72
|
+
].join("")
|
|
73
|
+
return `{% embed${attrs} %}`
|
|
74
|
+
}
|
|
65
75
|
|
|
66
76
|
case "content-ref":
|
|
67
77
|
return `{% content-ref url="${b.url}" %}\n${serializeInline(b.children)}\n{% endcontent-ref %}`
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { GitbookStreamdown } from "./
|
|
2
|
-
export type { GitbookStreamdownProps } from "./
|
|
1
|
+
export { PlaygroundStreamdown as GitbookStreamdown } from "./playground/PlaygroundStreamdown"
|
|
2
|
+
export type { PlaygroundStreamdownProps as GitbookStreamdownProps } from "./playground/PlaygroundStreamdown"
|
|
3
3
|
export { DocsRenderer, MarkdownContent } from "./docs/DocsRenderer"
|
|
4
4
|
export { ReplayEmbed, ReplayPreview } from "./replay"
|
|
5
5
|
export type {
|
|
@@ -12,6 +12,8 @@ export type {
|
|
|
12
12
|
export { isReplayQaUrl, normalizeReplayEmbedUrl } from "./replay"
|
|
13
13
|
export { VideoEmbed } from "./video"
|
|
14
14
|
export type { VideoEmbedProps } from "./video"
|
|
15
|
+
export { VizEmbed } from "./viz"
|
|
16
|
+
export type { VizEmbedProps } from "./viz"
|
|
15
17
|
export { OpenApiOperation } from "./openapi/OpenApiOperation"
|
|
16
18
|
export {
|
|
17
19
|
ReactCodePreview,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useMemo } from "react"
|
|
2
|
+
|
|
3
|
+
import { DocsRenderer, type LivePreviewRenderer } from "../docs/DocsRenderer"
|
|
4
|
+
import { parseMarkdown } from "../gitbook/parse"
|
|
5
|
+
import { ReactCodePreview } from "./ReactCodePreview"
|
|
6
|
+
|
|
7
|
+
export interface PlaygroundStreamdownProps {
|
|
8
|
+
children?: string
|
|
9
|
+
className?: string
|
|
10
|
+
isAnimating?: boolean
|
|
11
|
+
isStreaming?: boolean
|
|
12
|
+
markdown?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The full renderer variant. It keeps the optional almost-node playground
|
|
17
|
+
* behind an explicit import boundary; markdown-only consumers should use the
|
|
18
|
+
* `@brett_lamy/docstream/streamdown` entry instead.
|
|
19
|
+
*/
|
|
20
|
+
export function PlaygroundStreamdown({
|
|
21
|
+
children,
|
|
22
|
+
className,
|
|
23
|
+
isAnimating,
|
|
24
|
+
isStreaming = false,
|
|
25
|
+
markdown,
|
|
26
|
+
}: PlaygroundStreamdownProps) {
|
|
27
|
+
const content = markdown ?? children ?? ""
|
|
28
|
+
const doc = useMemo(() => parseMarkdown(content), [content])
|
|
29
|
+
const liveRenderer: LivePreviewRenderer = ({ files, entry, title }) => (
|
|
30
|
+
<ReactCodePreview files={files} entry={entry} title={title} />
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
aria-busy={isAnimating ?? isStreaming}
|
|
36
|
+
className={className}
|
|
37
|
+
data-docstream=""
|
|
38
|
+
data-streaming={isStreaming ? "" : undefined}
|
|
39
|
+
>
|
|
40
|
+
<DocsRenderer doc={doc} liveRenderer={liveRenderer} />
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -122,6 +122,22 @@
|
|
|
122
122
|
margin-top: 8px;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
.docstream-viz {
|
|
126
|
+
margin: 1.5rem 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.docstream-viz__player {
|
|
130
|
+
overflow: hidden;
|
|
131
|
+
border: 1px solid var(--gb-border);
|
|
132
|
+
border-radius: 12px;
|
|
133
|
+
background: var(--gb-muted);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.docstream-viz figcaption {
|
|
137
|
+
margin-top: 8px;
|
|
138
|
+
color: var(--gb-muted-foreground);
|
|
139
|
+
}
|
|
140
|
+
|
|
125
141
|
.docs-hint {
|
|
126
142
|
display: flex;
|
|
127
143
|
gap: 12px;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PlayerAudio, VideoScene } from "@brett_lamy/viz-engine"
|
|
2
|
+
import { VizPlayer } from "@brett_lamy/viz-engine"
|
|
3
|
+
|
|
4
|
+
export interface VizEmbedProps {
|
|
5
|
+
scene: VideoScene
|
|
6
|
+
audio?: PlayerAudio
|
|
7
|
+
title?: string
|
|
8
|
+
className?: string
|
|
9
|
+
autoplay?: boolean
|
|
10
|
+
loop?: boolean
|
|
11
|
+
showCaptions?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Mount a deterministic VizEngine scene inside a Docstream document. This is
|
|
16
|
+
* intentionally a component API rather than a markdown escape hatch: callers
|
|
17
|
+
* choose the scene and its evidence, while Docstream supplies document chrome
|
|
18
|
+
* and accessible figure semantics.
|
|
19
|
+
*/
|
|
20
|
+
export function VizEmbed({
|
|
21
|
+
scene,
|
|
22
|
+
audio,
|
|
23
|
+
title,
|
|
24
|
+
className,
|
|
25
|
+
autoplay = true,
|
|
26
|
+
loop = true,
|
|
27
|
+
showCaptions = false,
|
|
28
|
+
}: VizEmbedProps) {
|
|
29
|
+
return (
|
|
30
|
+
<figure className={`docstream-viz ${className ?? ""}`.trim()}>
|
|
31
|
+
<VizPlayer
|
|
32
|
+
scene={scene}
|
|
33
|
+
{...(audio === undefined ? {} : { audio })}
|
|
34
|
+
autoplay={autoplay}
|
|
35
|
+
loop={loop}
|
|
36
|
+
showCaptions={showCaptions}
|
|
37
|
+
className="docstream-viz__player"
|
|
38
|
+
/>
|
|
39
|
+
{title && <figcaption>{title}</figcaption>}
|
|
40
|
+
</figure>
|
|
41
|
+
)
|
|
42
|
+
}
|
package/src/viz/index.ts
ADDED