@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.
@@ -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
- function AppearanceMenu({ id, labels, preference, resolved, activeIndex, triggerRef, rootRef, close, select, move }) {
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 placement = signal({ visibility: "hidden", left: "0px", top: "0px", right: "auto", bottom: "auto", margin: "0", width: "260px", "max-height": "none" });
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 = 8;
59
- const width = Math.max(0, Math.min(260, window.innerWidth - gap * 2));
60
- const desiredHeight = menu.scrollHeight || 280;
61
- const roomBelow = window.innerHeight - rect.bottom - gap * 2;
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 = roomBelow < desiredHeight && roomAbove > roomBelow;
64
- const maxHeight = Math.max(80, Math.min(desiredHeight, above ? roomAbove : roomBelow));
65
- const left = Math.max(gap, Math.min(rect.right - width, window.innerWidth - width - gap));
66
- const top = above ? Math.max(gap, rect.top - gap - maxHeight) : rect.bottom + gap;
67
- placement.value = { visibility: "visible", left: `${left}px`, top: `${top}px`, right: "auto", bottom: "auto", margin: "0", width: `${width}px`, "max-height": `${maxHeight}px` };
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) itemRefs[activeIndex.value]?.current?.focus();
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", position, true);
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", position, true);
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
- itemRefs[(next + preferences.length) % preferences.length]?.current?.focus();
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 overflow-y-auto rounded-lg border border-muted bg-surface p-hsp-xs text-fg shadow-lg",
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" : ""} hover:bg-accent/10 focus-visible:bg-accent/10 focus-visible:outline-2 focus-visible:outline-accent`),
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-accent", children: computed(() => preference.value === option ? "\u2713" : "") })
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("div", { class: "px-hsp-sm py-vsp-xs text-small text-muted", children: computed(() => labels.systemHelper.replace("{mode}", labels[resolved.value])) })
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 resolved = signal(defaultMode);
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, resolved, activeIndex, triggerRef, rootRef, close, select, move }) })
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,2 @@
1
+ /** Visual snapshot policy only; persistence and sidebar state keep their owners. */
2
+ export declare function buildChromeTransitionBootstrap(): string;
@@ -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 full-page loading overlay.
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 stays in the host's global stylesheet, and the component is not
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
- * Full-page loading overlay shown during view-transition navigations.
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 lives in the host project's `src/styles/global.css` (`.page-loading-overlay`,
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 ref={navRef} tabindex={-1}>
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"