@takazudo/zudo-doc 4.2.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +3 -3
  3. package/bin/zudo-doc.mjs +54 -13
  4. package/dist/auto-logo/index.d.ts +18 -0
  5. package/dist/auto-logo/index.js +33 -0
  6. package/dist/auto-logo/shapes.d.ts +32 -0
  7. package/dist/auto-logo/shapes.js +183 -0
  8. package/dist/auto-logo/standalone.d.ts +7 -0
  9. package/dist/auto-logo/standalone.js +35 -0
  10. package/dist/color-scheme-utils.d.ts +4 -3
  11. package/dist/color-scheme-utils.js +1 -1
  12. package/dist/config/component-tokens.js +2 -2
  13. package/dist/config.d.ts +23 -1
  14. package/dist/config.js +3 -0
  15. package/dist/content.css +4 -3
  16. package/dist/doc-content-header/index.js +1 -1
  17. package/dist/doc-history/index.js +1 -1
  18. package/dist/doc-history-area/index.d.ts +1 -0
  19. package/dist/doc-history-area/index.js +3 -0
  20. package/dist/doc-page-shell/index.js +1 -1
  21. package/dist/doc-pager/index.js +2 -2
  22. package/dist/eject-logo/config-parse.d.ts +25 -0
  23. package/dist/eject-logo/config-parse.js +68 -0
  24. package/dist/eject-logo/config-rewriter.d.ts +22 -0
  25. package/dist/eject-logo/config-rewriter.js +68 -0
  26. package/dist/eject-logo/eject.d.ts +18 -0
  27. package/dist/eject-logo/eject.js +66 -0
  28. package/dist/eject-logo/index.d.ts +4 -0
  29. package/dist/eject-logo/index.js +16 -0
  30. package/dist/eject-logo/site-name.d.ts +21 -0
  31. package/dist/eject-logo/site-name.js +31 -0
  32. package/dist/features.css +8 -29
  33. package/dist/header/header.js +14 -6
  34. package/dist/header/nav-class-tokens.d.ts +12 -0
  35. package/dist/header/nav-class-tokens.js +80 -0
  36. package/dist/header/nav-overflow-script.js +33 -16
  37. package/dist/home-page/index.d.ts +34 -5
  38. package/dist/home-page/index.js +95 -38
  39. package/dist/home-page/prepare-home-data.d.ts +9 -0
  40. package/dist/home-page/prepare-home-data.js +10 -3
  41. package/dist/html-preview-wrapper/highlight-runtime.d.ts +40 -0
  42. package/dist/html-preview-wrapper/highlight-runtime.js +58 -0
  43. package/dist/html-preview-wrapper/highlighted-code.d.ts +3 -2
  44. package/dist/html-preview-wrapper/highlighted-code.js +11 -30
  45. package/dist/i18n-defaults/index.js +3 -0
  46. package/dist/metainfo/doc-metainfo.js +35 -22
  47. package/dist/plugins/doc-history.js +6 -2
  48. package/dist/plugins/internal/doc-history/index.d.ts +2 -0
  49. package/dist/plugins/internal/doc-history/index.js +5 -0
  50. package/dist/plugins/internal/doc-history/pre-build.d.ts +2 -0
  51. package/dist/plugins/internal/doc-history/pre-build.js +3 -0
  52. package/dist/preset.d.ts +1 -0
  53. package/dist/preset.js +2 -1
  54. package/dist/routes/index.js +13 -3
  55. package/dist/routes/locale-index.js +12 -2
  56. package/dist/safelist.css +1 -1
  57. package/dist/settings.d.ts +15 -0
  58. package/dist/sidebar-tree-island/index.js +3 -3
  59. package/dist/theme-cli/config-scanner.js +47 -0
  60. package/dist/theme-packs/academia/fonts/CourierPrime-400-latin.woff2 +0 -0
  61. package/dist/theme-packs/academia/fonts/CourierPrime-700-latin.woff2 +0 -0
  62. package/dist/theme-packs/academia/fonts/OFL.txt +94 -0
  63. package/dist/theme-packs/academia/fonts/SourceSerif4-italic-latin.woff2 +0 -0
  64. package/dist/theme-packs/academia/fonts/SourceSerif4-latin.woff2 +0 -0
  65. package/dist/theme-packs/academia/meta.json +37 -0
  66. package/dist/theme-packs/academia/pack.css +505 -0
  67. package/dist/theme-packs/bauhaus/fonts/ArchivoBlack-latin.woff2 +0 -0
  68. package/dist/theme-packs/bauhaus/fonts/Jost-latin.woff2 +0 -0
  69. package/dist/theme-packs/bauhaus/fonts/OFL.txt +95 -0
  70. package/dist/theme-packs/bauhaus/fonts/SpaceMono-400-latin.woff2 +0 -0
  71. package/dist/theme-packs/bauhaus/fonts/SpaceMono-700-latin.woff2 +0 -0
  72. package/dist/theme-packs/bauhaus/meta.json +38 -0
  73. package/dist/theme-packs/bauhaus/pack.css +614 -0
  74. package/dist/theme-packs/blueprint/fonts/IBMPlexMono-400-latin-ext.woff2 +0 -0
  75. package/dist/theme-packs/blueprint/fonts/IBMPlexMono-400-latin.woff2 +0 -0
  76. package/dist/theme-packs/blueprint/fonts/IBMPlexMono-500-latin-ext.woff2 +0 -0
  77. package/dist/theme-packs/blueprint/fonts/IBMPlexMono-500-latin.woff2 +0 -0
  78. package/dist/theme-packs/blueprint/fonts/Inter-latin-ext.woff2 +0 -0
  79. package/dist/theme-packs/blueprint/fonts/Inter-latin.woff2 +0 -0
  80. package/dist/theme-packs/blueprint/fonts/JosefinSans-latin.woff2 +0 -0
  81. package/dist/theme-packs/blueprint/fonts/OFL.txt +95 -0
  82. package/dist/theme-packs/blueprint/meta.json +38 -0
  83. package/dist/theme-packs/blueprint/pack.css +577 -0
  84. package/dist/theme-packs/botanica/fonts/CormorantGaramond-latin.woff2 +0 -0
  85. package/dist/theme-packs/botanica/fonts/CourierPrime-400-latin-ext.woff2 +0 -0
  86. package/dist/theme-packs/botanica/fonts/CourierPrime-400-latin.woff2 +0 -0
  87. package/dist/theme-packs/botanica/fonts/CourierPrime-700-latin-ext.woff2 +0 -0
  88. package/dist/theme-packs/botanica/fonts/CourierPrime-700-latin.woff2 +0 -0
  89. package/dist/theme-packs/botanica/fonts/CrimsonPro-latin-italic.woff2 +0 -0
  90. package/dist/theme-packs/botanica/fonts/CrimsonPro-latin.woff2 +0 -0
  91. package/dist/theme-packs/botanica/fonts/OFL.txt +95 -0
  92. package/dist/theme-packs/botanica/meta.json +38 -0
  93. package/dist/theme-packs/botanica/pack.css +546 -0
  94. package/dist/theme-packs/brutalist/meta.json +1 -1
  95. package/dist/theme-packs/brutalist/pack.css +10 -9
  96. package/dist/theme-packs/eink/fonts/CourierPrime-400-latin-ext.woff2 +0 -0
  97. package/dist/theme-packs/eink/fonts/CourierPrime-400-latin.woff2 +0 -0
  98. package/dist/theme-packs/eink/fonts/CourierPrime-700-latin-ext.woff2 +0 -0
  99. package/dist/theme-packs/eink/fonts/CourierPrime-700-latin.woff2 +0 -0
  100. package/dist/theme-packs/eink/fonts/EBGaramond-latin-italic.woff2 +0 -0
  101. package/dist/theme-packs/eink/fonts/EBGaramond-latin.woff2 +0 -0
  102. package/dist/theme-packs/eink/fonts/OFL.txt +94 -0
  103. package/dist/theme-packs/eink/meta.json +37 -0
  104. package/dist/theme-packs/eink/pack.css +490 -0
  105. package/dist/theme-packs/foundry/meta.json +1 -1
  106. package/dist/theme-packs/foundry/pack.css +2 -2
  107. package/dist/theme-packs/hearth/meta.json +1 -1
  108. package/dist/theme-packs/hearth/pack.css +15 -4
  109. package/dist/theme-packs/nocturne/pack.css +4 -0
  110. package/dist/theme-packs/observatory/meta.json +1 -1
  111. package/dist/theme-packs/observatory/pack.css +4 -1
  112. package/dist/theme-packs/onyx/pack.css +6 -0
  113. package/dist/theme-packs/phosphor/pack.css +4 -0
  114. package/dist/theme-packs/riso/fonts/ArchivoBlack-latin.woff2 +0 -0
  115. package/dist/theme-packs/riso/fonts/OFL.txt +95 -0
  116. package/dist/theme-packs/riso/fonts/SpaceGrotesk-latin.woff2 +0 -0
  117. package/dist/theme-packs/riso/fonts/SpaceMono-400-italic-latin.woff2 +0 -0
  118. package/dist/theme-packs/riso/fonts/SpaceMono-400-latin.woff2 +0 -0
  119. package/dist/theme-packs/riso/fonts/SpaceMono-700-latin.woff2 +0 -0
  120. package/dist/theme-packs/riso/meta.json +38 -0
  121. package/dist/theme-packs/riso/pack.css +602 -0
  122. package/dist/theme-packs/sakura/fonts/MPlus1Code-400-latin.woff2 +0 -0
  123. package/dist/theme-packs/sakura/fonts/MPlus1Code-500-latin.woff2 +0 -0
  124. package/dist/theme-packs/sakura/fonts/Nunito-latin.woff2 +0 -0
  125. package/dist/theme-packs/sakura/fonts/OFL.txt +95 -0
  126. package/dist/theme-packs/sakura/fonts/ShipporiMincho-600-latin.woff2 +0 -0
  127. package/dist/theme-packs/sakura/fonts/ShipporiMincho-700-latin.woff2 +0 -0
  128. package/dist/theme-packs/sakura/meta.json +38 -0
  129. package/dist/theme-packs/sakura/pack.css +473 -0
  130. package/dist/theme-packs/scandi/fonts/Manrope-latin.woff2 +0 -0
  131. package/dist/theme-packs/scandi/fonts/OFL.txt +94 -0
  132. package/dist/theme-packs/scandi/fonts/SplineSansMono-latin.woff2 +0 -0
  133. package/dist/theme-packs/scandi/meta.json +37 -0
  134. package/dist/theme-packs/scandi/pack.css +370 -0
  135. package/dist/theme-packs/sumi/meta.json +1 -1
  136. package/dist/theme-packs/sumi/pack.css +4 -1
  137. package/dist/theme-packs/swissgrid/pack.css +4 -0
  138. package/dist/theme-packs/tidepool/fonts/JetBrainsMono-latin-ext.woff2 +0 -0
  139. package/dist/theme-packs/tidepool/fonts/JetBrainsMono-latin.woff2 +0 -0
  140. package/dist/theme-packs/tidepool/fonts/OFL.txt +94 -0
  141. package/dist/theme-packs/tidepool/fonts/SpaceGrotesk-latin.woff2 +0 -0
  142. package/dist/theme-packs/tidepool/meta.json +37 -0
  143. package/dist/theme-packs/tidepool/pack.css +425 -0
  144. package/dist/theme-packs/timberline/fonts/Fraunces-italic-latin.woff2 +0 -0
  145. package/dist/theme-packs/timberline/fonts/Fraunces-latin.woff2 +0 -0
  146. package/dist/theme-packs/timberline/fonts/JetBrainsMono-latin.woff2 +0 -0
  147. package/dist/theme-packs/timberline/fonts/OFL.txt +95 -0
  148. package/dist/theme-packs/timberline/fonts/SourceSans3-italic-latin.woff2 +0 -0
  149. package/dist/theme-packs/timberline/fonts/SourceSans3-latin.woff2 +0 -0
  150. package/dist/theme-packs/timberline/meta.json +38 -0
  151. package/dist/theme-packs/timberline/pack.css +509 -0
  152. package/dist/theme-packs/washi/meta.json +1 -1
  153. package/dist/theme-packs/washi/pack.css +6 -26
  154. package/dist/theme-packs-registry/validator.js +24 -1
  155. package/dist/theme.css +1 -1
  156. package/eject/doc-history/index.tsx +1 -1
  157. package/eject/doc-pager/index.tsx +2 -2
  158. package/eject/header/header.tsx +16 -8
  159. package/eject/header/nav-class-tokens.ts +115 -0
  160. package/eject/header/nav-overflow-script.ts +49 -16
  161. package/eject/sidebar-tree-island/index.tsx +4 -4
  162. package/package.json +16 -13
  163. package/routes-src/index.tsx +10 -3
  164. package/routes-src/locale-index.tsx +9 -2
@@ -14,6 +14,7 @@ export interface DocHistoryMetaEntry {
14
14
  /** Settings subset read by the DocHistoryArea factory. */
15
15
  export interface DocHistoryAreaSettings {
16
16
  docHistory: boolean;
17
+ docHistoryExclude?: string[];
17
18
  bodyFootUtilArea: {
18
19
  viewSourceLink?: boolean;
19
20
  } | false | undefined;
@@ -1,5 +1,6 @@
1
1
  import { jsx, jsxs } from "preact/jsx-runtime";
2
2
  import { Island } from "@takazudo/zfb";
3
+ import { compileExclude } from "@takazudo/zudo-doc-history-server/exclude";
3
4
  import { BodyFootUtilArea } from "../body-foot-util/index.js";
4
5
  import { toHistorySlug } from "../slug/index.js";
5
6
  import { buildGitHubSourceUrl as buildGitHubSourceUrlBase } from "../github-helpers/index.js";
@@ -8,6 +9,7 @@ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
8
9
  function createDocHistoryArea(ctx) {
9
10
  assertChromeContext(ctx, "createDocHistoryArea");
10
11
  const settings = ctx.settings;
12
+ const isHistoryExcluded = compileExclude(settings.docHistoryExclude ?? []);
11
13
  const defaultLocale = ctx.defaultLocale;
12
14
  const docHistoryMeta = ctx.hostBindings.docHistoryMeta ?? {};
13
15
  const t = ctx.t;
@@ -28,6 +30,7 @@ function createDocHistoryArea(ctx) {
28
30
  }) {
29
31
  if (!settings.docHistory) return null;
30
32
  const historySlug = toHistorySlug(slug);
33
+ if (isHistoryExcluded(historySlug)) return null;
31
34
  const effectiveHistoryLocale = isFallback ? defaultLocale : locale;
32
35
  const composedSlug = effectiveHistoryLocale === defaultLocale ? historySlug : `${effectiveHistoryLocale}/${historySlug}`;
33
36
  const meta = docHistoryMeta[composedSlug];
@@ -133,7 +133,7 @@ function createDocPageShell(ctx) {
133
133
  Fragment (not <div>) so children become direct children of
134
134
  <article class="zd-content">, picking up the flow-space rule. */
135
135
  /* @__PURE__ */ jsxs(Fragment, { children: [
136
- /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-xs", children: autoIndexLabel }),
136
+ /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold border-b border-fg pb-vsp-xs mb-vsp-xs", children: autoIndexLabel }),
137
137
  metainfoSlot,
138
138
  description && /* @__PURE__ */ jsx("p", { class: "mb-vsp-lg text-title text-muted", "data-doc-description": true, children: description }),
139
139
  /* @__PURE__ */ jsx(NavCardGrid, { children: autoIndexChildren ?? [] })
@@ -12,7 +12,7 @@ function createDocPager(ctx) {
12
12
  href: prev.href,
13
13
  class: "group border border-muted rounded-lg p-hsp-lg hover:border-accent",
14
14
  children: [
15
- /* @__PURE__ */ jsxs("div", { class: "flex items-center gap-hsp-xs text-caption text-muted mb-vsp-2xs", children: [
15
+ /* @__PURE__ */ jsxs("div", { class: "flex items-center gap-hsp-xs text-caption text-muted mb-vsp-2xs group-hover:text-accent group-focus-visible:text-accent", children: [
16
16
  /* @__PURE__ */ jsx(ChevronLeft, { className: "h-[1.125rem] w-[1.125rem]" }),
17
17
  /* @__PURE__ */ jsx("span", { class: "no-underline", children: t("nav.previous", locale) })
18
18
  ] }),
@@ -26,7 +26,7 @@ function createDocPager(ctx) {
26
26
  href: next.href,
27
27
  class: "group border border-muted rounded-lg p-hsp-lg hover:border-accent text-right",
28
28
  children: [
29
- /* @__PURE__ */ jsxs("div", { class: "flex items-center justify-end gap-hsp-xs text-caption text-muted mb-vsp-2xs", children: [
29
+ /* @__PURE__ */ jsxs("div", { class: "flex items-center justify-end gap-hsp-xs text-caption text-muted mb-vsp-2xs group-hover:text-accent group-focus-visible:text-accent", children: [
30
30
  /* @__PURE__ */ jsx("span", { class: "no-underline", children: t("nav.next", locale) }),
31
31
  /* @__PURE__ */ jsx(ChevronRight, { className: "h-[1.125rem] w-[1.125rem]" })
32
32
  ] }),
@@ -0,0 +1,25 @@
1
+ import { type TopLevelMember } from "../theme-cli/config-scanner.js";
2
+ export interface ParsedZudoDocConfig {
3
+ ok: true;
4
+ members: TopLevelMember[];
5
+ /** Index of the zudoDoc({ ... }) call's opening `{`. */
6
+ braceOpenIdx: number;
7
+ /** Index of the matching closing `}`. */
8
+ braceCloseIdx: number;
9
+ }
10
+ export interface ParsedZudoDocConfigRefusal {
11
+ ok: false;
12
+ /** Human-readable WHY, with no field-specific hint appended — callers own
13
+ * wording their own remediation (a read-only lookup vs. a write refusal
14
+ * need different follow-up text). */
15
+ reason: string;
16
+ }
17
+ export type ParseZudoDocConfigResult = ParsedZudoDocConfig | ParsedZudoDocConfigRefusal;
18
+ /**
19
+ * Locate and validate the project's `zfb.config.ts` canonical shape — a
20
+ * single `zudoDoc({ ... })` call whose sole argument is a literal,
21
+ * non-spread object — and return its top-level members. Mirrors
22
+ * `theme-cli/config-rewriter.ts#applyThemePackToConfigSource`'s validation
23
+ * exactly, generalized to any caller field.
24
+ */
25
+ export declare function parseZudoDocConfigMembers(source: string): ParseZudoDocConfigResult;
@@ -0,0 +1,68 @@
1
+ import {
2
+ ConfigSyntaxError,
3
+ findMatchingBrace,
4
+ findZudoDocCallParens,
5
+ skipWhitespaceAndComments,
6
+ splitTopLevelMembers
7
+ } from "../theme-cli/config-scanner.js";
8
+ function parseZudoDocConfigMembers(source) {
9
+ let calls;
10
+ try {
11
+ calls = findZudoDocCallParens(source);
12
+ } catch (err) {
13
+ return { ok: false, reason: `could not parse zfb.config.ts (${err.message}).` };
14
+ }
15
+ if (calls.length === 0) {
16
+ return {
17
+ ok: false,
18
+ reason: "no zudoDoc(...) call found in zfb.config.ts. This CLI only reads/rewrites the canonical generated shape: export default defineConfig(zudoDoc({ ... }))."
19
+ };
20
+ }
21
+ if (calls.length > 1) {
22
+ return {
23
+ ok: false,
24
+ reason: `found ${calls.length} zudoDoc(...) call sites in zfb.config.ts \u2014 cannot safely determine which one to use.`
25
+ };
26
+ }
27
+ const openParenIdx = calls[0];
28
+ let braceOpenIdx;
29
+ let braceCloseIdx;
30
+ let afterBrace;
31
+ try {
32
+ braceOpenIdx = skipWhitespaceAndComments(source, openParenIdx + 1);
33
+ if (source[braceOpenIdx] !== "{") {
34
+ return {
35
+ ok: false,
36
+ reason: "zudoDoc(...) is not called with a plain object literal (found a computed or non-literal argument instead of zudoDoc({ ... })). This CLI only reads/rewrites the canonical generated shape."
37
+ };
38
+ }
39
+ braceCloseIdx = findMatchingBrace(source, braceOpenIdx);
40
+ afterBrace = skipWhitespaceAndComments(source, braceCloseIdx + 1);
41
+ } catch (err) {
42
+ return { ok: false, reason: `could not parse zfb.config.ts (${err.message}).` };
43
+ }
44
+ const afterOptionalComma = source[afterBrace] === "," ? skipWhitespaceAndComments(source, afterBrace + 1) : afterBrace;
45
+ if (source[afterOptionalComma] !== ")") {
46
+ return {
47
+ ok: false,
48
+ reason: "zudoDoc(...) is called with more than one argument \u2014 not the canonical single-object-literal shape."
49
+ };
50
+ }
51
+ let members;
52
+ try {
53
+ members = splitTopLevelMembers(source, braceOpenIdx + 1, braceCloseIdx);
54
+ } catch (err) {
55
+ return { ok: false, reason: `could not parse zfb.config.ts (${err.message}).` };
56
+ }
57
+ const spread = members.find((m) => m.isSpread);
58
+ if (spread) {
59
+ return {
60
+ ok: false,
61
+ reason: "zudoDoc({ ...spread }) uses a spread argument \u2014 not the canonical generated shape. This CLI only reads/rewrites a literal, hand-editable field list."
62
+ };
63
+ }
64
+ return { ok: true, members, braceOpenIdx, braceCloseIdx };
65
+ }
66
+ export {
67
+ parseZudoDocConfigMembers
68
+ };
@@ -0,0 +1,22 @@
1
+ export declare const EJECTED_LOGO_VALUE = "/img/logo.svg";
2
+ export interface ApplyLogoFieldToSourceOk {
3
+ ok: true;
4
+ /** The full rewritten file contents. Identical to the input when
5
+ * `changed` is false (idempotent re-run of an already-ejected logo). */
6
+ source: string;
7
+ changed: boolean;
8
+ mode: "replaced" | "inserted-after-site-name" | "inserted-first";
9
+ }
10
+ export interface ApplyLogoFieldToSourceRefusal {
11
+ ok: false;
12
+ /** Human-readable WHY, ending with the manual one-liner to add by hand.
13
+ * The input source is never touched when this is returned. */
14
+ reason: string;
15
+ }
16
+ export type ApplyLogoFieldToSourceResult = ApplyLogoFieldToSourceOk | ApplyLogoFieldToSourceRefusal;
17
+ /**
18
+ * Apply `logo: "/img/logo.svg"` to a `zfb.config.ts` source string. Pure and
19
+ * filesystem-free — callers own reading/writing the actual file (see
20
+ * `eject.ts`).
21
+ */
22
+ export declare function applyLogoFieldToConfigSource(source: string): ApplyLogoFieldToSourceResult;
@@ -0,0 +1,68 @@
1
+ import { isPlainStringLiteralSpan } from "../theme-cli/config-scanner.js";
2
+ import { parseZudoDocConfigMembers } from "./config-parse.js";
3
+ const EJECTED_LOGO_VALUE = "/img/logo.svg";
4
+ function manualHint() {
5
+ return `Add the field manually inside your zudoDoc({ ... }) call: logo: ${JSON.stringify(EJECTED_LOGO_VALUE)},`;
6
+ }
7
+ function refuse(reason) {
8
+ return { ok: false, reason: `${reason} ${manualHint()}` };
9
+ }
10
+ function isPlainFalseLiteralSpan(source, start, end) {
11
+ return source.slice(start, end).trim() === "false";
12
+ }
13
+ function detectIndent(source, members) {
14
+ const ref = members[0];
15
+ const refPos = ref ? ref.keyStart ?? ref.memberStart : null;
16
+ if (refPos !== null && refPos !== void 0) {
17
+ const newlineIdx = source.lastIndexOf("\n", refPos);
18
+ const lineStart = newlineIdx === -1 ? 0 : newlineIdx + 1;
19
+ const line = source.slice(lineStart, refPos);
20
+ if (/^[ \t]*$/.test(line) && line.length > 0) return line;
21
+ }
22
+ return " ";
23
+ }
24
+ function applyLogoFieldToConfigSource(source) {
25
+ const parsed = parseZudoDocConfigMembers(source);
26
+ if (!parsed.ok) return refuse(parsed.reason);
27
+ const { members, braceOpenIdx } = parsed;
28
+ const newValueLiteral = JSON.stringify(EJECTED_LOGO_VALUE);
29
+ const logoMembers = members.filter((m) => m.key === "logo");
30
+ if (logoMembers.length > 1) {
31
+ return refuse(
32
+ 'zfb.config.ts declares "logo" more than once inside zudoDoc({ ... }). This CLI only rewrites a single canonical field; remove the duplicate and re-run.'
33
+ );
34
+ }
35
+ const logoMember = logoMembers[0];
36
+ if (logoMember) {
37
+ if (logoMember.valueStart === null || logoMember.valueEnd === null) {
38
+ return refuse('the existing "logo" field in zfb.config.ts is not a simple `key: value` property this CLI understands.');
39
+ }
40
+ const isReplaceableLiteral = isPlainStringLiteralSpan(source, logoMember.valueStart, logoMember.valueEnd) || isPlainFalseLiteralSpan(source, logoMember.valueStart, logoMember.valueEnd);
41
+ if (!isReplaceableLiteral) {
42
+ return refuse(
43
+ 'the existing "logo" field\'s value is not a plain string literal or `false` \u2014 cannot safely rewrite it without risking corruption.'
44
+ );
45
+ }
46
+ const newSource2 = source.slice(0, logoMember.valueStart) + newValueLiteral + source.slice(logoMember.valueEnd);
47
+ return { ok: true, source: newSource2, changed: newSource2 !== source, mode: "replaced" };
48
+ }
49
+ const siteNameMember = members.find((m) => m.key === "siteName");
50
+ const indent = detectIndent(source, members);
51
+ if (siteNameMember) {
52
+ const insertAt2 = siteNameMember.commaIndex !== null ? siteNameMember.commaIndex + 1 : siteNameMember.memberEnd;
53
+ const prefix = siteNameMember.commaIndex !== null ? "" : ",";
54
+ const insertText2 = `${prefix}
55
+ ${indent}logo: ${newValueLiteral},`;
56
+ const newSource2 = source.slice(0, insertAt2) + insertText2 + source.slice(insertAt2);
57
+ return { ok: true, source: newSource2, changed: true, mode: "inserted-after-site-name" };
58
+ }
59
+ const insertAt = braceOpenIdx + 1;
60
+ const insertText = `
61
+ ${indent}logo: ${newValueLiteral},`;
62
+ const newSource = source.slice(0, insertAt) + insertText + source.slice(insertAt);
63
+ return { ok: true, source: newSource, changed: true, mode: "inserted-first" };
64
+ }
65
+ export {
66
+ EJECTED_LOGO_VALUE,
67
+ applyLogoFieldToConfigSource
68
+ };
@@ -0,0 +1,18 @@
1
+ export interface EjectLogoOptions {
2
+ cwd?: string;
3
+ /** Explicit seed override (already validated non-empty by the CLI arg
4
+ * parser in bin/zudo-doc.mjs). When omitted, the seed is resolved from
5
+ * zfb.config.ts's siteName field. */
6
+ seed?: string;
7
+ force?: boolean;
8
+ }
9
+ export interface EjectLogoResult {
10
+ ok: boolean;
11
+ /** Human-readable outcome message (success confirmation, or a refusal
12
+ * explaining WHY). */
13
+ message: string;
14
+ /** True when the SVG was written but the config rewrite was refused — a
15
+ * nonzero-exit partial success, not a total failure. */
16
+ partial?: boolean;
17
+ }
18
+ export declare function ejectLogo(options?: EjectLogoOptions): Promise<EjectLogoResult>;
@@ -0,0 +1,66 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import { renderAutoLogoStandaloneSvg } from "../auto-logo/standalone.js";
4
+ import { applyLogoFieldToConfigSource } from "./config-rewriter.js";
5
+ import { resolveSiteNameFromConfigSource } from "./site-name.js";
6
+ const ZFB_CONFIG_FILENAME = "zfb.config.ts";
7
+ const LOGO_RELATIVE_PATH = path.join("public", "img", "logo.svg");
8
+ const DEFAULT_SEED = "Docs";
9
+ async function ejectLogo(options = {}) {
10
+ const cwd = options.cwd ?? process.cwd();
11
+ const force = options.force ?? false;
12
+ const configPath = path.join(cwd, ZFB_CONFIG_FILENAME);
13
+ if (!await fs.pathExists(configPath)) {
14
+ return {
15
+ ok: false,
16
+ message: `zfb.config.ts not found at ${configPath}.
17
+ Run this command from your project root.`
18
+ };
19
+ }
20
+ const source = await fs.readFile(configPath, "utf8");
21
+ let seed;
22
+ if (options.seed) {
23
+ seed = options.seed;
24
+ } else {
25
+ const resolution = resolveSiteNameFromConfigSource(source);
26
+ if (resolution.kind === "unresolvable") {
27
+ return {
28
+ ok: false,
29
+ message: `Could not determine a logo seed from zfb.config.ts: ${resolution.reason}
30
+ Pass --seed <name> to specify the logo seed explicitly.`
31
+ };
32
+ }
33
+ seed = resolution.kind === "literal" ? resolution.value : DEFAULT_SEED;
34
+ }
35
+ const rewriteResult = applyLogoFieldToConfigSource(source);
36
+ const svgPath = path.join(cwd, LOGO_RELATIVE_PATH);
37
+ if (!force && await fs.pathExists(svgPath)) {
38
+ return {
39
+ ok: false,
40
+ message: `${LOGO_RELATIVE_PATH} already exists.
41
+ Pass --force to overwrite it.`
42
+ };
43
+ }
44
+ const svg = renderAutoLogoStandaloneSvg(seed);
45
+ await fs.ensureDir(path.dirname(svgPath));
46
+ await fs.writeFile(svgPath, svg, "utf8");
47
+ if (!rewriteResult.ok) {
48
+ return {
49
+ ok: false,
50
+ partial: true,
51
+ message: `Wrote ${LOGO_RELATIVE_PATH}, but could not update zfb.config.ts: ${rewriteResult.reason}`
52
+ };
53
+ }
54
+ if (rewriteResult.changed) {
55
+ await fs.writeFile(configPath, rewriteResult.source, "utf8");
56
+ }
57
+ const verb = rewriteResult.changed ? "Ejected logo" : "Ejected logo (field already set)";
58
+ return {
59
+ ok: true,
60
+ message: `${verb} \u2192 ${LOGO_RELATIVE_PATH}
61
+ Updated zfb.config.ts's logo field.`
62
+ };
63
+ }
64
+ export {
65
+ ejectLogo
66
+ };
@@ -0,0 +1,4 @@
1
+ export { ejectLogo, type EjectLogoOptions, type EjectLogoResult } from "./eject.js";
2
+ export { applyLogoFieldToConfigSource, EJECTED_LOGO_VALUE, type ApplyLogoFieldToSourceOk, type ApplyLogoFieldToSourceRefusal, type ApplyLogoFieldToSourceResult, } from "./config-rewriter.js";
3
+ export { resolveSiteNameFromConfigSource, type SiteNameResolution } from "./site-name.js";
4
+ export { parseZudoDocConfigMembers, type ParsedZudoDocConfig, type ParsedZudoDocConfigRefusal, type ParseZudoDocConfigResult, } from "./config-parse.js";
@@ -0,0 +1,16 @@
1
+ import { ejectLogo } from "./eject.js";
2
+ import {
3
+ applyLogoFieldToConfigSource,
4
+ EJECTED_LOGO_VALUE
5
+ } from "./config-rewriter.js";
6
+ import { resolveSiteNameFromConfigSource } from "./site-name.js";
7
+ import {
8
+ parseZudoDocConfigMembers
9
+ } from "./config-parse.js";
10
+ export {
11
+ EJECTED_LOGO_VALUE,
12
+ applyLogoFieldToConfigSource,
13
+ ejectLogo,
14
+ parseZudoDocConfigMembers,
15
+ resolveSiteNameFromConfigSource
16
+ };
@@ -0,0 +1,21 @@
1
+ export type SiteNameResolution = {
2
+ kind: "literal";
3
+ value: string;
4
+ } | {
5
+ kind: "absent";
6
+ } | {
7
+ kind: "unresolvable";
8
+ reason: string;
9
+ };
10
+ /**
11
+ * Resolve `siteName` from a `zfb.config.ts` source string.
12
+ *
13
+ * - Literal `siteName: "..."` → `{ kind: "literal", value }`.
14
+ * - Canonical literal config with no `siteName` field → `{ kind: "absent" }`
15
+ * (the runtime default `"Docs"` genuinely applies — this IS a confirmed
16
+ * reading, the caller decides the default value).
17
+ * - Anything else (no/duplicate zudoDoc call, spread argument, computed or
18
+ * duplicate `siteName`, non-string value) → `{ kind: "unresolvable",
19
+ * reason }`; the caller must require `--seed`.
20
+ */
21
+ export declare function resolveSiteNameFromConfigSource(source: string): SiteNameResolution;
@@ -0,0 +1,31 @@
1
+ import { isPlainStringLiteralSpan, unquoteStringLiteralSpan } from "../theme-cli/config-scanner.js";
2
+ import { parseZudoDocConfigMembers } from "./config-parse.js";
3
+ function resolveSiteNameFromConfigSource(source) {
4
+ const parsed = parseZudoDocConfigMembers(source);
5
+ if (!parsed.ok) return { kind: "unresolvable", reason: parsed.reason };
6
+ const siteNameMembers = parsed.members.filter((m) => m.key === "siteName");
7
+ if (siteNameMembers.length > 1) {
8
+ return {
9
+ kind: "unresolvable",
10
+ reason: 'zfb.config.ts declares "siteName" more than once inside zudoDoc({ ... }) \u2014 cannot safely determine which one the runtime reads.'
11
+ };
12
+ }
13
+ const member = siteNameMembers[0];
14
+ if (!member) return { kind: "absent" };
15
+ if (member.valueStart === null || member.valueEnd === null) {
16
+ return {
17
+ kind: "unresolvable",
18
+ reason: 'the "siteName" field in zfb.config.ts is not a simple `key: value` property this CLI understands.'
19
+ };
20
+ }
21
+ if (!isPlainStringLiteralSpan(source, member.valueStart, member.valueEnd)) {
22
+ return {
23
+ kind: "unresolvable",
24
+ reason: `the "siteName" field's value in zfb.config.ts is not a plain string literal.`
25
+ };
26
+ }
27
+ return { kind: "literal", value: unquoteStringLiteralSpan(source, member.valueStart, member.valueEnd) };
28
+ }
29
+ export {
30
+ resolveSiteNameFromConfigSource
31
+ };
package/dist/features.css CHANGED
@@ -179,31 +179,10 @@ header[data-header] {
179
179
  --zfb-hi-hd: var(--zd-syntax-keyword);
180
180
  }
181
181
 
182
- /* HTML Preview remains on Shiki until #2742. Its dual-theme variables resolve
183
- * through color-scheme. These selectors intentionally exclude `.hi-root`,
184
- * whose colors come from the semantic bridge above. */
185
- [data-theme] .shiki,
186
- [data-theme] .shiki span {
187
- color: light-dark(var(--shiki-light), var(--shiki-dark));
188
- font-style: light-dark(
189
- var(--shiki-light-font-style, inherit),
190
- var(--shiki-dark-font-style, inherit)
191
- );
192
- }
193
-
194
- /* Background only on the <pre> wrapper — deliberately NOT on token spans. The
195
- * --shiki-*-bg vars are set inline on the <pre> and inherit down to spans;
196
- * painting them per-span would lay an opaque base background over the
197
- * translucent line-/word-highlight backgrounds that sit on the enclosing
198
- * .line span (see .line.highlighted below). */
199
- [data-theme] .shiki {
200
- background-color: light-dark(var(--shiki-light-bg), var(--shiki-dark-bg));
201
- }
202
-
203
- /* HtmlPreview component — Shiki code blocks
204
- * Token colors inherit from the global [data-theme] .shiki rule above.
205
- * These rules only adjust layout and background for the preview context. */
206
- .zd-html-preview-code pre.shiki {
182
+ /* HtmlPreview component zfb semantic code blocks. Token colors and the root
183
+ * surface resolve through the shared class-mode bridge above; these rules only
184
+ * adjust layout for the preview context. */
185
+ .zd-html-preview-code pre.hi-root {
207
186
  margin: 0;
208
187
  padding: var(--spacing-hsp-md);
209
188
  font-size: var(--text-caption);
@@ -211,7 +190,7 @@ header[data-header] {
211
190
  overflow-x: auto;
212
191
  }
213
192
 
214
- .zd-html-preview-code pre.shiki code {
193
+ .zd-html-preview-code pre.hi-root code {
215
194
  font-family: var(--font-mono);
216
195
  white-space: pre;
217
196
  }
@@ -266,9 +245,9 @@ pre.hi-root .line .highlighted-word {
266
245
  * zfb's :::code-group emits <CodeGroup tabs={[...]}> with one
267
246
  * <pre data-lang="…">{RAW text}</pre> child per fence. These <pre> elements
268
247
  * are NOT syntax-highlighted (the Rust pipeline doesn't highlight inside
269
- * code-group fences), so they get no `.hi-root` class. Apply explicit
270
- * code-block visual treatment via tokens to match the highlighted code blocks
271
- * surrounding them.
248
+ * code-group fences), so they get no `.hi-root` class.
249
+ * Apply explicit code-block visual treatment via tokens to match the
250
+ * highlighted code blocks surrounding them.
272
251
  * ======================================== */
273
252
 
274
253
  .code-group-panel pre[data-lang] {
@@ -6,6 +6,14 @@ import {
6
6
  pathForMatch
7
7
  } from "./nav-active.js";
8
8
  import { NAV_OVERFLOW_SCRIPT } from "./nav-overflow-script.js";
9
+ import {
10
+ NAV_CHEVRON_ACTIVE,
11
+ NAV_CHEVRON_INACTIVE,
12
+ NAV_CHILD_ACTIVE,
13
+ NAV_CHILD_INACTIVE,
14
+ NAV_TOP_ACTIVE,
15
+ NAV_TOP_INACTIVE
16
+ } from "./nav-class-tokens.js";
9
17
  import { LANGUAGE_SWITCHER_INIT_SCRIPT } from "../i18n-version/language-switcher.js";
10
18
  import { VERSION_SWITCHER_REWIRE_SCRIPT } from "../i18n-version/version-switcher.js";
11
19
  import { GitHub as GitHubIcon } from "../icons/index.js";
@@ -81,7 +89,7 @@ function Header(props) {
81
89
  "button",
82
90
  {
83
91
  type: "button",
84
- class: "px-hsp-md py-vsp-2xs text-small font-medium text-muted hover:underline cursor-pointer",
92
+ class: "px-hsp-md py-vsp-2xs text-small font-medium text-muted hover:text-accent hover:underline focus-visible:text-accent focus-visible:underline cursor-pointer",
85
93
  "data-nav-more-toggle": true,
86
94
  "aria-expanded": "false",
87
95
  children: "\xB7\xB7\xB7"
@@ -177,7 +185,7 @@ function renderNavItem(item, activeNavPath, activeCategory, lang, currentVersion
177
185
  "aria-expanded": "false",
178
186
  class: [
179
187
  "flex items-center gap-x-hsp-xs px-hsp-md py-vsp-2xs text-small font-medium transition-colors",
180
- isActive ? "bg-fg text-bg" : "text-muted hover:underline focus:underline"
188
+ isActive ? NAV_TOP_ACTIVE.join(" ") : NAV_TOP_INACTIVE.join(" ")
181
189
  ].join(" "),
182
190
  children: [
183
191
  label,
@@ -186,7 +194,7 @@ function renderNavItem(item, activeNavPath, activeCategory, lang, currentVersion
186
194
  {
187
195
  class: [
188
196
  "h-[0.5rem] w-[0.5rem] shrink-0",
189
- isActive ? "text-bg" : "text-muted"
197
+ isActive ? NAV_CHEVRON_ACTIVE.join(" ") : NAV_CHEVRON_INACTIVE.join(" ")
190
198
  ].join(" "),
191
199
  fill: "none",
192
200
  viewBox: "0 0 24 24",
@@ -216,8 +224,8 @@ function renderNavItem(item, activeNavPath, activeCategory, lang, currentVersion
216
224
  href: childHref,
217
225
  "data-active": childActive ? "" : void 0,
218
226
  class: [
219
- "block px-hsp-md py-vsp-2xs text-small hover:bg-accent/10 hover:underline",
220
- childActive ? "font-bold text-accent" : "text-fg"
227
+ "block px-hsp-md py-vsp-2xs text-small hover:bg-accent/10 hover:underline focus-visible:underline",
228
+ childActive ? NAV_CHILD_ACTIVE.join(" ") : NAV_CHILD_INACTIVE.join(" ")
221
229
  ].join(" "),
222
230
  children: childLabel
223
231
  }
@@ -235,7 +243,7 @@ function renderNavItem(item, activeNavPath, activeCategory, lang, currentVersion
235
243
  "data-nav-item": true,
236
244
  class: [
237
245
  "px-hsp-md py-vsp-2xs text-small font-medium transition-colors shrink-0",
238
- isActive ? "bg-fg text-bg" : "text-muted hover:underline focus:underline"
246
+ isActive ? NAV_TOP_ACTIVE.join(" ") : NAV_TOP_INACTIVE.join(" ")
239
247
  ].join(" "),
240
248
  children: label
241
249
  }
@@ -0,0 +1,12 @@
1
+ export declare const NAV_TOP_ACTIVE: readonly string[];
2
+ export declare const NAV_TOP_INACTIVE: readonly string[];
3
+ export declare const NAV_CHEVRON_ACTIVE: readonly string[];
4
+ export declare const NAV_CHEVRON_INACTIVE: readonly string[];
5
+ export declare const NAV_CHILD_ACTIVE: readonly string[];
6
+ export declare const NAV_CHILD_INACTIVE: readonly string[];
7
+ export declare const NAV_MENU_PARENT: readonly string[];
8
+ export declare const NAV_MENU_PARENT_ACTIVE_SUFFIX: readonly string[];
9
+ export declare const NAV_MENU_PLAIN: readonly string[];
10
+ export declare const NAV_MENU_PLAIN_ACTIVE_SUFFIX: readonly string[];
11
+ export declare const NAV_MENU_CHILD_ACTIVE: readonly string[];
12
+ export declare const NAV_MENU_CHILD_INACTIVE: readonly string[];
@@ -0,0 +1,80 @@
1
+ const NAV_TOP_ACTIVE = ["bg-fg", "text-bg"];
2
+ const NAV_TOP_INACTIVE = [
3
+ "text-muted",
4
+ "hover:text-accent",
5
+ "hover:underline",
6
+ "focus:underline",
7
+ "focus:text-accent"
8
+ ];
9
+ const NAV_CHEVRON_ACTIVE = ["text-bg"];
10
+ const NAV_CHEVRON_INACTIVE = ["text-muted"];
11
+ const NAV_CHILD_ACTIVE = ["font-bold", "text-accent"];
12
+ const NAV_CHILD_INACTIVE = [
13
+ "text-fg",
14
+ "hover:text-accent",
15
+ "focus-visible:text-accent"
16
+ ];
17
+ const MENU_ROW_BASE = [
18
+ "block",
19
+ "px-hsp-md",
20
+ "py-vsp-2xs",
21
+ "text-small"
22
+ ];
23
+ const MENU_CHILD_BASE = [
24
+ "block",
25
+ "pl-hsp-xl",
26
+ "pr-hsp-md",
27
+ "py-vsp-2xs",
28
+ "text-small"
29
+ ];
30
+ const MENU_ROW_RESTING = [
31
+ "hover:bg-accent/10",
32
+ "hover:underline",
33
+ "focus-visible:underline",
34
+ "focus-visible:text-accent",
35
+ "text-fg",
36
+ "hover:text-accent"
37
+ ];
38
+ const NAV_MENU_PARENT = [
39
+ ...MENU_ROW_BASE,
40
+ "font-bold",
41
+ ...MENU_ROW_RESTING
42
+ ];
43
+ const NAV_MENU_PARENT_ACTIVE_SUFFIX = ["text-accent"];
44
+ const NAV_MENU_PLAIN = [
45
+ ...MENU_ROW_BASE,
46
+ ...MENU_ROW_RESTING
47
+ ];
48
+ const NAV_MENU_PLAIN_ACTIVE_SUFFIX = [
49
+ ...NAV_CHILD_ACTIVE
50
+ ];
51
+ const NAV_MENU_CHILD_ACTIVE = [
52
+ ...MENU_CHILD_BASE,
53
+ ...NAV_CHILD_ACTIVE,
54
+ "hover:bg-accent/10",
55
+ "hover:underline",
56
+ "focus-visible:underline"
57
+ ];
58
+ const NAV_MENU_CHILD_INACTIVE = [
59
+ ...MENU_CHILD_BASE,
60
+ "text-fg",
61
+ "hover:bg-accent/10",
62
+ "hover:text-accent",
63
+ "hover:underline",
64
+ "focus-visible:underline",
65
+ "focus-visible:text-accent"
66
+ ];
67
+ export {
68
+ NAV_CHEVRON_ACTIVE,
69
+ NAV_CHEVRON_INACTIVE,
70
+ NAV_CHILD_ACTIVE,
71
+ NAV_CHILD_INACTIVE,
72
+ NAV_MENU_CHILD_ACTIVE,
73
+ NAV_MENU_CHILD_INACTIVE,
74
+ NAV_MENU_PARENT,
75
+ NAV_MENU_PARENT_ACTIVE_SUFFIX,
76
+ NAV_MENU_PLAIN,
77
+ NAV_MENU_PLAIN_ACTIVE_SUFFIX,
78
+ NAV_TOP_ACTIVE,
79
+ NAV_TOP_INACTIVE
80
+ };