@takazudo/zudo-doc 9.0.0 → 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.
@@ -4,9 +4,23 @@ import { broaderSidebarScope, sidebarScopeNodes, reconcileSidebarScope, indexSid
4
4
  import { getNoteTrayItems, groupItems } from "../note-tray-model/index.js";
5
5
  import { AFTER_NAVIGATE_EVENT } from "../transitions/index.js";
6
6
 
7
+ export interface ScopeLabels {
8
+ broaden: string;
9
+ restore: string;
10
+ restoreTip: string;
11
+ focus: (label: string) => string;
12
+ focusTip: string;
13
+ focused: (label: string) => string;
14
+ focusedTip: string;
15
+ }
16
+
7
17
  export interface ScopeControls {
8
18
  focus: (node: SidebarNavNode) => void;
9
- focusLabel: string;
19
+ labels: ScopeLabels;
20
+ /** True when the unfiltered scope is exactly this one occurrence as its single root. */
21
+ isFocusedRoot: (occurrenceId: string | undefined) => boolean;
22
+ /** The single root a scope change just arrived at, briefly; null otherwise. */
23
+ arrived: ReadonlySignal<string | null>;
10
24
  /** Effective disclosure: the user's explicit choice, else revealed/scope root, else `baseline`. */
11
25
  isOpen: (key: string, baseline: boolean) => boolean;
12
26
  /** Records an explicit user disclosure choice. */
@@ -18,6 +32,8 @@ export interface ScopeControls {
18
32
  // Version 1 records (no `version`) mixed automatic reveal into `expansion`
19
33
  // without provenance, so their expansion is discarded once on read.
20
34
  const STATE_VERSION = 2;
35
+ // Matches the `zd-sidebar-arrival` keyframes in features.css.
36
+ const ARRIVAL_MS = 700;
21
37
 
22
38
  const isRecord = (value: unknown): value is Record<string, unknown> =>
23
39
  typeof value === "object" && value !== null && !Array.isArray(value);
@@ -42,11 +58,17 @@ export function useSidebarScope({ nodes, navigation, activeSlug, query, locale }
42
58
  const navRef: Ref<HTMLElement> = { current: null };
43
59
  const stateKey = "zd-sidebar-scope";
44
60
  const syncEvent = "zd:sidebar-scope";
45
- const labels = locale === "ja" ? {
46
- broaden: "ツリーを広げる", restore: "現在のページのツリーに戻す", focus: "この枝だけを表示",
61
+ const labels: ScopeLabels = locale === "ja" ? {
62
+ broaden: "ツリーを広げる", restore: "現在のページのサイドバーに戻す", restoreTip: "現在のページのサイドバーに戻す",
63
+ focus: (label) => `サイドバーに「${label}」だけを表示`, focusTip: "ここだけを表示",
64
+ focused: (label) => `すでに「${label}」だけを表示しています`, focusedTip: "すでにここだけを表示しています",
47
65
  } : {
48
- broaden: "Broaden tree", restore: "Restore current page’s local tree", focus: "Show only this branch",
66
+ broaden: "Broaden tree", restore: "Restore current page’s sidebar", restoreTip: "Restore this page’s sidebar",
67
+ focus: (label) => `Focus sidebar on ${label}`, focusTip: "Focus sidebar here",
68
+ focused: (label) => `Sidebar is already focused on ${label}`, focusedTip: "Already focused here",
49
69
  };
70
+ const arrived = signal<string | null>(null);
71
+ let arrivalTimer: ReturnType<typeof setTimeout> | undefined;
50
72
  const selectedNodes = computed(() => navigation ? sidebarScopeNodes(navigation, nodes, selected.value) : nodes);
51
73
  const broader = computed(() => navigation ? broaderSidebarScope(navigation, nodes, selected.value) : null);
52
74
  const occurrenceIndex = navigation ? indexSidebarOccurrences(navigation.roots) : new Map<string, SidebarNavNode>();
@@ -118,19 +140,26 @@ export function useSidebarScope({ nodes, navigation, activeSlug, query, locale }
118
140
  revealed.value = new Set([...revealed.value, ...withAncestors(previous), ...withAncestors(id)]);
119
141
  }
120
142
  save();
143
+ clearTimeout(arrivalTimer);
144
+ arrived.value = id === null ? null : scopeRoot.value ?? null;
145
+ if (arrived.value !== null) arrivalTimer = setTimeout(() => { arrived.value = null; }, ARRIVAL_MS);
121
146
  // Scroll only the sidebar's own viewport; never scrollIntoView, which also
122
147
  // scrolls the document and would move the article being read.
123
148
  queueMicrotask(() => {
124
149
  const nav = navRef.current;
125
- const controls = [...(nav?.querySelectorAll<HTMLButtonElement>("[data-sidebar-focus]") ?? [])];
150
+ // An already-focused root's frame is aria-disabled and out of the tab order,
151
+ // so it must never receive the fallback focus.
152
+ const controls = [...(nav?.querySelectorAll<HTMLButtonElement>('[data-sidebar-focus]:not([aria-disabled="true"])') ?? [])];
126
153
  const activeBranch = controls.reverse().find((button) => {
127
154
  const node = occurrenceIndex.get(button.dataset.sidebarFocusScope ?? "");
128
155
  const contains = (n: SidebarNavNode): boolean => n.slug === activeSlug.value || n.children.some(contains);
129
156
  return node && contains(node);
130
157
  });
131
- const rootLink = id && id !== SIDEBAR_FOREST_SCOPE && broader.value !== null ? nav?.querySelector<HTMLAnchorElement>("[data-sidebar-tree-root] a") : null;
158
+ // The tree's first link; a tree with no link at all falls back to its first disclosure.
159
+ const rootAction = nav?.querySelector<HTMLElement>("[data-sidebar-tree-root] a") ?? nav?.querySelector<HTMLElement>("[data-sidebar-tree-root] button[aria-expanded]");
160
+ const rootLink = id && id !== SIDEBAR_FOREST_SCOPE && broader.value !== null ? rootAction : null;
132
161
  const currentLink = nav?.querySelector<HTMLElement>('[aria-current="page"]');
133
- const control = (id === null ? currentLink : rootLink) ?? nav?.querySelector<HTMLButtonElement>("[data-sidebar-broaden]") ?? activeBranch ?? currentLink ?? nav?.querySelector<HTMLElement>('[data-sidebar-focus], [data-sidebar-tree-root] a, input');
162
+ const control = (id === null ? currentLink : rootLink) ?? nav?.querySelector<HTMLButtonElement>("[data-sidebar-broaden]") ?? activeBranch ?? currentLink ?? rootAction ?? nav?.querySelector<HTMLElement>('[data-sidebar-focus]:not([aria-disabled="true"]), [data-sidebar-tree-root] a, [data-sidebar-tree-root] button[aria-expanded], input');
134
163
  const target = control;
135
164
  let viewport = nav?.parentElement;
136
165
  while (viewport && viewport !== document.body && viewport.scrollHeight <= viewport.clientHeight) viewport = viewport.parentElement;
@@ -143,9 +172,10 @@ export function useSidebarScope({ nodes, navigation, activeSlug, query, locale }
143
172
  control?.focus({ preventScroll: true });
144
173
  });
145
174
  };
175
+ const isFocusedRoot = (occurrenceId: string | undefined) => occurrenceId !== undefined && scopeRoot.value === occurrenceId;
146
176
  const scopeControls: ScopeControls | undefined = navigation ? {
147
- focus: (node) => { if (node.occurrenceId) changeScope(node.occurrenceId); },
148
- focusLabel: labels.focus, defaultOpen, defaultGroupOpen,
177
+ focus: (node) => { if (node.occurrenceId && !isFocusedRoot(node.occurrenceId)) changeScope(node.occurrenceId); },
178
+ labels, isFocusedRoot, arrived, defaultOpen, defaultGroupOpen,
149
179
  isOpen: (key, baseline) => expansion.value[key] ?? automaticOpen(key, baseline),
150
180
  setOpen: (key, open) => {
151
181
  expansion.value = { ...expansion.value, [key]: open };
@@ -191,7 +221,7 @@ export function useSidebarScope({ nodes, navigation, activeSlug, query, locale }
191
221
  };
192
222
  document.addEventListener(syncEvent, sync);
193
223
  document.addEventListener(AFTER_NAVIGATE_EVENT, navigate);
194
- return () => { document.removeEventListener(syncEvent, sync); document.removeEventListener(AFTER_NAVIGATE_EVENT, navigate); };
224
+ return () => { clearTimeout(arrivalTimer); document.removeEventListener(syncEvent, sync); document.removeEventListener(AFTER_NAVIGATE_EVENT, navigate); };
195
225
  });
196
226
  return { selected, selectedNodes, broader, labels, canRestore, changeScope, save, navRef, scopeControls };
197
227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/zudo-doc",
3
- "version": "9.0.0",
3
+ "version": "9.1.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",
@@ -752,7 +752,7 @@
752
752
  ],
753
753
  "peerDependencies": {
754
754
  "@modelcontextprotocol/sdk": "1.32.2",
755
- "@takazudo/zdtp": "^0.5.2 || ^0.6.0 || ^0.7.0 || ^0.8.0",
755
+ "@takazudo/zdtp": "^0.5.2 || ^0.6.0 || ^0.7.0 || ^0.8.0 || ^0.9.0",
756
756
  "@takazudo/zfb": "^5.1.5",
757
757
  "@takazudo/zfb-md-wasm": "^5.1.5",
758
758
  "@takazudo/zfb-runtime": "^5.1.5",
@@ -782,16 +782,16 @@
782
782
  }
783
783
  },
784
784
  "dependencies": {
785
- "@inquirer/prompts": "^8.4.2",
786
- "esbuild": ">=0.28.1",
787
- "fs-extra": "^11.3.0",
785
+ "@inquirer/prompts": "^8.7.5",
786
+ "esbuild": "^0.28.2",
787
+ "fs-extra": "^11.4.1",
788
788
  "minimist": "^1.2.8",
789
789
  "parse5": "^8.0.1",
790
790
  "picocolors": "^1.1.1",
791
791
  "pluralize": "^8.0.0",
792
792
  "smol-toml": "^1.9.1",
793
- "tsx": "^4.21.0",
794
- "yaml": "^2.9.0"
793
+ "tsx": "^4.23.15",
794
+ "yaml": "^2.9.1"
795
795
  },
796
796
  "devDependencies": {
797
797
  "@modelcontextprotocol/sdk": "1.32.2",
@@ -801,14 +801,15 @@
801
801
  "@takazudo/zfb-runtime": "5.1.5",
802
802
  "@types/fs-extra": "^11.0.4",
803
803
  "@types/minimist": "^1.2.5",
804
- "@types/node": "^25.3.5",
804
+ "@types/node": "^25.9.9",
805
805
  "@types/pluralize": "^0.0.33",
806
- "happy-dom": "^20.10.6",
807
- "tsup": "^8.0.0",
806
+ "happy-dom": "^21.0.4",
807
+ "katex": "^0.19.1",
808
+ "tsup": "^9.0.0",
808
809
  "typescript": "^5.0.0",
809
- "vitest": "^4.1.0",
810
- "zod": "^4.3.6",
811
- "@takazudo/zudo-doc-history-server": "9.0.0"
810
+ "vitest": "^4.1.12",
811
+ "zod": "^4.6.7",
812
+ "@takazudo/zudo-doc-history-server": "9.1.0"
812
813
  },
813
814
  "scripts": {
814
815
  "gen:search-widget-script": "node scripts/gen-search-widget-script.mjs",