@brett_lamy/docstream 0.3.2 → 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
@@ -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.3",
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,14 @@ 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({ markdown, liveRenderer }: { markdown: string; liveRenderer?: LivePreviewRenderer }) {
340
+ return <Blocks blocks={parseMarkdown(markdown).children} liveRenderer={liveRenderer} />
336
341
  }
337
342
 
338
- export function DocsRenderer({ doc }: { doc: DocumentNode }) {
343
+ export function DocsRenderer({ doc, liveRenderer }: { doc: DocumentNode; liveRenderer?: LivePreviewRenderer }) {
339
344
  return (
340
345
  <article className="docs-article">
341
- <Blocks blocks={doc.children} />
346
+ <Blocks blocks={doc.children} liveRenderer={liveRenderer} />
342
347
  </article>
343
348
  )
344
349
  }
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
+ }