@takazudo/zudo-doc 0.2.22 → 1.0.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 (103) hide show
  1. package/bin/zudo-doc.mjs +83 -0
  2. package/dist/eject/index.d.ts +33 -0
  3. package/dist/eject/index.js +229 -0
  4. package/dist/plugins/routes.d.ts +2 -0
  5. package/dist/plugins/routes.js +100 -0
  6. package/dist/preset.d.ts +47 -1
  7. package/dist/preset.js +29 -13
  8. package/dist/routes/404.d.ts +7 -0
  9. package/dist/routes/404.js +47 -0
  10. package/dist/routes/_chrome.d.ts +32 -0
  11. package/dist/routes/_chrome.js +365 -0
  12. package/dist/routes/_context.d.ts +56 -0
  13. package/dist/routes/_context.js +222 -0
  14. package/dist/routes/_docs-helpers.d.ts +43 -0
  15. package/dist/routes/_docs-helpers.js +173 -0
  16. package/dist/routes/_virtual.d.js +0 -0
  17. package/dist/routes/api-ai-chat.d.ts +5 -0
  18. package/dist/routes/api-ai-chat.js +24 -0
  19. package/dist/routes/docs-slug.d.ts +21 -0
  20. package/dist/routes/docs-slug.js +26 -0
  21. package/dist/routes/docs-tags-index.d.ts +7 -0
  22. package/dist/routes/docs-tags-index.js +11 -0
  23. package/dist/routes/docs-tags-tag.d.ts +23 -0
  24. package/dist/routes/docs-tags-tag.js +19 -0
  25. package/dist/routes/docs-versions.d.ts +7 -0
  26. package/dist/routes/docs-versions.js +11 -0
  27. package/dist/routes/index.d.ts +7 -0
  28. package/dist/routes/index.js +117 -0
  29. package/dist/routes/locale-docs-slug.d.ts +27 -0
  30. package/dist/routes/locale-docs-slug.js +40 -0
  31. package/dist/routes/locale-docs-tags-index.d.ts +18 -0
  32. package/dist/routes/locale-docs-tags-index.js +15 -0
  33. package/dist/routes/locale-docs-tags-tag.d.ts +25 -0
  34. package/dist/routes/locale-docs-tags-tag.js +22 -0
  35. package/dist/routes/locale-docs-versions.d.ts +18 -0
  36. package/dist/routes/locale-docs-versions.js +15 -0
  37. package/dist/routes/locale-index.d.ts +24 -0
  38. package/dist/routes/locale-index.js +132 -0
  39. package/dist/routes/robots.txt.d.ts +3 -0
  40. package/dist/routes/robots.txt.js +8 -0
  41. package/dist/routes/sitemap.xml.d.ts +5 -0
  42. package/dist/routes/sitemap.xml.js +31 -0
  43. package/dist/routes/v-docs-slug.d.ts +27 -0
  44. package/dist/routes/v-docs-slug.js +34 -0
  45. package/dist/routes/v-locale-docs-slug.d.ts +31 -0
  46. package/dist/routes/v-locale-docs-slug.js +50 -0
  47. package/dist/safelist.css +1 -1
  48. package/dist/settings.d.ts +15 -0
  49. package/eject/breadcrumb/breadcrumb.tsx +194 -0
  50. package/eject/breadcrumb/find-path.ts +26 -0
  51. package/eject/breadcrumb/index.ts +4 -0
  52. package/eject/breadcrumb/types.ts +32 -0
  53. package/eject/code-group/index.tsx +78 -0
  54. package/eject/content-admonition/index.tsx +50 -0
  55. package/eject/details/details.tsx +34 -0
  56. package/eject/details/index.ts +4 -0
  57. package/eject/doc-pager/index.tsx +91 -0
  58. package/eject/footer/footer.tsx +167 -0
  59. package/eject/footer/index.ts +19 -0
  60. package/eject/footer/types.ts +60 -0
  61. package/eject/header/header.tsx +725 -0
  62. package/eject/header/index.ts +36 -0
  63. package/eject/header/nav-active.ts +122 -0
  64. package/eject/header/nav-overflow-script.ts +302 -0
  65. package/eject/header/right-items.ts +78 -0
  66. package/eject/header/types.ts +69 -0
  67. package/eject/page-loading/index.ts +10 -0
  68. package/eject/page-loading/page-loading-overlay.tsx +103 -0
  69. package/eject/sidebar/index.ts +27 -0
  70. package/eject/sidebar/sidebar.tsx +137 -0
  71. package/eject/sidebar/types.ts +73 -0
  72. package/eject/tab-item/index.ts +7 -0
  73. package/eject/tab-item/tab-item.tsx +70 -0
  74. package/eject/theme-toggle/color-scheme-sync.ts +73 -0
  75. package/eject/theme-toggle/index.tsx +120 -0
  76. package/eject/toc/cx.ts +38 -0
  77. package/eject/toc/index.ts +11 -0
  78. package/eject/toc/mobile-toc.tsx +133 -0
  79. package/eject/toc/toc-title.ts +33 -0
  80. package/eject/toc/toc.tsx +119 -0
  81. package/eject/toc/types.ts +14 -0
  82. package/eject/toc/use-active-heading.ts +162 -0
  83. package/package.json +87 -7
  84. package/routes-src/404.tsx +49 -0
  85. package/routes-src/_chrome.tsx +470 -0
  86. package/routes-src/_context.ts +314 -0
  87. package/routes-src/_docs-helpers.ts +284 -0
  88. package/routes-src/_virtual.d.ts +15 -0
  89. package/routes-src/api-ai-chat.tsx +46 -0
  90. package/routes-src/docs-slug.tsx +43 -0
  91. package/routes-src/docs-tags-index.tsx +14 -0
  92. package/routes-src/docs-tags-tag.tsx +29 -0
  93. package/routes-src/docs-versions.tsx +15 -0
  94. package/routes-src/index.tsx +134 -0
  95. package/routes-src/locale-docs-slug.tsx +69 -0
  96. package/routes-src/locale-docs-tags-index.tsx +23 -0
  97. package/routes-src/locale-docs-tags-tag.tsx +39 -0
  98. package/routes-src/locale-docs-versions.tsx +23 -0
  99. package/routes-src/locale-index.tsx +150 -0
  100. package/routes-src/robots.txt.tsx +13 -0
  101. package/routes-src/sitemap.xml.tsx +48 -0
  102. package/routes-src/v-docs-slug.tsx +64 -0
  103. package/routes-src/v-locale-docs-slug.tsx +85 -0
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Public entry for the JSX `<Header />` port.
3
+ *
4
+ * Consumers import from `@takazudo/zudo-doc/header`:
5
+ *
6
+ * import { Header } from "@takazudo/zudo-doc/header";
7
+ *
8
+ * The pure helpers used to compute active-link state are also exposed
9
+ * so downstream tests / custom layouts can reuse them without rebuilding
10
+ * the `pathForMatch` / `computeActiveNavPath` regex logic.
11
+ */
12
+
13
+ export type {
14
+ Locale,
15
+ HeaderNavChildItem,
16
+ HeaderNavItem,
17
+ HeaderRightComponentItem,
18
+ HeaderRightComponentName,
19
+ HeaderRightHtmlItem,
20
+ HeaderRightItem,
21
+ HeaderRightLinkItem,
22
+ HeaderRightTriggerItem,
23
+ HeaderRightTriggerName,
24
+ } from "./types.js";
25
+ export {
26
+ filterHeaderRightItems,
27
+ type HeaderRightItemFlags,
28
+ } from "./right-items.js";
29
+ export { Header, type HeaderProps } from "./header.js";
30
+ export {
31
+ computeActiveNavPath,
32
+ isNavItemActive,
33
+ pathForMatch,
34
+ type NavItemLike,
35
+ } from "./nav-active.js";
36
+ export { NAV_OVERFLOW_SCRIPT } from "./nav-overflow-script.js";
@@ -0,0 +1,122 @@
1
+ // Pure, settings-free helpers backing the JSX <Header /> port.
2
+ //
3
+ // These exist as their own module so unit tests can exercise the active-
4
+ // path resolution logic without booting the host project's @/config or
5
+ // @/utils modules. The Header component imports from here too so the
6
+ // runtime and the tests share a single source of truth.
7
+
8
+ /** Minimal shape the active-path resolver needs from a header nav item. */
9
+ export interface NavItemLike {
10
+ path: string;
11
+ /**
12
+ * The "big category" this nav entry claims (mirrors the host's
13
+ * `getNavSectionForSlug` / sidebar `categoryMatch`). When the host knows
14
+ * which category the current page belongs to, category matching is
15
+ * preferred over URL-path heuristics — see `isNavItemActiveByCategory`.
16
+ */
17
+ categoryMatch?: string;
18
+ children?: { path: string; categoryMatch?: string }[];
19
+ }
20
+
21
+ /**
22
+ * Strip the locale prefix from `pathWithoutBase` when the page is being
23
+ * served under a non-default locale. Mirrors the inline regex inside
24
+ * `header` so the JSX port matches the legacy template's
25
+ * active-link behaviour.
26
+ *
27
+ * Examples:
28
+ * pathForMatch("/ja/docs/intro", "ja", "en") === "/docs/intro"
29
+ * pathForMatch("/docs/intro", "en", "en") === "/docs/intro"
30
+ * pathForMatch(null, "ja", "en") === ""
31
+ */
32
+ export function pathForMatch(
33
+ pathWithoutBase: string,
34
+ lang: string | undefined,
35
+ defaultLocale: string,
36
+ ): string {
37
+ if (lang == null || lang === defaultLocale) return pathWithoutBase;
38
+ // Segment boundary required: "/ja-tutorial/..." must not lose its "/ja".
39
+ return pathWithoutBase.replace(new RegExp(`^/${lang}(?=/|$)`), "");
40
+ }
41
+
42
+ /**
43
+ * Pick the deepest header-nav path that the current page lives under.
44
+ *
45
+ * The legacy template flattens parent + child paths, filters by
46
+ * `pathForMatch.startsWith(p)`, and picks the longest match — this keeps
47
+ * a child link active even when its parent's path is also a prefix of
48
+ * the current page.
49
+ *
50
+ * Returns `undefined` when no nav entry matches; callers should treat
51
+ * that as "no active link."
52
+ */
53
+ /**
54
+ * Segment-aware prefix test: nav path `/docs/guides` matches `/docs/guides`
55
+ * and `/docs/guides/...` but NOT `/docs/guideship`.
56
+ */
57
+ function pathMatchesNavPath(currentPath: string, navPath: string): boolean {
58
+ if (currentPath === navPath) return true;
59
+ const prefix = navPath.endsWith("/") ? navPath : `${navPath}/`;
60
+ return currentPath.startsWith(prefix);
61
+ }
62
+
63
+ export function computeActiveNavPath(
64
+ navItems: readonly NavItemLike[],
65
+ pathForMatchValue: string,
66
+ ): string | undefined {
67
+ const allNavPaths = navItems.flatMap((item) => {
68
+ const paths = [item.path];
69
+ if (item.children) {
70
+ paths.push(...item.children.map((child) => child.path));
71
+ }
72
+ return paths;
73
+ });
74
+ return allNavPaths
75
+ .filter((p) => pathMatchesNavPath(pathForMatchValue, p))
76
+ .sort((a, b) => b.length - a.length)[0];
77
+ }
78
+
79
+ /**
80
+ * Active-state predicate for a top-level nav item: the item itself, or
81
+ * any of its children, matches the active path. Mirrors the inline
82
+ * `isNavItemActive` helper in the legacy template.
83
+ */
84
+ export function isNavItemActive(
85
+ item: NavItemLike,
86
+ activeNavPath: string | undefined,
87
+ ): boolean {
88
+ if (activeNavPath === item.path) return true;
89
+ if (item.children?.some((child) => activeNavPath === child.path)) return true;
90
+ return false;
91
+ }
92
+
93
+ /**
94
+ * Category-based active-state predicate. A nav entry is active when its
95
+ * own `categoryMatch` equals the page's resolved big category
96
+ * (`activeCategory`), or when any of its children claim that category.
97
+ *
98
+ * This is the authoritative "is the page under this big category?" check
99
+ * — it mirrors how the sidebar scopes its tree (`getNavSectionForSlug`),
100
+ * so the header highlight stays correct even when a category's page URLs
101
+ * do not share a prefix with the nav item's `path` (the case the older
102
+ * path-only heuristic missed). Callers prefer this over `isNavItemActive`
103
+ * and fall back to path matching when `activeCategory` is absent (e.g.
104
+ * home, 404, tag, and version pages, which carry no nav section).
105
+ *
106
+ * Works for both top-level items and child items: a child has no
107
+ * `children` of its own, so the call collapses to a single
108
+ * `categoryMatch` comparison.
109
+ */
110
+ export function isNavItemActiveByCategory(
111
+ item: NavItemLike,
112
+ activeCategory: string | undefined,
113
+ ): boolean {
114
+ if (activeCategory == null) return false;
115
+ // `activeCategory` is non-null here, so a plain `===` already rejects an
116
+ // entry whose `categoryMatch` is undefined — no extra null guard needed.
117
+ if (item.categoryMatch === activeCategory) return true;
118
+ if (item.children?.some((child) => child.categoryMatch === activeCategory)) {
119
+ return true;
120
+ }
121
+ return false;
122
+ }
@@ -0,0 +1,302 @@
1
+ // Inline-script source for the desktop-nav overflow controller.
2
+ //
3
+ // In the original `header` template the script lived directly
4
+ // inside a `<script>` tag (which Astro pipes through the bundler). For
5
+ // the JSX port we emit the same logic verbatim via
6
+ // `dangerouslySetInnerHTML`, so the value below is plain ECMAScript —
7
+ // any TypeScript-only constructs (generic params, type casts, parameter
8
+ // type annotations) have been dropped so the browser can parse it
9
+ // directly.
10
+ //
11
+ // The behaviour matches the original modulo lifecycle vocabulary:
12
+ // 1. Locate the `[data-header-nav]` flex container, its `[data-nav-more]`
13
+ // overflow trigger, and its dropdown menu.
14
+ // 2. Measure each top-level nav item, then greedily hide items that
15
+ // would overflow and rebuild the "..." dropdown to mirror them
16
+ // (including the bold-parent + indented-children pattern for
17
+ // `[data-nav-item-dropdown]` entries).
18
+ // 3. Wire toggle / outside-click / Escape handlers to the overflow
19
+ // menu and aria-expanded state to the in-place dropdowns.
20
+ // 4. Re-run on the v2 after-navigate event so the overflow stays
21
+ // correct after an SPA body-swap navigation. The event name is
22
+ // pulled from `AFTER_NAVIGATE_EVENT` in
23
+ // `transitions/page-events.ts` (today: `zfb:after-swap`) rather
24
+ // than a hard-coded `astro:*` literal — see that module for the
25
+ // vocabulary rationale. The first-paint init relies on the
26
+ // top-level `initNavOverflow()` call further down (after-swap
27
+ // does NOT fire on the initial page load).
28
+ //
29
+ // Kept as a separate module (rather than inlined in `header.tsx`) so
30
+ // the JSX file stays focused on markup and so future edits to the
31
+ // script can be reviewed in isolation.
32
+
33
+ import { AFTER_NAVIGATE_EVENT } from "../transitions/page-events.js";
34
+
35
+ export const NAV_OVERFLOW_SCRIPT = `(function () {
36
+ var cleanupNavOverflow = null;
37
+
38
+ function trimSlashes(p) {
39
+ while (p.length > 1 && p.charAt(p.length - 1) === "/") p = p.slice(0, -1);
40
+ return p || "/";
41
+ }
42
+
43
+ function navPathname(a) {
44
+ try { return trimSlashes(new URL(a.href, location.href).pathname); }
45
+ catch (e) { return ""; }
46
+ }
47
+
48
+ function isUnderPath(cur, p) {
49
+ if (!p) return false;
50
+ if (cur === p) return true;
51
+ return p !== "/" && cur.indexOf(p + "/") === 0;
52
+ }
53
+
54
+ // Recompute which header nav item is "active" from the CURRENT URL and
55
+ // repaint the highlight. SSR sets the active item on first paint, but the
56
+ // header is persisted across same-locale client-router swaps
57
+ // (data-zfb-transition-persist), so without this the highlight would stay
58
+ // frozen on the page where the header was first rendered. Mirrors the
59
+ // sidebar island's client-side approach (match location.pathname against
60
+ // each entry's href) and the SSR longest-match + dropdown-parent rules.
61
+ // URL-based: hrefs and location.pathname both carry the base + locale
62
+ // prefix, so they compare directly without stripping.
63
+ function applyActiveNav() {
64
+ var nav = document.querySelector("[data-header-nav]");
65
+ if (!nav) return;
66
+ var topItems = Array.from(nav.querySelectorAll(":scope > [data-nav-item]"));
67
+ if (topItems.length === 0) return;
68
+
69
+ var cur = trimSlashes(location.pathname);
70
+
71
+ // Deepest (longest) nav path the current URL lives under, across both
72
+ // top-level and dropdown-child paths — matches computeActiveNavPath.
73
+ var activePath = "";
74
+ topItems.forEach(function (it) {
75
+ var isDropdown = it.hasAttribute("data-nav-item-dropdown");
76
+ var topA = isDropdown ? it.querySelector(":scope > a") : it;
77
+ if (topA) {
78
+ var tp = navPathname(topA);
79
+ if (isUnderPath(cur, tp) && tp.length > activePath.length) activePath = tp;
80
+ }
81
+ if (isDropdown) {
82
+ it.querySelectorAll(":scope > div a").forEach(function (c) {
83
+ var cp = navPathname(c);
84
+ if (isUnderPath(cur, cp) && cp.length > activePath.length) activePath = cp;
85
+ });
86
+ }
87
+ });
88
+
89
+ function setTopActive(a, active) {
90
+ if (!a) return;
91
+ if (active) {
92
+ a.classList.add("bg-fg", "text-bg");
93
+ a.classList.remove("text-muted", "hover:underline", "focus:underline");
94
+ a.setAttribute("aria-current", "page");
95
+ } else {
96
+ a.classList.remove("bg-fg", "text-bg");
97
+ a.classList.add("text-muted", "hover:underline", "focus:underline");
98
+ a.removeAttribute("aria-current");
99
+ }
100
+ }
101
+
102
+ topItems.forEach(function (it) {
103
+ var isDropdown = it.hasAttribute("data-nav-item-dropdown");
104
+ var topA = isDropdown ? it.querySelector(":scope > a") : it;
105
+ var topActive = false;
106
+
107
+ if (isDropdown) {
108
+ var parentMatch = !!topA && navPathname(topA) === activePath && activePath !== "";
109
+ var anyChild = false;
110
+ it.querySelectorAll(":scope > div a").forEach(function (c) {
111
+ var childActive = navPathname(c) === activePath && activePath !== "";
112
+ if (childActive) {
113
+ anyChild = true;
114
+ c.setAttribute("data-active", "");
115
+ c.classList.add("font-bold", "text-accent");
116
+ c.classList.remove("text-fg");
117
+ } else {
118
+ c.removeAttribute("data-active");
119
+ c.classList.remove("font-bold", "text-accent");
120
+ c.classList.add("text-fg");
121
+ }
122
+ });
123
+ topActive = parentMatch || anyChild;
124
+ var svg = topA ? topA.querySelector("svg") : null;
125
+ if (svg) {
126
+ if (topActive) { svg.classList.add("text-bg"); svg.classList.remove("text-muted"); }
127
+ else { svg.classList.add("text-muted"); svg.classList.remove("text-bg"); }
128
+ }
129
+ } else {
130
+ topActive = activePath !== "" && navPathname(topA) === activePath;
131
+ }
132
+
133
+ setTopActive(topA, topActive);
134
+ });
135
+ }
136
+
137
+ function initNavOverflow() {
138
+ if (cleanupNavOverflow) cleanupNavOverflow();
139
+
140
+ // Repaint the active highlight for the current URL before measuring /
141
+ // cloning, so the overflow "···" menu mirrors the correct active state.
142
+ applyActiveNav();
143
+
144
+ var nav = document.querySelector("[data-header-nav]");
145
+ var moreContainer = document.querySelector("[data-nav-more]");
146
+ var moreMenu = document.querySelector("[data-nav-more-menu]");
147
+ var moreToggle = document.querySelector("[data-nav-more-toggle]");
148
+ if (!nav || !moreContainer || !moreMenu || !moreToggle) return;
149
+
150
+ var items = Array.from(nav.querySelectorAll(":scope > [data-nav-item]"));
151
+ if (items.length === 0) return;
152
+
153
+ var controller = new AbortController();
154
+
155
+ function update() {
156
+ items.forEach(function (el) { el.style.display = ""; });
157
+ moreContainer.style.display = "";
158
+ moreMenu.innerHTML = "";
159
+ moreMenu.classList.add("hidden");
160
+ moreToggle.setAttribute("aria-expanded", "false");
161
+
162
+ var itemWidths = items.map(function (el) { return el.offsetWidth; });
163
+ var moreWidth = moreContainer.offsetWidth;
164
+ var navGap = parseFloat(getComputedStyle(nav).columnGap) || 0;
165
+ var available = nav.clientWidth;
166
+
167
+ if (available <= 0) {
168
+ moreContainer.style.display = "none";
169
+ return;
170
+ }
171
+
172
+ var total = 0;
173
+ for (var i = 0; i < itemWidths.length; i++) {
174
+ total += itemWidths[i] + (i > 0 ? navGap : 0);
175
+ }
176
+
177
+ if (total <= available) {
178
+ moreContainer.style.display = "none";
179
+ return;
180
+ }
181
+
182
+ var used = 0;
183
+ var cutoffIndex = 0;
184
+
185
+ for (var i2 = 0; i2 < items.length; i2++) {
186
+ var w = itemWidths[i2] + (i2 > 0 ? navGap : 0);
187
+ if (used + w > available - moreWidth - navGap) break;
188
+ used += w;
189
+ cutoffIndex = i2 + 1;
190
+ }
191
+
192
+ for (var i3 = cutoffIndex; i3 < items.length; i3++) {
193
+ items[i3].style.display = "none";
194
+ }
195
+
196
+ for (var i4 = cutoffIndex; i4 < items.length; i4++) {
197
+ var el = items[i4];
198
+ var isDropdown = el.hasAttribute("data-nav-item-dropdown");
199
+
200
+ if (isDropdown) {
201
+ var parentLink = el.querySelector(":scope > a");
202
+ var childLinks = el.querySelectorAll(":scope > div a");
203
+ if (parentLink) {
204
+ var li = document.createElement("li");
205
+ var a = document.createElement("a");
206
+ a.href = parentLink.href;
207
+ var parentText = parentLink.textContent ? parentLink.textContent.trim().replace(/\\s+/g, " ") : "";
208
+ a.textContent = parentText;
209
+ a.className = "block px-hsp-md py-vsp-2xs text-small font-bold hover:bg-accent/10 hover:underline focus-visible:underline text-fg";
210
+ if (parentLink.getAttribute("aria-current") === "page") {
211
+ a.className += " text-accent";
212
+ }
213
+ li.appendChild(a);
214
+ moreMenu.appendChild(li);
215
+ }
216
+ childLinks.forEach(function (child) {
217
+ var li = document.createElement("li");
218
+ var a = document.createElement("a");
219
+ a.href = child.href;
220
+ a.textContent = child.textContent ? child.textContent.trim() : "";
221
+ var isChildActive = child.hasAttribute("data-active");
222
+ a.className = isChildActive
223
+ ? "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"
224
+ : "block pl-hsp-xl pr-hsp-md py-vsp-2xs text-small text-muted hover:bg-accent/10 hover:text-fg hover:underline focus-visible:underline";
225
+ li.appendChild(a);
226
+ moreMenu.appendChild(li);
227
+ });
228
+ } else {
229
+ var anchor = el;
230
+ var li2 = document.createElement("li");
231
+ var a2 = document.createElement("a");
232
+ a2.href = anchor.href;
233
+ a2.textContent = anchor.textContent ? anchor.textContent.trim() : "";
234
+ a2.className = "block px-hsp-md py-vsp-2xs text-small hover:bg-accent/10 hover:underline focus-visible:underline text-fg";
235
+ if (anchor.getAttribute("aria-current") === "page") {
236
+ a2.className += " font-bold text-accent";
237
+ }
238
+ li2.appendChild(a2);
239
+ moreMenu.appendChild(li2);
240
+ }
241
+ }
242
+ }
243
+
244
+ moreToggle.addEventListener("click", function () {
245
+ var isOpen = !moreMenu.classList.contains("hidden");
246
+ moreMenu.classList.toggle("hidden", isOpen);
247
+ moreToggle.setAttribute("aria-expanded", String(!isOpen));
248
+ }, { signal: controller.signal });
249
+
250
+ document.addEventListener("click", function (e) {
251
+ if (!moreContainer.contains(e.target)) {
252
+ moreMenu.classList.add("hidden");
253
+ moreToggle.setAttribute("aria-expanded", "false");
254
+ }
255
+ }, { signal: controller.signal });
256
+
257
+ document.addEventListener("keydown", function (e) {
258
+ if (e.key !== "Escape") return;
259
+ if (!moreMenu.classList.contains("hidden")) {
260
+ moreMenu.classList.add("hidden");
261
+ moreToggle.setAttribute("aria-expanded", "false");
262
+ moreToggle.focus();
263
+ return;
264
+ }
265
+ var active = document.activeElement;
266
+ var dropdown = active && active.closest ? active.closest("[data-nav-item-dropdown]") : null;
267
+ if (dropdown && active && active.blur) {
268
+ active.blur();
269
+ }
270
+ }, { signal: controller.signal });
271
+
272
+ var dropdowns = nav.querySelectorAll("[data-nav-item-dropdown]");
273
+ dropdowns.forEach(function (dd) {
274
+ var trigger = dd.querySelector(":scope > a");
275
+ if (!trigger) return;
276
+ function setExpanded(v) {
277
+ trigger.setAttribute("aria-expanded", String(v));
278
+ }
279
+ dd.addEventListener("mouseenter", function () { setExpanded(true); }, { signal: controller.signal });
280
+ dd.addEventListener("mouseleave", function () { setExpanded(false); }, { signal: controller.signal });
281
+ dd.addEventListener("focusin", function () { setExpanded(true); }, { signal: controller.signal });
282
+ dd.addEventListener("focusout", function (e) {
283
+ if (!dd.contains(e.relatedTarget)) {
284
+ setExpanded(false);
285
+ }
286
+ }, { signal: controller.signal });
287
+ });
288
+
289
+ var ro = new ResizeObserver(update);
290
+ ro.observe(nav);
291
+ controller.signal.addEventListener("abort", function () { ro.disconnect(); });
292
+
293
+ document.fonts.ready.then(update);
294
+
295
+ update();
296
+
297
+ cleanupNavOverflow = function () { controller.abort(); };
298
+ }
299
+
300
+ initNavOverflow();
301
+ document.addEventListener(${JSON.stringify(AFTER_NAVIGATE_EVENT)}, initNavOverflow);
302
+ })();`;
@@ -0,0 +1,78 @@
1
+ // `filterHeaderRightItems` was moved here from the host's
2
+ // `src/utils/header-right-items.ts` (super-epic #1724, sub-issue #1729).
3
+ //
4
+ // Why it moved: keeping the filter inside v2 lets the package own the full
5
+ // header-rendering contract — `<Header>` only renders items the caller has
6
+ // approved, and the approval rule is colocated with the renderer. The
7
+ // previous version pulled `settings` straight from the host's `@/config`
8
+ // alias, which is exactly the kind of host-coupling W3A removes.
9
+ //
10
+ // The new shape takes a `flags` object instead. The host computes the
11
+ // flags from its own `settings` / `i18n` modules and passes them in,
12
+ // preserving the original filter logic without dragging any `@/` import
13
+ // across the package boundary.
14
+
15
+ import type { HeaderRightItem } from "./types.js";
16
+
17
+ /**
18
+ * Runtime gates for `filterHeaderRightItems`.
19
+ *
20
+ * Mirrors the predicates in the legacy host-side helper (`src/utils/
21
+ * header-right-items.ts` pre-#1729). Each flag corresponds to a specific
22
+ * `settings.*` switch the original implementation consulted directly:
23
+ *
24
+ * - `designTokenPanel` — `settings.designTokenPanel`
25
+ * - `aiAssistant` — `settings.aiAssistant`
26
+ * - `colorMode` — `Boolean(settings.colorMode)`
27
+ * - `hasLocales` — `Object.keys(settings.locales).length > 0`
28
+ * - `hasVersions` — `Boolean(settings.versions)`
29
+ * - `hasGithubUrl` — `Boolean(settings.githubUrl)`
30
+ */
31
+ export interface HeaderRightItemFlags {
32
+ designTokenPanel: boolean;
33
+ aiAssistant: boolean;
34
+ colorMode: boolean;
35
+ hasLocales: boolean;
36
+ hasVersions: boolean;
37
+ hasGithubUrl: boolean;
38
+ }
39
+
40
+ /**
41
+ * Drop header-right items whose gating flag is off. Pure function — the
42
+ * caller is responsible for computing the flags from host config.
43
+ *
44
+ * Items not gated by any flag (links, raw HTML, version-switcher when
45
+ * versions are configured, search, …) pass through untouched.
46
+ */
47
+ export function filterHeaderRightItems(
48
+ items: HeaderRightItem[],
49
+ flags: HeaderRightItemFlags,
50
+ ): HeaderRightItem[] {
51
+ return items.filter((item) => {
52
+ if (item.type === "trigger") {
53
+ if (item.trigger === "design-token-panel") {
54
+ return flags.designTokenPanel;
55
+ }
56
+ if (item.trigger === "ai-chat") {
57
+ return flags.aiAssistant;
58
+ }
59
+ }
60
+
61
+ if (item.type === "component") {
62
+ if (item.component === "theme-toggle") {
63
+ return flags.colorMode;
64
+ }
65
+ if (item.component === "language-switcher") {
66
+ return flags.hasLocales;
67
+ }
68
+ if (item.component === "version-switcher") {
69
+ return flags.hasVersions;
70
+ }
71
+ if (item.component === "github-link") {
72
+ return flags.hasGithubUrl;
73
+ }
74
+ }
75
+
76
+ return true;
77
+ });
78
+ }
@@ -0,0 +1,69 @@
1
+ // Boundary types for the v2 `<Header />` shell.
2
+ //
3
+ // The host project (`src/config/settings-types.ts`) owns the canonical
4
+ // shapes for these structures. We copy them here so v2 has its own
5
+ // self-contained type surface — consumers import `HeaderNavItem` /
6
+ // `HeaderRightItem` from `@takazudo/zudo-doc/header` and don't reach
7
+ // into the host's `@/config` alias.
8
+ //
9
+ // Structural-only: the v2 copies are intentionally simple and broad. If
10
+ // the host extends a field (e.g. adds a new `HeaderRightTriggerName`),
11
+ // downstream consumers may continue to use the v2 types until the v2
12
+ // package itself widens them. Changes here are v2 breaking changes.
13
+
14
+ /**
15
+ * Locale code as seen by the header. Widened from the host's literal
16
+ * union (`"en" | "ja" | …`) to plain `string` at the v2 boundary so the
17
+ * package can render for any project's locale set without a generic
18
+ * parameter (super-epic #1724, sub-issue #1729).
19
+ */
20
+ export type Locale = string;
21
+
22
+ export interface HeaderNavChildItem {
23
+ label: string;
24
+ labelKey?: string;
25
+ path: string;
26
+ categoryMatch?: string;
27
+ }
28
+
29
+ export interface HeaderNavItem extends HeaderNavChildItem {
30
+ children?: HeaderNavChildItem[];
31
+ }
32
+
33
+ export type HeaderRightComponentName =
34
+ | "theme-toggle"
35
+ | "language-switcher"
36
+ | "version-switcher"
37
+ | "github-link"
38
+ | "search";
39
+
40
+ export type HeaderRightTriggerName = "design-token-panel" | "ai-chat";
41
+
42
+ export interface HeaderRightComponentItem {
43
+ type: "component";
44
+ component: HeaderRightComponentName;
45
+ }
46
+
47
+ export interface HeaderRightTriggerItem {
48
+ type: "trigger";
49
+ trigger: HeaderRightTriggerName;
50
+ }
51
+
52
+ export interface HeaderRightLinkItem {
53
+ type: "link";
54
+ href: string;
55
+ label?: string;
56
+ ariaLabel?: string;
57
+ icon?: "github";
58
+ }
59
+
60
+ export interface HeaderRightHtmlItem {
61
+ type: "html";
62
+ html: string;
63
+ }
64
+
65
+ export type HeaderRightItem =
66
+ | HeaderRightComponentItem
67
+ | HeaderRightTriggerItem
68
+ | HeaderRightLinkItem
69
+ | HeaderRightHtmlItem;
@@ -0,0 +1,10 @@
1
+ // E5 framework primitives — page-loading overlay.
2
+ //
3
+ // Subpath: `@takazudo/zudo-doc/page-loading`.
4
+
5
+ export {
6
+ default as PageLoadingOverlay,
7
+ PAGE_LOADING_OVERLAY_ID,
8
+ buildPageLoadingOverlayBootstrap,
9
+ type PageLoadingOverlayProps,
10
+ } from "./page-loading-overlay.js";