@brett_lamy/docstream-editor 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 CHANGED
@@ -172,3 +172,9 @@ import { GitbookStreamdown } from "@brett_lamy/docstream"
172
172
 
173
173
  <GitbookStreamdown markdown={markdown} isStreaming={false} />
174
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.1",
3
+ "version": "0.3.3",
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
  },
@@ -39,5 +39,18 @@
39
39
  "repository": {
40
40
  "type": "git",
41
41
  "url": "git+https://github.com/BLamy/docstream-editor.git"
42
+ },
43
+ "devDependencies": {
44
+ "@tiptap/core": "^3.26.1",
45
+ "@tiptap/extension-code-block-lowlight": "^3.26.1",
46
+ "@tiptap/extension-list": "^3.26.1",
47
+ "@tiptap/extension-placeholder": "^3.26.1",
48
+ "@tiptap/extension-table": "^3.26.1",
49
+ "@tiptap/pm": "^3.26.1",
50
+ "@tiptap/react": "^3.26.1",
51
+ "@tiptap/starter-kit": "^3.26.1",
52
+ "@tiptap/suggestion": "^3.26.1",
53
+ "react": "^19",
54
+ "react-dom": "^19"
42
55
  }
43
56
  }
@@ -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]" }]
package/src/styles.css CHANGED
@@ -559,9 +559,9 @@
559
559
 
560
560
  .gb-react-demo-editor {
561
561
  overflow: hidden;
562
- border: 1px solid var(--gb-border, var(--border));
562
+ border: 1px solid var(--gb-border, var(--border, #e4e4e7));
563
563
  border-radius: 12px;
564
- background: var(--gb-panel, var(--card));
564
+ background: var(--gb-panel, var(--card, #ffffff));
565
565
  }
566
566
  .gb-react-demo-editor-source {
567
567
  display: grid;
@@ -569,8 +569,8 @@
569
569
  min-height: 260px;
570
570
  }
571
571
  .gb-react-demo-files {
572
- border-right: 1px solid var(--gb-border, var(--border));
573
- background: var(--gb-muted, var(--muted));
572
+ border-right: 1px solid var(--gb-border, var(--border, #e4e4e7));
573
+ background: var(--gb-muted, var(--muted, #f4f4f5));
574
574
  }
575
575
  .gb-react-demo-files-header,
576
576
  .gb-react-demo-editor-toolbar {
@@ -579,19 +579,19 @@
579
579
  gap: 8px;
580
580
  min-height: 40px;
581
581
  padding: 8px 10px;
582
- border-bottom: 1px solid var(--gb-border, var(--border));
582
+ border-bottom: 1px solid var(--gb-border, var(--border, #e4e4e7));
583
583
  font-size: 12px;
584
584
  }
585
585
  .gb-react-demo-files-header svg {
586
586
  margin-left: auto;
587
- color: var(--gb-muted-foreground, var(--muted-foreground));
587
+ color: var(--gb-muted-foreground, var(--muted-foreground, #71717a));
588
588
  }
589
589
  .gb-react-demo-file {
590
590
  display: block;
591
591
  width: 100%;
592
592
  padding: 8px 10px;
593
593
  overflow: hidden;
594
- color: var(--gb-muted-foreground, var(--muted-foreground));
594
+ color: var(--gb-muted-foreground, var(--muted-foreground, #71717a));
595
595
  font-family: ui-monospace, Menlo, monospace;
596
596
  font-size: 11px;
597
597
  text-align: left;
@@ -599,8 +599,8 @@
599
599
  white-space: nowrap;
600
600
  }
601
601
  .gb-react-demo-file-active {
602
- color: var(--gb-panel-foreground, var(--foreground));
603
- background: var(--gb-panel, var(--card));
602
+ color: var(--gb-panel-foreground, var(--foreground, #18181b));
603
+ background: var(--gb-panel, var(--card, #ffffff));
604
604
  }
605
605
  .gb-react-demo-editor-pane {
606
606
  display: flex;
@@ -609,7 +609,7 @@
609
609
  }
610
610
  .gb-react-demo-editor-toolbar code {
611
611
  overflow: hidden;
612
- color: var(--gb-muted-foreground, var(--muted-foreground));
612
+ color: var(--gb-muted-foreground, var(--muted-foreground, #71717a));
613
613
  text-overflow: ellipsis;
614
614
  white-space: nowrap;
615
615
  }
@@ -618,10 +618,10 @@
618
618
  align-items: center;
619
619
  gap: 5px;
620
620
  margin-left: auto;
621
- border: 1px solid var(--gb-border, var(--border));
621
+ border: 1px solid var(--gb-border, var(--border, #e4e4e7));
622
622
  border-radius: 6px;
623
623
  padding: 4px 8px;
624
- color: var(--gb-panel-foreground, var(--foreground));
624
+ color: var(--gb-panel-foreground, var(--foreground, #18181b));
625
625
  font-size: 11px;
626
626
  }
627
627
  .gb-react-demo-preview-button:disabled { opacity: 0.5; }
@@ -633,15 +633,15 @@
633
633
  border: 0;
634
634
  padding: 14px;
635
635
  outline: none;
636
- color: var(--gb-panel-foreground, var(--foreground));
637
- background: var(--gb-panel, var(--card));
636
+ color: var(--gb-panel-foreground, var(--foreground, #18181b));
637
+ background: var(--gb-panel, var(--card, #ffffff));
638
638
  font-family: ui-monospace, Menlo, monospace;
639
639
  font-size: 12px;
640
640
  line-height: 1.6;
641
641
  }
642
642
  .gb-react-demo-editor-empty {
643
643
  padding: 18px;
644
- color: var(--gb-muted-foreground, var(--muted-foreground));
644
+ color: var(--gb-muted-foreground, var(--muted-foreground, #71717a));
645
645
  font-size: 12px;
646
646
  text-align: center;
647
647
  }
@@ -651,7 +651,7 @@
651
651
  display: flex;
652
652
  overflow-x: auto;
653
653
  border-right: 0;
654
- border-bottom: 1px solid var(--gb-border, var(--border));
654
+ border-bottom: 1px solid var(--gb-border, var(--border, #e4e4e7));
655
655
  }
656
656
  .gb-react-demo-files-header { flex: 0 0 auto; border-bottom: 0; }
657
657
  .gb-react-demo-file { width: auto; flex: 0 0 auto; }