@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,
|
|
@@ -56,7 +55,7 @@ interface AppearanceMenuProps {
|
|
|
56
55
|
id: string;
|
|
57
56
|
labels: ThemeToggleLabels;
|
|
58
57
|
preference: ReadonlySignal<ThemePreference>;
|
|
59
|
-
|
|
58
|
+
deviceMode: ReadonlySignal<ColorSchemeMode>;
|
|
60
59
|
activeIndex: ReadonlySignal<number>;
|
|
61
60
|
triggerRef: Ref<HTMLButtonElement>;
|
|
62
61
|
rootRef: Ref<HTMLDivElement>;
|
|
@@ -65,11 +64,36 @@ interface AppearanceMenuProps {
|
|
|
65
64
|
move: (index: number) => void;
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
|
|
67
|
+
const MENU_WIDTH_REM = 14.75;
|
|
68
|
+
const VIEWPORT_GAP = 8;
|
|
69
|
+
|
|
70
|
+
function viewportSize() {
|
|
71
|
+
const root = document.documentElement;
|
|
72
|
+
// clientWidth/Height exclude classic scrollbars; innerWidth/Height include them.
|
|
73
|
+
return {
|
|
74
|
+
width: Math.min(window.innerWidth, root.clientWidth || window.innerWidth),
|
|
75
|
+
height: Math.min(window.innerHeight, root.clientHeight || window.innerHeight),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function AppearanceMenu({ id, labels, preference, deviceMode, activeIndex, triggerRef, rootRef, close, select, move }: AppearanceMenuProps) {
|
|
69
80
|
const scope = getScope();
|
|
70
81
|
const menuRef: Ref<HTMLDivElement> = { current: null };
|
|
71
82
|
const itemRefs: Ref<HTMLButtonElement>[] = preferences.map(() => ({ current: null }));
|
|
72
|
-
const
|
|
83
|
+
const systemDescriptionId = `${id}-system-desc`;
|
|
84
|
+
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" });
|
|
85
|
+
|
|
86
|
+
// preventScroll keeps the document still, so reveal the row inside a last-resort scrolling menu by hand.
|
|
87
|
+
const focusItem = (index: number) => {
|
|
88
|
+
const item = itemRefs[index]?.current;
|
|
89
|
+
const menu = menuRef.current;
|
|
90
|
+
if (!item) return;
|
|
91
|
+
item.focus({ preventScroll: true });
|
|
92
|
+
if (!menu || menu.scrollHeight <= menu.clientHeight) return;
|
|
93
|
+
const bottom = item.offsetTop + item.offsetHeight;
|
|
94
|
+
if (item.offsetTop < menu.scrollTop) menu.scrollTop = item.offsetTop;
|
|
95
|
+
else if (bottom > menu.scrollTop + menu.clientHeight) menu.scrollTop = bottom - menu.clientHeight;
|
|
96
|
+
};
|
|
73
97
|
|
|
74
98
|
scope.onActivate(() => {
|
|
75
99
|
const menu = menuRef.current;
|
|
@@ -78,37 +102,53 @@ function AppearanceMenu({ id, labels, preference, resolved, activeIndex, trigger
|
|
|
78
102
|
const position = () => {
|
|
79
103
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
80
104
|
if (!rect) return;
|
|
81
|
-
const gap =
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
105
|
+
const gap = VIEWPORT_GAP;
|
|
106
|
+
const viewport = viewportSize();
|
|
107
|
+
const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
108
|
+
const width = Math.max(0, Math.min(MENU_WIDTH_REM * rootFontSize, viewport.width - gap * 2));
|
|
109
|
+
// Measure the natural border-box height at the final width before placing. Read it as
|
|
110
|
+
// content + chrome instead of lifting max-height, which would clamp a scrolled menu to 0.
|
|
111
|
+
menu.style.width = `${width}px`;
|
|
112
|
+
const height = menu.scrollHeight + menu.offsetHeight - menu.clientHeight;
|
|
113
|
+
const roomBelow = viewport.height - rect.bottom - gap * 2;
|
|
85
114
|
const roomAbove = rect.top - gap * 2;
|
|
86
|
-
const above =
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
115
|
+
const above = height > roomBelow && roomAbove > roomBelow;
|
|
116
|
+
const room = above ? roomAbove : roomBelow;
|
|
117
|
+
const fits = height <= viewport.height - gap * 2;
|
|
118
|
+
const shownHeight = fits ? height : viewport.height - gap * 2;
|
|
119
|
+
const preferredTop = above ? rect.top - gap - Math.min(height, room) : rect.bottom + gap;
|
|
120
|
+
const top = Math.max(gap, Math.min(preferredTop, viewport.height - gap - shownHeight));
|
|
121
|
+
const left = Math.max(gap, Math.min(rect.right - width, viewport.width - width - gap));
|
|
122
|
+
placement.value = {
|
|
123
|
+
visibility: "visible", left: `${left}px`, top: `${top}px`, right: "auto", bottom: "auto", margin: "0", width: `${width}px`,
|
|
124
|
+
// Scrolling is only a last resort when the natural menu is taller than the viewport.
|
|
125
|
+
"max-height": fits ? "none" : `${shownHeight}px`, overflow: fits ? "visible" : "auto",
|
|
126
|
+
};
|
|
91
127
|
};
|
|
92
128
|
const onPointerDown = (event: PointerEvent) => {
|
|
93
129
|
if (!rootRef.current?.contains(event.target as Node) && !menu.contains(event.target as Node)) close();
|
|
94
130
|
};
|
|
95
131
|
const onNavigate = () => close();
|
|
132
|
+
const onScroll = (event: Event) => { if (event.target !== menu) position(); };
|
|
96
133
|
position();
|
|
97
134
|
// The placement signal commits visibility after this activation callback.
|
|
98
135
|
// Focus only after the visible popover has reached the browser layout.
|
|
99
136
|
const focusFrame = requestAnimationFrame(() => {
|
|
100
|
-
if (!scope.abortSignal.aborted)
|
|
137
|
+
if (!scope.abortSignal.aborted) focusItem(activeIndex.value);
|
|
101
138
|
});
|
|
139
|
+
const resizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => position());
|
|
140
|
+
resizeObserver?.observe(menu);
|
|
102
141
|
document.addEventListener("pointerdown", onPointerDown);
|
|
103
142
|
window.addEventListener("resize", position);
|
|
104
|
-
window.addEventListener("scroll",
|
|
143
|
+
window.addEventListener("scroll", onScroll, true);
|
|
105
144
|
document.addEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
|
|
106
145
|
return () => {
|
|
107
146
|
cancelAnimationFrame(focusFrame);
|
|
147
|
+
resizeObserver?.disconnect();
|
|
108
148
|
if (menu.hidePopover && menu.matches(":popover-open")) menu.hidePopover();
|
|
109
149
|
document.removeEventListener("pointerdown", onPointerDown);
|
|
110
150
|
window.removeEventListener("resize", position);
|
|
111
|
-
window.removeEventListener("scroll",
|
|
151
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
112
152
|
document.removeEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
|
|
113
153
|
};
|
|
114
154
|
});
|
|
@@ -126,26 +166,29 @@ function AppearanceMenu({ id, labels, preference, resolved, activeIndex, trigger
|
|
|
126
166
|
keyboard.preventDefault();
|
|
127
167
|
const next = keyboard.key === "Home" ? 0 : keyboard.key === "End" ? 2 : activeIndex.value + (keyboard.key === "ArrowDown" ? 1 : -1);
|
|
128
168
|
move(next);
|
|
129
|
-
|
|
169
|
+
focusItem((next + preferences.length) % preferences.length);
|
|
130
170
|
}
|
|
131
171
|
};
|
|
132
172
|
|
|
133
173
|
return <div ref={menuRef} id={id} role="menu" aria-label={labels.appearance}
|
|
134
174
|
on:keydown={onMenuKeyDown}
|
|
135
|
-
class="fixed z-tooltip
|
|
175
|
+
class="fixed z-tooltip flex flex-col gap-hsp-2xs rounded-lg border border-muted bg-surface p-hsp-xs text-fg shadow-lg"
|
|
136
176
|
style={placement} popover="manual">
|
|
137
|
-
<div class="px-hsp-sm py-vsp-xs text-small font-semibold" aria-hidden="true">{labels.appearance}</div>
|
|
138
177
|
{preferences.map((option, index) => <button ref={itemRefs[index]} type="button"
|
|
139
|
-
role="menuitemradio" aria-checked={computed(() => preference.value === option)}
|
|
178
|
+
role="menuitemradio" tabindex={-1} aria-checked={computed(() => preference.value === option)}
|
|
179
|
+
aria-describedby={option === "system" ? systemDescriptionId : undefined}
|
|
140
180
|
on:focus={() => move(index)} on:click={() => select(option)}
|
|
141
|
-
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" : "
|
|
181
|
+
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`)}>
|
|
142
182
|
<PreferenceIcon preference={option} />
|
|
143
|
-
<span class="flex-1">{labels[option]}</span>
|
|
144
|
-
|
|
183
|
+
<span class="min-w-0 flex-1 break-words">{labels[option]}</span>
|
|
184
|
+
{option === "system"
|
|
185
|
+
? <span aria-hidden="true" data-zd-device-mode="" class="shrink-0 text-caption text-muted">{computed(() => labels[deviceMode.value])}</span>
|
|
186
|
+
: null}
|
|
187
|
+
<span aria-hidden="true" class="w-[1.25rem] shrink-0 text-center text-accent">{computed(() => preference.value === option ? "✓" : "")}</span>
|
|
145
188
|
</button>)}
|
|
146
|
-
<
|
|
147
|
-
{computed(() => labels.systemHelper.replace("{mode}", labels[
|
|
148
|
-
</
|
|
189
|
+
<span id={systemDescriptionId} hidden>
|
|
190
|
+
{computed(() => labels.systemHelper.replace("{mode}", labels[deviceMode.value]))}
|
|
191
|
+
</span>
|
|
149
192
|
</div>;
|
|
150
193
|
}
|
|
151
194
|
|
|
@@ -156,34 +199,35 @@ export function ThemeToggle({ defaultMode = "dark", respectPrefersColorScheme =
|
|
|
156
199
|
const rootRef: Ref<HTMLDivElement> = { current: null };
|
|
157
200
|
const triggerRef: Ref<HTMLButtonElement> = { current: null };
|
|
158
201
|
const preference = signal<ThemePreference>(respectPrefersColorScheme ? "system" : defaultMode);
|
|
159
|
-
const
|
|
202
|
+
const deviceMode = signal<ColorSchemeMode>(defaultMode);
|
|
160
203
|
const open = signal(false);
|
|
161
204
|
const activeIndex = signal(0);
|
|
162
205
|
let menuId = "";
|
|
163
206
|
const ensureMenuId = () => { if (!menuId) menuId = `zd-appearance-${++menuSequence}`; };
|
|
164
207
|
|
|
165
208
|
scope.onActivate(() => {
|
|
166
|
-
const sync = () => {
|
|
167
|
-
preference.value = readThemePreference(defaultMode, respectPrefersColorScheme);
|
|
168
|
-
resolved.value = readColorSchemeFromDom(defaultMode);
|
|
169
|
-
};
|
|
209
|
+
const sync = () => { preference.value = readThemePreference(defaultMode, respectPrefersColorScheme); };
|
|
170
210
|
sync();
|
|
211
|
+
// The device mode is independent of the page theme: it stays truthful under a manual Light/Dark choice.
|
|
212
|
+
const deviceQuery = window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
213
|
+
const syncDevice = () => { deviceMode.value = deviceQuery?.matches ? "dark" : "light"; };
|
|
214
|
+
if (deviceQuery) syncDevice();
|
|
215
|
+
deviceQuery?.addEventListener?.("change", syncDevice);
|
|
171
216
|
const unsubscribePreference = subscribeThemePreferenceChanged(() => { sync(); open.value = false; });
|
|
172
217
|
const unsubscribeScheme = subscribeColorSchemeChanged(sync);
|
|
173
|
-
return () => { unsubscribePreference(); unsubscribeScheme(); };
|
|
218
|
+
return () => { unsubscribePreference(); unsubscribeScheme(); deviceQuery?.removeEventListener?.("change", syncDevice); };
|
|
174
219
|
});
|
|
175
220
|
|
|
176
221
|
const close = (restoreFocus = false) => {
|
|
177
222
|
open.value = false;
|
|
178
223
|
if (restoreFocus) requestAnimationFrame(() => {
|
|
179
|
-
if (!scope.abortSignal.aborted && !open.value) triggerRef.current?.focus();
|
|
224
|
+
if (!scope.abortSignal.aborted && !open.value) triggerRef.current?.focus({ preventScroll: true });
|
|
180
225
|
});
|
|
181
226
|
};
|
|
182
227
|
const select = (next: ThemePreference) => {
|
|
183
228
|
close(true);
|
|
184
229
|
applyThemePreference(next);
|
|
185
230
|
preference.value = next;
|
|
186
|
-
resolved.value = readColorSchemeFromDom(defaultMode);
|
|
187
231
|
};
|
|
188
232
|
const move = (index: number) => { activeIndex.value = (index + preferences.length) % preferences.length; };
|
|
189
233
|
const show = (index: number) => { ensureMenuId(); move(index); open.value = true; };
|
|
@@ -210,7 +254,7 @@ export function ThemeToggle({ defaultMode = "dark", respectPrefersColorScheme =
|
|
|
210
254
|
<Show when={computed(() => preference.value === "dark")}>{() => <PreferenceIcon preference="dark" />}</Show>
|
|
211
255
|
<Show when={computed(() => preference.value === "system")}>{() => <PreferenceIcon preference="system" />}</Show>
|
|
212
256
|
</button>
|
|
213
|
-
<Show when={open}>{() => <AppearanceMenu id={menuId} labels={labels} preference={preference}
|
|
257
|
+
<Show when={open}>{() => <AppearanceMenu id={menuId} labels={labels} preference={preference} deviceMode={deviceMode} activeIndex={activeIndex} triggerRef={triggerRef} rootRef={rootRef} close={close} select={select} move={move} />}</Show>
|
|
214
258
|
</div>;
|
|
215
259
|
}
|
|
216
260
|
ThemeToggle.displayName = "ThemeToggle";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takazudo/zudo-doc",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "zudo-doc framework primitives layer that sits on top of zfb's engine — sidebar, theme, TOC, breadcrumb, layouts, head injection, View Transitions, SSR-skip wrappers (per ADR-003).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"bugs": {
|
|
15
15
|
"url": "https://github.com/zudolab/zudo-doc/issues"
|
|
16
16
|
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=24 <25"
|
|
19
|
+
},
|
|
17
20
|
"keywords": [
|
|
18
21
|
"zudo-doc",
|
|
19
22
|
"documentation",
|
|
@@ -733,12 +736,12 @@
|
|
|
733
736
|
"peerDependencies": {
|
|
734
737
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
735
738
|
"@takazudo/zdtp": "^0.5.2 || ^0.6.0 || ^0.7.0 || ^0.8.0",
|
|
736
|
-
"@takazudo/zfb": "^4.
|
|
737
|
-
"@takazudo/zfb-md-wasm": "^4.
|
|
738
|
-
"@takazudo/zfb-runtime": "^4.
|
|
739
|
-
"@takazudo/zudo-doc-history-server": "^
|
|
739
|
+
"@takazudo/zfb": "^4.5.2",
|
|
740
|
+
"@takazudo/zfb-md-wasm": "^4.5.2",
|
|
741
|
+
"@takazudo/zfb-runtime": "^4.5.2",
|
|
742
|
+
"@takazudo/zudo-doc-history-server": "^7.0.0",
|
|
740
743
|
"diff": "^8.0.0",
|
|
741
|
-
"katex": "^0.
|
|
744
|
+
"katex": "^0.18.2 || ^0.19.0",
|
|
742
745
|
"zod": "^4.3.6"
|
|
743
746
|
},
|
|
744
747
|
"peerDependenciesMeta": {
|
|
@@ -769,16 +772,16 @@
|
|
|
769
772
|
"parse5": "^8.0.1",
|
|
770
773
|
"picocolors": "^1.1.1",
|
|
771
774
|
"pluralize": "^8.0.0",
|
|
772
|
-
"smol-toml": "^1.
|
|
775
|
+
"smol-toml": "^1.9.1",
|
|
773
776
|
"tsx": "^4.21.0",
|
|
774
777
|
"yaml": "^2.9.0"
|
|
775
778
|
},
|
|
776
779
|
"devDependencies": {
|
|
777
780
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
778
781
|
"@takazudo/mdx-formatter": "1.3.0",
|
|
779
|
-
"@takazudo/zfb": "4.
|
|
780
|
-
"@takazudo/zfb-md-wasm": "4.
|
|
781
|
-
"@takazudo/zfb-runtime": "4.
|
|
782
|
+
"@takazudo/zfb": "4.5.2",
|
|
783
|
+
"@takazudo/zfb-md-wasm": "4.5.2",
|
|
784
|
+
"@takazudo/zfb-runtime": "4.5.2",
|
|
782
785
|
"@types/fs-extra": "^11.0.4",
|
|
783
786
|
"@types/minimist": "^1.2.5",
|
|
784
787
|
"@types/node": "^25.3.5",
|
|
@@ -788,7 +791,7 @@
|
|
|
788
791
|
"typescript": "^5.0.0",
|
|
789
792
|
"vitest": "^4.1.0",
|
|
790
793
|
"zod": "^4.3.6",
|
|
791
|
-
"@takazudo/zudo-doc-history-server": "
|
|
794
|
+
"@takazudo/zudo-doc-history-server": "7.0.0"
|
|
792
795
|
},
|
|
793
796
|
"scripts": {
|
|
794
797
|
"gen:search-widget-script": "node scripts/gen-search-widget-script.mjs",
|
package/tsconfig.base.json
CHANGED