@takazudo/zudo-doc 6.1.1 → 7.0.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 +33 -0
- package/README.md +17 -2
- package/dist/code-syntax/mermaid-init-script.d.ts +2 -2
- package/dist/code-syntax/mermaid-init-script.js +1 -1
- package/dist/compiled.css +37 -10
- package/dist/content.css +3 -0
- package/dist/doclayout/doc-layout.js +2 -0
- package/dist/features.css +31 -56
- package/dist/page-loading/page-loading-overlay.d.ts +2 -2
- package/dist/page-loading/page-loading-overlay.js +4 -2
- package/dist/page-loading.css +13 -2
- package/dist/sidebar-toggle-island/index.js +5 -0
- package/dist/sidebar-tree-island/index.js +106 -90
- package/dist/sidebar-tree-island/sidebar-scroll-preserve.d.ts +10 -8
- package/dist/sidebar-tree-island/sidebar-scroll-preserve.js +424 -24
- package/dist/sidebar-utils/index.d.ts +8 -0
- package/dist/sidebar-utils/index.js +51 -6
- package/dist/theme-toggle/index.js +76 -27
- package/dist/transitions/chrome-transition-bootstrap.d.ts +2 -0
- package/dist/transitions/chrome-transition-bootstrap.js +23 -0
- package/dist/transitions/page-events.d.ts +4 -0
- package/dist/transitions/page-events.js +4 -0
- package/dist/wind.json +2 -0
- package/eject/page-loading/page-loading-overlay.tsx +7 -5
- package/eject/sidebar-toggle-island/index.tsx +5 -0
- package/eject/sidebar-tree-island/index.tsx +16 -2
- package/eject/sidebar-tree-island/sidebar-scroll-preserve.ts +482 -35
- package/eject/theme-toggle/index.tsx +79 -35
- package/package.json +14 -11
- package/tsconfig.base.json +1 -2
|
@@ -5,7 +5,6 @@ import { hydrationPending } from "../hydration-pending.js";
|
|
|
5
5
|
import { AFTER_NAVIGATE_EVENT } from "../transitions/index.js";
|
|
6
6
|
import {
|
|
7
7
|
applyThemePreference,
|
|
8
|
-
readColorSchemeFromDom,
|
|
9
8
|
readThemePreference,
|
|
10
9
|
subscribeColorSchemeChanged,
|
|
11
10
|
subscribeThemePreferenceChanged
|
|
@@ -43,11 +42,31 @@ const englishLabels = {
|
|
|
43
42
|
system: "System",
|
|
44
43
|
systemHelper: "Follows device \xB7 currently {mode}"
|
|
45
44
|
};
|
|
46
|
-
|
|
45
|
+
const MENU_WIDTH_REM = 14.75;
|
|
46
|
+
const VIEWPORT_GAP = 8;
|
|
47
|
+
function viewportSize() {
|
|
48
|
+
const root = document.documentElement;
|
|
49
|
+
return {
|
|
50
|
+
width: Math.min(window.innerWidth, root.clientWidth || window.innerWidth),
|
|
51
|
+
height: Math.min(window.innerHeight, root.clientHeight || window.innerHeight)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function AppearanceMenu({ id, labels, preference, deviceMode, activeIndex, triggerRef, rootRef, close, select, move }) {
|
|
47
55
|
const scope = getScope();
|
|
48
56
|
const menuRef = { current: null };
|
|
49
57
|
const itemRefs = preferences.map(() => ({ current: null }));
|
|
50
|
-
const
|
|
58
|
+
const systemDescriptionId = `${id}-system-desc`;
|
|
59
|
+
const placement = signal({ visibility: "hidden", left: "0px", top: "0px", right: "auto", bottom: "auto", margin: "0", width: `${MENU_WIDTH_REM}rem`, "max-height": "none", overflow: "visible" });
|
|
60
|
+
const focusItem = (index) => {
|
|
61
|
+
const item = itemRefs[index]?.current;
|
|
62
|
+
const menu = menuRef.current;
|
|
63
|
+
if (!item) return;
|
|
64
|
+
item.focus({ preventScroll: true });
|
|
65
|
+
if (!menu || menu.scrollHeight <= menu.clientHeight) return;
|
|
66
|
+
const bottom = item.offsetTop + item.offsetHeight;
|
|
67
|
+
if (item.offsetTop < menu.scrollTop) menu.scrollTop = item.offsetTop;
|
|
68
|
+
else if (bottom > menu.scrollTop + menu.clientHeight) menu.scrollTop = bottom - menu.clientHeight;
|
|
69
|
+
};
|
|
51
70
|
scope.onActivate(() => {
|
|
52
71
|
const menu = menuRef.current;
|
|
53
72
|
if (!menu) return;
|
|
@@ -55,35 +74,58 @@ function AppearanceMenu({ id, labels, preference, resolved, activeIndex, trigger
|
|
|
55
74
|
const position = () => {
|
|
56
75
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
57
76
|
if (!rect) return;
|
|
58
|
-
const gap =
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
77
|
+
const gap = VIEWPORT_GAP;
|
|
78
|
+
const viewport = viewportSize();
|
|
79
|
+
const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
80
|
+
const width = Math.max(0, Math.min(MENU_WIDTH_REM * rootFontSize, viewport.width - gap * 2));
|
|
81
|
+
menu.style.width = `${width}px`;
|
|
82
|
+
const height = menu.scrollHeight + menu.offsetHeight - menu.clientHeight;
|
|
83
|
+
const roomBelow = viewport.height - rect.bottom - gap * 2;
|
|
62
84
|
const roomAbove = rect.top - gap * 2;
|
|
63
|
-
const above =
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
85
|
+
const above = height > roomBelow && roomAbove > roomBelow;
|
|
86
|
+
const room = above ? roomAbove : roomBelow;
|
|
87
|
+
const fits = height <= viewport.height - gap * 2;
|
|
88
|
+
const shownHeight = fits ? height : viewport.height - gap * 2;
|
|
89
|
+
const preferredTop = above ? rect.top - gap - Math.min(height, room) : rect.bottom + gap;
|
|
90
|
+
const top = Math.max(gap, Math.min(preferredTop, viewport.height - gap - shownHeight));
|
|
91
|
+
const left = Math.max(gap, Math.min(rect.right - width, viewport.width - width - gap));
|
|
92
|
+
placement.value = {
|
|
93
|
+
visibility: "visible",
|
|
94
|
+
left: `${left}px`,
|
|
95
|
+
top: `${top}px`,
|
|
96
|
+
right: "auto",
|
|
97
|
+
bottom: "auto",
|
|
98
|
+
margin: "0",
|
|
99
|
+
width: `${width}px`,
|
|
100
|
+
// Scrolling is only a last resort when the natural menu is taller than the viewport.
|
|
101
|
+
"max-height": fits ? "none" : `${shownHeight}px`,
|
|
102
|
+
overflow: fits ? "visible" : "auto"
|
|
103
|
+
};
|
|
68
104
|
};
|
|
69
105
|
const onPointerDown = (event) => {
|
|
70
106
|
if (!rootRef.current?.contains(event.target) && !menu.contains(event.target)) close();
|
|
71
107
|
};
|
|
72
108
|
const onNavigate = () => close();
|
|
109
|
+
const onScroll = (event) => {
|
|
110
|
+
if (event.target !== menu) position();
|
|
111
|
+
};
|
|
73
112
|
position();
|
|
74
113
|
const focusFrame = requestAnimationFrame(() => {
|
|
75
|
-
if (!scope.abortSignal.aborted)
|
|
114
|
+
if (!scope.abortSignal.aborted) focusItem(activeIndex.value);
|
|
76
115
|
});
|
|
116
|
+
const resizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => position());
|
|
117
|
+
resizeObserver?.observe(menu);
|
|
77
118
|
document.addEventListener("pointerdown", onPointerDown);
|
|
78
119
|
window.addEventListener("resize", position);
|
|
79
|
-
window.addEventListener("scroll",
|
|
120
|
+
window.addEventListener("scroll", onScroll, true);
|
|
80
121
|
document.addEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
|
|
81
122
|
return () => {
|
|
82
123
|
cancelAnimationFrame(focusFrame);
|
|
124
|
+
resizeObserver?.disconnect();
|
|
83
125
|
if (menu.hidePopover && menu.matches(":popover-open")) menu.hidePopover();
|
|
84
126
|
document.removeEventListener("pointerdown", onPointerDown);
|
|
85
127
|
window.removeEventListener("resize", position);
|
|
86
|
-
window.removeEventListener("scroll",
|
|
128
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
87
129
|
document.removeEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
|
|
88
130
|
};
|
|
89
131
|
});
|
|
@@ -100,7 +142,7 @@ function AppearanceMenu({ id, labels, preference, resolved, activeIndex, trigger
|
|
|
100
142
|
keyboard.preventDefault();
|
|
101
143
|
const next = keyboard.key === "Home" ? 0 : keyboard.key === "End" ? 2 : activeIndex.value + (keyboard.key === "ArrowDown" ? 1 : -1);
|
|
102
144
|
move(next);
|
|
103
|
-
|
|
145
|
+
focusItem((next + preferences.length) % preferences.length);
|
|
104
146
|
}
|
|
105
147
|
};
|
|
106
148
|
return /* @__PURE__ */ jsxs(
|
|
@@ -111,29 +153,31 @@ function AppearanceMenu({ id, labels, preference, resolved, activeIndex, trigger
|
|
|
111
153
|
role: "menu",
|
|
112
154
|
"aria-label": labels.appearance,
|
|
113
155
|
"on:keydown": onMenuKeyDown,
|
|
114
|
-
class: "fixed z-tooltip
|
|
156
|
+
class: "fixed z-tooltip flex flex-col gap-hsp-2xs rounded-lg border border-muted bg-surface p-hsp-xs text-fg shadow-lg",
|
|
115
157
|
style: placement,
|
|
116
158
|
popover: "manual",
|
|
117
159
|
children: [
|
|
118
|
-
/* @__PURE__ */ jsx("div", { class: "px-hsp-sm py-vsp-xs text-small font-semibold", "aria-hidden": "true", children: labels.appearance }),
|
|
119
160
|
preferences.map((option, index) => /* @__PURE__ */ jsxs(
|
|
120
161
|
"button",
|
|
121
162
|
{
|
|
122
163
|
ref: itemRefs[index],
|
|
123
164
|
type: "button",
|
|
124
165
|
role: "menuitemradio",
|
|
166
|
+
tabindex: -1,
|
|
125
167
|
"aria-checked": computed(() => preference.value === option),
|
|
168
|
+
"aria-describedby": option === "system" ? systemDescriptionId : void 0,
|
|
126
169
|
"on:focus": () => move(index),
|
|
127
170
|
"on:click": () => select(option),
|
|
128
|
-
class: computed(() => `flex min-h-[44px] w-full items-center gap-hsp-sm rounded px-hsp-sm text-left text-small ${preference.value === option ? "bg-accent/10" : "
|
|
171
|
+
class: computed(() => `flex min-h-[44px] w-full items-center gap-hsp-sm rounded px-hsp-sm text-left text-small ${preference.value === option ? "bg-accent/10" : "hover:bg-accent/5"} focus-visible:outline-2 focus-visible:outline-accent`),
|
|
129
172
|
children: [
|
|
130
173
|
/* @__PURE__ */ jsx(PreferenceIcon, { preference: option }),
|
|
131
|
-
/* @__PURE__ */ jsx("span", { class: "flex-1", children: labels[option] }),
|
|
132
|
-
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", class: "text-
|
|
174
|
+
/* @__PURE__ */ jsx("span", { class: "min-w-0 flex-1 break-words", children: labels[option] }),
|
|
175
|
+
option === "system" ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", "data-zd-device-mode": "", class: "shrink-0 text-caption text-muted", children: computed(() => labels[deviceMode.value]) }) : null,
|
|
176
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", class: "w-[1.25rem] shrink-0 text-center text-accent", children: computed(() => preference.value === option ? "\u2713" : "") })
|
|
133
177
|
]
|
|
134
178
|
}
|
|
135
179
|
)),
|
|
136
|
-
/* @__PURE__ */ jsx("
|
|
180
|
+
/* @__PURE__ */ jsx("span", { id: systemDescriptionId, hidden: true, children: computed(() => labels.systemHelper.replace("{mode}", labels[deviceMode.value])) })
|
|
137
181
|
]
|
|
138
182
|
}
|
|
139
183
|
);
|
|
@@ -144,7 +188,7 @@ function ThemeToggle({ defaultMode = "dark", respectPrefersColorScheme = true, l
|
|
|
144
188
|
const rootRef = { current: null };
|
|
145
189
|
const triggerRef = { current: null };
|
|
146
190
|
const preference = signal(respectPrefersColorScheme ? "system" : defaultMode);
|
|
147
|
-
const
|
|
191
|
+
const deviceMode = signal(defaultMode);
|
|
148
192
|
const open = signal(false);
|
|
149
193
|
const activeIndex = signal(0);
|
|
150
194
|
let menuId = "";
|
|
@@ -154,9 +198,14 @@ function ThemeToggle({ defaultMode = "dark", respectPrefersColorScheme = true, l
|
|
|
154
198
|
scope.onActivate(() => {
|
|
155
199
|
const sync = () => {
|
|
156
200
|
preference.value = readThemePreference(defaultMode, respectPrefersColorScheme);
|
|
157
|
-
resolved.value = readColorSchemeFromDom(defaultMode);
|
|
158
201
|
};
|
|
159
202
|
sync();
|
|
203
|
+
const deviceQuery = window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
204
|
+
const syncDevice = () => {
|
|
205
|
+
deviceMode.value = deviceQuery?.matches ? "dark" : "light";
|
|
206
|
+
};
|
|
207
|
+
if (deviceQuery) syncDevice();
|
|
208
|
+
deviceQuery?.addEventListener?.("change", syncDevice);
|
|
160
209
|
const unsubscribePreference = subscribeThemePreferenceChanged(() => {
|
|
161
210
|
sync();
|
|
162
211
|
open.value = false;
|
|
@@ -165,19 +214,19 @@ function ThemeToggle({ defaultMode = "dark", respectPrefersColorScheme = true, l
|
|
|
165
214
|
return () => {
|
|
166
215
|
unsubscribePreference();
|
|
167
216
|
unsubscribeScheme();
|
|
217
|
+
deviceQuery?.removeEventListener?.("change", syncDevice);
|
|
168
218
|
};
|
|
169
219
|
});
|
|
170
220
|
const close = (restoreFocus = false) => {
|
|
171
221
|
open.value = false;
|
|
172
222
|
if (restoreFocus) requestAnimationFrame(() => {
|
|
173
|
-
if (!scope.abortSignal.aborted && !open.value) triggerRef.current?.focus();
|
|
223
|
+
if (!scope.abortSignal.aborted && !open.value) triggerRef.current?.focus({ preventScroll: true });
|
|
174
224
|
});
|
|
175
225
|
};
|
|
176
226
|
const select = (next) => {
|
|
177
227
|
close(true);
|
|
178
228
|
applyThemePreference(next);
|
|
179
229
|
preference.value = next;
|
|
180
|
-
resolved.value = readColorSchemeFromDom(defaultMode);
|
|
181
230
|
};
|
|
182
231
|
const move = (index) => {
|
|
183
232
|
activeIndex.value = (index + preferences.length) % preferences.length;
|
|
@@ -228,7 +277,7 @@ function ThemeToggle({ defaultMode = "dark", respectPrefersColorScheme = true, l
|
|
|
228
277
|
]
|
|
229
278
|
}
|
|
230
279
|
),
|
|
231
|
-
/* @__PURE__ */ jsx(Show, { when: open, children: () => /* @__PURE__ */ jsx(AppearanceMenu, { id: menuId, labels, preference,
|
|
280
|
+
/* @__PURE__ */ jsx(Show, { when: open, children: () => /* @__PURE__ */ jsx(AppearanceMenu, { id: menuId, labels, preference, deviceMode, activeIndex, triggerRef, rootRef, close, select, move }) })
|
|
232
281
|
] });
|
|
233
282
|
}
|
|
234
283
|
ThemeToggle.displayName = "ThemeToggle";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BEFORE_SWAP_EVENT } from "./page-events.js";
|
|
2
|
+
function buildChromeTransitionBootstrap() {
|
|
3
|
+
return `(function(){
|
|
4
|
+
if(document.__zdChromeTransitionPolicy)return;
|
|
5
|
+
document.__zdChromeTransitionPolicy=true;
|
|
6
|
+
function identity(doc){
|
|
7
|
+
var sidebar=doc.getElementById("desktop-sidebar");
|
|
8
|
+
var context=sidebar&&sidebar.querySelector("[data-sidebar-context]");
|
|
9
|
+
var band=doc.querySelector("[data-zd-nav-section]");
|
|
10
|
+
return {section:band&&band.getAttribute("data-zd-nav-section"),context:band&&!band.hasAttribute("data-zd-nosidebar")&&context&&context.getAttribute("data-sidebar-context")};
|
|
11
|
+
}
|
|
12
|
+
document.addEventListener(${JSON.stringify(BEFORE_SWAP_EVENT)},function(ev){
|
|
13
|
+
var incoming=ev.newDocument;
|
|
14
|
+
if(!incoming||incoming.nodeType!==9)return;
|
|
15
|
+
var from=identity(document),to=identity(incoming);
|
|
16
|
+
var stationary=!!from.section&&!!from.context&&from.section===to.section&&from.context===to.context;
|
|
17
|
+
incoming.documentElement.setAttribute("data-zd-sidebar-transition",stationary?"stationary":"changing");
|
|
18
|
+
});
|
|
19
|
+
})();`;
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
buildChromeTransitionBootstrap
|
|
23
|
+
};
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* the v2 vocabulary.
|
|
8
8
|
*/
|
|
9
9
|
export declare const BEFORE_NAVIGATE_EVENT = "zfb:before-preparation";
|
|
10
|
+
/** Fetch/preparation finished, before the native old snapshot is captured. */
|
|
11
|
+
export declare const AFTER_PREPARATION_EVENT = "zfb:after-preparation";
|
|
12
|
+
/** A navigation was abandoned before committing the destination DOM. */
|
|
13
|
+
export declare const NAVIGATION_ABORTED_EVENT = "zfb:navigation-aborted";
|
|
10
14
|
/**
|
|
11
15
|
* Event name fired after a SPA body swap. Today resolves to
|
|
12
16
|
* `"zfb:after-swap"`. The initial page load uses `zfb:page-load` instead;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const BEFORE_NAVIGATE_EVENT = "zfb:before-preparation";
|
|
2
|
+
const AFTER_PREPARATION_EVENT = "zfb:after-preparation";
|
|
3
|
+
const NAVIGATION_ABORTED_EVENT = "zfb:navigation-aborted";
|
|
2
4
|
const AFTER_NAVIGATE_EVENT = "zfb:after-swap";
|
|
3
5
|
const BEFORE_SWAP_EVENT = "zfb:before-swap";
|
|
4
6
|
function onBeforeNavigate(handler) {
|
|
@@ -15,8 +17,10 @@ function onAfterNavigate(handler) {
|
|
|
15
17
|
}
|
|
16
18
|
export {
|
|
17
19
|
AFTER_NAVIGATE_EVENT,
|
|
20
|
+
AFTER_PREPARATION_EVENT,
|
|
18
21
|
BEFORE_NAVIGATE_EVENT,
|
|
19
22
|
BEFORE_SWAP_EVENT,
|
|
23
|
+
NAVIGATION_ABORTED_EVENT,
|
|
20
24
|
onAfterNavigate,
|
|
21
25
|
onBeforeNavigate
|
|
22
26
|
};
|
package/dist/wind.json
CHANGED
|
@@ -175,6 +175,7 @@
|
|
|
175
175
|
"hover:bg-[color-mix(in_srgb,var(--color-surface)_80%,var(--color-fg)_20%)]",
|
|
176
176
|
"hover:bg-accent-hover",
|
|
177
177
|
"hover:bg-accent/10",
|
|
178
|
+
"hover:bg-accent/5",
|
|
178
179
|
"hover:bg-bg/10",
|
|
179
180
|
"hover:bg-danger/10",
|
|
180
181
|
"hover:bg-surface",
|
|
@@ -457,6 +458,7 @@
|
|
|
457
458
|
"w-[0.625rem]",
|
|
458
459
|
"w-[0.875rem]",
|
|
459
460
|
"w-[1.125rem]",
|
|
461
|
+
"w-[1.25rem]",
|
|
460
462
|
"w-[1.575rem]",
|
|
461
463
|
"w-[1.5rem]",
|
|
462
464
|
"w-[1.75rem]",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @jsxRuntime automatic */
|
|
2
|
-
// Server-rendered, zero-hydration
|
|
2
|
+
// Server-rendered, zero-hydration content loading overlay.
|
|
3
3
|
//
|
|
4
4
|
// Renders the overlay and its bootstrap into the document:
|
|
5
5
|
//
|
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
// `zfb:after-swap` (AFTER_NAVIGATE_EVENT); see
|
|
16
16
|
// `transitions/page-events.ts` for rationale.
|
|
17
17
|
//
|
|
18
|
-
// Its CSS
|
|
18
|
+
// Its package CSS is imported by the host, and the component is not
|
|
19
19
|
// hydrated: the static script owns its navigation listeners.
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
22
|
AFTER_NAVIGATE_EVENT,
|
|
23
23
|
BEFORE_NAVIGATE_EVENT,
|
|
24
|
+
NAVIGATION_ABORTED_EVENT,
|
|
24
25
|
} from "../transitions/page-events.js";
|
|
25
26
|
|
|
26
27
|
/** Default `id` for the overlay element. Stable so test rigs can target it. */
|
|
@@ -52,7 +53,7 @@ export function buildPageLoadingOverlayBootstrap(overlayId: string): string {
|
|
|
52
53
|
const after = JSON.stringify(AFTER_NAVIGATE_EVENT);
|
|
53
54
|
return `(function(){
|
|
54
55
|
var id=${id};
|
|
55
|
-
function show(){var el=document.getElementById(id);if(!el)return;el.setAttribute("data-visible","");el.setAttribute("aria-hidden","false");}
|
|
56
|
+
function show(){var el=document.getElementById(id);if(!el)return;var content=document.querySelector(".zd-sidebar-content-wrapper");var header=document.querySelector("body > header");el.style.left=Math.max(0,content?content.getBoundingClientRect().left:0)+"px";el.style.top=Math.max(0,header?header.getBoundingClientRect().bottom:0)+"px";el.setAttribute("data-visible","");el.setAttribute("aria-hidden","false");}
|
|
56
57
|
function hide(){var el=document.getElementById(id);if(!el)return;el.removeAttribute("data-visible");el.setAttribute("aria-hidden","true");}
|
|
57
58
|
function setPending(ev){document.querySelectorAll("[data-zd-nav-pending]").forEach(function(el){el.removeAttribute("data-zd-nav-pending");});var src=ev&&ev.sourceElement;if(src&&src instanceof Element)src.setAttribute("data-zd-nav-pending","");}
|
|
58
59
|
function clearPending(){document.querySelectorAll("[data-zd-nav-pending]").forEach(function(el){el.removeAttribute("data-zd-nav-pending");});}
|
|
@@ -60,17 +61,18 @@ document.addEventListener(${before},show);
|
|
|
60
61
|
document.addEventListener(${after},hide);
|
|
61
62
|
document.addEventListener(${before},setPending);
|
|
62
63
|
document.addEventListener(${after},clearPending);
|
|
64
|
+
document.addEventListener(${JSON.stringify(NAVIGATION_ABORTED_EVENT)},function(){hide();clearPending();});
|
|
63
65
|
})();`;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
|
-
*
|
|
69
|
+
* Content loading overlay shown while a dynamic navigation is pending.
|
|
68
70
|
*
|
|
69
71
|
* Mount this once per layout (typically inside `DocLayoutWithDefaults`'s
|
|
70
72
|
* `bodyEnd` slot, alongside the existing body-end providers). It is
|
|
71
73
|
* server-rendered and self-wires its visibility — no hydration needed.
|
|
72
74
|
*
|
|
73
|
-
* CSS
|
|
75
|
+
* CSS ships in `@takazudo/zudo-doc/page-loading.css` (`.page-loading-overlay`,
|
|
74
76
|
* `.page-loading-spinner`, `[data-zd-nav-pending]` rules) rather than in an
|
|
75
77
|
* inline `<style>` block here — a `<style>` inside `<body>` violates HTML5
|
|
76
78
|
* element-permitted-content and fails html-validate (same fix applied to the
|
|
@@ -9,6 +9,7 @@ import { computed, getScope, signal, type Ref } from "@takazudo/zfb/zudo-react";
|
|
|
9
9
|
// collapse into duplicate import statements in an ejected copy.
|
|
10
10
|
import { AFTER_NAVIGATE_EVENT, ensureNestedIslandPropsRefresh } from "../transitions/index.js";
|
|
11
11
|
import { SidebarTree } from "../sidebar-tree-island/index.js";
|
|
12
|
+
import { notifySidebarScrollReady } from "../sidebar-tree-island/sidebar-scroll-preserve.js";
|
|
12
13
|
import type { ThemeToggleLabels } from "../theme-toggle/index.js";
|
|
13
14
|
import type { SidebarNavNode, SidebarRootMenuItem, SidebarLocaleLink, SidebarNavigationContext } from "../sidebar/types.js";
|
|
14
15
|
import type { ResolvedDateFormats } from "../settings.js";
|
|
@@ -93,6 +94,10 @@ export function SidebarToggle({
|
|
|
93
94
|
document.body.style.overflow = isOpen ? "hidden" : "";
|
|
94
95
|
if (isOpen) closeIconRef.current?.style.removeProperty("display");
|
|
95
96
|
else closeIconRef.current?.style.setProperty("display", "none");
|
|
97
|
+
if (isOpen) {
|
|
98
|
+
const scroller = document.querySelector("aside[data-zd-mobile-sidebar] .flex-1.overflow-y-auto");
|
|
99
|
+
if (scroller) notifySidebarScrollReady(scroller);
|
|
100
|
+
}
|
|
96
101
|
if (isOpen) menuIconRef.current?.style.setProperty("display", "none");
|
|
97
102
|
else menuIconRef.current?.style.removeProperty("display");
|
|
98
103
|
return () => {
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
CURRENT_PATH_DATASET_KEY,
|
|
35
35
|
readCurrentPath,
|
|
36
36
|
} from "../current-path/index.js";
|
|
37
|
-
import { ensureSidebarScrollPreserve } from "./sidebar-scroll-preserve.js";
|
|
37
|
+
import { ensureSidebarScrollPreserve, notifySidebarScrollReady } from "./sidebar-scroll-preserve.js";
|
|
38
38
|
import {
|
|
39
39
|
formatYearLabel,
|
|
40
40
|
formatYearMonthLabel,
|
|
@@ -309,6 +309,15 @@ export function SidebarTree({
|
|
|
309
309
|
const filteredNodes = computed(() =>
|
|
310
310
|
query.value ? filterTree(selectedNodes.value, query.value) : selectedNodes.value,
|
|
311
311
|
);
|
|
312
|
+
// Notify after scope, filter, and disclosure have committed. The controller
|
|
313
|
+
// restores only while a navigation is waiting, so later edits do not scroll.
|
|
314
|
+
scope.effect(() => {
|
|
315
|
+
void selected.value;
|
|
316
|
+
void selectedNodes.value;
|
|
317
|
+
void query.value;
|
|
318
|
+
const nav = navRef.current;
|
|
319
|
+
if (nav) notifySidebarScrollReady(nav);
|
|
320
|
+
});
|
|
312
321
|
const locale =
|
|
313
322
|
localeProp ?? localeLinks?.find((link) => link.active)?.code ?? "en";
|
|
314
323
|
const footer = (
|
|
@@ -333,7 +342,12 @@ export function SidebarTree({
|
|
|
333
342
|
() => activeSlug.value === undefined && !!rootMenuItems,
|
|
334
343
|
)}
|
|
335
344
|
fallback={() => (
|
|
336
|
-
<nav
|
|
345
|
+
<nav
|
|
346
|
+
ref={navRef}
|
|
347
|
+
tabindex={-1}
|
|
348
|
+
data-sidebar-context={navigation?.id}
|
|
349
|
+
data-sidebar-scope={computed(() => selected.value ?? "")}
|
|
350
|
+
>
|
|
337
351
|
{rootMenuItems ? (
|
|
338
352
|
<button
|
|
339
353
|
type="button"
|