@takazudo/zudo-doc 5.5.2 → 5.6.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 (70) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +39 -0
  3. package/dist/chrome/derive.d.ts +46 -0
  4. package/dist/chrome/derive.js +6 -2
  5. package/dist/config-assertions/index.d.ts +31 -0
  6. package/dist/config-assertions/index.js +24 -0
  7. package/dist/config.d.ts +23 -2
  8. package/dist/config.js +3 -0
  9. package/dist/current-path/index.d.ts +27 -0
  10. package/dist/current-path/index.js +11 -0
  11. package/dist/design-token-panel-bootstrap.d.ts +89 -23
  12. package/dist/design-token-panel-bootstrap.js +19 -6
  13. package/dist/doc-page-props/index.d.ts +5 -67
  14. package/dist/doc-route-entries/index.d.ts +10 -95
  15. package/dist/doc-route-entries/index.js +1 -78
  16. package/dist/doc-route-paths/index.d.ts +1 -1
  17. package/dist/head-with-defaults/index.d.ts +3 -1
  18. package/dist/head-with-defaults/index.js +76 -4
  19. package/dist/header/nav-active.d.ts +28 -0
  20. package/dist/header/nav-active.js +2 -1
  21. package/dist/header/nav-overflow-script.js +30 -17
  22. package/dist/header-with-defaults/index.js +11 -2
  23. package/dist/i18n-version/language-switcher.d.ts +6 -0
  24. package/dist/i18n-version/language-switcher.js +3 -1
  25. package/dist/i18n-version/version-switcher.d.ts +6 -0
  26. package/dist/i18n-version/version-switcher.js +3 -1
  27. package/dist/nav-source-docs/index.d.ts +7 -11
  28. package/dist/plugins/route-pages-candidates.d.ts +19 -0
  29. package/dist/plugins/route-pages-candidates.js +17 -0
  30. package/dist/plugins/routes.d.ts +46 -0
  31. package/dist/plugins/routes.js +72 -18
  32. package/dist/preset.d.ts +12 -1
  33. package/dist/preset.js +2 -0
  34. package/dist/route-context/index.js +2 -2
  35. package/dist/routes/_chrome.d.ts +1 -1
  36. package/dist/routes/_chrome.js +4 -0
  37. package/dist/routes/_context.d.ts +3 -3
  38. package/dist/routes/_design-token-panel-bootstrap.d.ts +18 -0
  39. package/dist/routes/_design-token-panel-bootstrap.js +11 -0
  40. package/dist/routes/_docs-helpers.d.ts +1 -36
  41. package/dist/routes/_docs-helpers.js +0 -138
  42. package/dist/safelist.css +1 -1
  43. package/dist/search-widget-script/generated-script.d.ts +8 -0
  44. package/dist/search-widget-script/generated-script.js +465 -0
  45. package/dist/search-widget-script/index.d.ts +1 -18
  46. package/dist/search-widget-script/index.js +1 -443
  47. package/dist/settings.d.ts +82 -1
  48. package/dist/sidebar-tree/category-meta.d.ts +9 -0
  49. package/dist/sidebar-tree/category-meta.js +21 -12
  50. package/dist/sidebar-tree-island/index.d.ts +8 -1
  51. package/dist/sidebar-tree-island/index.js +16 -14
  52. package/dist/site-schema/doc-route-entries.d.ts +89 -0
  53. package/dist/site-schema/doc-route-entries.js +83 -0
  54. package/dist/site-schema/index.d.ts +17 -0
  55. package/dist/site-schema/index.js +46 -0
  56. package/dist/site-schema/nav-tree.d.ts +28 -0
  57. package/dist/site-schema/nav-tree.js +138 -0
  58. package/dist/site-schema/types.d.ts +97 -0
  59. package/dist/site-schema/types.js +0 -0
  60. package/dist/theme/theme-pack-provider.d.ts +34 -3
  61. package/dist/theme/theme-pack-provider.js +30 -2
  62. package/eject/header/nav-active.ts +13 -1
  63. package/eject/header/nav-overflow-script.ts +30 -17
  64. package/eject/sidebar-tree-island/index.tsx +44 -20
  65. package/package.json +22 -12
  66. package/routes-src/_chrome.tsx +21 -9
  67. package/routes-src/_design-token-panel-bootstrap.tsx +63 -0
  68. package/routes-src/_docs-helpers.ts +18 -225
  69. package/routes-src/_virtual.d.ts +5 -2
  70. package/virtual-modules.d.ts +5 -2
@@ -2,100 +2,15 @@ import type { DocPageEntry, DocNavNode, AutoIndexNode, DocPageBaseProps } from "
2
2
  import type { NavSourceDocs } from "../nav-source-docs/index.js";
3
3
  import type { BreadcrumbItem } from "../breadcrumb/types.js";
4
4
  import type { HeadingItem } from "../extract-headings/index.js";
5
- import type { CategoryMeta } from "../sidebar-tree/index.js";
5
+ import type { BuildDocRouteEntriesArgs as BuildDocRouteEntriesArgsGeneric, DocRouteEntriesAPI as DocRouteEntriesAPIGeneric, DocRouteEntriesContext as DocRouteEntriesContextGeneric, DocRouteEntry as DocRouteEntryGeneric } from "../site-schema/doc-route-entries.js";
6
+ export { createDocRouteEntries } from "../site-schema/doc-route-entries.js";
6
7
  export type { DocPageEntry, DocNavNode, AutoIndexNode, DocPageBaseProps, NavSourceDocs };
7
- /** One enumerated doc route: a content entry or an auto-generated category
8
- * index, with all per-page derived data pre-computed. */
9
- export interface DocRouteEntry {
10
- /** Canonical route slug ("" for the docs root index — #1891). */
11
- slug: string;
12
- /** Optional-catchall params array — `toSlugParams(slug)` ([] for the root). */
13
- slugParams: string[];
14
- /**
15
- * True when the entry came from the base collection rather than the locale
16
- * collection (`!localeSlugSet.has(slug)`). Only meaningful on routes whose
17
- * nav source performs a locale merge — routes without one (default-locale /
18
- * versioned-EN, where `localeSlugSet` is empty) must ignore this field.
19
- * Always false for autoIndex items.
20
- */
21
- isFallback: boolean;
22
- /** Shared page props (kind/entry/autoIndex/breadcrumbs/prev/next/headings). */
23
- props: DocPageBaseProps;
24
- }
25
- export interface BuildDocRouteEntriesArgs {
26
- /** Identity-stable nav source for this route's (locale, version) context. */
27
- source: NavSourceDocs;
28
- /** Active locale for nav-tree labels and breadcrumbs. */
29
- locale: string;
30
- /**
31
- * Unique memo signature for this route context. Each route file passes its
32
- * own prefix plus the loop variables (version slug / locale), e.g.
33
- * "docs;en", "locale-docs;ja", "v-docs;1.0", "v-locale-docs;1.0;ja" —
34
- * call sites that share a docs array identity must never collide on a key.
35
- */
36
- routeSig: string;
37
- /** Versioned URL closure bound to the route's version (+ locale). Presence
38
- * switches the versioned behaviors (breadcrumbs, prev/next, child hrefs). */
39
- urlFor?: (slug: string) => string;
40
- }
41
8
  export type { BreadcrumbItem, HeadingItem };
42
- /**
43
- * Context slice `createDocRouteEntries` derives its bag from (epic Collapse
44
- * Wiring Shells #2420, FACTORIES #2424 breaking signature). STRUCTURAL SUBSET
45
- * of the unified `RouteContext`/`ChromeContext` — the nav-tree builder is
46
- * reconstructed by binding the context's 4-arg `buildNavTree` to `docsUrl`, and
47
- * the breadcrumb trail by binding the package `buildBreadcrumbs` to the
48
- * locale-prefixed `withBase` base, exactly as the pre-collapse wiring did.
49
- */
50
- export interface DocRouteEntriesContext {
51
- /** Default locale code (drives the breadcrumb base-prefix selection). */
52
- defaultLocale: string;
53
- /** Build the nav tree for a locale (4-arg form with an explicit href builder). */
54
- buildNavTree: (docs: DocPageEntry[], locale: string, categoryMeta: Map<string, CategoryMeta> | undefined, buildHref: (slug: string, locale: string) => string) => DocNavNode[];
55
- /** Build a docs URL for the given slug and locale. */
56
- docsUrl: (slug: string, locale?: string) => string;
57
- /** Prefix a path with the configured base directory. */
58
- withBase: (path: string) => string;
59
- /** Collect all auto-generated index nodes (categories without index.mdx). */
60
- collectAutoIndexNodes: (tree: DocNavNode[]) => AutoIndexNode[];
61
- /** Determine the nav section (categoryMatch) for a slug. */
62
- getNavSectionForSlug: (slug: string) => string | undefined;
63
- /** Filter top-level nav nodes by a categoryMatch value. */
64
- getNavSubtree: (tree: DocNavNode[], categoryMatch?: string) => DocNavNode[];
65
- /** Convert a content entry slug to a canonical route slug. */
66
- toRouteSlug: (entrySlug: string) => string;
67
- /** Convert a canonical route slug to an optional-catchall params array. */
68
- toSlugParams: (slug: string) => string[];
69
- /** Extract headings from a raw MDX body (bound to settings depth/strategy). */
70
- extractHeadings: (body: string) => HeadingItem[];
71
- }
72
- /**
73
- * Result of `createDocRouteEntries`. Contains the `buildDocRouteEntries`
74
- * function bound to the injected context.
75
- */
76
- export interface DocRouteEntriesAPI {
77
- /**
78
- * Enumerate all doc routes for one (locale, version) context, with per-entry
79
- * derived data pre-computed. Memoized per build on the identity-stable
80
- * `source.docs` array.
81
- */
82
- buildDocRouteEntries: (args: BuildDocRouteEntriesArgs) => DocRouteEntry[];
83
- }
84
- /**
85
- * Create the `buildDocRouteEntries` function bound to the host's injected
86
- * context.
87
- *
88
- * @example
89
- * ```ts
90
- * // pages/lib/_doc-route-entries.ts (thin stub)
91
- * import { createDocRouteEntries } from "@takazudo/zudo-doc/doc-route-entries";
92
- * import { buildNavTree, buildBreadcrumbs, collectAutoIndexNodes } from "@/utils/docs";
93
- * import { getNavSectionForSlug, getNavSubtree } from "@/utils/nav-scope";
94
- * import { toRouteSlug, toSlugParams } from "@takazudo/zudo-doc/slug";
95
- * import { extractHeadings } from "./_extract-headings";
96
- *
97
- * export const { buildDocRouteEntries } = createDocRouteEntries(routeContext);
98
- * export type { DocRouteEntry, BuildDocRouteEntriesArgs };
99
- * ```
100
- */
101
- export declare function createDocRouteEntries(ctx: DocRouteEntriesContext): DocRouteEntriesAPI;
9
+ /** One enumerated doc route, carrying a zfb collection entry. */
10
+ export type DocRouteEntry = DocRouteEntryGeneric<DocPageEntry>;
11
+ /** Arguments to `buildDocRouteEntries` for one (locale, version) context. */
12
+ export type BuildDocRouteEntriesArgs = BuildDocRouteEntriesArgsGeneric<DocPageEntry>;
13
+ /** The context slice `createDocRouteEntries` derives its bag from. */
14
+ export type DocRouteEntriesContext = DocRouteEntriesContextGeneric<DocPageEntry>;
15
+ /** The functions `createDocRouteEntries` returns. */
16
+ export type DocRouteEntriesAPI = DocRouteEntriesAPIGeneric<DocPageEntry>;
@@ -1,81 +1,4 @@
1
- import { memoizeDerived } from "../nav-source-cache/index.js";
2
- import { buildBreadcrumbs as buildBreadcrumbsImpl } from "../routes/_docs-helpers.js";
3
- import {
4
- resolveDocPrevNext,
5
- flattenSubtree,
6
- rewriteNavHref,
7
- remapNavChildHrefs
8
- } from "../doc-route-paths/index.js";
9
- function createDocRouteEntries(ctx) {
10
- const defaultLocale = ctx.defaultLocale;
11
- const buildNavTree = (docs, locale, categoryMeta) => ctx.buildNavTree(docs, locale, categoryMeta, (slug, loc) => ctx.docsUrl(slug, loc));
12
- const buildBreadcrumbs = (tree, slug, locale, urlFor) => buildBreadcrumbsImpl(
13
- tree,
14
- slug,
15
- locale === defaultLocale ? ctx.withBase("/") : ctx.withBase(`/${locale}/`),
16
- urlFor
17
- );
18
- const collectAutoIndexNodes = ctx.collectAutoIndexNodes;
19
- const getNavSectionForSlug = ctx.getNavSectionForSlug;
20
- const getNavSubtree = ctx.getNavSubtree;
21
- const toRouteSlug = ctx.toRouteSlug;
22
- const toSlugParams = ctx.toSlugParams;
23
- const extractHeadings = ctx.extractHeadings;
24
- function buildDocRouteEntries(args) {
25
- const { source, locale, routeSig, urlFor } = args;
26
- return memoizeDerived([source.docs], `docRouteEntries;${routeSig}`, () => {
27
- const { docs, navDocs, categoryMeta, localeSlugSet } = source;
28
- const tree = buildNavTree(navDocs, locale, categoryMeta);
29
- const breadcrumbTree = urlFor ? tree : buildNavTree(docs, locale, categoryMeta);
30
- const result = [];
31
- for (const entry of docs) {
32
- if (entry.data.category_no_page === true) continue;
33
- const slug = entry.data.slug ?? toRouteSlug(entry.slug);
34
- const navSection = getNavSectionForSlug(slug);
35
- const subtree = getNavSubtree(tree, navSection);
36
- const { prev: prevNode, next: nextNode } = resolveDocPrevNext(
37
- tree,
38
- flattenSubtree(subtree),
39
- slug,
40
- entry.data
41
- );
42
- result.push({
43
- slug,
44
- slugParams: toSlugParams(slug),
45
- isFallback: !localeSlugSet.has(slug),
46
- props: {
47
- kind: "entry",
48
- entry,
49
- breadcrumbs: buildBreadcrumbs(breadcrumbTree, slug, locale, urlFor),
50
- prev: rewriteNavHref(prevNode, urlFor),
51
- next: rewriteNavHref(nextNode, urlFor),
52
- headings: extractHeadings(entry.body ?? "")
53
- }
54
- });
55
- }
56
- for (const node of collectAutoIndexNodes(tree)) {
57
- result.push({
58
- slug: node.slug,
59
- slugParams: toSlugParams(node.slug),
60
- isFallback: false,
61
- props: {
62
- kind: "autoIndex",
63
- autoIndex: urlFor ? {
64
- ...node,
65
- children: remapNavChildHrefs(node.children, urlFor)
66
- } : node,
67
- breadcrumbs: buildBreadcrumbs(breadcrumbTree, node.slug, locale, urlFor),
68
- prev: null,
69
- next: null,
70
- headings: []
71
- }
72
- });
73
- }
74
- return result;
75
- });
76
- }
77
- return { buildDocRouteEntries };
78
- }
1
+ import { createDocRouteEntries } from "../site-schema/doc-route-entries.js";
79
2
  export {
80
3
  createDocRouteEntries
81
4
  };
@@ -1,4 +1,4 @@
1
- import type { DocNavNode } from "../doc-page-props/index.js";
1
+ import type { DocNavNode } from "../site-schema/types.js";
2
2
  export type { DocNavNode };
3
3
  /**
4
4
  * Flatten a nav tree into a pre-order traversal array.
@@ -4,7 +4,7 @@ import type { JSX } from "preact";
4
4
  import type { HeadProps } from "../head/types.js";
5
5
  import type { ColorSchemeProviderColorMode } from "../theme/color-scheme-provider.js";
6
6
  import type { ChromeContext } from "../factory-context/index.js";
7
- import type { Settings } from "../settings.js";
7
+ import type { Settings, FaviconConfig } from "../settings.js";
8
8
  export interface HeadWithDefaultsProps {
9
9
  /** Page title forwarded to og:title. Required. */
10
10
  title: string;
@@ -33,6 +33,8 @@ export interface HeadWithDefaultsSettings {
33
33
  sidebarResizer?: boolean;
34
34
  /** Configured theme-pack slug (ADR `docs/adr/theme-packs.md`, #2822). */
35
35
  themePack?: string;
36
+ /** Favicon link set — see {@link resolveFaviconLinks} for the emission table. */
37
+ favicon?: string | FaviconConfig | false;
36
38
  }
37
39
  /**
38
40
  * Create a `HeadWithDefaults` component from the unified {@link ChromeContext}
@@ -6,8 +6,82 @@ import ThemePackProvider, {
6
6
  themePackVersionMap
7
7
  } from "../theme/theme-pack-provider.js";
8
8
  import { DEFAULT_THEME_PACK_SLUG } from "../theme-pack-switcher/theme-pack-sync.js";
9
+ import { renderAutoLogoIconSvg } from "../auto-logo/icon.js";
9
10
  import { deriveComposeMetaTitle, deriveColorSchemeGenerators } from "../chrome/derive.js";
10
11
  import { assertChromeContext } from "../chrome/assert-chrome-context.js";
12
+ const FAVICON_AUTO = "auto";
13
+ const DEFAULT_FAVICON = {
14
+ svg: "/favicon.svg",
15
+ ico: "/favicon.ico",
16
+ png32: "/favicon-32x32.png",
17
+ png16: "/favicon-16x16.png"
18
+ };
19
+ const FAVICON_TYPE_BY_EXT = {
20
+ svg: "image/svg+xml",
21
+ png: "image/png",
22
+ ico: "image/x-icon",
23
+ jpg: "image/jpeg",
24
+ jpeg: "image/jpeg",
25
+ gif: "image/gif",
26
+ webp: "image/webp",
27
+ avif: "image/avif"
28
+ };
29
+ function faviconType(value) {
30
+ const path = value.split(/[?#]/)[0] ?? "";
31
+ const file = path.slice(path.lastIndexOf("/") + 1);
32
+ const dot = file.lastIndexOf(".");
33
+ if (dot < 0) return void 0;
34
+ return FAVICON_TYPE_BY_EXT[file.slice(dot + 1).toLowerCase()];
35
+ }
36
+ function faviconHref(value, withBase) {
37
+ return value.startsWith("/") ? withBase(value) : value;
38
+ }
39
+ function resolveFaviconLinks(favicon, siteName, withBase) {
40
+ if (favicon === false) return [];
41
+ if (typeof favicon === "string") {
42
+ if (favicon === FAVICON_AUTO) {
43
+ const svg = encodeURIComponent(renderAutoLogoIconSvg(siteName));
44
+ return [{ rel: "icon", type: "image/svg+xml", href: `data:image/svg+xml,${svg}` }];
45
+ }
46
+ const type = faviconType(favicon);
47
+ return [
48
+ {
49
+ rel: "icon",
50
+ ...type !== void 0 ? { type } : {},
51
+ href: faviconHref(favicon, withBase)
52
+ }
53
+ ];
54
+ }
55
+ const slots = favicon ?? DEFAULT_FAVICON;
56
+ const links = [];
57
+ if (slots.svg !== void 0) {
58
+ links.push({
59
+ rel: "icon",
60
+ type: faviconType(slots.svg) ?? "image/svg+xml",
61
+ href: faviconHref(slots.svg, withBase)
62
+ });
63
+ }
64
+ if (slots.ico !== void 0) {
65
+ links.push({ rel: "icon", href: faviconHref(slots.ico, withBase), sizes: "any" });
66
+ }
67
+ if (slots.png32 !== void 0) {
68
+ links.push({
69
+ rel: "icon",
70
+ type: faviconType(slots.png32) ?? "image/png",
71
+ sizes: "32x32",
72
+ href: faviconHref(slots.png32, withBase)
73
+ });
74
+ }
75
+ if (slots.png16 !== void 0) {
76
+ links.push({
77
+ rel: "icon",
78
+ type: faviconType(slots.png16) ?? "image/png",
79
+ sizes: "16x16",
80
+ href: faviconHref(slots.png16, withBase)
81
+ });
82
+ }
83
+ return links;
84
+ }
11
85
  function createHeadWithDefaults(ctx) {
12
86
  assertChromeContext(ctx, "createHeadWithDefaults");
13
87
  const settings = ctx.settings;
@@ -19,6 +93,7 @@ function createHeadWithDefaults(ctx) {
19
93
  const themePackEnabled = themePackRegistry !== null ? themePackVersionMap(themePackRegistry) : null;
20
94
  const themePackConfigured = settings.themePack ?? DEFAULT_THEME_PACK_SLUG;
21
95
  const themePackBase = withBase("/");
96
+ const faviconLinks = resolveFaviconLinks(settings.favicon, settings.siteName, withBase);
22
97
  function HeadWithDefaults({
23
98
  title,
24
99
  description,
@@ -65,10 +140,7 @@ function createHeadWithDefaults(ctx) {
65
140
  }
66
141
  ),
67
142
  settings.sidebarResizer && /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: SIDEBAR_RESIZER_RESTORE_SCRIPT } }),
68
- /* @__PURE__ */ jsx("link", { rel: "icon", type: "image/svg+xml", href: withBase("/favicon.svg") }),
69
- /* @__PURE__ */ jsx("link", { rel: "icon", href: withBase("/favicon.ico"), sizes: "any" }),
70
- /* @__PURE__ */ jsx("link", { rel: "icon", type: "image/png", sizes: "32x32", href: withBase("/favicon-32x32.png") }),
71
- /* @__PURE__ */ jsx("link", { rel: "icon", type: "image/png", sizes: "16x16", href: withBase("/favicon-16x16.png") }),
143
+ faviconLinks.map((attrs, i) => /* @__PURE__ */ jsx("link", { ...attrs }, i)),
72
144
  canonical !== void 0 && /* @__PURE__ */ jsx("link", { rel: "canonical", href: canonical }),
73
145
  ctx.settings.head && /* @__PURE__ */ jsxs(Fragment, { children: [
74
146
  ctx.settings.head.preconnect?.map((p, i) => /* @__PURE__ */ jsx(
@@ -25,6 +25,34 @@ export interface NavItemLike {
25
25
  * pathForMatch(null, "ja", "en") === ""
26
26
  */
27
27
  export declare function pathForMatch(pathWithoutBase: string, lang: string | undefined, defaultLocale: string): string;
28
+ /**
29
+ * Pick the deepest header-nav path that the current page lives under.
30
+ *
31
+ * The legacy template flattens parent + child paths, filters by
32
+ * `pathForMatch.startsWith(p)`, and picks the longest match — this keeps
33
+ * a child link active even when its parent's path is also a prefix of
34
+ * the current page.
35
+ *
36
+ * Returns `undefined` when no nav entry matches; callers should treat
37
+ * that as "no active link."
38
+ */
39
+ /**
40
+ * Segment-aware prefix test: nav path `/docs/guides` matches `/docs/guides`
41
+ * and `/docs/guides/...` but NOT `/docs/guideship`.
42
+ *
43
+ * Exported (not just module-private) so `nav-overflow-script.ts` can embed it
44
+ * verbatim via `.toString()` alongside `computeActiveNavPath` — that function
45
+ * closes over this one, so a bare `computeActiveNavPath.toString()` embed
46
+ * would reference an undefined `pathMatchesNavPath` in the browser (see the
47
+ * caution note on `computeActiveNavPath` below). Kept self-contained (no
48
+ * outer references) for the same reason.
49
+ */
50
+ export declare function pathMatchesNavPath(currentPath: string, navPath: string): boolean;
51
+ /**
52
+ * CAUTION (zudolab/zudo-doc#3398): this closes over `pathMatchesNavPath`
53
+ * above. `nav-overflow-script.ts` embeds both via `.toString()` (never this
54
+ * one alone) so the generated browser script is self-contained.
55
+ */
28
56
  export declare function computeActiveNavPath(navItems: readonly NavItemLike[], pathForMatchValue: string): string | undefined;
29
57
  /**
30
58
  * Active-state predicate for a top-level nav item: the item itself, or
@@ -34,5 +34,6 @@ export {
34
34
  computeActiveNavPath,
35
35
  isNavItemActive,
36
36
  isNavItemActiveByCategory,
37
- pathForMatch
37
+ pathForMatch,
38
+ pathMatchesNavPath
38
39
  };
@@ -1,4 +1,6 @@
1
1
  import { AFTER_NAVIGATE_EVENT } from "../transitions/page-events.js";
2
+ import { CURRENT_PATH_SCRIPT_PRELUDE } from "../current-path/index.js";
3
+ import { computeActiveNavPath, pathMatchesNavPath } from "./nav-active.js";
2
4
  import {
3
5
  NAV_CHEVRON_ACTIVE,
4
6
  NAV_CHEVRON_INACTIVE,
@@ -29,20 +31,26 @@ const NAV_OVERFLOW_SCRIPT = `(function () {
29
31
  catch (e) { return ""; }
30
32
  }
31
33
 
32
- function isUnderPath(cur, p) {
33
- if (!p) return false;
34
- if (cur === p) return true;
35
- return p !== "/" && cur.indexOf(p + "/") === 0;
36
- }
34
+ // Explicit current-route override, embedded from current-path/index.ts so
35
+ // this script cannot drift from the three other read sites
36
+ // (zudolab/zudo-doc#3398, #3408).
37
+ ${CURRENT_PATH_SCRIPT_PRELUDE}
38
+
39
+ // Shared matching core (zudolab/zudo-doc#3398): embedded verbatim from
40
+ // nav-active.ts so this script's longest-match walk cannot drift from the
41
+ // SSR header's own computeActiveNavPath call (header.tsx). computeActiveNavPath
42
+ // closes over pathMatchesNavPath, so both are embedded together.
43
+ var pathMatchesNavPath = ${pathMatchesNavPath.toString()};
44
+ var computeActiveNavPath = ${computeActiveNavPath.toString()};
37
45
 
38
46
  // Recompute which header nav item is "active" from the CURRENT URL and
39
47
  // repaint the highlight. SSR sets the active item on first paint, but the
40
48
  // header is persisted across same-locale client-router swaps
41
49
  // (data-zfb-transition-persist), so without this the highlight would stay
42
50
  // frozen on the page where the header was first rendered. Mirrors the
43
- // sidebar island's client-side approach (match location.pathname against
51
+ // sidebar island's client-side approach (match the current path against
44
52
  // each entry's href) and the SSR longest-match + dropdown-parent rules.
45
- // URL-based: hrefs and location.pathname both carry the base + locale
53
+ // URL-based: hrefs and the current path both carry the base + locale
46
54
  // prefix, so they compare directly without stripping.
47
55
  function applyActiveNav() {
48
56
  var nav = document.querySelector("[data-header-nav]");
@@ -50,26 +58,31 @@ const NAV_OVERFLOW_SCRIPT = `(function () {
50
58
  var topItems = Array.from(nav.querySelectorAll(":scope > [data-nav-item]"));
51
59
  if (topItems.length === 0) return;
52
60
 
53
- var cur = trimSlashes(location.pathname);
61
+ var cur = trimSlashes(readCurrentPath(CURRENT_PATH_DATASET_KEY));
54
62
 
55
- // Deepest (longest) nav path the current URL lives under, across both
56
- // top-level and dropdown-child paths \u2014 matches computeActiveNavPath.
57
- var activePath = "";
63
+ // Build NavItemLike-shaped entries from the live DOM so the shared
64
+ // computeActiveNavPath can do the deepest-match walk \u2014 the same call
65
+ // shape the SSR header uses (matches computeActiveNavPath). A dropdown
66
+ // missing its own top-level anchor is skipped entirely (path "" would
67
+ // otherwise match every current path \u2014 pathMatchesNavPath treats "" as
68
+ // the root "/"), mirroring the parentLink guard used below for the same
69
+ // malformed-markup case.
70
+ var navItems = [];
58
71
  topItems.forEach(function (it) {
59
72
  var isDropdown = it.hasAttribute("data-nav-item-dropdown");
60
73
  var topA = isDropdown ? it.querySelector(":scope > a") : it;
61
- if (topA) {
62
- var tp = navPathname(topA);
63
- if (isUnderPath(cur, tp) && tp.length > activePath.length) activePath = tp;
64
- }
74
+ if (!topA) return;
75
+ var children = [];
65
76
  if (isDropdown) {
66
77
  it.querySelectorAll(":scope > div a").forEach(function (c) {
67
- var cp = navPathname(c);
68
- if (isUnderPath(cur, cp) && cp.length > activePath.length) activePath = cp;
78
+ children.push({ path: navPathname(c) });
69
79
  });
70
80
  }
81
+ navItems.push({ path: navPathname(topA), children: children });
71
82
  });
72
83
 
84
+ var activePath = computeActiveNavPath(navItems, cur) || "";
85
+
73
86
  function setTopActive(a, active) {
74
87
  if (!a) return;
75
88
  if (active) {
@@ -12,7 +12,8 @@ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
12
12
  import {
13
13
  deriveGetUnavailableVersions,
14
14
  deriveNavDataPrep,
15
- deriveSearchWidgetSlot
15
+ deriveSearchWidgetSlot,
16
+ skipsPackageDefaultDesignTokenPanel
16
17
  } from "../chrome/derive.js";
17
18
  function createHeaderWithDefaults(ctx) {
18
19
  assertChromeContext(ctx, "createHeaderWithDefaults");
@@ -130,7 +131,15 @@ function createHeaderWithDefaults(ctx) {
130
131
  const headerRightItems = filterHeaderRightItems(
131
132
  settings.headerRightItems ?? [],
132
133
  {
133
- designTokenPanel: Boolean(settings.designTokenPanel),
134
+ // Mirrors `deriveBodyEndIslands`' slot resolution (#3414): when the
135
+ // derive-level skip leaves a page with no panel island and no
136
+ // pre-hydration toggle shim, the header toggle button must go with
137
+ // them — otherwise it dispatches `toggle-design-token-panel` into a
138
+ // document with no listener and no click queue (a dead button).
139
+ // `hostBindings.DesignTokenPanelBootstrap` covers the injected-route
140
+ // path too (`routes/_chrome.tsx` threads the configured island through
141
+ // that slot), so injected pages keep the button.
142
+ designTokenPanel: Boolean(settings.designTokenPanel) && (ctx.hostBindings.DesignTokenPanelBootstrap != null || !skipsPackageDefaultDesignTokenPanel(ctx.settings)),
134
143
  aiAssistant: Boolean(settings.aiAssistant),
135
144
  colorMode: Boolean(settings.colorMode),
136
145
  hasLocales: Object.keys(settings.locales).length > 0,
@@ -40,6 +40,12 @@ export declare function switchLocaleHref(pathname: string, config: LanguageSwitc
40
40
  * `window[FLAG]` makes it idempotent: the tag may re-execute on a hard reload
41
41
  * or a cross-locale header repaint, but the listener is registered exactly
42
42
  * once per page lifetime.
43
+ *
44
+ * The pathname fed to `switchLocaleHref` comes from the embedded
45
+ * `readCurrentPath`, which prefers the `data-zd-current-path` override over
46
+ * `location.pathname` — the same explicit current-route reader
47
+ * `nav-overflow-script.ts` / `sidebar-tree-island` / `version-switcher.tsx`
48
+ * use (zudolab/zudo-doc#3398, consolidated by #3408).
43
49
  */
44
50
  export declare const LANGUAGE_SWITCHER_INIT_SCRIPT: string;
45
51
  export interface LanguageSwitcherProps {
@@ -1,6 +1,7 @@
1
1
  import { jsx, jsxs } from "preact/jsx-runtime";
2
2
  import { Fragment } from "preact";
3
3
  import { AFTER_NAVIGATE_EVENT } from "../transitions/index.js";
4
+ import { CURRENT_PATH_SCRIPT_PRELUDE } from "../current-path/index.js";
4
5
  function switchLocaleHref(pathname, config, currentLang, targetLang) {
5
6
  const normalizedBase = config.base;
6
7
  const defaultLocale = config.defaultLocale;
@@ -45,6 +46,7 @@ const LANGUAGE_SWITCHER_INIT_SCRIPT = `(function(){
45
46
  var FLAG="__zdLanguageSwitcherInit";
46
47
  if(window[FLAG])return;
47
48
  window[FLAG]=true;
49
+ ${CURRENT_PATH_SCRIPT_PRELUDE}
48
50
  var switchLocaleHref=${switchLocaleHref.toString()};
49
51
  function rewire(){
50
52
  var containers=document.querySelectorAll("[data-language-switcher]");
@@ -57,7 +59,7 @@ for(var j=0;j<anchors.length;j++){
57
59
  var a=anchors[j];
58
60
  var target=a.getAttribute("lang");
59
61
  if(!target)continue;
60
- a.setAttribute("href",switchLocaleHref(window.location.pathname,config,currentLang,target));
62
+ a.setAttribute("href",switchLocaleHref(readCurrentPath(CURRENT_PATH_DATASET_KEY),config,currentLang,target));
61
63
  }
62
64
  }
63
65
  }
@@ -208,6 +208,12 @@ export declare const VERSION_SWITCHER_INIT_SCRIPT: string;
208
208
  * `window[FLAG]` makes it idempotent: the tag may re-execute on a hard reload or
209
209
  * a cross-locale header repaint, but the listener registers exactly once per
210
210
  * page lifetime.
211
+ *
212
+ * The pathname fed to `computeVersionSwitcherState` comes from the embedded
213
+ * `readCurrentPath`, which prefers the `data-zd-current-path` override over
214
+ * `location.pathname` — the same explicit current-route reader
215
+ * `nav-overflow-script.ts` / `sidebar-tree-island` / `language-switcher.tsx`
216
+ * use (zudolab/zudo-doc#3398, consolidated by #3408).
211
217
  */
212
218
  export declare const VERSION_SWITCHER_REWIRE_SCRIPT: string;
213
219
  export type { VersionEntry, VersionSwitcherLabels };
@@ -1,5 +1,6 @@
1
1
  import { jsx, jsxs } from "preact/jsx-runtime";
2
2
  import { AFTER_NAVIGATE_EVENT } from "../transitions/page-events.js";
3
+ import { CURRENT_PATH_SCRIPT_PRELUDE } from "../current-path/index.js";
3
4
  import { UNAVAILABLE_VERSIONS_ATTR } from "../version-availability/index.js";
4
5
  function computeVersionSwitcherState(pathname, config, versionSlugs) {
5
6
  const normalizedBase = config.base;
@@ -235,6 +236,7 @@ var FLAG="__zdVersionSwitcherRewire";
235
236
  if(window[FLAG])return;
236
237
  window[FLAG]=true;
237
238
  var ATTR=${JSON.stringify(UNAVAILABLE_VERSIONS_ATTR)};
239
+ ${CURRENT_PATH_SCRIPT_PRELUDE}
238
240
  var computeVersionSwitcherState=${computeVersionSwitcherState.toString()};
239
241
  function setActive(a,active){
240
242
  a.classList.toggle("font-bold",active);
@@ -276,7 +278,7 @@ for(var j=0;j<versionAnchors.length;j++){
276
278
  var s=versionAnchors[j].getAttribute("data-version-slug");
277
279
  if(s)slugs.push(s);
278
280
  }
279
- var state=computeVersionSwitcherState(window.location.pathname,config,slugs);
281
+ var state=computeVersionSwitcherState(readCurrentPath(CURRENT_PATH_DATASET_KEY),config,slugs);
280
282
  var latest=c.querySelector("[data-version-latest]");
281
283
  if(latest){
282
284
  latest.setAttribute("href",state.latestHref);
@@ -1,18 +1,14 @@
1
1
  import type { CategoryMeta } from "../sidebar-tree/index.js";
2
2
  import type { DocPageEntry } from "../doc-page-props/index.js";
3
+ import type { NavSourceDocs as NavSourceDocsShape } from "../site-schema/types.js";
3
4
  import type { Settings } from "../settings.js";
4
5
  export type { CategoryMeta, DocPageEntry };
5
- export interface NavSourceDocs {
6
- /** Full doc list (merged + draft-filtered; unlisted retained per options). */
7
- docs: DocPageEntry[];
8
- /** `docs.filter(isNavVisible)` stable instance for buildNavTree. */
9
- navDocs: DocPageEntry[];
10
- /** Stable category-meta Map for the active (locale, version). */
11
- categoryMeta: Map<string, CategoryMeta>;
12
- /** Slugs that came from the locale collection (for isFallback). Empty for
13
- * default-locale / single-collection cases. */
14
- localeSlugSet: ReadonlySet<string>;
15
- }
6
+ /**
7
+ * The resolved nav source for one (locale, version) context, carrying zfb
8
+ * collection entries. The shape itself is browser-safe and declared in
9
+ * `../site-schema/types.js` (#3395); this alias pins it to `DocPageEntry`.
10
+ */
11
+ export type NavSourceDocs = NavSourceDocsShape<DocPageEntry>;
16
12
  /**
17
13
  * How to filter the merged doc list.
18
14
  */
@@ -0,0 +1,19 @@
1
+ /**
2
+ * File extensions zfb's router accepts as `pages/` route sources. Mirrors
3
+ * the Rust-side single source of truth `zfb_types::ROUTABLE_PAGE_EXTENSIONS`
4
+ * (zfb crate `crates/zfb-types/src/page_extensions.rs`) — that constant is
5
+ * not importable from JS, so it is copied here deliberately; keep in sync if
6
+ * zfb ever changes its accepted extension list.
7
+ */
8
+ export declare const ROUTABLE_PAGE_EXTENSIONS: readonly ["tsx", "ts", "jsx", "js", "mdx", "md", "html"];
9
+ /**
10
+ * Derive every `pages/`-relative path that would shadow the given injected
11
+ * route pattern (a zfb `injectRoute` pattern string, e.g.
12
+ * `"/docs/tags/[tag]"` or `"/[locale]"`). Pure — no filesystem I/O; the
13
+ * caller probes each candidate against the host's real `pages/` dir.
14
+ *
15
+ * Returns `[]` for the empty/root pattern (`"/"`) — never produced by this
16
+ * plugin's route catalog (zfb rejects `injectRoute("/")` outright), so there
17
+ * is nothing meaningful to derive.
18
+ */
19
+ export declare function derivePagesCandidates(pattern: string): string[];
@@ -0,0 +1,17 @@
1
+ const ROUTABLE_PAGE_EXTENSIONS = ["tsx", "ts", "jsx", "js", "mdx", "md", "html"];
2
+ function derivePagesCandidates(pattern) {
3
+ const segments = pattern.split("/").filter(Boolean);
4
+ if (segments.length === 0) return [];
5
+ const lastSegment = segments[segments.length - 1];
6
+ const dirPrefix = segments.length > 1 ? `${segments.slice(0, -1).join("/")}/` : "";
7
+ const candidates = [];
8
+ for (const ext of ROUTABLE_PAGE_EXTENSIONS) {
9
+ candidates.push(`${dirPrefix}${lastSegment}.${ext}`);
10
+ candidates.push(`${dirPrefix}${lastSegment}/index.${ext}`);
11
+ }
12
+ return candidates;
13
+ }
14
+ export {
15
+ ROUTABLE_PAGE_EXTENSIONS,
16
+ derivePagesCandidates
17
+ };