@takazudo/zudo-doc 8.0.0 → 8.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +2 -2
  3. package/dist/asset-index-page/body.js +9 -9
  4. package/dist/compiled.css +15 -8
  5. package/dist/config-assertions/index.d.ts +13 -0
  6. package/dist/config-assertions/index.js +25 -0
  7. package/dist/config.d.ts +12 -1
  8. package/dist/config.js +2 -0
  9. package/dist/diff-loader.d.ts +3 -0
  10. package/dist/diff-loader.js +13 -0
  11. package/dist/doc-history/index.js +6 -5
  12. package/dist/doclayout/doc-layout.js +5 -1
  13. package/dist/features.css +24 -0
  14. package/dist/header/header.d.ts +9 -0
  15. package/dist/header/header.js +21 -2
  16. package/dist/header-with-defaults/index.d.ts +3 -1
  17. package/dist/header-with-defaults/index.js +1 -0
  18. package/dist/home-page/index.js +1 -1
  19. package/dist/katex-loader.d.ts +3 -0
  20. package/dist/katex-loader.js +12 -0
  21. package/dist/math-block/index.js +4 -9
  22. package/dist/md-utils/index.d.ts +8 -2
  23. package/dist/md-utils/index.js +10 -3
  24. package/dist/nav-indexing/note-tray-index-parts/card-list.js +25 -21
  25. package/dist/nav-indexing/note-tray-index-parts/date-line.d.ts +3 -1
  26. package/dist/nav-indexing/note-tray-index-parts/date-line.js +17 -10
  27. package/dist/nav-indexing/note-tray-index-parts/index-list.js +4 -3
  28. package/dist/plugins/diff-loader.d.ts +4 -0
  29. package/dist/plugins/diff-loader.js +13 -0
  30. package/dist/plugins/katex-loader.d.ts +4 -0
  31. package/dist/plugins/katex-loader.js +13 -0
  32. package/dist/preset.d.ts +6 -2
  33. package/dist/preset.js +14 -1
  34. package/dist/settings-defaults.js +1 -0
  35. package/dist/settings.d.ts +21 -0
  36. package/dist/sidebar-tree-island/index.js +2 -6
  37. package/dist/site-tree-nav-island/index.js +11 -8
  38. package/dist/theme-packs/brutalist/pack.css +2 -2
  39. package/dist/transitions/persisted-focus-settle-bootstrap.d.ts +2 -0
  40. package/dist/transitions/persisted-focus-settle-bootstrap.js +22 -0
  41. package/dist/tree-nav-shared/index.d.ts +2 -0
  42. package/dist/tree-nav-shared/index.js +8 -1
  43. package/dist/wind.json +0 -2
  44. package/eject/doc-history/index.tsx +13 -9
  45. package/eject/header/header.tsx +40 -0
  46. package/eject/sidebar-tree-island/index.tsx +2 -8
  47. package/eject/site-tree-nav-island/index.tsx +15 -8
  48. package/package.json +24 -8
package/CHANGELOG.md CHANGED
@@ -8,6 +8,46 @@ The format is based on Keep a Changelog, and release notes are generated from th
8
8
 
9
9
  No unreleased changes.
10
10
 
11
+ ## [8.1.1] - 2026-10-26
12
+
13
+ ### Bug Fixes
14
+
15
+ - Search-index bodies, llms.txt descriptions and agent-export fallback descriptions now read as plain prose.
16
+ - Inline code keeps its text and only the backticks are dropped. Before, `` `pnpm approve-builds` `` was deleted, leaving fragments like "by default ()".
17
+ - Whole-line directive fences are removed and their label or title text is kept. This covers `:::warning[Title]`, `:::note Title`, `:::name{attrs}`, nested `::::` and bare closing `:::`. (9d0bffb5, 64cae6a6)
18
+ - Sites that don't use math and don't install the optional `katex` peer no longer get a Rolldown `UNRESOLVED_IMPORT 'katex'` warning on every build.
19
+ - math-block now loads KaTeX through the package-owned `@takazudo/zudo-doc/katex-loader` subpath. With `math` off, the preset replaces that subpath with a stub.
20
+ - Bare `katex` imports in your own code are untouched.
21
+ - `` now requires `math: true`, as already documented. With `math` off it reports the missing-katex error even when `katex` happens to be installed. (82c24ea5, 64cae6a6)
22
+ - Sites with `docHistory` off that don't install the optional `diff` peer no longer get an `UNRESOLVED_IMPORT 'diff'` warning.
23
+ - The doc-history Compare view loads `diff` through the package-owned `@takazudo/zudo-doc/diff-loader` subpath. With `docHistory` off, the preset replaces that subpath with a stub.
24
+ - The dates-only mode (`docHistoryUi: false`) keeps the real loader. (6e6434ca)
25
+ - Some row descendants now follow their own row's hover and focus-visible accent, while keeping their resting colors and the inverted current-item style. This covers:
26
+ - numbered index ranks
27
+ - linked note-tray dates (index, compact and wide card stamps)
28
+ - sidebar tray and site-tree note-tray ranks and dates
29
+ - asset-tree folder and file icons, names and metadata (4c271316, 0efa5cbf)
30
+
31
+ ### Other Changes
32
+
33
+ - Requires zfb 5.1.3: the `@takazudo/zfb`, `zfb-runtime` and `zfb-md-wasm` peer floors move from `^5.1.2` to `^5.1.3`. zfb 5.1.3 rebuilds the native engine and WASM binaries; the JavaScript API is unchanged. Update the zfb family (and `zfb-adapter-cloudflare` when used) to 5.1.3 together with this release. (62648582)
34
+
35
+ ## [8.1.0] - 2026-10-25
36
+
37
+ ### Features
38
+
39
+ - Add the opt-in `headerBrand` setting (`false | { icon: string }`, default `false`): a decorative square icon painted with the text colour (CSS mask) before the site name in the header. `/`-prefixed paths receive the site `base` once, other URLs are used as-is, and an empty or missing `icon` (or `true`) throws at config resolution. The site name stays the link's accessible name, long names still ellipsize, and theme-pack leading emblems are hidden while an icon is set. (fb3358ca)
40
+ - Add a `data-zd-site-name` attribute to the home hero `<h1>` as a styling hook for the site name. (fb3358ca)
41
+
42
+ ### Bug Fixes
43
+
44
+ - With dynamic page transitions enabled, navigating from a footer link (or any focused link in persisted chrome) now finishes at the top of the destination page on current Chromium builds instead of staying near the footer. A package-owned inline head script settles the browser's queued focus reveal after the persisted DOM move and before the router applies its top, hash or history scroll. Focus, persistence, transitions and scroll restoration are unchanged. Sites that pin inline-script hashes in a Content Security Policy need to add the new script's hash. (524a440f)
45
+ - In `SiteTreeNav`, labels now always start a small gap after the end of their dashed branch. On wide viewports the double-width homepage branches used to run into the label text. (3e6e720f)
46
+
47
+ ### Other Changes
48
+
49
+ - Requires zfb 5.1.2: the `@takazudo/zfb`, `zfb-runtime` and `zfb-md-wasm` peer floors move from `^5.0.1` to `^5.1.2`. zfb 5.1.0 adds an islands build guard: the build now fails if an emitted islands chunk imports the islands entry, because that import would 404 in production. zfb 5.1.1 is a fix release for Windows hosts (bare-package subpath aliases and `pnpm.cmd` spawning) and for projects built from a symlinked path. zfb 5.1.2 ships rebuilt native engine and WASM binaries with no change to the JavaScript API. Chunking is otherwise unchanged. Upgrade the zfb family (and `zfb-adapter-cloudflare` when used) to 5.1.2 together with this release. (6eef9416, 44d7684b)
50
+
11
51
  ## [8.0.0] - 2026-10-24
12
52
 
13
53
  ### Breaking Changes
package/README.md CHANGED
@@ -109,7 +109,7 @@ If your project renders **semi-trusted or user-submitted** HTML in a preview, ov
109
109
 
110
110
  **6.0.0** is the coordinated major release of `@takazudo/zudo-doc`,
111
111
  `@takazudo/zudo-doc-history-server` and `create-zudo-doc`. The current workspace
112
- targets the published **zfb 5.0.1** family (peer floor **^5.0.1**).
112
+ targets the published **zfb 5.1.3** family (peer floor **^5.1.3**).
113
113
  [Upstream #4097](https://github.com/Takazudo/zudo-front-builder/issues/4097) is
114
114
  resolved; its strict published 4.3.1 consumer control passed 3/3 with no skips or
115
115
  retries in the [source-bound verification](../../docs/findings/zfb-4.3.1/verification.md).
@@ -126,7 +126,7 @@ are feature-dependent. Package defaults for `agentExport`, `mcp`, and `llmsTxt`
126
126
  are all `false`. The create-zudo-doc initializer enables `llmsTxt` by default;
127
127
  agent export and MCP remain opt-in.
128
128
 
129
- For the panel, the installed **zdtp 0.8.6** manifest carries **Preact ^10.29.1** as
129
+ For the panel, the installed **zdtp 0.8.7** manifest carries **Preact ^10.29.1** as
130
130
  its own dependency (it was a peer through 0.8.5) and exports `./styles.css` (also `./styles`) to `dist/zdtp.css`. Its internal
131
131
  Tailwind browser dependency and Preact subtree are isolated from host JSX.
132
132
  Install `@takazudo/zdtp` and import `@takazudo/zdtp/styles.css` when enabling it.
@@ -48,15 +48,15 @@ function iconFor(asset) {
48
48
  function AssetTree({ node, base, routePrefix, locale, fileCountLabel, fileCountSingleLabel, linesLabel, root = false }) {
49
49
  return /* @__PURE__ */ jsxs("ul", { "data-zd-asset-tree": root ? true : void 0, children: [
50
50
  node.dirs.map((dir) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("details", { open: true, children: [
51
- /* @__PURE__ */ jsxs("summary", { class: "flex cursor-pointer items-center gap-hsp-xs rounded px-hsp-sm py-vsp-3xs text-small hover:bg-accent/10 focus-visible:bg-accent/10", children: [
52
- /* @__PURE__ */ jsx(ChevronRight, { class: "h-icon-xs w-icon-xs shrink-0 text-muted transition-transform" }),
53
- /* @__PURE__ */ jsx(Folder, { class: "h-icon-sm w-icon-sm shrink-0 text-muted" }),
54
- /* @__PURE__ */ jsx(FolderOpen, { class: "h-icon-sm w-icon-sm shrink-0 text-muted" }),
55
- /* @__PURE__ */ jsxs("span", { class: "min-w-0 truncate font-mono text-fg", children: [
51
+ /* @__PURE__ */ jsxs("summary", { class: "group flex cursor-pointer items-center gap-hsp-xs rounded px-hsp-sm py-vsp-3xs text-small hover:bg-accent/10 focus-visible:bg-accent/10", children: [
52
+ /* @__PURE__ */ jsx(ChevronRight, { class: "h-icon-xs w-icon-xs shrink-0 text-muted transition-transform group-hover:text-accent group-focus-visible:text-accent" }),
53
+ /* @__PURE__ */ jsx(Folder, { class: "h-icon-sm w-icon-sm shrink-0 text-muted group-hover:text-accent group-focus-visible:text-accent" }),
54
+ /* @__PURE__ */ jsx(FolderOpen, { class: "h-icon-sm w-icon-sm shrink-0 text-muted group-hover:text-accent group-focus-visible:text-accent" }),
55
+ /* @__PURE__ */ jsxs("span", { class: "min-w-0 truncate font-mono text-fg group-hover:text-accent group-focus-visible:text-accent", children: [
56
56
  dir.name,
57
57
  "/"
58
58
  ] }),
59
- /* @__PURE__ */ jsxs("span", { class: "ml-auto hidden whitespace-nowrap pl-hsp-lg text-caption text-muted sm:block", children: [
59
+ /* @__PURE__ */ jsxs("span", { class: "ml-auto hidden whitespace-nowrap pl-hsp-lg text-caption text-muted sm:block group-hover:text-accent group-focus-visible:text-accent", children: [
60
60
  countLabel(dir.fileCount, fileCountLabel, fileCountSingleLabel),
61
61
  " \xB7 ",
62
62
  formatAssetBytes(dir.bytes)
@@ -68,10 +68,10 @@ function AssetTree({ node, base, routePrefix, locale, fileCountLabel, fileCountS
68
68
  const Icon = iconFor(asset);
69
69
  const facet = facetLabel(asset, asset.lines !== void 0 ? linesLabel.replace("{count}", String(asset.lines)) : void 0);
70
70
  const meta = [kindLabel(asset), facet, formatAssetBytes(asset.bytes)].filter(Boolean).join(" \xB7 ");
71
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", { href: assetViewerHref({ base, routePrefix, path: asset.path, locale }), title: asset.name, class: "flex min-w-0 items-center gap-hsp-xs rounded px-hsp-sm py-vsp-3xs text-small text-fg hover:bg-accent/10 hover:text-accent hover:underline focus-visible:bg-accent/10 focus-visible:text-accent focus-visible:underline", children: [
72
- /* @__PURE__ */ jsx(Icon, { class: "h-icon-sm w-icon-sm shrink-0 text-muted" }),
71
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", { href: assetViewerHref({ base, routePrefix, path: asset.path, locale }), title: asset.name, class: "group flex min-w-0 items-center gap-hsp-xs rounded px-hsp-sm py-vsp-3xs text-small text-fg hover:bg-accent/10 hover:text-accent hover:underline focus-visible:bg-accent/10 focus-visible:text-accent focus-visible:underline", children: [
72
+ /* @__PURE__ */ jsx(Icon, { class: "h-icon-sm w-icon-sm shrink-0 text-muted group-hover:text-accent group-focus-visible:text-accent" }),
73
73
  /* @__PURE__ */ jsx("span", { class: "min-w-0 truncate font-mono", children: basename(asset.path) }),
74
- /* @__PURE__ */ jsx("span", { class: "ml-auto hidden whitespace-nowrap pl-hsp-lg text-caption text-muted sm:block", children: meta })
74
+ /* @__PURE__ */ jsx("span", { class: "ml-auto hidden whitespace-nowrap pl-hsp-lg text-caption text-muted sm:block group-hover:text-accent group-focus-visible:text-accent", children: meta })
75
75
  ] }) });
76
76
  })
77
77
  ] });
package/dist/compiled.css CHANGED
@@ -1864,6 +1864,21 @@ nav[data-zd-toc], div[data-zd-mobile-toc] {
1864
1864
  font-family: var(--zdc-toc-font, var(--zdc-chrome-font, var(--font-sans)));
1865
1865
  }
1866
1866
 
1867
+ [data-header-brand-icon] {
1868
+ display: inline-block;
1869
+ width: 1.1em;
1870
+ height: 1.1em;
1871
+ vertical-align: -.2em;
1872
+ margin-inline-end: var(--spacing-hsp-sm);
1873
+ background-color: currentColor;
1874
+ -webkit-mask: var(--zd-header-brand-icon) center/contain no-repeat;
1875
+ mask: var(--zd-header-brand-icon) center/contain no-repeat;
1876
+ }
1877
+
1878
+ html[data-theme-pack] [data-header-logo][data-header-brand]:before {
1879
+ content: none;
1880
+ }
1881
+
1867
1882
  button[data-zd-pending] {
1868
1883
  opacity: .7;
1869
1884
  pointer-events: none;
@@ -3593,11 +3608,6 @@ site-search [data-search-results] > :is([data-search-placeholder], p) {
3593
3608
  color: var(--zw-color-bg);
3594
3609
  }
3595
3610
  }
3596
- @media (hover: hover) {
3597
- :where(.group:hover) .group-hover\:text-fg {
3598
- color: var(--zw-color-fg);
3599
- }
3600
- }
3601
3611
  @media (hover: hover) {
3602
3612
  :where(.group:hover) .group-hover\:bg-fg {
3603
3613
  background-color: var(--zw-color-fg);
@@ -3609,9 +3619,6 @@ site-search [data-search-results] > :is([data-search-placeholder], p) {
3609
3619
  :where(.group:focus-visible) .group-focus-visible\:text-accent {
3610
3620
  color: var(--zw-color-accent);
3611
3621
  }
3612
- :where(.group:focus-visible) .group-focus-visible\:text-fg {
3613
- color: var(--zw-color-fg);
3614
- }
3615
3622
  @media (hover: hover) {
3616
3623
  :where(.peer:hover) ~ .peer-hover\:text-accent {
3617
3624
  color: var(--zw-color-accent);
@@ -55,6 +55,19 @@ export interface EmptyStringFaviconOrLogoSubject {
55
55
  * object is not an empty string).
56
56
  */
57
57
  export declare function assertNoEmptyStringFaviconOrLogo(settings: EmptyStringFaviconOrLogoSubject): void;
58
+ /** Structural subset of `Settings` {@link assertValidHeaderBrand} reads. */
59
+ export interface HeaderBrandSubject {
60
+ headerBrand?: unknown;
61
+ }
62
+ /**
63
+ * Validate `headerBrand`: `undefined`, `false`, or `{ icon: <non-empty string> }`.
64
+ *
65
+ * The value is typed `false | HeaderBrandConfig`, but JavaScript callers and
66
+ * casted values can still pass `true`, a bare string, an array, or an object
67
+ * whose `icon` is missing/empty — each of which would render a broken mask in
68
+ * every page header. Fail loudly at config resolution, naming the field.
69
+ */
70
+ export declare function assertValidHeaderBrand(settings: HeaderBrandSubject): void;
58
71
  /** Structural subset of `Settings` the zdtp-bundling helpers read. */
59
72
  export interface ZdtpBundlingSubject {
60
73
  designTokenPanel?: boolean;
@@ -32,6 +32,30 @@ function assertNoEmptyStringFaviconOrLogo(settings) {
32
32
  }
33
33
  }
34
34
  }
35
+ function assertValidHeaderBrand(settings) {
36
+ const value = settings.headerBrand;
37
+ if (value === void 0 || value === false) return;
38
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
39
+ throw new TypeError(
40
+ `Invalid headerBrand ${describeValue(value)}: pass false (or omit it) for no icon, or an object { icon: "/img/icon.svg" }.`
41
+ );
42
+ }
43
+ const icon = value.icon;
44
+ if (typeof icon !== "string" || icon === "") {
45
+ throw new TypeError(
46
+ `Invalid headerBrand.icon ${describeValue(icon)}: must be a non-empty string (a "/"-prefixed path, a data: URL, or an absolute URL).`
47
+ );
48
+ }
49
+ }
50
+ function describeValue(value) {
51
+ if (value === void 0) return "undefined";
52
+ if (Array.isArray(value)) return "(array)";
53
+ try {
54
+ return JSON.stringify(value) ?? String(value);
55
+ } catch {
56
+ return String(value);
57
+ }
58
+ }
35
59
  function resolvesBundleZdtp(settings) {
36
60
  return settings.bundleZdtp ?? settings.designTokenPanel === true;
37
61
  }
@@ -92,6 +116,7 @@ function warnAmbiguousDropdownCategoryMatch(headerNav, logger = console) {
92
116
  export {
93
117
  assertAgentDocsConsistent,
94
118
  assertNoEmptyStringFaviconOrLogo,
119
+ assertValidHeaderBrand,
95
120
  assertValidSearchMaxBodyLength,
96
121
  assertZdtpBundlingConsistent,
97
122
  resolvesBundleZdtp,
package/dist/config.d.ts CHANGED
@@ -70,7 +70,7 @@ import type { ZfbConfig, BundleConfig, WindConfig } from "@takazudo/zfb/config";
70
70
  import type { ZodType } from "zod";
71
71
  import type { DirectiveVocabulary, PresetTranslations, PresetTagVocabularyEntry } from "./preset.js";
72
72
  import type { ColorScheme } from "./color-scheme-utils.js";
73
- import type { Settings, ColorModeConfig, LocaleConfig, MetaTagsConfig, SiteHeadConfig, TagPlacement, TagGovernanceMode, VersionConfig, FooterConfig, HeaderNavItem, HeaderRightItem, HomeConfig, AssetViewerIndexingConfig, FrontmatterPreviewConfig, BodyFootUtilAreaConfig, HtmlPreviewConfig, FaviconConfig, DateFormatSetting } from "./settings.js";
73
+ import type { Settings, ColorModeConfig, LocaleConfig, MetaTagsConfig, SiteHeadConfig, TagPlacement, TagGovernanceMode, VersionConfig, FooterConfig, HeaderNavItem, HeaderRightItem, HomeConfig, AssetViewerIndexingConfig, FrontmatterPreviewConfig, BodyFootUtilAreaConfig, HtmlPreviewConfig, FaviconConfig, HeaderBrandConfig, DateFormatSetting } from "./settings.js";
74
74
  /** The `settings.claudeResources` block (or `false` when disabled). */
75
75
  type ClaudeResourcesConfig = {
76
76
  claudeDir: string;
@@ -132,6 +132,17 @@ export interface ZudoDocConfig {
132
132
  * @default undefined
133
133
  */
134
134
  favicon?: string | FaviconConfig | false;
135
+ /**
136
+ * Header brand icon rendered before the site name in the header logo
137
+ * anchor, as a decorative `currentColor` CSS mask (`aria-hidden`; the
138
+ * anchor's accessible name stays `siteName`). `false` renders the site name
139
+ * alone. A `HeaderBrandConfig` (`{ icon }`) paints `icon`: a value starting
140
+ * with `/` is prefixed with `base`, anything else (`data:`, `https://…`) is
141
+ * used verbatim. `true`, arrays, other non-objects, and a missing,
142
+ * non-string or empty `icon` throw a `TypeError` at config resolution.
143
+ * @default false
144
+ */
145
+ headerBrand?: false | HeaderBrandConfig;
135
146
  /**
136
147
  * Public URL sub-path prefix mounted in front of every absolute asset URL
137
148
  * (e.g. `"/pj/my-site/"`). `"/"` = root-mounted.
package/dist/config.js CHANGED
@@ -6,6 +6,7 @@ import { defaultColorSchemes } from "./color-schemes-defaults/index.js";
6
6
  import { assertNoCommaInVersionSlugs } from "./version-availability/index.js";
7
7
  import {
8
8
  assertNoEmptyStringFaviconOrLogo,
9
+ assertValidHeaderBrand,
9
10
  assertZdtpBundlingConsistent,
10
11
  assertValidSearchMaxBodyLength,
11
12
  assertAgentDocsConsistent
@@ -79,6 +80,7 @@ function zudoDoc(user = {}) {
79
80
  );
80
81
  assertNoCommaInVersionSlugs(settings.versions);
81
82
  assertNoEmptyStringFaviconOrLogo(settings);
83
+ assertValidHeaderBrand(settings);
82
84
  assertZdtpBundlingConsistent(settings);
83
85
  assertValidSearchMaxBodyLength(settings.searchMaxBodyLength);
84
86
  assertAgentDocsConsistent(settings);
@@ -0,0 +1,3 @@
1
+ export type DiffLines = typeof import("diff").diffLines;
2
+ /** diff's `diffLines`, or `null` when the optional peer is not installed. */
3
+ export declare const diffLines: DiffLines | null;
@@ -0,0 +1,13 @@
1
+ function pickDiffLines(m) {
2
+ let cur = m;
3
+ for (let i = 0; i < 3 && cur && typeof cur === "object"; i++) {
4
+ const fn = cur.diffLines;
5
+ if (typeof fn === "function") return fn;
6
+ cur = cur.default;
7
+ }
8
+ return null;
9
+ }
10
+ const diffLines = await import("diff").then(pickDiffLines, () => null);
11
+ export {
12
+ diffLines
13
+ };
@@ -95,12 +95,13 @@ async function getCachedDiff(olderHash, newerHash, olderContent, newerContent, i
95
95
  diffCache.set(key, hit);
96
96
  return hit;
97
97
  }
98
- const { diffLines } = await import("diff").then(
99
- (m) => m,
100
- () => {
101
- throw new Error('Compare requires the optional peer "diff": install it to use docHistory');
102
- }
98
+ const diffLines = await import("@takazudo/zudo-doc/diff-loader").then(
99
+ (m) => m.diffLines,
100
+ () => null
103
101
  );
102
+ if (!diffLines) {
103
+ throw new Error('Compare requires the optional peer "diff": install it to use docHistory');
104
+ }
104
105
  if (!isCurrent()) throw new Error("Comparison cancelled");
105
106
  const changes = diffLines(olderContent, newerContent);
106
107
  diffCache.set(key, changes);
@@ -1,6 +1,7 @@
1
1
  import { jsx, jsxs } from "@takazudo/zfb/zudo-react/jsx-runtime";
2
2
  import { ClientRouter } from "@takazudo/zfb-runtime";
3
3
  import { buildChromeTransitionBootstrap } from "../transitions/chrome-transition-bootstrap.js";
4
+ import { buildPersistedFocusSettleBootstrap } from "../transitions/persisted-focus-settle-bootstrap.js";
4
5
  const DESKTOP_SIDEBAR_ID = "desktop-sidebar";
5
6
  function DocLayout(props) {
6
7
  const {
@@ -71,7 +72,10 @@ function DocLayout(props) {
71
72
  );
72
73
  }
73
74
  if (head !== void 0) headChildren.push(head);
74
- if (enableClientRouter !== false) headChildren.push(/* @__PURE__ */ jsx("script", { rawHtml: buildChromeTransitionBootstrap() }));
75
+ if (enableClientRouter !== false) {
76
+ headChildren.push(/* @__PURE__ */ jsx("script", { rawHtml: buildChromeTransitionBootstrap() }));
77
+ headChildren.push(/* @__PURE__ */ jsx("script", { rawHtml: buildPersistedFocusSettleBootstrap() }));
78
+ }
75
79
  return /* @__PURE__ */ jsxs("html", { ...htmlAttrs, children: [
76
80
  /* @__PURE__ */ jsx("head", { children: headChildren }),
77
81
  /* @__PURE__ */ jsxs("body", { class: "min-h-screen antialiased", children: [
package/dist/features.css CHANGED
@@ -1026,6 +1026,30 @@ nav[data-zd-toc], div[data-zd-mobile-toc] {
1026
1026
 
1027
1027
  /* END --zdc-* chrome component tokens */
1028
1028
 
1029
+ /* ======== Header brand icon (settings.headerBrand) ======== */
1030
+
1031
+ /* Decorative icon inside the site-name anchor. Sized in `em` so it follows a
1032
+ * theme pack's logo font-size inside the fixed-height header; painted as a
1033
+ * currentColor mask so it always matches the site-name ink. The anchor stays
1034
+ * a truncating flow box — the icon is an inline-block run-in, not a flex item. */
1035
+ [data-header-brand-icon] {
1036
+ display: inline-block;
1037
+ width: 1.1em;
1038
+ height: 1.1em;
1039
+ vertical-align: -0.2em;
1040
+ margin-inline-end: var(--spacing-hsp-sm);
1041
+ background-color: currentColor;
1042
+ -webkit-mask: var(--zd-header-brand-icon) center/contain no-repeat;
1043
+ mask: var(--zd-header-brand-icon) center/contain no-repeat;
1044
+ }
1045
+
1046
+ /* A brand icon replaces a theme pack's decorative logo emblem rather than
1047
+ * doubling it. Specificity (0,3,2) beats every pack's `[data-header-logo]::before`
1048
+ * rule (0,2,2); `::after` marks are left alone. */
1049
+ html[data-theme-pack] [data-header-logo][data-header-brand]::before {
1050
+ content: none;
1051
+ }
1052
+
1029
1053
  button[data-zd-pending] {
1030
1054
  opacity: 0.7;
1031
1055
  pointer-events: none;
@@ -2,6 +2,7 @@
2
2
  import type { Child } from "@takazudo/zfb/zudo-react";
3
3
  import type { JSX } from "@takazudo/zfb/zudo-react/jsx-runtime";
4
4
  import type { HeaderNavItem, HeaderRightComponentRegistry, HeaderRightItem, Locale } from "./types.js";
5
+ import type { HeaderBrandConfig } from "../settings.js";
5
6
  /**
6
7
  * Boundary helpers the host injects into `<Header>`. These are the URL
7
8
  * builders the legacy header.tsx used to import from `@/utils/base` —
@@ -92,6 +93,14 @@ export interface HeaderProps {
92
93
  persistKey: string;
93
94
  /** Site-name string shown in the logo anchor (host `settings.siteName`). */
94
95
  siteName: string;
96
+ /**
97
+ * Optional brand icon rendered before `siteName` inside the logo anchor
98
+ * (host `settings.headerBrand`). Omitted or `false` renders the anchor
99
+ * exactly as before. The icon is a decorative `currentColor` CSS mask; an
100
+ * `icon` starting with `/` goes through `urlHelpers.withBase()`, anything
101
+ * else is used verbatim.
102
+ */
103
+ headerBrand?: false | HeaderBrandConfig;
95
104
  /** Header-nav items in render order (host `settings.headerNav`). */
96
105
  headerNav: HeaderNavItem[];
97
106
  /**
@@ -32,6 +32,7 @@ function Header(props) {
32
32
  search,
33
33
  persistKey,
34
34
  siteName,
35
+ headerBrand,
35
36
  headerNav,
36
37
  headerRightItems,
37
38
  headerRightComponents,
@@ -48,6 +49,8 @@ function Header(props) {
48
49
  const pathWithoutVersion = stripCurrentVersionPrefix(pathWithoutBase, currentVersion);
49
50
  const matchPath = pathForMatch(pathWithoutVersion, lang, i18n.defaultLocale);
50
51
  const activeNavPath = computeActiveNavPath(headerNav, matchPath);
52
+ const brandIcon = headerBrand && typeof headerBrand.icon === "string" && headerBrand.icon !== "" ? headerBrand.icon : null;
53
+ const brandIconUrl = brandIcon === null ? null : brandIcon.startsWith("/") && !brandIcon.startsWith("//") ? urlHelpers.withBase(brandIcon) : brandIcon;
51
54
  const rightItemDispatch = createRightItemDispatch(headerRightComponents);
52
55
  const rightItems = headerRightItems.map((item, i) => ({
53
56
  item,
@@ -98,14 +101,27 @@ function Header(props) {
98
101
  // (mobile sidebar contents stay accessible to assistive tech
99
102
  // even without the toggle island).
100
103
  /* @__PURE__ */ jsx(SidebarSlotFallback, { children: sidebarSlot }),
101
- /* @__PURE__ */ jsx(
104
+ /* @__PURE__ */ jsxs(
102
105
  "a",
103
106
  {
104
107
  href: urlHelpers.withBase(isNonDefaultLocale ? `/${lang}/` : "/"),
105
108
  class: "min-w-0 truncate text-title font-bold text-fg hover:underline focus:underline",
106
109
  title: siteName,
107
110
  "data-header-logo": true,
108
- children: siteName
111
+ "data-header-brand": brandIconUrl !== null ? true : void 0,
112
+ children: [
113
+ brandIconUrl !== null && // Painted by features.css as a currentColor mask so it follows the
114
+ // site-name ink; decorative, the anchor's name stays the site name.
115
+ /* @__PURE__ */ jsx(
116
+ "span",
117
+ {
118
+ "aria-hidden": "true",
119
+ "data-header-brand-icon": true,
120
+ style: { "--zd-header-brand-icon": cssUrl(brandIconUrl) }
121
+ }
122
+ ),
123
+ siteName
124
+ ]
109
125
  }
110
126
  ),
111
127
  /* @__PURE__ */ jsxs(
@@ -486,6 +502,9 @@ function renderRightItem(item, index, ctx, dispatch) {
486
502
  }
487
503
  return null;
488
504
  }
505
+ function cssUrl(url) {
506
+ return `url("${url.replace(/[\\"]/g, "\\$&").replace(/[\n\r\f]/g, "\\a ")}")`;
507
+ }
489
508
  export {
490
509
  Header
491
510
  };
@@ -1,7 +1,7 @@
1
1
  /** @jsxRuntime automatic */
2
2
  import type { JSX } from "@takazudo/zfb/zudo-react/jsx-runtime";
3
3
  import type { ChromeContext } from "../factory-context/index.js";
4
- import type { Settings } from "../settings.js";
4
+ import type { HeaderBrandConfig, Settings } from "../settings.js";
5
5
  import type { SidebarNavNode } from "../sidebar/types.js";
6
6
  export interface HeaderWithDefaultsProps {
7
7
  /** Active locale; defaults to the configured defaultLocale. */
@@ -44,6 +44,8 @@ export interface HeaderVersionEntry {
44
44
  /** Settings subset read by {@link createHeaderWithDefaults}. */
45
45
  export interface HeaderWithDefaultsSettings {
46
46
  siteName: string;
47
+ /** Optional header brand icon — forwarded verbatim to `<Header>`. */
48
+ headerBrand?: false | HeaderBrandConfig;
47
49
  /** Site base path — fed to the language-switcher SPA re-wire config. */
48
50
  base: string;
49
51
  /** Trailing-slash policy — fed to the language-switcher SPA re-wire config. */
@@ -194,6 +194,7 @@ function createHeaderWithDefaults(ctx) {
194
194
  languageSwitcher,
195
195
  persistKey,
196
196
  siteName: settings.siteName,
197
+ headerBrand: settings.headerBrand,
197
198
  headerNav: settings.headerNav,
198
199
  headerRightItems,
199
200
  headerRightComponents: ctx.hostBindings.headerRightComponents,
@@ -174,7 +174,7 @@ function createHomePageView(ctx) {
174
174
  }
175
175
  ) : null,
176
176
  /* @__PURE__ */ jsxs("div", { class: "zd-home-copy min-w-0 lg:flex-1", children: [
177
- /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-2xs zd-wrap-anywhere", children: settings.siteName }),
177
+ /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-2xs zd-wrap-anywhere", "data-zd-site-name": true, children: settings.siteName }),
178
178
  /* @__PURE__ */ jsx("p", { class: "text-muted text-small mb-vsp-sm", children: rawDescription }),
179
179
  /* @__PURE__ */ jsx("div", { class: "zd-home-links flex flex-wrap items-center justify-center lg:justify-start gap-hsp-md text-small", children: rowContent })
180
180
  ] })
@@ -0,0 +1,3 @@
1
+ export type KatexLike = typeof import("katex").default;
2
+ /** katex's API, or `null` when the optional peer is not installed. */
3
+ export declare const katex: KatexLike | null;
@@ -0,0 +1,12 @@
1
+ function pickKatex(m) {
2
+ let cur = m;
3
+ for (let i = 0; i < 3 && cur && typeof cur === "object"; i++) {
4
+ if (typeof cur.renderToString === "function") return cur;
5
+ cur = cur.default;
6
+ }
7
+ return null;
8
+ }
9
+ const katex = await import("katex").then(pickKatex, () => null);
10
+ export {
11
+ katex
12
+ };
@@ -1,13 +1,8 @@
1
1
  import { jsx } from "@takazudo/zfb/zudo-react/jsx-runtime";
2
- function pickKatex(m) {
3
- let cur = m;
4
- for (let i = 0; i < 3 && cur && typeof cur === "object"; i++) {
5
- if (typeof cur.renderToString === "function") return cur;
6
- cur = cur.default;
7
- }
8
- return null;
9
- }
10
- const katex = await import("katex").then(pickKatex, () => null);
2
+ const katex = await import("@takazudo/zudo-doc/katex-loader").then(
3
+ (m) => m.katex,
4
+ () => null
5
+ );
11
6
  const MISSING_KATEX_MESSAGE = 'MathBlock requires the optional peer "katex": install it and set math: true';
12
7
  function MathBlock({ latex, block = false }) {
13
8
  if (!katex) throw new Error(MISSING_KATEX_MESSAGE);
@@ -34,8 +34,14 @@ export interface MdDocFrontmatter {
34
34
  * search excerpts and llms-txt descriptions stay byte-equal across the
35
35
  * cutover, with one deliberate divergence: it also strips JSX/MDX block
36
36
  * comments (the `{/*`-delimited MDX comment form), which the legacy
37
- * pipeline leaked into descriptions (zudo-doc#2175). Do not add other new
38
- * rules without also updating the byte-equality fixtures (topic-plugin-audit).
37
+ * pipeline leaked into descriptions (zudo-doc#2175), and a second one
38
+ * (zudo-doc#4647): inline code keeps its text (only the backticks are
39
+ * dropped, contents protected from the later rules) instead of being deleted,
40
+ * and remark-directive container fence lines (`:::name[label]{attrs}`,
41
+ * `:::name Title`, bare `:::` closers) are removed while the label/title
42
+ * text is kept. Do not add
43
+ * other new rules without also updating the byte-equality fixtures
44
+ * (topic-plugin-audit).
39
45
  */
40
46
  export declare function stripMarkdown(md: string): string;
41
47
  /**
@@ -31,11 +31,18 @@ function decodeCharacterReferences(value) {
31
31
  ).replace(/&amp;/g, "&");
32
32
  }
33
33
  function stripMarkdown(md) {
34
- const withoutCodeCommentsAndTags = md.replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "").replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<[^>]+>/g, "");
35
- return decodeCharacterReferences(withoutCodeCommentsAndTags).replace(/^#{1,6}\s+/gm, "").replace(/\*{1,3}([^*]+)\*{1,3}/g, "$1").replace(
34
+ const inlineCode = [];
35
+ const withoutCodeCommentsAndTags = md.replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, (_m, code) => {
36
+ inlineCode.push(code);
37
+ return `\uE000${inlineCode.length - 1}\uE001`;
38
+ }).replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<[^>]+>/g, "");
39
+ return decodeCharacterReferences(withoutCodeCommentsAndTags).replace(
40
+ /^[ \t]*:{3,}(?:[A-Za-z][\w-]*(?:\[([^\]\n]*)\])?(?:\{[^}\n]*\})?(?:[ \t]+(\S[^\n]*?))?)?[ \t]*$/gm,
41
+ (_m, label, title) => label ?? title ?? ""
42
+ ).replace(/^#{1,6}\s+/gm, "").replace(/\*{1,3}([^*]+)\*{1,3}/g, "$1").replace(
36
43
  /(^|[^\p{L}\p{N}])(_{1,3})(?=\S)([^_\n]*?\S)\2(?=$|[^\p{L}\p{N}])/gmu,
37
44
  "$1$3"
38
- ).replace(/!\[[^\]]*\]\([^)]+\)/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^>\s+/gm, "").replace(/^[-*_]{3,}\s*$/gm, "").replace(/^[\s]*[-*+]\s+/gm, "").replace(/^[\s]*\d+\.\s+/gm, "").replace(/^import\s+.*$/gm, "").replace(/^export\s+.*$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
45
+ ).replace(/!\[[^\]]*\]\([^)]+\)/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^>\s+/gm, "").replace(/^[-*_]{3,}\s*$/gm, "").replace(/^[\s]*[-*+]\s+/gm, "").replace(/^[\s]*\d+\.\s+/gm, "").replace(/^import\s+.*$/gm, "").replace(/^export\s+.*$/gm, "").replace(/\n{3,}/g, "\n\n").replace(/\uE000(\d+)\uE001/g, (_m, i) => inlineCode[Number(i)] ?? "").trim();
39
46
  }
40
47
  function collectMdFiles(dir) {
41
48
  const results = [];
@@ -23,7 +23,8 @@ function CardBody({
23
23
  item,
24
24
  locale: props.locale,
25
25
  updatedLabel: props.updatedLabel,
26
- dateFormats: props.dateFormats
26
+ dateFormats: props.dateFormats,
27
+ linked
27
28
  }
28
29
  ) })
29
30
  ] }),
@@ -36,24 +37,23 @@ function DateStamp({
36
37
  locale,
37
38
  updatedLabel,
38
39
  positioned,
40
+ linked,
39
41
  dateFormats
40
42
  }) {
41
- return /* @__PURE__ */ jsxs(
42
- "time",
43
- {
44
- datetime: date,
45
- class: positioned ? "hidden sm:flex sm:col-start-2 sm:row-start-1 sm:row-span-2 w-[6.5rem] flex-col items-end self-stretch border-l border-muted pl-hsp-xl text-muted tabular-nums" : "hidden sm:flex w-[6.5rem] flex-col items-end self-stretch border-l border-muted pl-hsp-xl text-muted tabular-nums",
46
- children: [
47
- /* @__PURE__ */ jsx("span", { class: "block text-title leading-tight font-medium", children: formatMonthDayLabel(date, locale, dateFormats?.monthDay) }),
48
- /* @__PURE__ */ jsx("span", { class: "block text-caption", children: formatYear(date, locale, dateFormats?.year) }),
49
- updated && /* @__PURE__ */ jsxs("span", { class: "mt-vsp-3xs block text-micro", children: [
50
- updatedLabel,
51
- " ",
52
- formatMonthDayLabel(updated, locale, dateFormats?.monthDay)
53
- ] })
54
- ]
55
- }
56
- );
43
+ const stampClass = [
44
+ "hidden sm:flex w-[6.5rem] flex-col items-end self-stretch border-l border-muted pl-hsp-xl text-muted tabular-nums",
45
+ positioned && "sm:col-start-2 sm:row-start-1 sm:row-span-2",
46
+ linked && "group-hover:text-accent group-focus-visible:text-accent"
47
+ ].filter(Boolean).join(" ");
48
+ return /* @__PURE__ */ jsxs("time", { datetime: date, class: stampClass, children: [
49
+ /* @__PURE__ */ jsx("span", { class: "block text-title leading-tight font-medium", children: formatMonthDayLabel(date, locale, dateFormats?.monthDay) }),
50
+ /* @__PURE__ */ jsx("span", { class: "block text-caption", children: formatYear(date, locale, dateFormats?.year) }),
51
+ updated && /* @__PURE__ */ jsxs("span", { class: "mt-vsp-3xs block text-micro", children: [
52
+ updatedLabel,
53
+ " ",
54
+ formatMonthDayLabel(updated, locale, dateFormats?.monthDay)
55
+ ] })
56
+ ] });
57
57
  }
58
58
  function CardList(props) {
59
59
  return /* @__PURE__ */ jsx("div", { class: "grid grid-cols-1 gap-vsp-md", children: props.items.map((item) => /* @__PURE__ */ jsx(
@@ -84,7 +84,8 @@ function CardList(props) {
84
84
  locale: props.locale,
85
85
  updatedLabel: props.updatedLabel,
86
86
  dateFormats: props.dateFormats,
87
- positioned: true
87
+ positioned: true,
88
+ linked: true
88
89
  }
89
90
  )
90
91
  ]
@@ -107,7 +108,8 @@ function CardList(props) {
107
108
  locale: props.locale,
108
109
  updatedLabel: props.updatedLabel,
109
110
  dateFormats: props.dateFormats,
110
- positioned: true
111
+ positioned: true,
112
+ linked: false
111
113
  }
112
114
  )
113
115
  ] }),
@@ -135,7 +137,8 @@ function CardList(props) {
135
137
  locale: props.locale,
136
138
  updatedLabel: props.updatedLabel,
137
139
  dateFormats: props.dateFormats,
138
- positioned: false
140
+ positioned: false,
141
+ linked: true
139
142
  }
140
143
  )
141
144
  ]
@@ -158,7 +161,8 @@ function CardList(props) {
158
161
  locale: props.locale,
159
162
  updatedLabel: props.updatedLabel,
160
163
  dateFormats: props.dateFormats,
161
- positioned: false
164
+ positioned: false,
165
+ linked: false
162
166
  }
163
167
  )
164
168
  ] })