@brett_lamy/docstream 0.6.3 → 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 +19 -0
- package/package.json +1 -1
- package/src/docs/HighlightedCode.tsx +3 -6
- package/src/playground/ReactCodePreview.tsx +42 -15
- package/src/styles.css +23 -4
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
|
@@ -7,10 +7,8 @@ type Span = {
|
|
|
7
7
|
end: number
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """)
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
91
|
-
const [
|
|
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
|
-
|
|
175
|
-
{
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
{
|
|
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
|
@@ -135,10 +135,6 @@
|
|
|
135
135
|
margin-block: 0.75em;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
:is([data-docstream], .docs-article) [data-docstream-blocks] > :is(.docs-table, table) {
|
|
139
|
-
margin-block: 0;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
138
|
:is([data-docstream], .docs-article) [data-docstream-blocks] > :is(.docs-cards, .docs-code, .docs-math, .docs-hint, .docs-tabs, .docs-expandable, .docs-stepper, .docs-mermaid-preview, .oas, .docs-embed, .docstream-viz, figure, blockquote, ul, ol, hr) {
|
|
143
139
|
margin-block: 0.75em;
|
|
144
140
|
}
|
|
@@ -511,6 +507,17 @@
|
|
|
511
507
|
display: block;
|
|
512
508
|
}
|
|
513
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
|
+
|
|
514
521
|
.docs-code-line {
|
|
515
522
|
display: flex;
|
|
516
523
|
gap: 14px;
|
|
@@ -977,6 +984,12 @@
|
|
|
977
984
|
background: #fff;
|
|
978
985
|
}
|
|
979
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
|
+
|
|
980
993
|
.docs-react-demo-placeholder {
|
|
981
994
|
display: grid;
|
|
982
995
|
place-items: center;
|
|
@@ -1295,3 +1308,9 @@
|
|
|
1295
1308
|
height: 14px;
|
|
1296
1309
|
border-radius: 4px;
|
|
1297
1310
|
}
|
|
1311
|
+
|
|
1312
|
+
/* Keep every rendered table variant flush, after the shared block spacing rules. */
|
|
1313
|
+
:is([data-docstream], .docs-article) [data-docstream-blocks] > :is(.docs-table, table, .docs-cards) {
|
|
1314
|
+
margin-block-start: 0;
|
|
1315
|
+
margin-block-end: 0;
|
|
1316
|
+
}
|