create-zudo-doc 0.2.21 → 0.2.22

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 (71) hide show
  1. package/dist/features/image-enlarge.d.ts +10 -2
  2. package/dist/features/image-enlarge.js +10 -2
  3. package/dist/scaffold.js +20 -22
  4. package/dist/zfb-config-gen.d.ts +16 -10
  5. package/dist/zfb-config-gen.js +34 -239
  6. package/package.json +1 -1
  7. package/templates/base/pages/lib/_body-end-islands.tsx +6 -13
  8. package/templates/base/pages/lib/_category-nav.tsx +30 -115
  9. package/templates/base/pages/lib/_category-tree-nav.tsx +38 -65
  10. package/templates/base/pages/lib/_compose-meta-title.ts +2 -6
  11. package/templates/base/pages/lib/_doc-body-end.tsx +3 -35
  12. package/templates/base/pages/lib/_doc-content-header.tsx +10 -111
  13. package/templates/base/pages/lib/_doc-history-area.tsx +19 -211
  14. package/templates/base/pages/lib/_doc-metainfo-area.tsx +10 -113
  15. package/templates/base/pages/lib/_doc-page-renderer.tsx +22 -183
  16. package/templates/base/pages/lib/_doc-page-shell.tsx +17 -251
  17. package/templates/base/pages/lib/_doc-pager.tsx +4 -74
  18. package/templates/base/pages/lib/_doc-route-entries.ts +43 -177
  19. package/templates/base/pages/lib/_doc-route-paths.ts +16 -101
  20. package/templates/base/pages/lib/_doc-tags-area.tsx +14 -77
  21. package/templates/base/pages/lib/_footer-with-defaults.tsx +37 -225
  22. package/templates/base/pages/lib/_frontmatter-preview-data.ts +5 -31
  23. package/templates/base/pages/lib/_head-with-defaults.tsx +13 -138
  24. package/templates/base/pages/lib/_header-with-defaults.tsx +24 -324
  25. package/templates/base/pages/lib/_inline-version-switcher.tsx +16 -78
  26. package/templates/base/pages/lib/_nav-data-prep.ts +32 -51
  27. package/templates/base/pages/lib/_nav-source-cache.ts +12 -57
  28. package/templates/base/pages/lib/_nav-source-docs.ts +33 -233
  29. package/templates/base/pages/lib/_search-widget-script.ts +2 -470
  30. package/templates/base/pages/lib/_search-widget.tsx +9 -195
  31. package/templates/base/pages/lib/_sidebar-prepaint.tsx +6 -59
  32. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +16 -118
  33. package/templates/base/pages/lib/_site-tree-nav.tsx +29 -80
  34. package/templates/base/pages/lib/doc-page-props.ts +26 -44
  35. package/templates/base/pages/lib/locale-merge.ts +32 -145
  36. package/templates/base/pages/lib/route-enumerators.ts +52 -286
  37. package/templates/base/pages/robots.txt.tsx +2 -26
  38. package/templates/base/src/components/ai-chat-modal.tsx +9 -8
  39. package/templates/base/src/components/doc-history.tsx +9 -8
  40. package/templates/base/src/components/image-enlarge.tsx +11 -8
  41. package/templates/base/src/components/sidebar-toggle.tsx +6 -170
  42. package/templates/base/src/components/sidebar-tree.tsx +6 -548
  43. package/templates/base/src/components/site-tree-nav.tsx +6 -220
  44. package/templates/base/src/config/color-scheme-utils.ts +34 -158
  45. package/templates/base/src/config/i18n.ts +9 -0
  46. package/templates/base/src/config/z-index-tokens.ts +5 -4
  47. package/templates/base/src/styles/global.css +5 -579
  48. package/templates/base/src/utils/base.ts +1 -1
  49. package/templates/base/src/utils/docs.ts +47 -16
  50. package/templates/base/src/utils/github.ts +12 -9
  51. package/templates/base/src/utils/nav-scope.ts +13 -42
  52. package/templates/base/src/utils/sidebar.ts +18 -86
  53. package/templates/base/src/utils/slug.ts +10 -53
  54. package/templates/base/src/utils/smart-break.tsx +12 -120
  55. package/templates/base/src/utils/tags.ts +25 -68
  56. package/templates/features/bodyFootUtil/files/src/utils/github.ts +12 -9
  57. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +13 -39
  58. package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -636
  59. package/templates/features/docHistory/files/src/types/doc-history.ts +7 -23
  60. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +35 -201
  61. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +8 -269
  62. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +6 -99
  63. package/templates/features/tagGovernance/files/scripts/tags-audit.ts +67 -515
  64. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +21 -73
  65. package/templates/base/plugins/connect-adapter.mjs +0 -169
  66. package/templates/base/plugins/search-index-plugin.mjs +0 -66
  67. package/templates/base/scripts/gen-z-index.mjs +0 -157
  68. package/templates/base/src/components/mermaid-enlarge.tsx +0 -490
  69. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +0 -47
  70. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +0 -111
  71. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +0 -93
@@ -1,548 +1,6 @@
1
- "use client";
2
-
3
- // Use preact hook entrypoints directly the "react" → "preact/compat" alias
4
- // lets us consume React-typed components in this Preact app (configured
5
- // project-wide).
6
- import { useState, useCallback, useEffect, useMemo, useRef } from "preact/hooks";
7
- import type { NavNode } from "@/utils/docs";
8
- import type { LocaleLink } from "@/types/locale";
9
- import { INDENT, BASE_PAD, connectorLeft, ConnectorLines, CategoryLinkIcon } from "./tree-nav-shared";
10
- // BARE ThemeToggle (#2012 E2) — this footer toggle renders inside the
11
- // SidebarToggle island, so it must NOT bring its own island wrapper.
12
- import { ThemeToggle } from "@takazudo/zudo-doc/theme-toggle";
13
- import { smartBreakToHtml } from "@/utils/smart-break";
14
-
15
- function ToggleChevron({ isExpanded, className }: { isExpanded: boolean; className?: string }) {
16
- return (
17
- <svg
18
- xmlns="http://www.w3.org/2000/svg"
19
- className={`h-[0.625rem] w-[0.625rem] shrink-0 transition-transform duration-150 ${isExpanded ? "rotate-90" : ""} ${className ?? ""}`}
20
- fill="none"
21
- viewBox="0 0 24 24"
22
- stroke="currentColor"
23
- strokeWidth={2}
24
- aria-hidden="true"
25
- >
26
- <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
27
- </svg>
28
- );
29
- }
30
-
31
- const STORAGE_KEY = "zd-sidebar-open";
32
-
33
- function padLeft(depth: number, forCategory: boolean): string {
34
- if (depth === 0) return `calc(${BASE_PAD} + ${forCategory ? "0.15rem" : "0rem"})`;
35
- return `calc(${depth} * ${INDENT} + 1.25rem + 5px)`;
36
- }
37
-
38
- function getOpenSet(): Set<string> {
39
- try {
40
- const raw = sessionStorage.getItem(STORAGE_KEY);
41
- if (!raw) return new Set();
42
- const parsed: unknown = JSON.parse(raw);
43
- return Array.isArray(parsed) ? new Set(parsed.filter((v): v is string => typeof v === "string")) : new Set();
44
- } catch {
45
- return new Set();
46
- }
47
- }
48
-
49
- function saveOpenSet(set: Set<string>) {
50
- try {
51
- sessionStorage.setItem(STORAGE_KEY, JSON.stringify([...set]));
52
- } catch {
53
- // ignore
54
- }
55
- }
56
-
57
- function normalizePath(p: string): string {
58
- return p.replace(/\/$/, "") || "/";
59
- }
60
-
61
- /** Find the slug of the node whose href matches the given pathname */
62
- function findActiveSlug(nodes: NavNode[], pathname: string): string | undefined {
63
- for (const node of nodes) {
64
- if (node.href && normalizePath(node.href) === pathname) return node.slug;
65
- const found = findActiveSlug(node.children, pathname);
66
- // "" is the canonical root-index slug (#1891) — a truthiness check
67
- // would discard a legitimate root match.
68
- if (found !== undefined) return found;
69
- }
70
- return undefined;
71
- }
72
-
73
- /** Track current active slug, updating on View Transition navigations */
74
- function useActiveSlug(nodes: NavNode[], initial?: string): string | undefined {
75
- const [slug, setSlug] = useState(initial);
76
-
77
- useEffect(() => {
78
- const update = () => {
79
- const pathname = normalizePath(window.location.pathname);
80
- const found = findActiveSlug(nodes, pathname);
81
- if (found !== undefined) setSlug(found);
82
- };
83
- update();
84
- // zfb's `<ViewTransitions />` does a real page load on every
85
- // navigation, so `DOMContentLoaded` is the post-navigate signal.
86
- document.addEventListener("DOMContentLoaded", update);
87
- return () => document.removeEventListener("DOMContentLoaded", update);
88
- }, [nodes]);
89
-
90
- return slug;
91
- }
92
-
93
- function filterTree(nodes: NavNode[], query: string): NavNode[] {
94
- return nodes.reduce<NavNode[]>((acc, node) => {
95
- const matchesLabel = node.label.toLowerCase().includes(query.toLowerCase());
96
- const filteredChildren = node.children.length > 0
97
- ? filterTree(node.children, query)
98
- : [];
99
-
100
- if (matchesLabel || filteredChildren.length > 0) {
101
- acc.push({
102
- ...node,
103
- children: matchesLabel ? node.children : filteredChildren,
104
- });
105
- }
106
- return acc;
107
- }, []);
108
- }
109
-
110
- interface RootMenuItem {
111
- label: string;
112
- href: string;
113
- children?: RootMenuItem[];
114
- }
115
-
116
- function RootMenuItemEntry({ item }: { item: RootMenuItem }) {
117
- const [expanded, setExpanded] = useState(false);
118
- const hasChildren = item.children && item.children.length > 0;
119
-
120
- return (
121
- <div className="border-t border-muted">
122
- <div className="flex items-center">
123
- <a
124
- href={item.href}
125
- className="flex flex-1 items-center gap-hsp-xs px-hsp-sm py-vsp-xs text-small font-semibold text-fg hover:text-accent hover:underline break-words"
126
- >
127
- <CategoryLinkIcon className="w-[14px]" />
128
- <span dangerouslySetInnerHTML={{ __html: smartBreakToHtml(item.label) }} />
129
- </a>
130
- {hasChildren && (
131
- <button
132
- type="button"
133
- onClick={() => setExpanded((prev) => !prev)}
134
- className="flex items-center justify-center px-hsp-sm py-vsp-xs text-muted hover:text-fg"
135
- aria-expanded={expanded}
136
- aria-label={expanded ? `Collapse ${item.label}` : `Expand ${item.label}`}
137
- >
138
- <ToggleChevron isExpanded={expanded} className="text-muted" />
139
- </button>
140
- )}
141
- </div>
142
- {hasChildren && expanded && (
143
- <div className="pb-vsp-xs">
144
- {item.children!.map((child) => (
145
- <a
146
- key={child.href}
147
- href={child.href}
148
- className="block pl-hsp-xl pr-hsp-sm py-vsp-2xs text-small text-muted hover:text-accent hover:underline break-words"
149
- >
150
- <span dangerouslySetInnerHTML={{ __html: smartBreakToHtml(child.label) }} />
151
- </a>
152
- ))}
153
- </div>
154
- )}
155
- </div>
156
- );
157
- }
158
-
159
- interface SidebarTreeProps {
160
- nodes: NavNode[];
161
- currentSlug?: string;
162
- rootMenuItems?: RootMenuItem[];
163
- backToMenuLabel?: string;
164
- localeLinks?: LocaleLink[];
165
- themeDefaultMode?: "light" | "dark";
166
- }
167
-
168
- function SidebarFooter({ links, themeDefaultMode }: { links?: LocaleLink[]; themeDefaultMode?: "light" | "dark" }) {
169
- if (!links && !themeDefaultMode) return null;
170
- return (
171
- // pb-[50vh] provides scroll room so the footer doesn't sit at the very bottom of the viewport
172
- <div className="lg:hidden flex items-center gap-hsp-md border-t border-muted px-hsp-sm py-vsp-xs pb-[50vh] text-small">
173
- {themeDefaultMode && <ThemeToggle defaultMode={themeDefaultMode} />}
174
- {links && links.map((link, i) => (
175
- <span key={link.href} className="flex items-center gap-hsp-xs">
176
- {i > 0 && <span className="text-muted">/</span>}
177
- {link.active ? (
178
- <span aria-current="true" className="font-medium text-fg">{link.label}</span>
179
- ) : (
180
- <a href={link.href} lang={link.code} className="text-muted hover:text-fg">
181
- {link.label}
182
- </a>
183
- )}
184
- </span>
185
- ))}
186
- </div>
187
- );
188
- }
189
-
190
- export default function SidebarTree({ nodes, currentSlug, rootMenuItems, backToMenuLabel, localeLinks, themeDefaultMode }: SidebarTreeProps) {
191
- const activeSlug = useActiveSlug(nodes, currentSlug);
192
- const [query, setQuery] = useState("");
193
- const [showingRootMenu, setShowingRootMenu] = useState(false);
194
- const filterRef = useRef<HTMLInputElement>(null);
195
- const [filterPlaceholder, setFilterPlaceholder] = useState("Filter...");
196
-
197
- // Detect OS to show appropriate keyboard shortcut in placeholder
198
- useEffect(() => {
199
- const platform = (navigator as { userAgentData?: { platform: string } }).userAgentData?.platform ?? navigator.platform;
200
- const isMac = /mac/i.test(platform);
201
- setFilterPlaceholder(isMac ? "Filter... (\u2318 + /)" : "Filter... (Ctrl + /)");
202
- }, []);
203
-
204
- // Global shortcut: Cmd+/ (Mac) or Ctrl+/ to focus the filter input
205
- useEffect(() => {
206
- function handleKeyDown(e: KeyboardEvent) {
207
- if (e.isComposing) return;
208
- if (e.key === "/" && (e.metaKey || e.ctrlKey)) {
209
- const el = filterRef.current;
210
- if (!el || el.offsetParent === null) return; // skip if hidden
211
- e.preventDefault();
212
- el.focus();
213
- el.select();
214
- }
215
- }
216
- document.addEventListener("keydown", handleKeyDown);
217
- return () => document.removeEventListener("keydown", handleKeyDown);
218
- }, []);
219
-
220
- const filteredNodes = useMemo(
221
- () => (query ? filterTree(nodes, query) : nodes),
222
- [nodes, query],
223
- );
224
-
225
- const footer = useMemo(
226
- () => (localeLinks || themeDefaultMode) ? <SidebarFooter links={localeLinks} themeDefaultMode={themeDefaultMode} /> : null,
227
- [localeLinks, themeDefaultMode],
228
- );
229
-
230
- // Root menu view: show headerNav items as a simple list (Docusaurus-style)
231
- if (showingRootMenu && rootMenuItems) {
232
- return (
233
- <nav>
234
- <button
235
- type="button"
236
- onClick={() => setShowingRootMenu(false)}
237
- className="flex w-full items-center gap-hsp-xs px-hsp-sm py-vsp-xs text-left text-small text-muted hover:text-fg border-b border-muted"
238
- >
239
- <svg className="h-icon-sm w-icon-sm shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
240
- <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
241
- </svg>
242
- {backToMenuLabel ?? "Back to main menu"}
243
- </button>
244
- {rootMenuItems.map((item) => (
245
- <RootMenuItemEntry key={item.href} item={item} />
246
- ))}
247
- {footer}
248
- </nav>
249
- );
250
- }
251
-
252
- // Top page: show only header nav links, no doc tree or filter.
253
- // Derived from activeSlug (runtime-synced) so it stays correct across View
254
- // Transitions. Must be an undefined check, not truthiness: "" is the
255
- // canonical root-index doc slug (#1891) and gets the full tree.
256
- if (activeSlug === undefined && rootMenuItems) {
257
- return (
258
- <nav>
259
- {rootMenuItems.map((item) => (
260
- <RootMenuItemEntry key={item.href} item={item} />
261
- ))}
262
- {footer}
263
- </nav>
264
- );
265
- }
266
-
267
- return (
268
- <nav>
269
- {rootMenuItems && (
270
- <button
271
- type="button"
272
- onClick={() => setShowingRootMenu(true)}
273
- className="lg:hidden flex w-full items-center gap-hsp-xs px-hsp-sm py-vsp-xs text-left text-small text-muted hover:text-fg border-b border-muted"
274
- >
275
- <svg className="h-icon-sm w-icon-sm shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
276
- <path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
277
- </svg>
278
- {backToMenuLabel ?? "Back to main menu"}
279
- </button>
280
- )}
281
- <div className="px-hsp-sm py-vsp-xs">
282
- <div className="flex items-center gap-hsp-xs bg-surface rounded px-hsp-sm py-vsp-2xs">
283
- <svg className="h-[14px] w-[14px] text-muted shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
284
- <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
285
- </svg>
286
- <input
287
- ref={filterRef}
288
- type="text"
289
- placeholder={filterPlaceholder}
290
- value={query}
291
- onInput={(e) => setQuery(e.currentTarget.value)}
292
- className="bg-transparent text-small outline-none w-full text-fg placeholder:text-muted"
293
- />
294
- </div>
295
- </div>
296
- <NodeList
297
- nodes={filteredNodes}
298
- currentSlug={activeSlug}
299
- depth={0}
300
- forceOpen={!!query}
301
- />
302
- {footer}
303
- </nav>
304
- );
305
- }
306
-
307
- function NodeList({
308
- nodes,
309
- currentSlug,
310
- depth,
311
- forceOpen,
312
- }: {
313
- nodes: NavNode[];
314
- currentSlug?: string;
315
- depth: number;
316
- forceOpen: boolean;
317
- }) {
318
- return (
319
- <>
320
- {nodes.map((node, index) => {
321
- const isLast = index === nodes.length - 1;
322
- return node.children.length > 0 ? (
323
- <CategoryNode
324
- key={node.slug}
325
- node={node}
326
- currentSlug={currentSlug}
327
- depth={depth}
328
- isLast={isLast}
329
- forceOpen={forceOpen}
330
- />
331
- ) : (
332
- <LeafNode
333
- key={node.slug}
334
- node={node}
335
- currentSlug={currentSlug}
336
- depth={depth}
337
- isLast={isLast}
338
- />
339
- );
340
- })}
341
- </>
342
- );
343
- }
344
-
345
- /** Check if currentSlug is anywhere in this node's subtree */
346
- function subtreeContainsSlug(node: NavNode, slug?: string): boolean {
347
- if (!slug) return false;
348
- if (node.slug === slug) return true;
349
- return node.children.some((child) => subtreeContainsSlug(child, slug));
350
- }
351
-
352
- function CategoryNode({
353
- node,
354
- currentSlug,
355
- depth,
356
- isLast,
357
- forceOpen,
358
- }: {
359
- node: NavNode;
360
- currentSlug?: string;
361
- depth: number;
362
- isLast: boolean;
363
- forceOpen: boolean;
364
- }) {
365
- const containsCurrent = subtreeContainsSlug(node, currentSlug);
366
- const isActive = node.slug === currentSlug;
367
-
368
- // Initial state must match server render (no sessionStorage access)
369
- // to avoid hydration mismatch. Stored state is restored in useEffect below.
370
- const [open, setOpen] = useState(containsCurrent ? true : !node.collapsed);
371
-
372
- // Restore open state from sessionStorage after hydration
373
- useEffect(() => {
374
- const stored = getOpenSet();
375
- if (stored.has(node.slug) && !open) {
376
- setOpen(true);
377
- }
378
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
379
-
380
- // Auto-open category when navigation lands on a descendant
381
- useEffect(() => {
382
- if (subtreeContainsSlug(node, currentSlug) && !open) {
383
- setOpen(true);
384
- const stored = getOpenSet();
385
- stored.add(node.slug);
386
- saveOpenSet(stored);
387
- }
388
- }, [currentSlug]); // eslint-disable-line react-hooks/exhaustive-deps
389
-
390
- // Sync auto-opened state to sessionStorage so it persists across View Transitions
391
- useEffect(() => {
392
- if (open) {
393
- const stored = getOpenSet();
394
- if (!stored.has(node.slug)) {
395
- stored.add(node.slug);
396
- saveOpenSet(stored);
397
- }
398
- }
399
- }, [open, node.slug]);
400
-
401
- const toggle = useCallback(() => {
402
- setOpen((prev) => {
403
- const next = !prev;
404
- const stored = getOpenSet();
405
- if (next) {
406
- stored.add(node.slug);
407
- } else {
408
- stored.delete(node.slug);
409
- }
410
- saveOpenSet(stored);
411
- return next;
412
- });
413
- }, [node.slug]);
414
-
415
- const isExpanded = forceOpen || open;
416
- const paddingLeft = padLeft(depth, true);
417
-
418
- return (
419
- <div className={`${depth === 0 ? "border-t border-muted" : ""} ${depth >= 1 && !isLast ? "relative" : ""}`}>
420
- {depth >= 1 && !isLast && isExpanded && (
421
- <div
422
- className="absolute border-l border-solid border-muted z-local-1"
423
- style={{
424
- left: connectorLeft(depth),
425
- top: 0,
426
- bottom: 0,
427
- }}
428
- />
429
- )}
430
- <div className="relative">
431
- <ConnectorLines depth={depth} isLast={isLast} topPad="calc(0.15rem + var(--spacing-vsp-xs))" />
432
- {node.href ? (
433
- <div
434
- className={`flex w-full items-center text-small font-semibold pt-[0.15rem] ${isActive ? "bg-fg text-bg" : "text-fg"}`}
435
- >
436
- <a
437
- href={node.href}
438
- aria-current={isActive ? "page" : undefined}
439
- className={`flex-1 flex items-start gap-hsp-xs py-vsp-xs hover:underline focus:underline break-words ${isActive ? "text-bg" : "text-fg"}`}
440
- style={{ paddingLeft }}
441
- >
442
- {depth === 0 && (
443
- <span className="flex h-[1lh] items-center">
444
- <CategoryLinkIcon className={`w-[14px] ${isActive ? "text-bg" : ""}`} />
445
- </span>
446
- )}
447
- <span dangerouslySetInnerHTML={{ __html: smartBreakToHtml(node.label) }} />
448
- </a>
449
- <button
450
- type="button"
451
- onClick={toggle}
452
- className={`aspect-square flex items-center justify-center w-[1.5rem] border-y border-l hover:underline focus:underline ${isActive ? "border-bg/30" : "border-muted"}`}
453
- aria-expanded={isExpanded}
454
- aria-label={isExpanded ? `Collapse ${node.label}` : `Expand ${node.label}`}
455
- >
456
- <ToggleChevron isExpanded={isExpanded} className={isActive ? "text-bg" : "text-muted"} />
457
- </button>
458
- </div>
459
- ) : (
460
- <button
461
- type="button"
462
- onClick={toggle}
463
- className={`flex w-full items-center gap-hsp-md text-left text-small font-semibold py-vsp-xs text-fg hover:underline focus:underline break-words`}
464
- style={{ paddingLeft }}
465
- aria-expanded={isExpanded}
466
- aria-label={isExpanded ? `Collapse ${node.label}` : `Expand ${node.label}`}
467
- >
468
- <span className="aspect-square flex items-center justify-center w-[1.5rem] shrink-0 border border-muted">
469
- <ToggleChevron isExpanded={isExpanded} className="text-muted" />
470
- </span>
471
- <span dangerouslySetInnerHTML={{ __html: smartBreakToHtml(node.label) }} />
472
- </button>
473
- )}
474
- </div>
475
- {isExpanded && (
476
- <div>
477
- <NodeList
478
- nodes={node.children}
479
- currentSlug={currentSlug}
480
- depth={depth + 1}
481
- forceOpen={forceOpen}
482
- />
483
- </div>
484
- )}
485
- </div>
486
- );
487
- }
488
-
489
- function LeafNode({
490
- node,
491
- currentSlug,
492
- depth,
493
- isLast,
494
- }: {
495
- node: NavNode;
496
- currentSlug?: string;
497
- depth: number;
498
- isLast: boolean;
499
- }) {
500
- if (!node.href) return null;
501
- const isActive = node.slug === currentSlug;
502
- const isRoot = depth === 0;
503
- const paddingLeft = padLeft(depth, isRoot);
504
-
505
- // For nested last leaves, add visual breathing space as margin on the outer wrapper
506
- // rather than padding on the anchor — padding would grow the row box and throw off
507
- // the ConnectorLines geometry (which now uses topPad + 0.5lh of the row to land the
508
- // horizontal connector at the first-line midpoint).
509
- const outerClass = isRoot
510
- ? "border-t border-muted"
511
- : !isRoot && isLast
512
- ? "pb-vsp-md"
513
- : "";
514
-
515
- const topPad = isRoot
516
- ? "calc(var(--spacing-vsp-xs) + 0.15rem)"
517
- : "var(--spacing-vsp-2xs)";
518
-
519
- return (
520
- <div className={outerClass}>
521
- <div className="relative">
522
- <ConnectorLines depth={depth} isLast={isLast} topPad={topPad} />
523
- <a
524
- href={node.href}
525
- aria-current={isActive ? "page" : undefined}
526
- className={isRoot
527
- ? `flex items-start gap-hsp-xs py-[calc(var(--spacing-vsp-xs)+0.15rem)] pr-[4px] text-small font-semibold break-words ${
528
- isActive ? "bg-fg text-bg" : "text-fg hover:underline focus:underline"
529
- }`
530
- : `block py-vsp-2xs pr-[4px] text-small break-words ${
531
- isActive
532
- ? "bg-fg font-medium text-bg"
533
- : "text-muted hover:underline focus:underline"
534
- }`
535
- }
536
- style={{ paddingLeft }}
537
- >
538
- {isRoot && (
539
- <span className="flex h-[1lh] items-center">
540
- <CategoryLinkIcon className={`w-[14px] ${isActive ? "text-bg" : ""}`} />
541
- </span>
542
- )}
543
- <span dangerouslySetInnerHTML={{ __html: smartBreakToHtml(node.label) }} />
544
- </a>
545
- </div>
546
- </div>
547
- );
548
- }
1
+ // Thin re-export shim. SidebarTree moved into the package
2
+ // (`@takazudo/zudo-doc/sidebar-tree-island`) as part of the package-first
3
+ // migration (#2344, S2). Host code using `@/components/sidebar-tree`
4
+ // still resolves correctly; the implementation lives in the package.
5
+ export { SidebarTree } from "@takazudo/zudo-doc/sidebar-tree-island";
6
+ export type { SidebarTreeProps } from "@takazudo/zudo-doc/sidebar-tree-island";