@takazudo/zudo-doc 8.1.3 → 9.1.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 (64) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +6 -5
  3. package/dist/ai-chat-modal/index.js +1 -0
  4. package/dist/catalog.js +1 -1
  5. package/dist/chrome/derive.js +2 -2
  6. package/dist/compiled.css +109 -3
  7. package/dist/config.d.ts +13 -7
  8. package/dist/config.js +6 -0
  9. package/dist/design-token-panel-bootstrap-island.d.ts +1 -1
  10. package/dist/features.css +110 -0
  11. package/dist/footer/footer.js +1 -1
  12. package/dist/head-with-defaults/index.js +2 -2
  13. package/dist/html-preview-wrapper/preview-base.js +2 -2
  14. package/dist/icons/index.d.ts +2 -0
  15. package/dist/icons/index.js +36 -0
  16. package/dist/metainfo/doc-tags.js +3 -0
  17. package/dist/nav-indexing/tag-nav.js +7 -1
  18. package/dist/nav-scope/index.d.ts +1 -1
  19. package/dist/nav-scope/index.js +7 -8
  20. package/dist/page-loading/page-loading-overlay.d.ts +5 -1
  21. package/dist/page-loading/page-loading-overlay.js +1 -1
  22. package/dist/page-loading.css +2 -1
  23. package/dist/plugins/zdtp-loader.d.ts +1 -0
  24. package/dist/plugins/zdtp-loader.js +5 -1
  25. package/dist/preset.d.ts +10 -4
  26. package/dist/preset.js +11 -1
  27. package/dist/route-context-payload/index.d.ts +4 -1
  28. package/dist/route-context-payload/index.js +6 -0
  29. package/dist/settings-defaults.js +3 -1
  30. package/dist/settings.d.ts +9 -5
  31. package/dist/sidebar-tree-island/branch-preview.d.ts +19 -0
  32. package/dist/sidebar-tree-island/branch-preview.js +199 -0
  33. package/dist/sidebar-tree-island/index.js +82 -33
  34. package/dist/sidebar-tree-island/scope-state.d.ts +15 -6
  35. package/dist/sidebar-tree-island/scope-state.js +30 -9
  36. package/dist/sidebar-utils/index.js +5 -2
  37. package/dist/theme/theme-pack-provider.d.ts +4 -2
  38. package/dist/theme/theme-pack-provider.js +2 -1
  39. package/dist/theme-cli/config-io.js +26 -3
  40. package/dist/theme-cli/list.js +1 -1
  41. package/dist/theme-packs/default/meta.json +2 -2
  42. package/dist/theme-packs/graphite/meta.json +37 -0
  43. package/dist/theme-packs/graphite/pack.css +401 -0
  44. package/dist/theme-packs/quiet-slate/meta.json +37 -0
  45. package/dist/theme-packs/quiet-slate/pack.css +401 -0
  46. package/dist/theme-packs/warm-mineral/meta.json +37 -0
  47. package/dist/theme-packs/warm-mineral/pack.css +401 -0
  48. package/dist/theme-packs-registry/configured-theme-pack.d.ts +21 -0
  49. package/dist/theme-packs-registry/configured-theme-pack.js +13 -0
  50. package/dist/theme-packs-registry/index.d.ts +1 -0
  51. package/dist/theme-packs-registry/index.js +6 -0
  52. package/dist/theme-packs-registry/resolve-enabled-packs.d.ts +13 -6
  53. package/dist/theme-packs-registry/resolve-enabled-packs.js +15 -5
  54. package/dist/tree-nav-shared/index.d.ts +5 -1
  55. package/dist/tree-nav-shared/index.js +7 -3
  56. package/dist/wind.json +6 -1
  57. package/dist/zdtp-loader.d.ts +1 -1
  58. package/eject/footer/footer.tsx +1 -1
  59. package/eject/header/gen-nav-overflow-script.mjs +2 -2
  60. package/eject/page-loading/page-loading-overlay.tsx +7 -3
  61. package/eject/sidebar-tree-island/branch-preview.ts +247 -0
  62. package/eject/sidebar-tree-island/index.tsx +86 -19
  63. package/eject/sidebar-tree-island/scope-state.ts +40 -10
  64. package/package.json +21 -20
@@ -0,0 +1,199 @@
1
+ import { computed, signal } from "@takazudo/zfb/zudo-react";
2
+ const TIP_ATTR = "data-sidebar-tip";
3
+ const TIP_DETAIL_ATTR = "data-sidebar-tip-detail";
4
+ const TRIGGER_SELECTOR = `[${TIP_ATTR}]`;
5
+ const FRAME_SELECTOR = "[data-sidebar-focus]";
6
+ const POINTER_SHOW_DELAY_MS = 250;
7
+ const HIDE_DELAY_MS = 120;
8
+ const GAP_PX = 6;
9
+ const VIEWPORT_MARGIN_PX = 8;
10
+ let instanceCount = 0;
11
+ const isHoverPointer = (event) => {
12
+ const type = event.pointerType;
13
+ return type === "mouse" || type === "pen";
14
+ };
15
+ const isFocusVisible = (element) => {
16
+ try {
17
+ return element.matches(":focus-visible");
18
+ } catch {
19
+ return true;
20
+ }
21
+ };
22
+ const isActionableFrame = (element) => element.matches(FRAME_SELECTOR) && element.getAttribute("aria-disabled") !== "true";
23
+ const previewKey = (element) => isActionableFrame(element) ? element.dataset.sidebarFocusScope || null : null;
24
+ function createBranchPreview(navRef) {
25
+ const pointerKey = signal(null);
26
+ const focusKey = signal(null);
27
+ const active = computed(() => pointerKey.value ?? focusKey.value);
28
+ let tooltip = null;
29
+ let owner = null;
30
+ let showTimer;
31
+ let hideTimer;
32
+ const hideTooltip = () => {
33
+ clearTimeout(showTimer);
34
+ clearTimeout(hideTimer);
35
+ if (owner?.getAttribute("aria-describedby") === tooltip?.id) owner?.removeAttribute("aria-describedby");
36
+ owner = null;
37
+ if (tooltip) tooltip.hidden = true;
38
+ };
39
+ const clear = () => {
40
+ pointerKey.value = null;
41
+ focusKey.value = null;
42
+ hideTooltip();
43
+ };
44
+ const place = (trigger, tip) => {
45
+ tip.style.left = "0px";
46
+ tip.style.top = "0px";
47
+ const anchor = trigger.getBoundingClientRect();
48
+ const box = tip.getBoundingClientRect();
49
+ const maxLeft = window.innerWidth - box.width - VIEWPORT_MARGIN_PX;
50
+ const edge = Math.max(anchor.right, navRef.current?.getBoundingClientRect().right ?? anchor.right);
51
+ let left = edge + GAP_PX;
52
+ let top = anchor.top + anchor.height / 2 - box.height / 2;
53
+ if (left > maxLeft) {
54
+ left = Math.max(VIEWPORT_MARGIN_PX, Math.min(anchor.left + anchor.width / 2 - box.width / 2, maxLeft));
55
+ top = anchor.bottom + GAP_PX;
56
+ if (top + box.height > window.innerHeight - VIEWPORT_MARGIN_PX) top = anchor.top - GAP_PX - box.height;
57
+ }
58
+ top = Math.max(VIEWPORT_MARGIN_PX, Math.min(top, window.innerHeight - box.height - VIEWPORT_MARGIN_PX));
59
+ tip.style.left = `${Math.round(left)}px`;
60
+ tip.style.top = `${Math.round(top)}px`;
61
+ };
62
+ const showTooltip = (trigger) => {
63
+ if (!tooltip) return;
64
+ clearTimeout(hideTimer);
65
+ if (owner && owner !== trigger && owner.getAttribute("aria-describedby") === tooltip.id) owner.removeAttribute("aria-describedby");
66
+ owner = trigger;
67
+ const title = document.createElement("strong");
68
+ title.textContent = trigger.getAttribute(TIP_ATTR) ?? "";
69
+ const parts = [title];
70
+ const detail = trigger.getAttribute(TIP_DETAIL_ATTR);
71
+ if (detail) {
72
+ const line = document.createElement("span");
73
+ line.textContent = detail;
74
+ parts.push(line);
75
+ }
76
+ tooltip.replaceChildren(...parts);
77
+ tooltip.hidden = false;
78
+ trigger.setAttribute("aria-describedby", tooltip.id);
79
+ place(trigger, tooltip);
80
+ };
81
+ const scheduleHide = () => {
82
+ clearTimeout(showTimer);
83
+ clearTimeout(hideTimer);
84
+ hideTimer = setTimeout(hideTooltip, HIDE_DELAY_MS);
85
+ };
86
+ const triggerOf = (target) => {
87
+ const nav = navRef.current;
88
+ if (!nav || !(target instanceof Element)) return null;
89
+ const trigger = target.closest(TRIGGER_SELECTOR);
90
+ return trigger && nav.contains(trigger) ? trigger : null;
91
+ };
92
+ const activate = () => {
93
+ const tip = document.createElement("div");
94
+ tip.id = `zd-sidebar-tip-${++instanceCount}`;
95
+ tip.setAttribute("role", "tooltip");
96
+ tip.setAttribute("data-sidebar-tooltip", "");
97
+ tip.hidden = true;
98
+ document.body.append(tip);
99
+ tooltip = tip;
100
+ const onPointerOver = (event) => {
101
+ const trigger = triggerOf(event.target);
102
+ if (!trigger || !isHoverPointer(event) || trigger.contains(event.relatedTarget)) return;
103
+ pointerKey.value = previewKey(trigger);
104
+ clearTimeout(hideTimer);
105
+ clearTimeout(showTimer);
106
+ if (owner === trigger && !tip.hidden) return;
107
+ showTimer = setTimeout(() => showTooltip(trigger), POINTER_SHOW_DELAY_MS);
108
+ };
109
+ const onPointerOut = (event) => {
110
+ const trigger = triggerOf(event.target);
111
+ const next = event.relatedTarget;
112
+ if (!trigger || trigger.contains(next)) return;
113
+ pointerKey.value = null;
114
+ if (next && tip.contains(next)) return;
115
+ if (owner === trigger && trigger === document.activeElement && isFocusVisible(trigger)) return;
116
+ scheduleHide();
117
+ };
118
+ const onFocusIn = (event) => {
119
+ const trigger = triggerOf(event.target);
120
+ if (!trigger || !isFocusVisible(trigger)) return;
121
+ focusKey.value = previewKey(trigger);
122
+ clearTimeout(showTimer);
123
+ showTooltip(trigger);
124
+ };
125
+ const onFocusOut = (event) => {
126
+ const trigger = triggerOf(event.target);
127
+ if (!trigger) return;
128
+ focusKey.value = null;
129
+ if (owner === trigger) scheduleHide();
130
+ };
131
+ const onTipEnter = () => clearTimeout(hideTimer);
132
+ const onTipLeave = (event) => {
133
+ const next = event.relatedTarget;
134
+ if (owner && next && owner.contains(next)) return;
135
+ if (owner && owner === document.activeElement && isFocusVisible(owner)) return;
136
+ scheduleHide();
137
+ };
138
+ const onKeyDown = (event) => {
139
+ const key = event;
140
+ if (key.key !== "Escape" || key.isComposing || tip.hidden) return;
141
+ const target = key.target;
142
+ if (target instanceof Element && target !== document.body && !navRef.current?.contains(target) && !tip.contains(target)) {
143
+ hideTooltip();
144
+ pointerKey.value = null;
145
+ return;
146
+ }
147
+ key.preventDefault();
148
+ key.stopPropagation();
149
+ hideTooltip();
150
+ pointerKey.value = null;
151
+ };
152
+ let scrollFrame = 0;
153
+ const onScroll = () => {
154
+ if (tip.hidden || scrollFrame) return;
155
+ scrollFrame = requestAnimationFrame(() => {
156
+ scrollFrame = 0;
157
+ if (tip.hidden) return;
158
+ if (owner?.isConnected) place(owner, tip);
159
+ else hideTooltip();
160
+ });
161
+ };
162
+ const drawer = navRef.current ? navRef.current.closest("[data-zd-mobile-sidebar]") : document.querySelector("[data-zd-mobile-sidebar]");
163
+ const observer = drawer ? new MutationObserver(() => {
164
+ if (drawer.hasAttribute("inert")) clear();
165
+ }) : void 0;
166
+ observer?.observe(drawer, { attributes: true, attributeFilter: ["inert"] });
167
+ const media = window.matchMedia?.("(min-width: 1024px)");
168
+ const onBreakpoint = () => clear();
169
+ media?.addEventListener?.("change", onBreakpoint);
170
+ document.addEventListener("pointerover", onPointerOver);
171
+ document.addEventListener("pointerout", onPointerOut);
172
+ document.addEventListener("focusin", onFocusIn);
173
+ document.addEventListener("focusout", onFocusOut);
174
+ document.addEventListener("keydown", onKeyDown, true);
175
+ document.addEventListener("scroll", onScroll, true);
176
+ tip.addEventListener("pointerenter", onTipEnter);
177
+ tip.addEventListener("pointerleave", onTipLeave);
178
+ return () => {
179
+ cancelAnimationFrame(scrollFrame);
180
+ clear();
181
+ observer?.disconnect();
182
+ media?.removeEventListener?.("change", onBreakpoint);
183
+ document.removeEventListener("pointerover", onPointerOver);
184
+ document.removeEventListener("pointerout", onPointerOut);
185
+ document.removeEventListener("focusin", onFocusIn);
186
+ document.removeEventListener("focusout", onFocusOut);
187
+ document.removeEventListener("keydown", onKeyDown, true);
188
+ document.removeEventListener("scroll", onScroll, true);
189
+ tip.remove();
190
+ tooltip = null;
191
+ };
192
+ };
193
+ return { active, clear, activate };
194
+ }
195
+ export {
196
+ TIP_ATTR,
197
+ TIP_DETAIL_ATTR,
198
+ createBranchPreview
199
+ };
@@ -10,15 +10,17 @@ import {
10
10
  import {
11
11
  INDENT,
12
12
  BASE_PAD,
13
+ CONNECTOR_OFFSET,
13
14
  connectorLeft,
14
15
  ConnectorLines,
15
16
  CategoryLinkIcon
16
17
  } from "../tree-nav-shared/index.js";
17
- import { ChevronRight, ChevronLeft, Search } from "../icons/index.js";
18
+ import { ChevronRight, ChevronLeft, Search, FocusFrame, RestoreArrow } from "../icons/index.js";
18
19
  import { ThemeToggle } from "../theme-toggle/index.js";
19
20
  import { smartBreakToHtml } from "../smart-break/index.js";
20
21
  import { AFTER_NAVIGATE_EVENT } from "../transitions/index.js";
21
22
  import { useSidebarScope } from "./scope-state.js";
23
+ import { createBranchPreview } from "./branch-preview.js";
22
24
  import { filterTree } from "../sidebar-filter/index.js";
23
25
  import { findActiveSlug, normalizePath } from "../sidebar-active-slug/index.js";
24
26
  import {
@@ -36,6 +38,7 @@ import {
36
38
  import { formatMonthDay } from "../format-date/index.js";
37
39
  const STORAGE_KEY = "zd-sidebar-open";
38
40
  const GROUPED_TRAY_ITEM_DEPTH = 3;
41
+ const TOGGLE_BOX_SIZE = "1.5rem";
39
42
  function ToggleChevron({
40
43
  isExpanded,
41
44
  className
@@ -56,6 +59,9 @@ function padLeft(depth, forCategory) {
56
59
  return `calc(${BASE_PAD} + ${forCategory ? "0.15rem" : "0rem"})`;
57
60
  return `calc(${depth} * ${INDENT} + 1.25rem + 5px)`;
58
61
  }
62
+ function toggleGuideLeft(depth) {
63
+ return `calc(${padLeft(depth, true)} + ${TOGGLE_BOX_SIZE} / 2 - 0.5px)`;
64
+ }
59
65
  function getOpenSet() {
60
66
  try {
61
67
  const raw = sessionStorage.getItem(STORAGE_KEY);
@@ -207,13 +213,31 @@ function SidebarTree({
207
213
  const scope = getScope();
208
214
  const activeSlug = useActiveSlug(navigation?.roots ?? nodes, currentSlug, currentPath);
209
215
  const query = signal("");
210
- const { selected, selectedNodes, broader, labels, canRestore, changeScope, save, navRef, scopeControls } = useSidebarScope({
216
+ const { selected, selectedNodes, broader, labels, canRestore, changeScope, save, navRef, scopeControls: baseScopeControls } = useSidebarScope({
211
217
  nodes,
212
218
  navigation,
213
219
  activeSlug,
214
220
  query,
215
221
  locale: localeProp ?? localeLinks?.find((link) => link.active)?.code ?? "en"
216
222
  });
223
+ const branchPreview = createBranchPreview(navRef);
224
+ const scopeControls = baseScopeControls ? { ...baseScopeControls, preview: branchPreview.active } : void 0;
225
+ if (navigation) {
226
+ scope.onActivate(() => {
227
+ const dispose = branchPreview.activate();
228
+ const onNavigate = () => branchPreview.clear();
229
+ document.addEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
230
+ return () => {
231
+ document.removeEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
232
+ dispose();
233
+ };
234
+ });
235
+ scope.effect(() => {
236
+ void selected.value;
237
+ void query.value;
238
+ branchPreview.clear();
239
+ });
240
+ }
217
241
  const showingRootMenu = signal(false);
218
242
  const filterRef = { current: null };
219
243
  const filterPlaceholder = signal("Filter...");
@@ -330,15 +354,11 @@ function SidebarTree({
330
354
  type: "button",
331
355
  "data-sidebar-restore": true,
332
356
  "on:click": () => changeScope(null),
333
- title: labels.restore,
334
357
  "aria-label": labels.restore,
358
+ "data-sidebar-tip": labels.restoreTip,
335
359
  style: "width:40px;height:40px",
336
- class: "grid place-items-center shrink-0 rounded text-muted hover:text-fg focus-visible:text-fg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
337
- children: /* @__PURE__ */ jsxs("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "aria-hidden": "true", children: [
338
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
339
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "6" }),
340
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2" })
341
- ] })
360
+ class: "grid place-items-center shrink-0 rounded text-muted hover:text-accent hover:bg-surface focus-visible:text-accent focus-visible:bg-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
361
+ children: /* @__PURE__ */ jsx(RestoreArrow, { class: "h-[1.0625rem] w-[1.0625rem]" })
342
362
  }
343
363
  ) })
344
364
  ] }) }) }) : null,
@@ -478,6 +498,7 @@ function TrayItem({
478
498
  isLast,
479
499
  showDate = false,
480
500
  depth = 1,
501
+ guideLeft,
481
502
  locale,
482
503
  dateFormats
483
504
  }) {
@@ -491,7 +512,9 @@ function TrayItem({
491
512
  {
492
513
  depth,
493
514
  isLast,
494
- topPad: "var(--spacing-vsp-2xs)"
515
+ topPad: "var(--spacing-vsp-2xs)",
516
+ left: guideLeft,
517
+ width: guideLeft ? `calc(${padLeft(depth, false)} - ${CONNECTOR_OFFSET} - ${guideLeft})` : void 0
495
518
  }
496
519
  ),
497
520
  /* @__PURE__ */ jsxs(
@@ -536,14 +559,16 @@ function TrayItem({
536
559
  function ConnectorLinesLive({
537
560
  depth,
538
561
  isLast,
539
- topPad
562
+ topPad,
563
+ left,
564
+ width
540
565
  }) {
541
566
  return /* @__PURE__ */ jsx(
542
567
  Show,
543
568
  {
544
569
  when: isLast,
545
- fallback: () => /* @__PURE__ */ jsx(ConnectorLines, { depth, isLast: false, topPad }),
546
- children: () => /* @__PURE__ */ jsx(ConnectorLines, { depth, isLast: true, topPad })
570
+ fallback: () => /* @__PURE__ */ jsx(ConnectorLines, { depth, isLast: false, topPad, left, width }),
571
+ children: () => /* @__PURE__ */ jsx(ConnectorLines, { depth, isLast: true, topPad, left, width })
547
572
  }
548
573
  );
549
574
  }
@@ -647,20 +672,34 @@ function TrayGroupNode({
647
672
  }
648
673
  )
649
674
  ] }),
650
- /* @__PURE__ */ jsx(Show, { when: expanded, children: () => /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(For, { each: items, by: (item) => item.slug, children: (item, index) => /* @__PURE__ */ jsx(
651
- TrayItem,
652
- {
653
- item,
654
- currentSlug,
655
- isLast: computed(
656
- () => index.value === items.value.length - 1
657
- ),
658
- showDate: true,
659
- locale,
660
- dateFormats,
661
- depth: GROUPED_TRAY_ITEM_DEPTH
662
- }
663
- ) }) }) })
675
+ /* @__PURE__ */ jsx(Show, { when: expanded, children: () => /* @__PURE__ */ jsxs("div", { class: "relative", children: [
676
+ /* @__PURE__ */ jsx(
677
+ "div",
678
+ {
679
+ class: "absolute border-l border-dashed border-muted",
680
+ style: {
681
+ left: toggleGuideLeft(1),
682
+ top: "calc(-1 * var(--spacing-vsp-xs))",
683
+ height: "var(--spacing-vsp-xs)"
684
+ }
685
+ }
686
+ ),
687
+ /* @__PURE__ */ jsx(For, { each: items, by: (item) => item.slug, children: (item, index) => /* @__PURE__ */ jsx(
688
+ TrayItem,
689
+ {
690
+ item,
691
+ currentSlug,
692
+ isLast: computed(
693
+ () => index.value === items.value.length - 1
694
+ ),
695
+ showDate: true,
696
+ locale,
697
+ dateFormats,
698
+ depth: GROUPED_TRAY_ITEM_DEPTH,
699
+ guideLeft: toggleGuideLeft(1)
700
+ }
701
+ ) })
702
+ ] }) })
664
703
  ]
665
704
  }
666
705
  );
@@ -753,6 +792,9 @@ function CategoryNode({
753
792
  if (scopeControls) open.value = scopeControls.isOpen(occurrenceKey(), scopeControls.defaultOpen(node.value));
754
793
  });
755
794
  const expanded = computed(() => forceOpen.value || open.value);
795
+ const focusedRoot = computed(() => !!scopeControls?.isFocusedRoot(node.value.occurrenceId));
796
+ const previewed = computed(() => !!node.value.occurrenceId && scopeControls?.preview.value === node.value.occurrenceId);
797
+ const arrived = computed(() => !!node.value.occurrenceId && scopeControls?.arrived.value === node.value.occurrenceId);
756
798
  const focusButton = scopeControls ? /* @__PURE__ */ jsx(
757
799
  "button",
758
800
  {
@@ -760,16 +802,22 @@ function CategoryNode({
760
802
  "data-sidebar-focus": true,
761
803
  "data-sidebar-focus-scope": computed(() => node.value.occurrenceId ?? ""),
762
804
  "on:click": () => scopeControls.focus(node.value),
763
- "aria-label": computed(() => `${scopeControls.focusLabel}: ${node.value.label}`),
764
- title: computed(() => `${scopeControls.focusLabel}: ${node.value.label}`),
765
- class: computed(() => `ml-auto shrink-0 rounded px-hsp-xs py-vsp-2xs ${active.value ? "text-bg hover:bg-bg/10 focus-visible:bg-bg/10" : "text-muted hover:bg-surface hover:text-fg focus-visible:bg-surface focus-visible:text-fg"}`),
766
- children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u25CE" })
805
+ "aria-disabled": computed(() => focusedRoot.value ? "true" : null),
806
+ tabindex: computed(() => focusedRoot.value ? -1 : null),
807
+ "aria-label": computed(() => focusedRoot.value ? scopeControls.labels.focused(node.value.label) : scopeControls.labels.focus(node.value.label)),
808
+ "data-sidebar-tip": computed(() => focusedRoot.value ? scopeControls.labels.focusedTip : scopeControls.labels.focusTip),
809
+ "data-sidebar-tip-detail": computed(() => node.value.label),
810
+ class: computed(() => `ml-auto grid shrink-0 place-items-center rounded w-[1.875rem] h-[1.875rem] ${focusedRoot.value ? `cursor-default opacity-50 ${active.value ? "text-bg" : "text-muted"}` : active.value ? "text-bg hover:bg-bg/10 focus-visible:bg-bg/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent" : "text-muted hover:bg-accent/10 hover:text-accent focus-visible:bg-accent/10 focus-visible:text-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"}`),
811
+ children: /* @__PURE__ */ jsx(FocusFrame, { class: "h-[1.125rem] w-[1.125rem]" })
767
812
  }
768
813
  ) : null;
769
814
  const paddingLeft = padLeft(depth, true);
770
815
  return /* @__PURE__ */ jsxs(
771
816
  "div",
772
817
  {
818
+ "data-sidebar-branch": true,
819
+ "data-sidebar-preview": computed(() => previewed.value ? "" : null),
820
+ "data-sidebar-arrived": computed(() => arrived.value ? "" : null),
773
821
  class: computed(
774
822
  () => `${depth === 0 ? "border-t border-muted first:border-t-0" : ""} ${depth >= 1 && !isLast.value ? "relative" : ""}`
775
823
  ),
@@ -782,12 +830,13 @@ function CategoryNode({
782
830
  "div",
783
831
  {
784
832
  class: "absolute border-l border-solid border-muted z-local-1",
833
+ "data-tree-connector": true,
785
834
  style: { left: connectorLeft(depth), top: "0px", bottom: "0px" }
786
835
  }
787
836
  )
788
837
  }
789
838
  ),
790
- /* @__PURE__ */ jsxs("div", { class: "relative", children: [
839
+ /* @__PURE__ */ jsxs("div", { class: "relative", "data-sidebar-row": true, "data-sidebar-current": computed(() => active.value ? "" : null), children: [
791
840
  /* @__PURE__ */ jsx(
792
841
  ConnectorLinesLive,
793
842
  {
@@ -886,7 +935,7 @@ function CategoryNode({
886
935
  }
887
936
  )
888
937
  ] }),
889
- /* @__PURE__ */ jsx(Show, { when: expanded, children: () => /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
938
+ /* @__PURE__ */ jsx(Show, { when: expanded, children: () => /* @__PURE__ */ jsx("div", { "data-sidebar-children": true, children: /* @__PURE__ */ jsx(
890
939
  NodeList,
891
940
  {
892
941
  nodes: computed(() => node.value.children),
@@ -1,8 +1,21 @@
1
1
  import { type ReadonlySignal, type Ref } from "@takazudo/zfb/zudo-react";
2
2
  import type { SidebarNavNode, SidebarNavigationContext } from "../sidebar/types.js";
3
+ export interface ScopeLabels {
4
+ broaden: string;
5
+ restore: string;
6
+ restoreTip: string;
7
+ focus: (label: string) => string;
8
+ focusTip: string;
9
+ focused: (label: string) => string;
10
+ focusedTip: string;
11
+ }
3
12
  export interface ScopeControls {
4
13
  focus: (node: SidebarNavNode) => void;
5
- focusLabel: string;
14
+ labels: ScopeLabels;
15
+ /** True when the unfiltered scope is exactly this one occurrence as its single root. */
16
+ isFocusedRoot: (occurrenceId: string | undefined) => boolean;
17
+ /** The single root a scope change just arrived at, briefly; null otherwise. */
18
+ arrived: ReadonlySignal<string | null>;
6
19
  /** Effective disclosure: the user's explicit choice, else revealed/scope root, else `baseline`. */
7
20
  isOpen: (key: string, baseline: boolean) => boolean;
8
21
  /** Records an explicit user disclosure choice. */
@@ -28,11 +41,7 @@ export declare function useSidebarScope({ nodes, navigation, activeSlug, query,
28
41
  selected: import("@takazudo/zfb/zudo-react").Signal<string | null>;
29
42
  selectedNodes: ReadonlySignal<SidebarNavNode[]>;
30
43
  broader: ReadonlySignal<string | null>;
31
- labels: {
32
- broaden: string;
33
- restore: string;
34
- focus: string;
35
- };
44
+ labels: ScopeLabels;
36
45
  canRestore: ReadonlySignal<boolean>;
37
46
  changeScope: (id: string | null) => void;
38
47
  save: (broadcast?: boolean) => void;
@@ -3,6 +3,7 @@ import { broaderSidebarScope, sidebarScopeNodes, reconcileSidebarScope, indexSid
3
3
  import { getNoteTrayItems, groupItems } from "../note-tray-model/index.js";
4
4
  import { AFTER_NAVIGATE_EVENT } from "../transitions/index.js";
5
5
  const STATE_VERSION = 2;
6
+ const ARRIVAL_MS = 700;
6
7
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7
8
  function useSidebarScope({ nodes, navigation, activeSlug, query, locale }) {
8
9
  const selected = signal(null);
@@ -14,13 +15,23 @@ function useSidebarScope({ nodes, navigation, activeSlug, query, locale }) {
14
15
  const syncEvent = "zd:sidebar-scope";
15
16
  const labels = locale === "ja" ? {
16
17
  broaden: "\u30C4\u30EA\u30FC\u3092\u5E83\u3052\u308B",
17
- restore: "\u73FE\u5728\u306E\u30DA\u30FC\u30B8\u306E\u30C4\u30EA\u30FC\u306B\u623B\u3059",
18
- focus: "\u3053\u306E\u679D\u3060\u3051\u3092\u8868\u793A"
18
+ restore: "\u73FE\u5728\u306E\u30DA\u30FC\u30B8\u306E\u30B5\u30A4\u30C9\u30D0\u30FC\u306B\u623B\u3059",
19
+ restoreTip: "\u73FE\u5728\u306E\u30DA\u30FC\u30B8\u306E\u30B5\u30A4\u30C9\u30D0\u30FC\u306B\u623B\u3059",
20
+ focus: (label) => `\u30B5\u30A4\u30C9\u30D0\u30FC\u306B\u300C${label}\u300D\u3060\u3051\u3092\u8868\u793A`,
21
+ focusTip: "\u3053\u3053\u3060\u3051\u3092\u8868\u793A",
22
+ focused: (label) => `\u3059\u3067\u306B\u300C${label}\u300D\u3060\u3051\u3092\u8868\u793A\u3057\u3066\u3044\u307E\u3059`,
23
+ focusedTip: "\u3059\u3067\u306B\u3053\u3053\u3060\u3051\u3092\u8868\u793A\u3057\u3066\u3044\u307E\u3059"
19
24
  } : {
20
25
  broaden: "Broaden tree",
21
- restore: "Restore current page\u2019s local tree",
22
- focus: "Show only this branch"
26
+ restore: "Restore current page\u2019s sidebar",
27
+ restoreTip: "Restore this page\u2019s sidebar",
28
+ focus: (label) => `Focus sidebar on ${label}`,
29
+ focusTip: "Focus sidebar here",
30
+ focused: (label) => `Sidebar is already focused on ${label}`,
31
+ focusedTip: "Already focused here"
23
32
  };
33
+ const arrived = signal(null);
34
+ let arrivalTimer;
24
35
  const selectedNodes = computed(() => navigation ? sidebarScopeNodes(navigation, nodes, selected.value) : nodes);
25
36
  const broader = computed(() => navigation ? broaderSidebarScope(navigation, nodes, selected.value) : null);
26
37
  const occurrenceIndex = navigation ? indexSidebarOccurrences(navigation.roots) : /* @__PURE__ */ new Map();
@@ -87,17 +98,23 @@ function useSidebarScope({ nodes, navigation, activeSlug, query, locale }) {
87
98
  revealed.value = /* @__PURE__ */ new Set([...revealed.value, ...withAncestors(previous), ...withAncestors(id)]);
88
99
  }
89
100
  save();
101
+ clearTimeout(arrivalTimer);
102
+ arrived.value = id === null ? null : scopeRoot.value ?? null;
103
+ if (arrived.value !== null) arrivalTimer = setTimeout(() => {
104
+ arrived.value = null;
105
+ }, ARRIVAL_MS);
90
106
  queueMicrotask(() => {
91
107
  const nav = navRef.current;
92
- const controls = [...nav?.querySelectorAll("[data-sidebar-focus]") ?? []];
108
+ const controls = [...nav?.querySelectorAll('[data-sidebar-focus]:not([aria-disabled="true"])') ?? []];
93
109
  const activeBranch = controls.reverse().find((button) => {
94
110
  const node = occurrenceIndex.get(button.dataset.sidebarFocusScope ?? "");
95
111
  const contains = (n) => n.slug === activeSlug.value || n.children.some(contains);
96
112
  return node && contains(node);
97
113
  });
98
- const rootLink = id && id !== SIDEBAR_FOREST_SCOPE && broader.value !== null ? nav?.querySelector("[data-sidebar-tree-root] a") : null;
114
+ const rootAction = nav?.querySelector("[data-sidebar-tree-root] a") ?? nav?.querySelector("[data-sidebar-tree-root] button[aria-expanded]");
115
+ const rootLink = id && id !== SIDEBAR_FOREST_SCOPE && broader.value !== null ? rootAction : null;
99
116
  const currentLink = nav?.querySelector('[aria-current="page"]');
100
- const control = (id === null ? currentLink : rootLink) ?? nav?.querySelector("[data-sidebar-broaden]") ?? activeBranch ?? currentLink ?? nav?.querySelector("[data-sidebar-focus], [data-sidebar-tree-root] a, input");
117
+ const control = (id === null ? currentLink : rootLink) ?? nav?.querySelector("[data-sidebar-broaden]") ?? activeBranch ?? currentLink ?? rootAction ?? nav?.querySelector('[data-sidebar-focus]:not([aria-disabled="true"]), [data-sidebar-tree-root] a, [data-sidebar-tree-root] button[aria-expanded], input');
101
118
  const target = control;
102
119
  let viewport = nav?.parentElement;
103
120
  while (viewport && viewport !== document.body && viewport.scrollHeight <= viewport.clientHeight) viewport = viewport.parentElement;
@@ -110,11 +127,14 @@ function useSidebarScope({ nodes, navigation, activeSlug, query, locale }) {
110
127
  control?.focus({ preventScroll: true });
111
128
  });
112
129
  };
130
+ const isFocusedRoot = (occurrenceId) => occurrenceId !== void 0 && scopeRoot.value === occurrenceId;
113
131
  const scopeControls = navigation ? {
114
132
  focus: (node) => {
115
- if (node.occurrenceId) changeScope(node.occurrenceId);
133
+ if (node.occurrenceId && !isFocusedRoot(node.occurrenceId)) changeScope(node.occurrenceId);
116
134
  },
117
- focusLabel: labels.focus,
135
+ labels,
136
+ isFocusedRoot,
137
+ arrived,
118
138
  defaultOpen,
119
139
  defaultGroupOpen,
120
140
  isOpen: (key, baseline) => expansion.value[key] ?? automaticOpen(key, baseline),
@@ -161,6 +181,7 @@ function useSidebarScope({ nodes, navigation, activeSlug, query, locale }) {
161
181
  document.addEventListener(syncEvent, sync);
162
182
  document.addEventListener(AFTER_NAVIGATE_EVENT, navigate);
163
183
  return () => {
184
+ clearTimeout(arrivalTimer);
164
185
  document.removeEventListener(syncEvent, sync);
165
186
  document.removeEventListener(AFTER_NAVIGATE_EVENT, navigate);
166
187
  };
@@ -65,14 +65,17 @@ function resolveItem(item, tree, lang) {
65
65
  }
66
66
  }
67
67
  }
68
+ function matchesCategory(slug, match) {
69
+ return slug === match || slug.startsWith(match + "/");
70
+ }
68
71
  function filterByCategory(tree, categoryMatch, explicitPrefixes) {
69
72
  if (!categoryMatch) return tree;
70
73
  if (categoryMatch === "!") {
71
74
  return tree.filter(
72
- (node) => !explicitPrefixes.some((prefix) => node.slug.startsWith(prefix))
75
+ (node) => !explicitPrefixes.some((prefix) => matchesCategory(node.slug, prefix))
73
76
  );
74
77
  }
75
- return tree.filter((node) => node.slug.startsWith(categoryMatch));
78
+ return tree.filter((node) => matchesCategory(node.slug, categoryMatch));
76
79
  }
77
80
  function buildSidebarForSection(docs, lang, categoryMatch, categoryMeta, sidebars, buildNavTree, explicitCategoryPrefixes) {
78
81
  const tree = buildNavTree(docs, lang, categoryMeta);
@@ -30,7 +30,8 @@ export declare const THEME_PACK_LOAD_WATCHDOG_MS = 2000;
30
30
  */
31
31
  export declare const THEME_PACK_LATCH_CSS = "html[data-zd-theme-pack-loading] body{visibility:hidden}";
32
32
  export interface ThemePackProviderProps {
33
- /** The build-configured pack slug (`settings.themePack`, default `"default"`). */
33
+ /** The build-configured pack slug (`settings.themePack`, normalized by
34
+ * `resolveConfiguredThemePack` — `"graphite"` when omitted). */
34
35
  configuredSlug: string;
35
36
  /**
36
37
  * Ordered `{ slug → meta.version }` map of the ENABLED packs — build with
@@ -55,7 +56,8 @@ export declare function themePackVersionMap(registry: ThemePackRegistry): Record
55
56
  * `packageOwnedRoutes: false` host that builds its own payload).
56
57
  */
57
58
  export declare function resolveThemePackSsrSlug(registry: ThemePackRegistry | null, settings: {
58
- themePack?: string;
59
+ themePack?: string | null;
60
+ colorMode?: unknown;
59
61
  }): string | undefined;
60
62
  /**
61
63
  * Bootstrap script for the theme-pack layer. Values are inlined as JSON
@@ -11,6 +11,7 @@ import {
11
11
  THEME_PACK_STORAGE_KEY,
12
12
  buildPackCssUrl
13
13
  } from "../theme-pack-switcher/theme-pack-sync.js";
14
+ import { resolveConfiguredThemePack } from "../theme-packs-registry/configured-theme-pack.js";
14
15
  const THEME_PACK_LOADING_ATTR = "data-zd-theme-pack-loading";
15
16
  const THEME_PACK_LOAD_WATCHDOG_MS = 2e3;
16
17
  const THEME_PACK_LATCH_CSS = `html[${THEME_PACK_LOADING_ATTR}] body{visibility:hidden}`;
@@ -23,7 +24,7 @@ function themePackVersionMap(registry) {
23
24
  }
24
25
  function resolveThemePackSsrSlug(registry, settings) {
25
26
  if (registry === null) return void 0;
26
- return settings.themePack ?? DEFAULT_THEME_PACK_SLUG;
27
+ return resolveConfiguredThemePack(settings);
27
28
  }
28
29
  function buildThemePackBootstrap(configuredSlug, enabled, base) {
29
30
  const configured = JSON.stringify(configuredSlug);
@@ -1,6 +1,9 @@
1
1
  import fs from "fs-extra";
2
2
  import path from "node:path";
3
- import { DEFAULT_THEME_PACK_SLUG } from "../theme-packs-registry/index.js";
3
+ import {
4
+ CONFIGURED_DEFAULT_THEME_PACK_SLUG,
5
+ DEFAULT_THEME_PACK_SLUG
6
+ } from "../theme-packs-registry/index.js";
4
7
  import {
5
8
  ConfigSyntaxError,
6
9
  findMatchingBrace,
@@ -16,7 +19,10 @@ function resolveZfbConfigPath(cwd) {
16
19
  }
17
20
  async function detectActiveThemePack(cwd) {
18
21
  const configPath = resolveZfbConfigPath(cwd);
19
- const fallback = { slug: DEFAULT_THEME_PACK_SLUG, detected: false };
22
+ const fallback = {
23
+ slug: CONFIGURED_DEFAULT_THEME_PACK_SLUG,
24
+ detected: false
25
+ };
20
26
  if (!await fs.pathExists(configPath)) return fallback;
21
27
  let source;
22
28
  try {
@@ -33,7 +39,24 @@ async function detectActiveThemePack(cwd) {
33
39
  const members = splitTopLevelMembers(source, braceOpenIdx + 1, braceCloseIdx);
34
40
  if (members.some((m) => m.isSpread)) return fallback;
35
41
  const themePackMember = members.find((m) => m.key === "themePack");
36
- if (!themePackMember) return { slug: DEFAULT_THEME_PACK_SLUG, detected: true };
42
+ if (!themePackMember) {
43
+ const valueText = (key) => {
44
+ const member = members.find((m) => m.key === key);
45
+ if (!member) return void 0;
46
+ if (member.valueStart === null || member.valueEnd === null) return null;
47
+ return source.slice(member.valueStart, member.valueEnd).trim();
48
+ };
49
+ const colorSchemes = valueText("colorSchemes");
50
+ const colorMode = valueText("colorMode");
51
+ if (colorSchemes === null || colorMode === null) return fallback;
52
+ const customSchemes = colorSchemes !== void 0 && colorSchemes !== "undefined" && colorSchemes !== "null" && colorSchemes !== "defaultColorSchemes";
53
+ if (customSchemes || colorMode === "false") {
54
+ return { slug: DEFAULT_THEME_PACK_SLUG, detected: true };
55
+ }
56
+ const colorModeKnownNotFalse = colorMode === void 0 || colorMode === "undefined" || colorMode === "true" || colorMode.startsWith("{");
57
+ if (!colorModeKnownNotFalse) return fallback;
58
+ return { slug: CONFIGURED_DEFAULT_THEME_PACK_SLUG, detected: true };
59
+ }
37
60
  if (themePackMember.valueStart === null || themePackMember.valueEnd === null || !isPlainStringLiteralSpan(source, themePackMember.valueStart, themePackMember.valueEnd)) {
38
61
  return fallback;
39
62
  }