@takazudo/zudo-doc 2.0.1 → 2.1.1

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.
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+ // @takazudo/zudo-doc/bin/gen-component-tokens.mjs
3
+ //
4
+ // Package bin: regenerate (or check) the BEGIN/END-marked --zdc-* blocks from
5
+ // the single source of truth in packages/zudo-doc/src/config/component-tokens.ts.
6
+ //
7
+ // Each token declares a `surface`, and the generator ROUTES its backing rule to
8
+ // the matching stylesheet:
9
+ // - surface "content" → block in packages/zudo-doc/src/content.css
10
+ // - surface "chrome" → block in packages/zudo-doc/src/features.css
11
+ // A token missing `surface` is a hard error (the parser throws).
12
+ //
13
+ // Paths are resolved relative to this file's location (the package root),
14
+ // NOT process.cwd() — this tool operates on the package's own source files
15
+ // regardless of the working directory from which it is invoked.
16
+ //
17
+ // Usage (after pnpm install, via scripts in the root package.json):
18
+ // gen-component-tokens # rewrite the --zdc-* blocks in both files
19
+ // gen-component-tokens --check # verify committed blocks are up to date (exit 1 on drift)
20
+ //
21
+ // CI invokes the file by path from the repo root:
22
+ // node packages/zudo-doc/bin/gen-component-tokens.mjs --check
23
+ //
24
+ // Each block is a set of --zdc-* CSS custom-property rules grouped by selector,
25
+ // giving consumers a single `:root` override point to rebrand component-level
26
+ // typography/chrome without touching JSX or fighting specificity.
27
+ //
28
+ // Pure Node (fs only — NO npm deps). Idempotent: running twice produces no diff.
29
+ //
30
+ // MAINTENANCE: edit packages/zudo-doc/src/config/component-tokens.ts (the source
31
+ // of truth), then run `pnpm gen:component-tokens` and commit the regenerated
32
+ // content.css / features.css. Never hand-edit the block between the BEGIN/END
33
+ // markers.
34
+
35
+ import { readFileSync, writeFileSync, realpathSync } from "node:fs";
36
+ import { resolve, dirname } from "node:path";
37
+ import { fileURLToPath } from "node:url";
38
+
39
+ // Paths are relative to this bin file's location so the generator works
40
+ // regardless of cwd (running from the repo root, package dir, or CI).
41
+ const __dirname = dirname(fileURLToPath(import.meta.url));
42
+ const PKG_ROOT = resolve(__dirname, "..");
43
+
44
+ const TOKENS_PATH = resolve(PKG_ROOT, "src/config/component-tokens.ts");
45
+ const CONTENT_CSS_PATH = resolve(PKG_ROOT, "src/content.css");
46
+ const FEATURES_CSS_PATH = resolve(PKG_ROOT, "src/features.css");
47
+
48
+ // Default marker pair = the content-surface block. Kept under the original
49
+ // names so buildBlock/replaceBlock default to the content block, preserving the
50
+ // byte-identical content.css output (and the existing unit-test call sites).
51
+ const BEGIN_MARKER =
52
+ "/* BEGIN --zdc-* component tokens (generated by gen-component-tokens; do not edit by hand) */";
53
+ const END_MARKER = "/* END --zdc-* component tokens */";
54
+
55
+ // Chrome-surface block markers — a DISTINCT pair so the two blocks never
56
+ // collide when both live in files that the generator scans.
57
+ const CHROME_BEGIN_MARKER =
58
+ "/* BEGIN --zdc-* chrome component tokens (generated by gen-component-tokens; do not edit by hand) */";
59
+ const CHROME_END_MARKER = "/* END --zdc-* chrome component tokens */";
60
+
61
+ /**
62
+ * Surface routing table — source order is the file-write order. Each entry maps
63
+ * a token `surface` to its destination stylesheet, marker pair, and a
64
+ * repo-relative path used in console/drift messages.
65
+ */
66
+ const SURFACES = [
67
+ {
68
+ surface: "content",
69
+ cssPath: CONTENT_CSS_PATH,
70
+ relPath: "packages/zudo-doc/src/content.css",
71
+ beginMarker: BEGIN_MARKER,
72
+ endMarker: END_MARKER,
73
+ },
74
+ {
75
+ surface: "chrome",
76
+ cssPath: FEATURES_CSS_PATH,
77
+ relPath: "packages/zudo-doc/src/features.css",
78
+ beginMarker: CHROME_BEGIN_MARKER,
79
+ endMarker: CHROME_END_MARKER,
80
+ },
81
+ ];
82
+
83
+ /** The set of valid `surface` values, derived from the routing table. */
84
+ const VALID_SURFACES = new Set(SURFACES.map((s) => s.surface));
85
+
86
+ /**
87
+ * Parse the COMPONENT_TOKENS array out of component-tokens.ts WITHOUT importing
88
+ * it (this bin is a dependency-free .mjs and cannot resolve TypeScript). Reads
89
+ * each `{ cssVar: "...", selector: "...", property: "...", default: "...",
90
+ * surface: "...", ... }` object literal. Throws on a malformed source — or a
91
+ * token missing the required `surface` field — so drift between the parser and
92
+ * the file surfaces loudly.
93
+ *
94
+ * Returns an array of { cssVar, selector, property, default, surface } objects
95
+ * in source order — iteration order is the CSS rule order.
96
+ *
97
+ * Exported for unit testing.
98
+ */
99
+ export function parseTokens(src) {
100
+ const arrayMatch = src.match(
101
+ /export const COMPONENT_TOKENS[^=]*=\s*\[([\s\S]*?)\];/,
102
+ );
103
+ if (!arrayMatch) {
104
+ throw new Error(
105
+ `Could not locate "export const COMPONENT_TOKENS = [ ... ]" in ${TOKENS_PATH}`,
106
+ );
107
+ }
108
+ const body = arrayMatch[1];
109
+ const tokens = [];
110
+
111
+ // Each token is a `{ ... }` object literal; iterate top-level braces.
112
+ // Description strings may contain backticks and parentheses but not braces,
113
+ // so lazy `{...}` matching correctly isolates each object.
114
+ const objectRe = /\{([\s\S]*?)\}/g;
115
+ let m;
116
+ while ((m = objectRe.exec(body)) !== null) {
117
+ const obj = m[1];
118
+ const cssVarMatch = obj.match(/cssVar:\s*"([^"]+)"/);
119
+ const selectorMatch = obj.match(/selector:\s*"([^"]+)"/);
120
+ const propertyMatch = obj.match(/property:\s*"([^"]+)"/);
121
+ const defaultMatch = obj.match(/default:\s*"([^"]+)"/);
122
+
123
+ if (!cssVarMatch || !selectorMatch || !propertyMatch || !defaultMatch) {
124
+ throw new Error(
125
+ `Malformed token object in COMPONENT_TOKENS (missing cssVar/selector/property/default): ${obj.trim()}`,
126
+ );
127
+ }
128
+
129
+ // `surface` is REQUIRED — it routes the rule to content.css vs features.css.
130
+ // Fail loudly rather than silently dropping a token from both files.
131
+ const surfaceMatch = obj.match(/surface:\s*"([^"]+)"/);
132
+ if (!surfaceMatch) {
133
+ throw new Error(
134
+ `Token ${cssVarMatch[1]} in COMPONENT_TOKENS is missing the required "surface" field ` +
135
+ `(expected surface: "content" | "chrome").`,
136
+ );
137
+ }
138
+
139
+ tokens.push({
140
+ cssVar: cssVarMatch[1],
141
+ selector: selectorMatch[1],
142
+ property: propertyMatch[1],
143
+ default: defaultMatch[1],
144
+ surface: surfaceMatch[1],
145
+ });
146
+ }
147
+
148
+ if (tokens.length === 0) {
149
+ throw new Error(
150
+ `COMPONENT_TOKENS in ${TOKENS_PATH} parsed to an empty list`,
151
+ );
152
+ }
153
+ return tokens;
154
+ }
155
+
156
+ /**
157
+ * Group tokens by selector, preserving the first-appearance order of selectors
158
+ * and the within-selector declaration order. Deterministic (source-order stable).
159
+ *
160
+ * Exported for unit testing.
161
+ */
162
+ export function groupBySelector(tokens) {
163
+ const map = new Map();
164
+ for (const token of tokens) {
165
+ if (!map.has(token.selector)) {
166
+ map.set(token.selector, []);
167
+ }
168
+ map.get(token.selector).push(token);
169
+ }
170
+ return map;
171
+ }
172
+
173
+ /**
174
+ * Partition tokens by their `surface`, returning a Map keyed by every known
175
+ * surface (in routing-table order) so callers can emit an EMPTY block for a
176
+ * surface that currently has no tokens (e.g. the seeded chrome block). Throws
177
+ * loudly on an unknown surface value — a routing typo must never silently drop
178
+ * a rule from both files.
179
+ *
180
+ * Exported for unit testing.
181
+ */
182
+ export function routeBySurface(tokens) {
183
+ const routes = new Map(SURFACES.map((s) => [s.surface, []]));
184
+ for (const token of tokens) {
185
+ if (!routes.has(token.surface)) {
186
+ throw new Error(
187
+ `Token ${token.cssVar} in COMPONENT_TOKENS has unknown surface "${token.surface}" ` +
188
+ `(expected one of: ${[...VALID_SURFACES].map((s) => `"${s}"`).join(", ")}).`,
189
+ );
190
+ }
191
+ routes.get(token.surface).push(token);
192
+ }
193
+ return routes;
194
+ }
195
+
196
+ /**
197
+ * Build the full generated block (markers included). Selectors are emitted in
198
+ * source-file order; declarations within each selector are in source-file order.
199
+ * No trailing newline — the caller appends css.slice(lineEnd) which starts with
200
+ * the \n that follows the END marker line.
201
+ *
202
+ * `beginMarker`/`endMarker` default to the CONTENT block markers so existing
203
+ * call sites keep producing the byte-identical content.css block; the chrome
204
+ * surface passes its own marker pair. An empty `tokens` array yields just the
205
+ * marker pair separated by a blank line (the seeded-empty chrome block).
206
+ *
207
+ * Exported for unit testing.
208
+ */
209
+ export function buildBlock(tokens, beginMarker = BEGIN_MARKER, endMarker = END_MARKER) {
210
+ const groups = groupBySelector(tokens);
211
+ const lines = [];
212
+
213
+ lines.push(beginMarker);
214
+
215
+ for (const [selector, entries] of groups) {
216
+ lines.push("");
217
+ lines.push(`${selector} {`);
218
+ for (const entry of entries) {
219
+ lines.push(` ${entry.property}: var(${entry.cssVar}, ${entry.default});`);
220
+ }
221
+ lines.push("}");
222
+ }
223
+
224
+ lines.push("");
225
+ lines.push(endMarker);
226
+
227
+ return lines.join("\n");
228
+ }
229
+
230
+ /**
231
+ * Replace the existing BEGIN…END block in `css` with `block`. Throws if the
232
+ * markers are missing (the block must be seeded once by hand — see content.css
233
+ * / features.css). `beginMarker`/`endMarker` default to the content block; the
234
+ * chrome surface passes its own pair. `cssPath` only feeds the error message.
235
+ *
236
+ * Exported for unit testing.
237
+ */
238
+ export function replaceBlock(
239
+ css,
240
+ block,
241
+ beginMarker = BEGIN_MARKER,
242
+ endMarker = END_MARKER,
243
+ cssPath = CONTENT_CSS_PATH,
244
+ ) {
245
+ const beginIdx = css.indexOf(beginMarker);
246
+ const endIdx = css.indexOf(endMarker);
247
+ if (beginIdx === -1 || endIdx === -1) {
248
+ throw new Error(
249
+ `Could not find BEGIN/END markers in ${cssPath}.\n` +
250
+ `Seed the marker block once by hand, then re-run the generator.`,
251
+ );
252
+ }
253
+ // Expand to the full line that opens the block and the end of the closing
254
+ // line so the whole region (including any per-selector comments the hand-
255
+ // written block may have had) is replaced cleanly.
256
+ const lineStart = css.lastIndexOf("\n", beginIdx) + 1;
257
+ const afterEnd = css.indexOf("\n", endIdx);
258
+ const lineEnd = afterEnd === -1 ? css.length : afterEnd;
259
+ return css.slice(0, lineStart) + block + css.slice(lineEnd);
260
+ }
261
+
262
+ function main() {
263
+ const check = process.argv.includes("--check");
264
+
265
+ const tokensSrc = readFileSync(TOKENS_PATH, "utf8");
266
+ const tokens = parseTokens(tokensSrc);
267
+ const routes = routeBySurface(tokens);
268
+
269
+ let drift = false;
270
+ let wroteAny = false;
271
+ const summaries = [];
272
+
273
+ for (const target of SURFACES) {
274
+ const surfaceTokens = routes.get(target.surface);
275
+ const selectorCount = [...groupBySelector(surfaceTokens).keys()].length;
276
+ summaries.push(
277
+ `${target.surface}: ${surfaceTokens.length} token(s), ${selectorCount} selector(s)`,
278
+ );
279
+
280
+ const css = readFileSync(target.cssPath, "utf8");
281
+ const block = buildBlock(surfaceTokens, target.beginMarker, target.endMarker);
282
+ const next = replaceBlock(
283
+ css,
284
+ block,
285
+ target.beginMarker,
286
+ target.endMarker,
287
+ target.cssPath,
288
+ );
289
+
290
+ if (next === css) continue;
291
+
292
+ if (check) {
293
+ console.error(
294
+ `component-tokens codegen drift detected: ${target.relPath} (${target.surface} surface) is out of date.`,
295
+ );
296
+ drift = true;
297
+ } else {
298
+ writeFileSync(target.cssPath, next);
299
+ wroteAny = true;
300
+ console.log(
301
+ `Wrote --zdc-* ${target.surface} component token block to ${target.relPath} ` +
302
+ `(${surfaceTokens.length} token(s), ${selectorCount} selector(s)).`,
303
+ );
304
+ }
305
+ }
306
+
307
+ if (check) {
308
+ if (drift) {
309
+ console.error("Run `pnpm gen:component-tokens` and commit the result.");
310
+ return 1;
311
+ }
312
+ console.log(
313
+ `OK — --zdc-* component token blocks up to date [${summaries.join("; ")}].`,
314
+ );
315
+ return 0;
316
+ }
317
+
318
+ if (!wroteAny) {
319
+ console.log(
320
+ `--zdc-* component token blocks already up to date [${summaries.join("; ")}]; no change.`,
321
+ );
322
+ }
323
+ return 0;
324
+ }
325
+
326
+ // Run the CLI only when executed directly, NOT when imported by tests (an
327
+ // import must not rewrite content.css as a side effect — it would break
328
+ // `pnpm test`). Compare REAL paths on both sides: when invoked through the
329
+ // pnpm bin shim (root `pnpm gen:/check:component-tokens`, b4push), argv[1] is
330
+ // the `node_modules/.bin/…` symlink, NOT the real workspace file, so a raw
331
+ // path-equality check is always false and main() would silently no-op — the
332
+ // drift guard would pass even on real drift. realpathSync resolves the shim/
333
+ // symlink to the real file on both sides so direct invocation is detected
334
+ // however the bin is launched.
335
+ function isDirectInvocation() {
336
+ if (!process.argv[1]) return false;
337
+ try {
338
+ return (
339
+ realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])
340
+ );
341
+ } catch {
342
+ return false;
343
+ }
344
+ }
345
+
346
+ if (isDirectInvocation()) {
347
+ process.exit(main());
348
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Assert that `ctx` is a fully-wired 2.0 ChromeContext (i.e. has `hostBindings`).
3
+ *
4
+ * Throws an actionable Error when the caller passes a bare pre-2.0 deps-bag
5
+ * instead of the unified ChromeContext the 2.0 factories require. This catches
6
+ * runtime crashes (`Cannot read properties of undefined (reading 'tagVocabulary')`)
7
+ * at the factory boundary instead of deep inside the component render.
8
+ *
9
+ * See packages/zudo-doc/API.md — "Migration Notes" for upgrade instructions.
10
+ */
11
+ export declare function assertChromeContext(ctx: unknown, factoryName: string): void;
@@ -0,0 +1,10 @@
1
+ function assertChromeContext(ctx, factoryName) {
2
+ if (ctx == null || ctx.hostBindings == null) {
3
+ throw new Error(
4
+ `\`${factoryName}\` received a context without \`hostBindings\` \u2014 the 2.0 factories take a single ChromeContext, not the pre-2.0 deps-bag. See packages/zudo-doc/API.md.`
5
+ );
6
+ }
7
+ }
8
+ export {
9
+ assertChromeContext
10
+ };
@@ -0,0 +1,56 @@
1
+ /** Grouping category for a component token (drives docs/registry sectioning). */
2
+ export type ComponentTokenCategory = "typography" | "shape" | "layout";
3
+ /**
4
+ * Which rendering surface a token's backing rule targets — drives WHERE the
5
+ * generator emits the rule:
6
+ * - `content` → the `.zd-content` typography surface; rule lands in the
7
+ * BEGIN/END block in `src/content.css` (shipped as `…/content.css`).
8
+ * - `chrome` → the app shell (header/footer/sidebar/toc/cards); rule lands
9
+ * in the BEGIN/END block in `src/features.css` (shipped as `…/features.css`).
10
+ * The two surfaces ship as separate stylesheets, so a token MUST declare its
11
+ * surface — the generator routes by it and fails loudly when it is absent.
12
+ */
13
+ export type ComponentTokenSurface = "content" | "chrome";
14
+ /**
15
+ * Union of every `--zdc-*` CSS custom property name in the registry.
16
+ * Provides autocomplete when a consumer iterates or references individual tokens.
17
+ * Must be kept in sync with the `cssVar` values in `COMPONENT_TOKENS` below.
18
+ */
19
+ export type ComponentTokenName = "--zdc-doc-title-font" | "--zdc-doc-title-weight" | "--zdc-doc-title-tracking" | "--zdc-doc-h2-font" | "--zdc-doc-h2-weight" | "--zdc-doc-h2-tracking" | "--zdc-doc-h3-weight" | "--zdc-doc-h4-weight" | "--zdc-doc-prose-font" | "--zdc-doc-link-decoration" | "--zdc-admonition-radius" | "--zdc-admonition-border-width" | "--zdc-card-radius" | "--zdc-content-max-width" | "--zdc-toc-width" | "--zdc-nav-active-indicator-color" | "--zdc-nav-active-weight";
20
+ export interface ComponentToken {
21
+ /**
22
+ * The CSS custom-property name the consumer redefines in `:root`, e.g.
23
+ * `--zdc-doc-title-font`. Always `--zdc-`-prefixed.
24
+ */
25
+ cssVar: `--zdc-${string}`;
26
+ /**
27
+ * The already-rendered, component-anchored selector the backing rule targets
28
+ * (the classes the component already emits — no JSX change). Tokens sharing a
29
+ * selector are emitted into a single CSS rule by the generator.
30
+ */
31
+ selector: string;
32
+ /** The CSS property this token drives, e.g. `font-family`. */
33
+ property: string;
34
+ /**
35
+ * Default value used as the `var(<cssVar>, <default>)` fallback. SHOULD chain
36
+ * to an existing design token or `inherit` so consumer `@theme` overrides flow
37
+ * through. Bare CSS literals (e.g. `4px`, `underline`) are allowed only when
38
+ * no design token exists for the property — document the exception in the
39
+ * snapshot test. The rendered default MUST be byte-identical to the pre-token
40
+ * output in every case.
41
+ */
42
+ default: string;
43
+ /** The component this token rebrands (e.g. `doc-title`). */
44
+ component: string;
45
+ /**
46
+ * Rendering surface the backing rule targets. The generator routes the rule
47
+ * to `content.css` (`content`) or `features.css` (`chrome`) by this field.
48
+ * Required — codegen throws if it is missing.
49
+ */
50
+ surface: ComponentTokenSurface;
51
+ /** Grouping category. */
52
+ category: ComponentTokenCategory;
53
+ /** Human-readable description for the registry/docs. */
54
+ description: string;
55
+ }
56
+ export declare const COMPONENT_TOKENS: ComponentToken[];
@@ -0,0 +1,262 @@
1
+ const COMPONENT_TOKENS = [
2
+ {
3
+ cssVar: "--zdc-doc-title-font",
4
+ selector: "h1.text-heading",
5
+ property: "font-family",
6
+ default: "inherit",
7
+ component: "doc-title",
8
+ surface: "content",
9
+ category: "typography",
10
+ description: "Font family of the doc-page title <h1> (and the other page-title h1s that share `text-heading`). Defaults to `inherit` so the title keeps the host's brand heading font; redefine in :root to rebrand."
11
+ },
12
+ {
13
+ cssVar: "--zdc-doc-title-weight",
14
+ selector: "h1.text-heading",
15
+ property: "font-weight",
16
+ default: "var(--font-weight-bold)",
17
+ component: "doc-title",
18
+ surface: "content",
19
+ category: "typography",
20
+ description: "Font weight of the doc-page title <h1> (and the other page-title h1s that share `text-heading`). Defaults to the `--font-weight-bold` token (matching the original `font-bold` utility)."
21
+ },
22
+ // `--tracking-normal: normal` (S2 scale token) — letter-spacing chains to the
23
+ // global scale so a consumer can override the entire scale in one place.
24
+ // Byte-identical because `normal` is the browser default for letter-spacing.
25
+ {
26
+ cssVar: "--zdc-doc-title-tracking",
27
+ selector: "h1.text-heading",
28
+ property: "letter-spacing",
29
+ default: "var(--tracking-normal)",
30
+ component: "doc-title",
31
+ surface: "content",
32
+ category: "typography",
33
+ description: "Letter spacing of the doc-page title <h1> (and the other page-title h1s that share `text-heading`). Defaults to `var(--tracking-normal)` (resolves to `normal` \u2014 byte-identical to the browser default); redefine in :root or tighten via `--tracking-tight`."
34
+ },
35
+ // ── Content heading h2 (HeadingH2 component, selector census: #2449) ───────
36
+ // `h2.text-title` matches every h2 with the `text-title` size role in the
37
+ // package: HeadingH2 content override (primary target), plus tag-section
38
+ // headings on index/locale-index pages, versions-page section headers, and
39
+ // modal headings (ai-chat, color-tweak-export). All share identical
40
+ // `text-title font-bold` styling — a semantically-consistent superset
41
+ // analogous to `h1.text-heading`. Byte-identical defaults produce zero
42
+ // computed change; a `:root` override rebrands all consistently.
43
+ {
44
+ cssVar: "--zdc-doc-h2-font",
45
+ selector: "h2.text-title",
46
+ property: "font-family",
47
+ default: "inherit",
48
+ component: "heading-h2",
49
+ surface: "content",
50
+ category: "typography",
51
+ description: "Font family of content h2 headings (HeadingH2 component and other h2s sharing `text-title`). Defaults to `inherit` so the heading keeps the host's brand font; redefine in :root to rebrand."
52
+ },
53
+ {
54
+ cssVar: "--zdc-doc-h2-weight",
55
+ selector: "h2.text-title",
56
+ property: "font-weight",
57
+ default: "var(--font-weight-bold)",
58
+ component: "heading-h2",
59
+ surface: "content",
60
+ category: "typography",
61
+ description: "Font weight of content h2 headings (HeadingH2 component and other h2s sharing `text-title`). Defaults to the `--font-weight-bold` token (matching the original `font-bold` utility)."
62
+ },
63
+ {
64
+ cssVar: "--zdc-doc-h2-tracking",
65
+ selector: "h2.text-title",
66
+ property: "letter-spacing",
67
+ default: "var(--tracking-normal)",
68
+ component: "heading-h2",
69
+ surface: "content",
70
+ category: "typography",
71
+ description: "Letter spacing of content h2 headings (HeadingH2 component and other h2s sharing `text-title`). Defaults to `var(--tracking-normal)` (byte-identical to the browser default); redefine in :root to tighten or widen."
72
+ },
73
+ // ── Content heading h3 (HeadingH3 component, selector census: #2449) ───────
74
+ // `h3.text-body.font-bold` is unique to the HeadingH3 component — no other
75
+ // h3 in the package emits both `text-body` and `font-bold`.
76
+ {
77
+ cssVar: "--zdc-doc-h3-weight",
78
+ selector: "h3.text-body.font-bold",
79
+ property: "font-weight",
80
+ default: "var(--font-weight-bold)",
81
+ component: "heading-h3",
82
+ surface: "content",
83
+ category: "typography",
84
+ description: "Font weight of content h3 headings (HeadingH3 component). Defaults to the `--font-weight-bold` token (matching the original `font-bold` utility)."
85
+ },
86
+ // ── Content heading h4 (HeadingH4 component, selector census: #2449) ───────
87
+ // `h4.text-body.font-semibold` is unique to the HeadingH4 component — no
88
+ // other h4 in the package emits both `text-body` and `font-semibold`.
89
+ {
90
+ cssVar: "--zdc-doc-h4-weight",
91
+ selector: "h4.text-body.font-semibold",
92
+ property: "font-weight",
93
+ default: "var(--font-weight-semibold)",
94
+ component: "heading-h4",
95
+ surface: "content",
96
+ category: "typography",
97
+ description: "Font weight of content h4 headings (HeadingH4 component). Defaults to the `--font-weight-semibold` token (matching the original `font-semibold` utility)."
98
+ },
99
+ // ── Content prose font (.zd-content, #2460) ────────────────────────────────
100
+ // Sets font-family on the content root so every element inside inherits it by
101
+ // default. The headings whose `--zdc-doc-*-font` default to `inherit` will
102
+ // inherit this value — safe because the current computed heading family already
103
+ // resolves to `var(--font-sans)` (Tailwind preflight sets it on `html`).
104
+ // Byte-identical: `.zd-content` currently carries no explicit font-family, so
105
+ // setting it to `var(--font-sans)` produces the same computed value.
106
+ {
107
+ cssVar: "--zdc-doc-prose-font",
108
+ selector: ".zd-content",
109
+ property: "font-family",
110
+ default: "var(--font-sans)",
111
+ component: "doc-prose",
112
+ surface: "content",
113
+ category: "typography",
114
+ description: "Font family of the prose content area (.zd-content). Defaults to `var(--font-sans)` (the inherited font \u2014 byte-identical); redefine in :root to use a distinct body font for doc content. Headings with `--zdc-doc-*-font: inherit` inherit this value."
115
+ },
116
+ // ── Content link (ContentLink component, styled path, #2460) ─────────────
117
+ // ContentLink returns early (unstyled) for `block` and `hash-link` classes;
118
+ // the styled path emits `text-accent underline hover:text-accent-hover`.
119
+ // Selector `a.text-accent.underline` (specificity 0,2,1) beats the `.underline`
120
+ // utility (0,1,0) and only matches the styled anchor — not block or hash-link
121
+ // variants. Default `underline` is a CSS keyword literal: no text-decoration
122
+ // token exists in the scale (documented exception to the prefer-var-() rule).
123
+ {
124
+ cssVar: "--zdc-doc-link-decoration",
125
+ selector: "a.text-accent.underline",
126
+ property: "text-decoration",
127
+ default: "underline",
128
+ component: "content-link",
129
+ surface: "content",
130
+ category: "typography",
131
+ description: "Text decoration of styled content links (the `text-accent underline` path in ContentLink; excludes block and hash-link variants). Defaults to `underline` (byte-identical to the current utility). Set to `none` to remove underlines from content links."
132
+ },
133
+ // ── Admonition block ([data-admonition], content-admonition.tsx, #2460) ───
134
+ // The base `[data-admonition]` rule in content.css has:
135
+ // border-left: 4px solid var(--color-muted);
136
+ // border-radius: 0 var(--radius-DEFAULT) var(--radius-DEFAULT) 0;
137
+ // Both properties are tokenized below. The token rules land in the BEGIN/END
138
+ // block (after the base rule in source order), so at equal specificity the
139
+ // token defaults override the originals — byte-identical because the defaults
140
+ // ARE the original values.
141
+ // Exception: `4px` is a bare literal — no border-width scale token exists.
142
+ {
143
+ cssVar: "--zdc-admonition-radius",
144
+ selector: "[data-admonition]",
145
+ property: "border-radius",
146
+ default: "0 var(--radius-DEFAULT) var(--radius-DEFAULT) 0",
147
+ component: "admonition",
148
+ surface: "content",
149
+ category: "shape",
150
+ description: "Border radius of the admonition/callout block. Defaults to the current value (0 on the left edges, `--radius-DEFAULT` on the right \u2014 preserving the flush-left accent look); redefine to round all corners or square them off."
151
+ },
152
+ {
153
+ cssVar: "--zdc-admonition-border-width",
154
+ selector: "[data-admonition]",
155
+ property: "border-left-width",
156
+ default: "4px",
157
+ component: "admonition",
158
+ surface: "content",
159
+ category: "shape",
160
+ description: "Width of the admonition left accent border. Defaults to `4px` (byte-identical to the current `border-left: 4px solid` rule). No border-width scale token exists \u2014 bare literal is the documented exception."
161
+ },
162
+ // ── Card radius (nav-indexing card grids, chrome, #2461) ──────────────────
163
+ // Selector census: `a.group.block` also appears in search-widget-script
164
+ // (search result link: `class="group block px-hsp-lg py-vsp-sm ..."`).
165
+ // Adding `.rounded` (present on all three card anchors — doc-card-grid,
166
+ // nav-card-grid, category-nav — and absent from the search result link)
167
+ // makes the selector unique to cards. `a.group.block.rounded` specificity
168
+ // 0,3,1 beats `.rounded` utility (0,1,0); unlayered beats layered (@layer
169
+ // utilities). No JSX change needed — cards already emit `rounded`.
170
+ // `--zdc-surface-radius` is a consumer meta-knob: NOT a registry entry
171
+ // (no single backing selector); it lives only in this default chain so a
172
+ // consumer can set `--zdc-surface-radius` in :root to round all surfaces.
173
+ {
174
+ cssVar: "--zdc-card-radius",
175
+ selector: "a.group.block.rounded",
176
+ property: "border-radius",
177
+ default: "var(--zdc-surface-radius, var(--radius-DEFAULT))",
178
+ component: "card-grid",
179
+ surface: "chrome",
180
+ category: "shape",
181
+ description: "Border radius of the nav/doc card anchor (doc-card-grid, nav-card-grid, category-nav). Defaults to `var(--zdc-surface-radius, var(--radius-DEFAULT))` \u2014 chains through the meta-knob so setting `--zdc-surface-radius` in :root rounds all surfaces at once."
182
+ },
183
+ // ── Content band max-width (.zd-doc-content-band, chrome, #2461) ──────────
184
+ // Two widths exist in doc-layout.tsx:
185
+ // • showSidebar=true: max-w-[clamp(50rem,75vw,90rem)] → tokenized here
186
+ // • showSidebar=false: max-w-[80rem] → replaced by `data-zd-nosidebar`
187
+ // attr on the band div + `.zd-doc-content-band[data-zd-nosidebar]` rule
188
+ // in features.css (unlayered, specificity 0,2,0 > token rule 0,1,0).
189
+ // Token rule is unlayered and beats the Tailwind utility (layered). The
190
+ // `max-w-[clamp(50rem,75vw,90rem)]` JSX class is removed from the sidebar
191
+ // branch; default value is the transcribed literal — byte-identical.
192
+ // JS-toggle (html[data-sidebar-hidden]) remains covered by the existing
193
+ // `html[data-sidebar-hidden] .zd-doc-content-band` rule (specificity 0,2,1).
194
+ // Exception: `clamp(50rem,75vw,90rem)` is a bare CSS function expression —
195
+ // no equivalent design token exists (documented exception to the prefer-var()).
196
+ {
197
+ cssVar: "--zdc-content-max-width",
198
+ selector: ".zd-doc-content-band",
199
+ property: "max-width",
200
+ default: "clamp(50rem,75vw,90rem)",
201
+ component: "doc-content-band",
202
+ surface: "chrome",
203
+ category: "layout",
204
+ description: "Max width of the doc reading column (sidebar-present case). Defaults to `clamp(50rem,75vw,90rem)` (byte-identical to the current utility). The hide_sidebar 80rem variant is handled by `.zd-doc-content-band[data-zd-nosidebar]` in features.css; the JS-toggle variant by `html[data-sidebar-hidden] .zd-doc-content-band`."
205
+ },
206
+ // ── TOC width (nav[data-zd-toc], chrome, #2461) ───────────────────────────
207
+ // `nav[aria-label="Table of contents"]` has inner quotes so the parseTokens
208
+ // regex (which matches double-quoted strings) cannot handle it as a bare
209
+ // double-quoted field. `data-zd-toc` is added to the nav element in
210
+ // toc.tsx instead, making the selector parser-safe and an explicit anchor.
211
+ // Specificity 0,1,1 (element + attribute) beats `w-[280px]` (0,1,0);
212
+ // unlayered beats layered (@layer utilities).
213
+ // Exception: `280px` is a bare pixel literal — no fixed-width token exists
214
+ // (documented exception to the prefer-var-() rule).
215
+ {
216
+ cssVar: "--zdc-toc-width",
217
+ selector: "nav[data-zd-toc]",
218
+ property: "width",
219
+ default: "280px",
220
+ component: "toc",
221
+ surface: "chrome",
222
+ category: "layout",
223
+ description: "Width of the desktop Table of Contents right rail. Defaults to `280px` (byte-identical to the current `w-[280px]` utility in toc.tsx). No fixed-width design token exists \u2014 bare literal is the documented exception."
224
+ },
225
+ // ── Sidebar nav active item (a[data-nav-active], chrome, #2462) ───────────
226
+ // Targets non-root leaf active links in the SSR sidebar-tree island.
227
+ // `data-nav-active=""` is emitted on the `<a>` when `isActive && !isRoot`
228
+ // (sidebar-tree-island/index.tsx LeafNode). Root-level and category active
229
+ // items retain their unconditional `font-semibold` class (not conditional
230
+ // on active state) — targeting those with a single weight default would
231
+ // break byte-identity. Non-root leaf active is the canonical "current page"
232
+ // link: the most brand-identifying nav detail (#2462).
233
+ //
234
+ // Selector `a[data-nav-active]` specificity 0,1,1 beats the Tailwind
235
+ // utilities `bg-fg` (0,1,0) and `font-medium` (0,1,0); unlayered beats
236
+ // layered (@layer utilities). Defaults match the current classes exactly:
237
+ // `bg-fg` → background-color: var(--color-fg) (byte-identical)
238
+ // `font-medium`→ font-weight: var(--font-weight-medium) (byte-identical)
239
+ {
240
+ cssVar: "--zdc-nav-active-indicator-color",
241
+ selector: "a[data-nav-active]",
242
+ property: "background-color",
243
+ default: "var(--color-fg)",
244
+ component: "nav-active",
245
+ surface: "chrome",
246
+ category: "typography",
247
+ description: "Background color of the active sidebar nav link (non-root leaf, the current page). Defaults to `var(--color-fg)` (byte-identical to the current `bg-fg` utility \u2014 the inversion fill that marks the active item). Redefine in :root to use an accent stripe or custom fill instead."
248
+ },
249
+ {
250
+ cssVar: "--zdc-nav-active-weight",
251
+ selector: "a[data-nav-active]",
252
+ property: "font-weight",
253
+ default: "var(--font-weight-medium)",
254
+ component: "nav-active",
255
+ surface: "chrome",
256
+ category: "typography",
257
+ description: "Font weight of the active sidebar nav link (non-root leaf, the current page). Defaults to `var(--font-weight-medium)` (byte-identical to the current `font-medium` utility). Redefine in :root to use semibold or bold for stronger active emphasis."
258
+ }
259
+ ];
260
+ export {
261
+ COMPONENT_TOKENS
262
+ };
package/dist/content.css CHANGED
@@ -462,3 +462,59 @@
462
462
  .zd-content .mermaid foreignObject {
463
463
  overflow: visible;
464
464
  }
465
+
466
+ /* ============================================================================
467
+ * Component tokens (`--zdc-*`) — epic zudolab/zudo-doc#2446
468
+ *
469
+ * Token-backed rules on EXISTING, component-anchored selectors. Each rule
470
+ * targets the classes the component already emits (NO JSX/class change) with
471
+ * specificity that beats the consumer's UNLAYERED Tailwind utilities, and is
472
+ * itself kept UNLAYERED (NOT in `@layer zd-flow`). Defaults chain to existing
473
+ * tokens / `inherit` (never literals) so the rendered default is byte-identical
474
+ * to the pre-token output — the #2425 route-injection byte-hash gate stays
475
+ * green — and consumer `@theme` overrides keep flowing through.
476
+ *
477
+ * Single source of truth: `src/config/component-tokens.ts`. Wave 2 (#2448) adds
478
+ * the `gen-component-tokens` generator that owns the BEGIN/END block below
479
+ * (mirroring `gen-z-index`); until then the block is maintained by hand to
480
+ * match what the generator will emit. Consumers rebrand a knob by redefining
481
+ * its `--zdc-*` custom property in `:root` — no specificity fight, reaches
482
+ * every route.
483
+ * ========================================================================== */
484
+
485
+ /* BEGIN --zdc-* component tokens (generated by gen-component-tokens; do not edit by hand) */
486
+
487
+ h1.text-heading {
488
+ font-family: var(--zdc-doc-title-font, inherit);
489
+ font-weight: var(--zdc-doc-title-weight, var(--font-weight-bold));
490
+ letter-spacing: var(--zdc-doc-title-tracking, var(--tracking-normal));
491
+ }
492
+
493
+ h2.text-title {
494
+ font-family: var(--zdc-doc-h2-font, inherit);
495
+ font-weight: var(--zdc-doc-h2-weight, var(--font-weight-bold));
496
+ letter-spacing: var(--zdc-doc-h2-tracking, var(--tracking-normal));
497
+ }
498
+
499
+ h3.text-body.font-bold {
500
+ font-weight: var(--zdc-doc-h3-weight, var(--font-weight-bold));
501
+ }
502
+
503
+ h4.text-body.font-semibold {
504
+ font-weight: var(--zdc-doc-h4-weight, var(--font-weight-semibold));
505
+ }
506
+
507
+ .zd-content {
508
+ font-family: var(--zdc-doc-prose-font, var(--font-sans));
509
+ }
510
+
511
+ a.text-accent.underline {
512
+ text-decoration: var(--zdc-doc-link-decoration, underline);
513
+ }
514
+
515
+ [data-admonition] {
516
+ border-radius: var(--zdc-admonition-radius, 0 var(--radius-DEFAULT) var(--radius-DEFAULT) 0);
517
+ border-left-width: var(--zdc-admonition-border-width, 4px);
518
+ }
519
+
520
+ /* END --zdc-* component tokens */
@@ -1,7 +1,9 @@
1
1
  import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
2
2
  import { SidebarResizerInit } from "../sidebar-resizer/index.js";
3
3
  import { deriveBodyEndIslands } from "../chrome/derive.js";
4
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
4
5
  function createDocBodyEnd(ctx) {
6
+ assertChromeContext(ctx, "createDocBodyEnd");
5
7
  const settings = ctx.settings;
6
8
  const BodyEndIslands = deriveBodyEndIslands(ctx);
7
9
  function DocBodyEnd() {
@@ -2,7 +2,9 @@ import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
2
2
  import { FrontmatterPreview } from "../metainfo/index.js";
3
3
  import { createDocMetainfoArea } from "../doc-metainfo-area/index.js";
4
4
  import { createDocTagsArea } from "../doc-tags-area/index.js";
5
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
5
6
  function createDocContentHeader(ctx) {
7
+ assertChromeContext(ctx, "createDocContentHeader");
6
8
  const t = ctx.t;
7
9
  const buildFrontmatterPreviewEntries = ctx.hostBindings.buildFrontmatterPreviewEntries ?? (() => []);
8
10
  const frontmatterRenderers = ctx.hostBindings.frontmatterRenderers ?? {};
@@ -4,7 +4,9 @@ import { BodyFootUtilArea } from "../body-foot-util/index.js";
4
4
  import { toHistorySlug } from "../slug/index.js";
5
5
  import { buildGitHubSourceUrl as buildGitHubSourceUrlBase } from "../github-helpers/index.js";
6
6
  import { deriveDocHistorySlot } from "../chrome/derive.js";
7
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
7
8
  function createDocHistoryArea(ctx) {
9
+ assertChromeContext(ctx, "createDocHistoryArea");
8
10
  const settings = ctx.settings;
9
11
  const defaultLocale = ctx.defaultLocale;
10
12
  const docHistoryMeta = ctx.hostBindings.docHistoryMeta ?? {};
@@ -1,6 +1,7 @@
1
1
  import { jsx } from "preact/jsx-runtime";
2
2
  import { DocMetainfo } from "../metainfo/index.js";
3
3
  import { toHistorySlug } from "../slug/index.js";
4
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
4
5
  const LOCALE_TO_BCP47 = {
5
6
  en: "en-US",
6
7
  ja: "ja-JP",
@@ -16,6 +17,7 @@ function formatDate(isoDate, locale) {
16
17
  });
17
18
  }
18
19
  function createDocMetainfoArea(ctx) {
20
+ assertChromeContext(ctx, "createDocMetainfoArea");
19
21
  const settings = ctx.settings;
20
22
  const defaultLocale = ctx.defaultLocale;
21
23
  const docHistoryMeta = ctx.hostBindings.docHistoryMeta ?? {};
@@ -4,7 +4,9 @@ import { createDocContentHeader } from "../doc-content-header/index.js";
4
4
  import { createDocMetainfoArea } from "../doc-metainfo-area/index.js";
5
5
  import { createDocHistoryArea } from "../doc-history-area/index.js";
6
6
  import { deriveMdxComponents, deriveInlineVersionSwitcher } from "../chrome/derive.js";
7
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
7
8
  function createRenderDocPage(ctx) {
9
+ assertChromeContext(ctx, "createRenderDocPage");
8
10
  const docsUrl = ctx.docsUrl;
9
11
  const versionedDocsUrl = ctx.versionedDocsUrl;
10
12
  const absoluteUrl = ctx.absoluteUrl;
@@ -12,7 +12,9 @@ import { createFooterWithDefaults } from "../footer-with-defaults/index.js";
12
12
  import { createDocBodyEnd } from "../doc-body-end/index.js";
13
13
  import { createDocPager } from "../doc-pager/index.js";
14
14
  import { deriveComposeMetaTitle } from "../chrome/derive.js";
15
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
15
16
  function createDocPageShell(ctx) {
17
+ assertChromeContext(ctx, "createDocPageShell");
16
18
  const settings = ctx.settings;
17
19
  const composeMetaTitle = deriveComposeMetaTitle(ctx);
18
20
  const HeadWithDefaults = createHeadWithDefaults(ctx);
@@ -1,6 +1,8 @@
1
1
  import { jsx, jsxs } from "preact/jsx-runtime";
2
2
  import { ChevronLeft, ChevronRight } from "../icons/index.js";
3
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
3
4
  function createDocPager(ctx) {
5
+ assertChromeContext(ctx, "createDocPager");
4
6
  const t = ctx.t;
5
7
  function DocPager({ prev, next, locale }) {
6
8
  return /* @__PURE__ */ jsxs("nav", { class: "mt-vsp-2xl grid grid-cols-2 gap-hsp-xl", children: [
@@ -1,7 +1,9 @@
1
1
  import { jsx } from "preact/jsx-runtime";
2
2
  import { DocTags } from "../metainfo/index.js";
3
3
  import { resolvePageTags } from "../tag-helpers/index.js";
4
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
4
5
  function createDocTagsArea(ctx) {
6
+ assertChromeContext(ctx, "createDocTagsArea");
5
7
  const settings = ctx.settings;
6
8
  const defaultLocale = ctx.defaultLocale;
7
9
  const withBase = ctx.withBase;
@@ -70,7 +70,8 @@ function DocLayout(props) {
70
70
  /* @__PURE__ */ jsx("div", { class: "flex min-h-[calc(100vh-3.5rem)] justify-center", children: /* @__PURE__ */ jsxs(
71
71
  "div",
72
72
  {
73
- class: `zd-doc-content-band flex w-full gap-[clamp(1.5rem,3vw,4rem)] ${showSidebar ? "max-w-[clamp(50rem,75vw,90rem)]" : "max-w-[80rem]"}`,
73
+ class: "zd-doc-content-band flex w-full gap-[clamp(1.5rem,3vw,4rem)]",
74
+ ...!showSidebar ? { "data-zd-nosidebar": "" } : {},
74
75
  children: [
75
76
  /* @__PURE__ */ jsxs("main", { class: "flex-1 min-w-0 px-hsp-xl py-vsp-xl lg:px-hsp-2xl lg:py-vsp-2xl", children: [
76
77
  breadcrumb,
package/dist/features.css CHANGED
@@ -309,12 +309,13 @@ pre[class^="syntect-"] .line .highlighted-word {
309
309
  margin-left: 0;
310
310
  }
311
311
 
312
- /* When hidden via the toggle, narrow the content band to the
313
- * hide_sidebar frontmatter width so it centers (the flex parent already
312
+ /* When the sidebar is hidden via the JS toggle, narrow the content band
313
+ * to the hide_sidebar width so it centers (the flex parent already
314
314
  * applies justify-content: center) instead of leaving a dead gap where
315
- * the sidebar was. 80rem must match the `max-w-[80rem]` hide_sidebar
316
- * branch in doc-layout.tsx. These rules are unlayered, so they win over
317
- * the Tailwind `max-w-[clamp(...)]` utility (utilities layer). (#2002) */
315
+ * the sidebar was. The 80rem value mirrors the static hide_sidebar band
316
+ * width (see `.zd-doc-content-band[data-zd-nosidebar]` below and in the
317
+ * --zdc-content-max-width chrome token). These rules are unlayered, so
318
+ * they win over the Tailwind utilities layer. (#2002) */
318
319
  .zd-doc-content-band {
319
320
  transition: max-width var(--zd-transition-slow) ease-in-out;
320
321
  }
@@ -324,6 +325,16 @@ pre[class^="syntect-"] .line .highlighted-word {
324
325
  }
325
326
  }
326
327
 
328
+ /* hide_sidebar band width — applies when doc-layout.tsx sets showSidebar=false
329
+ * (hide_sidebar frontmatter). The `max-w-[80rem]` JSX utility was replaced by a
330
+ * data-zd-nosidebar attr + this rule when --zdc-content-max-width was added (#2461).
331
+ * Specificity 0,2,0 (class + attr) beats the generated token rule (0,1,0) so
332
+ * hide_sidebar pages keep their narrower 80rem reading column. All-screen (not
333
+ * inside @media) because the band element exists at every breakpoint. */
334
+ .zd-doc-content-band[data-zd-nosidebar] {
335
+ max-width: 80rem;
336
+ }
337
+
327
338
  /* Sidebar toggle button — left position uses CSS variable, needs global rule */
328
339
  @media (min-width: 1024px) {
329
340
  .zd-desktop-sidebar-toggle {
@@ -880,3 +891,41 @@ dialog.zd-mermaid-dialog::backdrop {
880
891
  .diff-line-empty {
881
892
  background-color: color-mix(in oklch, var(--color-muted) 8%, transparent);
882
893
  }
894
+
895
+ /* ============================================================================
896
+ * Chrome component tokens (`--zdc-*`) — epic zudolab/zudo-doc#2446
897
+ *
898
+ * The CHROME-surface counterpart of the content-surface `--zdc-*` block in
899
+ * `src/content.css`. Token-backed rules on EXISTING, component-anchored
900
+ * selectors for the app shell (header/footer/sidebar/toc/cards). Each rule
901
+ * targets the classes the component already emits (NO JSX/class change) with
902
+ * specificity that beats the consumer's UNLAYERED Tailwind utilities, and is
903
+ * itself kept UNLAYERED (mirroring the content block) so a consumer rebrands a
904
+ * knob by redefining its `--zdc-*` custom property in `:root`.
905
+ *
906
+ * Single source of truth: `src/config/component-tokens.ts` (tokens with
907
+ * `surface: "chrome"`). The `gen-component-tokens` generator owns the BEGIN/END
908
+ * block below — never hand-edit between the markers. Seeded empty: no chrome
909
+ * tokens exist yet (S1, #2458 added only the routing mechanism).
910
+ * ========================================================================== */
911
+
912
+ /* BEGIN --zdc-* chrome component tokens (generated by gen-component-tokens; do not edit by hand) */
913
+
914
+ a.group.block.rounded {
915
+ border-radius: var(--zdc-card-radius, var(--zdc-surface-radius, var(--radius-DEFAULT)));
916
+ }
917
+
918
+ .zd-doc-content-band {
919
+ max-width: var(--zdc-content-max-width, clamp(50rem,75vw,90rem));
920
+ }
921
+
922
+ nav[data-zd-toc] {
923
+ width: var(--zdc-toc-width, 280px);
924
+ }
925
+
926
+ a[data-nav-active] {
927
+ background-color: var(--zdc-nav-active-indicator-color, var(--color-fg));
928
+ font-weight: var(--zdc-nav-active-weight, var(--font-weight-medium));
929
+ }
930
+
931
+ /* END --zdc-* chrome component tokens */
@@ -1,6 +1,8 @@
1
1
  import { jsx } from "preact/jsx-runtime";
2
2
  import { Footer } from "../footer/index.js";
3
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
3
4
  function createFooterWithDefaults(ctx) {
5
+ assertChromeContext(ctx, "createFooterWithDefaults");
4
6
  const settings = ctx.settings;
5
7
  const defaultLocale = ctx.defaultLocale;
6
8
  const tagVocabulary = ctx.hostBindings.tagVocabulary ?? [];
@@ -3,7 +3,9 @@ import { OgTags, TwitterCard } from "../head/index.js";
3
3
  import { SIDEBAR_RESIZER_RESTORE_SCRIPT } from "../sidebar-resizer/index.js";
4
4
  import ColorSchemeProvider from "../theme/color-scheme-provider.js";
5
5
  import { deriveComposeMetaTitle, deriveColorSchemeGenerators } from "../chrome/derive.js";
6
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
6
7
  function createHeadWithDefaults(ctx) {
8
+ assertChromeContext(ctx, "createHeadWithDefaults");
7
9
  const settings = ctx.settings;
8
10
  const composeMetaTitle = deriveComposeMetaTitle(ctx);
9
11
  const withBase = ctx.withBase;
@@ -8,8 +8,10 @@ import {
8
8
  import { ThemeToggle } from "../theme-toggle/index.js";
9
9
  import { SidebarToggle } from "../sidebar-toggle-island/index.js";
10
10
  import { buildGitHubRepoUrl as buildGitHubRepoUrlBase } from "../github-helpers/index.js";
11
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
11
12
  import { deriveNavDataPrep, deriveSearchWidgetSlot } from "../chrome/derive.js";
12
13
  function createHeaderWithDefaults(ctx) {
14
+ assertChromeContext(ctx, "createHeaderWithDefaults");
13
15
  const settings = ctx.settings;
14
16
  const defaultLocale = ctx.defaultLocale;
15
17
  const locales = ctx.locales;
package/dist/safelist.css CHANGED
@@ -1,2 +1,2 @@
1
1
  /* generated by gen-safelist.mjs — do not edit by hand */
2
- @source inline("-link -mb-px -ml-hsp-sm -noscript -translate-x-full 2xl:w-[24px] [&::-webkit-details-marker]:hidden [&_a]:text-accent [&_a]:underline [&_nav]:mb-0 [data-kbd-shortcut] [doc-history-meta] [doc-history] [doc-layout] [llms-txt] a a2 abbr about above absent absolute across activated active actual added admonition admonition- admonition-body admonition-title after after-breadcrumb after-content after-navigate after-sidebar after-title against agent agents ai-chat ai-chat-md ai-chat-trigger alert align-top all allow-same-origin allow-scripts alone already already-executed an anchor anchored and and/or animate-spin announce antialiased any application/json application/xml applies apply-css-vars approach are area arg aria-atomic aria-busy aria-controls aria-current aria-disabled aria-expanded aria-haspopup aria-hidden aria-label aria-live aria-orientation aria-pressed aria-selected aria-valuemax aria-valuemin aria-valuenow arrive arrows article as asc aside aspect-[1200/630] aspect-square asset- assets assigning assistant async at attach attribute attributes auto autogenerated available avoid await away b back backdrop:bg-bg/30 backdrop:bg-bg/80 backdrop:bg-overlay/60 backdrop:z-modal-backdrop background backtick backticks baked banner bare base base64 based batch be because before below best between bg bg-[#fff] bg-accent bg-bg bg-chat-assistant-bg bg-chat-user-bg bg-code-bg bg-fg bg-info/10 bg-info/5 bg-muted bg-overlay/30 bg-surface bg-transparent bg-warning/10 bg-warning/5 bi bigint bin blank blanks blob block blockquote blocks blur body body-end-components body-end-scripts boolean bootstrap border border-accent border-b border-b-2 border-b-[5px] border-bg/30 border-collapse border-danger border-dashed border-fg border-info/30 border-l border-l-0 border-l-[3px] border-muted border-none border-r border-solid border-t border-t-[2px] border-t-[3px] border-transparent border-warning/30 border-y both bottom-vsp-xl box-border br breadcrumb:end breadcrumb:start break-words browser browsers btn bug build bundler but button buttons by bypassed cached call caller can cancellation cannot canonical caption carry cases cat-nav- catch category catppuccin-latte caught caution center center/contain change changed changes checkbox child ci circle cite class class-less claude claude-agents claude-commands claude-md claude-skills cleaned clear clear-css-vars clearing click client client-router client-side clip clobbering close closed closing code code-block-sr-announce code-group code-group-panel col col-resize colgroup collision color color-scheme color-scheme-changed color-scheme-provider color-tweak colorization colors command commands commit compare component component:github-link component:language-switcher component:search component:theme-toggle component:version-switcher compute computed concrete configuration configure configured connect const consumer consumes container containers containing content content-admonition content-type content-wrapper:end content-wrapper:start contents context controller controls converts copy correct correctly corrupt count covered covers cp crashes created cross-component crumb- cs css cur current cursor cursor-not-allowed cursor-pointer custom danger dark data data-active data-admonition data-base data-close-search data-group-id data-header data-header-logo data-header-nav data-header-right data-kbd-shortcut data-loading-index data-mermaid-enlarge-ready data-mermaid-rendered data-mermaid-src data-nav-item data-nav-item-dropdown data-nav-more data-nav-more-menu data-nav-more-toggle data-no-results data-open-search data-pan-active data-processed data-result-count-template data-search-count data-search-count-narrow data-search-dialog data-search-input data-search-placeholder data-search-results data-search-unavailable data-sidebar-hidden data-sidebar-resizer data-site-nav data-tab-btn data-tab-default data-tab-label data-tab-value data-tabs data-taglist-group data-testid data-theme data-theme/style data-variant data-version-banner data-version-menu data-version-switcher data-version-toggle data-zfb-transition-persist dd debounced decimal declare decoration-muted default defaults del delegated desc description design design-token-panel design-token-trigger desktop desktop-sidebar desktop-sidebar-toggle desktop-sidebar-toggle-island destructive detach detached details deterministic develop dfn diagram diagrams dialog dieser diff diff-line-added diff-line-content diff-line-empty diff-line-num diff-line-removed diff-row dir directly directories directory disabled disabled:opacity-50 disc display:none dist div dl do doc doc-card- doc-history doc-history-generate doc-history-panel doc-history-trigger doc-pager docs docs- docs-v- document document-level does double-registration drag draggable drop dropdown dropdown-child dropdown-parent dropdowns dt duration-150 duration-200 during dynamically e e2e each earlier ease-in-out edge eject ejected el element elements els else em emit emitting empty empty/undefined en enable end enlarged entire entities entries entry error escape escaped even eventually every exactly excerpt existing exists exit expected export extends failed fall fallback fallbacks falls false fast feature feed fg fields fieldset figcaption figure file fill fills finally find fire fires first fixed fixtures flag flash flat flex flex-1 flex-col flex-wrap flip flipping flips focus focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2 focus-visible:underline focus:border-accent focus:outline-none focus:underline font font-bold font-medium font-mono font-semibold footer footer- for form found free fresh from frontmatter frontmatter-preview frozen fs-extra full fully function further g gap-[0.3em] gap-[clamp(1.5rem,3vw,4rem)] gap-hsp-2xs gap-hsp-md gap-hsp-sm gap-hsp-xl gap-hsp-xs gap-vsp-2xs gap-vsp-lg gap-vsp-md gap-vsp-xs gap-x-hsp-2xs gap-x-hsp-lg gap-x-hsp-md gap-x-hsp-sm gap-x-hsp-xs gap-y-vsp-2xs gap-y-vsp-lg gap-y-vsp-md gap-y-vsp-xs gaps gate generate generation genuine geometry get github github-link go got graph gray-matter grid grid-cols-1 grid-cols-2 group group-focus-visible:text-accent group-focus-visible:underline group-focus-within:block group-hover:bg-fg group-hover:block group-hover:text-accent group-hover:text-bg group-hover:underline group-open:rotate-90 guard h-[0.5rem] h-[0.625rem] h-[0.875rem] h-[1.125rem] h-[1.25rem] h-[1.575rem] h-[14px] h-[1lh] h-[2rem] h-[3.5rem] h-[3rem] h-[90vh] h-[calc(100%-3rem)] h-[calc(100vh-3.5rem)] h-dvh h-full h-icon-lg h-icon-md h-icon-sm h-icon-xs h1 h2 h3 h4 h5 h6 half hand handle handled handler handlers has hash-link have head head-links head-scripts header header- header-call:end header-call:start height here hex hidden highlight highlighter history hit horizontal host hover:bg-[color-mix(in_srgb,var(--color-surface)_80%,var(--color-fg)_20%)] hover:bg-accent-hover hover:bg-accent/10 hover:bg-surface hover:border-accent hover:border-accent-hover hover:border-fg hover:text-accent hover:text-accent-hover hover:text-fg hover:underline hr href hrefs html i i18n/theme. i2 i3 i4 icon identical idle idx if iframe image image-enlarge image/png img import important imports in inactive inbox includes index index-load info inherit initial initialised injected inline inline-block inline-flex input input-clear ins inset-0 inside install instance instanceof instead instructions intended intent into invalid inverse invoke is issues it italic item item- items items-baseline items-center items-start its itself javascript justify-between justify-center justify-end justify-start katex kbd keep keeps kept keyboard keyboard-shortcut keydown keystroke keywords khroma label landing language-switcher last:border-b-0 later launch leading-relaxed leading-snug leading-tight leaf- leak leaves leaving left left-0 left:calc legend legitimate lg lg:block lg:border lg:border-fg lg:border-solid lg:flex lg:flex-col lg:flex-row lg:gap-hsp-xl lg:grid-cols-[repeat(auto-fit,minmax(12rem,1fr))] lg:h-[90vh] lg:hidden lg:justify-start lg:m-auto lg:max-h-[90vh] lg:max-w-[52.5rem] lg:ml-[var(--zd-sidebar-w)] lg:pt-vsp-2xl lg:px-hsp-2xl lg:py-vsp-2xl lg:text-left lg:w-[90vw] lg:w-[clamp(16rem,25%,22rem)] li li2 light like likely line linger link link- links list-disc list-none listener literal literals lives llms llms-txt load local locale locales log longest-match look lostpointercapture lower lowercased luminance m m-0 m21 m6 main make malformed malicious maps mark marks matches matching math math-display math-inline max max-h-[80vh] max-h-[85vh] max-h-[90vh] max-h-full max-h-none max-w-[46rem] max-w-[80rem] max-w-[85%] max-w-[85vw] max-w-[90vw] max-w-[clamp(50rem,75vw,90rem)] max-w-full max-w-none may mb-0 mb-vsp-2xs mb-vsp-lg mb-vsp-md mb-vsp-sm mb-vsp-xl mb-vsp-xs measures measuring mechanism menu merged mermaid message messages meta metadata min-h-[60vh] min-h-[calc(100vh-3.5rem)] min-h-screen min-w-0 min-w-[10rem] min-w-[8rem] mirror mirrors missing ml-auto ml-hsp-2xl ml-hsp-lg ml-hsp-sm ml-hsp-xl mod mode mounted mouseenter mouseleave mr-hsp-sm mt-0 mt-vsp-2xl mt-vsp-2xs mt-vsp-3xs mt-vsp-lg mt-vsp-md mt-vsp-sm mt-vsp-xl must mutates mutation mutations mx-auto my-vsp-lg my-vsp-md name named native nav nav-card- navigating navigation navigations near needs new newly-swapped next no no-enlarge no-op no-repeat no-underline node node:buffer node:fs node:fs/promises node:module node:path nodes nofollow noindex non-empty non-persisted non-string none noopener noreferrer normal noscript not not-object note now null number numeric object object-contain observe observer occurred of off og:description og:image og:image:alt og:image:height og:image:width og:title og:type og:url ol old older on once one only opacity-60 open open/close option or original other others out outline-none over overflow overflow-auto overflow-hidden overflow-x-auto overflow-y-auto overflow-y:auto overwrite own p p-0 p-hsp-lg p-hsp-md p-hsp-sm p-hsp-xl package package-owned packages padding page page-loading page-loading-overlay page-loading-spinner page-navigate-end pages pages/. paint pan panel panels parent parse parsed pass passed path paths pattern pb-[50vh] pb-vsp-md pb-vsp-xl pb-vsp-xs per per-call per-link permanently persisted pi pick picked picks picocolors pins pipelines pl-[1.25rem] pl-hsp-lg pl-hsp-sm pl-hsp-xl place placeholder placeholder:text-muted plain plural plus pnpm pointer-events-none pointercancel pointerdown pointermove pointerup polite polygon polyline populates port position position:fixed pr-[4px] pr-hsp-lg pr-hsp-md pr-hsp-sm pr-hsp-xl pre pre-lowercased preact preact/compat preact/hooks preact/jsx-runtime preconnect preload pres preserved print produce produced produces production project propagating properties property props provided proxy pt-[0.15rem] pt-[2px] pt-vsp-3xs pt-vsp-md pt-vsp-sm pt-vsp-xl pt-vsp-xs ptag- public purely px px-hsp-2xl px-hsp-2xs px-hsp-lg px-hsp-md px-hsp-sm px-hsp-xl px-hsp-xs py-0 py-[2px] py-[calc(var(--spacing-vsp-xs)+0.15rem)] py-hsp-2xs py-hsp-sm py-hsp-xs py-vsp-2xs py-vsp-3xs py-vsp-lg py-vsp-md py-vsp-sm py-vsp-xl py-vsp-xs q query question r raw re-encode/decode re-lowercase re-querying re-render re-renders re-run re-running re-selects reach reached reaches read reading reads ready real real-value recorded recovers ref- references refetch refreshes regenerate regenerates reinit reinits relative reload remains remove removed render rendered renders reorder repaint repeated repeating replaced replaces repopulate requires reserved resize resize-x resolve resolved resolves response restore restores result result-click results results-area retry return returns revision revisions rewrite right- right-0 ro robots role root rotate-180 rotate-90 round round-trip rounded rounded-[0.75rem] rounded-bl-[0.25rem] rounded-bl-[1rem] rounded-br-[0.25rem] rounded-br-[1rem] rounded-full rounded-lg rounded-t-[1rem] route router routes routes-src running runs runtime s safe safer same same-locale samp scanned schema-mismatch schema-missing scheme score scored script script- script-eval script-evaluation script-injection scripts scroll scrollbar scrolled scrollend search search-index searched section section- see sel-bg sel-fg select select-none self self-start sentinel separator server server-rendered set sets setup shadow-[0_1px_3px_color-mix(in_srgb,var(--color-fg)_8%,transparent)] shadow-lg shared shiki ship ships short shortcut should show shown shrink-0 sidebar sidebar- sidebar-toggle-island sidebar-tree-island signal similarity single singular site-search site-tree-nav-island sitemap- sites size skill skills skipping skips slash slug sm:border sm:border-muted sm:flex-row sm:grid-cols-2 sm:h-auto sm:items-center sm:justify-between sm:max-h-[80vh] sm:max-w-[52rem] sm:mx-auto sm:my-[10vh] sm:rounded-lg small smooth snapshot snapshots so soft soft-nav solid some somehow source sources space-y-vsp-2xs spacing span spans spec specifier specifiers sr-only src stale start state status stay sticky still stop stored stray string strings strip stroke-linecap stroke-linejoin stroke-width strong style style-attribute styles stylesheet sub subagents subsequent success successful summary sup support survives svg swap swapped swaps synchronous synchronously syntactically syntect t tab tab-item tab-panel tabindex table tablist tabpanel tabs tabs-container tabs-content tabs-nav tag tag- tag-item- tags tags:audit tbody td temp temp-element template temporary temporary-element terminal terms test-results text text-accent text-bg text-body text-caption text-center text-chat-assistant-text text-chat-user-text text-code-fg text-danger text-display text-fg text-heading text-info text-left text-micro text-muted text-muted/50 text-right text-small text-title text-warning text/plain textarea tfoot th that the thead their them theme theme-color theme-toggle theme/token then there these they this through throw time tip title to toggle toggle-ai-chat toggle-design-token-panel toggles token tokens tolerates too toolbar top-0 top-[3.5rem] top-full top-level total touches tp tr tracked tracking-wider trade-off transition transition-[background,color,border-color] transition-[left,color] transition-colors transition-transform translate-x-0 translations transparent treats tree tree-child- tree-item- tree-top- trick trigger trigger:ai-chat trigger:design-token-panel triggers true truncate truncated try turn twitter:card twitter:creator twitter:description twitter:image twitter:site twitter:title two type u ul unavailable unchanged undefined under underline understand unknown unmaintained unobserve unreadable unrelated unreleased unreliable unset unsupported up uppercase usage use used user uses utf-8 utf8 utilities v v2 val value value-reader values var variable version version- version-menu version-switcher vertical via video viewport virtual:zudo-doc-route-context visible vitesse-dark vocabulary w w-1/2 w-[0.5rem] w-[0.625rem] w-[0.875rem] w-[1.125rem] w-[1.575rem] w-[1.5rem] w-[1.75rem] w-[14px] w-[16px] w-[16rem] w-[18px] w-[280px] w-[2rem] w-[320px] w-[90vw] w-[var(--zd-sidebar-w)] w-dvw w-full w-icon-lg w-icon-md w-icon-sm w-icon-xs want warning was watching wbr wbr- we website went were what when where whereas whether which while whitespace-nowrap whitespace-pre whole will with without word working worktrees would wrap wrapper wrappers written wrong wrote xl:flex xl:hidden y-scrollbar yet you your z-dropdown z-local-1 z-modal z-modal-backdrop z-sidebar z-toolbar zd-content zd-desktop-sidebar-toggle zd-doc-content-band zd-enlarge-btn zd-enlarge-dialog zd-enlarge-dialog-close zd-enlargeable zd-html-preview-code zd-mermaid-dialog zd-mermaid-enlargeable zd-mermaid-tool-btn zd-mermaid-toolbar zd-mermaid-transform zd-mermaid-viewport zd-sidebar-content-wrapper zd-sidebar-open zfb zfb:after-swap zfb:before-preparation zod zoom zudo-doc-design-tokens/v1 zudo-doc-sidebar-visible zudo-doc-sidebar-width zudo-doc-theme zudo-doc-theme-bridge");
2
+ @source inline("-link -mb-px -ml-hsp-sm -noscript -translate-x-full 2xl:w-[24px] [&::-webkit-details-marker]:hidden [&_a]:text-accent [&_a]:underline [&_nav]:mb-0 [data-admonition] [data-kbd-shortcut] [doc-history-meta] [doc-history] [doc-layout] [llms-txt] a a2 abbr about above absent absolute accent across activated active actual added admonition admonition- admonition-body admonition-title admonition/callout after after-breadcrumb after-content after-navigate after-sidebar after-title against agent agents ai-chat ai-chat-md ai-chat-trigger alert align-top all allow-same-origin allow-scripts alone already already-executed an anchor anchored and and/or animate-spin announce antialiased any application/json application/xml applies apply-css-vars approach are area arg aria-atomic aria-busy aria-controls aria-current aria-disabled aria-expanded aria-haspopup aria-hidden aria-label aria-live aria-orientation aria-pressed aria-selected aria-valuemax aria-valuemin aria-valuenow arrive arrows article as asc aside aspect-[1200/630] aspect-square asset- assets assigning assistant async at attach attribute attributes auto autogenerated available avoid await away b back backdrop:bg-bg/30 backdrop:bg-bg/80 backdrop:bg-overlay/60 backdrop:z-modal-backdrop background background-color backtick backticks baked banner bare base base64 based batch be because before below best between bg bg-[#fff] bg-accent bg-bg bg-chat-assistant-bg bg-chat-user-bg bg-code-bg bg-fg bg-info/10 bg-info/5 bg-muted bg-overlay/30 bg-surface bg-transparent bg-warning/10 bg-warning/5 bi bigint bin blank blanks blob block blockquote blocks blur body body-end-components body-end-scripts bold boolean bootstrap border border-accent border-b border-b-2 border-b-[5px] border-bg/30 border-collapse border-danger border-dashed border-fg border-info/30 border-l border-l-0 border-l-[3px] border-left-width border-muted border-none border-r border-radius border-solid border-t border-t-[2px] border-t-[3px] border-transparent border-warning/30 border-width border-y both bottom-vsp-xl box-border br brand breadcrumb:end breadcrumb:start break-words browser browsers btn bug build bundler but button buttons by bypassed byte-identical cached call caller can cancellation cannot canonical caption card card-grid carry cases cat-nav- catch category catppuccin-latte caught caution center center/contain chains change changed changes checkbox child chrome ci circle cite class class-less claude claude-agents claude-commands claude-md claude-skills cleaned clear clear-css-vars clearing click client client-router client-side clip clobbering close closed closing code code-block-sr-announce code-group code-group-panel col col-resize colgroup collision color color-scheme color-scheme-changed color-scheme-provider color-tweak colorization colors column command commands commit compare component component:github-link component:language-switcher component:search component:theme-toggle component:version-switcher compute computed concrete configuration configure configured connect const consumer consumes container containers containing content content-admonition content-link content-type content-wrapper:end content-wrapper:start contents context controller controls converts copy corners correct correctly corrupt count covered covers cp crashes created cross-component crumb- cs css cur current cursor cursor-not-allowed cursor-pointer custom danger dark data data-active data-admonition data-base data-close-search data-group-id data-header data-header-logo data-header-nav data-header-right data-kbd-shortcut data-loading-index data-mermaid-enlarge-ready data-mermaid-rendered data-mermaid-src data-nav-active data-nav-item data-nav-item-dropdown data-nav-more data-nav-more-menu data-nav-more-toggle data-no-results data-open-search data-pan-active data-processed data-result-count-template data-search-count data-search-count-narrow data-search-dialog data-search-input data-search-placeholder data-search-results data-search-unavailable data-sidebar-hidden data-sidebar-resizer data-site-nav data-tab-btn data-tab-default data-tab-label data-tab-value data-tabs data-taglist-group data-testid data-theme data-theme/style data-variant data-version-banner data-version-menu data-version-switcher data-version-toggle data-zd-nosidebar data-zd-toc data-zfb-transition-persist dd debounced decimal declare decoration decoration-muted default defaults del delegated desc description design design-token-panel design-token-trigger desktop desktop-sidebar desktop-sidebar-toggle desktop-sidebar-toggle-island destructive detach detached details deterministic develop dfn diagram diagrams dialog dieser diff diff-line-added diff-line-content diff-line-empty diff-line-num diff-line-removed diff-row dir directly directories directory disabled disabled:opacity-50 disc display:none dist distinct div dl do doc doc-card- doc-content-band doc-history doc-history-generate doc-history-panel doc-history-trigger doc-page doc-pager doc-prose doc-title docs docs- docs-v- document document-level documented does double-registration drag draggable drop dropdown dropdown-child dropdown-parent dropdowns dt duration-150 duration-200 during dynamically e e2e each earlier ease-in-out edge eject ejected el element elements els else em emit emitting empty empty/undefined en enable end enlarged entire entities entries entry error escape escaped even eventually every exactly excerpt excludes existing exists exit expected export extends factories failed fall fallback fallbacks falls false family fast feature feed fg fields fieldset figcaption figure file fill fills finally find fire fires first fixed fixed-width fixtures flag flash flat flex flex-1 flex-col flex-wrap flip flipping flips flush-left focus focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2 focus-visible:underline focus:border-accent focus:outline-none focus:underline font font-bold font-family font-medium font-mono font-semibold font-weight footer footer- for form found free fresh from frontmatter frontmatter-preview frozen fs-extra full fully function further g gap-[0.3em] gap-[clamp(1.5rem,3vw,4rem)] gap-hsp-2xs gap-hsp-md gap-hsp-sm gap-hsp-xl gap-hsp-xs gap-vsp-2xs gap-vsp-lg gap-vsp-md gap-vsp-xs gap-x-hsp-2xs gap-x-hsp-lg gap-x-hsp-md gap-x-hsp-sm gap-x-hsp-xs gap-y-vsp-2xs gap-y-vsp-lg gap-y-vsp-md gap-y-vsp-xs gaps gate generate generation genuine geometry get github github-link go got graph gray-matter grid grid-cols-1 grid-cols-2 group group-focus-visible:text-accent group-focus-visible:underline group-focus-within:block group-hover:bg-fg group-hover:block group-hover:text-accent group-hover:text-bg group-hover:underline group-open:rotate-90 guard h-[0.5rem] h-[0.625rem] h-[0.875rem] h-[1.125rem] h-[1.25rem] h-[1.575rem] h-[14px] h-[1lh] h-[2rem] h-[3.5rem] h-[3rem] h-[90vh] h-[calc(100%-3rem)] h-[calc(100vh-3.5rem)] h-dvh h-full h-icon-lg h-icon-md h-icon-sm h-icon-xs h1 h1s h2 h2s h3 h4 h5 h6 half hand handle handled handler handlers has hash-link have head head-links head-scripts header header- header-call:end header-call:start heading heading-h2 heading-h3 heading-h4 headings height here hex hidden highlight highlighter history hit horizontal host hover:bg-[color-mix(in_srgb,var(--color-surface)_80%,var(--color-fg)_20%)] hover:bg-accent-hover hover:bg-accent/10 hover:bg-surface hover:border-accent hover:border-accent-hover hover:border-fg hover:text-accent hover:text-accent-hover hover:text-fg hover:underline hr href hrefs html i i18n/theme. i2 i3 i4 icon identical idle idx if iframe image image-enlarge image/png img import important imports in inactive inbox includes index index-load info inherit inherited initial initialised injected inline inline-block inline-flex input input-clear ins inset-0 inside install instance instanceof instead instructions intended intent into invalid inverse inversion invoke is issues it italic item item- items items-baseline items-center items-start its itself javascript justify-between justify-center justify-end justify-start katex kbd keep keeps kept keyboard keyboard-shortcut keydown keystroke keywords khroma label landing language-switcher last:border-b-0 later launch layout leading-relaxed leading-snug leading-tight leaf- leak leaves leaving left left-0 left:calc legend legitimate letter-spacing lg lg:block lg:border lg:border-fg lg:border-solid lg:flex lg:flex-col lg:flex-row lg:gap-hsp-xl lg:grid-cols-[repeat(auto-fit,minmax(12rem,1fr))] lg:h-[90vh] lg:hidden lg:justify-start lg:m-auto lg:max-h-[90vh] lg:max-w-[52.5rem] lg:ml-[var(--zd-sidebar-w)] lg:pt-vsp-2xl lg:px-hsp-2xl lg:py-vsp-2xl lg:text-left lg:w-[90vw] lg:w-[clamp(16rem,25%,22rem)] li li2 light like likely line linger link link- links list-disc list-none listener literal literals lives llms llms-txt load local locale locales log longest-match look lostpointercapture lower lowercased luminance m m-0 m21 m6 main make malformed malicious maps mark marks matches matching math math-display math-inline max max-h-[80vh] max-h-[85vh] max-h-[90vh] max-h-full max-h-none max-w-[46rem] max-w-[85%] max-w-[85vw] max-w-[90vw] max-w-[clamp(50rem,75vw,90rem)] max-w-full max-w-none max-width may mb-0 mb-vsp-2xs mb-vsp-lg mb-vsp-md mb-vsp-sm mb-vsp-xl mb-vsp-xs measures measuring mechanism menu merged mermaid message messages meta meta-knob metadata min-h-[60vh] min-h-[calc(100vh-3.5rem)] min-h-screen min-w-0 min-w-[10rem] min-w-[8rem] mirror mirrors missing ml-auto ml-hsp-2xl ml-hsp-lg ml-hsp-sm ml-hsp-xl mod mode mounted mouseenter mouseleave mr-hsp-sm mt-0 mt-vsp-2xl mt-vsp-2xs mt-vsp-3xs mt-vsp-lg mt-vsp-md mt-vsp-sm mt-vsp-xl must mutates mutation mutations mx-auto my-vsp-lg my-vsp-md name named native nav nav-active nav-card- nav/doc navigating navigation navigations near needs new newly-swapped next no no-enlarge no-op no-repeat no-underline node node:buffer node:fs node:fs/promises node:module node:path nodes nofollow noindex non-empty non-persisted non-string none noopener noreferrer normal noscript not not-object note now null number numeric object object-contain observe observer occurred of off og:description og:image og:image:alt og:image:height og:image:width og:title og:type og:url ol old older on once one only opacity-60 open open/close option or original other others out outline-none over overflow overflow-auto overflow-hidden overflow-x-auto overflow-y-auto overflow-y:auto overwrite own p p-0 p-hsp-lg p-hsp-md p-hsp-sm p-hsp-xl package package-owned packages padding page page-loading page-loading-overlay page-loading-spinner page-navigate-end page-title pages pages/. paint pan panel panels parent parse parsed pass passed path paths pattern pb-[50vh] pb-vsp-md pb-vsp-xl pb-vsp-xs per per-call per-link permanently persisted pi pick picked picks picocolors pins pipelines pl-[1.25rem] pl-hsp-lg pl-hsp-sm pl-hsp-xl place placeholder placeholder:text-muted plain plural plus pnpm pointer-events-none pointercancel pointerdown pointermove pointerup polite polygon polyline populates port position position:fixed pr-[4px] pr-hsp-lg pr-hsp-md pr-hsp-sm pr-hsp-xl pre pre-lowercased preact preact/compat preact/hooks preact/jsx-runtime preconnect preload pres preserved preserving print produce produced produces production project propagating properties property props prose provided proxy pt-[0.15rem] pt-[2px] pt-vsp-3xs pt-vsp-md pt-vsp-sm pt-vsp-xl pt-vsp-xs ptag- public purely px px-hsp-2xl px-hsp-2xs px-hsp-lg px-hsp-md px-hsp-sm px-hsp-xl px-hsp-xs py-0 py-[2px] py-[calc(var(--spacing-vsp-xs)+0.15rem)] py-hsp-2xs py-hsp-sm py-hsp-xs py-vsp-2xs py-vsp-3xs py-vsp-lg py-vsp-md py-vsp-sm py-vsp-xl py-vsp-xs q query question r radius raw re-encode/decode re-lowercase re-querying re-render re-renders re-run re-running re-selects reach reached reaches read reading reads ready real real-value received recorded recovers redefine ref- references refetch refreshes regenerate regenerates reinit reinits relative reload remains remove removed render rendered renders reorder repaint repeated repeating replaced replaces repopulate requires reserved resize resize-x resolve resolved resolves response restore restores result result-click results results-area retry return returns revision revisions rewrite right right- right-0 ro robots role root rotate-180 rotate-90 round round-trip rounded rounded-[0.75rem] rounded-bl-[0.25rem] rounded-bl-[1rem] rounded-br-[0.25rem] rounded-br-[1rem] rounded-full rounded-lg rounded-t-[1rem] rounds route router routes routes-src running runs runtime s safe safer same same-locale samp scale scanned schema-mismatch schema-missing scheme score scored script script- script-eval script-evaluation script-injection scripts scroll scrollbar scrolled scrollend search search-index searched section section- see sel-bg sel-fg select select-none self self-start semibold sentinel separator server server-rendered set sets setting setup shadow-[0_1px_3px_color-mix(in_srgb,var(--color-fg)_8%,transparent)] shadow-lg shape share shared sharing shiki ship ships short shortcut should show shown shrink-0 sidebar sidebar- sidebar-toggle-island sidebar-tree-island signal similarity single singular site-search site-tree-nav-island sitemap- sites size skill skills skipping skips slash slug sm:border sm:border-muted sm:flex-row sm:grid-cols-2 sm:h-auto sm:items-center sm:justify-between sm:max-h-[80vh] sm:max-w-[52rem] sm:mx-auto sm:my-[10vh] sm:rounded-lg small smooth snapshot snapshots so soft soft-nav solid some somehow source sources space-y-vsp-2xs spacing span spans spec specifier specifiers square sr-only src stale start state status stay sticky still stop stored stray string strings strip stripe stroke-linecap stroke-linejoin stroke-width strong stronger style style-attribute styled styles stylesheet sub subagents subsequent success successful summary sup support surfaces survives svg swap swapped swaps synchronous synchronously syntactically syntect t tab tab-item tab-panel tabindex table tablist tabpanel tabs tabs-container tabs-content tabs-nav tag tag- tag-item- tags tags:audit take tbody td temp temp-element template temporary temporary-element terminal terms test-results text text-accent text-bg text-body text-caption text-center text-chat-assistant-text text-chat-user-text text-code-fg text-danger text-decoration text-display text-fg text-heading text-info text-left text-micro text-muted text-muted/50 text-right text-small text-title text-warning text/plain textarea tfoot th that the thead their them theme theme-color theme-toggle theme/token then there these they this through throw tighten time tip title to toc toggle toggle-ai-chat toggle-design-token-panel toggles token tokens tolerates too toolbar top-0 top-[3.5rem] top-full top-level total touches tp tr tracked tracking-wider trade-off transition transition-[background,color,border-color] transition-[left,color] transition-colors transition-transform translate-x-0 translations transparent treats tree tree-child- tree-item- tree-top- trick trigger trigger:ai-chat trigger:design-token-panel triggers true truncate truncated try turn twitter:card twitter:creator twitter:description twitter:image twitter:site twitter:title two type typography u ul unavailable unchanged undefined under underline underlines understand unknown unmaintained unobserve unreadable unrelated unreleased unreliable unset unsupported up uppercase usage use used user uses utf-8 utf8 utilities utility v v2 val value value-reader values var variable variant version version- version-menu version-switcher vertical via video viewport virtual:zudo-doc-route-context visible vitesse-dark vocabulary w w-1/2 w-[0.5rem] w-[0.625rem] w-[0.875rem] w-[1.125rem] w-[1.575rem] w-[1.5rem] w-[1.75rem] w-[14px] w-[16px] w-[16rem] w-[18px] w-[280px] w-[2rem] w-[320px] w-[90vw] w-[var(--zd-sidebar-w)] w-dvw w-full w-icon-lg w-icon-md w-icon-sm w-icon-xs want warning was watching wbr wbr- we website weight went were what when where whereas whether which while whitespace-nowrap whitespace-pre whole width will with without word working worktrees would wrap wrapper wrappers written wrong wrote xl:flex xl:hidden y-scrollbar yet you your z-dropdown z-local-1 z-modal z-modal-backdrop z-sidebar z-toolbar zd-content zd-desktop-sidebar-toggle zd-doc-content-band zd-enlarge-btn zd-enlarge-dialog zd-enlarge-dialog-close zd-enlargeable zd-html-preview-code zd-mermaid-dialog zd-mermaid-enlargeable zd-mermaid-tool-btn zd-mermaid-toolbar zd-mermaid-transform zd-mermaid-viewport zd-sidebar-content-wrapper zd-sidebar-open zfb zfb:after-swap zfb:before-preparation zod zoom zudo-doc-design-tokens/v1 zudo-doc-sidebar-visible zudo-doc-sidebar-width zudo-doc-theme zudo-doc-theme-bridge");
@@ -439,6 +439,7 @@ const LeafNode = memo(function LeafNode2({
439
439
  {
440
440
  href: node.href,
441
441
  "aria-current": isActive ? "page" : void 0,
442
+ "data-nav-active": !isRoot && isActive ? "" : void 0,
442
443
  className: isRoot ? `flex items-start gap-hsp-xs py-[calc(var(--spacing-vsp-xs)+0.15rem)] pr-[4px] text-small font-semibold break-words ${isActive ? "bg-fg text-bg" : "text-fg hover:underline focus:underline"}` : `block py-vsp-2xs pr-[4px] text-small break-words ${isActive ? "bg-fg font-medium text-bg" : "text-muted hover:underline focus:underline"}`,
443
444
  style: { paddingLeft },
444
445
  children: [
@@ -2,7 +2,9 @@ import { jsx } from "preact/jsx-runtime";
2
2
  import { Island } from "@takazudo/zfb";
3
3
  import { SidebarTree } from "../sidebar-tree-island/index.js";
4
4
  import { deriveNavDataPrep } from "../chrome/derive.js";
5
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
5
6
  function createSidebarWithDefaults(ctx) {
7
+ assertChromeContext(ctx, "createSidebarWithDefaults");
6
8
  const defaultLocale = ctx.defaultLocale;
7
9
  const localeCount = ctx.locales.length;
8
10
  const t = ctx.t;
@@ -10,7 +10,9 @@ import { createHeaderWithDefaults } from "../header-with-defaults/index.js";
10
10
  import { createFooterWithDefaults } from "../footer-with-defaults/index.js";
11
11
  import { createDocHistoryArea } from "../doc-history-area/index.js";
12
12
  import { deriveComposeMetaTitle, deriveBodyEndIslands } from "../chrome/derive.js";
13
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
13
14
  function createTagPages(ctx) {
15
+ assertChromeContext(ctx, "createTagPages");
14
16
  const settings = ctx.settings;
15
17
  const defaultLocale = ctx.defaultLocale;
16
18
  const t = ctx.t;
package/dist/toc/toc.js CHANGED
@@ -14,6 +14,7 @@ function Toc({ headings, title = "On this page" }) {
14
14
  "nav",
15
15
  {
16
16
  "aria-label": "Table of contents",
17
+ "data-zd-toc": true,
17
18
  className: cx(
18
19
  "hidden xl:flex flex-col",
19
20
  "w-[280px] shrink-0",
@@ -5,7 +5,9 @@ import { createHeadWithDefaults } from "../head-with-defaults/index.js";
5
5
  import { createHeaderWithDefaults } from "../header-with-defaults/index.js";
6
6
  import { createFooterWithDefaults } from "../footer-with-defaults/index.js";
7
7
  import { deriveComposeMetaTitle, deriveBodyEndIslands } from "../chrome/derive.js";
8
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
8
9
  function createVersionsPageView(ctx) {
10
+ assertChromeContext(ctx, "createVersionsPageView");
9
11
  const settings = ctx.settings;
10
12
  const defaultLocale = ctx.defaultLocale;
11
13
  const t = ctx.t;
@@ -11,6 +11,7 @@ import type { JSX } from "preact";
11
11
  import { ChevronLeft, ChevronRight } from "../icons/index.js";
12
12
  import type { ChromeContext } from "../factory-context/index.js";
13
13
  import type { Settings } from "../settings.js";
14
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
14
15
 
15
16
  // NavNode is a superset; we only need the fields the pager uses.
16
17
  interface PagerNode {
@@ -35,6 +36,7 @@ export interface DocPagerProps {
35
36
  export function createDocPager<S extends Settings = Settings>(
36
37
  ctx: ChromeContext<S>,
37
38
  ): (props: DocPagerProps) => JSX.Element {
39
+ assertChromeContext(ctx, "createDocPager");
38
40
  const t = ctx.t;
39
41
 
40
42
  /**
@@ -564,6 +564,7 @@ const LeafNode = memo(function LeafNode({
564
564
  <a
565
565
  href={node.href}
566
566
  aria-current={isActive ? "page" : undefined}
567
+ data-nav-active={!isRoot && isActive ? "" : undefined}
567
568
  className={isRoot
568
569
  ? `flex items-start gap-hsp-xs py-[calc(var(--spacing-vsp-xs)+0.15rem)] pr-[4px] text-small font-semibold break-words ${
569
570
  isActive ? "bg-fg text-bg" : "text-fg hover:underline focus:underline"
package/eject/toc/toc.tsx CHANGED
@@ -70,6 +70,7 @@ export function Toc({ headings, title = "On this page" }: TocProps): VNode {
70
70
  return (
71
71
  <nav
72
72
  aria-label="Table of contents"
73
+ data-zd-toc
73
74
  className={cx(
74
75
  "hidden xl:flex flex-col",
75
76
  "w-[280px] shrink-0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/zudo-doc",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "description": "zudo-doc framework primitives layer that sits on top of zfb's engine — sidebar, theme, TOC, breadcrumb, layouts, head injection, View Transitions, SSR-skip wrappers (per ADR-003).",
6
6
  "license": "MIT",
@@ -163,6 +163,10 @@
163
163
  "types": "./dist/preset.d.ts",
164
164
  "default": "./dist/preset.js"
165
165
  },
166
+ "./component-tokens": {
167
+ "types": "./dist/config/component-tokens.d.ts",
168
+ "default": "./dist/config/component-tokens.js"
169
+ },
166
170
  "./plugins/doc-history": {
167
171
  "types": "./dist/plugins/doc-history.d.ts",
168
172
  "default": "./dist/plugins/doc-history.js"
@@ -510,6 +514,7 @@
510
514
  }
511
515
  },
512
516
  "bin": {
517
+ "gen-component-tokens": "./bin/gen-component-tokens.mjs",
513
518
  "gen-z-index": "./bin/gen-z-index.mjs",
514
519
  "tags-audit": "./bin/tags-audit.mjs",
515
520
  "zudo-doc": "./bin/zudo-doc.mjs"
@@ -525,8 +530,8 @@
525
530
  "preact": "^10.29.1",
526
531
  "@takazudo/zfb": "^0.1.0-next.71",
527
532
  "@takazudo/zfb-runtime": "^0.1.0-next.71",
528
- "@takazudo/zudo-doc-history-server": "^1.2.0",
529
- "@takazudo/zdtp": "^0.3.3",
533
+ "@takazudo/zudo-doc-history-server": "^2.1.0",
534
+ "@takazudo/zdtp": "^0.4.1",
530
535
  "shiki": "^4.0.2",
531
536
  "zod": "^4.3.6",
532
537
  "katex": "^0.16.0",