@brett_lamy/docstream-editor 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
@@ -11,8 +11,6 @@ TipTap editor components for Docstream GitBook-style markdown documents.
11
11
  - Conversion helpers between the Docstream AST and TipTap JSON.
12
12
  - Slash menu for inserting supported blocks.
13
13
  - Code block support through `lowlight`.
14
- - Live React/JSX/TSX code blocks backed by almost-node.
15
- - `ReactDemoEditor` for editing and running a complete multi-file project.
16
14
  - Table, task list, heading, quote, and inline formatting support.
17
15
  - Preserves Docstream/GitBook block semantics when converting back to markdown.
18
16
 
@@ -102,53 +100,9 @@ The editor supports common ProseMirror/TipTap content plus GitBook-flavored bloc
102
100
  - Columns
103
101
  - Figures and images
104
102
  - OpenAPI operation placeholders
105
- - Video and public Replay QA previews inside embed blocks
106
103
 
107
104
  Some blocks are intentionally represented as structured nodes rather than fully bespoke editing controls. They are preserved through parse, edit, and serialize flows so the document can continue to round-trip as GitBook-style markdown.
108
105
 
109
- ### Video and Replay QA embeds
110
-
111
- Insert an embed block from the slash menu, then enter a direct video URL or a
112
- public Loop QA project URL. The editor renders video assets through Docstream's
113
- `VideoEmbed` and converts Loop QA project paths to their chrome-free public
114
- preview route. The serialized markdown remains portable:
115
-
116
- ```md
117
- {% embed url="https://loop-qa.replay.io/projects/project-id/tasks/task-id" /%}
118
- ```
119
-
120
- ### Live React code
121
-
122
- Code blocks can opt into a live preview by setting `live="true"`. The block
123
- must be a complete entry module (including its `createRoot` call); ordinary
124
- code snippets remain read-only:
125
-
126
- ````md
127
- {% code language="jsx" live="true" entry="/src/main.jsx" %}
128
- ```jsx
129
- import { createRoot } from "react-dom/client"
130
- createRoot(document.getElementById("root")).render(<h1>Hello</h1>)
131
- ```
132
- {% endcode %}
133
- ````
134
-
135
- For multi-file demos, use the standalone editor component:
136
-
137
- ```tsx
138
- import { ReactDemoEditor } from "@brett_lamy/docstream-editor"
139
-
140
- <ReactDemoEditor
141
- files={{
142
- "/src/main.jsx": mainSource,
143
- "/src/App.jsx": appSource,
144
- }}
145
- entry="/src/main.jsx"
146
- />
147
- ```
148
-
149
- The host app must install `@agent-wasm/core` and expose its service worker with
150
- `almostnodePlugin()` from `@agent-wasm/core/vite`.
151
-
152
106
  ## Slash Menu
153
107
 
154
108
  The editor includes a slash menu extension for inserting supported block structures. Type `/` in an empty paragraph to open block insertion options.
@@ -218,3 +172,9 @@ import { GitbookStreamdown } from "@brett_lamy/docstream"
218
172
 
219
173
  <GitbookStreamdown markdown={markdown} isStreaming={false} />
220
174
  ```
175
+
176
+ Embed nodes preserve the Docstream media attributes (`title`, `poster`,
177
+ `autoplay`, `loop`, `muted`, and `controls`) when converting between the
178
+ editable TipTap document and GitBook markdown. This makes short, muted,
179
+ autoplaying clips round-trip through the editor without losing their playback
180
+ contract.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brett_lamy/docstream-editor",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "TipTap editor for Docstream GitBook-style markdown documents.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -20,7 +20,7 @@
20
20
  "./styles.css": "./src/styles.css"
21
21
  },
22
22
  "dependencies": {
23
- "@brett_lamy/docstream": "0.3.0",
23
+ "@brett_lamy/docstream": "0.3.2",
24
24
  "lowlight": "^3.3.0",
25
25
  "lucide-react": "^1.17.0"
26
26
  },
@@ -35,5 +35,9 @@
35
35
  "@tiptap/starter-kit": "^3.26.1",
36
36
  "@tiptap/suggestion": "^3.26.1",
37
37
  "react": ">=18"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/BLamy/docstream-editor.git"
38
42
  }
39
43
  }
@@ -98,8 +98,16 @@ function blockToPM(b: Block): PMNode {
98
98
  content: blocksPM(s.children),
99
99
  })),
100
100
  }
101
- case "embed":
102
- return { type: "gbEmbed", attrs: { url: b.url } }
101
+ case "embed": {
102
+ const attrs: Record<string, string | boolean> = { url: b.url }
103
+ if (b.title !== undefined) attrs.title = b.title
104
+ if (b.poster !== undefined) attrs.poster = b.poster
105
+ if (b.autoplay !== undefined) attrs.autoplay = b.autoplay
106
+ if (b.loop !== undefined) attrs.loop = b.loop
107
+ if (b.muted !== undefined) attrs.muted = b.muted
108
+ if (b.controls !== undefined) attrs.controls = b.controls
109
+ return { type: "gbEmbed", attrs }
110
+ }
103
111
  case "content-ref":
104
112
  return { type: "gbContentRef", attrs: { url: b.url, label: plainText(b.children) } }
105
113
  case "columns":
@@ -255,8 +263,19 @@ function pmToBlock(n: PMNode): Block | null {
255
263
  children: pmToBlocks(s.content),
256
264
  })),
257
265
  }
258
- case "gbEmbed":
259
- return { type: "embed", url: String(n.attrs?.url ?? "") }
266
+ case "gbEmbed": {
267
+ const embed: Extract<Block, { type: "embed" }> = {
268
+ type: "embed",
269
+ url: String(n.attrs?.url ?? ""),
270
+ }
271
+ if (typeof n.attrs?.title === "string") embed.title = n.attrs.title
272
+ if (typeof n.attrs?.poster === "string") embed.poster = n.attrs.poster
273
+ if (typeof n.attrs?.autoplay === "boolean") embed.autoplay = n.attrs.autoplay
274
+ if (typeof n.attrs?.loop === "boolean") embed.loop = n.attrs.loop
275
+ if (typeof n.attrs?.muted === "boolean") embed.muted = n.attrs.muted
276
+ if (typeof n.attrs?.controls === "boolean") embed.controls = n.attrs.controls
277
+ return embed
278
+ }
260
279
  case "gbContentRef":
261
280
  return {
262
281
  type: "content-ref",
@@ -306,15 +306,34 @@ function isDirectVideo(url: string): boolean {
306
306
  return /\.(?:mp4|webm|ogg)(?:[?#]|$)/i.test(url)
307
307
  }
308
308
 
309
+ function booleanAttribute(value: unknown): boolean | undefined {
310
+ return typeof value === "boolean" ? value : undefined
311
+ }
312
+
309
313
  function EmbedView({ node, updateAttributes, editor }: NodeViewProps) {
310
314
  const url = node.attrs.url as string
311
315
  const preview = embedPreview(url)
316
+ const title = typeof node.attrs.title === "string" && node.attrs.title ? node.attrs.title : url
317
+ const poster = typeof node.attrs.poster === "string" ? node.attrs.poster : undefined
318
+ const autoplay = booleanAttribute(node.attrs.autoplay)
319
+ const loop = booleanAttribute(node.attrs.loop)
320
+ const muted = booleanAttribute(node.attrs.muted)
321
+ const controls = booleanAttribute(node.attrs.controls)
312
322
  return (
313
323
  <NodeViewWrapper className="gb-embed" contentEditable={false}>
314
324
  {isReplayQaUrl(url) ? (
315
325
  <ReplayPreview source={url} title="Replay preview" />
316
326
  ) : preview || isDirectVideo(url) ? (
317
- <VideoEmbed src={preview ?? url} className="gb-embed-frame" title={url} />
327
+ <VideoEmbed
328
+ src={preview ?? url}
329
+ className="gb-embed-frame"
330
+ title={title}
331
+ {...(autoplay === undefined ? {} : { autoplay })}
332
+ {...(loop === undefined ? {} : { loop })}
333
+ {...(muted === undefined ? {} : { muted })}
334
+ {...(controls === undefined ? {} : { controls })}
335
+ {...(poster === undefined ? {} : { poster })}
336
+ />
318
337
  ) : (
319
338
  <a className="gb-embed-link" href={url} target="_blank" rel="noreferrer">
320
339
  <Link2 className="size-4" /> {url || "Embed URL…"}
@@ -337,7 +356,15 @@ export const GbEmbed = Node.create({
337
356
  group: "block",
338
357
  atom: true,
339
358
  addAttributes() {
340
- return { url: { default: "" } }
359
+ return {
360
+ url: { default: "" },
361
+ title: { default: null },
362
+ autoplay: { default: null },
363
+ loop: { default: null },
364
+ muted: { default: null },
365
+ controls: { default: null },
366
+ poster: { default: null },
367
+ }
341
368
  },
342
369
  parseHTML() {
343
370
  return [{ tag: "div[data-gb-embed]" }]