@dogsbay/docs-layout 0.2.0-beta.45 → 0.2.0-beta.47
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/package.json +3 -3
- package/src/DocsLayout.astro +33 -1
- package/src/DocsNavClient.astro +89 -0
- package/src/docs-nav-client.ts +265 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/docs-layout",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.47",
|
|
4
4
|
"description": "Standard documentation layout components for Dogsbay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"./json-ld": "./src/json-ld.ts"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@dogsbay/ui": "0.2.0-beta.
|
|
33
|
-
"@dogsbay/primitives": "0.2.0-beta.
|
|
32
|
+
"@dogsbay/ui": "0.2.0-beta.47",
|
|
33
|
+
"@dogsbay/primitives": "0.2.0-beta.47"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"vitest": "^3.0.0"
|
package/src/DocsLayout.astro
CHANGED
|
@@ -31,6 +31,7 @@ import SidebarMenuItem from "@dogsbay/ui/sidebar/SidebarMenuItem.astro";
|
|
|
31
31
|
import SidebarMenuButton from "@dogsbay/ui/sidebar/SidebarMenuButton.astro";
|
|
32
32
|
import SidebarSeparator from "@dogsbay/ui/sidebar/SidebarSeparator.astro";
|
|
33
33
|
import SidebarNavTree from "@dogsbay/ui/sidebar/SidebarNavTree.astro";
|
|
34
|
+
import DocsNavClient from "./DocsNavClient.astro";
|
|
34
35
|
import Separator from "@dogsbay/ui/separator/Separator.astro";
|
|
35
36
|
import ThemeToggle from "@dogsbay/ui/theme-toggle/ThemeToggle.astro";
|
|
36
37
|
import DocsToc from "./DocsToc.astro";
|
|
@@ -356,6 +357,19 @@ interface Props {
|
|
|
356
357
|
* (`<basePath>/<version>/`). Defaults to "/docs".
|
|
357
358
|
*/
|
|
358
359
|
basePath?: string;
|
|
360
|
+
/**
|
|
361
|
+
* Sidebar navigation render mode.
|
|
362
|
+
*
|
|
363
|
+
* - `"client"` (default): emit the small `<DocsNavClient />` placeholder;
|
|
364
|
+
* the tree is hydrated from `/_dogsbay/nav.json` once per session.
|
|
365
|
+
* Page HTML shrinks dramatically at scale (~50 KB vs ~1.2 MB on a
|
|
366
|
+
* 2k-page site). No-JS users see a sitemap fallback link.
|
|
367
|
+
* - `"ssr-full"`: render the full nav tree into every page's HTML.
|
|
368
|
+
* Best for very small sites or strict no-JS / SEO contexts.
|
|
369
|
+
*
|
|
370
|
+
* See plans/client-rendered-nav.md.
|
|
371
|
+
*/
|
|
372
|
+
navMode?: "client" | "ssr-full";
|
|
359
373
|
/**
|
|
360
374
|
* Per-page LLM action UI. When set and `enabled !== false`, renders
|
|
361
375
|
* the PageActions cluster (Copy markdown + Open in Claude/ChatGPT/
|
|
@@ -447,6 +461,7 @@ const {
|
|
|
447
461
|
multiSource,
|
|
448
462
|
switcherMap,
|
|
449
463
|
basePath,
|
|
464
|
+
navMode = "client",
|
|
450
465
|
wideLayout = false,
|
|
451
466
|
class: className,
|
|
452
467
|
} = Astro.props;
|
|
@@ -692,7 +707,24 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
|
|
692
707
|
<SidebarSeparator />
|
|
693
708
|
|
|
694
709
|
<SidebarContent>
|
|
695
|
-
{
|
|
710
|
+
{navMode === "client" ? (
|
|
711
|
+
// Client-render mode: emit one DocsNavClient placeholder
|
|
712
|
+
// inside a single SidebarGroup, regardless of navGroups.
|
|
713
|
+
// The group-label shape doesn't apply when the tree is
|
|
714
|
+
// hydrated by JS — multi-group nav is reconstructed
|
|
715
|
+
// client-side from the same /_dogsbay/nav.json shape.
|
|
716
|
+
// See plans/client-rendered-nav.md.
|
|
717
|
+
<SidebarGroup>
|
|
718
|
+
<SidebarGroupContent>
|
|
719
|
+
<DocsNavClient
|
|
720
|
+
currentPath={currentPath}
|
|
721
|
+
basePath={basePath ?? ""}
|
|
722
|
+
version={multiSource?.version}
|
|
723
|
+
locale={multiSource?.locale}
|
|
724
|
+
/>
|
|
725
|
+
</SidebarGroupContent>
|
|
726
|
+
</SidebarGroup>
|
|
727
|
+
) : navGroups ? (
|
|
696
728
|
navGroups.map(group => (
|
|
697
729
|
<SidebarGroup>
|
|
698
730
|
<SidebarGroupLabel>{group.label}</SidebarGroupLabel>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Client-rendered drop-in replacement for SidebarNavTree.
|
|
4
|
+
*
|
|
5
|
+
* Emits an empty placeholder with the metadata the hydration script
|
|
6
|
+
* needs (nav-url to fetch, current-path to highlight, basePath for
|
|
7
|
+
* version/locale filtering). The actual tree DOM is rendered by
|
|
8
|
+
* `docs-nav-client.ts` after a single fetch of `nav.json` (cached
|
|
9
|
+
* once per session — subsequent navigations re-highlight without
|
|
10
|
+
* re-fetching, courtesy of view transitions).
|
|
11
|
+
*
|
|
12
|
+
* The placeholder shows a few skeleton rows so the layout doesn't
|
|
13
|
+
* shift when the real tree pops in. Skeleton uses the same width as
|
|
14
|
+
* the sidebar so visual jump is minimal even on a slow connection.
|
|
15
|
+
*
|
|
16
|
+
* Trade-offs vs SidebarNavTree:
|
|
17
|
+
* - HTML per page: ~200 bytes (this placeholder + a tiny script
|
|
18
|
+
* tag) vs ~600 KB+ for the SSR tree at scale.
|
|
19
|
+
* - No-JS users see only the skeleton + the `<noscript>` fallback
|
|
20
|
+
* link. A `sitemap.xml` link covers no-JS navigation.
|
|
21
|
+
* - First paint waits for the JS bundle + the JSON fetch. On a 4G
|
|
22
|
+
* connection that's typically <200 ms; the skeleton fills the
|
|
23
|
+
* space until then.
|
|
24
|
+
*
|
|
25
|
+
* See plans/client-rendered-nav.md.
|
|
26
|
+
*/
|
|
27
|
+
interface Props {
|
|
28
|
+
/** Current page's URL pathname; the script uses it to mark the active item. */
|
|
29
|
+
currentPath: string;
|
|
30
|
+
/**
|
|
31
|
+
* URL prefix the host serves under (combined `urlBase` + `basePath`).
|
|
32
|
+
* The script joins this with `/_dogsbay/nav.json` to locate the
|
|
33
|
+
* fetchable nav tree; also threaded to the version/locale filter
|
|
34
|
+
* so multi-axis sites work the same as SSR.
|
|
35
|
+
*/
|
|
36
|
+
basePath?: string;
|
|
37
|
+
/** Current source's version axis value, if multi-version site. */
|
|
38
|
+
version?: string;
|
|
39
|
+
/** Current source's locale axis value, if multi-locale site. */
|
|
40
|
+
locale?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { currentPath, basePath = "", version, locale } = Astro.props;
|
|
44
|
+
const navUrl = `${basePath}/_dogsbay/nav.json`;
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
<div
|
|
48
|
+
id="docs-nav-root"
|
|
49
|
+
data-nav-url={navUrl}
|
|
50
|
+
data-current-path={currentPath}
|
|
51
|
+
data-base-path={basePath}
|
|
52
|
+
data-version={version ?? ""}
|
|
53
|
+
data-locale={locale ?? ""}
|
|
54
|
+
aria-busy="true"
|
|
55
|
+
aria-label="Documentation navigation"
|
|
56
|
+
>
|
|
57
|
+
<ul class="flex min-w-0 flex-col" data-sidebar="nav-tree" data-level="0">
|
|
58
|
+
{[0, 1, 2, 3, 4, 5].map((i) => (
|
|
59
|
+
<li>
|
|
60
|
+
<div
|
|
61
|
+
class="flex h-8 w-full items-center gap-2 rounded-md px-2"
|
|
62
|
+
aria-hidden="true"
|
|
63
|
+
>
|
|
64
|
+
<div class="h-2 w-2 rounded-full bg-sidebar-foreground/10" />
|
|
65
|
+
<div
|
|
66
|
+
class="h-2 rounded bg-sidebar-foreground/10"
|
|
67
|
+
style={`width: ${50 + ((i * 17) % 35)}%;`}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
</li>
|
|
71
|
+
))}
|
|
72
|
+
</ul>
|
|
73
|
+
<noscript>
|
|
74
|
+
<p class="px-2 py-1.5 text-sm text-sidebar-foreground/70">
|
|
75
|
+
JavaScript is required to render the sidebar. Use the
|
|
76
|
+
<a href={`${basePath}/sitemap.xml`} class="underline">sitemap</a>
|
|
77
|
+
to browse all pages.
|
|
78
|
+
</p>
|
|
79
|
+
</noscript>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<script>
|
|
83
|
+
// Single import — Astro/Vite bundles this into one shared chunk
|
|
84
|
+
// referenced from every page that uses DocsNavClient. Browsers
|
|
85
|
+
// cache the chunk, so the nav script ships once per session
|
|
86
|
+
// regardless of how many pages the user visits.
|
|
87
|
+
import { hydrateDocsNav } from "./docs-nav-client.ts";
|
|
88
|
+
hydrateDocsNav();
|
|
89
|
+
</script>
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side sidebar nav hydration.
|
|
3
|
+
*
|
|
4
|
+
* Fetches `/_dogsbay/nav.json` once per session, renders the tree DOM
|
|
5
|
+
* to mirror `@dogsbay/ui/sidebar/SidebarNavTree.astro`'s structure,
|
|
6
|
+
* and re-highlights the current page on each Astro view-transition
|
|
7
|
+
* page-load.
|
|
8
|
+
*
|
|
9
|
+
* Design constraints:
|
|
10
|
+
* - DOM output matches SidebarNavTree exactly (same tags, classes,
|
|
11
|
+
* data-attributes) so Tailwind's compiled CSS styles us correctly
|
|
12
|
+
* and any sidebar-system selectors (e.g. `data-sidebar="nav-tree"`
|
|
13
|
+
* hooks) keep working.
|
|
14
|
+
* - Filter by `version` / `locale` matches `nav-filter.ts`'s SSR
|
|
15
|
+
* filter so a multi-axis site renders the same nav as `ssr-full`
|
|
16
|
+
* mode (without the multiplicative HTML cost).
|
|
17
|
+
* - One fetch per session. The `nav.json` URL is served with
|
|
18
|
+
* `Cache-Control: immutable` by Astro's static build (it lives
|
|
19
|
+
* under `public/_dogsbay/`), so the browser cache holds it across
|
|
20
|
+
* navigations.
|
|
21
|
+
*
|
|
22
|
+
* See plans/client-rendered-nav.md.
|
|
23
|
+
*/
|
|
24
|
+
import { filterNavByAxis } from "./nav-filter.js";
|
|
25
|
+
|
|
26
|
+
interface NavItem {
|
|
27
|
+
label: string;
|
|
28
|
+
href?: string;
|
|
29
|
+
icon?: string;
|
|
30
|
+
children?: NavItem[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Module-level cache so multiple page loads in the same SPA session
|
|
35
|
+
* share the same fetched nav data. Cleared by re-loads (full page
|
|
36
|
+
* navigations) but Astro's view transitions re-execute the script
|
|
37
|
+
* module without reloading the page — so on view-transition the
|
|
38
|
+
* cached promise is reused and no extra fetch fires.
|
|
39
|
+
*/
|
|
40
|
+
let navPromise: Promise<NavItem[]> | null = null;
|
|
41
|
+
|
|
42
|
+
function fetchNav(url: string): Promise<NavItem[]> {
|
|
43
|
+
if (!navPromise) {
|
|
44
|
+
navPromise = fetch(url, { credentials: "same-origin" }).then((r) => {
|
|
45
|
+
if (!r.ok) throw new Error(`nav.json fetch failed: ${r.status}`);
|
|
46
|
+
return r.json() as Promise<NavItem[]>;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return navPromise;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalize(path: string): string {
|
|
53
|
+
return path.replace(/\/$/, "") || "/";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasActiveDescendant(item: NavItem, current: string): boolean {
|
|
57
|
+
if (item.href && normalize(item.href) === current) return true;
|
|
58
|
+
return item.children?.some((c) => hasActiveDescendant(c, current)) ?? false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Render a single nav item into a `<li>` DOM node. Matches
|
|
63
|
+
* SidebarNavTree's per-level class set exactly so styling stays in
|
|
64
|
+
* sync. Padding is computed from `level` the same way (`8 + level*12`
|
|
65
|
+
* pixels) so indentation lines up across the same render.
|
|
66
|
+
*/
|
|
67
|
+
function renderItem(item: NavItem, current: string, level: number): HTMLLIElement {
|
|
68
|
+
const li = document.createElement("li");
|
|
69
|
+
li.dataset.sidebar = "nav-tree-item";
|
|
70
|
+
|
|
71
|
+
const active = item.href ? normalize(item.href) === current : false;
|
|
72
|
+
const hasChildren = !!item.children && item.children.length > 0;
|
|
73
|
+
const padLeft = `${8 + level * 12}px`;
|
|
74
|
+
const heightClass = level === 0 ? "h-8" : "h-7";
|
|
75
|
+
|
|
76
|
+
if (hasChildren) {
|
|
77
|
+
const details = document.createElement("details");
|
|
78
|
+
if (active || hasActiveDescendant(item, current)) details.open = true;
|
|
79
|
+
|
|
80
|
+
const summary = document.createElement("summary");
|
|
81
|
+
summary.className = [
|
|
82
|
+
"flex w-full min-w-0 cursor-pointer items-center gap-2 rounded-md text-sm text-sidebar-foreground outline-none [list-style:none] ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&::-webkit-details-marker]:hidden",
|
|
83
|
+
heightClass,
|
|
84
|
+
active ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground" : "",
|
|
85
|
+
]
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.join(" ");
|
|
88
|
+
summary.style.paddingLeft = padLeft;
|
|
89
|
+
if (item.href) summary.dataset.navHref = item.href;
|
|
90
|
+
if (active) summary.dataset.active = "true";
|
|
91
|
+
|
|
92
|
+
// Chevron SVG — matches SidebarNavTree's rotation-on-open via CSS
|
|
93
|
+
// (`details[open] > summary [data-chevron] { transform: rotate(90deg); }`).
|
|
94
|
+
summary.innerHTML =
|
|
95
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0 transition-transform duration-200" data-chevron aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>';
|
|
96
|
+
|
|
97
|
+
if (item.icon) {
|
|
98
|
+
const iconSpan = document.createElement("span");
|
|
99
|
+
iconSpan.className = "shrink-0 [&>svg]:size-4";
|
|
100
|
+
iconSpan.innerHTML = item.icon;
|
|
101
|
+
summary.appendChild(iconSpan);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const label = document.createElement("span");
|
|
105
|
+
label.className = "truncate";
|
|
106
|
+
label.textContent = item.label;
|
|
107
|
+
summary.appendChild(label);
|
|
108
|
+
|
|
109
|
+
details.appendChild(summary);
|
|
110
|
+
|
|
111
|
+
// Recurse — nested `<ul>` mirrors SidebarNavTree's `<Astro.self>`.
|
|
112
|
+
const childUl = document.createElement("ul");
|
|
113
|
+
childUl.className = "flex min-w-0 flex-col";
|
|
114
|
+
childUl.dataset.sidebar = "nav-tree";
|
|
115
|
+
childUl.dataset.level = String(level + 1);
|
|
116
|
+
for (const child of item.children!) {
|
|
117
|
+
childUl.appendChild(renderItem(child, current, level + 1));
|
|
118
|
+
}
|
|
119
|
+
details.appendChild(childUl);
|
|
120
|
+
|
|
121
|
+
li.appendChild(details);
|
|
122
|
+
} else {
|
|
123
|
+
const a = document.createElement("a");
|
|
124
|
+
a.href = item.href || "#";
|
|
125
|
+
a.className = [
|
|
126
|
+
"flex w-full min-w-0 items-center gap-2 rounded-md text-sm text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2",
|
|
127
|
+
heightClass,
|
|
128
|
+
active ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground" : "",
|
|
129
|
+
]
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.join(" ");
|
|
132
|
+
a.style.paddingLeft = padLeft;
|
|
133
|
+
if (active) a.dataset.active = "true";
|
|
134
|
+
if (item.href) a.dataset.navHref = item.href;
|
|
135
|
+
|
|
136
|
+
// Spacer to align leaf text with branch text (which has a chevron).
|
|
137
|
+
const spacer = document.createElement("span");
|
|
138
|
+
spacer.className = "size-4 shrink-0";
|
|
139
|
+
a.appendChild(spacer);
|
|
140
|
+
|
|
141
|
+
if (item.icon) {
|
|
142
|
+
const iconSpan = document.createElement("span");
|
|
143
|
+
iconSpan.className = "shrink-0 [&>svg]:size-4";
|
|
144
|
+
iconSpan.innerHTML = item.icon;
|
|
145
|
+
a.appendChild(iconSpan);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const label = document.createElement("span");
|
|
149
|
+
label.className = "truncate";
|
|
150
|
+
label.textContent = item.label;
|
|
151
|
+
a.appendChild(label);
|
|
152
|
+
|
|
153
|
+
li.appendChild(a);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return li;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function renderTree(items: NavItem[], current: string, root: HTMLElement): void {
|
|
160
|
+
const ul = document.createElement("ul");
|
|
161
|
+
ul.className =
|
|
162
|
+
"flex min-w-0 flex-col w-full group-data-[collapsible=icon]:hidden";
|
|
163
|
+
ul.dataset.sidebar = "nav-tree";
|
|
164
|
+
ul.dataset.level = "0";
|
|
165
|
+
for (const item of items) {
|
|
166
|
+
ul.appendChild(renderItem(item, current, 0));
|
|
167
|
+
}
|
|
168
|
+
// Replace skeleton in one DOM op so there's no flash of partial state.
|
|
169
|
+
root.replaceChildren(ul);
|
|
170
|
+
root.removeAttribute("aria-busy");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Re-highlight the active item without rebuilding the whole tree.
|
|
175
|
+
* Used on `astro:page-load` when view transitions land on a new
|
|
176
|
+
* route — we re-toggle the `data-active` attribute and re-apply the
|
|
177
|
+
* active classes, then expand the new active branch's ancestors.
|
|
178
|
+
*
|
|
179
|
+
* Cheaper than a full re-render (no fetch, no DOM rebuild). For
|
|
180
|
+
* the wider hydration loop we still re-render when a brand-new nav
|
|
181
|
+
* structure is needed (e.g. switching versions), but path-only
|
|
182
|
+
* navigation just re-highlights.
|
|
183
|
+
*/
|
|
184
|
+
function rehighlight(root: HTMLElement, current: string): void {
|
|
185
|
+
const ACTIVE_CLASSES = [
|
|
186
|
+
"bg-sidebar-accent",
|
|
187
|
+
"font-medium",
|
|
188
|
+
"text-sidebar-accent-foreground",
|
|
189
|
+
];
|
|
190
|
+
const items = root.querySelectorAll<HTMLElement>("[data-nav-href]");
|
|
191
|
+
for (const el of Array.from(items)) {
|
|
192
|
+
const href = el.dataset.navHref;
|
|
193
|
+
const active = !!href && normalize(href) === current;
|
|
194
|
+
if (active) {
|
|
195
|
+
el.dataset.active = "true";
|
|
196
|
+
el.classList.add(...ACTIVE_CLASSES);
|
|
197
|
+
} else {
|
|
198
|
+
delete el.dataset.active;
|
|
199
|
+
el.classList.remove(...ACTIVE_CLASSES);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Expand ancestors of the new active item.
|
|
203
|
+
const active = root.querySelector<HTMLElement>('[data-active="true"]');
|
|
204
|
+
if (active) {
|
|
205
|
+
let parent: HTMLElement | null = active.parentElement;
|
|
206
|
+
while (parent) {
|
|
207
|
+
if (parent.tagName === "DETAILS") {
|
|
208
|
+
(parent as HTMLDetailsElement).open = true;
|
|
209
|
+
}
|
|
210
|
+
parent = parent.parentElement;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Public entry point. Called once from `<DocsNavClient />`'s inline
|
|
217
|
+
* script tag. Idempotent — subsequent calls (e.g. from view
|
|
218
|
+
* transitions) are no-ops once the tree has been rendered; they just
|
|
219
|
+
* re-highlight against the new pathname.
|
|
220
|
+
*/
|
|
221
|
+
export async function hydrateDocsNav(): Promise<void> {
|
|
222
|
+
const root = document.getElementById("docs-nav-root");
|
|
223
|
+
if (!root) return;
|
|
224
|
+
|
|
225
|
+
const navUrl = root.dataset.navUrl;
|
|
226
|
+
if (!navUrl) {
|
|
227
|
+
console.warn("docs-nav: missing data-nav-url");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const current = normalize(root.dataset.currentPath || location.pathname);
|
|
231
|
+
const basePath = root.dataset.basePath || "";
|
|
232
|
+
const version = root.dataset.version || undefined;
|
|
233
|
+
const locale = root.dataset.locale || undefined;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const nav = await fetchNav(navUrl);
|
|
237
|
+
const filtered = filterNavByAxis(nav, {
|
|
238
|
+
basePath: basePath || "/docs",
|
|
239
|
+
version: version || undefined,
|
|
240
|
+
locale: locale || undefined,
|
|
241
|
+
});
|
|
242
|
+
renderTree(filtered, current, root);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.error("docs-nav: hydration failed", err);
|
|
245
|
+
root.setAttribute("aria-busy", "false");
|
|
246
|
+
// Keep skeleton so layout doesn't collapse on failure; a real
|
|
247
|
+
// user will reload or follow the noscript link.
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Re-run on view transitions. astro:page-load fires both on initial
|
|
252
|
+
// load and after each ClientRouter transition; the module-level
|
|
253
|
+
// `navPromise` cache makes the post-transition path a fast
|
|
254
|
+
// highlight-only pass without re-fetching.
|
|
255
|
+
document.addEventListener("astro:page-load", () => {
|
|
256
|
+
const root = document.getElementById("docs-nav-root");
|
|
257
|
+
if (!root) return;
|
|
258
|
+
// If tree is already rendered (no aria-busy), just re-highlight.
|
|
259
|
+
if (root.getAttribute("aria-busy") === null) {
|
|
260
|
+
const current = normalize(root.dataset.currentPath || location.pathname);
|
|
261
|
+
rehighlight(root, current);
|
|
262
|
+
} else {
|
|
263
|
+
void hydrateDocsNav();
|
|
264
|
+
}
|
|
265
|
+
});
|