@takazudo/zudo-doc 7.1.0 → 8.1.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 (49) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +1 -1
  3. package/dist/chrome/derive.js +1 -0
  4. package/dist/compiled.css +25 -2
  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 +21 -1
  8. package/dist/config.js +2 -0
  9. package/dist/content.css +13 -2
  10. package/dist/doclayout/doc-layout.js +5 -1
  11. package/dist/features.css +24 -0
  12. package/dist/header/header.d.ts +9 -0
  13. package/dist/header/header.js +21 -2
  14. package/dist/header-with-defaults/index.d.ts +3 -1
  15. package/dist/header-with-defaults/index.js +1 -0
  16. package/dist/home-page/index.d.ts +1 -1
  17. package/dist/home-page/index.js +3 -2
  18. package/dist/nav-indexing/note-tray-index-parts/index-list.js +2 -2
  19. package/dist/preset.d.ts +4 -1
  20. package/dist/preset.js +3 -14
  21. package/dist/settings-defaults.js +2 -0
  22. package/dist/settings.d.ts +23 -0
  23. package/dist/site-tree-nav/index.d.ts +2 -0
  24. package/dist/site-tree-nav/index.js +2 -0
  25. package/dist/site-tree-nav/normalize-island-props.d.ts +1 -0
  26. package/dist/site-tree-nav/normalize-island-props.js +6 -0
  27. package/dist/site-tree-nav-island/index.d.ts +1 -1
  28. package/dist/site-tree-nav-island/index.js +15 -11
  29. package/dist/theme-packs/drift/pack.css +6 -0
  30. package/dist/theme-packs/foundry/pack.css +6 -0
  31. package/dist/theme-packs/futura-editorial/pack.css +6 -0
  32. package/dist/theme-packs/hollow/pack.css +3 -0
  33. package/dist/theme-packs/ledger/pack.css +3 -0
  34. package/dist/theme-packs/matcha/pack.css +3 -0
  35. package/dist/theme-packs/observatory/pack.css +3 -0
  36. package/dist/theme-packs/sakura/pack.css +3 -0
  37. package/dist/theme-packs/scandi/pack.css +3 -0
  38. package/dist/theme-packs/solar/pack.css +3 -0
  39. package/dist/theme-packs/tidepool/pack.css +3 -0
  40. package/dist/theme-packs/washi/pack.css +3 -0
  41. package/dist/transitions/persisted-focus-settle-bootstrap.d.ts +2 -0
  42. package/dist/transitions/persisted-focus-settle-bootstrap.js +22 -0
  43. package/dist/tree-nav-shared/index.d.ts +2 -0
  44. package/dist/tree-nav-shared/index.js +8 -1
  45. package/eject/header/header.tsx +40 -0
  46. package/eject/site-tree-nav-island/index.tsx +21 -12
  47. package/package.json +12 -16
  48. package/dist/plugins/optional-peers.d.ts +0 -3
  49. package/dist/plugins/optional-peers.js +0 -34
@@ -12,6 +12,12 @@ function normalizeSiteTreeNavProps(input) {
12
12
  "categoryIgnore"
13
13
  );
14
14
  }
15
+ if (input.initiallyCollapsedCategorySlugs !== void 0) {
16
+ normalized.initiallyCollapsedCategorySlugs = normalizeStringArray(
17
+ input.initiallyCollapsedCategorySlugs,
18
+ "initiallyCollapsedCategorySlugs"
19
+ );
20
+ }
15
21
  if (input.ariaLabel !== void 0) {
16
22
  normalized.ariaLabel = requireString(input.ariaLabel, "ariaLabel");
17
23
  }
@@ -5,7 +5,7 @@ export interface SiteTreeNavProps {
5
5
  ariaLabel?: string;
6
6
  categoryOrder?: string[];
7
7
  categoryIgnore?: string[];
8
- /** Root-category slugs that should start collapsed. */
8
+ /** Exact full category slugs at any depth that should start collapsed. */
9
9
  initiallyCollapsedCategorySlugs?: string[];
10
10
  /** Locale used by dated note-tray rows. */
11
11
  locale?: string;
@@ -9,6 +9,7 @@ import {
9
9
  import {
10
10
  INDENT,
11
11
  connectorLeft,
12
+ connectorLabelStart,
12
13
  ConnectorLines,
13
14
  CategoryLinkIcon
14
15
  } from "../tree-nav-shared/index.js";
@@ -24,9 +25,11 @@ import {
24
25
  import { formatMonthDay } from "../format-date/index.js";
25
26
  import { initialCategoryOpenState, toggleCategoryOpenState } from "./state.js";
26
27
  const SITE_BASE_PAD = "clamp(0.5rem, 0.8vw, 1rem)";
28
+ const CONNECTOR_WIDTH_SCALE = 2;
29
+ const CONNECTOR_LABEL_GAP = "var(--spacing-hsp-xs)";
27
30
  function padLeft(depth) {
28
31
  if (depth === 0) return SITE_BASE_PAD;
29
- return `calc(${depth} * ${INDENT} + 1.25rem + 5px)`;
32
+ return `max(calc(${depth} * ${INDENT} + 1.25rem + 5px), ${connectorLabelStart(depth, CONNECTOR_WIDTH_SCALE, CONNECTOR_LABEL_GAP)})`;
30
33
  }
31
34
  function reorderTree(tree, order) {
32
35
  const map = new Map(tree.map((node) => [node.slug, node]));
@@ -91,7 +94,7 @@ function SiteTreeNav({
91
94
  node,
92
95
  depth: 0,
93
96
  isLast,
94
- initiallyCollapsed: initiallyCollapsed.has(node.value.slug),
97
+ initiallyCollapsedCategorySlugs: initiallyCollapsed,
95
98
  locale,
96
99
  dateFormats,
97
100
  updatedLabel
@@ -108,7 +111,8 @@ function SiteTreeNav({
108
111
  SiteTreeNav.displayName = "SiteTreeNav";
109
112
  function NodeList({
110
113
  nodes,
111
- depth
114
+ depth,
115
+ initiallyCollapsedCategorySlugs
112
116
  }) {
113
117
  return /* @__PURE__ */ jsx(For, { each: nodes, by: (node) => node.slug, children: (node, index) => {
114
118
  const isLast = computed(() => index.value === nodes.value.length - 1);
@@ -117,7 +121,7 @@ function NodeList({
117
121
  {
118
122
  when: computed(() => node.value.children.length > 0),
119
123
  fallback: () => /* @__PURE__ */ jsx(LeafNode, { node, depth, isLast }),
120
- children: () => /* @__PURE__ */ jsx(CategoryNode, { node, depth, isLast })
124
+ children: () => /* @__PURE__ */ jsx(CategoryNode, { node, depth, isLast, initiallyCollapsedCategorySlugs })
121
125
  }
122
126
  );
123
127
  } });
@@ -126,12 +130,12 @@ function CategoryNode({
126
130
  node,
127
131
  depth,
128
132
  isLast,
129
- initiallyCollapsed = false,
133
+ initiallyCollapsedCategorySlugs,
130
134
  locale = "en",
131
135
  dateFormats,
132
136
  updatedLabel = "Updated"
133
137
  }) {
134
- const open = signal(initialCategoryOpenState(initiallyCollapsed));
138
+ const open = signal(initialCategoryOpenState(initiallyCollapsedCategorySlugs.has(node.value.slug)));
135
139
  const toggle = () => {
136
140
  open.value = toggleCategoryOpenState(open.value);
137
141
  };
@@ -157,7 +161,7 @@ function CategoryNode({
157
161
  {
158
162
  depth,
159
163
  isLast: true,
160
- widthScale: 2,
164
+ widthScale: CONNECTOR_WIDTH_SCALE,
161
165
  topPad: "calc(0.15rem + var(--spacing-vsp-xs))"
162
166
  }
163
167
  ) }),
@@ -166,7 +170,7 @@ function CategoryNode({
166
170
  {
167
171
  depth,
168
172
  isLast: false,
169
- widthScale: 2,
173
+ widthScale: CONNECTOR_WIDTH_SCALE,
170
174
  topPad: "calc(0.15rem + var(--spacing-vsp-xs))"
171
175
  }
172
176
  ) }),
@@ -234,7 +238,7 @@ function CategoryNode({
234
238
  when: computed(
235
239
  () => node.value.shape === "note-tray" && depth === 0
236
240
  ),
237
- fallback: () => /* @__PURE__ */ jsx(NodeList, { nodes: children, depth: depth + 1 }),
241
+ fallback: () => /* @__PURE__ */ jsx(NodeList, { nodes: children, depth: depth + 1, initiallyCollapsedCategorySlugs }),
238
242
  children: () => /* @__PURE__ */ jsx(
239
243
  NoteTrayNodeList,
240
244
  {
@@ -369,7 +373,7 @@ function LeafNode({
369
373
  {
370
374
  depth,
371
375
  isLast: true,
372
- widthScale: 2,
376
+ widthScale: CONNECTOR_WIDTH_SCALE,
373
377
  topPad
374
378
  }
375
379
  ) }),
@@ -378,7 +382,7 @@ function LeafNode({
378
382
  {
379
383
  depth,
380
384
  isLast: false,
381
- widthScale: 2,
385
+ widthScale: CONNECTOR_WIDTH_SCALE,
382
386
  topPad
383
387
  }
384
388
  ) }),
@@ -314,6 +314,9 @@ html[data-theme-pack="drift"] a[data-nav-active] {
314
314
  html[data-theme-pack="drift"] .zd-content h1 {
315
315
  text-shadow: 0 0 34px color-mix(in oklch, var(--zd-accent) 32%, transparent);
316
316
  }
317
+ html[data-theme-pack="drift"] .zd-content h2:where(:not(.zd-home-heading)):where(:not(.zd-compact-prose *)) {
318
+ font-size: 1.6rem;
319
+ }
317
320
  html[data-theme-pack="drift"] .zd-content h2 {
318
321
  border-top-width: 1px;
319
322
  border-image: linear-gradient(
@@ -323,6 +326,9 @@ html[data-theme-pack="drift"] .zd-content h2 {
323
326
  )
324
327
  1 !important;
325
328
  }
329
+ html[data-theme-pack="drift"] .zd-content h3:where(:not(.zd-compact-prose *)) {
330
+ border-top-width: 1px;
331
+ }
326
332
 
327
333
  /* links: airy underline that brightens on approach */
328
334
  html[data-theme-pack="drift"] .zd-content a {
@@ -107,11 +107,17 @@ html[data-theme-pack="foundry"] .zd-content h1 {
107
107
  padding-bottom: 0.55rem;
108
108
  border-bottom: 1px solid color-mix(in srgb, var(--zd-muted) 42%, transparent);
109
109
  }
110
+ html[data-theme-pack="foundry"] .zd-content h2:where(:not(.zd-home-heading)):where(:not(.zd-compact-prose *)) {
111
+ font-size: 1.6rem;
112
+ }
110
113
  html[data-theme-pack="foundry"] .zd-content h2 {
111
114
  border-image: none !important;
112
115
  border-top-width: 1px;
113
116
  border-top-color: color-mix(in srgb, var(--zd-muted) 42%, transparent);
114
117
  }
118
+ html[data-theme-pack="foundry"] .zd-content h3:where(:not(.zd-compact-prose *)) {
119
+ border-top-width: 1px;
120
+ }
115
121
 
116
122
  /* inline code: neutral wash that holds up in both modes */
117
123
  html[data-theme-pack="foundry"] .zd-content code:not(pre code) {
@@ -216,11 +216,17 @@ html[data-theme-pack="futura-editorial"] .zd-content > p[data-doc-description] {
216
216
  fade (signature move #2). The h2 rule needs !important to defeat the
217
217
  SSR-inlined `style="border-image:linear-gradient(...)"` (Decision
218
218
  6.6's sole allowlisted carve-out). */
219
+ html[data-theme-pack="futura-editorial"] .zd-content h2:where(:not(.zd-home-heading)):where(:not(.zd-compact-prose *)) {
220
+ font-size: 1.6rem;
221
+ }
219
222
  html[data-theme-pack="futura-editorial"] .zd-content h2 {
220
223
  border-image: none !important;
221
224
  border-top-width: 1px;
222
225
  border-top-color: var(--zd-fg);
223
226
  }
227
+ html[data-theme-pack="futura-editorial"] .zd-content h3:where(:not(.zd-compact-prose *)) {
228
+ border-top-width: 1px;
229
+ }
224
230
  html[data-theme-pack="futura-editorial"] .zd-content h3 {
225
231
  font-family: var(--font-display);
226
232
  letter-spacing: 0.01em;
@@ -264,6 +264,9 @@ html[data-theme-pack="hollow"] .zd-content h1 {
264
264
  /* h2: the SSR-inlined gradient bar is re-pointed at a fading pink
265
265
  rule (Decision 6.6's sole allowlisted !important carve-out —
266
266
  border-image only) */
267
+ html[data-theme-pack="hollow"] .zd-content h2:where(:not(.zd-home-heading)):where(:not(.zd-compact-prose *)) {
268
+ font-size: 1.5rem;
269
+ }
267
270
  html[data-theme-pack="hollow"] .zd-content h2 {
268
271
  border-image: linear-gradient(
269
272
  to right,
@@ -281,6 +281,9 @@ html[data-theme-pack="ledger"] .zd-content h2 {
281
281
  font-style: italic;
282
282
  font-size: 1.6rem;
283
283
  }
284
+ html[data-theme-pack="ledger"] .zd-content h3:where(:not(.zd-compact-prose *)) {
285
+ border-top-width: 1px;
286
+ }
284
287
  html[data-theme-pack="ledger"] .zd-content h3 {
285
288
  font-style: italic;
286
289
  font-size: 1.3rem;
@@ -229,6 +229,9 @@ html[data-theme-pack="matcha"] .zd-content h2 {
229
229
  }
230
230
 
231
231
  /* h3: quiet, slightly tracked (weight 600 rides --zdc-doc-h3-weight) */
232
+ html[data-theme-pack="matcha"] .zd-content h3:where(:not(.zd-compact-prose *)) {
233
+ border-top-width: 1px;
234
+ }
232
235
  html[data-theme-pack="matcha"] .zd-content h3 {
233
236
  letter-spacing: 0.01em;
234
237
  }
@@ -422,6 +422,9 @@ html[data-theme-pack="observatory"] .zd-content > p[data-doc-description] {
422
422
  allowlisted carve-out) + a zeroed border-top; the node-to-line rule
423
423
  is then painted as a top-anchored background gradient, with the two
424
424
  glowing star nodes as pseudo-element dots. */
425
+ html[data-theme-pack="observatory"] .zd-content h2:where(:not(.zd-home-heading)):where(:not(.zd-compact-prose *)) {
426
+ font-size: 1.75rem;
427
+ }
425
428
  html[data-theme-pack="observatory"] .zd-content h2 {
426
429
  border-image: none !important;
427
430
  border-top-width: 0;
@@ -284,6 +284,9 @@ html[data-theme-pack="sakura"] a[data-nav-active] {
284
284
  into drifting transparency. Re-points the SSR-inlined
285
285
  `style="border-image:linear-gradient(...)"` gradient — the
286
286
  border-image !important carve-out (Decision 6.6). */
287
+ html[data-theme-pack="sakura"] .zd-content h2:where(:not(.zd-home-heading)):where(:not(.zd-compact-prose *)) {
288
+ font-size: 1.6rem;
289
+ }
287
290
  html[data-theme-pack="sakura"] .zd-content h2 {
288
291
  border-top-width: 2px;
289
292
  border-image: linear-gradient(
@@ -225,6 +225,9 @@ html[data-theme-pack="scandi"] .zd-content h2 {
225
225
  border-top-width: 1px;
226
226
  border-top-color: color-mix(in srgb, var(--zd-accent) 38%, transparent);
227
227
  }
228
+ html[data-theme-pack="scandi"] .zd-content h3:where(:not(.zd-compact-prose *)) {
229
+ border-top-width: 1px;
230
+ }
228
231
 
229
232
  /* links: sage ink with a soft sage underline that firms on hover */
230
233
  html[data-theme-pack="scandi"] .zd-content a {
@@ -215,6 +215,9 @@ html[data-theme-pack="solar"] main nav[aria-label="Breadcrumb"] li > span {
215
215
  bar is defeated with border-image none !important (Decision 6.6's
216
216
  sole allowlisted carve-out) + a zeroed border-top; the fade is then
217
217
  painted as a 2px top-anchored background gradient. */
218
+ html[data-theme-pack="solar"] .zd-content h2:where(:not(.zd-home-heading)):where(:not(.zd-compact-prose *)) {
219
+ font-size: 1.5rem;
220
+ }
218
221
  html[data-theme-pack="solar"] .zd-content h2 {
219
222
  border-image: none !important;
220
223
  border-top-width: 0;
@@ -219,6 +219,9 @@ html[data-theme-pack="tidepool"] a[data-nav-active] {
219
219
  html[data-theme-pack="tidepool"] .zd-content h1 {
220
220
  color: color-mix(in srgb, var(--zd-accent) 12%, var(--zd-fg));
221
221
  }
222
+ html[data-theme-pack="tidepool"] .zd-content h2:where(:not(.zd-home-heading)):where(:not(.zd-compact-prose *)) {
223
+ font-size: 1.5rem;
224
+ }
222
225
  html[data-theme-pack="tidepool"] .zd-content h2 {
223
226
  border-image: none !important;
224
227
  border-top-width: 0;
@@ -314,6 +314,9 @@ html[data-theme-pack="washi"] .zd-content > p[data-doc-description] {
314
314
  SSR-inlined `style="border-image:linear-gradient(...)"` — the
315
315
  border-image !important carve-out (Decision 6.6), here re-pointing
316
316
  the gradient rather than clearing it. */
317
+ html[data-theme-pack="washi"] .zd-content h2:where(:not(.zd-home-heading)):where(:not(.zd-compact-prose *)) {
318
+ font-size: 1.6rem;
319
+ }
317
320
  html[data-theme-pack="washi"] .zd-content h2 {
318
321
  border-top-width: 2px;
319
322
  border-image: linear-gradient(
@@ -0,0 +1,2 @@
1
+ /** Settles a retained persisted focus before the router applies its scroll policy. */
2
+ export declare function buildPersistedFocusSettleBootstrap(): string;
@@ -0,0 +1,22 @@
1
+ import { BEFORE_SWAP_EVENT } from "./page-events.js";
2
+ function buildPersistedFocusSettleBootstrap() {
3
+ return `(function(){
4
+ if(document.__zdPersistedFocusSettle)return;
5
+ document.__zdPersistedFocusSettle=true;
6
+ document.addEventListener(${JSON.stringify(BEFORE_SWAP_EVENT)},function(ev){
7
+ if(typeof ev.swap!=="function")return;
8
+ var originalSwap=ev.swap;
9
+ var wrapped=function(){
10
+ var active=document.activeElement;
11
+ var retained=!!active&&typeof active.closest==="function"&&!!active.closest("[data-zfb-transition-persist]");
12
+ var result=originalSwap.apply(this,arguments);
13
+ if(retained&&active.isConnected&&document.activeElement===active)void document.documentElement.offsetHeight;
14
+ return result;
15
+ };
16
+ try{ev.swap=wrapped;}catch(e){}
17
+ });
18
+ })();`;
19
+ }
20
+ export {
21
+ buildPersistedFocusSettleBootstrap
22
+ };
@@ -4,6 +4,8 @@ export declare const CONNECTOR_OFFSET = "clamp(0.2rem, 0.3vw, 0.5rem)";
4
4
  export declare const CONNECTOR_WIDTH = "clamp(0.4rem, 0.6vw, 1rem)";
5
5
  export declare const BASE_PAD = "clamp(0.4rem, 0.8vw, 1.3rem)";
6
6
  export declare function connectorLeft(depth: number): string;
7
+ /** Left edge a label needs so it starts `gap` after the horizontal connector's endpoint. */
8
+ export declare function connectorLabelStart(depth: number, widthScale: number, gap: string): string;
7
9
  export declare function CategoryLinkIcon({ class: className }: {
8
10
  class?: string;
9
11
  }): import("@takazudo/zfb").Description;
@@ -6,6 +6,12 @@ const BASE_PAD = "clamp(0.4rem, 0.8vw, 1.3rem)";
6
6
  function connectorLeft(depth) {
7
7
  return `calc(${depth} * ${INDENT} + ${CONNECTOR_OFFSET})`;
8
8
  }
9
+ function connectorWidth(widthScale) {
10
+ return widthScale === 1 ? CONNECTOR_WIDTH : `calc(${CONNECTOR_WIDTH} * ${widthScale})`;
11
+ }
12
+ function connectorLabelStart(depth, widthScale, gap) {
13
+ return `calc(${connectorLeft(depth)} + ${connectorWidth(widthScale)} + ${gap})`;
14
+ }
9
15
  const CATEGORY_LINK_PATH = "M5.746 5.74 0 11.49l20.987 20.96C34.126 45.572 41.963 53.45 41.948 53.523c-.012.062-9.456 9.544-20.986 21.07L0 95.55l5.714 5.715c3.142 3.143 5.748 5.715 5.79 5.715s2.63-2.563 5.75-5.696l17.939-18.001c21.867-21.94 29.443-29.599 29.443-29.768 0-.114-.665-.804-5.084-5.275C51.872 40.47 11.71.125 11.565.036 11.525.01 8.906 2.578 5.746 5.74m38.345-.066c-3.132 3.13-5.696 5.71-5.696 5.732-.001.022 2.16 2.185 4.8 4.807 2.641 2.623 8.382 8.338 12.758 12.702 15.38 15.337 23.763 23.641 24.314 24.086.19.153.346.336.346.405 0 .07-1.738 1.847-3.887 3.976a17515 17515 0 0 0-20.35 20.264 19555 19555 0 0 1-17.223 17.158c-.416.409-.757.77-.757.8 0 .083 11.415 11.485 11.457 11.445.235-.22 53.542-53.528 53.542-53.543C103.395 53.472 49.891.02 49.837 0c-.028-.01-2.613 2.543-5.746 5.674";
10
16
  function CategoryLinkIcon({ class: className }) {
11
17
  return /* @__PURE__ */ jsx(
@@ -27,7 +33,7 @@ function ConnectorLines({
27
33
  }) {
28
34
  if (depth === 0) return null;
29
35
  const left = connectorLeft(depth);
30
- const width = widthScale === 1 ? CONNECTOR_WIDTH : `calc(${CONNECTOR_WIDTH} * ${widthScale})`;
36
+ const width = connectorWidth(widthScale);
31
37
  const firstLineMid = `calc(${topPad} + 0.5lh)`;
32
38
  return /* @__PURE__ */ jsxs(Fragment, { children: [
33
39
  /* @__PURE__ */ jsx(
@@ -61,5 +67,6 @@ export {
61
67
  CategoryLinkIcon,
62
68
  ConnectorLines,
63
69
  INDENT,
70
+ connectorLabelStart,
64
71
  connectorLeft
65
72
  };
@@ -60,6 +60,7 @@ import type {
60
60
  Locale,
61
61
  } from "./types.js";
62
62
  import { GitHub as GitHubIcon } from "../icons/index.js";
63
+ import type { HeaderBrandConfig } from "../settings.js";
63
64
 
64
65
  /**
65
66
  * Boundary helpers the host injects into `<Header>`. These are the URL
@@ -168,6 +169,15 @@ export interface HeaderProps {
168
169
  /** Site-name string shown in the logo anchor (host `settings.siteName`). */
169
170
  siteName: string;
170
171
 
172
+ /**
173
+ * Optional brand icon rendered before `siteName` inside the logo anchor
174
+ * (host `settings.headerBrand`). Omitted or `false` renders the anchor
175
+ * exactly as before. The icon is a decorative `currentColor` CSS mask; an
176
+ * `icon` starting with `/` goes through `urlHelpers.withBase()`, anything
177
+ * else is used verbatim.
178
+ */
179
+ headerBrand?: false | HeaderBrandConfig;
180
+
171
181
  /** Header-nav items in render order (host `settings.headerNav`). */
172
182
  headerNav: HeaderNavItem[];
173
183
 
@@ -265,6 +275,7 @@ export function Header(props: HeaderProps): JSX.Element {
265
275
  search,
266
276
  persistKey,
267
277
  siteName,
278
+ headerBrand,
268
279
  headerNav,
269
280
  headerRightItems,
270
281
  headerRightComponents,
@@ -289,6 +300,16 @@ export function Header(props: HeaderProps): JSX.Element {
289
300
  const matchPath = pathForMatch(pathWithoutVersion, lang, i18n.defaultLocale);
290
301
 
291
302
  const activeNavPath = computeActiveNavPath(headerNav, matchPath);
303
+ // Direct `<Header>` consumers and raw route payloads bypass the config-time
304
+ // `assertValidHeaderBrand`, so a malformed value degrades to "no icon".
305
+ const brandIcon = headerBrand && typeof headerBrand.icon === "string" && headerBrand.icon !== ""
306
+ ? headerBrand.icon
307
+ : null;
308
+ const brandIconUrl = brandIcon === null
309
+ ? null
310
+ : brandIcon.startsWith("/") && !brandIcon.startsWith("//")
311
+ ? urlHelpers.withBase(brandIcon)
312
+ : brandIcon;
292
313
  const rightItemDispatch = createRightItemDispatch(headerRightComponents);
293
314
  const rightItems = headerRightItems
294
315
  .map((item, i) => ({
@@ -397,7 +418,17 @@ export function Header(props: HeaderProps): JSX.Element {
397
418
  class="min-w-0 truncate text-title font-bold text-fg hover:underline focus:underline"
398
419
  title={siteName}
399
420
  data-header-logo
421
+ data-header-brand={brandIconUrl !== null ? true : undefined}
400
422
  >
423
+ {brandIconUrl !== null && (
424
+ // Painted by features.css as a currentColor mask so it follows the
425
+ // site-name ink; decorative, the anchor's name stays the site name.
426
+ <span
427
+ aria-hidden="true"
428
+ data-header-brand-icon
429
+ style={{ "--zd-header-brand-icon": cssUrl(brandIconUrl) }}
430
+ />
431
+ )}
401
432
  {siteName}
402
433
  </a>
403
434
 
@@ -908,3 +939,12 @@ function renderRightItem(
908
939
 
909
940
  return null;
910
941
  }
942
+
943
+ /**
944
+ * Serialize a URL as a quoted CSS `url()` token so paths with whitespace,
945
+ * parentheses or quotes survive inside the custom property. Kept local to
946
+ * this module because the header is ejectable (see `eject/index.ts`).
947
+ */
948
+ function cssUrl(url: string): string {
949
+ return `url("${url.replace(/[\\"]/g, "\\$&").replace(/[\n\r\f]/g, "\\a ")}")`;
950
+ }
@@ -12,6 +12,7 @@ import type { ResolvedDateFormats } from "../settings.js";
12
12
  import {
13
13
  INDENT,
14
14
  connectorLeft,
15
+ connectorLabelStart,
15
16
  ConnectorLines,
16
17
  CategoryLinkIcon,
17
18
  } from "../tree-nav-shared/index.js";
@@ -30,9 +31,15 @@ import { initialCategoryOpenState, toggleCategoryOpenState } from "./state.js";
30
31
  // site-tree-nav uses wider padding than the narrow sidebar
31
32
  const SITE_BASE_PAD = "clamp(0.5rem, 0.8vw, 1rem)";
32
33
 
34
+ // The homepage draws its connectors at twice the sidebar width, so on wide
35
+ // viewports the branch endpoint passes the fixed label indent. The label starts
36
+ // at whichever is further right: that indent, or the endpoint plus a gap.
37
+ const CONNECTOR_WIDTH_SCALE = 2;
38
+ const CONNECTOR_LABEL_GAP = "var(--spacing-hsp-xs)";
39
+
33
40
  function padLeft(depth: number): string {
34
41
  if (depth === 0) return SITE_BASE_PAD;
35
- return `calc(${depth} * ${INDENT} + 1.25rem + 5px)`;
42
+ return `max(calc(${depth} * ${INDENT} + 1.25rem + 5px), ${connectorLabelStart(depth, CONNECTOR_WIDTH_SCALE, CONNECTOR_LABEL_GAP)})`;
36
43
  }
37
44
 
38
45
  function reorderTree(tree: SidebarNavNode[], order: string[]): SidebarNavNode[] {
@@ -57,7 +64,7 @@ export interface SiteTreeNavProps {
57
64
  ariaLabel?: string;
58
65
  categoryOrder?: string[];
59
66
  categoryIgnore?: string[];
60
- /** Root-category slugs that should start collapsed. */
67
+ /** Exact full category slugs at any depth that should start collapsed. */
61
68
  initiallyCollapsedCategorySlugs?: string[];
62
69
  /** Locale used by dated note-tray rows. */
63
70
  locale?: string;
@@ -129,7 +136,7 @@ export function SiteTreeNav({
129
136
  node={node}
130
137
  depth={0}
131
138
  isLast={isLast}
132
- initiallyCollapsed={initiallyCollapsed.has(node.value.slug)}
139
+ initiallyCollapsedCategorySlugs={initiallyCollapsed}
133
140
  locale={locale}
134
141
  dateFormats={dateFormats}
135
142
  updatedLabel={updatedLabel}
@@ -150,9 +157,11 @@ SiteTreeNav.displayName = "SiteTreeNav";
150
157
  function NodeList({
151
158
  nodes,
152
159
  depth,
160
+ initiallyCollapsedCategorySlugs,
153
161
  }: {
154
162
  nodes: ReadonlySignal<readonly SidebarNavNode[]>;
155
163
  depth: number;
164
+ initiallyCollapsedCategorySlugs: ReadonlySet<string>;
156
165
  }) {
157
166
  return (
158
167
  <For each={nodes} by={(node) => node.slug}>
@@ -166,7 +175,7 @@ function NodeList({
166
175
  )}
167
176
  >
168
177
  {() => (
169
- <CategoryNode node={node} depth={depth} isLast={isLast} />
178
+ <CategoryNode node={node} depth={depth} isLast={isLast} initiallyCollapsedCategorySlugs={initiallyCollapsedCategorySlugs} />
170
179
  )}
171
180
  </Show>
172
181
  );
@@ -179,7 +188,7 @@ function CategoryNode({
179
188
  node,
180
189
  depth,
181
190
  isLast,
182
- initiallyCollapsed = false,
191
+ initiallyCollapsedCategorySlugs,
183
192
  locale = "en",
184
193
  dateFormats,
185
194
  updatedLabel = "Updated",
@@ -187,12 +196,12 @@ function CategoryNode({
187
196
  node: ReadonlySignal<SidebarNavNode>;
188
197
  depth: number;
189
198
  isLast: ReadonlySignal<boolean>;
190
- initiallyCollapsed?: boolean;
199
+ initiallyCollapsedCategorySlugs: ReadonlySet<string>;
191
200
  locale?: string;
192
201
  dateFormats?: ResolvedDateFormats;
193
202
  updatedLabel?: string;
194
203
  }) {
195
- const open = signal(initialCategoryOpenState(initiallyCollapsed));
204
+ const open = signal(initialCategoryOpenState(initiallyCollapsedCategorySlugs.has(node.value.slug)));
196
205
  const toggle = () => {
197
206
  open.value = toggleCategoryOpenState(open.value);
198
207
  };
@@ -221,7 +230,7 @@ function CategoryNode({
221
230
  <ConnectorLines
222
231
  depth={depth}
223
232
  isLast={true}
224
- widthScale={2}
233
+ widthScale={CONNECTOR_WIDTH_SCALE}
225
234
  topPad="calc(0.15rem + var(--spacing-vsp-xs))"
226
235
  />
227
236
  )}
@@ -231,7 +240,7 @@ function CategoryNode({
231
240
  <ConnectorLines
232
241
  depth={depth}
233
242
  isLast={false}
234
- widthScale={2}
243
+ widthScale={CONNECTOR_WIDTH_SCALE}
235
244
  topPad="calc(0.15rem + var(--spacing-vsp-xs))"
236
245
  />
237
246
  )}
@@ -294,7 +303,7 @@ function CategoryNode({
294
303
  () => node.value.shape === "note-tray" && depth === 0,
295
304
  )}
296
305
  fallback={() => (
297
- <NodeList nodes={children} depth={depth + 1} />
306
+ <NodeList nodes={children} depth={depth + 1} initiallyCollapsedCategorySlugs={initiallyCollapsedCategorySlugs} />
298
307
  )}
299
308
  >
300
309
  {() => (
@@ -493,7 +502,7 @@ function LeafNode({
493
502
  <ConnectorLines
494
503
  depth={depth}
495
504
  isLast={true}
496
- widthScale={2}
505
+ widthScale={CONNECTOR_WIDTH_SCALE}
497
506
  topPad={topPad}
498
507
  />
499
508
  )}
@@ -503,7 +512,7 @@ function LeafNode({
503
512
  <ConnectorLines
504
513
  depth={depth}
505
514
  isLast={false}
506
- widthScale={2}
515
+ widthScale={CONNECTOR_WIDTH_SCALE}
507
516
  topPad={topPad}
508
517
  />
509
518
  )}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/zudo-doc",
3
- "version": "7.1.0",
3
+ "version": "8.1.0",
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",
@@ -15,7 +15,7 @@
15
15
  "url": "https://github.com/zudolab/zudo-doc/issues"
16
16
  },
17
17
  "engines": {
18
- "node": ">=24 <25"
18
+ "node": ">=24"
19
19
  },
20
20
  "keywords": [
21
21
  "zudo-doc",
@@ -206,10 +206,6 @@
206
206
  "types": "./dist/plugins/zdtp-loader.d.ts",
207
207
  "default": "./dist/plugins/zdtp-loader.js"
208
208
  },
209
- "./plugins/optional-peers": {
210
- "types": "./dist/plugins/optional-peers.d.ts",
211
- "default": "./dist/plugins/optional-peers.js"
212
- },
213
209
  "./content-admonition": {
214
210
  "types": "./dist/content-admonition/index.d.ts",
215
211
  "default": "./dist/content-admonition/index.js"
@@ -738,12 +734,12 @@
738
734
  "CHANGELOG.md"
739
735
  ],
740
736
  "peerDependencies": {
741
- "@modelcontextprotocol/sdk": "1.32.1",
737
+ "@modelcontextprotocol/sdk": "1.32.2",
742
738
  "@takazudo/zdtp": "^0.5.2 || ^0.6.0 || ^0.7.0 || ^0.8.0",
743
- "@takazudo/zfb": "^4.7.0",
744
- "@takazudo/zfb-md-wasm": "^4.7.0",
745
- "@takazudo/zfb-runtime": "^4.7.0",
746
- "@takazudo/zudo-doc-history-server": "^7.0.0",
739
+ "@takazudo/zfb": "^5.1.2",
740
+ "@takazudo/zfb-md-wasm": "^5.1.2",
741
+ "@takazudo/zfb-runtime": "^5.1.2",
742
+ "@takazudo/zudo-doc-history-server": "^8.0.0",
747
743
  "diff": "^8.0.0",
748
744
  "katex": "^0.18.2 || ^0.19.0",
749
745
  "zod": "^4.3.6"
@@ -781,11 +777,11 @@
781
777
  "yaml": "^2.9.0"
782
778
  },
783
779
  "devDependencies": {
784
- "@modelcontextprotocol/sdk": "1.32.1",
780
+ "@modelcontextprotocol/sdk": "1.32.2",
785
781
  "@takazudo/mdx-formatter": "1.3.0",
786
- "@takazudo/zfb": "4.7.0",
787
- "@takazudo/zfb-md-wasm": "4.7.0",
788
- "@takazudo/zfb-runtime": "4.7.0",
782
+ "@takazudo/zfb": "5.1.2",
783
+ "@takazudo/zfb-md-wasm": "5.1.2",
784
+ "@takazudo/zfb-runtime": "5.1.2",
789
785
  "@types/fs-extra": "^11.0.4",
790
786
  "@types/minimist": "^1.2.5",
791
787
  "@types/node": "^25.3.5",
@@ -795,7 +791,7 @@
795
791
  "typescript": "^5.0.0",
796
792
  "vitest": "^4.1.0",
797
793
  "zod": "^4.3.6",
798
- "@takazudo/zudo-doc-history-server": "7.1.0"
794
+ "@takazudo/zudo-doc-history-server": "8.1.0"
799
795
  },
800
796
  "scripts": {
801
797
  "gen:search-widget-script": "node scripts/gen-search-widget-script.mjs",
@@ -1,3 +0,0 @@
1
- import type { ZfbPlugin } from "@takazudo/zfb/plugins";
2
- declare const plugin: ZfbPlugin;
3
- export default plugin;