@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 +6 -0
- package/package.json +15 -2
- package/src/editor/convert.ts +23 -4
- package/src/editor/nodes.tsx +29 -2
- package/src/styles.css +16 -16
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.
|
|
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.
|
|
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
|
}
|
package/src/editor/convert.ts
CHANGED
|
@@ -98,8 +98,16 @@ function blockToPM(b: Block): PMNode {
|
|
|
98
98
|
content: blocksPM(s.children),
|
|
99
99
|
})),
|
|
100
100
|
}
|
|
101
|
-
case "embed":
|
|
102
|
-
|
|
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
|
-
|
|
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",
|
package/src/editor/nodes.tsx
CHANGED
|
@@ -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
|
|
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 {
|
|
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; }
|