@brett_lamy/docstream 1.2.1 → 1.2.2
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 +36 -5
- package/package.json +1 -1
- package/src/demo/DemoViewer.tsx +52 -9
- package/src/demo/index.ts +1 -0
- package/src/demo/markdown.ts +17 -3
- package/src/demo/surface.ts +67 -0
- package/src/gitbook/ast.ts +27 -2
- package/src/gitbook/attrs.ts +39 -0
- package/src/gitbook/flatten.ts +1 -1
- package/src/gitbook/index.ts +1 -0
- package/src/gitbook/parse.ts +26 -16
- package/src/gitbook/serialize.ts +50 -42
- package/src/styles.css +29 -19
package/README.md
CHANGED
|
@@ -107,6 +107,18 @@ Helpful context for the reader.
|
|
|
107
107
|
|
|
108
108
|
Supported styles are `info`, `success`, `warning`, and `danger`.
|
|
109
109
|
|
|
110
|
+
### Code fences
|
|
111
|
+
|
|
112
|
+
````md
|
|
113
|
+
```ts title="client.ts"
|
|
114
|
+
const client = createClient()
|
|
115
|
+
```
|
|
116
|
+
````
|
|
117
|
+
|
|
118
|
+
Titled (file) fences show line numbers; untitled fences don't. `lineNumbers="true"`
|
|
119
|
+
or `lineNumbers="false"` overrides either way, and an explicit attribute is kept on
|
|
120
|
+
serialize even when it matches the default.
|
|
121
|
+
|
|
110
122
|
### Tabs
|
|
111
123
|
|
|
112
124
|
````md
|
|
@@ -289,11 +301,30 @@ examples/button/sizes/
|
|
|
289
301
|
```
|
|
290
302
|
|
|
291
303
|
`meta.height` is a minimum for single-file previews (in-page components may grow
|
|
292
|
-
past it
|
|
293
|
-
|
|
294
|
-
`
|
|
295
|
-
`
|
|
296
|
-
|
|
304
|
+
past it; without a height they size to their content — hosts can set a floor with
|
|
305
|
+
`--docs-demo-preview-min-height`) and the stage height for the multi-file viewer.
|
|
306
|
+
`className` / `surface` (a style object, CSS custom properties welcome) style the
|
|
307
|
+
preview canvas, `bleed` drops its padding, `status` shows a small label in the
|
|
308
|
+
header ("Live", "Needs network"), and `variantsWidth` sizes the variant switch.
|
|
309
|
+
The canvas is block flow: block-level demo roots fill its width and lay out as on a
|
|
310
|
+
page (`max-width: 480px; margin: 0 auto` centres at 480px); intrinsically sized
|
|
311
|
+
(inline-level) ones — a button, a badge, an image — are centred.
|
|
312
|
+
|
|
313
|
+
Every `meta.json` field has a tag attribute of the same name:
|
|
314
|
+
|
|
315
|
+
```md
|
|
316
|
+
{% demo src="button/sizes" title="Sizes \"S\" to \"L\"" status="Live" bleed="true" className="brand"
|
|
317
|
+
surface="background: var(--brand-50); --demo-gap: 12px" variantsWidth="220" %}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
(one line in practice). `surface` is written as CSS declarations; `bleed="false"`
|
|
321
|
+
switches off a `meta.json` `bleed: true`. **Precedence:** a tag attribute wins,
|
|
322
|
+
then `meta.json`, then the built-in default — field by field. Copy page folds
|
|
323
|
+
`meta.json` into the tag, so the copied page renders the same without the resolver.
|
|
324
|
+
|
|
325
|
+
Attribute values are double-quoted; inside them a double quote is written `\"`, a
|
|
326
|
+
backslash `\\`, and `%}` as `%\}`. The serializer escapes, the parser unescapes
|
|
327
|
+
(other backslashes are kept as written).
|
|
297
328
|
|
|
298
329
|
#### Demos that carry their files (block form)
|
|
299
330
|
|
package/package.json
CHANGED
package/src/demo/DemoViewer.tsx
CHANGED
|
@@ -38,6 +38,7 @@ import { PillTabs, Segmented, rovingKeyDown } from "../docs/controls"
|
|
|
38
38
|
import { useDemoResolver, useDemoRuntime } from "./context"
|
|
39
39
|
import { languageForPath } from "./glob"
|
|
40
40
|
import { inlineDemoMarkdown } from "./markdown"
|
|
41
|
+
import { cssToSurface, surfaceStyle, surfaceToCss } from "./surface"
|
|
41
42
|
import type { DemoComponent, DemoFile, DemoMeta, DemoResolver, DemoVariant, InlineDemoRuntime } from "./types"
|
|
42
43
|
|
|
43
44
|
export interface DemoViewerProps {
|
|
@@ -66,7 +67,23 @@ export interface DemoViewerProps {
|
|
|
66
67
|
runtime?: InlineDemoRuntime
|
|
67
68
|
/** Extra npm dependencies for the runtime, merged over `demoDependencies` from context. */
|
|
68
69
|
dependencies?: Record<string, string>
|
|
70
|
+
/** Class on the viewer's outer element. (The preview canvas's class is `canvasClassName`.) */
|
|
69
71
|
className?: string
|
|
72
|
+
/*
|
|
73
|
+
* Look. Each falls back to the resolver's `meta.json` field of the same name when unset —
|
|
74
|
+
* props (a `{% demo %}` tag's attributes) win over `meta.json`, which wins over the
|
|
75
|
+
* defaults — so a copied page renders the same with or without the resolver.
|
|
76
|
+
*/
|
|
77
|
+
/** Status shown in the header (`meta.status`). */
|
|
78
|
+
status?: string
|
|
79
|
+
/** Full-bleed canvas without the padded surface (`meta.bleed`). */
|
|
80
|
+
bleed?: boolean
|
|
81
|
+
/** Extra class on the preview canvas (`meta.className`). */
|
|
82
|
+
canvasClassName?: string
|
|
83
|
+
/** Canvas inline style: an object like `meta.surface`, or CSS declarations as in the tag's `surface`. */
|
|
84
|
+
surface?: Record<string, string | number> | string
|
|
85
|
+
/** Width of the variant switch (`meta.variantsWidth`). */
|
|
86
|
+
variantsWidth?: number | string
|
|
70
87
|
}
|
|
71
88
|
|
|
72
89
|
/* ---------- data ---------- */
|
|
@@ -249,7 +266,11 @@ function DemoCanvas({ src, preview, variant, bleed, look }: {
|
|
|
249
266
|
}
|
|
250
267
|
if (preview.kind === "runtime") {
|
|
251
268
|
return (
|
|
252
|
-
<div
|
|
269
|
+
<div
|
|
270
|
+
className={["docs-demo-canvas docs-demo-canvas-bleed docs-demo-canvas-runtime", look?.className ?? ""].filter(Boolean).join(" ")}
|
|
271
|
+
style={surfaceStyle(look?.surface)}
|
|
272
|
+
data-variant={variant}
|
|
273
|
+
>
|
|
253
274
|
<DemoErrorBoundary src={src || "demo"}>{preview.render(variant)}</DemoErrorBoundary>
|
|
254
275
|
</div>
|
|
255
276
|
)
|
|
@@ -258,7 +279,7 @@ function DemoCanvas({ src, preview, variant, bleed, look }: {
|
|
|
258
279
|
return (
|
|
259
280
|
<div
|
|
260
281
|
className={["docs-demo-canvas", bleed ? "docs-demo-canvas-bleed" : "", look?.className ?? ""].filter(Boolean).join(" ")}
|
|
261
|
-
style={look?.surface
|
|
282
|
+
style={surfaceStyle(look?.surface)}
|
|
262
283
|
data-variant={variant}
|
|
263
284
|
>
|
|
264
285
|
{component.status === "ready" ? (
|
|
@@ -498,7 +519,7 @@ function VariantSwitch({ variants, value, onChange, title, width }: {
|
|
|
498
519
|
onChange={onChange}
|
|
499
520
|
label={`${title} variant`}
|
|
500
521
|
className={width === undefined ? "docs-demo-variants" : "docs-demo-variants docs-demo-variants-sized"}
|
|
501
|
-
style={width === undefined ? undefined : { width:
|
|
522
|
+
style={width === undefined ? undefined : { width: cssLength(width, "auto") }}
|
|
502
523
|
/>
|
|
503
524
|
)
|
|
504
525
|
}
|
|
@@ -600,10 +621,15 @@ export function DemoViewer(props: DemoViewerProps) {
|
|
|
600
621
|
const layout: "single" | "multi" | null =
|
|
601
622
|
layoutPref !== "auto" ? layoutPref : fileList ? (fileList.length > 1 ? "multi" : "single") : null
|
|
602
623
|
const heightValue = props.height ?? metaValue?.height
|
|
603
|
-
|
|
604
|
-
const
|
|
605
|
-
const
|
|
606
|
-
const
|
|
624
|
+
// Look: the tag's attributes (props) first, then `meta.json`.
|
|
625
|
+
const bleed = props.bleed ?? metaValue?.bleed
|
|
626
|
+
const propSurface = typeof props.surface === "string" ? cssToSurface(props.surface) : props.surface
|
|
627
|
+
const look: CanvasLook = {
|
|
628
|
+
className: props.canvasClassName ?? metaValue?.className,
|
|
629
|
+
surface: propSurface ?? metaValue?.surface,
|
|
630
|
+
}
|
|
631
|
+
const variantsWidth = props.variantsWidth ?? metaValue?.variantsWidth
|
|
632
|
+
const status = props.status ?? metaValue?.status
|
|
607
633
|
const [fullscreen, setFullscreen] = useState(false)
|
|
608
634
|
const href = mode === "resolver" ? resolver?.href?.(src, variant ? { variant } : undefined) : undefined
|
|
609
635
|
const baseId = useId()
|
|
@@ -637,6 +663,11 @@ export function DemoViewer(props: DemoViewerProps) {
|
|
|
637
663
|
...(props.layout ? { layout: props.layout } : {}),
|
|
638
664
|
...(props.variants?.length ? { variants: props.variants } : {}),
|
|
639
665
|
...(props.viewport ? { viewport: props.viewport } : {}),
|
|
666
|
+
...(props.status ? { status: props.status } : {}),
|
|
667
|
+
...(props.bleed !== undefined ? { bleed: props.bleed } : {}),
|
|
668
|
+
...(props.canvasClassName ? { className: props.canvasClassName } : {}),
|
|
669
|
+
...(props.surface ? { surface: typeof props.surface === "string" ? props.surface : surfaceToCss(props.surface) } : {}),
|
|
670
|
+
...(props.variantsWidth !== undefined && props.variantsWidth !== "" ? { variantsWidth: String(props.variantsWidth) } : {}),
|
|
640
671
|
}
|
|
641
672
|
const copyDemo = () => inlineDemoMarkdown(node, fileList ?? [], metaValue)
|
|
642
673
|
|
|
@@ -676,8 +707,15 @@ export function DemoViewer(props: DemoViewerProps) {
|
|
|
676
707
|
{runnable ? (
|
|
677
708
|
<div
|
|
678
709
|
className="docs-react-demo-preview docs-demo-single-preview"
|
|
679
|
-
// A hint, not a cap: in-page components may grow past it
|
|
680
|
-
|
|
710
|
+
// A hint, not a cap: in-page components may grow past it (no height: size to content;
|
|
711
|
+
// hosts may set a floor with --docs-demo-preview-min-height). An iframe needs a real height.
|
|
712
|
+
style={
|
|
713
|
+
preview.kind === "runtime"
|
|
714
|
+
? { height: cssLength(heightValue, "360px") }
|
|
715
|
+
: heightValue === undefined || heightValue === ""
|
|
716
|
+
? undefined
|
|
717
|
+
: { minHeight: cssLength(heightValue, "0px") }
|
|
718
|
+
}
|
|
681
719
|
>
|
|
682
720
|
{near ? <DemoCanvas src={src} preview={preview} variant={variant} bleed={bleed} look={look} /> : <Spinner />}
|
|
683
721
|
</div>
|
|
@@ -844,6 +882,11 @@ export function DemoBlock({ node }: { node: DemoNode }) {
|
|
|
844
882
|
files={node.files}
|
|
845
883
|
entry={node.entry}
|
|
846
884
|
streaming={node.open}
|
|
885
|
+
status={node.status}
|
|
886
|
+
bleed={node.bleed}
|
|
887
|
+
canvasClassName={node.className}
|
|
888
|
+
surface={node.surface}
|
|
889
|
+
variantsWidth={node.variantsWidth}
|
|
847
890
|
/>
|
|
848
891
|
)
|
|
849
892
|
}
|
package/src/demo/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export {
|
|
|
11
11
|
} from "./glob"
|
|
12
12
|
export type { GlobDemoResolverOptions } from "./glob"
|
|
13
13
|
export { defaultDemoMarkdown, inlineDemoMarkdown, inlineDemoNode, resolveDemosToMarkdown, toInlineFiles } from "./markdown"
|
|
14
|
+
export { cssToSurface, surfaceToCss } from "./surface"
|
|
14
15
|
export type { DemoMarkdownFormat, ResolveDemosOptions, ResolvedDemo } from "./markdown"
|
|
15
16
|
export type {
|
|
16
17
|
DemoComponent,
|
package/src/demo/markdown.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { DemoInlineFile, DemoNode } from "../gitbook/ast"
|
|
2
|
+
import { TAG_ATTRS } from "../gitbook/attrs"
|
|
2
3
|
import { flattenForPlainMarkdown } from "../gitbook/flatten"
|
|
3
4
|
import { demoBody, fenceTracker, parseBlocks, parseMarkdown } from "../gitbook/parse"
|
|
4
5
|
import { fenceFor, serializeBlocks, serializeMarkdown } from "../gitbook/serialize"
|
|
5
6
|
import type { DemoFile, DemoMeta, DemoResolver } from "./types"
|
|
6
7
|
import { languageForPath } from "./glob"
|
|
8
|
+
import { surfaceToCss } from "./surface"
|
|
7
9
|
|
|
8
10
|
export { fenceFor }
|
|
9
11
|
|
|
@@ -39,7 +41,7 @@ export interface ResolvedDemo {
|
|
|
39
41
|
title: string
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
const TAG_RE =
|
|
44
|
+
const TAG_RE = new RegExp(String.raw`^(\s*)\{%\s*demo(\s${TAG_ATTRS})?\s*\/?%\}\s*$`)
|
|
43
45
|
|
|
44
46
|
function titleFor(src: string, node: DemoNode, meta: DemoMeta | undefined) {
|
|
45
47
|
return node.title ?? meta?.title ?? (src.split("/").pop() || src || "Demo")
|
|
@@ -77,7 +79,9 @@ export function toInlineFiles(files: DemoFile[]): DemoInlineFile[] {
|
|
|
77
79
|
/**
|
|
78
80
|
* A demo as a self-contained block-form `{% demo %}`: the node's attributes, with the
|
|
79
81
|
* folder's `meta.json` defaults folded in (so the block renders the same without the
|
|
80
|
-
* resolver), and
|
|
82
|
+
* resolver) — title, description, height, layout, variants, entry and the look fields
|
|
83
|
+
* `status`, `bleed`, `className`, `surface` (as CSS declarations) and `variantsWidth` —
|
|
84
|
+
* and `files` as inline fences. Tag attributes win over `meta.json` where both exist.
|
|
81
85
|
*/
|
|
82
86
|
export function inlineDemoNode(node: DemoNode, files: DemoFile[], meta?: DemoMeta): DemoNode {
|
|
83
87
|
const inline = toInlineFiles(files)
|
|
@@ -87,6 +91,11 @@ export function inlineDemoNode(node: DemoNode, files: DemoFile[], meta?: DemoMet
|
|
|
87
91
|
const layout = node.layout ?? meta?.layout
|
|
88
92
|
const variants = node.variants?.length ? node.variants : meta?.variants?.length ? meta.variants : undefined
|
|
89
93
|
const entry = node.entry ?? (meta?.entry && inline[0]?.path !== meta.entry ? meta.entry : undefined)
|
|
94
|
+
const status = node.status ?? meta?.status
|
|
95
|
+
const bleed = node.bleed ?? meta?.bleed
|
|
96
|
+
const className = node.className ?? meta?.className
|
|
97
|
+
const surface = node.surface ?? (meta?.surface ? surfaceToCss(meta.surface) : undefined)
|
|
98
|
+
const variantsWidth = node.variantsWidth ?? (meta?.variantsWidth === undefined || meta.variantsWidth === "" ? undefined : String(meta.variantsWidth))
|
|
90
99
|
return {
|
|
91
100
|
type: "demo",
|
|
92
101
|
src: node.src,
|
|
@@ -97,6 +106,11 @@ export function inlineDemoNode(node: DemoNode, files: DemoFile[], meta?: DemoMet
|
|
|
97
106
|
...(variants ? { variants } : {}),
|
|
98
107
|
...(node.viewport ? { viewport: node.viewport } : {}),
|
|
99
108
|
...(entry ? { entry } : {}),
|
|
109
|
+
...(status ? { status } : {}),
|
|
110
|
+
...(bleed === undefined ? {} : { bleed }),
|
|
111
|
+
...(className ? { className } : {}),
|
|
112
|
+
...(surface ? { surface } : {}),
|
|
113
|
+
...(variantsWidth ? { variantsWidth } : {}),
|
|
100
114
|
...(inline.length ? { files: inline } : {}),
|
|
101
115
|
}
|
|
102
116
|
}
|
|
@@ -123,7 +137,7 @@ export async function resolveDemosToMarkdown(
|
|
|
123
137
|
options: ResolveDemosOptions = {},
|
|
124
138
|
): Promise<string> {
|
|
125
139
|
// Plain output also flattens docstream-only presentation blocks (command boxes, titled tabs).
|
|
126
|
-
if (options.format === "plain" && /\{%\s*(?:command\b|tabs\s[
|
|
140
|
+
if (options.format === "plain" && /\{%\s*(?:command\b|tabs\s(?:[^%\n]|%(?!\}))*\btitle=)/.test(markdown)) {
|
|
127
141
|
markdown = serializeMarkdown(flattenForPlainMarkdown(parseMarkdown(markdown)))
|
|
128
142
|
}
|
|
129
143
|
const lines = markdown.split("\n")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { CSSProperties } from "react"
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* A demo canvas's `surface` (inline style) has two spellings:
|
|
5
|
+
* - `meta.json`: an object, `{ "background": "var(--brand-50)", "--demo-gap": "12px", "padding": 12 }`;
|
|
6
|
+
* - the `{% demo surface="…" %}` attribute: CSS declarations,
|
|
7
|
+
* `background: var(--brand-50); --demo-gap: 12px; padding: 12`.
|
|
8
|
+
* Keys are kept as written (camelCase, kebab-case or custom properties).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** A `meta.json` surface object as the declaration string the demo tag carries. */
|
|
12
|
+
export function surfaceToCss(surface: Record<string, string | number>): string {
|
|
13
|
+
return Object.entries(surface)
|
|
14
|
+
.filter(([key, value]) => key.trim() && value !== undefined && value !== null && String(value).trim() !== "")
|
|
15
|
+
.map(([key, value]) => `${key.trim()}: ${String(value).trim()}`)
|
|
16
|
+
.join("; ")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Split on `;` outside parentheses and quotes (`url("a;b")` stays whole). */
|
|
20
|
+
function declarations(css: string): string[] {
|
|
21
|
+
const out: string[] = []
|
|
22
|
+
let depth = 0
|
|
23
|
+
let quote = ""
|
|
24
|
+
let current = ""
|
|
25
|
+
for (const ch of css) {
|
|
26
|
+
if (quote) {
|
|
27
|
+
if (ch === quote) quote = ""
|
|
28
|
+
} else if (ch === '"' || ch === "'") quote = ch
|
|
29
|
+
else if (ch === "(") depth++
|
|
30
|
+
else if (ch === ")") depth = Math.max(0, depth - 1)
|
|
31
|
+
else if (ch === ";" && depth === 0) {
|
|
32
|
+
out.push(current)
|
|
33
|
+
current = ""
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
current += ch
|
|
37
|
+
}
|
|
38
|
+
out.push(current)
|
|
39
|
+
return out
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The tag's declaration string as a React style object. Custom properties keep their
|
|
44
|
+
* name; `kebab-case` becomes camelCase; plain numbers on ordinary properties become
|
|
45
|
+
* numbers (so `padding: 12` means 12px, as it does in `meta.json`).
|
|
46
|
+
*/
|
|
47
|
+
export function cssToSurface(css: string): Record<string, string | number> {
|
|
48
|
+
const style: Record<string, string | number> = {}
|
|
49
|
+
for (const declaration of declarations(css)) {
|
|
50
|
+
const colon = declaration.indexOf(":")
|
|
51
|
+
if (colon < 0) continue
|
|
52
|
+
const key = declaration.slice(0, colon).trim()
|
|
53
|
+
const value = declaration.slice(colon + 1).trim()
|
|
54
|
+
if (!key || !value) continue
|
|
55
|
+
if (key.startsWith("--")) {
|
|
56
|
+
style[key] = value
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
const name = key.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())
|
|
60
|
+
style[name] = /^-?\d+(?:\.\d+)?$/.test(value) ? Number(value) : value
|
|
61
|
+
}
|
|
62
|
+
return style
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function surfaceStyle(surface: Record<string, string | number> | undefined): CSSProperties | undefined {
|
|
66
|
+
return surface && Object.keys(surface).length ? (surface as CSSProperties) : undefined
|
|
67
|
+
}
|
package/src/gitbook/ast.ts
CHANGED
|
@@ -67,7 +67,16 @@ export interface CodeBlockNode {
|
|
|
67
67
|
type: "code"
|
|
68
68
|
language: string | null
|
|
69
69
|
title: string | null
|
|
70
|
+
/**
|
|
71
|
+
* Show line numbers. Defaults (when the fence doesn't say) to on for titled fences and
|
|
72
|
+
* off for untitled ones; `lineNumbers="true"` / `"false"` overrides.
|
|
73
|
+
*/
|
|
70
74
|
lineNumbers: boolean
|
|
75
|
+
/**
|
|
76
|
+
* The source wrote `lineNumbers` (or `showLineNumbers`) explicitly: the serializer keeps
|
|
77
|
+
* the attribute even when it matches the default, so pages round-trip byte-for-byte.
|
|
78
|
+
*/
|
|
79
|
+
lineNumbersExplicit?: boolean
|
|
71
80
|
code: string
|
|
72
81
|
/** Run a React/JSX/TSX entry file in the optional almost-node preview. */
|
|
73
82
|
live?: boolean
|
|
@@ -201,8 +210,11 @@ export interface DemoInlineFile {
|
|
|
201
210
|
* A live demo: `{% demo src="<page>/<example>" %}`. With a host `DemoResolver`
|
|
202
211
|
* that knows `src`, the example folder is the authority for the component, its
|
|
203
212
|
* source files and its defaults; attributes here override the folder's
|
|
204
|
-
* `meta.json
|
|
205
|
-
*
|
|
213
|
+
* `meta.json` field by field (tag attribute first, then `meta.json`, then the built-in
|
|
214
|
+
* default) — including the look attributes `status`, `bleed`, `className`, `surface` and
|
|
215
|
+
* `variantsWidth`, which Copy page writes so the block renders the same without the resolver.
|
|
216
|
+
* The block form carries the files inline so the Markdown renders anywhere, resolver
|
|
217
|
+
* or not:
|
|
206
218
|
*
|
|
207
219
|
* {% demo src="composer/scroll-fab" title="Scroll → FAB" %}
|
|
208
220
|
* ```tsx title="index.tsx"
|
|
@@ -225,6 +237,19 @@ export interface DemoNode {
|
|
|
225
237
|
viewport?: DemoViewport
|
|
226
238
|
/** Entry file among the inline files (`entry="App.tsx"`). Defaults to the first file. */
|
|
227
239
|
entry?: string
|
|
240
|
+
/** Short status shown in the demo header (`status="Live"`). */
|
|
241
|
+
status?: string
|
|
242
|
+
/** Drop the padded preview surface (`bleed="true"`) for demos with their own full-bleed frame. */
|
|
243
|
+
bleed?: boolean
|
|
244
|
+
/** Extra class on the preview canvas (`className="brand-surface"`). */
|
|
245
|
+
className?: string
|
|
246
|
+
/**
|
|
247
|
+
* Inline style for the preview canvas as CSS declarations, kept verbatim:
|
|
248
|
+
* `surface="background: var(--brand-50); --demo-gap: 12px"`.
|
|
249
|
+
*/
|
|
250
|
+
surface?: string
|
|
251
|
+
/** Width of the variant switch, kept verbatim (`"240"`, `"16rem"`). */
|
|
252
|
+
variantsWidth?: string
|
|
228
253
|
/** Files carried inline by the block form, in document order. */
|
|
229
254
|
files?: DemoInlineFile[]
|
|
230
255
|
/**
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tag and fence attribute values (`{% demo title="…" %}`, ```` ```ts title="…" ````).
|
|
3
|
+
*
|
|
4
|
+
* Values are double-quoted. On serialize a backslash becomes `\\`, a double quote `\"`,
|
|
5
|
+
* and the tag terminator `%}` becomes `%\}` — so a value can never end its tag early or
|
|
6
|
+
* truncate at a quote. On parse `\\`, `\"` and `\}` are unescaped; any other backslash is
|
|
7
|
+
* kept literally (so `C:\temp` written by hand still reads as written).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Escape one attribute value for a double-quoted `name="…"` pair. */
|
|
11
|
+
export function escapeAttr(value: string): string {
|
|
12
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%\}/g, "%\\}")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Undo `escapeAttr`. */
|
|
16
|
+
export function unescapeAttr(raw: string): string {
|
|
17
|
+
return raw.replace(/\\(["\\}])/g, "$1")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** ` name="value"` with the value escaped (leading space included). */
|
|
21
|
+
export function attr(name: string, value: string | number | boolean): string {
|
|
22
|
+
return ` ${name}="${escapeAttr(String(value))}"`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Every `name="value"` pair in `raw` (escapes honoured, values unescaped). */
|
|
26
|
+
export function parseAttrs(raw: string | undefined): Record<string, string> {
|
|
27
|
+
const attrs: Record<string, string> = {}
|
|
28
|
+
if (!raw) return attrs
|
|
29
|
+
for (const m of raw.matchAll(/([\w-]+)="((?:[^"\\]|\\.)*)"/g)) {
|
|
30
|
+
attrs[m[1]] = unescapeAttr(m[2])
|
|
31
|
+
}
|
|
32
|
+
return attrs
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The attribute part of a `{% name … %}` tag, as a regex source: anything up to the
|
|
37
|
+
* closing `%}` (a lone `%` is fine; `%}` inside a value is always escaped as `%\}`).
|
|
38
|
+
*/
|
|
39
|
+
export const TAG_ATTRS = String.raw`(?:[^%\n]|%(?!\}))*?`
|
package/src/gitbook/flatten.ts
CHANGED
|
@@ -19,7 +19,7 @@ export function flattenBlocks(blocks: Block[]): Block[] {
|
|
|
19
19
|
return blocks.flatMap((b): Block[] => {
|
|
20
20
|
switch (b.type) {
|
|
21
21
|
case "command":
|
|
22
|
-
return [{ type: "code", language: "sh", title: null, lineNumbers:
|
|
22
|
+
return [{ type: "code", language: "sh", title: null, lineNumbers: false, code: b.command }]
|
|
23
23
|
case "tabs":
|
|
24
24
|
if (b.title) {
|
|
25
25
|
return [
|
package/src/gitbook/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Pure GitBook markdown engine (no React, no mermaid/streamdown). Safe to import
|
|
2
2
|
// in non-DOM environments such as a server-side AI agent or a Cloudflare Worker.
|
|
3
3
|
export type * from "./ast"
|
|
4
|
+
export { escapeAttr, unescapeAttr } from "./attrs"
|
|
4
5
|
export { closesFence, demoBody, fenceOpener, fenceTracker, parseDemoVariants, parseMarkdown, parseBlocks, trimPartialInlineToken } from "./parse"
|
|
5
6
|
export { fenceFor, serializeBlocks, serializeDemoFile, serializeMarkdown } from "./serialize"
|
|
6
7
|
export { footnoteDefinitions, parseInline, plainText, refDefinitions, serializeInline, serializeReference } from "./inline"
|
package/src/gitbook/parse.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Block,
|
|
3
|
+
CodeBlockNode,
|
|
3
4
|
ColumnNode,
|
|
4
5
|
CommandNode,
|
|
5
6
|
DemoInlineFile,
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
TabNode,
|
|
16
17
|
UpdateNode,
|
|
17
18
|
} from "./ast"
|
|
19
|
+
import { TAG_ATTRS, parseAttrs } from "./attrs"
|
|
18
20
|
import { footnoteDefinitions, parseInline, plainText, refDefinitions } from "./inline"
|
|
19
21
|
|
|
20
22
|
// Minimal HTML-inline → markdown-inline bridge for HTML table cells.
|
|
@@ -61,22 +63,23 @@ function splitTableRow(row: string): string[] {
|
|
|
61
63
|
return cells
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
const TEMPLATE_RE =
|
|
65
|
-
|
|
66
|
-
function parseAttrs(raw: string | undefined): Record<string, string> {
|
|
67
|
-
const attrs: Record<string, string> = {}
|
|
68
|
-
if (!raw) return attrs
|
|
69
|
-
for (const m of raw.matchAll(/([\w-]+)="([^"]*)"/g)) {
|
|
70
|
-
attrs[m[1]] = m[2]
|
|
71
|
-
}
|
|
72
|
-
return attrs
|
|
73
|
-
}
|
|
66
|
+
const TEMPLATE_RE = new RegExp(String.raw`^\s*\{%\s*(\S+?)(\s+${TAG_ATTRS})?\s*%\}\s*$`)
|
|
74
67
|
|
|
75
68
|
function booleanAttr(attrs: Record<string, string>, key: string): boolean | undefined {
|
|
76
69
|
if (!(key in attrs)) return undefined
|
|
77
70
|
return attrs[key] !== "false"
|
|
78
71
|
}
|
|
79
72
|
|
|
73
|
+
/**
|
|
74
|
+
* A fence's line numbers: `lineNumbers` / `showLineNumbers` when written (remembered as
|
|
75
|
+
* explicit so the attribute round-trips even when it equals the default), otherwise on for
|
|
76
|
+
* titled (file) fences and off for untitled ones.
|
|
77
|
+
*/
|
|
78
|
+
function lineNumbersFrom(attrs: Record<string, string>, titled: boolean): Pick<CodeBlockNode, "lineNumbers" | "lineNumbersExplicit"> {
|
|
79
|
+
const written = booleanAttr(attrs, "lineNumbers") ?? booleanAttr(attrs, "showLineNumbers")
|
|
80
|
+
return written === undefined ? { lineNumbers: titled } : { lineNumbers: written, lineNumbersExplicit: true }
|
|
81
|
+
}
|
|
82
|
+
|
|
80
83
|
function positiveNumberAttr(attrs: Record<string, string>, key: string): number | undefined {
|
|
81
84
|
if (!(key in attrs)) return undefined
|
|
82
85
|
const value = Number(attrs[key])
|
|
@@ -224,7 +227,7 @@ export function parseDemoVariants(raw: string): DemoVariantOption[] {
|
|
|
224
227
|
}
|
|
225
228
|
|
|
226
229
|
/** `{% command %}npm install x{% endcommand %}` on one line. */
|
|
227
|
-
const COMMAND_ONE_LINE_RE =
|
|
230
|
+
const COMMAND_ONE_LINE_RE = new RegExp(String.raw`^\s*\{%\s*command(\s${TAG_ATTRS})?\s*%\}(.*?)\{%\s*endcommand\s*%\}\s*$`)
|
|
228
231
|
|
|
229
232
|
function commandNode(attrs: Record<string, string>, raw: string): CommandNode {
|
|
230
233
|
const overrides: NonNullable<CommandNode["overrides"]> = {}
|
|
@@ -326,6 +329,11 @@ function parseDemoTag(attrs: Record<string, string>): DemoNode {
|
|
|
326
329
|
...(variants.length ? { variants } : {}),
|
|
327
330
|
...((DEMO_VIEWPORTS as string[]).includes(attrs.viewport) ? { viewport: attrs.viewport as DemoViewport } : {}),
|
|
328
331
|
...(attrs.entry ? { entry: attrs.entry } : {}),
|
|
332
|
+
...(attrs.status ? { status: attrs.status } : {}),
|
|
333
|
+
...(booleanAttr(attrs, "bleed") === undefined ? {} : { bleed: booleanAttr(attrs, "bleed") }),
|
|
334
|
+
...(attrs.className ? { className: attrs.className } : {}),
|
|
335
|
+
...(attrs.surface?.trim() ? { surface: attrs.surface } : {}),
|
|
336
|
+
...(attrs.variantsWidth ? { variantsWidth: attrs.variantsWidth } : {}),
|
|
329
337
|
}
|
|
330
338
|
}
|
|
331
339
|
|
|
@@ -435,7 +443,7 @@ export function parseBlocks(lines: string[]): Block[] {
|
|
|
435
443
|
const code = inner.find((b) => b.type === "code")
|
|
436
444
|
if (code && code.type === "code") {
|
|
437
445
|
code.title = tag.attrs.title ?? null
|
|
438
|
-
|
|
446
|
+
Object.assign(code, lineNumbersFrom(tag.attrs, code.title !== null))
|
|
439
447
|
code.live = tag.attrs.live === "true"
|
|
440
448
|
code.entry = tag.attrs.entry ?? null
|
|
441
449
|
blocks.push(code)
|
|
@@ -655,7 +663,9 @@ export function parseBlocks(lines: string[]): Block[] {
|
|
|
655
663
|
const fence = trimmed.match(/^(`{3,}|~{3,})([^\s`]*)?(?:\s+(.*?))?\s*$/)
|
|
656
664
|
if (fence) {
|
|
657
665
|
flushParagraph()
|
|
658
|
-
|
|
666
|
+
// ```` ```title="x" ```` (attributes, no language): the first word is not a language.
|
|
667
|
+
const bareAttrs = !!fence[2]?.includes("=")
|
|
668
|
+
const attrs = parseAttrs(bareAttrs ? [fence[2], fence[3]].filter(Boolean).join(" ") : fence[3])
|
|
659
669
|
const code: string[] = []
|
|
660
670
|
i++
|
|
661
671
|
while (i < lines.length && !lines[i].trim().startsWith(fence[1])) {
|
|
@@ -665,9 +675,9 @@ export function parseBlocks(lines: string[]): Block[] {
|
|
|
665
675
|
i++
|
|
666
676
|
blocks.push({
|
|
667
677
|
type: "code",
|
|
668
|
-
language: fence[2] || null,
|
|
678
|
+
language: (!bareAttrs && fence[2]) || null,
|
|
669
679
|
title: attrs.title ?? null,
|
|
670
|
-
|
|
680
|
+
...lineNumbersFrom(attrs, attrs.title !== undefined),
|
|
671
681
|
code: code.join("\n"),
|
|
672
682
|
...(booleanAttr(attrs, "live") === undefined ? {} : { live: booleanAttr(attrs, "live") }),
|
|
673
683
|
...(attrs.entry ? { entry: attrs.entry } : {}),
|
|
@@ -691,7 +701,7 @@ export function parseBlocks(lines: string[]): Block[] {
|
|
|
691
701
|
code.push(lines[i].slice(4))
|
|
692
702
|
i++
|
|
693
703
|
}
|
|
694
|
-
blocks.push({ type: "code", language: null, title: null, lineNumbers:
|
|
704
|
+
blocks.push({ type: "code", language: null, title: null, lineNumbers: false, code: code.join("\n") })
|
|
695
705
|
continue
|
|
696
706
|
}
|
|
697
707
|
|
package/src/gitbook/serialize.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Block, DemoInlineFile, DemoNode, DocumentNode, Inline, ListItemNode } from "./ast"
|
|
2
|
+
import { attr } from "./attrs"
|
|
2
3
|
import { serializeInline, serializeReference } from "./inline"
|
|
3
4
|
|
|
4
5
|
export function serializeMarkdown(doc: DocumentNode): string {
|
|
@@ -34,36 +35,38 @@ function serializeBlock(b: Block): string {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
case "code": {
|
|
38
|
+
// Line numbers default to on for titled fences, off for untitled ones.
|
|
39
|
+
const numbersByDefault = b.title !== null && b.title !== undefined && b.title !== ""
|
|
37
40
|
const attrs = [
|
|
38
|
-
b.title ?
|
|
39
|
-
|
|
40
|
-
b.live ?
|
|
41
|
-
b.entry ?
|
|
42
|
-
b.collapsedCodeLines ?
|
|
43
|
-
b.expandedCodeLines ?
|
|
44
|
-
].
|
|
45
|
-
const info =
|
|
41
|
+
b.title ? attr("title", b.title) : "",
|
|
42
|
+
b.lineNumbers !== numbersByDefault || b.lineNumbersExplicit ? attr("lineNumbers", b.lineNumbers) : "",
|
|
43
|
+
b.live ? attr("live", true) : "",
|
|
44
|
+
b.entry ? attr("entry", b.entry) : "",
|
|
45
|
+
b.collapsedCodeLines ? attr("collapsedCodeLines", b.collapsedCodeLines) : "",
|
|
46
|
+
b.expandedCodeLines ? attr("expandedCodeLines", b.expandedCodeLines) : "",
|
|
47
|
+
].join("")
|
|
48
|
+
const info = `${b.language ?? ""}${b.language ? attrs : attrs.trimStart()}`
|
|
46
49
|
return `\`\`\`${info}\n${b.code}\n\`\`\``
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
case "hint":
|
|
50
|
-
return `{% hint
|
|
53
|
+
return `{% hint${attr("style", b.style)} %}\n${serializeBlocks(b.children)}\n{% endhint %}`
|
|
51
54
|
|
|
52
55
|
case "tabs": {
|
|
53
56
|
const attrs = [
|
|
54
|
-
b.title ?
|
|
55
|
-
b.title && b.level && b.level !== 2 ?
|
|
56
|
-
b.sync ?
|
|
57
|
+
b.title ? attr("title", b.title) : "",
|
|
58
|
+
b.title && b.level && b.level !== 2 ? attr("level", b.level) : "",
|
|
59
|
+
b.sync ? attr("sync", b.sync) : "",
|
|
57
60
|
].join("")
|
|
58
61
|
return `{% tabs${attrs} %}\n${b.tabs
|
|
59
|
-
.map((t) => `{% tab
|
|
62
|
+
.map((t) => `{% tab${attr("title", t.title)} %}\n${serializeBlocks(t.children)}\n{% endtab %}`)
|
|
60
63
|
.join("\n\n")}\n{% endtabs %}`
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
case "command": {
|
|
64
67
|
const attrs = [
|
|
65
|
-
...(["pnpm", "yarn", "bun"] as const).map((pm) => (b.overrides?.[pm] ?
|
|
66
|
-
b.sync && b.sync !== "pm" ?
|
|
68
|
+
...(["pnpm", "yarn", "bun"] as const).map((pm) => (b.overrides?.[pm] ? attr(pm, b.overrides[pm]) : "")),
|
|
69
|
+
b.sync && b.sync !== "pm" ? attr("sync", b.sync) : "",
|
|
67
70
|
].join("")
|
|
68
71
|
return b.command.includes("\n")
|
|
69
72
|
? `{% command${attrs} %}\n${b.command}\n{% endcommand %}`
|
|
@@ -83,43 +86,48 @@ function serializeBlock(b: Block): string {
|
|
|
83
86
|
|
|
84
87
|
case "embed": {
|
|
85
88
|
const attrs = [
|
|
86
|
-
|
|
87
|
-
b.title ?
|
|
88
|
-
b.poster ?
|
|
89
|
-
b.autoplay === undefined ? "" :
|
|
90
|
-
b.loop === undefined ? "" :
|
|
91
|
-
b.muted === undefined ? "" :
|
|
92
|
-
b.controls === undefined ? "" :
|
|
89
|
+
attr("url", b.url),
|
|
90
|
+
b.title ? attr("title", b.title) : "",
|
|
91
|
+
b.poster ? attr("poster", b.poster) : "",
|
|
92
|
+
b.autoplay === undefined ? "" : attr("autoplay", b.autoplay),
|
|
93
|
+
b.loop === undefined ? "" : attr("loop", b.loop),
|
|
94
|
+
b.muted === undefined ? "" : attr("muted", b.muted),
|
|
95
|
+
b.controls === undefined ? "" : attr("controls", b.controls),
|
|
93
96
|
].join("")
|
|
94
97
|
return `{% embed${attrs} %}`
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
case "content-ref":
|
|
98
|
-
return `{% content-ref
|
|
101
|
+
return `{% content-ref${attr("url", b.url)} %}\n${serializeInline(b.children)}\n{% endcontent-ref %}`
|
|
99
102
|
|
|
100
103
|
case "source-ref": {
|
|
101
104
|
const attrs = [
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
b.title ?
|
|
105
|
+
attr("mount", b.mount),
|
|
106
|
+
attr("path", b.path),
|
|
107
|
+
attr("export", b.exportName),
|
|
108
|
+
attr("kind", b.kind),
|
|
109
|
+
b.title ? attr("title", b.title) : "",
|
|
107
110
|
].join("")
|
|
108
111
|
return `{% source-ref${attrs} %}`
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
case "demo": {
|
|
112
115
|
const attrs = [
|
|
113
|
-
|
|
114
|
-
b.title ?
|
|
115
|
-
b.description ?
|
|
116
|
-
b.height ?
|
|
117
|
-
b.layout ?
|
|
116
|
+
attr("src", b.src),
|
|
117
|
+
b.title ? attr("title", b.title) : "",
|
|
118
|
+
b.description ? attr("description", b.description) : "",
|
|
119
|
+
b.height ? attr("height", b.height) : "",
|
|
120
|
+
b.layout ? attr("layout", b.layout) : "",
|
|
118
121
|
b.variants?.length
|
|
119
|
-
?
|
|
122
|
+
? attr("variants", b.variants.map((v) => (v.label === v.id ? v.id : `${v.id}:${v.label}`)).join(","))
|
|
120
123
|
: "",
|
|
121
|
-
b.viewport ?
|
|
122
|
-
b.entry ?
|
|
124
|
+
b.viewport ? attr("viewport", b.viewport) : "",
|
|
125
|
+
b.entry ? attr("entry", b.entry) : "",
|
|
126
|
+
b.status ? attr("status", b.status) : "",
|
|
127
|
+
b.bleed === undefined ? "" : attr("bleed", b.bleed),
|
|
128
|
+
b.className ? attr("className", b.className) : "",
|
|
129
|
+
b.surface ? attr("surface", b.surface) : "",
|
|
130
|
+
b.variantsWidth ? attr("variantsWidth", b.variantsWidth) : "",
|
|
123
131
|
].join("")
|
|
124
132
|
return `{% demo${attrs} %}${serializeDemoFiles(b)}`
|
|
125
133
|
}
|
|
@@ -167,18 +175,18 @@ function serializeBlock(b: Block): string {
|
|
|
167
175
|
return `$$\n${b.formula}\n$$`
|
|
168
176
|
|
|
169
177
|
case "updates":
|
|
170
|
-
return `{% updates${b.format ?
|
|
178
|
+
return `{% updates${b.format ? attr("format", b.format) : ""} %}\n${b.updates
|
|
171
179
|
.map(
|
|
172
180
|
(u) =>
|
|
173
|
-
`{% update
|
|
181
|
+
`{% update${attr("date", u.date)} %}\n${serializeBlocks(u.children)}\n{% endupdate %}`
|
|
174
182
|
)
|
|
175
183
|
.join("\n\n")}\n{% endupdates %}`
|
|
176
184
|
|
|
177
185
|
case "openapi-operation": {
|
|
178
186
|
const attrs = [
|
|
179
|
-
b.spec ?
|
|
180
|
-
b.path ?
|
|
181
|
-
b.method ?
|
|
187
|
+
b.spec ? attr("spec", b.spec) : "",
|
|
188
|
+
b.path ? attr("path", b.path) : "",
|
|
189
|
+
b.method ? attr("method", b.method) : "",
|
|
182
190
|
].join("")
|
|
183
191
|
const inner = b.specUrl ? `\n[${b.label || b.spec || "OpenAPI"}](${b.specUrl})` : ""
|
|
184
192
|
return `{% openapi-operation${attrs} %}${inner}\n{% endopenapi-operation %}`
|
|
@@ -225,7 +233,7 @@ export function fenceFor(content: string): string {
|
|
|
225
233
|
/** One inline demo file as a titled fence. */
|
|
226
234
|
export function serializeDemoFile(file: DemoInlineFile): string {
|
|
227
235
|
const fence = fenceFor(file.content)
|
|
228
|
-
return `${fence}${file.language ?? ""}
|
|
236
|
+
return `${fence}${file.language ?? ""}${attr("title", file.path)}\n${file.content}\n${fence}`
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
/** The body of a block-form `{% demo %}`: files as titled fences, then `{% enddemo %}`. Empty without files. */
|
package/src/styles.css
CHANGED
|
@@ -1530,9 +1530,13 @@
|
|
|
1530
1530
|
margin-bottom: 12px;
|
|
1531
1531
|
}
|
|
1532
1532
|
|
|
1533
|
-
:
|
|
1533
|
+
/* The heading sits centred on the switch's row: no block margin, padding or rule of its own
|
|
1534
|
+
(page heading rules — docstream's and a host's `h2 { padding-top }` — would push it off
|
|
1535
|
+
centre; 0,4,0 outranks them). */
|
|
1536
|
+
:is([data-docstream], .docs-article) .docs-tabs-section > .docs-tabs-section-head > .docs-tabs-section-title {
|
|
1537
|
+
align-self: center;
|
|
1534
1538
|
margin: 0;
|
|
1535
|
-
padding-
|
|
1539
|
+
padding-block: 0;
|
|
1536
1540
|
border-bottom: 0;
|
|
1537
1541
|
scroll-margin-top: 80px;
|
|
1538
1542
|
}
|
|
@@ -1543,15 +1547,18 @@
|
|
|
1543
1547
|
gap: 10px;
|
|
1544
1548
|
}
|
|
1545
1549
|
|
|
1546
|
-
.
|
|
1550
|
+
/* One 10px rhythm inside a section. These must outrank the generic block spacing
|
|
1551
|
+
(`:is([data-docstream], .docs-article) [data-docstream-blocks] > :is(.docs-code, …)`,
|
|
1552
|
+
headings and demos, specificity 0,3,0), hence the extra `.docs-tabs-section >` (0,4,0). */
|
|
1553
|
+
:is([data-docstream], .docs-article) .docs-tabs-section > .docs-tabs-section-body > [data-docstream-blocks] > * {
|
|
1547
1554
|
margin-block: 0 10px;
|
|
1548
1555
|
}
|
|
1549
1556
|
|
|
1550
|
-
.docs-tabs-section-body > [data-docstream-blocks] > :last-child {
|
|
1557
|
+
:is([data-docstream], .docs-article) .docs-tabs-section > .docs-tabs-section-body > [data-docstream-blocks] > :last-child {
|
|
1551
1558
|
margin-bottom: 0;
|
|
1552
1559
|
}
|
|
1553
1560
|
|
|
1554
|
-
.docs-tabs-section-body > [data-docstream-blocks] > p {
|
|
1561
|
+
:is([data-docstream], .docs-article) .docs-tabs-section > .docs-tabs-section-body > [data-docstream-blocks] > p {
|
|
1555
1562
|
color: var(--gb-muted-foreground);
|
|
1556
1563
|
font-size: 0.93em;
|
|
1557
1564
|
}
|
|
@@ -1945,35 +1952,36 @@
|
|
|
1945
1952
|
opacity: 1;
|
|
1946
1953
|
}
|
|
1947
1954
|
|
|
1948
|
-
/*
|
|
1949
|
-
|
|
1955
|
+
/* The canvas is block flow, so a block-level demo root lays out exactly as it would on a
|
|
1956
|
+
page: it fills the width, and `max-width: 480px; margin: 0 auto` centres it at 480px (a
|
|
1957
|
+
grid or flex canvas would shrink it to its content). Intrinsically sized roots — inline /
|
|
1958
|
+
inline-block / inline-flex elements such as a lone button, badge or image — sit on the
|
|
1959
|
+
canvas's centred line box. Content is centred vertically (`align-content` on a block
|
|
1960
|
+
container). The canvas's `text-align: center` stops at its children: the reset below has
|
|
1961
|
+
zero specificity, so any rule of the demo's own wins. */
|
|
1950
1962
|
.docs-demo-canvas {
|
|
1951
|
-
display:
|
|
1963
|
+
display: block;
|
|
1952
1964
|
align-content: center;
|
|
1953
|
-
justify-items: stretch;
|
|
1954
1965
|
box-sizing: border-box;
|
|
1955
1966
|
min-height: 100%;
|
|
1956
1967
|
padding: 28px 24px;
|
|
1957
1968
|
color: var(--foreground, var(--gb-panel-foreground));
|
|
1969
|
+
text-align: center;
|
|
1958
1970
|
}
|
|
1959
1971
|
|
|
1960
|
-
.docs-demo-canvas > * {
|
|
1961
|
-
min-width: 0;
|
|
1972
|
+
:where(.docs-demo-canvas) > * {
|
|
1962
1973
|
max-width: 100%;
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
.docs-demo-canvas > :where(button, a, img, svg, video, canvas, input, select, textarea, label, span, code) {
|
|
1966
|
-
justify-self: center;
|
|
1974
|
+
text-align: start;
|
|
1967
1975
|
}
|
|
1968
1976
|
|
|
1969
1977
|
.docs-demo-canvas-bleed {
|
|
1970
|
-
|
|
1971
|
-
place-items: normal;
|
|
1978
|
+
align-content: normal;
|
|
1972
1979
|
height: 100%;
|
|
1973
1980
|
padding: 0;
|
|
1981
|
+
text-align: start;
|
|
1974
1982
|
}
|
|
1975
1983
|
|
|
1976
|
-
.docs-demo-canvas-bleed > * {
|
|
1984
|
+
:where(.docs-demo-canvas-bleed) > * {
|
|
1977
1985
|
max-width: none;
|
|
1978
1986
|
}
|
|
1979
1987
|
|
|
@@ -2241,9 +2249,11 @@
|
|
|
2241
2249
|
background: var(--gb-hover);
|
|
2242
2250
|
}
|
|
2243
2251
|
|
|
2252
|
+
/* Sizes to its content; a `height` in meta or on the tag sets a minimum (inline style), and
|
|
2253
|
+
hosts may set a floor for every single-file preview with --docs-demo-preview-min-height. */
|
|
2244
2254
|
.docs-demo-single-preview {
|
|
2245
2255
|
display: grid;
|
|
2246
|
-
min-height:
|
|
2256
|
+
min-height: var(--docs-demo-preview-min-height, 0px);
|
|
2247
2257
|
background: var(--background, var(--gb-panel));
|
|
2248
2258
|
}
|
|
2249
2259
|
|