@brett_lamy/docstream 0.3.0 → 0.3.2

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 CHANGED
@@ -10,11 +10,10 @@ GitBook-aware markdown rendering for React applications and AI streaming surface
10
10
  - Streaming-friendly `GitbookStreamdown` component inspired by `vercel/streamdown`.
11
11
  - Parser and serializer for round-tripping supported GitBook syntax.
12
12
  - Syntax-highlighted code blocks through `lowlight`.
13
- - Optional live React/JSX/TSX previews powered by the almost-node runtime.
14
- - A complete virtual-filesystem helper for multi-file React demos.
15
13
  - GitBook block support for hints, tabs, expandables, steppers, embeds, content refs, columns, figures, tables, math, dividers, updates, and OpenAPI operations.
16
- - Embed blocks render YouTube/direct video URLs through the built-in `VideoEmbed` and public Loop QA task, journey, exploration, or test-run URLs through `ReplayPreview`.
17
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.
18
17
 
19
18
  ## Installation
20
19
 
@@ -70,62 +69,6 @@ export function Preview() {
70
69
  }
71
70
  ```
72
71
 
73
- ## Run React and JSX demos
74
-
75
- Install `@agent-wasm/core` in the host app and serve its service worker through
76
- the Vite plugin:
77
-
78
- ```ts
79
- import { almostnodePlugin } from "@agent-wasm/core/vite"
80
-
81
- export default defineConfig({
82
- plugins: [almostnodePlugin()],
83
- })
84
- ```
85
-
86
- `ReactDemo` creates the complete file tree in almost-node, starts its Vite
87
- server, and renders the resulting app in a preview iframe. The file map can
88
- contain any number of files:
89
-
90
- ```tsx
91
- import { ReactDemo } from "@brett_lamy/docstream"
92
-
93
- const files = {
94
- "/src/main.jsx": `
95
- import React from "react"
96
- import { createRoot } from "react-dom/client"
97
- import App from "./App.jsx"
98
- createRoot(document.getElementById("root")).render(<App />)
99
- `,
100
- "/src/App.jsx": `
101
- import Card from "./Card.jsx"
102
- export default function App() { return <Card title="Hello" /> }
103
- `,
104
- "/src/Card.jsx": `
105
- export default function Card({ title }) { return <button>{title}</button> }
106
- `,
107
- }
108
-
109
- <ReactDemo files={files} entry="/src/main.jsx" />
110
- ```
111
-
112
- For lower-level integrations, `createAlmostNodeFilesystem(files)` creates a
113
- `VirtualFS`, while `createAlmostNodeWorkspace(files)` creates a container and
114
- populates its complete filesystem so callers can run shell commands, npm, or
115
- Vite themselves.
116
-
117
- Single-file live code blocks opt in explicitly so ordinary documentation
118
- snippets remain inert:
119
-
120
- ````md
121
- {% code language="jsx" live="true" entry="/src/main.jsx" %}
122
- ```jsx
123
- import { createRoot } from "react-dom/client"
124
- createRoot(document.getElementById("root")).render(<h1>Hello</h1>)
125
- ```
126
- {% endcode %}
127
- ````
128
-
129
72
  ## Markdown Helper Component
130
73
 
131
74
  `MarkdownContent` parses and renders a markdown string in one step:
@@ -138,25 +81,6 @@ export function Preview({ markdown }: { markdown: string }) {
138
81
  }
139
82
  ```
140
83
 
141
- ## Video and Replay QA embeds
142
-
143
- `embed` blocks accept direct `.mp4`, `.webm`, and `.ogg` assets, YouTube URLs,
144
- and public Loop QA project URLs. Loop QA project URLs are converted to the
145
- chrome-free `/p/...` route automatically:
146
-
147
- ```md
148
- {% embed url="https://loop-qa.replay.io/projects/project-id/journeys/journey-id" /%}
149
- ```
150
-
151
- The replay component is also available directly when a document needs a
152
- custom source or native rrweb events:
153
-
154
- ```tsx
155
- import { ReplayPreview } from "@brett_lamy/docstream"
156
-
157
- <ReplayPreview source="https://loop-qa.replay.io/projects/project-id/tasks/task-id" />
158
- ```
159
-
160
84
  ## GitBook Syntax
161
85
 
162
86
  The parser supports normal Markdown plus GitBook-style block tags.
@@ -217,6 +141,40 @@ Call the API.
217
141
 
218
142
  When a spec cannot be resolved, the renderer displays a fallback asking for an OpenAPI spec URL.
219
143
 
144
+ ### Inline video clips
145
+
146
+ Direct media embeds can opt into browser-safe inline playback. `muted` is
147
+ important when `autoplay` is enabled:
148
+
149
+ ```md
150
+ {% embed url="/generated/example/clips/offsets.mp4"
151
+ title="A reader resumes from its bookmark"
152
+ autoplay="true" loop="true" muted="true" controls="false" %}
153
+ ```
154
+
155
+ The attributes round-trip through `parseMarkdown` and `serializeMarkdown` and
156
+ are rendered as a native `<video playsInline>` element.
157
+
158
+ ### VizEngine scenes
159
+
160
+ Install the optional peer dependency when a document needs a live, seekable
161
+ scene rather than a rendered clip:
162
+
163
+ ```sh
164
+ npm install @brett_lamy/docstream @brett_lamy/viz-engine
165
+ ```
166
+
167
+ ```tsx
168
+ import { VizEmbed } from "@brett_lamy/docstream/viz"
169
+ import "@brett_lamy/docstream/styles.css"
170
+
171
+ <VizEmbed scene={scene} title="The same event, replayed into two projections" />
172
+ ```
173
+
174
+ `VizEmbed` keeps the scene's timeline deterministic and delegates the clock to
175
+ VizEngine, so a reader can pause or scrub the same explanation used to produce
176
+ the short clip.
177
+
220
178
  ## Assets and OpenAPI Specs
221
179
 
222
180
  Relative image and OpenAPI spec paths can be resolved against an asset base:
@@ -236,8 +194,6 @@ You can also resolve paths yourself with `resolveAsset`.
236
194
  - `GitbookStreamdown`: Parses and renders markdown for read-only stream output.
237
195
  - `DocsRenderer`: Renders a parsed `DocumentNode`.
238
196
  - `MarkdownContent`: Parses and renders a markdown string.
239
- - `ReplayPreview` / `ReplayEmbed`: Embeds public Loop QA pages or plays CORS-enabled rrweb event data.
240
- - `VideoEmbed`: Renders direct video assets or hosted video players.
241
197
  - `OpenApiOperation`: Renders a parsed OpenAPI operation block.
242
198
 
243
199
  ### Parser and Serializer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brett_lamy/docstream",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "GitBook-aware readonly markdown and AI stream renderer.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -37,6 +37,10 @@
37
37
  "types": "./src/video/index.ts",
38
38
  "import": "./src/video/index.ts"
39
39
  },
40
+ "./viz": {
41
+ "types": "./src/viz/index.ts",
42
+ "import": "./src/viz/index.ts"
43
+ },
40
44
  "./playground": {
41
45
  "types": "./src/playground/index.ts",
42
46
  "import": "./src/playground/index.ts"
@@ -50,15 +54,21 @@
50
54
  "mermaid": "^11.15.0",
51
55
  "rrweb": "2.0.0-alpha.20",
52
56
  "streamdown": "^2.5.0",
53
- "yaml": "^2.9.0"
57
+ "yaml": "^2.9.0",
58
+ "@brett_lamy/viz-engine": "^0.2.0"
54
59
  },
55
60
  "peerDependencies": {
56
- "react": ">=18",
57
- "@agent-wasm/core": ">=0.4.0"
61
+ "@agent-wasm/core": ">=0.4.0",
62
+ "@brett_lamy/viz-engine": ">=0.2.0",
63
+ "react": ">=18"
64
+ },
65
+ "repository": {
66
+ "type": "git",
67
+ "url": "git+https://github.com/BLamy/docstream.git"
58
68
  },
59
- "peerDependenciesMeta": {
60
- "@agent-wasm/core": {
61
- "optional": true
62
- }
69
+ "devDependencies": {
70
+ "@agent-wasm/core": "^0.4.0",
71
+ "@types/react": "^18.3.3",
72
+ "typescript": "^5.5.4"
63
73
  }
64
74
  }
@@ -187,7 +187,12 @@ function BlockView({ block }: { block: Block }) {
187
187
  <VideoEmbed
188
188
  src={src ?? block.url}
189
189
  className="docs-embed"
190
- title={block.url}
190
+ title={block.title ?? block.url}
191
+ {...(block.autoplay === undefined ? {} : { autoplay: block.autoplay })}
192
+ {...(block.loop === undefined ? {} : { loop: block.loop })}
193
+ {...(block.muted === undefined ? {} : { muted: block.muted })}
194
+ {...(block.controls === undefined ? {} : { controls: block.controls })}
195
+ {...(block.poster === undefined ? {} : { poster: resolveAsset(block.poster) })}
191
196
  />
192
197
  ) : (
193
198
  <a className="docs-content-ref" href={block.url} target="_blank" rel="noreferrer">
package/src/env.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module "*.css" {
2
+ const href: string
3
+ export default href
4
+ }
@@ -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 {
@@ -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({ type: "embed", url: tag.attrs.url ?? "" })
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++
@@ -60,8 +60,18 @@ function serializeBlock(b: Block): string {
60
60
  })
61
61
  .join("\n\n")}\n{% endstepper %}`
62
62
 
63
- case "embed":
64
- return `{% embed url="${b.url}" %}`
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
@@ -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,
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
+ }
@@ -0,0 +1,4 @@
1
+ import "@brett_lamy/viz-engine/styles.css"
2
+
3
+ export { VizEmbed } from "./VizEmbed"
4
+ export type { VizEmbedProps } from "./VizEmbed"