@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.
- package/CHANGELOG.md +18 -0
- package/README.md +5 -4
- package/dist/compiled.css +109 -3
- package/dist/design-token-panel-bootstrap-island.d.ts +1 -1
- package/dist/features.css +110 -0
- package/dist/html-preview-wrapper/preview-base.js +2 -2
- package/dist/icons/index.d.ts +2 -0
- package/dist/icons/index.js +36 -0
- package/dist/nav-scope/index.d.ts +1 -1
- package/dist/nav-scope/index.js +7 -8
- package/dist/page-loading/page-loading-overlay.d.ts +5 -1
- package/dist/page-loading/page-loading-overlay.js +1 -1
- package/dist/page-loading.css +2 -1
- package/dist/plugins/zdtp-loader.d.ts +1 -0
- package/dist/plugins/zdtp-loader.js +5 -1
- package/dist/sidebar-tree-island/branch-preview.d.ts +19 -0
- package/dist/sidebar-tree-island/branch-preview.js +199 -0
- package/dist/sidebar-tree-island/index.js +82 -33
- package/dist/sidebar-tree-island/scope-state.d.ts +15 -6
- package/dist/sidebar-tree-island/scope-state.js +30 -9
- package/dist/sidebar-utils/index.js +5 -2
- package/dist/tree-nav-shared/index.d.ts +5 -1
- package/dist/tree-nav-shared/index.js +7 -3
- package/dist/wind.json +6 -1
- package/dist/zdtp-loader.d.ts +1 -1
- package/eject/header/gen-nav-overflow-script.mjs +2 -2
- package/eject/page-loading/page-loading-overlay.tsx +7 -3
- package/eject/sidebar-tree-island/branch-preview.ts +247 -0
- package/eject/sidebar-tree-island/index.tsx +86 -19
- package/eject/sidebar-tree-island/scope-state.ts +40 -10
- package/package.json +14 -13
package/dist/wind.json
CHANGED
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"col-span-full",
|
|
68
68
|
"col-start-1",
|
|
69
69
|
"contents",
|
|
70
|
+
"cursor-default",
|
|
70
71
|
"cursor-not-allowed",
|
|
71
72
|
"cursor-pointer",
|
|
72
73
|
"disabled:cursor-default",
|
|
@@ -91,7 +92,6 @@
|
|
|
91
92
|
"focus-visible:outline-accent",
|
|
92
93
|
"focus-visible:outline-offset-2",
|
|
93
94
|
"focus-visible:text-accent",
|
|
94
|
-
"focus-visible:text-fg",
|
|
95
95
|
"focus-visible:underline",
|
|
96
96
|
"focus-within:border-accent",
|
|
97
97
|
"focus-within:z-local-1",
|
|
@@ -147,9 +147,11 @@
|
|
|
147
147
|
"h-[0.5rem]",
|
|
148
148
|
"h-[0.625rem]",
|
|
149
149
|
"h-[0.875rem]",
|
|
150
|
+
"h-[1.0625rem]",
|
|
150
151
|
"h-[1.125rem]",
|
|
151
152
|
"h-[1.25rem]",
|
|
152
153
|
"h-[1.575rem]",
|
|
154
|
+
"h-[1.875rem]",
|
|
153
155
|
"h-[10rem]",
|
|
154
156
|
"h-[14px]",
|
|
155
157
|
"h-[1em]",
|
|
@@ -290,6 +292,7 @@
|
|
|
290
292
|
"my-vsp-md",
|
|
291
293
|
"no-underline",
|
|
292
294
|
"object-contain",
|
|
295
|
+
"opacity-50",
|
|
293
296
|
"opacity-60",
|
|
294
297
|
"outline-none",
|
|
295
298
|
"overflow-auto",
|
|
@@ -455,11 +458,13 @@
|
|
|
455
458
|
"w-[0.5rem]",
|
|
456
459
|
"w-[0.625rem]",
|
|
457
460
|
"w-[0.875rem]",
|
|
461
|
+
"w-[1.0625rem]",
|
|
458
462
|
"w-[1.125rem]",
|
|
459
463
|
"w-[1.25rem]",
|
|
460
464
|
"w-[1.575rem]",
|
|
461
465
|
"w-[1.5rem]",
|
|
462
466
|
"w-[1.75rem]",
|
|
467
|
+
"w-[1.875rem]",
|
|
463
468
|
"w-[12rem]",
|
|
464
469
|
"w-[14px]",
|
|
465
470
|
"w-[16px]",
|
package/dist/zdtp-loader.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PanelConfig, PanelInstanceHandle } from "@takazudo/zdtp";
|
|
2
2
|
export * from "@takazudo/zdtp";
|
|
3
3
|
/**
|
|
4
|
-
* Mount zdtp's opaque
|
|
4
|
+
* Mount zdtp's opaque panel root only while the activating island still owns
|
|
5
5
|
* the pending import. Once configured, zdtp keeps its document-lifetime root
|
|
6
6
|
* and its existing navigation cleanup semantics.
|
|
7
7
|
*/
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
// SOURCE, strips TypeScript types deterministically via esbuild's
|
|
29
29
|
// `transformSync()` (same rationale as gen-search-widget-script.mjs — see
|
|
30
30
|
// that file's header comment for the full `ts.transpileModule()` vs esbuild
|
|
31
|
-
// history, zudolab/zudo-doc#3422 / #3430 — identical here: esbuild is
|
|
31
|
+
// history, zudolab/zudo-doc#3422 / #3430 — identical here: esbuild is a
|
|
32
32
|
// package dependency (it is also used by the shipped ejected-header generator),
|
|
33
|
-
// and
|
|
33
|
+
// and its range matches the root `pnpm.overrides` entry), executes
|
|
34
34
|
// each transpiled CommonJS module in an isolated sandbox (empty
|
|
35
35
|
// `module`/`exports`; all four source files are import-free), then reads
|
|
36
36
|
// the REAL runtime values off each sandbox's `exports`:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @jsxRuntime automatic */
|
|
2
|
-
// Server-rendered, zero-hydration
|
|
2
|
+
// Server-rendered, zero-hydration full-viewport loading overlay.
|
|
3
3
|
//
|
|
4
4
|
// Renders the overlay and its bootstrap into the document:
|
|
5
5
|
//
|
|
@@ -53,7 +53,7 @@ export function buildPageLoadingOverlayBootstrap(overlayId: string): string {
|
|
|
53
53
|
const after = JSON.stringify(AFTER_NAVIGATE_EVENT);
|
|
54
54
|
return `(function(){
|
|
55
55
|
var id=${id};
|
|
56
|
-
function show(){var el=document.getElementById(id);if(!el)return;
|
|
56
|
+
function show(){var el=document.getElementById(id);if(!el)return;el.setAttribute("data-visible","");el.setAttribute("aria-hidden","false");}
|
|
57
57
|
function hide(){var el=document.getElementById(id);if(!el)return;el.removeAttribute("data-visible");el.setAttribute("aria-hidden","true");}
|
|
58
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","");}
|
|
59
59
|
function clearPending(){document.querySelectorAll("[data-zd-nav-pending]").forEach(function(el){el.removeAttribute("data-zd-nav-pending");});}
|
|
@@ -66,7 +66,11 @@ document.addEventListener(${JSON.stringify(NAVIGATION_ABORTED_EVENT)},function()
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
69
|
+
* Full-viewport loading overlay shown while a dynamic navigation is pending.
|
|
70
|
+
*
|
|
71
|
+
* Geometry belongs to `page-loading.css` (`position: fixed; inset: 0`); the
|
|
72
|
+
* bootstrap only toggles visibility. Header and sidebar stay stationary through
|
|
73
|
+
* their own named View Transition snapshots, not by clipping the scrim.
|
|
70
74
|
*
|
|
71
75
|
* Mount this once per layout (typically inside `DocLayoutWithDefaults`'s
|
|
72
76
|
* `bodyEnd` slot, alongside the existing body-end providers). It is
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { computed, signal, type ReadonlySignal, type Ref } from "@takazudo/zfb/zudo-react";
|
|
2
|
+
|
|
3
|
+
/** Trigger attribute: the tooltip's title text. */
|
|
4
|
+
export const TIP_ATTR = "data-sidebar-tip";
|
|
5
|
+
/** Trigger attribute: optional secondary tooltip line. */
|
|
6
|
+
export const TIP_DETAIL_ATTR = "data-sidebar-tip-detail";
|
|
7
|
+
|
|
8
|
+
const TRIGGER_SELECTOR = `[${TIP_ATTR}]`;
|
|
9
|
+
const FRAME_SELECTOR = "[data-sidebar-focus]";
|
|
10
|
+
const POINTER_SHOW_DELAY_MS = 250;
|
|
11
|
+
// Long enough to cross the gap from the trigger onto the tooltip.
|
|
12
|
+
const HIDE_DELAY_MS = 120;
|
|
13
|
+
const GAP_PX = 6;
|
|
14
|
+
const VIEWPORT_MARGIN_PX = 8;
|
|
15
|
+
|
|
16
|
+
let instanceCount = 0;
|
|
17
|
+
|
|
18
|
+
export interface BranchPreview {
|
|
19
|
+
/** Occurrence previewed in this sidebar instance, or null. Never persisted or broadcast. */
|
|
20
|
+
active: ReadonlySignal<string | null>;
|
|
21
|
+
clear: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Hover preview is mouse/pen only so a touch tap focuses in one step and leaves nothing behind. */
|
|
25
|
+
const isHoverPointer = (event: Event) => {
|
|
26
|
+
const type = (event as PointerEvent).pointerType;
|
|
27
|
+
return type === "mouse" || type === "pen";
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const isFocusVisible = (element: Element) => {
|
|
31
|
+
try {
|
|
32
|
+
return element.matches(":focus-visible");
|
|
33
|
+
} catch {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const isActionableFrame = (element: Element) =>
|
|
39
|
+
element.matches(FRAME_SELECTOR) && element.getAttribute("aria-disabled") !== "true";
|
|
40
|
+
|
|
41
|
+
const previewKey = (element: Element) =>
|
|
42
|
+
isActionableFrame(element) ? (element as HTMLElement).dataset.sidebarFocusScope || null : null;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Transient branch preview + tooltip for one sidebar island instance.
|
|
46
|
+
* Call `activate()` from the island's `onActivate`; it returns the cleanup.
|
|
47
|
+
* The desktop tree and the mobile drawer are separate instances, so each
|
|
48
|
+
* visible sidebar owns at most one preview and one tooltip.
|
|
49
|
+
*/
|
|
50
|
+
export function createBranchPreview(navRef: Ref<HTMLElement>): BranchPreview & { activate: () => () => void } {
|
|
51
|
+
const pointerKey = signal<string | null>(null);
|
|
52
|
+
const focusKey = signal<string | null>(null);
|
|
53
|
+
// Pointer hover over an actionable frame wins over the keyboard-focused one.
|
|
54
|
+
const active = computed(() => pointerKey.value ?? focusKey.value);
|
|
55
|
+
let tooltip: HTMLElement | null = null;
|
|
56
|
+
let owner: HTMLElement | null = null;
|
|
57
|
+
let showTimer: ReturnType<typeof setTimeout> | undefined;
|
|
58
|
+
let hideTimer: ReturnType<typeof setTimeout> | undefined;
|
|
59
|
+
|
|
60
|
+
const hideTooltip = () => {
|
|
61
|
+
clearTimeout(showTimer);
|
|
62
|
+
clearTimeout(hideTimer);
|
|
63
|
+
if (owner?.getAttribute("aria-describedby") === tooltip?.id) owner?.removeAttribute("aria-describedby");
|
|
64
|
+
owner = null;
|
|
65
|
+
if (tooltip) tooltip.hidden = true;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const clear = () => {
|
|
69
|
+
pointerKey.value = null;
|
|
70
|
+
focusKey.value = null;
|
|
71
|
+
hideTooltip();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const place = (trigger: HTMLElement, tip: HTMLElement) => {
|
|
75
|
+
// Measure from the origin: a stale right-side `left` would shrink-wrap the
|
|
76
|
+
// fixed tooltip to the remaining viewport width.
|
|
77
|
+
tip.style.left = "0px";
|
|
78
|
+
tip.style.top = "0px";
|
|
79
|
+
const anchor = trigger.getBoundingClientRect();
|
|
80
|
+
const box = tip.getBoundingClientRect();
|
|
81
|
+
const maxLeft = window.innerWidth - box.width - VIEWPORT_MARGIN_PX;
|
|
82
|
+
// Start past the sidebar's edge so the hoverable tooltip never covers the
|
|
83
|
+
// row's disclosure, which sits right beside the frame.
|
|
84
|
+
const edge = Math.max(anchor.right, navRef.current?.getBoundingClientRect().right ?? anchor.right);
|
|
85
|
+
let left = edge + GAP_PX;
|
|
86
|
+
let top = anchor.top + anchor.height / 2 - box.height / 2;
|
|
87
|
+
// No room beside the trigger (narrow drawer): drop below it instead.
|
|
88
|
+
if (left > maxLeft) {
|
|
89
|
+
left = Math.max(VIEWPORT_MARGIN_PX, Math.min(anchor.left + anchor.width / 2 - box.width / 2, maxLeft));
|
|
90
|
+
top = anchor.bottom + GAP_PX;
|
|
91
|
+
if (top + box.height > window.innerHeight - VIEWPORT_MARGIN_PX) top = anchor.top - GAP_PX - box.height;
|
|
92
|
+
}
|
|
93
|
+
top = Math.max(VIEWPORT_MARGIN_PX, Math.min(top, window.innerHeight - box.height - VIEWPORT_MARGIN_PX));
|
|
94
|
+
tip.style.left = `${Math.round(left)}px`;
|
|
95
|
+
tip.style.top = `${Math.round(top)}px`;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const showTooltip = (trigger: HTMLElement) => {
|
|
99
|
+
if (!tooltip) return;
|
|
100
|
+
clearTimeout(hideTimer);
|
|
101
|
+
if (owner && owner !== trigger && owner.getAttribute("aria-describedby") === tooltip.id) owner.removeAttribute("aria-describedby");
|
|
102
|
+
owner = trigger;
|
|
103
|
+
const title = document.createElement("strong");
|
|
104
|
+
title.textContent = trigger.getAttribute(TIP_ATTR) ?? "";
|
|
105
|
+
const parts: Node[] = [title];
|
|
106
|
+
const detail = trigger.getAttribute(TIP_DETAIL_ATTR);
|
|
107
|
+
if (detail) {
|
|
108
|
+
const line = document.createElement("span");
|
|
109
|
+
line.textContent = detail;
|
|
110
|
+
parts.push(line);
|
|
111
|
+
}
|
|
112
|
+
tooltip.replaceChildren(...parts);
|
|
113
|
+
tooltip.hidden = false;
|
|
114
|
+
trigger.setAttribute("aria-describedby", tooltip.id);
|
|
115
|
+
place(trigger, tooltip);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const scheduleHide = () => {
|
|
119
|
+
clearTimeout(showTimer);
|
|
120
|
+
clearTimeout(hideTimer);
|
|
121
|
+
hideTimer = setTimeout(hideTooltip, HIDE_DELAY_MS);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const triggerOf = (target: EventTarget | null) => {
|
|
125
|
+
const nav = navRef.current;
|
|
126
|
+
if (!nav || !(target instanceof Element)) return null;
|
|
127
|
+
const trigger = target.closest<HTMLElement>(TRIGGER_SELECTOR);
|
|
128
|
+
return trigger && nav.contains(trigger) ? trigger : null;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const activate = () => {
|
|
132
|
+
const tip = document.createElement("div");
|
|
133
|
+
tip.id = `zd-sidebar-tip-${++instanceCount}`;
|
|
134
|
+
tip.setAttribute("role", "tooltip");
|
|
135
|
+
tip.setAttribute("data-sidebar-tooltip", "");
|
|
136
|
+
tip.hidden = true;
|
|
137
|
+
document.body.append(tip);
|
|
138
|
+
tooltip = tip;
|
|
139
|
+
|
|
140
|
+
const onPointerOver = (event: Event) => {
|
|
141
|
+
const trigger = triggerOf(event.target);
|
|
142
|
+
if (!trigger || !isHoverPointer(event) || trigger.contains((event as PointerEvent).relatedTarget as Node | null)) return;
|
|
143
|
+
pointerKey.value = previewKey(trigger);
|
|
144
|
+
clearTimeout(hideTimer);
|
|
145
|
+
clearTimeout(showTimer);
|
|
146
|
+
if (owner === trigger && !tip.hidden) return;
|
|
147
|
+
showTimer = setTimeout(() => showTooltip(trigger), POINTER_SHOW_DELAY_MS);
|
|
148
|
+
};
|
|
149
|
+
const onPointerOut = (event: Event) => {
|
|
150
|
+
const trigger = triggerOf(event.target);
|
|
151
|
+
const next = (event as PointerEvent).relatedTarget as Node | null;
|
|
152
|
+
if (!trigger || trigger.contains(next)) return;
|
|
153
|
+
pointerKey.value = null;
|
|
154
|
+
// Moving onto the tooltip keeps it open (hoverable content).
|
|
155
|
+
if (next && tip.contains(next)) return;
|
|
156
|
+
if (owner === trigger && trigger === document.activeElement && isFocusVisible(trigger)) return;
|
|
157
|
+
scheduleHide();
|
|
158
|
+
};
|
|
159
|
+
const onFocusIn = (event: Event) => {
|
|
160
|
+
const trigger = triggerOf(event.target);
|
|
161
|
+
if (!trigger || !isFocusVisible(trigger)) return;
|
|
162
|
+
focusKey.value = previewKey(trigger);
|
|
163
|
+
clearTimeout(showTimer);
|
|
164
|
+
showTooltip(trigger);
|
|
165
|
+
};
|
|
166
|
+
const onFocusOut = (event: Event) => {
|
|
167
|
+
const trigger = triggerOf(event.target);
|
|
168
|
+
if (!trigger) return;
|
|
169
|
+
focusKey.value = null;
|
|
170
|
+
if (owner === trigger) scheduleHide();
|
|
171
|
+
};
|
|
172
|
+
const onTipEnter = () => clearTimeout(hideTimer);
|
|
173
|
+
const onTipLeave = (event: Event) => {
|
|
174
|
+
const next = (event as PointerEvent).relatedTarget as Node | null;
|
|
175
|
+
if (owner && next && owner.contains(next)) return;
|
|
176
|
+
if (owner && owner === document.activeElement && isFocusVisible(owner)) return;
|
|
177
|
+
scheduleHide();
|
|
178
|
+
};
|
|
179
|
+
// Capture phase so an open tooltip consumes the first Escape before the
|
|
180
|
+
// mobile drawer's document listener sees it (that listener honours
|
|
181
|
+
// defaultPrevented — layered Escape ownership, zudolab/zudo-doc#4393).
|
|
182
|
+
const onKeyDown = (event: Event) => {
|
|
183
|
+
const key = event as KeyboardEvent;
|
|
184
|
+
if (key.key !== "Escape" || key.isComposing || tip.hidden) return;
|
|
185
|
+
// Escape aimed at an unrelated surface (e.g. a dialog opened while the
|
|
186
|
+
// pointer still rests on a frame) must reach its own owner.
|
|
187
|
+
const target = key.target;
|
|
188
|
+
if (target instanceof Element && target !== document.body && !navRef.current?.contains(target) && !tip.contains(target)) {
|
|
189
|
+
hideTooltip();
|
|
190
|
+
pointerKey.value = null;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
key.preventDefault();
|
|
194
|
+
key.stopPropagation();
|
|
195
|
+
hideTooltip();
|
|
196
|
+
pointerKey.value = null;
|
|
197
|
+
};
|
|
198
|
+
// Keyboard focus scrolls the sidebar to reveal its target, so follow the
|
|
199
|
+
// trigger instead of dismissing; a trigger that left the DOM dismisses.
|
|
200
|
+
let scrollFrame = 0;
|
|
201
|
+
const onScroll = () => {
|
|
202
|
+
if (tip.hidden || scrollFrame) return;
|
|
203
|
+
scrollFrame = requestAnimationFrame(() => {
|
|
204
|
+
scrollFrame = 0;
|
|
205
|
+
if (tip.hidden) return;
|
|
206
|
+
if (owner?.isConnected) place(owner, tip);
|
|
207
|
+
else hideTooltip();
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
// The drawer stays mounted when closed and goes `inert`; a hidden sidebar
|
|
211
|
+
// must not keep a preview or tooltip.
|
|
212
|
+
// The <nav> sits inside a <Show> (the root-menu view can replace it), so
|
|
213
|
+
// fall back to the drawer itself when the ref is not mounted yet.
|
|
214
|
+
const drawer = navRef.current ? navRef.current.closest("[data-zd-mobile-sidebar]") : document.querySelector("[data-zd-mobile-sidebar]");
|
|
215
|
+
const observer = drawer ? new MutationObserver(() => { if (drawer.hasAttribute("inert")) clear(); }) : undefined;
|
|
216
|
+
observer?.observe(drawer!, { attributes: true, attributeFilter: ["inert"] });
|
|
217
|
+
// The lg breakpoint (Wind `lg:` / features.css) where the drawer and the desktop sidebar swap.
|
|
218
|
+
const media = window.matchMedia?.("(min-width: 1024px)");
|
|
219
|
+
const onBreakpoint = () => clear();
|
|
220
|
+
media?.addEventListener?.("change", onBreakpoint);
|
|
221
|
+
|
|
222
|
+
document.addEventListener("pointerover", onPointerOver);
|
|
223
|
+
document.addEventListener("pointerout", onPointerOut);
|
|
224
|
+
document.addEventListener("focusin", onFocusIn);
|
|
225
|
+
document.addEventListener("focusout", onFocusOut);
|
|
226
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
227
|
+
document.addEventListener("scroll", onScroll, true);
|
|
228
|
+
tip.addEventListener("pointerenter", onTipEnter);
|
|
229
|
+
tip.addEventListener("pointerleave", onTipLeave);
|
|
230
|
+
return () => {
|
|
231
|
+
cancelAnimationFrame(scrollFrame);
|
|
232
|
+
clear();
|
|
233
|
+
observer?.disconnect();
|
|
234
|
+
media?.removeEventListener?.("change", onBreakpoint);
|
|
235
|
+
document.removeEventListener("pointerover", onPointerOver);
|
|
236
|
+
document.removeEventListener("pointerout", onPointerOut);
|
|
237
|
+
document.removeEventListener("focusin", onFocusIn);
|
|
238
|
+
document.removeEventListener("focusout", onFocusOut);
|
|
239
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
240
|
+
document.removeEventListener("scroll", onScroll, true);
|
|
241
|
+
tip.remove();
|
|
242
|
+
tooltip = null;
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return { active, clear, activate };
|
|
247
|
+
}
|
|
@@ -19,15 +19,17 @@ import type { ResolvedDateFormats } from "../settings.js";
|
|
|
19
19
|
import {
|
|
20
20
|
INDENT,
|
|
21
21
|
BASE_PAD,
|
|
22
|
+
CONNECTOR_OFFSET,
|
|
22
23
|
connectorLeft,
|
|
23
24
|
ConnectorLines,
|
|
24
25
|
CategoryLinkIcon,
|
|
25
26
|
} from "../tree-nav-shared/index.js";
|
|
26
|
-
import { ChevronRight, ChevronLeft, Search } from "../icons/index.js";
|
|
27
|
+
import { ChevronRight, ChevronLeft, Search, FocusFrame, RestoreArrow } from "../icons/index.js";
|
|
27
28
|
import { ThemeToggle, type ThemeToggleLabels } from "../theme-toggle/index.js";
|
|
28
29
|
import { smartBreakToHtml } from "../smart-break/index.js";
|
|
29
30
|
import { AFTER_NAVIGATE_EVENT } from "../transitions/index.js";
|
|
30
31
|
import { useSidebarScope, type ScopeControls } from "./scope-state.js";
|
|
32
|
+
import { createBranchPreview } from "./branch-preview.js";
|
|
31
33
|
import { filterTree } from "../sidebar-filter/index.js";
|
|
32
34
|
import { findActiveSlug, normalizePath } from "../sidebar-active-slug/index.js";
|
|
33
35
|
import {
|
|
@@ -47,6 +49,7 @@ import { formatMonthDay } from "../format-date/index.js";
|
|
|
47
49
|
|
|
48
50
|
const STORAGE_KEY = "zd-sidebar-open";
|
|
49
51
|
const GROUPED_TRAY_ITEM_DEPTH = 3;
|
|
52
|
+
const TOGGLE_BOX_SIZE = "1.5rem";
|
|
50
53
|
|
|
51
54
|
function ToggleChevron({
|
|
52
55
|
isExpanded,
|
|
@@ -73,6 +76,11 @@ function padLeft(depth: number, forCategory: boolean): string {
|
|
|
73
76
|
return `calc(${depth} * ${INDENT} + 1.25rem + 5px)`;
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
/** Puts a 1px vertical guide on the center of a category row's leading toggle box. */
|
|
80
|
+
function toggleGuideLeft(depth: number): string {
|
|
81
|
+
return `calc(${padLeft(depth, true)} + ${TOGGLE_BOX_SIZE} / 2 - 0.5px)`;
|
|
82
|
+
}
|
|
83
|
+
|
|
76
84
|
function getOpenSet(): Set<string> {
|
|
77
85
|
try {
|
|
78
86
|
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
@@ -274,10 +282,31 @@ export function SidebarTree({
|
|
|
274
282
|
const scope = getScope();
|
|
275
283
|
const activeSlug = useActiveSlug(navigation?.roots ?? nodes, currentSlug, currentPath);
|
|
276
284
|
const query = signal("");
|
|
277
|
-
const { selected, selectedNodes, broader, labels, canRestore, changeScope, save, navRef, scopeControls } = useSidebarScope({
|
|
285
|
+
const { selected, selectedNodes, broader, labels, canRestore, changeScope, save, navRef, scopeControls: baseScopeControls } = useSidebarScope({
|
|
278
286
|
nodes, navigation, activeSlug, query,
|
|
279
287
|
locale: localeProp ?? localeLinks?.find((link) => link.active)?.code ?? "en",
|
|
280
288
|
});
|
|
289
|
+
const branchPreview = createBranchPreview(navRef);
|
|
290
|
+
const scopeControls: TreeScopeControls | undefined = baseScopeControls
|
|
291
|
+
? { ...baseScopeControls, preview: branchPreview.active }
|
|
292
|
+
: undefined;
|
|
293
|
+
if (navigation) {
|
|
294
|
+
scope.onActivate(() => {
|
|
295
|
+
const dispose = branchPreview.activate();
|
|
296
|
+
const onNavigate = () => branchPreview.clear();
|
|
297
|
+
document.addEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
|
|
298
|
+
return () => {
|
|
299
|
+
document.removeEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
|
|
300
|
+
dispose();
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
// Scope changes, filtering and remounted rows invalidate a transient preview.
|
|
304
|
+
scope.effect(() => {
|
|
305
|
+
void selected.value;
|
|
306
|
+
void query.value;
|
|
307
|
+
branchPreview.clear();
|
|
308
|
+
});
|
|
309
|
+
}
|
|
281
310
|
const showingRootMenu = signal(false);
|
|
282
311
|
const filterRef: Ref<HTMLInputElement> = { current: null };
|
|
283
312
|
const filterPlaceholder = signal("Filter...");
|
|
@@ -386,14 +415,10 @@ export function SidebarTree({
|
|
|
386
415
|
</button>
|
|
387
416
|
<Show when={canRestore}>{() => (
|
|
388
417
|
<button type="button" data-sidebar-restore on:click={() => changeScope(null)}
|
|
389
|
-
|
|
418
|
+
aria-label={labels.restore} data-sidebar-tip={labels.restoreTip}
|
|
390
419
|
style="width:40px;height:40px"
|
|
391
|
-
class="grid place-items-center shrink-0 rounded text-muted hover:text-
|
|
392
|
-
<
|
|
393
|
-
<circle cx="12" cy="12" r="10" />
|
|
394
|
-
<circle cx="12" cy="12" r="6" />
|
|
395
|
-
<circle cx="12" cy="12" r="2" />
|
|
396
|
-
</svg>
|
|
420
|
+
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">
|
|
421
|
+
<RestoreArrow class="h-[1.0625rem] w-[1.0625rem]" />
|
|
397
422
|
</button>
|
|
398
423
|
)}</Show>
|
|
399
424
|
</div>
|
|
@@ -443,8 +468,12 @@ export function SidebarTree({
|
|
|
443
468
|
}
|
|
444
469
|
SidebarTree.displayName = "SidebarTree";
|
|
445
470
|
|
|
471
|
+
interface TreeScopeControls extends ScopeControls {
|
|
472
|
+
/** This sidebar instance's transient branch preview (occurrence id). */
|
|
473
|
+
preview: ReadonlySignal<string | null>;
|
|
474
|
+
}
|
|
446
475
|
interface ActiveProps {
|
|
447
|
-
scopeControls?:
|
|
476
|
+
scopeControls?: TreeScopeControls;
|
|
448
477
|
currentSlug: ReadonlySignal<string | undefined>;
|
|
449
478
|
forceOpen: ReadonlySignal<boolean>;
|
|
450
479
|
}
|
|
@@ -547,6 +576,7 @@ function TrayItem({
|
|
|
547
576
|
isLast,
|
|
548
577
|
showDate = false,
|
|
549
578
|
depth = 1,
|
|
579
|
+
guideLeft,
|
|
550
580
|
locale,
|
|
551
581
|
dateFormats,
|
|
552
582
|
}: {
|
|
@@ -556,6 +586,8 @@ function TrayItem({
|
|
|
556
586
|
isLast: ReadonlySignal<boolean>;
|
|
557
587
|
showDate?: boolean;
|
|
558
588
|
depth?: number;
|
|
589
|
+
/** Draws this row's connector from the parent's toggle center instead of its depth column. */
|
|
590
|
+
guideLeft?: string;
|
|
559
591
|
locale?: string;
|
|
560
592
|
dateFormats?: ResolvedDateFormats;
|
|
561
593
|
}) {
|
|
@@ -574,6 +606,12 @@ function TrayItem({
|
|
|
574
606
|
depth={depth}
|
|
575
607
|
isLast={isLast}
|
|
576
608
|
topPad="var(--spacing-vsp-2xs)"
|
|
609
|
+
left={guideLeft}
|
|
610
|
+
width={
|
|
611
|
+
guideLeft
|
|
612
|
+
? `calc(${padLeft(depth, false)} - ${CONNECTOR_OFFSET} - ${guideLeft})`
|
|
613
|
+
: undefined
|
|
614
|
+
}
|
|
577
615
|
/>
|
|
578
616
|
<a
|
|
579
617
|
href={computed(() => item.value.href ?? "")}
|
|
@@ -621,19 +659,23 @@ function ConnectorLinesLive({
|
|
|
621
659
|
depth,
|
|
622
660
|
isLast,
|
|
623
661
|
topPad,
|
|
662
|
+
left,
|
|
663
|
+
width,
|
|
624
664
|
}: {
|
|
625
665
|
depth: number;
|
|
626
666
|
isLast: ReadonlySignal<boolean>;
|
|
627
667
|
topPad: string;
|
|
668
|
+
left?: string;
|
|
669
|
+
width?: string;
|
|
628
670
|
}) {
|
|
629
671
|
return (
|
|
630
672
|
<Show
|
|
631
673
|
when={isLast}
|
|
632
674
|
fallback={() => (
|
|
633
|
-
<ConnectorLines depth={depth} isLast={false} topPad={topPad} />
|
|
675
|
+
<ConnectorLines depth={depth} isLast={false} topPad={topPad} left={left} width={width} />
|
|
634
676
|
)}
|
|
635
677
|
>
|
|
636
|
-
{() => <ConnectorLines depth={depth} isLast={true} topPad={topPad} />}
|
|
678
|
+
{() => <ConnectorLines depth={depth} isLast={true} topPad={topPad} left={left} width={width} />}
|
|
637
679
|
</Show>
|
|
638
680
|
);
|
|
639
681
|
}
|
|
@@ -746,7 +788,15 @@ function TrayGroupNode({
|
|
|
746
788
|
</div>
|
|
747
789
|
<Show when={expanded}>
|
|
748
790
|
{() => (
|
|
749
|
-
<div>
|
|
791
|
+
<div class="relative">
|
|
792
|
+
<div
|
|
793
|
+
class="absolute border-l border-dashed border-muted"
|
|
794
|
+
style={{
|
|
795
|
+
left: toggleGuideLeft(1),
|
|
796
|
+
top: "calc(-1 * var(--spacing-vsp-xs))",
|
|
797
|
+
height: "var(--spacing-vsp-xs)",
|
|
798
|
+
}}
|
|
799
|
+
/>
|
|
750
800
|
<For each={items} by={(item) => item.slug}>
|
|
751
801
|
{(item, index) => (
|
|
752
802
|
<TrayItem
|
|
@@ -759,6 +809,7 @@ function TrayGroupNode({
|
|
|
759
809
|
locale={locale}
|
|
760
810
|
dateFormats={dateFormats}
|
|
761
811
|
depth={GROUPED_TRAY_ITEM_DEPTH}
|
|
812
|
+
guideLeft={toggleGuideLeft(1)}
|
|
762
813
|
/>
|
|
763
814
|
)}
|
|
764
815
|
</For>
|
|
@@ -865,18 +916,33 @@ function CategoryNode({
|
|
|
865
916
|
if (scopeControls) open.value = scopeControls.isOpen(occurrenceKey(), scopeControls.defaultOpen(node.value));
|
|
866
917
|
});
|
|
867
918
|
const expanded = computed(() => forceOpen.value || open.value);
|
|
919
|
+
const focusedRoot = computed(() => !!scopeControls?.isFocusedRoot(node.value.occurrenceId));
|
|
920
|
+
const previewed = computed(() => !!node.value.occurrenceId && scopeControls?.preview.value === node.value.occurrenceId);
|
|
921
|
+
const arrived = computed(() => !!node.value.occurrenceId && scopeControls?.arrived.value === node.value.occurrenceId);
|
|
868
922
|
const focusButton = scopeControls ? (
|
|
869
923
|
<button type="button" data-sidebar-focus data-sidebar-focus-scope={computed(() => node.value.occurrenceId ?? "")}
|
|
870
924
|
on:click={() => scopeControls.focus(node.value)}
|
|
871
|
-
aria-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
925
|
+
aria-disabled={computed(() => (focusedRoot.value ? "true" : null))}
|
|
926
|
+
tabindex={computed(() => (focusedRoot.value ? -1 : null))}
|
|
927
|
+
aria-label={computed(() => (focusedRoot.value ? scopeControls.labels.focused(node.value.label) : scopeControls.labels.focus(node.value.label)))}
|
|
928
|
+
data-sidebar-tip={computed(() => (focusedRoot.value ? scopeControls.labels.focusedTip : scopeControls.labels.focusTip))}
|
|
929
|
+
data-sidebar-tip-detail={computed(() => node.value.label)}
|
|
930
|
+
class={computed(() => `ml-auto grid shrink-0 place-items-center rounded w-[1.875rem] h-[1.875rem] ${
|
|
931
|
+
focusedRoot.value
|
|
932
|
+
? `cursor-default opacity-50 ${active.value ? "text-bg" : "text-muted"}`
|
|
933
|
+
: active.value
|
|
934
|
+
? "text-bg hover:bg-bg/10 focus-visible:bg-bg/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
|
935
|
+
: "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"
|
|
936
|
+
}`)}>
|
|
937
|
+
<FocusFrame class="h-[1.125rem] w-[1.125rem]" />
|
|
875
938
|
</button>
|
|
876
939
|
) : null;
|
|
877
940
|
const paddingLeft = padLeft(depth, true);
|
|
878
941
|
return (
|
|
879
942
|
<div
|
|
943
|
+
data-sidebar-branch
|
|
944
|
+
data-sidebar-preview={computed(() => (previewed.value ? "" : null))}
|
|
945
|
+
data-sidebar-arrived={computed(() => (arrived.value ? "" : null))}
|
|
880
946
|
class={computed(
|
|
881
947
|
() =>
|
|
882
948
|
`${depth === 0 ? "border-t border-muted first:border-t-0" : ""} ${depth >= 1 && !isLast.value ? "relative" : ""}`,
|
|
@@ -888,11 +954,12 @@ function CategoryNode({
|
|
|
888
954
|
{() => (
|
|
889
955
|
<div
|
|
890
956
|
class="absolute border-l border-solid border-muted z-local-1"
|
|
957
|
+
data-tree-connector
|
|
891
958
|
style={{ left: connectorLeft(depth), top: "0px", bottom: "0px" }}
|
|
892
959
|
/>
|
|
893
960
|
)}
|
|
894
961
|
</Show>
|
|
895
|
-
<div class="relative">
|
|
962
|
+
<div class="relative" data-sidebar-row data-sidebar-current={computed(() => (active.value ? "" : null))}>
|
|
896
963
|
<ConnectorLinesLive
|
|
897
964
|
depth={depth}
|
|
898
965
|
isLast={isLast}
|
|
@@ -980,7 +1047,7 @@ function CategoryNode({
|
|
|
980
1047
|
</div>
|
|
981
1048
|
<Show when={expanded}>
|
|
982
1049
|
{() => (
|
|
983
|
-
<div>
|
|
1050
|
+
<div data-sidebar-children>
|
|
984
1051
|
<NodeList
|
|
985
1052
|
nodes={computed(() => node.value.children)}
|
|
986
1053
|
currentSlug={currentSlug}
|