@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
@@ -3,11 +3,13 @@ import type { JSX } from "@takazudo/zfb/zudo-react/jsx-runtime";
3
3
  import type { ResolvedDateFormats } from "../../settings.js";
4
4
  import type { NoteTrayIndexItem } from "../note-tray-index.js";
5
5
  import type { TagNavLabels } from "../types.js";
6
- export declare function DateLine({ item, locale, updatedLabel, dateFormats, }: {
6
+ export declare function DateLine({ item, locale, updatedLabel, dateFormats, linked, }: {
7
7
  item: NoteTrayIndexItem;
8
8
  locale: string;
9
9
  updatedLabel: string;
10
10
  dateFormats?: ResolvedDateFormats;
11
+ /** Inside a `group` link: follow that link's hover/focus-visible accent. */
12
+ linked?: boolean;
11
13
  }): JSX.Element | null;
12
14
  export declare function ItemTags({ item, labels }: {
13
15
  item: NoteTrayIndexItem;
@@ -5,18 +5,25 @@ function DateLine({
5
5
  item,
6
6
  locale,
7
7
  updatedLabel,
8
- dateFormats
8
+ dateFormats,
9
+ linked = false
9
10
  }) {
10
11
  if (!item.date && !item.updated) return null;
11
- return /* @__PURE__ */ jsxs("span", { class: "tabular-nums text-caption text-muted", children: [
12
- item.date && /* @__PURE__ */ jsx("time", { datetime: item.date, children: formatDate(item.date, locale, dateFormats?.full) }),
13
- item.date && item.updated && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: " \xB7 " }),
14
- item.updated && /* @__PURE__ */ jsxs("span", { children: [
15
- updatedLabel,
16
- " ",
17
- /* @__PURE__ */ jsx("time", { datetime: item.updated, children: formatDate(item.updated, locale, dateFormats?.full) })
18
- ] })
19
- ] });
12
+ return /* @__PURE__ */ jsxs(
13
+ "span",
14
+ {
15
+ class: linked ? "tabular-nums text-caption text-muted group-hover:text-accent group-focus-visible:text-accent" : "tabular-nums text-caption text-muted",
16
+ children: [
17
+ item.date && /* @__PURE__ */ jsx("time", { datetime: item.date, children: formatDate(item.date, locale, dateFormats?.full) }),
18
+ item.date && item.updated && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: " \xB7 " }),
19
+ item.updated && /* @__PURE__ */ jsxs("span", { children: [
20
+ updatedLabel,
21
+ " ",
22
+ /* @__PURE__ */ jsx("time", { datetime: item.updated, children: formatDate(item.updated, locale, dateFormats?.full) })
23
+ ] })
24
+ ]
25
+ }
26
+ );
20
27
  }
21
28
  function ItemTags({ item, labels }) {
22
29
  if (!labels || !item.tagLinks?.length) return null;
@@ -5,12 +5,12 @@ function IndexList(props) {
5
5
  const width = rankWidth(props.items);
6
6
  return /* @__PURE__ */ jsx("ol", { class: "zd-list-items-no-margin", children: props.items.map((item) => {
7
7
  const rowClass = item.href ? "relative -mt-px first:mt-0 border-y border-muted hover:z-local-1 hover:border-accent focus-within:z-local-1 focus-within:border-accent" : "relative -mt-px first:mt-0 border-y border-muted";
8
- const rankClass = item.href ? "tabular-nums text-heading leading-none text-muted group-hover:text-fg group-focus-visible:text-fg" : "tabular-nums text-heading leading-none text-muted";
8
+ const rankClass = item.href ? "whitespace-nowrap tabular-nums text-heading leading-none text-muted group-hover:text-accent group-focus-visible:text-accent" : "whitespace-nowrap tabular-nums text-heading leading-none text-muted";
9
9
  const labelClass = item.href ? "font-medium text-fg group-hover:text-accent group-hover:underline group-focus-visible:text-accent group-focus-visible:underline" : "font-medium text-fg";
10
10
  const detailClass = item.href ? "mt-vsp-2xs block text-small text-muted group-hover:text-accent group-hover:underline group-focus-visible:text-accent group-focus-visible:underline" : "mt-vsp-2xs block text-small text-muted";
11
11
  const dateClass = item.href ? "mt-vsp-2xs block text-caption text-muted group-hover:text-accent group-hover:underline group-focus-visible:text-accent group-focus-visible:underline" : "mt-vsp-2xs block text-caption text-muted";
12
12
  const rowContent = /* @__PURE__ */ jsxs(Fragment, { children: [
13
- /* @__PURE__ */ jsx("span", { class: rankClass, style: { width: `${width}ch` }, children: item.rank === void 0 ? "" : String(item.rank).padStart(width, "0") }),
13
+ /* @__PURE__ */ jsx("span", { class: rankClass, style: { "min-width": `${width}ch` }, children: item.rank === void 0 ? "" : String(item.rank).padStart(width, "0") }),
14
14
  /* @__PURE__ */ jsxs("span", { class: "min-w-0", children: [
15
15
  /* @__PURE__ */ jsx("span", { class: labelClass, children: item.label }),
16
16
  item.description && /* @__PURE__ */ jsx("span", { class: detailClass, children: item.description }),
@@ -20,7 +20,8 @@ function IndexList(props) {
20
20
  item,
21
21
  locale: props.locale,
22
22
  updatedLabel: props.updatedLabel,
23
- dateFormats: props.dateFormats
23
+ dateFormats: props.dateFormats,
24
+ linked: Boolean(item.href)
24
25
  }
25
26
  ) })
26
27
  ] })
@@ -0,0 +1,4 @@
1
+ import type { ZfbPlugin } from "@takazudo/zfb/plugins";
2
+ export declare const DIFF_LOADER_SPECIFIER = "@takazudo/zudo-doc/diff-loader";
3
+ declare const plugin: ZfbPlugin;
4
+ export default plugin;
@@ -0,0 +1,13 @@
1
+ const DIFF_LOADER_SPECIFIER = "@takazudo/zudo-doc/diff-loader";
2
+ const DISABLED_LOADER_SOURCE = 'throw new Error("diff is not bundled: docHistory is off, so this build shadows @takazudo/zudo-doc/diff-loader. To use the doc history Compare view, set docHistory: true and install diff.");\nexport {};\n';
3
+ const plugin = {
4
+ name: "diff-loader",
5
+ setup(ctx) {
6
+ ctx.addVirtualModule(DIFF_LOADER_SPECIFIER, () => DISABLED_LOADER_SOURCE);
7
+ }
8
+ };
9
+ var diff_loader_default = plugin;
10
+ export {
11
+ DIFF_LOADER_SPECIFIER,
12
+ diff_loader_default as default
13
+ };
@@ -0,0 +1,4 @@
1
+ import type { ZfbPlugin } from "@takazudo/zfb/plugins";
2
+ export declare const KATEX_LOADER_SPECIFIER = "@takazudo/zudo-doc/katex-loader";
3
+ declare const plugin: ZfbPlugin;
4
+ export default plugin;
@@ -0,0 +1,13 @@
1
+ const KATEX_LOADER_SPECIFIER = "@takazudo/zudo-doc/katex-loader";
2
+ const DISABLED_LOADER_SOURCE = 'throw new Error("katex is not bundled: math is off, so this build shadows @takazudo/zudo-doc/katex-loader. To render <MathBlock>, set math: true and install katex.");\nexport {};\n';
3
+ const plugin = {
4
+ name: "katex-loader",
5
+ setup(ctx) {
6
+ ctx.addVirtualModule(KATEX_LOADER_SPECIFIER, () => DISABLED_LOADER_SOURCE);
7
+ }
8
+ };
9
+ var katex_loader_default = plugin;
10
+ export {
11
+ KATEX_LOADER_SPECIFIER,
12
+ katex_loader_default as default
13
+ };
package/dist/preset.d.ts CHANGED
@@ -36,7 +36,7 @@
36
36
  import { z } from "zod";
37
37
  import type { DirectiveSpec, ZfbConfig } from "@takazudo/zfb/config";
38
38
  import type { ColorScheme } from "./color-scheme-utils.js";
39
- import type { AssetViewerIndexingConfig, TagVocabularyEntry, FaviconConfig, HomeConfig } from "./settings.js";
39
+ import type { AssetViewerIndexingConfig, TagVocabularyEntry, FaviconConfig, HeaderBrandConfig, HomeConfig } from "./settings.js";
40
40
  /** A single locale's content directory (`settings.locales[code]`). */
41
41
  export interface PresetLocaleConfig {
42
42
  dir: string;
@@ -109,13 +109,16 @@ export interface PresetSettings {
109
109
  /** Favicon links. Same rationale as {@link PresetSettings.logo} — carried
110
110
  * only for `assertNoEmptyStringFaviconOrLogo` (#3474). */
111
111
  favicon?: string | FaviconConfig | false;
112
+ /** Header brand icon. Same rationale as {@link PresetSettings.logo} — carried
113
+ * only for `assertValidHeaderBrand`; rendering happens in the header. */
114
+ headerBrand?: false | HeaderBrandConfig;
112
115
  /** GitHub repository URL used by asset backlink metadata. */
113
116
  githubUrl?: string | false;
114
117
  siteUrl: string;
115
118
  trailingSlash: boolean;
116
119
  minifyHtml?: boolean;
117
120
  mermaid: boolean;
118
- /** KaTeX math rendering; when off, the optional `katex` peer is shadowed. */
121
+ /** KaTeX math rendering; when off, `@takazudo/zudo-doc/katex-loader` is shadowed (bare `katex` is not). */
119
122
  math?: boolean;
120
123
  /** Enable transclusion of other Markdown/MDX files. */
121
124
  transclude?: boolean;
@@ -128,6 +131,7 @@ export interface PresetSettings {
128
131
  changelogs?: PresetChangelogConfig[] | false;
129
132
  /** Metadata fields shown in the doc metadata area. */
130
133
  docMetainfoFields?: Array<"created" | "updated" | "author">;
134
+ /** Per-page git history; when off, `@takazudo/zudo-doc/diff-loader` is shadowed (bare `diff` is not). */
131
135
  docHistory?: boolean;
132
136
  /** Whether the doc history dropdown UI and related artifacts are enabled. */
133
137
  docHistoryUi?: boolean;
package/dist/preset.js CHANGED
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { assertNoCommaInVersionSlugs } from "./version-availability/index.js";
3
3
  import {
4
4
  assertNoEmptyStringFaviconOrLogo,
5
+ assertValidHeaderBrand,
5
6
  assertZdtpBundlingConsistent,
6
7
  assertValidSearchMaxBodyLength,
7
8
  assertAgentDocsConsistent,
@@ -19,6 +20,7 @@ function zudoDocPreset({
19
20
  }) {
20
21
  assertNoCommaInVersionSlugs(settings.versions);
21
22
  assertNoEmptyStringFaviconOrLogo(settings);
23
+ assertValidHeaderBrand(settings);
22
24
  assertZdtpBundlingConsistent(settings);
23
25
  assertValidSearchMaxBodyLength(settings.searchMaxBodyLength);
24
26
  assertAgentDocsConsistent(settings);
@@ -337,7 +339,18 @@ function buildPlugins(settings, routeContext) {
337
339
  // `design-token-panel-bootstrap` (#4261). Preset-level rather than inside
338
340
  // the routes plugin because the bootstrap is reachable from chrome even
339
341
  // when packageOwnedRoutes is off.
340
- ...resolvesBundleZdtp(settings) ? [] : [{ name: "@takazudo/zudo-doc/plugins/zdtp-loader", options: {} }]
342
+ ...resolvesBundleZdtp(settings) ? [] : [{ name: "@takazudo/zudo-doc/plugins/zdtp-loader", options: {} }],
343
+ // math off → shadow math-block's `@takazudo/zudo-doc/katex-loader` with a
344
+ // throwing virtual module so a site without the optional katex peer gets no
345
+ // Rolldown UNRESOLVED_IMPORT warning (#4651). The bare `katex` is untouched.
346
+ ...settings.math ? [] : [{ name: "@takazudo/zudo-doc/plugins/katex-loader", options: {} }],
347
+ // docHistory off → shadow doc-history's `@takazudo/zudo-doc/diff-loader`
348
+ // the same way (#4652). Gated on `docHistory`, NOT `docHistoryUi`: the
349
+ // dates-only mode (`docHistory: true, docHistoryUi: false`) keeps the doc
350
+ // history feature on, so the shadow stays out of every configuration where
351
+ // the Compare view is still part of the enabled feature. Bare `diff` is
352
+ // untouched.
353
+ ...settings.docHistory ? [] : [{ name: "@takazudo/zudo-doc/plugins/diff-loader", options: {} }]
341
354
  ];
342
355
  }
343
356
  export {
@@ -10,6 +10,7 @@ const DEFAULT_SETTINGS = {
10
10
  siteDescription: "",
11
11
  logo: "auto",
12
12
  favicon: void 0,
13
+ headerBrand: false,
13
14
  base: "/",
14
15
  trailingSlash: false,
15
16
  home: { wide: false, introMarkdown: "", sitemapHeading: "" },
@@ -260,6 +260,19 @@ export interface FaviconConfig {
260
260
  png16?: string;
261
261
  ico?: string;
262
262
  }
263
+ /**
264
+ * Opt-in header brand icon rendered before the site name in the header logo
265
+ * anchor. The icon is painted as a `currentColor` CSS mask, so it follows the
266
+ * site-name ink in every color scheme and theme pack; it is decorative
267
+ * (`aria-hidden`) and the anchor's accessible name stays the site name.
268
+ *
269
+ * `icon` values starting with `/` are passed through `withBase()`; anything
270
+ * else (`data:`, `https://…`) is used verbatim. The value must be a non-empty
271
+ * string — anything else throws a `TypeError` at config resolution.
272
+ */
273
+ export interface HeaderBrandConfig {
274
+ icon: string;
275
+ }
263
276
  /**
264
277
  * Site-wide custom `<head>` extras injected into every page via
265
278
  * {@link HeadWithDefaults}. All fields are JSON-serializable (no VNodes or
@@ -341,6 +354,14 @@ export interface Settings {
341
354
  * resolves to the current document — pass `false` or omit instead (#3471).
342
355
  */
343
356
  favicon?: string | FaviconConfig | false;
357
+ /**
358
+ * Header brand icon shown before the site name in the header logo anchor.
359
+ * Omitted or `false` (the default) renders the site name alone, exactly as
360
+ * before; a {@link HeaderBrandConfig} renders its `icon` as a decorative
361
+ * `currentColor` mask. Invalid values (`true`, a non-object, an array, a
362
+ * missing/non-string/empty `icon`) throw a `TypeError` at config resolution.
363
+ */
364
+ headerBrand?: false | HeaderBrandConfig;
344
365
  base: string;
345
366
  trailingSlash: boolean;
346
367
  /** Package-owned home-page layout. Narrow when omitted. */
@@ -508,9 +508,7 @@ function TrayItem({
508
508
  rankDigits !== void 0 ? /* @__PURE__ */ jsx(
509
509
  "span",
510
510
  {
511
- class: computed(
512
- () => `shrink-0 tabular-nums${active.value ? "" : " text-muted"}`
513
- ),
511
+ class: "shrink-0 tabular-nums",
514
512
  children: computed(
515
513
  () => item.value.rank === void 0 ? "" : String(item.value.rank).padStart(rankDigits, "0")
516
514
  )
@@ -526,9 +524,7 @@ function TrayItem({
526
524
  /* @__PURE__ */ jsx(Show, { when: computed(() => showDate && !!shortDate.value), children: () => /* @__PURE__ */ jsx(
527
525
  "span",
528
526
  {
529
- class: computed(
530
- () => `shrink-0 tabular-nums${active.value ? "" : " text-muted"}`
531
- ),
527
+ class: "shrink-0 tabular-nums",
532
528
  children: shortDate
533
529
  }
534
530
  ) })
@@ -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]));
@@ -158,7 +161,7 @@ function CategoryNode({
158
161
  {
159
162
  depth,
160
163
  isLast: true,
161
- widthScale: 2,
164
+ widthScale: CONNECTOR_WIDTH_SCALE,
162
165
  topPad: "calc(0.15rem + var(--spacing-vsp-xs))"
163
166
  }
164
167
  ) }),
@@ -167,7 +170,7 @@ function CategoryNode({
167
170
  {
168
171
  depth,
169
172
  isLast: false,
170
- widthScale: 2,
173
+ widthScale: CONNECTOR_WIDTH_SCALE,
171
174
  topPad: "calc(0.15rem + var(--spacing-vsp-xs))"
172
175
  }
173
176
  ) }),
@@ -325,7 +328,7 @@ function NoteTrayRow({
325
328
  {
326
329
  href: computed(() => href.value ?? ""),
327
330
  "data-note-tray-row": true,
328
- class: "flex items-start gap-hsp-sm py-vsp-2xs text-small text-fg hover:text-accent hover:underline focus:underline focus-visible:text-accent",
331
+ class: "group flex items-start gap-hsp-sm py-vsp-2xs text-small text-fg hover:text-accent hover:underline focus:underline focus-visible:text-accent",
329
332
  children: [
330
333
  /* @__PURE__ */ jsx(
331
334
  Show,
@@ -334,7 +337,7 @@ function NoteTrayRow({
334
337
  fallback: () => /* @__PURE__ */ jsx(
335
338
  "span",
336
339
  {
337
- class: "shrink-0 font-mono tabular-nums text-caption text-muted",
340
+ class: "shrink-0 font-mono tabular-nums text-caption text-muted group-hover:text-accent group-focus-visible:text-accent",
338
341
  style: computed(() => ({ width: `${width.value}ch` })),
339
342
  children: rankLabel
340
343
  }
@@ -343,7 +346,7 @@ function NoteTrayRow({
343
346
  "time",
344
347
  {
345
348
  datetime: computed(() => item.value.date ?? ""),
346
- class: "shrink-0 font-mono tabular-nums text-caption text-muted",
349
+ class: "shrink-0 font-mono tabular-nums text-caption text-muted group-hover:text-accent group-focus-visible:text-accent",
347
350
  children: computed(() => dateLabel.value ?? "")
348
351
  }
349
352
  )
@@ -370,7 +373,7 @@ function LeafNode({
370
373
  {
371
374
  depth,
372
375
  isLast: true,
373
- widthScale: 2,
376
+ widthScale: CONNECTOR_WIDTH_SCALE,
374
377
  topPad
375
378
  }
376
379
  ) }),
@@ -379,7 +382,7 @@ function LeafNode({
379
382
  {
380
383
  depth,
381
384
  isLast: false,
382
- widthScale: 2,
385
+ widthScale: CONNECTOR_WIDTH_SCALE,
383
386
  topPad
384
387
  }
385
388
  ) }),
@@ -387,8 +387,8 @@ html[data-theme-pack="brutalist"] .zd-content blockquote a {
387
387
  font-weight: 500;
388
388
  }
389
389
  /* Card-style prose links (e.g. the note-tray IndexList rank/label/
390
- description spans) put their own color on group-hover: utility
391
- classes (group-hover:text-fg / group-hover:text-accent), which beats
390
+ description/date spans) put their own color on group-hover: utility
391
+ classes (group-hover:text-accent), which beats
392
392
  the anchor's inherited `color` — so the anchor-level color below
393
393
  never reached that text. Measured 2.63:1 (rgb(245 245 245) on
394
394
  rgb(255 106 31), dark) / 4.06:1 (rgb(0 0 0) on rgb(194 65 12), light)
@@ -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
  };
package/dist/wind.json CHANGED
@@ -136,14 +136,12 @@
136
136
  "grid-rows-subgrid",
137
137
  "group",
138
138
  "group-focus-visible:text-accent",
139
- "group-focus-visible:text-fg",
140
139
  "group-focus-visible:underline",
141
140
  "group-focus-within:block",
142
141
  "group-hover:bg-fg",
143
142
  "group-hover:block",
144
143
  "group-hover:text-accent",
145
144
  "group-hover:text-bg",
146
- "group-hover:text-fg",
147
145
  "group-hover:underline",
148
146
  "group-open:rotate-90",
149
147
  "h-[0.5rem]",
@@ -180,16 +180,20 @@ async function getCachedDiff(
180
180
  return hit;
181
181
  }
182
182
  // Lazy-load diff — only needed after History → Compare. This keeps the
183
- // module out of the eager islands bundle. `diff` is an optional peer: the
184
- // literal `import("diff").then(onFulfilled, onRejected)` shape is what lets
185
- // esbuild leave it unresolved when absent instead of failing the build
186
- // (#4209). The rejection surfaces through DiffViewer's `.catch()` → diffError.
187
- const { diffLines } = await import("diff").then(
188
- (m) => m,
189
- () => {
190
- throw new Error('Compare requires the optional peer "diff": install it to use docHistory');
191
- },
183
+ // module out of the eager islands bundle. `diff` is an optional peer reached
184
+ // ONLY through the package-owned `@takazudo/zudo-doc/diff-loader` (#4652):
185
+ // with `docHistory` off the preset's `diff-loader` plugin shadows that
186
+ // specifier with a throwing virtual module, so the bundler never tries to
187
+ // resolve an absent `diff` (no UNRESOLVED_IMPORT); with `docHistory: true`
188
+ // and diff missing the loader resolves `diffLines` to `null` (#4209). The
189
+ // thrown error surfaces through DiffViewer's `.catch()` → diffError.
190
+ const diffLines = await import("@takazudo/zudo-doc/diff-loader").then(
191
+ (m) => m.diffLines,
192
+ () => null,
192
193
  );
194
+ if (!diffLines) {
195
+ throw new Error('Compare requires the optional peer "diff": install it to use docHistory');
196
+ }
193
197
  // The module import cannot be aborted; a replaced comparison must not do
194
198
  // work or populate the cache after its owning scope has been disposed.
195
199
  if (!isCurrent()) throw new Error("Comparison cancelled");
@@ -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
+ }
@@ -587,10 +587,7 @@ function TrayItem({
587
587
  >
588
588
  {rankDigits !== undefined ? (
589
589
  <span
590
- class={computed(
591
- () =>
592
- `shrink-0 tabular-nums${active.value ? "" : " text-muted"}`,
593
- )}
590
+ class="shrink-0 tabular-nums"
594
591
  >
595
592
  {computed(() =>
596
593
  item.value.rank === undefined
@@ -606,10 +603,7 @@ function TrayItem({
606
603
  <Show when={computed(() => showDate && !!shortDate.value)}>
607
604
  {() => (
608
605
  <span
609
- class={computed(
610
- () =>
611
- `shrink-0 tabular-nums${active.value ? "" : " text-muted"}`,
612
- )}
606
+ class="shrink-0 tabular-nums"
613
607
  >
614
608
  {shortDate}
615
609
  </span>