create-zudo-doc 0.2.21 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/compose.d.ts +6 -3
  2. package/dist/compose.js +6 -4
  3. package/dist/features/image-enlarge.d.ts +22 -12
  4. package/dist/features/image-enlarge.js +23 -191
  5. package/dist/scaffold.d.ts +11 -0
  6. package/dist/scaffold.js +51 -24
  7. package/dist/settings-gen.js +4 -0
  8. package/dist/zfb-config-gen.d.ts +16 -10
  9. package/dist/zfb-config-gen.js +34 -239
  10. package/package.json +1 -1
  11. package/templates/base/pages/_mdx-components.ts +64 -185
  12. package/templates/base/pages/index.tsx +1 -1
  13. package/templates/base/pages/lib/_body-end-islands.tsx +6 -13
  14. package/templates/base/pages/lib/_category-nav.tsx +30 -115
  15. package/templates/base/pages/lib/_category-tree-nav.tsx +38 -65
  16. package/templates/base/pages/lib/_compose-meta-title.ts +2 -6
  17. package/templates/base/pages/lib/_doc-body-end.tsx +3 -35
  18. package/templates/base/pages/lib/_doc-content-header.tsx +10 -111
  19. package/templates/base/pages/lib/_doc-history-area.tsx +19 -211
  20. package/templates/base/pages/lib/_doc-metainfo-area.tsx +10 -113
  21. package/templates/base/pages/lib/_doc-page-renderer.tsx +22 -183
  22. package/templates/base/pages/lib/_doc-page-shell.tsx +17 -251
  23. package/templates/base/pages/lib/_doc-pager.tsx +4 -74
  24. package/templates/base/pages/lib/_doc-route-entries.ts +43 -177
  25. package/templates/base/pages/lib/_doc-route-paths.ts +16 -101
  26. package/templates/base/pages/lib/_doc-tags-area.tsx +14 -77
  27. package/templates/base/pages/lib/_extract-headings.ts +21 -295
  28. package/templates/base/pages/lib/_footer-with-defaults.tsx +37 -225
  29. package/templates/base/pages/lib/_frontmatter-preview-data.ts +5 -31
  30. package/templates/base/pages/lib/_head-with-defaults.tsx +13 -138
  31. package/templates/base/pages/lib/_header-with-defaults.tsx +24 -324
  32. package/templates/base/pages/lib/_inline-version-switcher.tsx +16 -78
  33. package/templates/base/pages/lib/_math-block.tsx +4 -63
  34. package/templates/base/pages/lib/_nav-data-prep.ts +32 -51
  35. package/templates/base/pages/lib/_nav-source-cache.ts +12 -57
  36. package/templates/base/pages/lib/_nav-source-docs.ts +33 -233
  37. package/templates/base/pages/lib/_search-widget-script.ts +2 -470
  38. package/templates/base/pages/lib/_search-widget.tsx +9 -195
  39. package/templates/base/pages/lib/_sidebar-prepaint.tsx +6 -59
  40. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +16 -118
  41. package/templates/base/pages/lib/_site-tree-nav.tsx +29 -80
  42. package/templates/base/pages/lib/doc-page-props.ts +26 -44
  43. package/templates/base/pages/lib/locale-merge.ts +32 -145
  44. package/templates/base/pages/lib/route-enumerators.ts +52 -286
  45. package/templates/base/src/components/ai-chat-modal.tsx +9 -8
  46. package/templates/base/src/components/content/code-group.tsx +3 -76
  47. package/templates/base/src/components/content/content-admonition.tsx +4 -56
  48. package/templates/base/src/components/doc-history.tsx +9 -8
  49. package/templates/base/src/components/image-enlarge.tsx +11 -8
  50. package/templates/base/src/components/sidebar-toggle.tsx +6 -170
  51. package/templates/base/src/components/sidebar-tree.tsx +6 -548
  52. package/templates/base/src/config/color-scheme-utils.ts +34 -158
  53. package/templates/base/src/config/i18n.ts +9 -0
  54. package/templates/base/src/config/settings-types.ts +34 -172
  55. package/templates/base/src/config/z-index-tokens.ts +5 -4
  56. package/templates/base/src/styles/global.css +40 -587
  57. package/templates/base/src/utils/base.ts +1 -1
  58. package/templates/base/src/utils/docs.ts +47 -16
  59. package/templates/base/src/utils/github.ts +12 -9
  60. package/templates/base/src/utils/nav-scope.ts +13 -42
  61. package/templates/base/src/utils/sidebar.ts +18 -86
  62. package/templates/base/src/utils/slug.ts +10 -53
  63. package/templates/base/src/utils/smart-break.tsx +12 -120
  64. package/templates/base/src/utils/tags.ts +25 -68
  65. package/templates/features/bodyFootUtil/files/src/utils/github.ts +12 -9
  66. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +13 -39
  67. package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -636
  68. package/templates/features/docHistory/files/src/types/doc-history.ts +7 -23
  69. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +35 -201
  70. package/templates/features/i18n/files/pages/[locale]/index.tsx +1 -1
  71. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +8 -269
  72. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +6 -99
  73. package/templates/features/tagGovernance/files/scripts/tags-audit.ts +67 -515
  74. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +21 -73
  75. package/templates/base/pages/404.tsx +0 -61
  76. package/templates/base/pages/robots.txt.tsx +0 -29
  77. package/templates/base/pages/sitemap.xml.tsx +0 -59
  78. package/templates/base/plugins/connect-adapter.mjs +0 -169
  79. package/templates/base/plugins/copy-public-plugin.mjs +0 -58
  80. package/templates/base/plugins/search-index-plugin.mjs +0 -66
  81. package/templates/base/scripts/gen-z-index.mjs +0 -157
  82. package/templates/base/src/components/mermaid-enlarge.tsx +0 -490
  83. package/templates/base/src/components/site-tree-nav.tsx +0 -220
  84. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +0 -47
  85. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +0 -111
  86. package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +0 -59
  87. package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +0 -39
  88. package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +0 -43
  89. package/templates/features/docTags/files/pages/docs/tags/index.tsx +0 -20
  90. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +0 -93
  91. package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +0 -48
  92. package/templates/features/versioning/files/pages/docs/versions.tsx +0 -20
@@ -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";