@takazudo/zudo-doc 5.26.5 → 5.28.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/dist/compiled.css +18 -0
  3. package/dist/config-assertions/index.d.ts +13 -0
  4. package/dist/config-assertions/index.js +9 -0
  5. package/dist/config.d.ts +14 -0
  6. package/dist/config.js +4 -1
  7. package/dist/extract-headings/index.d.ts +2 -0
  8. package/dist/extract-headings/index.js +165 -16
  9. package/dist/header/header.js +47 -19
  10. package/dist/header-with-defaults/index.js +5 -1
  11. package/dist/i18n-defaults/index.js +10 -0
  12. package/dist/plugins/internal/search-index/collect.js +15 -12
  13. package/dist/plugins/internal/search-index/types.d.ts +25 -3
  14. package/dist/plugins/internal/search-index/types.js +1 -1
  15. package/dist/preset.d.ts +8 -0
  16. package/dist/preset.js +4 -1
  17. package/dist/safelist.css +1 -1
  18. package/dist/search-widget/index.js +1 -1
  19. package/dist/settings.d.ts +1 -0
  20. package/dist/sidebar-toggle-island/index.d.ts +4 -1
  21. package/dist/sidebar-toggle-island/index.js +5 -0
  22. package/dist/sidebar-tree-island/index.d.ts +4 -1
  23. package/dist/sidebar-tree-island/index.js +5 -5
  24. package/dist/sidebar-with-defaults/index.js +3 -0
  25. package/dist/theme/color-scheme-provider.js +40 -10
  26. package/dist/theme/theme-toggle.d.ts +1 -1
  27. package/dist/theme-toggle/color-scheme-sync.d.ts +19 -39
  28. package/dist/theme-toggle/color-scheme-sync.js +81 -6
  29. package/dist/theme-toggle/index.d.ts +10 -1
  30. package/dist/theme-toggle/index.js +219 -58
  31. package/dist/theme-toggle/labels.d.ts +3 -0
  32. package/dist/theme-toggle/labels.js +12 -0
  33. package/eject/header/header.tsx +56 -19
  34. package/eject/sidebar-toggle-island/index.tsx +14 -0
  35. package/eject/sidebar-tree-island/index.tsx +8 -6
  36. package/eject/theme-toggle/color-scheme-sync.ts +108 -63
  37. package/eject/theme-toggle/index.tsx +217 -89
  38. package/eject/theme-toggle/labels.ts +15 -0
  39. package/package.json +9 -9
@@ -1,41 +1,19 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "preact/jsx-runtime";
3
- import { useState, useEffect } from "preact/hooks";
2
+ import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
3
+ import { useState, useEffect, useRef } from "preact/hooks";
4
+ import { createPortal } from "preact/compat";
4
5
  import { useHydrationPending } from "../hydration-pending.js";
6
+ import { AFTER_NAVIGATE_EVENT } from "../transitions/index.js";
5
7
  import {
6
- applyColorScheme,
8
+ applyThemePreference,
7
9
  readColorSchemeFromDom,
8
- subscribeColorSchemeChanged
10
+ readThemePreference,
11
+ subscribeColorSchemeChanged,
12
+ subscribeThemePreferenceChanged
9
13
  } from "./color-scheme-sync.js";
10
- function SunIcon() {
11
- return /* @__PURE__ */ jsxs(
12
- "svg",
13
- {
14
- "aria-hidden": "true",
15
- xmlns: "http://www.w3.org/2000/svg",
16
- width: "20",
17
- height: "20",
18
- viewBox: "0 0 24 24",
19
- fill: "none",
20
- stroke: "currentColor",
21
- strokeWidth: "2",
22
- strokeLinecap: "round",
23
- strokeLinejoin: "round",
24
- children: [
25
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "5" }),
26
- /* @__PURE__ */ jsx("line", { x1: "12", y1: "1", x2: "12", y2: "3" }),
27
- /* @__PURE__ */ jsx("line", { x1: "12", y1: "21", x2: "12", y2: "23" }),
28
- /* @__PURE__ */ jsx("line", { x1: "4.22", y1: "4.22", x2: "5.64", y2: "5.64" }),
29
- /* @__PURE__ */ jsx("line", { x1: "18.36", y1: "18.36", x2: "19.78", y2: "19.78" }),
30
- /* @__PURE__ */ jsx("line", { x1: "1", y1: "12", x2: "3", y2: "12" }),
31
- /* @__PURE__ */ jsx("line", { x1: "21", y1: "12", x2: "23", y2: "12" }),
32
- /* @__PURE__ */ jsx("line", { x1: "4.22", y1: "19.78", x2: "5.64", y2: "18.36" }),
33
- /* @__PURE__ */ jsx("line", { x1: "18.36", y1: "5.64", x2: "19.78", y2: "4.22" })
34
- ]
35
- }
36
- );
37
- }
38
- function MoonIcon() {
14
+ const preferences = ["light", "dark", "system"];
15
+ let menuSequence = 0;
16
+ function PreferenceIcon({ preference }) {
39
17
  return /* @__PURE__ */ jsx(
40
18
  "svg",
41
19
  {
@@ -46,42 +24,225 @@ function MoonIcon() {
46
24
  viewBox: "0 0 24 24",
47
25
  fill: "none",
48
26
  stroke: "currentColor",
49
- strokeWidth: "2",
50
- strokeLinecap: "round",
51
- strokeLinejoin: "round",
52
- children: /* @__PURE__ */ jsx("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" })
27
+ strokeWidth: "1.2",
28
+ strokeLinecap: "square",
29
+ strokeLinejoin: "miter",
30
+ children: preference === "light" ? /* @__PURE__ */ jsxs(Fragment, { children: [
31
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3.6" }),
32
+ /* @__PURE__ */ jsx("path", { d: "M12 2.5V6M12 18V21.5M2.5 12H6M18 12H21.5M5.3 5.3L7.8 7.8M16.2 16.2L18.7 18.7M5.3 18.7L7.8 16.2M16.2 7.8L18.7 5.3" })
33
+ ] }) : preference === "dark" ? /* @__PURE__ */ jsx("path", { d: "M10.2 2.9C5.7 3.9 2.6 7.9 3 12.6C3.4 17.7 7.9 21.5 13 21.1C16.9 20.7 20.1 17.9 21 14.1C18.6 15.7 15.6 15.8 13.2 14.3C9.3 12 8 6.8 10.2 2.9Z" }) : /* @__PURE__ */ jsx("path", { d: "M2.5 3.5H21.5V16.5H2.5ZM12 16.5V20.5M7.5 20.5H16.5" })
53
34
  }
54
35
  );
55
36
  }
37
+ const englishLabels = {
38
+ appearance: "Appearance",
39
+ light: "Light",
40
+ dark: "Dark",
41
+ system: "System",
42
+ systemHelper: "Follows device \xB7 currently {mode}"
43
+ };
56
44
  function ThemeToggle({
57
45
  defaultMode = "dark",
46
+ respectPrefersColorScheme = true,
47
+ labels = englishLabels,
58
48
  pendingUntilHydrated = true
59
49
  }) {
60
50
  const pending = useHydrationPending(pendingUntilHydrated);
61
- const [mode, setMode] = useState(defaultMode);
51
+ const menuId = useRef("");
52
+ const ensureMenuId = () => {
53
+ if (!menuId.current) menuId.current = `zd-appearance-${++menuSequence}`;
54
+ };
55
+ const rootRef = useRef(null);
56
+ const menuRef = useRef(null);
57
+ const triggerRef = useRef(null);
58
+ const itemRefs = useRef([]);
59
+ const restoreFocusRef = useRef(false);
60
+ const [preference, setPreference] = useState(
61
+ respectPrefersColorScheme ? "system" : defaultMode
62
+ );
63
+ const [resolved, setResolved] = useState(defaultMode);
64
+ const [open, setOpen] = useState(false);
65
+ const [activeIndex, setActiveIndex] = useState(0);
66
+ const [placement, setPlacement] = useState(null);
62
67
  useEffect(() => {
63
- const sync = () => setMode(readColorSchemeFromDom(defaultMode));
68
+ const sync = () => {
69
+ setPreference(readThemePreference(defaultMode, respectPrefersColorScheme));
70
+ setResolved(readColorSchemeFromDom(defaultMode));
71
+ };
64
72
  sync();
65
- return subscribeColorSchemeChanged(sync);
66
- }, []);
67
- function toggle() {
68
- if (pending) return;
69
- const next = mode === "dark" ? "light" : "dark";
70
- setMode(next);
71
- applyColorScheme(next);
72
- }
73
- const nextMode = mode === "dark" ? "light" : "dark";
74
- return /* @__PURE__ */ jsx(
75
- "button",
76
- {
77
- onClick: toggle,
78
- "aria-label": `Switch to ${nextMode} mode`,
79
- "aria-disabled": pending ? "true" : void 0,
80
- "data-zd-pending": pending ? "" : void 0,
81
- className: "text-muted hover:text-fg transition-colors p-hsp-sm focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2",
82
- children: mode === "dark" ? /* @__PURE__ */ jsx(SunIcon, {}) : /* @__PURE__ */ jsx(MoonIcon, {})
73
+ const unsubscribePreference = subscribeThemePreferenceChanged(() => {
74
+ sync();
75
+ setOpen(false);
76
+ });
77
+ const unsubscribeScheme = subscribeColorSchemeChanged(sync);
78
+ return () => {
79
+ unsubscribePreference();
80
+ unsubscribeScheme();
81
+ };
82
+ }, [defaultMode, respectPrefersColorScheme]);
83
+ useEffect(() => {
84
+ if (!open) return;
85
+ const menu = menuRef.current;
86
+ menu?.showPopover?.();
87
+ const position = () => {
88
+ const rect = triggerRef.current?.getBoundingClientRect();
89
+ if (!rect) return;
90
+ const gap = 8;
91
+ const width = Math.min(260, window.innerWidth - gap * 2);
92
+ const desiredHeight = menu?.scrollHeight ?? 280;
93
+ const roomBelow = window.innerHeight - rect.bottom - gap * 2;
94
+ const roomAbove = rect.top - gap * 2;
95
+ const above = roomBelow < desiredHeight && roomAbove > roomBelow;
96
+ const maxHeight = Math.max(80, Math.min(desiredHeight, above ? roomAbove : roomBelow));
97
+ setPlacement({
98
+ left: Math.max(gap, Math.min(rect.right - width, window.innerWidth - width - gap)),
99
+ top: above ? Math.max(gap, rect.top - gap - maxHeight) : rect.bottom + gap,
100
+ width,
101
+ maxHeight
102
+ });
103
+ };
104
+ const onPointerDown = (event) => {
105
+ if (!rootRef.current?.contains(event.target) && !menuRef.current?.contains(event.target)) setOpen(false);
106
+ };
107
+ const onNavigate = () => setOpen(false);
108
+ position();
109
+ document.addEventListener("pointerdown", onPointerDown);
110
+ window.addEventListener("resize", position);
111
+ window.addEventListener("scroll", position, true);
112
+ document.addEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
113
+ return () => {
114
+ if (menu?.hidePopover && menu.matches(":popover-open")) menu.hidePopover();
115
+ document.removeEventListener("pointerdown", onPointerDown);
116
+ window.removeEventListener("resize", position);
117
+ window.removeEventListener("scroll", position, true);
118
+ document.removeEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
119
+ };
120
+ }, [open]);
121
+ useEffect(() => {
122
+ if (open && placement) itemRefs.current[activeIndex]?.focus();
123
+ }, [open, placement]);
124
+ useEffect(() => {
125
+ if (open || !restoreFocusRef.current) return;
126
+ restoreFocusRef.current = false;
127
+ requestAnimationFrame(() => triggerRef.current?.focus());
128
+ }, [open]);
129
+ const close = (restoreFocus = false) => {
130
+ restoreFocusRef.current = restoreFocus;
131
+ setOpen(false);
132
+ };
133
+ const select = (next) => {
134
+ close(true);
135
+ applyThemePreference(next);
136
+ setPreference(next);
137
+ setResolved(readColorSchemeFromDom(defaultMode));
138
+ };
139
+ const move = (index) => {
140
+ const next = (index + preferences.length) % preferences.length;
141
+ setActiveIndex(next);
142
+ itemRefs.current[next]?.focus();
143
+ };
144
+ const onMenuKeyDown = (event) => {
145
+ if (event.key === "Escape") {
146
+ event.preventDefault();
147
+ event.stopPropagation();
148
+ close(true);
149
+ } else if (event.key === "Tab") {
150
+ window.setTimeout(() => close(), 0);
151
+ } else if (event.key === "ArrowDown") {
152
+ event.preventDefault();
153
+ move(activeIndex + 1);
154
+ } else if (event.key === "ArrowUp") {
155
+ event.preventDefault();
156
+ move(activeIndex - 1);
157
+ } else if (event.key === "Home") {
158
+ event.preventDefault();
159
+ move(0);
160
+ } else if (event.key === "End") {
161
+ event.preventDefault();
162
+ move(2);
83
163
  }
84
- );
164
+ };
165
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: "relative inline-flex", "data-zd-theme-menu": "", children: [
166
+ /* @__PURE__ */ jsx(
167
+ "button",
168
+ {
169
+ ref: triggerRef,
170
+ type: "button",
171
+ "aria-haspopup": "menu",
172
+ "aria-expanded": open,
173
+ "aria-controls": open ? menuId.current : void 0,
174
+ "aria-label": `${labels.appearance}: ${labels[preference]}`,
175
+ "aria-disabled": pending ? "true" : void 0,
176
+ "data-zd-pending": pending ? "" : void 0,
177
+ onClick: () => {
178
+ if (pending) return;
179
+ if (open) close(true);
180
+ else {
181
+ setPlacement(null);
182
+ ensureMenuId();
183
+ setActiveIndex(preferences.indexOf(preference));
184
+ setOpen(true);
185
+ }
186
+ },
187
+ onKeyDown: (event) => {
188
+ if (pending) {
189
+ if (event.key === "Enter" || event.key === " ") event.preventDefault();
190
+ return;
191
+ }
192
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
193
+ event.preventDefault();
194
+ setPlacement(null);
195
+ ensureMenuId();
196
+ setActiveIndex(event.key === "ArrowDown" ? 0 : 2);
197
+ setOpen(true);
198
+ } else if (event.key === "Escape" && open) {
199
+ event.preventDefault();
200
+ event.stopPropagation();
201
+ close(true);
202
+ }
203
+ },
204
+ className: "inline-flex h-[40px] w-[40px] shrink-0 items-center justify-center text-muted hover:text-fg focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2",
205
+ children: /* @__PURE__ */ jsx(PreferenceIcon, { preference })
206
+ }
207
+ ),
208
+ open && createPortal(/* @__PURE__ */ jsxs(
209
+ "div",
210
+ {
211
+ ref: menuRef,
212
+ id: menuId.current,
213
+ role: "menu",
214
+ "aria-label": labels.appearance,
215
+ popover: typeof HTMLElement !== "undefined" && "showPopover" in HTMLElement.prototype ? "manual" : void 0,
216
+ onKeyDown: onMenuKeyDown,
217
+ className: "fixed z-tooltip overflow-y-auto rounded-lg border border-muted bg-surface p-hsp-xs text-fg shadow-lg",
218
+ style: placement ? { left: placement.left, top: placement.top, right: "auto", bottom: "auto", margin: 0, width: placement.width, maxHeight: placement.maxHeight } : { visibility: "hidden", left: 0, top: 0, right: "auto", bottom: "auto", margin: 0, width: Math.min(260, window.innerWidth - 16) },
219
+ children: [
220
+ /* @__PURE__ */ jsx("div", { className: "px-hsp-sm py-vsp-xs text-small font-semibold", "aria-hidden": "true", children: labels.appearance }),
221
+ preferences.map((option, index) => /* @__PURE__ */ jsxs(
222
+ "button",
223
+ {
224
+ ref: (node) => {
225
+ itemRefs.current[index] = node;
226
+ },
227
+ type: "button",
228
+ role: "menuitemradio",
229
+ "aria-checked": preference === option,
230
+ onFocus: () => setActiveIndex(index),
231
+ onClick: () => select(option),
232
+ className: `flex min-h-[44px] w-full items-center gap-hsp-sm rounded px-hsp-sm text-left text-small ${preference === option ? "bg-accent/10" : ""} hover:bg-accent/10 focus-visible:bg-accent/10 focus-visible:outline-2 focus-visible:outline-accent`,
233
+ children: [
234
+ /* @__PURE__ */ jsx(PreferenceIcon, { preference: option }),
235
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: labels[option] }),
236
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-accent", children: preference === option ? "\u2713" : "" })
237
+ ]
238
+ },
239
+ option
240
+ )),
241
+ /* @__PURE__ */ jsx("div", { className: "px-hsp-sm py-vsp-xs text-small text-muted", children: labels.systemHelper.replace("{mode}", labels[resolved]) })
242
+ ]
243
+ }
244
+ ), document.body)
245
+ ] });
85
246
  }
86
247
  ThemeToggle.displayName = "ThemeToggle";
87
248
  export {
@@ -0,0 +1,3 @@
1
+ import type { ThemeToggleLabels } from "./index.js";
2
+ /** Resolve labels before island serialization so host translation overrides apply. */
3
+ export declare function themeToggleLabels(t: (key: string, locale: string) => string, locale: string): ThemeToggleLabels;
@@ -0,0 +1,12 @@
1
+ function themeToggleLabels(t, locale) {
2
+ return {
3
+ appearance: t("appearance.title", locale),
4
+ light: t("appearance.light", locale),
5
+ dark: t("appearance.dark", locale),
6
+ system: t("appearance.system", locale),
7
+ systemHelper: t("appearance.systemHelper", locale)
8
+ };
9
+ }
10
+ export {
11
+ themeToggleLabels
12
+ };
@@ -294,6 +294,49 @@ export function Header(props: HeaderProps): JSX.Element {
294
294
 
295
295
  const activeNavPath = computeActiveNavPath(headerNav, matchPath);
296
296
  const rightItemDispatch = createRightItemDispatch(headerRightComponents);
297
+ const rightItems = headerRightItems
298
+ .map((item, i) => ({
299
+ item,
300
+ node: renderRightItem(
301
+ item,
302
+ i,
303
+ {
304
+ lang,
305
+ githubRepoUrl,
306
+ githubLabel,
307
+ themeToggle,
308
+ languageSwitcher,
309
+ versionSwitcher,
310
+ search,
311
+ colorModeEnabled,
312
+ hasLocales,
313
+ },
314
+ rightItemDispatch,
315
+ ),
316
+ }))
317
+ .filter((entry): entry is typeof entry & { node: VNode } => entry.node !== null);
318
+ const rightGroups: VNode[] = [];
319
+ let iconGroup: VNode[] = [];
320
+
321
+ const flushIconGroup = () => {
322
+ if (iconGroup.length === 0) return;
323
+ rightGroups.push(
324
+ <div class="flex items-center" data-header-icon-group>
325
+ {iconGroup}
326
+ </div>,
327
+ );
328
+ iconGroup = [];
329
+ };
330
+
331
+ for (const { item, node } of rightItems) {
332
+ if (isHeaderIconItem(item)) {
333
+ iconGroup.push(node);
334
+ } else {
335
+ flushIconGroup();
336
+ rightGroups.push(node);
337
+ }
338
+ }
339
+ flushIconGroup();
297
340
 
298
341
  return (
299
342
  <header
@@ -412,22 +455,7 @@ export function Header(props: HeaderProps): JSX.Element {
412
455
  class="ml-auto flex shrink-0 items-center gap-x-hsp-md"
413
456
  data-header-right
414
457
  >
415
- {headerRightItems.map((item, i) => renderRightItem(
416
- item,
417
- i,
418
- {
419
- lang,
420
- githubRepoUrl,
421
- githubLabel,
422
- themeToggle,
423
- languageSwitcher,
424
- versionSwitcher,
425
- search,
426
- colorModeEnabled,
427
- hasLocales,
428
- },
429
- rightItemDispatch,
430
- ))}
458
+ {rightGroups}
431
459
  </div>
432
460
 
433
461
  <script dangerouslySetInnerHTML={{ __html: NAV_OVERFLOW_SCRIPT }} />
@@ -592,6 +620,13 @@ function stripCurrentVersionPrefix(path: string, currentVersion?: string): strin
592
620
 
593
621
  type RightItemContext = Omit<HeaderRightComponentProps, "item" | "index">;
594
622
 
623
+ function isHeaderIconItem(item: HeaderRightItem): boolean {
624
+ if (item.type === "trigger") return true;
625
+ if (item.type === "link") return item.icon === "github";
626
+ if (item.type !== "component") return false;
627
+ return ["github-link", "theme-toggle", "search"].includes(item.component);
628
+ }
629
+
595
630
  /**
596
631
  * Shared trigger-button shell for header-right items that dispatch a
597
632
  * CustomEvent on click. The legacy template used an inline `onclick`
@@ -620,7 +655,7 @@ function TriggerButton({
620
655
  key={`right-${index}`}
621
656
  id={id}
622
657
  type="button"
623
- class="flex items-center justify-center text-muted transition-colors hover:text-fg"
658
+ class="flex h-[40px] w-[40px] shrink-0 items-center justify-center text-muted transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2"
624
659
  aria-label={ariaLabel}
625
660
  {...inlineOnclick}
626
661
  >
@@ -730,7 +765,7 @@ const BASE_RIGHT_ITEM_DISPATCH: Readonly<Record<string, RightItemHandler>> = {
730
765
  href={ctx.githubRepoUrl}
731
766
  target="_blank"
732
767
  rel="noopener noreferrer"
733
- class="flex items-center justify-center text-muted transition-colors hover:text-fg"
768
+ class="flex h-[40px] w-[40px] shrink-0 items-center justify-center text-muted transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2"
734
769
  aria-label={ctx.githubLabel}
735
770
  title={ctx.githubLabel}
736
771
  >
@@ -782,7 +817,9 @@ const BASE_RIGHT_ITEM_DISPATCH: Readonly<Record<string, RightItemHandler>> = {
782
817
  href={item.href}
783
818
  target={isExternal ? "_blank" : undefined}
784
819
  rel={isExternal ? "noopener noreferrer" : undefined}
785
- class="flex items-center justify-center text-muted transition-colors hover:text-fg"
820
+ class={item.icon === "github"
821
+ ? "flex h-[40px] w-[40px] shrink-0 items-center justify-center text-muted transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2"
822
+ : "flex items-center justify-center text-muted transition-colors hover:text-fg"}
786
823
  aria-label={item.ariaLabel}
787
824
  title={label}
788
825
  >
@@ -14,6 +14,7 @@ import { useState, useEffect, useRef } from "preact/hooks";
14
14
  // collapse into duplicate import statements in an ejected copy.
15
15
  import { AFTER_NAVIGATE_EVENT, ensureNestedIslandPropsRefresh } from "../transitions/index.js";
16
16
  import { SidebarTree } from "../sidebar-tree-island/index.js";
17
+ import type { ThemeToggleLabels } from "../theme-toggle/index.js";
17
18
  import type { SidebarNavNode, SidebarRootMenuItem, SidebarLocaleLink } from "../sidebar/types.js";
18
19
  import type { ResolvedDateFormats } from "../settings.js";
19
20
 
@@ -57,6 +58,8 @@ export interface SidebarToggleProps {
57
58
  locale?: string;
58
59
  localeLinks?: SidebarLocaleLink[];
59
60
  themeDefaultMode?: "light" | "dark";
61
+ themeLabels?: ThemeToggleLabels;
62
+ themeRespectSystem?: boolean;
60
63
  /**
61
64
  * Forwarded verbatim to the hosted `<SidebarTree>`. This island is the one
62
65
  * nested under the persisted `<header>`, so on a same-locale soft navigation
@@ -79,6 +82,8 @@ export function SidebarToggle({
79
82
  locale,
80
83
  localeLinks,
81
84
  themeDefaultMode,
85
+ themeLabels,
86
+ themeRespectSystem,
82
87
  dateFormats,
83
88
  }: SidebarToggleProps) {
84
89
  // Initial state must match SSR (`open=false`) so the hydration DOM
@@ -131,6 +136,13 @@ export function SidebarToggle({
131
136
  // yank focus to the hamburger. Same guard the sibling document-level
132
137
  // shortcut in `sidebar-tree-island/index.tsx` already uses.
133
138
  if (event.isComposing) return;
139
+ // A nested popover (the Appearance menu, or its trigger button) that
140
+ // owns this Escape already called preventDefault() + stopPropagation()
141
+ // to consume it for itself — see theme-toggle/index.tsx. Respecting
142
+ // defaultPrevented here establishes layered Escape ownership: the first
143
+ // Escape closes only the nested layer, and only a second, unconsumed
144
+ // Escape reaches the drawer (zudolab/zudo-doc#4393).
145
+ if (event.defaultPrevented) return;
134
146
  if (event.key === "Escape") {
135
147
  setOpen(false);
136
148
  hamburgerRef.current?.focus();
@@ -255,6 +267,8 @@ export function SidebarToggle({
255
267
  locale={locale}
256
268
  localeLinks={localeLinks}
257
269
  themeDefaultMode={themeDefaultMode}
270
+ themeLabels={themeLabels}
271
+ themeRespectSystem={themeRespectSystem}
258
272
  dateFormats={dateFormats}
259
273
  />
260
274
  </div>
@@ -12,7 +12,7 @@ import { INDENT, BASE_PAD, connectorLeft, ConnectorLines, CategoryLinkIcon } fro
12
12
  import { ChevronRight, ChevronLeft, Search } from "../icons/index.js";
13
13
  // BARE ThemeToggle — renders inside the SidebarToggle island, so it must
14
14
  // NOT bring its own island wrapper.
15
- import { ThemeToggle } from "../theme-toggle/index.js";
15
+ import { ThemeToggle, type ThemeToggleLabels } from "../theme-toggle/index.js";
16
16
  import { smartBreakToHtml } from "../smart-break/index.js";
17
17
  // After zudolab/zudo-doc#1335 the host components also pull lifecycle event
18
18
  // names from the v2 transitions module rather than hard-coding literals.
@@ -184,6 +184,8 @@ export interface SidebarTreeProps {
184
184
  locale?: string;
185
185
  localeLinks?: SidebarLocaleLink[];
186
186
  themeDefaultMode?: "light" | "dark";
187
+ themeLabels?: ThemeToggleLabels;
188
+ themeRespectSystem?: boolean;
187
189
  /**
188
190
  * Per-role date patterns already resolved for this page's locale, serialized
189
191
  * into the island's `data-props` by the SSR wrappers (`sidebar-with-defaults`
@@ -197,13 +199,13 @@ export interface SidebarTreeProps {
197
199
  dateFormats?: ResolvedDateFormats;
198
200
  }
199
201
 
200
- function SidebarFooter({ links, themeDefaultMode }: { links?: SidebarLocaleLink[]; themeDefaultMode?: "light" | "dark" }) {
202
+ function SidebarFooter({ links, themeDefaultMode, themeLabels, themeRespectSystem }: { links?: SidebarLocaleLink[]; themeDefaultMode?: "light" | "dark"; themeLabels?: ThemeToggleLabels; themeRespectSystem?: boolean }) {
201
203
  if (!links && !themeDefaultMode) return null;
202
204
  return (
203
205
  // pb-[50vh] provides scroll room so the footer doesn't sit at the very bottom of the viewport
204
206
  <div className="lg:hidden flex items-center gap-hsp-md border-t border-muted px-hsp-sm py-vsp-xs pb-[50vh] text-small">
205
207
  {themeDefaultMode && (
206
- <ThemeToggle defaultMode={themeDefaultMode} pendingUntilHydrated={true} />
208
+ <ThemeToggle defaultMode={themeDefaultMode} labels={themeLabels} respectPrefersColorScheme={themeRespectSystem} pendingUntilHydrated={true} />
207
209
  )}
208
210
  {links && links.map((link, i) => (
209
211
  <span key={link.href} className="flex items-center gap-hsp-xs">
@@ -221,7 +223,7 @@ function SidebarFooter({ links, themeDefaultMode }: { links?: SidebarLocaleLink[
221
223
  );
222
224
  }
223
225
 
224
- export function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, backToMenuLabel, locale: localeProp, localeLinks, themeDefaultMode, dateFormats }: SidebarTreeProps) {
226
+ export function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, backToMenuLabel, locale: localeProp, localeLinks, themeDefaultMode, themeLabels, themeRespectSystem, dateFormats }: SidebarTreeProps) {
225
227
  const activeSlug = useActiveSlug(nodes, currentSlug, currentPath);
226
228
  const [query, setQuery] = useState("");
227
229
  const [showingRootMenu, setShowingRootMenu] = useState(false);
@@ -257,8 +259,8 @@ export function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, ba
257
259
  );
258
260
 
259
261
  const footer = useMemo(
260
- () => (localeLinks || themeDefaultMode) ? <SidebarFooter links={localeLinks} themeDefaultMode={themeDefaultMode} /> : null,
261
- [localeLinks, themeDefaultMode],
262
+ () => (localeLinks || themeDefaultMode) ? <SidebarFooter links={localeLinks} themeDefaultMode={themeDefaultMode} themeLabels={themeLabels} themeRespectSystem={themeRespectSystem} /> : null,
263
+ [localeLinks, themeDefaultMode, themeLabels, themeRespectSystem],
262
264
  );
263
265
 
264
266
  const noteTrayRoot = nodes.length === 1 && nodes[0]?.shape === "note-tray" ? nodes[0] : undefined;