@takazudo/zudo-doc 5.6.0 → 5.7.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.
- package/CHANGELOG.md +25 -0
- package/dist/header/nav-active.d.ts +11 -8
- package/dist/header/nav-overflow-generated-script.d.ts +11 -0
- package/dist/header/nav-overflow-generated-script.js +302 -0
- package/dist/header/nav-overflow-script.d.ts +1 -1
- package/dist/header/nav-overflow-script.js +1 -299
- package/dist/md-utils/index.js +33 -1
- package/dist/safelist.css +1 -1
- package/dist/sidebar-toggle-island/index.js +2 -1
- package/dist/transitions/index.d.ts +1 -0
- package/dist/transitions/index.js +2 -0
- package/dist/transitions/nested-island-props-refresh.d.ts +19 -0
- package/dist/transitions/nested-island-props-refresh.js +104 -0
- package/eject/header/header.tsx +13 -5
- package/eject/header/nav-active.ts +11 -8
- package/eject/header/nav-class-tokens.ts +9 -5
- package/eject/header/nav-overflow-generated-script.ts +29 -0
- package/eject/header/nav-overflow-script.ts +26 -317
- package/eject/sidebar-toggle-island/index.tsx +13 -1
- package/package.json +7 -4
|
@@ -54,12 +54,14 @@ export function pathForMatch(
|
|
|
54
54
|
* Segment-aware prefix test: nav path `/docs/guides` matches `/docs/guides`
|
|
55
55
|
* and `/docs/guides/...` but NOT `/docs/guideship`.
|
|
56
56
|
*
|
|
57
|
-
* Exported (not just module-private) so `nav-overflow-script.
|
|
58
|
-
* verbatim via `.toString()` alongside `computeActiveNavPath` —
|
|
59
|
-
* closes over this one, so a bare `computeActiveNavPath.toString()`
|
|
60
|
-
* would reference an undefined `pathMatchesNavPath` in the browser (see
|
|
61
|
-
* caution note on `computeActiveNavPath` below). Kept self-contained (no
|
|
62
|
-
* outer references) for the same reason.
|
|
57
|
+
* Exported (not just module-private) so `scripts/gen-nav-overflow-script.mjs`
|
|
58
|
+
* can embed it verbatim via `.toString()` alongside `computeActiveNavPath` —
|
|
59
|
+
* that function closes over this one, so a bare `computeActiveNavPath.toString()`
|
|
60
|
+
* embed would reference an undefined `pathMatchesNavPath` in the browser (see
|
|
61
|
+
* the caution note on `computeActiveNavPath` below). Kept self-contained (no
|
|
62
|
+
* outer references) for the same reason. The generator freezes both into the
|
|
63
|
+
* committed `nav-overflow-generated-script.ts` literal at package build time
|
|
64
|
+
* (zudolab/zudo-doc#3534) — this is no longer a live per-module-eval embed.
|
|
63
65
|
*/
|
|
64
66
|
export function pathMatchesNavPath(currentPath: string, navPath: string): boolean {
|
|
65
67
|
if (currentPath === navPath) return true;
|
|
@@ -69,8 +71,9 @@ export function pathMatchesNavPath(currentPath: string, navPath: string): boolea
|
|
|
69
71
|
|
|
70
72
|
/**
|
|
71
73
|
* CAUTION (zudolab/zudo-doc#3398): this closes over `pathMatchesNavPath`
|
|
72
|
-
* above. `nav-overflow-script.
|
|
73
|
-
* one alone) so the
|
|
74
|
+
* above. `scripts/gen-nav-overflow-script.mjs` embeds both via `.toString()`
|
|
75
|
+
* (never this one alone) so the frozen browser script (`./nav-overflow-generated-script.ts`,
|
|
76
|
+
* zudolab/zudo-doc#3534) is self-contained.
|
|
74
77
|
*/
|
|
75
78
|
export function computeActiveNavPath(
|
|
76
79
|
navItems: readonly NavItemLike[],
|
|
@@ -13,11 +13,15 @@
|
|
|
13
13
|
//
|
|
14
14
|
// Tokens are ARRAYS, not whole strings, because the two consumers need
|
|
15
15
|
// different shapes: `header.tsx` joins them into `class={}` attributes, while
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
16
|
+
// `scripts/gen-nav-overflow-script.mjs` (zudolab/zudo-doc#3534 — the assembly
|
|
17
|
+
// that used to live in `nav-overflow-script.ts` at module-eval time, now
|
|
18
|
+
// frozen into the committed `nav-overflow-generated-script.ts` literal at
|
|
19
|
+
// package build time) reads these arrays off this module's exports and both
|
|
20
|
+
// spreads them into `classList.add/remove(...)` calls and joins them into
|
|
21
|
+
// `className` assignments. Keeping them composable per-part also lets the
|
|
22
|
+
// overflow-menu recipes (which intentionally differ from the header-bar
|
|
23
|
+
// items in padding/weight/base) reuse the shared color tokens instead of
|
|
24
|
+
// re-typing them.
|
|
21
25
|
|
|
22
26
|
// ── Header-bar top-level item: active-state color toggle ─────────────────
|
|
23
27
|
// setTopActive() (script) ↔ the top-level <a> in renderNavItem() (SSR).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// GENERATED FILE — do not edit by hand.
|
|
2
|
+
// Produced by scripts/gen-nav-overflow-script.mjs (zudolab/zudo-doc#3534,
|
|
3
|
+
// epic #3533) from src/current-path/index.ts (CURRENT_PATH_SCRIPT_PRELUDE),
|
|
4
|
+
// src/header/nav-active.ts (pathMatchesNavPath/computeActiveNavPath,
|
|
5
|
+
// type-stripped), src/header/nav-class-tokens.ts (the twelve class-token
|
|
6
|
+
// arrays), and src/transitions/page-events.ts (AFTER_NAVIGATE_EVENT).
|
|
7
|
+
// Re-run `pnpm --filter @takazudo/zudo-doc gen:nav-overflow-script` (or any
|
|
8
|
+
// build/dev entry point, which already runs it) to regenerate after editing
|
|
9
|
+
// any of those source files.
|
|
10
|
+
//
|
|
11
|
+
// This file is committed to git (mirrors search-widget-script/generated-script.ts,
|
|
12
|
+
// zudolab/zudo-doc#3421 / #3431) — a deliberate departure from this repo's
|
|
13
|
+
// usual gitignored-generated-file convention (routes-src/, virtual-modules.d.ts).
|
|
14
|
+
// Regenerate AND commit the result after editing any of the four source files.
|
|
15
|
+
|
|
16
|
+
/** Returns the frozen desktop-nav overflow controller IIFE script. NOTE: the
|
|
17
|
+
* vitest drift guard imports buildNavOverflowScript from
|
|
18
|
+
* scripts/gen-nav-overflow-script.mjs (a fresh re-generation) — NEVER from
|
|
19
|
+
* this module: comparing NAV_OVERFLOW_SCRIPT below against this same file's
|
|
20
|
+
* function would be a vacuous self-comparison. */
|
|
21
|
+
export function buildNavOverflowScript(): string {
|
|
22
|
+
return "(function () {\n var cleanupNavOverflow = null;\n\n function trimSlashes(p) {\n while (p.length > 1 && p.charAt(p.length - 1) === \"/\") p = p.slice(0, -1);\n return p || \"/\";\n }\n\n function navPathname(a) {\n try { return trimSlashes(new URL(a.href, location.href).pathname); }\n catch (e) { return \"\"; }\n }\n\n // Explicit current-route override, embedded from current-path/index.ts so\n // this script cannot drift from the three other read sites\n // (zudolab/zudo-doc#3398, #3408).\n var CURRENT_PATH_DATASET_KEY=\"zdCurrentPath\";var readCurrentPath=function readCurrentPath(datasetKey, explicit) {\n const override = typeof document === \"undefined\" ? void 0 : document.documentElement.dataset[datasetKey];\n return explicit || override || (typeof window === \"undefined\" ? void 0 : window.location.pathname);\n};\n\n // Shared matching core (zudolab/zudo-doc#3398): embedded verbatim from\n // nav-active.ts so this script's longest-match walk cannot drift from the\n // SSR header's own computeActiveNavPath call (header.tsx). computeActiveNavPath\n // closes over pathMatchesNavPath, so both are embedded together.\n var pathMatchesNavPath = function pathMatchesNavPath(currentPath, navPath) {\n if (currentPath === navPath) return true;\n const prefix = navPath.endsWith(\"/\") ? navPath : `${navPath}/`;\n return currentPath.startsWith(prefix);\n};\n var computeActiveNavPath = function computeActiveNavPath(navItems, pathForMatchValue) {\n const allNavPaths = navItems.flatMap((item) => {\n const paths = [item.path];\n if (item.children) {\n paths.push(...item.children.map((child) => child.path));\n }\n return paths;\n });\n return allNavPaths.filter((p) => pathMatchesNavPath(pathForMatchValue, p)).sort((a, b) => b.length - a.length)[0];\n};\n\n // Recompute which header nav item is \"active\" from the CURRENT URL and\n // repaint the highlight. SSR sets the active item on first paint, but the\n // header is persisted across same-locale client-router swaps\n // (data-zfb-transition-persist), so without this the highlight would stay\n // frozen on the page where the header was first rendered. Mirrors the\n // sidebar island's client-side approach (match the current path against\n // each entry's href) and the SSR longest-match + dropdown-parent rules.\n // URL-based: hrefs and the current path both carry the base + locale\n // prefix, so they compare directly without stripping.\n function applyActiveNav() {\n var nav = document.querySelector(\"[data-header-nav]\");\n if (!nav) return;\n var topItems = Array.from(nav.querySelectorAll(\":scope > [data-nav-item]\"));\n if (topItems.length === 0) return;\n\n var cur = trimSlashes(readCurrentPath(CURRENT_PATH_DATASET_KEY));\n\n // Build NavItemLike-shaped entries from the live DOM so the shared\n // computeActiveNavPath can do the deepest-match walk — the same call\n // shape the SSR header uses (matches computeActiveNavPath). A dropdown\n // missing its own top-level anchor is skipped entirely (path \"\" would\n // otherwise match every current path — pathMatchesNavPath treats \"\" as\n // the root \"/\"), mirroring the parentLink guard used below for the same\n // malformed-markup case.\n var navItems = [];\n topItems.forEach(function (it) {\n var isDropdown = it.hasAttribute(\"data-nav-item-dropdown\");\n var topA = isDropdown ? it.querySelector(\":scope > a\") : it;\n if (!topA) return;\n var children = [];\n if (isDropdown) {\n it.querySelectorAll(\":scope > div a\").forEach(function (c) {\n children.push({ path: navPathname(c) });\n });\n }\n navItems.push({ path: navPathname(topA), children: children });\n });\n\n var activePath = computeActiveNavPath(navItems, cur) || \"\";\n\n function setTopActive(a, active) {\n if (!a) return;\n if (active) {\n a.classList.add(\"bg-fg\", \"text-bg\");\n a.classList.remove(\"text-muted\", \"hover:text-accent\", \"hover:underline\", \"focus:underline\", \"focus:text-accent\");\n a.setAttribute(\"aria-current\", \"page\");\n } else {\n a.classList.remove(\"bg-fg\", \"text-bg\");\n a.classList.add(\"text-muted\", \"hover:text-accent\", \"hover:underline\", \"focus:underline\", \"focus:text-accent\");\n a.removeAttribute(\"aria-current\");\n }\n }\n\n topItems.forEach(function (it) {\n var isDropdown = it.hasAttribute(\"data-nav-item-dropdown\");\n var topA = isDropdown ? it.querySelector(\":scope > a\") : it;\n var topActive = false;\n\n if (isDropdown) {\n var parentMatch = !!topA && navPathname(topA) === activePath && activePath !== \"\";\n var anyChild = false;\n it.querySelectorAll(\":scope > div a\").forEach(function (c) {\n var childActive = navPathname(c) === activePath && activePath !== \"\";\n if (childActive) {\n anyChild = true;\n c.setAttribute(\"data-active\", \"\");\n c.classList.add(\"font-bold\", \"text-accent\");\n c.classList.remove(\"text-fg\", \"hover:text-accent\", \"focus-visible:text-accent\");\n } else {\n c.removeAttribute(\"data-active\");\n c.classList.remove(\"font-bold\", \"text-accent\");\n c.classList.add(\"text-fg\", \"hover:text-accent\", \"focus-visible:text-accent\");\n }\n });\n topActive = parentMatch || anyChild;\n var svg = topA ? topA.querySelector(\"svg\") : null;\n if (svg) {\n if (topActive) { svg.classList.add(\"text-bg\"); svg.classList.remove(\"text-muted\"); }\n else { svg.classList.add(\"text-muted\"); svg.classList.remove(\"text-bg\"); }\n }\n } else {\n topActive = activePath !== \"\" && navPathname(topA) === activePath;\n }\n\n setTopActive(topA, topActive);\n });\n }\n\n function initNavOverflow() {\n if (cleanupNavOverflow) cleanupNavOverflow();\n\n // Repaint the active highlight for the current URL before measuring /\n // cloning, so the overflow \"···\" menu mirrors the correct active state.\n applyActiveNav();\n\n var nav = document.querySelector(\"[data-header-nav]\");\n var moreContainer = document.querySelector(\"[data-nav-more]\");\n var moreMenu = document.querySelector(\"[data-nav-more-menu]\");\n var moreToggle = document.querySelector(\"[data-nav-more-toggle]\");\n if (!nav || !moreContainer || !moreMenu || !moreToggle) return;\n\n var items = Array.from(nav.querySelectorAll(\":scope > [data-nav-item]\"));\n if (items.length === 0) return;\n\n var controller = new AbortController();\n\n function update() {\n items.forEach(function (el) { el.style.display = \"\"; });\n moreContainer.style.display = \"\";\n moreMenu.innerHTML = \"\";\n moreMenu.classList.add(\"hidden\");\n moreToggle.setAttribute(\"aria-expanded\", \"false\");\n\n var itemWidths = items.map(function (el) { return el.offsetWidth; });\n var moreWidth = moreContainer.offsetWidth;\n var navGap = parseFloat(getComputedStyle(nav).columnGap) || 0;\n var available = nav.clientWidth;\n\n if (available <= 0) {\n moreContainer.style.display = \"none\";\n return;\n }\n\n var total = 0;\n for (var i = 0; i < itemWidths.length; i++) {\n total += itemWidths[i] + (i > 0 ? navGap : 0);\n }\n\n if (total <= available) {\n moreContainer.style.display = \"none\";\n return;\n }\n\n var used = 0;\n var cutoffIndex = 0;\n\n for (var i2 = 0; i2 < items.length; i2++) {\n var w = itemWidths[i2] + (i2 > 0 ? navGap : 0);\n if (used + w > available - moreWidth - navGap) break;\n used += w;\n cutoffIndex = i2 + 1;\n }\n\n for (var i3 = cutoffIndex; i3 < items.length; i3++) {\n items[i3].style.display = \"none\";\n }\n\n for (var i4 = cutoffIndex; i4 < items.length; i4++) {\n var el = items[i4];\n var isDropdown = el.hasAttribute(\"data-nav-item-dropdown\");\n\n if (isDropdown) {\n var parentLink = el.querySelector(\":scope > a\");\n var childLinks = el.querySelectorAll(\":scope > div a\");\n if (parentLink) {\n var li = document.createElement(\"li\");\n var a = document.createElement(\"a\");\n a.href = parentLink.href;\n var parentText = parentLink.textContent ? parentLink.textContent.trim().replace(/\\s+/g, \" \") : \"\";\n a.textContent = parentText;\n a.className = \"block px-hsp-md py-vsp-2xs text-small font-bold hover:bg-accent/10 hover:underline focus-visible:underline focus-visible:text-accent text-fg hover:text-accent\";\n if (parentLink.getAttribute(\"aria-current\") === \"page\") {\n a.className += \" text-accent\";\n }\n li.appendChild(a);\n moreMenu.appendChild(li);\n }\n childLinks.forEach(function (child) {\n var li = document.createElement(\"li\");\n var a = document.createElement(\"a\");\n a.href = child.href;\n a.textContent = child.textContent ? child.textContent.trim() : \"\";\n var isChildActive = child.hasAttribute(\"data-active\");\n a.className = isChildActive\n ? \"block pl-hsp-xl pr-hsp-md py-vsp-2xs text-small font-bold text-accent hover:bg-accent/10 hover:underline focus-visible:underline\"\n : \"block pl-hsp-xl pr-hsp-md py-vsp-2xs text-small text-fg hover:bg-accent/10 hover:text-accent hover:underline focus-visible:underline focus-visible:text-accent\";\n li.appendChild(a);\n moreMenu.appendChild(li);\n });\n } else {\n var anchor = el;\n var li2 = document.createElement(\"li\");\n var a2 = document.createElement(\"a\");\n a2.href = anchor.href;\n a2.textContent = anchor.textContent ? anchor.textContent.trim() : \"\";\n a2.className = \"block px-hsp-md py-vsp-2xs text-small hover:bg-accent/10 hover:underline focus-visible:underline focus-visible:text-accent text-fg hover:text-accent\";\n if (anchor.getAttribute(\"aria-current\") === \"page\") {\n a2.className += \" font-bold text-accent\";\n }\n li2.appendChild(a2);\n moreMenu.appendChild(li2);\n }\n }\n }\n\n moreToggle.addEventListener(\"click\", function () {\n var isOpen = !moreMenu.classList.contains(\"hidden\");\n moreMenu.classList.toggle(\"hidden\", isOpen);\n moreToggle.setAttribute(\"aria-expanded\", String(!isOpen));\n }, { signal: controller.signal });\n\n document.addEventListener(\"click\", function (e) {\n if (!moreContainer.contains(e.target)) {\n moreMenu.classList.add(\"hidden\");\n moreToggle.setAttribute(\"aria-expanded\", \"false\");\n }\n }, { signal: controller.signal });\n\n document.addEventListener(\"keydown\", function (e) {\n if (e.key !== \"Escape\") return;\n if (!moreMenu.classList.contains(\"hidden\")) {\n moreMenu.classList.add(\"hidden\");\n moreToggle.setAttribute(\"aria-expanded\", \"false\");\n moreToggle.focus();\n return;\n }\n var active = document.activeElement;\n var dropdown = active && active.closest ? active.closest(\"[data-nav-item-dropdown]\") : null;\n if (dropdown && active && active.blur) {\n active.blur();\n }\n }, { signal: controller.signal });\n\n var dropdowns = nav.querySelectorAll(\"[data-nav-item-dropdown]\");\n dropdowns.forEach(function (dd) {\n var trigger = dd.querySelector(\":scope > a\");\n if (!trigger) return;\n function setExpanded(v) {\n trigger.setAttribute(\"aria-expanded\", String(v));\n }\n dd.addEventListener(\"mouseenter\", function () { setExpanded(true); }, { signal: controller.signal });\n dd.addEventListener(\"mouseleave\", function () { setExpanded(false); }, { signal: controller.signal });\n dd.addEventListener(\"focusin\", function () { setExpanded(true); }, { signal: controller.signal });\n dd.addEventListener(\"focusout\", function (e) {\n if (!dd.contains(e.relatedTarget)) {\n setExpanded(false);\n }\n }, { signal: controller.signal });\n });\n\n var ro = new ResizeObserver(update);\n ro.observe(nav);\n controller.signal.addEventListener(\"abort\", function () { ro.disconnect(); });\n\n document.fonts.ready.then(update);\n\n update();\n\n cleanupNavOverflow = function () { controller.abort(); };\n }\n\n initNavOverflow();\n document.addEventListener(\"zfb:after-swap\", initNavOverflow);\n})();";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Client-side script string for the desktop header nav overflow controller.
|
|
26
|
+
* See the module header of this generator for the embedding contract; see
|
|
27
|
+
* current-path/index.ts / header/nav-active.ts / header/nav-class-tokens.ts /
|
|
28
|
+
* transitions/page-events.ts for the frozen sources. */
|
|
29
|
+
export const NAV_OVERFLOW_SCRIPT: string = buildNavOverflowScript();
|
|
@@ -29,320 +29,29 @@
|
|
|
29
29
|
// Kept as a separate module (rather than inlined in `header.tsx`) so
|
|
30
30
|
// the JSX file stays focused on markup and so future edits to the
|
|
31
31
|
// script can be reviewed in isolation.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
// -> `"bg-fg", "text-bg"` — argument list for a classList.add/remove(...) call.
|
|
59
|
-
const clsArgs = (tokens: readonly string[]): string =>
|
|
60
|
-
tokens.map((token) => JSON.stringify(token)).join(", ");
|
|
61
|
-
|
|
62
|
-
// -> `"bg-fg text-bg"` — a single class-string literal for `className = ...`.
|
|
63
|
-
const clsLiteral = (tokens: readonly string[]): string =>
|
|
64
|
-
JSON.stringify(tokens.join(" "));
|
|
65
|
-
|
|
66
|
-
// -> `" font-bold text-accent"` — leading-space append for `className += ...`.
|
|
67
|
-
const clsAppend = (tokens: readonly string[]): string =>
|
|
68
|
-
JSON.stringify(" " + tokens.join(" "));
|
|
69
|
-
|
|
70
|
-
export const NAV_OVERFLOW_SCRIPT = `(function () {
|
|
71
|
-
var cleanupNavOverflow = null;
|
|
72
|
-
|
|
73
|
-
function trimSlashes(p) {
|
|
74
|
-
while (p.length > 1 && p.charAt(p.length - 1) === "/") p = p.slice(0, -1);
|
|
75
|
-
return p || "/";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function navPathname(a) {
|
|
79
|
-
try { return trimSlashes(new URL(a.href, location.href).pathname); }
|
|
80
|
-
catch (e) { return ""; }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Explicit current-route override, embedded from current-path/index.ts so
|
|
84
|
-
// this script cannot drift from the three other read sites
|
|
85
|
-
// (zudolab/zudo-doc#3398, #3408).
|
|
86
|
-
${CURRENT_PATH_SCRIPT_PRELUDE}
|
|
87
|
-
|
|
88
|
-
// Shared matching core (zudolab/zudo-doc#3398): embedded verbatim from
|
|
89
|
-
// nav-active.ts so this script's longest-match walk cannot drift from the
|
|
90
|
-
// SSR header's own computeActiveNavPath call (header.tsx). computeActiveNavPath
|
|
91
|
-
// closes over pathMatchesNavPath, so both are embedded together.
|
|
92
|
-
var pathMatchesNavPath = ${pathMatchesNavPath.toString()};
|
|
93
|
-
var computeActiveNavPath = ${computeActiveNavPath.toString()};
|
|
94
|
-
|
|
95
|
-
// Recompute which header nav item is "active" from the CURRENT URL and
|
|
96
|
-
// repaint the highlight. SSR sets the active item on first paint, but the
|
|
97
|
-
// header is persisted across same-locale client-router swaps
|
|
98
|
-
// (data-zfb-transition-persist), so without this the highlight would stay
|
|
99
|
-
// frozen on the page where the header was first rendered. Mirrors the
|
|
100
|
-
// sidebar island's client-side approach (match the current path against
|
|
101
|
-
// each entry's href) and the SSR longest-match + dropdown-parent rules.
|
|
102
|
-
// URL-based: hrefs and the current path both carry the base + locale
|
|
103
|
-
// prefix, so they compare directly without stripping.
|
|
104
|
-
function applyActiveNav() {
|
|
105
|
-
var nav = document.querySelector("[data-header-nav]");
|
|
106
|
-
if (!nav) return;
|
|
107
|
-
var topItems = Array.from(nav.querySelectorAll(":scope > [data-nav-item]"));
|
|
108
|
-
if (topItems.length === 0) return;
|
|
109
|
-
|
|
110
|
-
var cur = trimSlashes(readCurrentPath(CURRENT_PATH_DATASET_KEY));
|
|
111
|
-
|
|
112
|
-
// Build NavItemLike-shaped entries from the live DOM so the shared
|
|
113
|
-
// computeActiveNavPath can do the deepest-match walk — the same call
|
|
114
|
-
// shape the SSR header uses (matches computeActiveNavPath). A dropdown
|
|
115
|
-
// missing its own top-level anchor is skipped entirely (path "" would
|
|
116
|
-
// otherwise match every current path — pathMatchesNavPath treats "" as
|
|
117
|
-
// the root "/"), mirroring the parentLink guard used below for the same
|
|
118
|
-
// malformed-markup case.
|
|
119
|
-
var navItems = [];
|
|
120
|
-
topItems.forEach(function (it) {
|
|
121
|
-
var isDropdown = it.hasAttribute("data-nav-item-dropdown");
|
|
122
|
-
var topA = isDropdown ? it.querySelector(":scope > a") : it;
|
|
123
|
-
if (!topA) return;
|
|
124
|
-
var children = [];
|
|
125
|
-
if (isDropdown) {
|
|
126
|
-
it.querySelectorAll(":scope > div a").forEach(function (c) {
|
|
127
|
-
children.push({ path: navPathname(c) });
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
navItems.push({ path: navPathname(topA), children: children });
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
var activePath = computeActiveNavPath(navItems, cur) || "";
|
|
134
|
-
|
|
135
|
-
function setTopActive(a, active) {
|
|
136
|
-
if (!a) return;
|
|
137
|
-
if (active) {
|
|
138
|
-
a.classList.add(${clsArgs(NAV_TOP_ACTIVE)});
|
|
139
|
-
a.classList.remove(${clsArgs(NAV_TOP_INACTIVE)});
|
|
140
|
-
a.setAttribute("aria-current", "page");
|
|
141
|
-
} else {
|
|
142
|
-
a.classList.remove(${clsArgs(NAV_TOP_ACTIVE)});
|
|
143
|
-
a.classList.add(${clsArgs(NAV_TOP_INACTIVE)});
|
|
144
|
-
a.removeAttribute("aria-current");
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
topItems.forEach(function (it) {
|
|
149
|
-
var isDropdown = it.hasAttribute("data-nav-item-dropdown");
|
|
150
|
-
var topA = isDropdown ? it.querySelector(":scope > a") : it;
|
|
151
|
-
var topActive = false;
|
|
152
|
-
|
|
153
|
-
if (isDropdown) {
|
|
154
|
-
var parentMatch = !!topA && navPathname(topA) === activePath && activePath !== "";
|
|
155
|
-
var anyChild = false;
|
|
156
|
-
it.querySelectorAll(":scope > div a").forEach(function (c) {
|
|
157
|
-
var childActive = navPathname(c) === activePath && activePath !== "";
|
|
158
|
-
if (childActive) {
|
|
159
|
-
anyChild = true;
|
|
160
|
-
c.setAttribute("data-active", "");
|
|
161
|
-
c.classList.add(${clsArgs(NAV_CHILD_ACTIVE)});
|
|
162
|
-
c.classList.remove(${clsArgs(NAV_CHILD_INACTIVE)});
|
|
163
|
-
} else {
|
|
164
|
-
c.removeAttribute("data-active");
|
|
165
|
-
c.classList.remove(${clsArgs(NAV_CHILD_ACTIVE)});
|
|
166
|
-
c.classList.add(${clsArgs(NAV_CHILD_INACTIVE)});
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
topActive = parentMatch || anyChild;
|
|
170
|
-
var svg = topA ? topA.querySelector("svg") : null;
|
|
171
|
-
if (svg) {
|
|
172
|
-
if (topActive) { svg.classList.add(${clsArgs(NAV_CHEVRON_ACTIVE)}); svg.classList.remove(${clsArgs(NAV_CHEVRON_INACTIVE)}); }
|
|
173
|
-
else { svg.classList.add(${clsArgs(NAV_CHEVRON_INACTIVE)}); svg.classList.remove(${clsArgs(NAV_CHEVRON_ACTIVE)}); }
|
|
174
|
-
}
|
|
175
|
-
} else {
|
|
176
|
-
topActive = activePath !== "" && navPathname(topA) === activePath;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
setTopActive(topA, topActive);
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function initNavOverflow() {
|
|
184
|
-
if (cleanupNavOverflow) cleanupNavOverflow();
|
|
185
|
-
|
|
186
|
-
// Repaint the active highlight for the current URL before measuring /
|
|
187
|
-
// cloning, so the overflow "···" menu mirrors the correct active state.
|
|
188
|
-
applyActiveNav();
|
|
189
|
-
|
|
190
|
-
var nav = document.querySelector("[data-header-nav]");
|
|
191
|
-
var moreContainer = document.querySelector("[data-nav-more]");
|
|
192
|
-
var moreMenu = document.querySelector("[data-nav-more-menu]");
|
|
193
|
-
var moreToggle = document.querySelector("[data-nav-more-toggle]");
|
|
194
|
-
if (!nav || !moreContainer || !moreMenu || !moreToggle) return;
|
|
195
|
-
|
|
196
|
-
var items = Array.from(nav.querySelectorAll(":scope > [data-nav-item]"));
|
|
197
|
-
if (items.length === 0) return;
|
|
198
|
-
|
|
199
|
-
var controller = new AbortController();
|
|
200
|
-
|
|
201
|
-
function update() {
|
|
202
|
-
items.forEach(function (el) { el.style.display = ""; });
|
|
203
|
-
moreContainer.style.display = "";
|
|
204
|
-
moreMenu.innerHTML = "";
|
|
205
|
-
moreMenu.classList.add("hidden");
|
|
206
|
-
moreToggle.setAttribute("aria-expanded", "false");
|
|
207
|
-
|
|
208
|
-
var itemWidths = items.map(function (el) { return el.offsetWidth; });
|
|
209
|
-
var moreWidth = moreContainer.offsetWidth;
|
|
210
|
-
var navGap = parseFloat(getComputedStyle(nav).columnGap) || 0;
|
|
211
|
-
var available = nav.clientWidth;
|
|
212
|
-
|
|
213
|
-
if (available <= 0) {
|
|
214
|
-
moreContainer.style.display = "none";
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
var total = 0;
|
|
219
|
-
for (var i = 0; i < itemWidths.length; i++) {
|
|
220
|
-
total += itemWidths[i] + (i > 0 ? navGap : 0);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (total <= available) {
|
|
224
|
-
moreContainer.style.display = "none";
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
var used = 0;
|
|
229
|
-
var cutoffIndex = 0;
|
|
230
|
-
|
|
231
|
-
for (var i2 = 0; i2 < items.length; i2++) {
|
|
232
|
-
var w = itemWidths[i2] + (i2 > 0 ? navGap : 0);
|
|
233
|
-
if (used + w > available - moreWidth - navGap) break;
|
|
234
|
-
used += w;
|
|
235
|
-
cutoffIndex = i2 + 1;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
for (var i3 = cutoffIndex; i3 < items.length; i3++) {
|
|
239
|
-
items[i3].style.display = "none";
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
for (var i4 = cutoffIndex; i4 < items.length; i4++) {
|
|
243
|
-
var el = items[i4];
|
|
244
|
-
var isDropdown = el.hasAttribute("data-nav-item-dropdown");
|
|
245
|
-
|
|
246
|
-
if (isDropdown) {
|
|
247
|
-
var parentLink = el.querySelector(":scope > a");
|
|
248
|
-
var childLinks = el.querySelectorAll(":scope > div a");
|
|
249
|
-
if (parentLink) {
|
|
250
|
-
var li = document.createElement("li");
|
|
251
|
-
var a = document.createElement("a");
|
|
252
|
-
a.href = parentLink.href;
|
|
253
|
-
var parentText = parentLink.textContent ? parentLink.textContent.trim().replace(/\\s+/g, " ") : "";
|
|
254
|
-
a.textContent = parentText;
|
|
255
|
-
a.className = ${clsLiteral(NAV_MENU_PARENT)};
|
|
256
|
-
if (parentLink.getAttribute("aria-current") === "page") {
|
|
257
|
-
a.className += ${clsAppend(NAV_MENU_PARENT_ACTIVE_SUFFIX)};
|
|
258
|
-
}
|
|
259
|
-
li.appendChild(a);
|
|
260
|
-
moreMenu.appendChild(li);
|
|
261
|
-
}
|
|
262
|
-
childLinks.forEach(function (child) {
|
|
263
|
-
var li = document.createElement("li");
|
|
264
|
-
var a = document.createElement("a");
|
|
265
|
-
a.href = child.href;
|
|
266
|
-
a.textContent = child.textContent ? child.textContent.trim() : "";
|
|
267
|
-
var isChildActive = child.hasAttribute("data-active");
|
|
268
|
-
a.className = isChildActive
|
|
269
|
-
? ${clsLiteral(NAV_MENU_CHILD_ACTIVE)}
|
|
270
|
-
: ${clsLiteral(NAV_MENU_CHILD_INACTIVE)};
|
|
271
|
-
li.appendChild(a);
|
|
272
|
-
moreMenu.appendChild(li);
|
|
273
|
-
});
|
|
274
|
-
} else {
|
|
275
|
-
var anchor = el;
|
|
276
|
-
var li2 = document.createElement("li");
|
|
277
|
-
var a2 = document.createElement("a");
|
|
278
|
-
a2.href = anchor.href;
|
|
279
|
-
a2.textContent = anchor.textContent ? anchor.textContent.trim() : "";
|
|
280
|
-
a2.className = ${clsLiteral(NAV_MENU_PLAIN)};
|
|
281
|
-
if (anchor.getAttribute("aria-current") === "page") {
|
|
282
|
-
a2.className += ${clsAppend(NAV_MENU_PLAIN_ACTIVE_SUFFIX)};
|
|
283
|
-
}
|
|
284
|
-
li2.appendChild(a2);
|
|
285
|
-
moreMenu.appendChild(li2);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
moreToggle.addEventListener("click", function () {
|
|
291
|
-
var isOpen = !moreMenu.classList.contains("hidden");
|
|
292
|
-
moreMenu.classList.toggle("hidden", isOpen);
|
|
293
|
-
moreToggle.setAttribute("aria-expanded", String(!isOpen));
|
|
294
|
-
}, { signal: controller.signal });
|
|
295
|
-
|
|
296
|
-
document.addEventListener("click", function (e) {
|
|
297
|
-
if (!moreContainer.contains(e.target)) {
|
|
298
|
-
moreMenu.classList.add("hidden");
|
|
299
|
-
moreToggle.setAttribute("aria-expanded", "false");
|
|
300
|
-
}
|
|
301
|
-
}, { signal: controller.signal });
|
|
302
|
-
|
|
303
|
-
document.addEventListener("keydown", function (e) {
|
|
304
|
-
if (e.key !== "Escape") return;
|
|
305
|
-
if (!moreMenu.classList.contains("hidden")) {
|
|
306
|
-
moreMenu.classList.add("hidden");
|
|
307
|
-
moreToggle.setAttribute("aria-expanded", "false");
|
|
308
|
-
moreToggle.focus();
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
var active = document.activeElement;
|
|
312
|
-
var dropdown = active && active.closest ? active.closest("[data-nav-item-dropdown]") : null;
|
|
313
|
-
if (dropdown && active && active.blur) {
|
|
314
|
-
active.blur();
|
|
315
|
-
}
|
|
316
|
-
}, { signal: controller.signal });
|
|
317
|
-
|
|
318
|
-
var dropdowns = nav.querySelectorAll("[data-nav-item-dropdown]");
|
|
319
|
-
dropdowns.forEach(function (dd) {
|
|
320
|
-
var trigger = dd.querySelector(":scope > a");
|
|
321
|
-
if (!trigger) return;
|
|
322
|
-
function setExpanded(v) {
|
|
323
|
-
trigger.setAttribute("aria-expanded", String(v));
|
|
324
|
-
}
|
|
325
|
-
dd.addEventListener("mouseenter", function () { setExpanded(true); }, { signal: controller.signal });
|
|
326
|
-
dd.addEventListener("mouseleave", function () { setExpanded(false); }, { signal: controller.signal });
|
|
327
|
-
dd.addEventListener("focusin", function () { setExpanded(true); }, { signal: controller.signal });
|
|
328
|
-
dd.addEventListener("focusout", function (e) {
|
|
329
|
-
if (!dd.contains(e.relatedTarget)) {
|
|
330
|
-
setExpanded(false);
|
|
331
|
-
}
|
|
332
|
-
}, { signal: controller.signal });
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
var ro = new ResizeObserver(update);
|
|
336
|
-
ro.observe(nav);
|
|
337
|
-
controller.signal.addEventListener("abort", function () { ro.disconnect(); });
|
|
338
|
-
|
|
339
|
-
document.fonts.ready.then(update);
|
|
340
|
-
|
|
341
|
-
update();
|
|
342
|
-
|
|
343
|
-
cleanupNavOverflow = function () { controller.abort(); };
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
initNavOverflow();
|
|
347
|
-
document.addEventListener(${JSON.stringify(AFTER_NAVIGATE_EVENT)}, initNavOverflow);
|
|
348
|
-
})();`;
|
|
32
|
+
//
|
|
33
|
+
// FROZEN (zudolab/zudo-doc#3534, epic #3533): the script body used to be
|
|
34
|
+
// assembled here, at module-eval time, from three `Function.prototype.toString()`
|
|
35
|
+
// embeddings (`CURRENT_PATH_SCRIPT_PRELUDE`, `pathMatchesNavPath`,
|
|
36
|
+
// `computeActiveNavPath`) plus splice formatters over the twelve
|
|
37
|
+
// `nav-class-tokens.ts` arrays. That made the emitted bytes depend on the
|
|
38
|
+
// CONSUMING bundler (zudolab/zudo-doc#3502), so no CSP hash could be pinned
|
|
39
|
+
// against a stable value. The assembly logic now lives in
|
|
40
|
+
// `scripts/gen-nav-overflow-script.mjs`, which freezes it ONCE at package
|
|
41
|
+
// build time into the committed `./nav-overflow-generated-script.ts` literal
|
|
42
|
+
// re-exported below. Regenerate via
|
|
43
|
+
// `pnpm --filter @takazudo/zudo-doc gen:nav-overflow-script` after editing
|
|
44
|
+
// any of the four source files it reads from (see the generator's header
|
|
45
|
+
// comment for the list); the vitest guard at
|
|
46
|
+
// `src/header/__tests__/nav-overflow-script.test.ts` proves the committed
|
|
47
|
+
// literal still matches a fresh regeneration.
|
|
48
|
+
//
|
|
49
|
+
// EJECTED COPIES (`zudo-doc eject header`): the generator is NOT shipped, so
|
|
50
|
+
// in an ejected tree the re-exported literal is permanently frozen — editing
|
|
51
|
+
// the ejected `./nav-class-tokens.ts` or `./nav-active.ts` changes the SSR
|
|
52
|
+
// markup (header.tsx imports them live) but NOT this client script, breaking
|
|
53
|
+
// the SSR ↔ runtime class lockstep those files exist to guarantee. To change
|
|
54
|
+
// the client script in an ejected copy, edit the literal in
|
|
55
|
+
// `./nav-overflow-generated-script.ts` directly (it is plain JS in a string)
|
|
56
|
+
// and keep it in step with your token edits by hand.
|
|
57
|
+
export { NAV_OVERFLOW_SCRIPT } from "./nav-overflow-generated-script.js";
|
|
@@ -7,10 +7,22 @@
|
|
|
7
7
|
import { useState, useEffect } from "preact/hooks";
|
|
8
8
|
// After zudolab/zudo-doc#1335 the host components pull lifecycle event names
|
|
9
9
|
// from the v2 transitions module rather than hard-coding `astro:*` literals.
|
|
10
|
-
|
|
10
|
+
// `ensureNestedIslandPropsRefresh` is imported through the barrel (not the
|
|
11
|
+
// deep `./nested-island-props-refresh.js` path) on purpose: eject rewrites
|
|
12
|
+
// EVERY `../transitions/<anything>.js` import to the single specifier
|
|
13
|
+
// `@takazudo/zudo-doc/transitions`, so two distinct relative imports would
|
|
14
|
+
// collapse into duplicate import statements in an ejected copy.
|
|
15
|
+
import { AFTER_NAVIGATE_EVENT, ensureNestedIslandPropsRefresh } from "../transitions/index.js";
|
|
11
16
|
import { SidebarTree } from "../sidebar-tree-island/index.js";
|
|
12
17
|
import type { SidebarNavNode, SidebarRootMenuItem, SidebarLocaleLink } from "../sidebar/types.js";
|
|
13
18
|
|
|
19
|
+
// This island lives INSIDE the persisted `<header>`, so a same-locale swap
|
|
20
|
+
// lifts it verbatim and would re-mount it from the previous page's serialized
|
|
21
|
+
// props (zudolab/zudo-doc#3525). The refresh has to outlive the island's own
|
|
22
|
+
// mount/unmount cycle across a swap, so it is installed at document lifetime
|
|
23
|
+
// here rather than from an effect. SSR evaluation is a safe no-op.
|
|
24
|
+
ensureNestedIslandPropsRefresh();
|
|
25
|
+
|
|
14
26
|
const cx = (...classes: Array<string | false | null | undefined>) =>
|
|
15
27
|
classes.filter(Boolean).join(" ");
|
|
16
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takazudo/zudo-doc",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.7.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",
|
|
@@ -672,16 +672,19 @@
|
|
|
672
672
|
"typescript": "^5.0.0",
|
|
673
673
|
"vitest": "^4.1.0",
|
|
674
674
|
"zod": "^4.3.6",
|
|
675
|
-
"@takazudo/zudo-doc-history-server": "5.
|
|
675
|
+
"@takazudo/zudo-doc-history-server": "5.7.0"
|
|
676
676
|
},
|
|
677
677
|
"scripts": {
|
|
678
678
|
"gen:search-widget-script": "node scripts/gen-search-widget-script.mjs",
|
|
679
|
-
"
|
|
680
|
-
"
|
|
679
|
+
"gen:nav-overflow-script": "node scripts/gen-nav-overflow-script.mjs",
|
|
680
|
+
"build": "node scripts/gen-search-widget-script.mjs && node scripts/gen-nav-overflow-script.mjs && tsup && tsc -p tsconfig.build.json",
|
|
681
|
+
"predev": "node ../../scripts/ensure-workspace-build.mjs && node scripts/gen-search-widget-script.mjs && node scripts/gen-nav-overflow-script.mjs",
|
|
681
682
|
"// dev": "Do NOT add `--continue-on-error` here. It stops a fatal watcher exit from cascading, but the dead watcher then fails silently — a dead dev:dts leaves dist/*.d.ts frozen while the JS keeps updating, which typechecks cleanly against stale types instead of failing loudly like #3113's absent declarations did. See the #3129 section in CLAUDE.md.",
|
|
682
683
|
"dev": "run-p dev:js dev:dts",
|
|
683
684
|
"dev:js": "tsup --watch",
|
|
684
685
|
"dev:dts": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
686
|
+
"// check:prepack-contract": "check:search-widget-drift and check:nav-overflow-drift run here (not just b4push/CI) because `prepare` regenerates BOTH frozen literals unconditionally — without a prepack gate, packing from a tree where a source changed but a literal was never re-committed would silently publish new script bytes and break every consumer's pinned CSP hash (#3534/#3535 for nav-overflow; #3540 extended the same gate to search-widget, the exact failure class the freezes exist to prevent).",
|
|
687
|
+
"check:prepack-contract": "pnpm --dir ../.. check:search-widget-drift && pnpm --dir ../.. check:nav-overflow-drift && pnpm --dir ../.. gen:changelog && node scripts/check-theme-css.mjs && node scripts/check-safelist.mjs && node scripts/check-content-css.mjs && node scripts/check-page-loading-css.mjs && node scripts/check-features-css.mjs && node scripts/check-theme-packs.mjs && node scripts/check-catalog.mjs && node scripts/check-site-schema.mjs && node scripts/check-plugins.mjs && node scripts/check-plugin-resolution.mjs && node scripts/check-eject-sources.mjs && node scripts/check-routes-src.mjs && node scripts/check-shim-artifacts.mjs && node scripts/check-virtual-modules.mjs && node bin/gen-component-tokens.mjs --check",
|
|
685
688
|
"test:plugin-resolution": "node scripts/check-plugin-resolution.mjs",
|
|
686
689
|
"test": "vitest run --config vitest.config.ts",
|
|
687
690
|
"test:slow": "vitest run --config vitest.slow.config.ts",
|