@brett_lamy/docstream 0.6.4 → 0.7.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/README.md CHANGED
@@ -311,6 +311,25 @@ The package CSS is intentionally token-driven. It uses normal CSS variables and
311
311
 
312
312
  Import the CSS once, then set tokens globally in your app. Components also expose stable classes such as `docs-code`, `docs-tabs`, `docs-hint`, `docs-table`, and `docs-openapi`.
313
313
 
314
+ Code highlighting follows the surrounding `color-scheme`: set `color-scheme: dark` on a dark page (or any dark container) and code switches to the dark palette. Each token color is a custom property (`--docs-tok-keyword`, `--docs-tok-string`, `--docs-tok-type`, `--docs-tok-function`, `--docs-tok-number`, `--docs-tok-constant`, `--docs-tok-operator`, `--docs-tok-comment`) if you want your own palette.
315
+
316
+ ### Live demos with in-page components
317
+
318
+ `ReactDemo` from `@brett_lamy/docstream/playground` boots an almost-node Vite server for self-contained projects. When the components already run on your page, pass `preview` instead: you keep the same frame, header, and collapsible code panel without a runtime.
319
+
320
+ ```tsx
321
+ import { ReactDemo } from "@brett_lamy/docstream/playground"
322
+
323
+ <ReactDemo
324
+ title="Counter"
325
+ status={false}
326
+ preview={<Counter />}
327
+ height="auto"
328
+ code={`import { Counter } from "my-ui"\n\nexport default () => <Counter />`}
329
+ language="tsx"
330
+ />
331
+ ```
332
+
314
333
  ## Bundler Notes
315
334
 
316
335
  This release ships TypeScript and TSX source through ESM exports:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brett_lamy/docstream",
3
- "version": "0.6.4",
3
+ "version": "0.7.0",
4
4
  "description": "GitBook-aware readonly markdown and AI stream renderer.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -7,10 +7,8 @@ type Span = {
7
7
  end: number
8
8
  }
9
9
 
10
- const COLORS: Record<Span["type"], string | undefined> = {
11
- plain: undefined, comment: "#6B6B78", string: "#A5D6A7", number: "#F78C6C",
12
- keyword: "#C792EA", type: "#FFCB6B", function: "#82AAFF", constant: "#F07178", operator: "#89DDFF",
13
- }
10
+ /* Token colors live in styles.css (`.docs-tok-*`) and follow the host's color-scheme, so code reads on
11
+ light and dark backgrounds alike. Hosts can retheme via the --docs-tok-* custom properties. */
14
12
 
15
13
  function escapeHtml(value: string) {
16
14
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\"/g, "&quot;")
@@ -22,8 +20,7 @@ function spansToHtml(code: string, spans: Span[]) {
22
20
  const prefix = escapeHtml(code.slice(cursor, span.start))
23
21
  const text = escapeHtml(code.slice(span.start, span.end))
24
22
  cursor = span.end
25
- const color = COLORS[span.type]
26
- return `${prefix}${color ? `<span style="color:${color}">${text}</span>` : text}`
23
+ return `${prefix}${span.type === "plain" ? text : `<span class="docs-tok docs-tok-${span.type}">${text}</span>`}`
27
24
  }).join("") + escapeHtml(code.slice(cursor))
28
25
  }
29
26
 
@@ -1,4 +1,4 @@
1
- import { useEffect, useId, useMemo, useState, type CSSProperties } from "react"
1
+ import { useEffect, useId, useMemo, useState, type CSSProperties, type ReactNode } from "react"
2
2
  import { HighlightedCode } from "../docs/HighlightedCode"
3
3
  import {
4
4
  createAlmostNodeWorkspace,
@@ -11,8 +11,21 @@ import {
11
11
  } from "./filesystem"
12
12
 
13
13
  export interface ReactDemoProps {
14
- /** Complete project file map. Relative paths are rooted at `/`. */
15
- files: AlmostNodeFiles
14
+ /** Complete project file map. Relative paths are rooted at `/`. Not needed when `preview` is given. */
15
+ files?: AlmostNodeFiles
16
+ /**
17
+ * Render this in place of the almost-node iframe — for hosts that can mount the demo directly
18
+ * (components already on the page). No runtime starts; the frame, header, and code panel stay the same.
19
+ */
20
+ preview?: ReactNode
21
+ /** Source text for the code panel. Overrides reading `codeFile` from `files`. */
22
+ code?: string
23
+ /** Highlighting language for `code` (e.g. "tsx"). Defaults to the extension of `codeFile`. */
24
+ language?: string
25
+ /** Header status. Defaults to the runtime state ("ready", "starting"…); pass `false` to hide it. */
26
+ status?: ReactNode | false
27
+ /** Extra header content after the status, e.g. controls that switch between demo variants. */
28
+ actions?: ReactNode
16
29
  /** Vite entry file. Defaults to `/src/main.jsx`. */
17
30
  entry?: string
18
31
  /** Preferred virtual port. almost-node picks the next free port if needed. */
@@ -71,8 +84,15 @@ function sourceText(files: AlmostNodeFiles, requestedPath: string): string | nul
71
84
  }
72
85
 
73
86
  /** Render a multi-file React/JSX/TSX project through almost-node's Vite server. */
87
+ const NO_FILES: AlmostNodeFiles = {}
88
+
74
89
  export function ReactDemo({
75
- files,
90
+ files = NO_FILES,
91
+ preview,
92
+ code: codeProp,
93
+ language,
94
+ status,
95
+ actions,
76
96
  entry = "/src/main.jsx",
77
97
  port = 4173,
78
98
  height = 360,
@@ -87,16 +107,17 @@ export function ReactDemo({
87
107
  onReady,
88
108
  onError,
89
109
  }: ReactDemoProps) {
90
- const [run, setRun] = useState(autoStart ? 1 : 0)
91
- const [state, setState] = useState<PreviewState>(autoStart ? "starting" : "idle")
110
+ const inline = preview !== undefined
111
+ const [run, setRun] = useState(autoStart && !inline ? 1 : 0)
112
+ const [state, setState] = useState<PreviewState>(inline ? "ready" : autoStart ? "starting" : "idle")
92
113
  const [url, setUrl] = useState<string | null>(null)
93
114
  const [error, setError] = useState<string | null>(null)
94
115
  const [codeExpanded, setCodeExpanded] = useState(false)
95
116
  const codePanelId = useId()
96
117
  const displayedCodeFile = codeFile ?? entry
97
118
  const code = useMemo(
98
- () => sourceText(files, displayedCodeFile),
99
- [displayedCodeFile, files],
119
+ () => codeProp ?? sourceText(files, displayedCodeFile),
120
+ [codeProp, displayedCodeFile, files],
100
121
  )
101
122
  const collapsedLines = positiveLineCount(collapsedCodeLines, 3)
102
123
  const expandedLines = Math.max(
@@ -105,6 +126,7 @@ export function ReactDemo({
105
126
  )
106
127
 
107
128
  useEffect(() => {
129
+ if (inline) return
108
130
  if (!run) {
109
131
  setState("idle")
110
132
  setUrl(null)
@@ -162,7 +184,7 @@ export function ReactDemo({
162
184
  cancelled = true
163
185
  workspace?.dispose()
164
186
  }
165
- }, [entry, files, onError, onReady, port, run, workspaceOptions])
187
+ }, [entry, files, inline, onError, onReady, port, run, workspaceOptions])
166
188
 
167
189
  const wrapperClass = className ? `docs-react-demo ${className}` : "docs-react-demo"
168
190
  const running = state === "starting"
@@ -171,10 +193,13 @@ export function ReactDemo({
171
193
  <section className={wrapperClass} data-docstream-react-demo="">
172
194
  <header className="docs-react-demo-header">
173
195
  <span>{title}</span>
174
- <span className={`docs-react-demo-status docs-react-demo-status-${state}`}>
175
- {state === "ready" ? "ready" : state === "starting" ? "starting" : state}
176
- </span>
177
- {!autoStart || state === "error" ? (
196
+ {status === false ? null : (
197
+ <span className={`docs-react-demo-status docs-react-demo-status-${state}`}>
198
+ {status ?? (state === "ready" ? "ready" : state === "starting" ? "starting" : state)}
199
+ </span>
200
+ )}
201
+ {actions}
202
+ {!inline && (!autoStart || state === "error") ? (
178
203
  <button
179
204
  type="button"
180
205
  className="docs-react-demo-run"
@@ -185,7 +210,9 @@ export function ReactDemo({
185
210
  </button>
186
211
  ) : null}
187
212
  </header>
188
- {error ? (
213
+ {inline ? (
214
+ <div className="docs-react-demo-preview" style={frameHeight(height)}>{preview}</div>
215
+ ) : error ? (
189
216
  <pre className="docs-react-demo-error">{error}</pre>
190
217
  ) : url ? (
191
218
  <iframe
@@ -211,7 +238,7 @@ export function ReactDemo({
211
238
  >
212
239
  <HighlightedCode
213
240
  code={code}
214
- language={sourceLanguage(displayedCodeFile)}
241
+ language={language ?? sourceLanguage(displayedCodeFile)}
215
242
  lineNumbers
216
243
  />
217
244
  </pre>
package/src/styles.css CHANGED
@@ -507,6 +507,17 @@
507
507
  display: block;
508
508
  }
509
509
 
510
+ /* Syntax tokens (HighlightedCode). Palenight on dark color-schemes, same-hue AA colors on light ones.
511
+ Override any --docs-tok-* property to retheme. */
512
+ .docs-tok-keyword { color: var(--docs-tok-keyword, light-dark(#8839C4, #C792EA)); }
513
+ .docs-tok-type { color: var(--docs-tok-type, light-dark(#A15C00, #FFCB6B)); }
514
+ .docs-tok-function { color: var(--docs-tok-function, light-dark(#2F5BD3, #82AAFF)); }
515
+ .docs-tok-string { color: var(--docs-tok-string, light-dark(#23803A, #A5D6A7)); }
516
+ .docs-tok-number { color: var(--docs-tok-number, light-dark(#C2410C, #F78C6C)); }
517
+ .docs-tok-constant { color: var(--docs-tok-constant, light-dark(#C8283F, #F07178)); }
518
+ .docs-tok-operator { color: var(--docs-tok-operator, light-dark(#0B7285, #89DDFF)); }
519
+ .docs-tok-comment { color: var(--docs-tok-comment, light-dark(#6E6E7A, #8A8A98)); font-style: italic; }
520
+
510
521
  .docs-code-line {
511
522
  display: flex;
512
523
  gap: 14px;
@@ -973,6 +984,12 @@
973
984
  background: #fff;
974
985
  }
975
986
 
987
+ /* Inline preview (`preview` prop): the host's own content, so no forced background. `height: auto` grows to fit. */
988
+ .docs-react-demo-preview {
989
+ position: relative;
990
+ overflow: hidden;
991
+ }
992
+
976
993
  .docs-react-demo-placeholder {
977
994
  display: grid;
978
995
  place-items: center;