@takazudo/zudo-doc 0.2.22 → 1.0.1

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 (103) hide show
  1. package/bin/zudo-doc.mjs +83 -0
  2. package/dist/eject/index.d.ts +33 -0
  3. package/dist/eject/index.js +229 -0
  4. package/dist/plugins/routes.d.ts +2 -0
  5. package/dist/plugins/routes.js +100 -0
  6. package/dist/preset.d.ts +47 -1
  7. package/dist/preset.js +29 -13
  8. package/dist/routes/404.d.ts +7 -0
  9. package/dist/routes/404.js +47 -0
  10. package/dist/routes/_chrome.d.ts +32 -0
  11. package/dist/routes/_chrome.js +365 -0
  12. package/dist/routes/_context.d.ts +56 -0
  13. package/dist/routes/_context.js +222 -0
  14. package/dist/routes/_docs-helpers.d.ts +43 -0
  15. package/dist/routes/_docs-helpers.js +173 -0
  16. package/dist/routes/_virtual.d.js +0 -0
  17. package/dist/routes/api-ai-chat.d.ts +5 -0
  18. package/dist/routes/api-ai-chat.js +24 -0
  19. package/dist/routes/docs-slug.d.ts +21 -0
  20. package/dist/routes/docs-slug.js +26 -0
  21. package/dist/routes/docs-tags-index.d.ts +7 -0
  22. package/dist/routes/docs-tags-index.js +11 -0
  23. package/dist/routes/docs-tags-tag.d.ts +23 -0
  24. package/dist/routes/docs-tags-tag.js +19 -0
  25. package/dist/routes/docs-versions.d.ts +7 -0
  26. package/dist/routes/docs-versions.js +11 -0
  27. package/dist/routes/index.d.ts +7 -0
  28. package/dist/routes/index.js +117 -0
  29. package/dist/routes/locale-docs-slug.d.ts +27 -0
  30. package/dist/routes/locale-docs-slug.js +40 -0
  31. package/dist/routes/locale-docs-tags-index.d.ts +18 -0
  32. package/dist/routes/locale-docs-tags-index.js +15 -0
  33. package/dist/routes/locale-docs-tags-tag.d.ts +25 -0
  34. package/dist/routes/locale-docs-tags-tag.js +22 -0
  35. package/dist/routes/locale-docs-versions.d.ts +18 -0
  36. package/dist/routes/locale-docs-versions.js +15 -0
  37. package/dist/routes/locale-index.d.ts +24 -0
  38. package/dist/routes/locale-index.js +132 -0
  39. package/dist/routes/robots.txt.d.ts +3 -0
  40. package/dist/routes/robots.txt.js +8 -0
  41. package/dist/routes/sitemap.xml.d.ts +5 -0
  42. package/dist/routes/sitemap.xml.js +31 -0
  43. package/dist/routes/v-docs-slug.d.ts +27 -0
  44. package/dist/routes/v-docs-slug.js +34 -0
  45. package/dist/routes/v-locale-docs-slug.d.ts +31 -0
  46. package/dist/routes/v-locale-docs-slug.js +50 -0
  47. package/dist/safelist.css +1 -1
  48. package/dist/settings.d.ts +15 -0
  49. package/eject/breadcrumb/breadcrumb.tsx +194 -0
  50. package/eject/breadcrumb/find-path.ts +26 -0
  51. package/eject/breadcrumb/index.ts +4 -0
  52. package/eject/breadcrumb/types.ts +32 -0
  53. package/eject/code-group/index.tsx +78 -0
  54. package/eject/content-admonition/index.tsx +50 -0
  55. package/eject/details/details.tsx +34 -0
  56. package/eject/details/index.ts +4 -0
  57. package/eject/doc-pager/index.tsx +91 -0
  58. package/eject/footer/footer.tsx +167 -0
  59. package/eject/footer/index.ts +19 -0
  60. package/eject/footer/types.ts +60 -0
  61. package/eject/header/header.tsx +725 -0
  62. package/eject/header/index.ts +36 -0
  63. package/eject/header/nav-active.ts +122 -0
  64. package/eject/header/nav-overflow-script.ts +302 -0
  65. package/eject/header/right-items.ts +78 -0
  66. package/eject/header/types.ts +69 -0
  67. package/eject/page-loading/index.ts +10 -0
  68. package/eject/page-loading/page-loading-overlay.tsx +103 -0
  69. package/eject/sidebar/index.ts +27 -0
  70. package/eject/sidebar/sidebar.tsx +137 -0
  71. package/eject/sidebar/types.ts +73 -0
  72. package/eject/tab-item/index.ts +7 -0
  73. package/eject/tab-item/tab-item.tsx +70 -0
  74. package/eject/theme-toggle/color-scheme-sync.ts +73 -0
  75. package/eject/theme-toggle/index.tsx +120 -0
  76. package/eject/toc/cx.ts +38 -0
  77. package/eject/toc/index.ts +11 -0
  78. package/eject/toc/mobile-toc.tsx +133 -0
  79. package/eject/toc/toc-title.ts +33 -0
  80. package/eject/toc/toc.tsx +119 -0
  81. package/eject/toc/types.ts +14 -0
  82. package/eject/toc/use-active-heading.ts +162 -0
  83. package/package.json +87 -7
  84. package/routes-src/404.tsx +49 -0
  85. package/routes-src/_chrome.tsx +470 -0
  86. package/routes-src/_context.ts +314 -0
  87. package/routes-src/_docs-helpers.ts +284 -0
  88. package/routes-src/_virtual.d.ts +15 -0
  89. package/routes-src/api-ai-chat.tsx +46 -0
  90. package/routes-src/docs-slug.tsx +43 -0
  91. package/routes-src/docs-tags-index.tsx +14 -0
  92. package/routes-src/docs-tags-tag.tsx +29 -0
  93. package/routes-src/docs-versions.tsx +15 -0
  94. package/routes-src/index.tsx +134 -0
  95. package/routes-src/locale-docs-slug.tsx +69 -0
  96. package/routes-src/locale-docs-tags-index.tsx +23 -0
  97. package/routes-src/locale-docs-tags-tag.tsx +39 -0
  98. package/routes-src/locale-docs-versions.tsx +23 -0
  99. package/routes-src/locale-index.tsx +150 -0
  100. package/routes-src/robots.txt.tsx +13 -0
  101. package/routes-src/sitemap.xml.tsx +48 -0
  102. package/routes-src/v-docs-slug.tsx +64 -0
  103. package/routes-src/v-locale-docs-slug.tsx +85 -0
@@ -0,0 +1,194 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+
4
+ import type { ComponentChildren, VNode } from "preact";
5
+ import { findPath } from "./find-path.js";
6
+ import type { BreadcrumbItem, SidebarNode } from "./types.js";
7
+ import { ChevronRight } from "../icons/index.js";
8
+
9
+ /**
10
+ * Convert a sidebar tree + the current page's id into the ordered list
11
+ * of crumbs the renderer walks. The first crumb is the home rung
12
+ * (icon-only, label is the empty string), followed by each ancestor in
13
+ * the tree, ending with the current page (no `href` so the renderer
14
+ * paints it as plain text).
15
+ *
16
+ * Mirrors the legacy buildBreadcrumbs() behaviour from
17
+ * src/utils/docs.ts so the rendered output stays parity-equivalent
18
+ * with the original Astro breadcrumb component.
19
+ */
20
+ export function buildBreadcrumbItems(
21
+ tree: SidebarNode[],
22
+ currentId: string,
23
+ homeHref: string,
24
+ ): BreadcrumbItem[] {
25
+ const items: BreadcrumbItem[] = [{ label: "", href: homeHref }];
26
+ const path = findPath(tree, currentId);
27
+ for (let i = 0; i < path.length; i++) {
28
+ const node = path[i];
29
+ if (!node) continue;
30
+ const isLast = i === path.length - 1;
31
+ items.push({
32
+ label: node.label,
33
+ href: isLast ? undefined : node.href,
34
+ });
35
+ }
36
+ return items;
37
+ }
38
+
39
+ interface SmartLabelProps {
40
+ label: string;
41
+ }
42
+
43
+ /**
44
+ * Inline replacement for the Astro template's `set:html` /
45
+ * smartBreakToHtml call. Keeps the breadcrumb self-contained inside the
46
+ * v2 package — the legacy smart-break util lives in the host project's
47
+ * src/utils/, which v2 must not reach into.
48
+ *
49
+ * Inserts a Preact <wbr/> after each delimiter character when the label
50
+ * looks "path-like" (URLs, slash-separated paths, etc.). Prose labels
51
+ * pass through unchanged.
52
+ */
53
+ function SmartLabel({ label }: SmartLabelProps): VNode | string {
54
+ if (!isPathLike(label)) return label;
55
+ const parts = label.split(DELIM_SPLIT);
56
+ const nodes: (string | VNode)[] = [];
57
+ for (let i = 0; i < parts.length; i++) {
58
+ const part = parts[i];
59
+ if (!part) continue;
60
+ nodes.push(part);
61
+ if (i % 2 === 1) nodes.push(<wbr key={`wbr-${i}`} />);
62
+ }
63
+ return <>{nodes}</>;
64
+ }
65
+
66
+ const DELIM_SPLIT = /([/\\\-_.:?#&=])/;
67
+
68
+ /** Heuristic copied from src/utils/smart-break.tsx — kept inline so v2
69
+ * has no upward dependency. Returns true when delimiter-aware
70
+ * line-break hints are useful (paths, URLs), false for prose. */
71
+ function isPathLike(text: string): boolean {
72
+ if (!text) return false;
73
+ if (text.includes("://")) return true;
74
+ if (/^\.{0,2}\//.test(text)) return true;
75
+ if (/^[A-Za-z]:[\\/]/.test(text)) return true;
76
+ const forwardMatches = text.match(/[A-Za-z0-9]\/[A-Za-z0-9]/g);
77
+ if (forwardMatches && forwardMatches.length >= 2) return true;
78
+ const backMatches = text.match(/[A-Za-z0-9]\\[A-Za-z0-9]/g);
79
+ if (backMatches && backMatches.length >= 2) return true;
80
+ const hasDomainDot = /[A-Za-z0-9]\.[A-Za-z0-9]/.test(text);
81
+ const hasSlash = /[\\/]/.test(text);
82
+ if (hasDomainDot && hasSlash) return true;
83
+ return false;
84
+ }
85
+
86
+ function ChevronIcon(): VNode {
87
+ return <ChevronRight className="h-icon-xs w-icon-xs text-muted shrink-0" />;
88
+ }
89
+
90
+ function HomeIcon(): VNode {
91
+ return (
92
+ <svg
93
+ xmlns="http://www.w3.org/2000/svg"
94
+ class="h-[1.575rem] w-[1.575rem] shrink-0"
95
+ fill="none"
96
+ viewBox="0 0 24 24"
97
+ stroke="currentColor"
98
+ stroke-width="2"
99
+ aria-hidden="true"
100
+ >
101
+ <path
102
+ stroke-linecap="round"
103
+ stroke-linejoin="round"
104
+ d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 01-1 1h-2z"
105
+ />
106
+ </svg>
107
+ );
108
+ }
109
+
110
+ export interface BreadcrumbProps {
111
+ /** Pre-built items, or omit and pass `tree` + `currentId` instead. */
112
+ items?: BreadcrumbItem[];
113
+ /** Sidebar tree to derive the trail from. Required when `items` is omitted. */
114
+ tree?: SidebarNode[];
115
+ /** Id of the current page (matches SidebarNode.id). Required when `items` is omitted. */
116
+ currentId?: string;
117
+ /** Href for the leading home rung. Defaults to "/". */
118
+ homeHref?: string;
119
+ /**
120
+ * Optional slot rendered at the right end of the breadcrumb row.
121
+ * When provided, the breadcrumb nav and this slot are wrapped in a
122
+ * flex row that matches the original Astro doc-layout's combined
123
+ * breadcrumb + version-switcher row:
124
+ *
125
+ * class="mb-vsp-sm flex flex-col items-start gap-vsp-xs
126
+ * sm:flex-row sm:items-center sm:justify-between [&_nav]:mb-0"
127
+ *
128
+ * The `[&_nav]:mb-0` rule strips the nav's own `mb-vsp-md` so the
129
+ * wrapper margin controls spacing instead. Use this to place the
130
+ * VersionSwitcher pill inline at the right of the breadcrumb row,
131
+ * matching the reference site's layout on category index pages.
132
+ */
133
+ rightSlot?: ComponentChildren;
134
+ }
135
+
136
+ /**
137
+ * Breadcrumb trail component.
138
+ *
139
+ * Two usage shapes:
140
+ * 1. Pass pre-built `items` (existing call-sites that already
141
+ * compute crumbs upstream).
142
+ * 2. Pass `tree` + `currentId` and the component derives the trail.
143
+ *
144
+ * Returns null when no items resolve, matching the documented
145
+ * `items.length > 0 &&` guard.
146
+ */
147
+ export function Breadcrumb(props: BreadcrumbProps): VNode | null {
148
+ const items =
149
+ props.items ??
150
+ (props.tree && props.currentId !== undefined
151
+ ? buildBreadcrumbItems(props.tree, props.currentId, props.homeHref ?? "/")
152
+ : []);
153
+
154
+ if (items.length === 0) return null;
155
+
156
+ const nav = (
157
+ <nav class="mb-vsp-md text-small" aria-label="Breadcrumb">
158
+ <ol class="flex flex-wrap items-center gap-x-hsp-xs">
159
+ {items.map((item, i) => (
160
+ <li key={`crumb-${i}`} class="flex items-center gap-x-hsp-xs">
161
+ {i > 0 && <ChevronIcon />}
162
+ {item.href ? (
163
+ <a
164
+ href={item.href}
165
+ class="text-muted underline hover:text-fg flex items-center gap-x-hsp-2xs"
166
+ >
167
+ {i === 0 && <HomeIcon />}
168
+ <SmartLabel label={item.label} />
169
+ </a>
170
+ ) : (
171
+ <span class="text-fg">
172
+ <SmartLabel label={item.label} />
173
+ </span>
174
+ )}
175
+ </li>
176
+ ))}
177
+ </ol>
178
+ </nav>
179
+ );
180
+
181
+ if (props.rightSlot === undefined) return nav;
182
+
183
+ // When a rightSlot is provided, wrap both elements in a flex row that
184
+ // matches the original Astro doc-layout's breadcrumb + version-switcher
185
+ // row. The `[&_nav]:mb-0` rule cancels the nav's own bottom margin so
186
+ // the wrapper margin controls spacing instead (matching the Astro
187
+ // reference which used `mb-vsp-sm` on the outer div).
188
+ return (
189
+ <div class="mb-vsp-sm flex flex-col items-start gap-vsp-xs sm:flex-row sm:items-center sm:justify-between [&_nav]:mb-0">
190
+ {nav}
191
+ {props.rightSlot}
192
+ </div>
193
+ );
194
+ }
@@ -0,0 +1,26 @@
1
+ import type { BreadcrumbNode, SidebarNode } from "./types.js";
2
+
3
+ /**
4
+ * Walk the sidebar tree and return the chain of nodes from the root
5
+ * level down to (and including) the node whose `id` matches `targetId`.
6
+ *
7
+ * Returns an empty array when no node matches. Pure: never mutates the
8
+ * input tree, never reads anything outside of it.
9
+ *
10
+ * Used by the breadcrumb component to derive the current page's
11
+ * ancestry. The home/root crumb is the renderer's responsibility — this
12
+ * helper deals strictly with the tree itself.
13
+ */
14
+ export function findPath<T extends BreadcrumbNode = SidebarNode>(
15
+ nodes: readonly T[],
16
+ targetId: string,
17
+ ): T[] {
18
+ for (const node of nodes) {
19
+ if (node.id === targetId) return [node];
20
+ if (node.children && node.children.length > 0) {
21
+ const sub = findPath(node.children as readonly T[], targetId);
22
+ if (sub.length > 0) return [node, ...sub];
23
+ }
24
+ }
25
+ return [];
26
+ }
@@ -0,0 +1,4 @@
1
+ export { Breadcrumb, buildBreadcrumbItems } from "./breadcrumb.js";
2
+ export type { BreadcrumbProps } from "./breadcrumb.js";
3
+ export { findPath } from "./find-path.js";
4
+ export type { BreadcrumbItem, SidebarNode } from "./types.js";
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Type contract for the breadcrumb component.
3
+ *
4
+ * Breadcrumb only walks the tree to find an ancestor chain — it does not
5
+ * need the full SidebarNode shape from sidebar-tree. We declare a minimal
6
+ * structural interface here. The canonical SidebarNode (from sidebar-tree)
7
+ * is structurally assignable to this, so callers can pass either shape
8
+ * unchanged.
9
+ */
10
+ export interface BreadcrumbNode {
11
+ type: "doc" | "category";
12
+ id: string;
13
+ label: string;
14
+ href?: string;
15
+ sidebar_position?: number;
16
+ children?: readonly BreadcrumbNode[];
17
+ }
18
+
19
+ /**
20
+ * Backward-compatible alias retained for callers that previously imported
21
+ * `SidebarNode` from this subpath.
22
+ */
23
+ export type SidebarNode = BreadcrumbNode;
24
+
25
+ /**
26
+ * One rung of the breadcrumb trail. `href` is omitted on the final
27
+ * (current-page) item so the renderer can present it as plain text.
28
+ */
29
+ export interface BreadcrumbItem {
30
+ label: string;
31
+ href?: string;
32
+ }
@@ -0,0 +1,78 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // CodeGroup — adapter for zfb's `:::code-group` directive (codeTabs Option A).
4
+ // Moved from the showcase's `src/components/content/code-group.tsx` into the
5
+ // shared package as part of the package-first migration (epic #2321, S4 #2327).
6
+ //
7
+ // zfb emits:
8
+ // <CodeGroup tabs={["label1", "label2", ...]}>
9
+ // <pre data-lang="ts">{RAW code text}</pre>
10
+ // <pre data-lang="js">{RAW code text}</pre>
11
+ // </CodeGroup>
12
+ //
13
+ // The existing <Tabs>/<TabItem> UI uses a children-based API, so this
14
+ // component zips the `tabs` label array with the `<pre data-lang>` children
15
+ // by index, wrapping each in a <TabItem> with the matching label.
16
+ //
17
+ // Code inside each <pre> is raw text (NOT syntect-highlighted — the Rust
18
+ // pipeline does not run highlight inside code-group fences). We render
19
+ // the <pre> inside a <TabItem> with explicit styling via Tailwind tokens
20
+ // so it looks like a code block visually.
21
+ //
22
+ // TabsInit (the companion init script) is expected to be present in the
23
+ // consumer's layout — we do not duplicate it here.
24
+
25
+ import type { ComponentChildren } from "preact";
26
+ import { Tabs } from "../code-syntax/index.js";
27
+ import { TabItem } from "../tab-item/index.js";
28
+
29
+ type Props = {
30
+ tabs?: string[];
31
+ children?: ComponentChildren;
32
+ [key: string]: unknown;
33
+ };
34
+
35
+ function toArray(children: ComponentChildren): ComponentChildren[] {
36
+ if (!children) return [];
37
+ if (Array.isArray(children)) return children;
38
+ return [children];
39
+ }
40
+
41
+ export function CodeGroup({ tabs = [], children, name }: Props) {
42
+ const childArray = toArray(children);
43
+
44
+ // Zip tabs labels with pre children by index. Extra children beyond the
45
+ // tabs array (shouldn't happen in normal zfb output) are ignored.
46
+ const items = tabs.map((label, i) => {
47
+ const child = childArray[i];
48
+ return { label, child };
49
+ });
50
+
51
+ if (items.length === 0) {
52
+ // Degenerate: no tabs — render children as-is.
53
+ return <>{children}</>;
54
+ }
55
+
56
+ // zfb forwards `:::code-group{name="x"}` as the `name` prop; Tabs persists
57
+ // the active tab per group via `groupId`.
58
+ const groupId = typeof name === "string" ? name : undefined;
59
+
60
+ return (
61
+ <Tabs groupId={groupId}>
62
+ {items.map(({ label, child }, i) => (
63
+ // value is suffixed with the index so two fences sharing a label
64
+ // (e.g. both titled "ts") get distinct stable tab identities; the
65
+ // label stays the visible text.
66
+ <TabItem
67
+ key={`${label}-${i}`}
68
+ label={label}
69
+ value={`${label}-${i}`}
70
+ default={i === 0 ? true : undefined}
71
+ >
72
+ {/* Raw-code <pre> from zfb: apply code-block visual treatment via tokens */}
73
+ <div class="code-group-panel">{child}</div>
74
+ </TabItem>
75
+ ))}
76
+ </Tabs>
77
+ );
78
+ }
@@ -0,0 +1,50 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // Admonition factory for MDX callout variants (Note/Tip/Info/Warning/
4
+ // Danger/Caution/Important). Moved from the showcase's
5
+ // `src/components/content/content-admonition.tsx` into the shared package
6
+ // as part of the package-first migration (epic #2321, S4 #2327).
7
+ //
8
+ // Markup contract — KEEP STABLE. The structure:
9
+ // <div data-admonition="<variant>" class="admonition admonition-<variant>">
10
+ // <p class="admonition-title">…</p>
11
+ // <div class="admonition-body">…</div>
12
+ // </div>
13
+ // is the hook both the design-system CSS (`.admonition-<variant>` rules in
14
+ // the consumer's global.css) and e2e smoke specs target. Per-variant
15
+ // color + icon live in CSS keyed off `data-admonition`, so this component
16
+ // stays presentation-agnostic.
17
+ import type { ComponentChildren, VNode } from "preact";
18
+
19
+ export type AdmonitionVariant =
20
+ | "note"
21
+ | "tip"
22
+ | "info"
23
+ | "warning"
24
+ | "danger"
25
+ | "caution"
26
+ | "important";
27
+
28
+ export interface AdmonitionProps {
29
+ /** Custom title; falls back to the capitalized variant name (e.g. "Note"). */
30
+ title?: string;
31
+ children?: ComponentChildren;
32
+ }
33
+
34
+ /**
35
+ * Build the admonition component for a single variant. The title row is always
36
+ * rendered — defaulting to the capitalized variant name when the author gives
37
+ * no `title` — matching the Astro reference where every callout shows a title.
38
+ */
39
+ export function makeAdmonition(variant: AdmonitionVariant) {
40
+ const defaultTitle = variant.charAt(0).toUpperCase() + variant.slice(1);
41
+ return function Admonition({ title, children }: AdmonitionProps): VNode {
42
+ const heading = title && title.length > 0 ? title : defaultTitle;
43
+ return (
44
+ <div data-admonition={variant} class={`admonition admonition-${variant}`}>
45
+ <p class="admonition-title">{heading}</p>
46
+ <div class="admonition-body">{children}</div>
47
+ </div>
48
+ );
49
+ };
50
+ }
@@ -0,0 +1,34 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+
4
+ import type { ComponentChildren, VNode } from "preact";
5
+
6
+ export interface DetailsProps {
7
+ /**
8
+ * Summary / toggle label shown in the `<summary>` element.
9
+ * Defaults to `"Details"` — the same fallback used by the legacy
10
+ * `src/components/details`.
11
+ */
12
+ title?: string;
13
+ /** Slot content rendered inside the collapsed body. */
14
+ children?: ComponentChildren;
15
+ }
16
+
17
+ /**
18
+ * Collapsible `<details>` / `<summary>` block — JSX port of
19
+ * `src/components/details`.
20
+ *
21
+ * Keeps layout and token classes identical to the original template.
22
+ * The legacy component used Astro's `<slot />`; v2 accepts standard
23
+ * Preact `children`.
24
+ */
25
+ export function Details({ title = "Details", children }: DetailsProps): VNode {
26
+ return (
27
+ <details class="my-vsp-md border border-muted rounded-lg overflow-hidden">
28
+ <summary class="cursor-pointer px-hsp-lg py-vsp-sm bg-surface font-medium text-fg select-none hover:text-accent">
29
+ {title}
30
+ </summary>
31
+ <div class="px-hsp-lg py-vsp-sm zd-content">{children}</div>
32
+ </details>
33
+ );
34
+ }
@@ -0,0 +1,4 @@
1
+ // Barrel for the details topic — JSX port of
2
+ // `src/components/details`.
3
+ export { Details } from "./details.js";
4
+ export type { DetailsProps } from "./details.js";
@@ -0,0 +1,91 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // doc-pager — factory for the shared prev/next pager <nav> (epic #2344, S7).
4
+ //
5
+ // The host's `pages/lib/_doc-pager.tsx` previously read the `t` function
6
+ // from `@/config/i18n` at module scope. This factory receives `t` as an
7
+ // injected dependency so the logic lives in the package while the host stub
8
+ // keeps the singleton imports.
9
+
10
+ import type { JSX } from "preact";
11
+ import { ChevronLeft, ChevronRight } from "../icons/index.js";
12
+
13
+ // NavNode is a superset; we only need the fields the pager uses.
14
+ interface PagerNode {
15
+ href?: string;
16
+ label: string;
17
+ }
18
+
19
+ export interface DocPagerProps {
20
+ /** Previous page node (null = no previous page → renders placeholder). */
21
+ prev: PagerNode | null;
22
+ /** Next page node (null = no next page → renders placeholder). */
23
+ next: PagerNode | null;
24
+ /** Active locale for translated "Previous" / "Next" labels. */
25
+ locale: string;
26
+ }
27
+
28
+ /** Dependencies injected by the host stub. */
29
+ export interface DocPagerDeps {
30
+ /** Translate a UI string key for a locale. */
31
+ t: (key: string, locale: string) => string;
32
+ }
33
+
34
+ /**
35
+ * Create a `DocPager` component bound to the host's injected `t` translator.
36
+ */
37
+ export function createDocPager(
38
+ deps: DocPagerDeps,
39
+ ): (props: DocPagerProps) => JSX.Element {
40
+ const { t } = deps;
41
+
42
+ /**
43
+ * Prev/next pagination nav shared by all four doc-route page components.
44
+ *
45
+ * Renders a two-column grid: prev card on the left, next card on the right.
46
+ * When a node is absent its slot is filled with an empty `<div>` to maintain
47
+ * the two-column layout. Placement: immediately after MDX content, before
48
+ * `DocHistoryArea` (Astro reference order — content → pager → utilities,
49
+ * per #1535).
50
+ */
51
+ function DocPager({ prev, next, locale }: DocPagerProps): JSX.Element {
52
+ return (
53
+ <nav class="mt-vsp-2xl grid grid-cols-2 gap-hsp-xl">
54
+ {prev ? (
55
+ <a
56
+ href={prev.href}
57
+ class="group border border-muted rounded-lg p-hsp-lg hover:border-accent"
58
+ >
59
+ <div class="flex items-center gap-hsp-xs text-caption text-muted mb-vsp-2xs">
60
+ <ChevronLeft className="h-[1.125rem] w-[1.125rem]" />
61
+ <span class="no-underline">{t("nav.previous", locale)}</span>
62
+ </div>
63
+ <p class="text-small font-semibold underline group-hover:text-accent">
64
+ {prev.label}
65
+ </p>
66
+ </a>
67
+ ) : (
68
+ <div />
69
+ )}
70
+ {next ? (
71
+ <a
72
+ href={next.href}
73
+ class="group border border-muted rounded-lg p-hsp-lg hover:border-accent text-right"
74
+ >
75
+ <div class="flex items-center justify-end gap-hsp-xs text-caption text-muted mb-vsp-2xs">
76
+ <span class="no-underline">{t("nav.next", locale)}</span>
77
+ <ChevronRight className="h-[1.125rem] w-[1.125rem]" />
78
+ </div>
79
+ <p class="text-small font-semibold underline group-hover:text-accent">
80
+ {next.label}
81
+ </p>
82
+ </a>
83
+ ) : (
84
+ <div />
85
+ )}
86
+ </nav>
87
+ );
88
+ }
89
+
90
+ return DocPager;
91
+ }