@takazudo/zudo-doc 6.1.0 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +2 -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 +8 -0
- 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/wind.json +2 -0
- 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 +9 -9
- 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";
|
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]",
|
|
@@ -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"
|