@brett_lamy/docstream 0.3.2 → 0.3.4

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
@@ -41,6 +41,13 @@ If your TypeScript app checks CSS side-effect imports, include Vite's standard e
41
41
 
42
42
  Use `GitbookStreamdown` when markdown may arrive incrementally from an AI stream. The component accepts either `markdown` or string children.
43
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
+
44
51
  ```tsx
45
52
  import { GitbookStreamdown } from "@brett_lamy/docstream"
46
53
  import "@brett_lamy/docstream/styles.css"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brett_lamy/docstream",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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"
@@ -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
- import { ReactCodePreview } from "../playground"
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
- <ReactCodePreview
119
- files={{ [entry]: block.code }}
120
- entry={entry}
121
- title={block.title ?? "Live React preview"}
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
  ))}
@@ -215,7 +220,7 @@ function BlockView({ block }: { block: Block }) {
215
220
  <div className="docs-columns">
216
221
  {block.columns.map((c, i) => (
217
222
  <div key={i} className="docs-column">
218
- <Blocks blocks={c.children} />
223
+ <Blocks blocks={c.children} liveRenderer={liveRenderer} />
219
224
  </div>
220
225
  ))}
221
226
  </div>
@@ -234,7 +239,7 @@ function BlockView({ block }: { block: Block }) {
234
239
  {block.items.map((item, i) => (
235
240
  <li key={i}>
236
241
  {block.task && <input type="checkbox" checked={!!item.checked} readOnly />}
237
- <Blocks blocks={item.children} inline />
242
+ <Blocks blocks={item.children} inline liveRenderer={liveRenderer} />
238
243
  </li>
239
244
  ))}
240
245
  </Tag>
@@ -243,7 +248,7 @@ function BlockView({ block }: { block: Block }) {
243
248
  case "blockquote":
244
249
  return (
245
250
  <blockquote>
246
- <Blocks blocks={block.children} />
251
+ <Blocks blocks={block.children} liveRenderer={liveRenderer} />
247
252
  </blockquote>
248
253
  )
249
254
  case "divider":
@@ -298,7 +303,7 @@ function BlockView({ block }: { block: Block }) {
298
303
  <div key={i} className="docs-update">
299
304
  <div className="docs-update-date">{u.date}</div>
300
305
  <div className="docs-update-body">
301
- <Blocks blocks={u.children} />
306
+ <Blocks blocks={u.children} liveRenderer={liveRenderer} />
302
307
  </div>
303
308
  </div>
304
309
  ))}
@@ -319,11 +324,11 @@ function isReactLanguage(language: string | null): boolean {
319
324
  return language === "jsx" || language === "tsx" || language === "react"
320
325
  }
321
326
 
322
- function Blocks({ blocks, inline }: { blocks: Block[]; inline?: boolean }) {
327
+ function Blocks({ blocks, inline, liveRenderer }: { blocks: Block[]; inline?: boolean; liveRenderer?: LivePreviewRenderer }) {
323
328
  return (
324
329
  <div className={inline ? "docs-blocks-inline" : undefined} style={inline ? { display: "inline" } : undefined}>
325
330
  {blocks.map((b, i) => (
326
- <BlockView key={i} block={b} />
331
+ <BlockView key={i} block={b} liveRenderer={liveRenderer} />
327
332
  ))}
328
333
  </div>
329
334
  )
@@ -331,14 +336,22 @@ function Blocks({ blocks, inline }: { blocks: Block[]; inline?: boolean }) {
331
336
 
332
337
  // Renders a markdown string inline — used for embedded markdown like
333
338
  // OpenAPI descriptions, which can themselves contain GitBook blocks.
334
- export function MarkdownContent({ markdown }: { markdown: string }) {
335
- return <Blocks blocks={parseMarkdown(markdown).children} />
339
+ export function MarkdownContent({
340
+ markdown,
341
+ liveRenderer,
342
+ className,
343
+ }: { markdown: string; liveRenderer?: LivePreviewRenderer; className?: string }) {
344
+ return (
345
+ <div data-docstream="" className={className}>
346
+ <Blocks blocks={parseMarkdown(markdown).children} liveRenderer={liveRenderer} />
347
+ </div>
348
+ )
336
349
  }
337
350
 
338
- export function DocsRenderer({ doc }: { doc: DocumentNode }) {
351
+ export function DocsRenderer({ doc, liveRenderer }: { doc: DocumentNode; liveRenderer?: LivePreviewRenderer }) {
339
352
  return (
340
353
  <article className="docs-article">
341
- <Blocks blocks={doc.children} />
354
+ <Blocks blocks={doc.children} liveRenderer={liveRenderer} />
342
355
  </article>
343
356
  )
344
357
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { GitbookStreamdown } from "./streamdown"
2
- export type { GitbookStreamdownProps } from "./streamdown"
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 {
@@ -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
@@ -2,19 +2,19 @@
2
2
 
3
3
  [data-docstream],
4
4
  .docs-article {
5
- --gb-panel: var(--card);
6
- --gb-panel-foreground: var(--card-foreground);
7
- --gb-muted: var(--muted);
8
- --gb-muted-foreground: var(--muted-foreground);
9
- --gb-border: var(--border);
10
- --gb-accent: var(--accent);
11
- --gb-accent-foreground: var(--accent-foreground);
12
- --gb-primary: var(--primary);
13
- --gb-primary-foreground: var(--primary-foreground);
5
+ --gb-panel: var(--card, #ffffff);
6
+ --gb-panel-foreground: var(--card-foreground, #18181b);
7
+ --gb-muted: var(--muted, #f4f4f5);
8
+ --gb-muted-foreground: var(--muted-foreground, #71717a);
9
+ --gb-border: var(--border, #e4e4e7);
10
+ --gb-accent: var(--accent, #f4f4f5);
11
+ --gb-accent-foreground: var(--accent-foreground, #18181b);
12
+ --gb-primary: var(--primary, #4f46e5);
13
+ --gb-primary-foreground: var(--primary-foreground, #fafafa);
14
14
  /* Plain rgba/hex values (no color-mix()/oklch()) so the theme renders
15
15
  correctly in older Chromium forks; hosts can override any token. */
16
16
  --gb-code-bg: rgba(128, 128, 128, 0.13);
17
- --gb-code-fg: var(--foreground);
17
+ --gb-code-fg: var(--foreground, #18181b);
18
18
 
19
19
  /* Semantic hint palette — solid color plus precomputed tints. Matches the
20
20
  editor palette in @brett_lamy/docstream-editor/styles.css. */
@@ -55,7 +55,7 @@
55
55
  font-weight: 650;
56
56
  margin-top: 1.4em;
57
57
  padding-bottom: 0.2em;
58
- border-bottom: 1px solid var(--border);
58
+ border-bottom: 1px solid var(--border, #e4e4e7);
59
59
  }
60
60
 
61
61
  .docs-article h3 {
@@ -216,8 +216,8 @@
216
216
  }
217
217
 
218
218
  .docs-tabs-header .docs-tab-active {
219
- background: var(--background);
220
- color: var(--foreground);
219
+ background: var(--background, #ffffff);
220
+ color: var(--foreground, #18181b);
221
221
  font-weight: 500;
222
222
  }
223
223
 
@@ -298,7 +298,7 @@
298
298
  border-radius: 12px;
299
299
  font-weight: 500;
300
300
  text-decoration: none !important;
301
- color: var(--foreground) !important;
301
+ color: var(--foreground, #18181b) !important;
302
302
  }
303
303
 
304
304
  .docs-content-ref:hover {
@@ -463,7 +463,7 @@
463
463
  padding: 16px;
464
464
  border: 1px solid var(--gb-border);
465
465
  border-radius: 12px;
466
- background: var(--background);
466
+ background: var(--background, #ffffff);
467
467
  }
468
468
 
469
469
  .docs-mermaid svg {
@@ -560,7 +560,7 @@
560
560
  }
561
561
 
562
562
  .oas-desc {
563
- color: var(--foreground);
563
+ color: var(--foreground, #18181b);
564
564
  margin-bottom: 8px;
565
565
  }
566
566