@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.
- package/bin/zudo-doc.mjs +83 -0
- package/dist/eject/index.d.ts +33 -0
- package/dist/eject/index.js +229 -0
- package/dist/plugins/routes.d.ts +2 -0
- package/dist/plugins/routes.js +100 -0
- package/dist/preset.d.ts +47 -1
- package/dist/preset.js +29 -13
- package/dist/routes/404.d.ts +7 -0
- package/dist/routes/404.js +47 -0
- package/dist/routes/_chrome.d.ts +32 -0
- package/dist/routes/_chrome.js +365 -0
- package/dist/routes/_context.d.ts +56 -0
- package/dist/routes/_context.js +222 -0
- package/dist/routes/_docs-helpers.d.ts +43 -0
- package/dist/routes/_docs-helpers.js +173 -0
- package/dist/routes/_virtual.d.js +0 -0
- package/dist/routes/api-ai-chat.d.ts +5 -0
- package/dist/routes/api-ai-chat.js +24 -0
- package/dist/routes/docs-slug.d.ts +21 -0
- package/dist/routes/docs-slug.js +26 -0
- package/dist/routes/docs-tags-index.d.ts +7 -0
- package/dist/routes/docs-tags-index.js +11 -0
- package/dist/routes/docs-tags-tag.d.ts +23 -0
- package/dist/routes/docs-tags-tag.js +19 -0
- package/dist/routes/docs-versions.d.ts +7 -0
- package/dist/routes/docs-versions.js +11 -0
- package/dist/routes/index.d.ts +7 -0
- package/dist/routes/index.js +117 -0
- package/dist/routes/locale-docs-slug.d.ts +27 -0
- package/dist/routes/locale-docs-slug.js +40 -0
- package/dist/routes/locale-docs-tags-index.d.ts +18 -0
- package/dist/routes/locale-docs-tags-index.js +15 -0
- package/dist/routes/locale-docs-tags-tag.d.ts +25 -0
- package/dist/routes/locale-docs-tags-tag.js +22 -0
- package/dist/routes/locale-docs-versions.d.ts +18 -0
- package/dist/routes/locale-docs-versions.js +15 -0
- package/dist/routes/locale-index.d.ts +24 -0
- package/dist/routes/locale-index.js +132 -0
- package/dist/routes/robots.txt.d.ts +3 -0
- package/dist/routes/robots.txt.js +8 -0
- package/dist/routes/sitemap.xml.d.ts +5 -0
- package/dist/routes/sitemap.xml.js +31 -0
- package/dist/routes/v-docs-slug.d.ts +27 -0
- package/dist/routes/v-docs-slug.js +34 -0
- package/dist/routes/v-locale-docs-slug.d.ts +31 -0
- package/dist/routes/v-locale-docs-slug.js +50 -0
- package/dist/safelist.css +1 -1
- package/dist/settings.d.ts +15 -0
- package/eject/breadcrumb/breadcrumb.tsx +194 -0
- package/eject/breadcrumb/find-path.ts +26 -0
- package/eject/breadcrumb/index.ts +4 -0
- package/eject/breadcrumb/types.ts +32 -0
- package/eject/code-group/index.tsx +78 -0
- package/eject/content-admonition/index.tsx +50 -0
- package/eject/details/details.tsx +34 -0
- package/eject/details/index.ts +4 -0
- package/eject/doc-pager/index.tsx +91 -0
- package/eject/footer/footer.tsx +167 -0
- package/eject/footer/index.ts +19 -0
- package/eject/footer/types.ts +60 -0
- package/eject/header/header.tsx +725 -0
- package/eject/header/index.ts +36 -0
- package/eject/header/nav-active.ts +122 -0
- package/eject/header/nav-overflow-script.ts +302 -0
- package/eject/header/right-items.ts +78 -0
- package/eject/header/types.ts +69 -0
- package/eject/page-loading/index.ts +10 -0
- package/eject/page-loading/page-loading-overlay.tsx +103 -0
- package/eject/sidebar/index.ts +27 -0
- package/eject/sidebar/sidebar.tsx +137 -0
- package/eject/sidebar/types.ts +73 -0
- package/eject/tab-item/index.ts +7 -0
- package/eject/tab-item/tab-item.tsx +70 -0
- package/eject/theme-toggle/color-scheme-sync.ts +73 -0
- package/eject/theme-toggle/index.tsx +120 -0
- package/eject/toc/cx.ts +38 -0
- package/eject/toc/index.ts +11 -0
- package/eject/toc/mobile-toc.tsx +133 -0
- package/eject/toc/toc-title.ts +33 -0
- package/eject/toc/toc.tsx +119 -0
- package/eject/toc/types.ts +14 -0
- package/eject/toc/use-active-heading.ts +162 -0
- package/package.json +87 -7
- package/routes-src/404.tsx +49 -0
- package/routes-src/_chrome.tsx +470 -0
- package/routes-src/_context.ts +314 -0
- package/routes-src/_docs-helpers.ts +284 -0
- package/routes-src/_virtual.d.ts +15 -0
- package/routes-src/api-ai-chat.tsx +46 -0
- package/routes-src/docs-slug.tsx +43 -0
- package/routes-src/docs-tags-index.tsx +14 -0
- package/routes-src/docs-tags-tag.tsx +29 -0
- package/routes-src/docs-versions.tsx +15 -0
- package/routes-src/index.tsx +134 -0
- package/routes-src/locale-docs-slug.tsx +69 -0
- package/routes-src/locale-docs-tags-index.tsx +23 -0
- package/routes-src/locale-docs-tags-tag.tsx +39 -0
- package/routes-src/locale-docs-versions.tsx +23 -0
- package/routes-src/locale-index.tsx +150 -0
- package/routes-src/robots.txt.tsx +13 -0
- package/routes-src/sitemap.xml.tsx +48 -0
- package/routes-src/v-docs-slug.tsx +64 -0
- 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";
|