@brett_lamy/docstream 1.1.1 → 1.2.0

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/src/index.ts CHANGED
@@ -13,25 +13,35 @@ export {
13
13
  DemoFullscreen,
14
14
  DemoViewer,
15
15
  DocstreamDemoContext,
16
+ DocstreamDemoRuntimeContext,
16
17
  createGlobDemoResolver,
17
18
  defaultDemoMarkdown,
19
+ inlineDemoMarkdown,
20
+ inlineDemoNode,
18
21
  demoComponentFrom,
19
22
  demoFromSearch,
20
23
  demoHref,
24
+ preloadDemos,
21
25
  resolveDemosToMarkdown,
22
26
  sortDemoFiles,
27
+ toInlineFiles,
23
28
  useDemoResolver,
29
+ useDemoRuntime,
24
30
  } from "./demo"
25
31
  export type {
26
32
  DemoComponent,
27
33
  DemoComponentProps,
28
34
  DemoFile,
29
35
  DemoFullscreenProps,
36
+ DemoMarkdownFormat,
30
37
  DemoMeta,
31
38
  DemoResolver,
39
+ DemoRuntimeOptions,
32
40
  DemoVariant,
33
41
  DemoViewerProps,
34
42
  GlobDemoResolverOptions,
43
+ InlineDemoRuntime,
44
+ InlineDemoRuntimeProps,
35
45
  ResolveDemosOptions,
36
46
  ResolvedDemo,
37
47
  } from "./demo"
@@ -52,8 +62,12 @@ export { VizEmbed } from "./viz"
52
62
  export type { VizEmbedProps } from "./viz"
53
63
  export { OpenApiOperation } from "./openapi/OpenApiOperation"
54
64
  export {
65
+ InlineDemoPreview,
55
66
  ReactCodePreview,
56
67
  ReactDemo,
68
+ almostNodeDemoRuntime,
69
+ createAlmostNodeDemoRuntime,
70
+ createInlineDemoProject,
57
71
  createReactDemoFiles,
58
72
  createAlmostNodeFilesystem,
59
73
  createAlmostNodeWorkspace,
@@ -65,6 +79,10 @@ export type {
65
79
  AlmostNodeFilesystemOptions,
66
80
  AlmostNodeWorkspace,
67
81
  AlmostNodeWorkspaceOptions,
82
+ AlmostNodeDemoRuntimeOptions,
83
+ InlineDemoPreviewProps,
84
+ InlineDemoProject,
85
+ InlineDemoProjectOptions,
68
86
  ReactCodePreviewProps,
69
87
  ReactDemoProps,
70
88
  } from "./playground"
@@ -79,7 +97,7 @@ export type {
79
97
  SourceReferenceClient,
80
98
  } from "./source"
81
99
  export { parseDemoVariants, parseMarkdown, parseBlocks, trimPartialInlineToken } from "./gitbook/parse"
82
- export { serializeBlocks, serializeMarkdown } from "./gitbook/serialize"
100
+ export { fenceFor, serializeBlocks, serializeDemoFile, serializeMarkdown } from "./gitbook/serialize"
83
101
  export {
84
102
  footnoteDefinitions,
85
103
  parseInline,
@@ -91,3 +109,7 @@ export {
91
109
  export { Streamdown } from "streamdown"
92
110
  export type { StreamdownProps } from "streamdown"
93
111
  export type * from "./gitbook/ast"
112
+ export { PACKAGE_MANAGERS, packageManagerCommands } from "./gitbook/package-managers"
113
+ export { flattenBlocks, flattenForPlainMarkdown } from "./gitbook/flatten"
114
+ export { documentOutline, slugify } from "./gitbook/outline"
115
+ export type { OutlineEntry } from "./gitbook/outline"
@@ -0,0 +1,77 @@
1
+ /* Runs a `{% demo %}` block's inline files in almost-node — the Preview of a pasted,
2
+ self-contained demo when no resolver knows it. Just the iframe: the DemoViewer around it
3
+ supplies the header, variants, viewports, Code view and fullscreen. */
4
+ import { useMemo, type ReactNode } from "react"
5
+
6
+ import type { DemoFile, InlineDemoRuntime, InlineDemoRuntimeProps } from "../demo/types"
7
+ import { createInlineDemoProject, type AlmostNodeWorkspaceOptions } from "./filesystem"
8
+ import { useAlmostNodeServer } from "./ReactCodePreview"
9
+
10
+ export interface InlineDemoPreviewProps extends InlineDemoRuntimeProps {
11
+ /** Preferred virtual port. almost-node picks the next free one if taken. */
12
+ port?: number
13
+ /** iframe sandbox value (same-origin lets the almost-node service worker route it). */
14
+ sandbox?: string
15
+ /** Extra options for `createAlmostNodeWorkspace` (e.g. `basePath` for static hosting). */
16
+ workspaceOptions?: Partial<AlmostNodeWorkspaceOptions>
17
+ }
18
+
19
+ function filesKey(files: DemoFile[], entry: string, dependencies: Record<string, string> | undefined) {
20
+ return JSON.stringify([entry, dependencies ?? null, files.map((f) => [f.path, f.content])])
21
+ }
22
+
23
+ export function InlineDemoPreview({
24
+ title,
25
+ files,
26
+ entry,
27
+ variant,
28
+ dependencies,
29
+ port = 4173,
30
+ sandbox = "allow-scripts allow-same-origin allow-forms allow-modals",
31
+ workspaceOptions,
32
+ }: InlineDemoPreviewProps) {
33
+ // Content-keyed so re-renders (variant switches, parent state) never restart Vite.
34
+ const key = filesKey(files, entry, dependencies)
35
+ const project = useMemo(
36
+ () => {
37
+ try {
38
+ return { value: createInlineDemoProject(files, { entry, dependencies }) }
39
+ } catch (error) {
40
+ return { error: error instanceof Error ? error.message : String(error) }
41
+ }
42
+ },
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
+ [key],
45
+ )
46
+ const { state, url, error } = useAlmostNodeServer({
47
+ files: project.value?.files ?? {},
48
+ port,
49
+ run: project.value ? 1 : 0,
50
+ workspaceOptions,
51
+ })
52
+
53
+ const message = project.error ?? error
54
+ if (message) return <pre className="docs-demo-error" role="alert">{message}</pre>
55
+ if (state !== "ready" || !url) {
56
+ return <div className="docs-demo-runtime-status" role="status">Starting almost-node…</div>
57
+ }
58
+ const src = project.value?.wrapped && variant !== undefined ? `${url}?variant=${encodeURIComponent(variant)}` : url
59
+ return <iframe key={src} title={title} className="docs-demo-runtime-frame" src={src} sandbox={sandbox} />
60
+ }
61
+
62
+ export interface AlmostNodeDemoRuntimeOptions {
63
+ port?: number
64
+ sandbox?: string
65
+ workspaceOptions?: Partial<AlmostNodeWorkspaceOptions>
66
+ }
67
+
68
+ /**
69
+ * An `InlineDemoRuntime` backed by almost-node (`@agent-wasm/core`, loaded lazily on first
70
+ * use). Pass it as `demoRuntime`; `PlaygroundStreamdown` uses one with default options.
71
+ */
72
+ export function createAlmostNodeDemoRuntime(options: AlmostNodeDemoRuntimeOptions = {}): InlineDemoRuntime {
73
+ return (props): ReactNode => <InlineDemoPreview {...props} {...options} />
74
+ }
75
+
76
+ /** The default almost-node runtime (no base path, port 4173+). */
77
+ export const almostNodeDemoRuntime: InlineDemoRuntime = createAlmostNodeDemoRuntime()
@@ -1,7 +1,9 @@
1
- import { useMemo } from "react"
1
+ import { useContext, useMemo } from "react"
2
2
 
3
3
  import { DocsRenderer, type DocRenderOptions, type LivePreviewRenderer, type SourceReferenceRenderer } from "../docs/DocsRenderer"
4
4
  import { parseMarkdown, trimPartialInlineToken } from "../gitbook/parse"
5
+ import { DocstreamDemoRuntimeContext } from "../demo/context"
6
+ import { almostNodeDemoRuntime } from "./InlineDemoPreview"
5
7
  import { ReactCodePreview } from "./ReactCodePreview"
6
8
 
7
9
  export interface PlaygroundStreamdownProps extends DocRenderOptions {
@@ -16,7 +18,8 @@ export interface PlaygroundStreamdownProps extends DocRenderOptions {
16
18
  /**
17
19
  * The full renderer variant. It keeps the optional almost-node playground
18
20
  * behind an explicit import boundary; markdown-only consumers should use the
19
- * `@brett_lamy/docstream/streamdown` entry instead.
21
+ * `@brett_lamy/docstream/streamdown` entry instead. Live code fences and
22
+ * `{% demo %}` blocks with inline files (and no resolver) run in almost-node.
20
23
  */
21
24
  export function PlaygroundStreamdown({
22
25
  children,
@@ -26,9 +29,14 @@ export function PlaygroundStreamdown({
26
29
  markdown,
27
30
  sourceRenderer,
28
31
  demoResolver,
32
+ demoRuntime,
33
+ demoDependencies,
29
34
  pageActions,
30
35
  }: PlaygroundStreamdownProps) {
31
36
  const content = markdown ?? children ?? ""
37
+ // Inline-file demos run in almost-node unless the host (or an outer renderer) chose a runtime.
38
+ const inheritedRuntime = useContext(DocstreamDemoRuntimeContext).runtime
39
+ const runtime = demoRuntime ?? inheritedRuntime ?? almostNodeDemoRuntime
32
40
  const doc = useMemo(
33
41
  () => parseMarkdown(isStreaming ? trimPartialInlineToken(content) : content),
34
42
  [content, isStreaming]
@@ -56,6 +64,8 @@ export function PlaygroundStreamdown({
56
64
  liveRenderer={liveRenderer}
57
65
  sourceRenderer={sourceRenderer}
58
66
  demoResolver={demoResolver}
67
+ demoRuntime={runtime}
68
+ demoDependencies={demoDependencies}
59
69
  pageActions={isStreaming ? false : pageActions}
60
70
  />
61
71
  </div>
@@ -53,7 +53,7 @@ export interface ReactDemoProps {
53
53
 
54
54
  export type ReactCodePreviewProps = ReactDemoProps
55
55
 
56
- type PreviewState = "idle" | "starting" | "ready" | "error"
56
+ export type PreviewState = "idle" | "starting" | "ready" | "error"
57
57
 
58
58
  const DETACHED_SERVER_ENV = { ALMOSTNODE_DETACH_DEV_SERVERS: "1" }
59
59
 
@@ -79,43 +79,30 @@ function sourceText(files: AlmostNodeFiles, requestedPath: string): string | nul
79
79
  return typeof file.content === "string" ? file.content : new TextDecoder().decode(file.content)
80
80
  }
81
81
 
82
- /** Render a multi-file React/JSX/TSX project through almost-node's Vite server. */
83
- const NO_FILES: AlmostNodeFiles = {}
82
+ export interface AlmostNodeServerOptions {
83
+ /** The complete project (see `createReactDemoFiles`). Keep its identity stable between renders. */
84
+ files: AlmostNodeFiles
85
+ port?: number
86
+ /** Start when > 0; bump to restart. 0 stops (and disposes) the server. */
87
+ run: number
88
+ workspaceOptions?: Partial<AlmostNodeWorkspaceOptions>
89
+ onReady?: (url: string) => void
90
+ onError?: (error: Error) => void
91
+ }
84
92
 
85
- export function ReactDemo({
86
- files = NO_FILES,
87
- preview,
88
- code: codeProp,
89
- language,
90
- status,
91
- actions,
92
- entry = "/src/main.jsx",
93
- port = 4173,
94
- height = 360,
95
- codeFile,
96
- collapsedCodeLines = 3,
97
- expandedCodeLines = 30,
98
- title = "Live React preview",
99
- autoStart = true,
100
- sandbox = "allow-scripts allow-same-origin allow-forms allow-modals",
101
- workspaceOptions,
102
- className,
103
- onReady,
104
- onError,
105
- }: ReactDemoProps) {
106
- const inline = preview !== undefined
107
- const [run, setRun] = useState(autoStart && !inline ? 1 : 0)
108
- const [state, setState] = useState<PreviewState>(inline ? "ready" : autoStart ? "starting" : "idle")
93
+ export interface AlmostNodeServerState {
94
+ state: PreviewState
95
+ url: string | null
96
+ error: string | null
97
+ }
98
+
99
+ /** Boots `vite` for `files` in an almost-node container; disposes it on change/unmount. */
100
+ export function useAlmostNodeServer({ files, port = 4173, run, workspaceOptions, onReady, onError }: AlmostNodeServerOptions): AlmostNodeServerState {
101
+ const [state, setState] = useState<PreviewState>(run ? "starting" : "idle")
109
102
  const [url, setUrl] = useState<string | null>(null)
110
103
  const [error, setError] = useState<string | null>(null)
111
- const displayedCodeFile = codeFile ?? entry
112
- const code = useMemo(
113
- () => codeProp ?? sourceText(files, displayedCodeFile),
114
- [codeProp, displayedCodeFile, files],
115
- )
116
104
 
117
105
  useEffect(() => {
118
- if (inline) return
119
106
  if (!run) {
120
107
  setState("idle")
121
108
  setUrl(null)
@@ -132,8 +119,7 @@ export function ReactDemo({
132
119
 
133
120
  const start = async () => {
134
121
  try {
135
- const projectFiles = createReactDemoFiles(files, { entry })
136
- workspace = await createAlmostNodeWorkspace(projectFiles, {
122
+ workspace = await createAlmostNodeWorkspace(files, {
137
123
  ...workspaceOptions,
138
124
  env: { ...DETACHED_SERVER_ENV, ...workspaceOptions?.env },
139
125
  onServerReady: (serverPort, serverUrl) => {
@@ -173,7 +159,52 @@ export function ReactDemo({
173
159
  cancelled = true
174
160
  workspace?.dispose()
175
161
  }
176
- }, [entry, files, inline, onError, onReady, port, run, workspaceOptions])
162
+ }, [files, onError, onReady, port, run, workspaceOptions])
163
+
164
+ return { state, url, error }
165
+ }
166
+
167
+ /** Render a multi-file React/JSX/TSX project through almost-node's Vite server. */
168
+ const NO_FILES: AlmostNodeFiles = {}
169
+
170
+ export function ReactDemo({
171
+ files = NO_FILES,
172
+ preview,
173
+ code: codeProp,
174
+ language,
175
+ status,
176
+ actions,
177
+ entry = "/src/main.jsx",
178
+ port = 4173,
179
+ height = 360,
180
+ codeFile,
181
+ collapsedCodeLines = 3,
182
+ expandedCodeLines = 30,
183
+ title = "Live React preview",
184
+ autoStart = true,
185
+ sandbox = "allow-scripts allow-same-origin allow-forms allow-modals",
186
+ workspaceOptions,
187
+ className,
188
+ onReady,
189
+ onError,
190
+ }: ReactDemoProps) {
191
+ const inline = preview !== undefined
192
+ const [run, setRun] = useState(autoStart && !inline ? 1 : 0)
193
+ const projectFiles = useMemo(() => createReactDemoFiles(files, { entry }), [files, entry])
194
+ const { state: serverState, url, error } = useAlmostNodeServer({
195
+ files: projectFiles,
196
+ port,
197
+ run: inline ? 0 : run,
198
+ workspaceOptions,
199
+ onReady,
200
+ onError,
201
+ })
202
+ const state: PreviewState = inline ? "ready" : serverState
203
+ const displayedCodeFile = codeFile ?? entry
204
+ const code = useMemo(
205
+ () => codeProp ?? sourceText(files, displayedCodeFile),
206
+ [codeProp, displayedCodeFile, files],
207
+ )
177
208
 
178
209
  const wrapperClass = className ? `docs-react-demo ${className}` : "docs-react-demo"
179
210
  const running = state === "starting"
@@ -154,6 +154,8 @@ export async function createAlmostNodeWorkspace(
154
154
 
155
155
  export interface ReactDemoFilesOptions {
156
156
  entry?: string
157
+ /** Extra npm dependencies for the generated package.json (ignored when the files bring one). */
158
+ dependencies?: Record<string, string>
157
159
  }
158
160
 
159
161
  /**
@@ -174,7 +176,7 @@ export function createReactDemoFiles(
174
176
  name: "docstream-react-demo",
175
177
  private: true,
176
178
  type: "module",
177
- dependencies: { react: "^19.0.0", "react-dom": "^19.0.0" },
179
+ dependencies: { react: "^19.0.0", "react-dom": "^19.0.0", ...options.dependencies },
178
180
  },
179
181
  null,
180
182
  2,
@@ -200,3 +202,83 @@ export function createReactDemoFiles(
200
202
 
201
203
  return map
202
204
  }
205
+
206
+ export interface InlineDemoProjectOptions {
207
+ /** Entry file among `files` (relative path). Defaults to the first file. */
208
+ entry?: string
209
+ /** Extra npm dependencies (merged over a `package.json` among the files, if any). */
210
+ dependencies?: Record<string, string>
211
+ }
212
+
213
+ export interface InlineDemoProject {
214
+ /** Complete almost-node project: the demo under `/src/demo/`, a wrapper entry, package.json, index.html. */
215
+ files: Record<string, AlmostNodeFileContent>
216
+ /** The Vite entry to hand to `createReactDemoFiles` / `ReactDemo`. */
217
+ entry: string
218
+ /** Whether the entry is wrapped (so `?variant=` reaches the component as its `variant` prop). */
219
+ wrapped: boolean
220
+ }
221
+
222
+ /** Folder the inline demo's files are written to inside the almost-node project. */
223
+ export const INLINE_DEMO_DIR = "/src/demo"
224
+ const WRAPPER_ENTRY = "/src/__docstream_demo_entry.js"
225
+
226
+ function selfMounting(source: string): boolean {
227
+ return /\bcreateRoot\s*\(|\bReactDOM\.render\s*\(|\bhydrateRoot\s*\(/.test(source)
228
+ }
229
+
230
+ /**
231
+ * Turn a `{% demo %}` block's inline files into a runnable almost-node project.
232
+ *
233
+ * The files go under `/src/demo/` (relative imports between them keep working). A tiny
234
+ * wrapper entry imports the entry file, takes its default export (or the first exported
235
+ * function), and renders it with `variant` read from the page's `?variant=` query — so
236
+ * switching variants just reloads the iframe, no new Vite server. An entry that mounts
237
+ * itself (`createRoot(…)`) is used directly instead, and receives no `variant`.
238
+ *
239
+ * A `package.json` among the files contributes its `dependencies`; `options.dependencies`
240
+ * are merged over them. Bare imports resolve through almost-node (installed packages, else
241
+ * esm.sh at the major version declared here), with `react`/`react-dom` provided.
242
+ */
243
+ export function createInlineDemoProject(
244
+ files: readonly { path: string; content: string }[],
245
+ options: InlineDemoProjectOptions = {},
246
+ ): InlineDemoProject {
247
+ const map: Record<string, AlmostNodeFileContent> = {}
248
+ let packageDeps: Record<string, string> = {}
249
+ for (const file of files) {
250
+ const relative = file.path.replace(/^\.?\/+/, "")
251
+ if (relative === "package.json") {
252
+ try {
253
+ const pkg = JSON.parse(file.content) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
254
+ packageDeps = { ...pkg.devDependencies, ...pkg.dependencies }
255
+ } catch {
256
+ // Not JSON: keep it as a plain file.
257
+ }
258
+ }
259
+ map[normalizeAlmostNodePath(relative, INLINE_DEMO_DIR)] = file.content
260
+ }
261
+ const entryFile = files.find((f) => f.path === options.entry) ?? files[0]
262
+ if (!entryFile) throw new Error("An inline demo needs at least one file")
263
+ const entryPath = normalizeAlmostNodePath(entryFile.path.replace(/^\.?\/+/, ""), INLINE_DEMO_DIR)
264
+ const dependencies = { ...packageDeps, ...options.dependencies }
265
+ const wrapped = !selfMounting(entryFile.content)
266
+ if (wrapped) {
267
+ const specifier = `.${entryPath.slice("/src".length)}`
268
+ map[WRAPPER_ENTRY] = `import { createElement } from "react"
269
+ import { createRoot } from "react-dom/client"
270
+ import * as demo from ${JSON.stringify(specifier)}
271
+
272
+ const Component = demo.default ?? Object.values(demo).find((value) => typeof value === "function")
273
+ const variant = new URLSearchParams(location.search).get("variant")
274
+ const props = variant === null ? {} : { variant }
275
+ createRoot(document.getElementById("root")).render(
276
+ Component
277
+ ? createElement(Component, props)
278
+ : createElement("pre", null, ${JSON.stringify(`${entryFile.path} has no default export to render.`)}),
279
+ )
280
+ `
281
+ }
282
+ const entry = wrapped ? WRAPPER_ENTRY : entryPath
283
+ return { files: createReactDemoFiles(map, { entry, dependencies }), entry, wrapped }
284
+ }
@@ -1,8 +1,11 @@
1
- export { ReactCodePreview, ReactDemo } from "./ReactCodePreview"
2
- export type { ReactCodePreviewProps, ReactDemoProps } from "./ReactCodePreview"
1
+ export { ReactCodePreview, ReactDemo, useAlmostNodeServer } from "./ReactCodePreview"
2
+ export type { AlmostNodeServerOptions, AlmostNodeServerState, PreviewState, ReactCodePreviewProps, ReactDemoProps } from "./ReactCodePreview"
3
+ export { InlineDemoPreview, almostNodeDemoRuntime, createAlmostNodeDemoRuntime } from "./InlineDemoPreview"
4
+ export type { AlmostNodeDemoRuntimeOptions, InlineDemoPreviewProps } from "./InlineDemoPreview"
3
5
  export {
4
6
  createAlmostNodeFilesystem,
5
7
  createAlmostNodeWorkspace,
8
+ createInlineDemoProject,
6
9
  createReactDemoFiles,
7
10
  normalizeAlmostNodeFiles,
8
11
  normalizeAlmostNodePath,
@@ -15,5 +18,7 @@ export type {
15
18
  AlmostNodeFilesystemOptions,
16
19
  AlmostNodeWorkspace,
17
20
  AlmostNodeWorkspaceOptions,
21
+ InlineDemoProject,
22
+ InlineDemoProjectOptions,
18
23
  ReactDemoFilesOptions,
19
24
  } from "./filesystem"
@@ -8,8 +8,8 @@ export { CitationSources, DocstreamRefContext, InlineReference } from "./docs/re
8
8
  export { DocPageActions } from "./docs/PageActions"
9
9
  export type { DocPageAction, DocPageActionsOptions, DocPageActionsProps } from "./docs/PageActions"
10
10
  export type { DocRenderOptions } from "./docs/DocsRenderer"
11
- export { DocstreamDemoContext, DemoFullscreen, DemoViewer } from "./demo"
12
- export type { DemoFile, DemoMeta, DemoResolver, DemoVariant } from "./demo"
11
+ export { DocstreamDemoContext, DocstreamDemoRuntimeContext, DemoFullscreen, DemoViewer } from "./demo"
12
+ export type { DemoFile, DemoMeta, DemoResolver, DemoVariant, InlineDemoRuntime, InlineDemoRuntimeProps } from "./demo"
13
13
  export type { CitationPreview, CitationPreviewResolver, DocstreamRefContextValue } from "./docs/reference"
14
14
 
15
15
  export interface GitbookStreamdownProps extends DocRenderOptions {
@@ -31,6 +31,8 @@ export function GitbookStreamdown({
31
31
  liveRenderer,
32
32
  sourceRenderer,
33
33
  demoResolver,
34
+ demoRuntime,
35
+ demoDependencies,
34
36
  pageActions,
35
37
  }: GitbookStreamdownProps) {
36
38
  const content = markdown ?? children ?? ""
@@ -52,6 +54,8 @@ export function GitbookStreamdown({
52
54
  liveRenderer={liveRenderer}
53
55
  sourceRenderer={sourceRenderer}
54
56
  demoResolver={demoResolver}
57
+ demoRuntime={demoRuntime}
58
+ demoDependencies={demoDependencies}
55
59
  pageActions={isStreaming ? false : pageActions}
56
60
  />
57
61
  </div>
package/src/styles.css CHANGED
@@ -24,6 +24,10 @@
24
24
  --gb-pill-shadow: 0 1px 2px rgba(15, 15, 35, 0.14), 0 0 0 0.5px rgba(15, 15, 35, 0.1);
25
25
  --gb-hover: rgba(128, 128, 128, 0.12);
26
26
  --gb-menu-shadow: 0 14px 34px -10px rgba(15, 15, 35, 0.28), 0 2px 6px rgba(15, 15, 35, 0.08);
27
+ /* The terminal glyph in command boxes. Its own tokens (not --gb-panel-foreground), so a host
28
+ that sets panel colors to currentColor still gets a visible glyph. */
29
+ --gb-term-bg: rgba(128, 128, 128, 0.2);
30
+ --gb-term-fg: var(--gb-muted-foreground);
27
31
 
28
32
  /* Semantic hint palette — solid color plus precomputed tints. Matches the
29
33
  editor palette in @brett_lamy/docstream-editor/styles.css. */
@@ -148,6 +152,10 @@
148
152
  margin-block: 0.75em;
149
153
  }
150
154
 
155
+ :is([data-docstream], .docs-article) [data-docstream-blocks] > .docs-tabs-section {
156
+ margin-block: 1.6em 0.9em;
157
+ }
158
+
151
159
  :is([data-docstream], .docs-article) a {
152
160
  color: var(--gb-primary);
153
161
  text-decoration: underline;
@@ -1370,13 +1378,14 @@
1370
1378
  margin: -3px 0;
1371
1379
  }
1372
1380
 
1381
+ /* Untitled code blocks: no header bar, just the code and a copy button in its top-right corner. */
1373
1382
  .docs-code-copy-float {
1374
1383
  position: absolute;
1375
- top: 7px;
1376
- right: 7px;
1384
+ top: 9px;
1385
+ right: 9px;
1377
1386
  z-index: 1;
1378
- background: var(--gb-panel);
1379
- opacity: 0;
1387
+ background: transparent;
1388
+ opacity: 0.75;
1380
1389
  }
1381
1390
 
1382
1391
  .docs-code:hover .docs-code-copy-float,
@@ -1506,9 +1515,45 @@
1506
1515
  width: 20px;
1507
1516
  height: 20px;
1508
1517
  border-radius: 5px;
1509
- background: var(--gb-panel-foreground);
1510
- color: var(--gb-panel);
1511
- opacity: 0.85;
1518
+ background: var(--gb-term-bg, rgba(128, 128, 128, 0.2));
1519
+ color: var(--gb-term-fg, #71717a);
1520
+ }
1521
+
1522
+ /* ---- Titled tabs (`{% tabs title="Installation" %}`): heading + pill switch, then the tab ---- */
1523
+
1524
+ .docs-tabs-section-head {
1525
+ display: flex;
1526
+ flex-wrap: wrap;
1527
+ align-items: center;
1528
+ justify-content: space-between;
1529
+ gap: 12px;
1530
+ margin-bottom: 12px;
1531
+ }
1532
+
1533
+ :is([data-docstream], .docs-article) .docs-tabs-section-head .docs-tabs-section-title {
1534
+ margin: 0;
1535
+ padding-bottom: 0;
1536
+ border-bottom: 0;
1537
+ scroll-margin-top: 80px;
1538
+ }
1539
+
1540
+ .docs-tabs-section-body {
1541
+ display: flex;
1542
+ flex-direction: column;
1543
+ gap: 10px;
1544
+ }
1545
+
1546
+ .docs-tabs-section-body > [data-docstream-blocks] > * {
1547
+ margin-block: 0 10px;
1548
+ }
1549
+
1550
+ .docs-tabs-section-body > [data-docstream-blocks] > :last-child {
1551
+ margin-bottom: 0;
1552
+ }
1553
+
1554
+ .docs-tabs-section-body > [data-docstream-blocks] > p {
1555
+ color: var(--gb-muted-foreground);
1556
+ font-size: 0.93em;
1512
1557
  }
1513
1558
 
1514
1559
  .docs-tabs-code-title {
@@ -1900,9 +1945,12 @@
1900
1945
  opacity: 1;
1901
1946
  }
1902
1947
 
1948
+ /* Block-level demo roots fill the width (place-items: center would shrink them to their
1949
+ content); content is centred vertically, and intrinsically sized roots horizontally. */
1903
1950
  .docs-demo-canvas {
1904
1951
  display: grid;
1905
- place-items: center;
1952
+ align-content: center;
1953
+ justify-items: stretch;
1906
1954
  box-sizing: border-box;
1907
1955
  min-height: 100%;
1908
1956
  padding: 28px 24px;
@@ -1910,9 +1958,14 @@
1910
1958
  }
1911
1959
 
1912
1960
  .docs-demo-canvas > * {
1961
+ min-width: 0;
1913
1962
  max-width: 100%;
1914
1963
  }
1915
1964
 
1965
+ .docs-demo-canvas > :where(button, a, img, svg, video, canvas, input, select, textarea, label, span, code) {
1966
+ justify-self: center;
1967
+ }
1968
+
1916
1969
  .docs-demo-canvas-bleed {
1917
1970
  display: block;
1918
1971
  place-items: normal;
@@ -1924,6 +1977,45 @@
1924
1977
  max-width: none;
1925
1978
  }
1926
1979
 
1980
+ /* Inline-file demos: the runtime's iframe fills the frame. */
1981
+ .docs-demo-canvas-runtime {
1982
+ display: flex;
1983
+ flex-direction: column;
1984
+ }
1985
+
1986
+ .docs-demo-canvas-runtime > * {
1987
+ flex: 1 1 auto;
1988
+ min-height: 0;
1989
+ }
1990
+
1991
+ .docs-demo-runtime-frame {
1992
+ display: block;
1993
+ width: 100%;
1994
+ height: 100%;
1995
+ min-height: 120px;
1996
+ border: 0;
1997
+ background: #fff;
1998
+ }
1999
+
2000
+ .docs-demo-runtime-status {
2001
+ display: grid;
2002
+ place-items: center;
2003
+ height: 100%;
2004
+ min-height: 120px;
2005
+ color: var(--gb-muted-foreground);
2006
+ font-size: 12.5px;
2007
+ }
2008
+
2009
+ /* No runtime for an inline-file demo (or still streaming): a one-line note, then the code. */
2010
+ .docs-demo-note {
2011
+ margin: 0;
2012
+ padding: 8px 14px;
2013
+ border-bottom: 1px solid var(--gb-border);
2014
+ color: var(--gb-muted-foreground);
2015
+ font-size: 12.5px;
2016
+ line-height: 1.5;
2017
+ }
2018
+
1927
2019
  .docs-demo-loading {
1928
2020
  display: grid;
1929
2021
  place-items: center;
@@ -2150,10 +2242,27 @@
2150
2242
  }
2151
2243
 
2152
2244
  .docs-demo-single-preview {
2245
+ display: grid;
2153
2246
  min-height: 120px;
2154
2247
  background: var(--background, var(--gb-panel));
2155
2248
  }
2156
2249
 
2250
+ .docs-demo-status {
2251
+ flex-shrink: 0;
2252
+ padding: 3px 8px;
2253
+ border-radius: 999px;
2254
+ background: var(--gb-pill-track);
2255
+ color: var(--gb-muted-foreground);
2256
+ font-size: 11.5px;
2257
+ font-weight: 600;
2258
+ line-height: 1.2;
2259
+ white-space: nowrap;
2260
+ }
2261
+
2262
+ .docs-demo-variants-sized .docs-pill {
2263
+ flex: 1 1 0;
2264
+ }
2265
+
2157
2266
  /* Fullscreen overlay + deep link */
2158
2267
 
2159
2268
  .docs-demo-dialog {