@takazudo/zudo-doc 1.0.2 → 1.2.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/dist/doc-body-end-islands/index.d.ts +32 -0
- package/dist/doc-body-end-islands/index.js +40 -0
- package/dist/doc-page-renderer/index.js +5 -2
- package/dist/eject/index.js +24 -0
- package/dist/nav-indexing/doc-card-grid.js +1 -1
- package/dist/nav-indexing/nav-card-grid.js +1 -1
- package/dist/plugins/routes.js +3 -1
- package/dist/preset.d.ts +28 -8
- package/dist/preset.js +37 -9
- package/dist/routes/404.js +2 -2
- package/dist/routes/_chrome.d.ts +9 -9
- package/dist/routes/_chrome.js +53 -12
- package/dist/routes/_context.d.ts +5 -0
- package/dist/routes/_context.js +2 -0
- package/dist/routes/index.js +2 -2
- package/dist/routes/locale-index.js +2 -2
- package/dist/safelist.css +1 -1
- package/dist/settings.d.ts +12 -10
- package/eject/desktop-sidebar-toggle-island/index.tsx +105 -0
- package/eject/doc-history/index.tsx +649 -0
- package/eject/image-enlarge/index.tsx +219 -0
- package/eject/sidebar-toggle-island/index.tsx +165 -0
- package/eject/sidebar-tree-island/index.tsx +589 -0
- package/eject/site-tree-nav-island/index.tsx +222 -0
- package/package.json +10 -6
- package/routes-src/404.tsx +2 -2
- package/routes-src/_chrome.tsx +105 -17
- package/routes-src/_context.ts +5 -0
- package/routes-src/_virtual.d.ts +6 -2
- package/routes-src/index.tsx +2 -2
- package/routes-src/locale-index.tsx +2 -2
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/** @jsxRuntime automatic */
|
|
4
|
+
/** @jsxImportSource preact */
|
|
5
|
+
// Use preact hook entrypoints directly — the "react" → "preact/compat" alias
|
|
6
|
+
// lets us consume React-typed components in this Preact app.
|
|
7
|
+
import { useState } from "preact/hooks";
|
|
8
|
+
import type { SidebarNavNode } from "../sidebar/types.js";
|
|
9
|
+
import { INDENT, connectorLeft, ConnectorLines, CategoryLinkIcon } from "../tree-nav-shared/index.js";
|
|
10
|
+
import { ChevronRight } from "../icons/index.js";
|
|
11
|
+
|
|
12
|
+
// site-tree-nav uses wider padding than the narrow sidebar
|
|
13
|
+
const SITE_BASE_PAD = "clamp(0.5rem, 0.8vw, 1rem)";
|
|
14
|
+
|
|
15
|
+
function padLeft(depth: number): string {
|
|
16
|
+
if (depth === 0) return SITE_BASE_PAD;
|
|
17
|
+
return `calc(${depth} * ${INDENT} + 1.25rem + 5px)`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function reorderTree(tree: SidebarNavNode[], order: string[]): SidebarNavNode[] {
|
|
21
|
+
const map = new Map(tree.map((node) => [node.slug, node]));
|
|
22
|
+
const ordered: SidebarNavNode[] = [];
|
|
23
|
+
for (const slug of order) {
|
|
24
|
+
const node = map.get(slug);
|
|
25
|
+
if (node) {
|
|
26
|
+
ordered.push(node);
|
|
27
|
+
map.delete(slug);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// append unmatched nodes at end
|
|
31
|
+
for (const node of map.values()) {
|
|
32
|
+
ordered.push(node);
|
|
33
|
+
}
|
|
34
|
+
return ordered;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SiteTreeNavProps {
|
|
38
|
+
tree: SidebarNavNode[];
|
|
39
|
+
ariaLabel?: string;
|
|
40
|
+
categoryOrder?: string[];
|
|
41
|
+
categoryIgnore?: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function SiteTreeNav({
|
|
45
|
+
tree,
|
|
46
|
+
ariaLabel = "Site index",
|
|
47
|
+
categoryOrder,
|
|
48
|
+
categoryIgnore,
|
|
49
|
+
}: SiteTreeNavProps) {
|
|
50
|
+
let processedTree = tree;
|
|
51
|
+
if (categoryIgnore) {
|
|
52
|
+
const ignoreSet = new Set(categoryIgnore);
|
|
53
|
+
processedTree = processedTree.filter((node) => !ignoreSet.has(node.slug));
|
|
54
|
+
}
|
|
55
|
+
if (categoryOrder) {
|
|
56
|
+
processedTree = reorderTree(processedTree, categoryOrder);
|
|
57
|
+
}
|
|
58
|
+
return (
|
|
59
|
+
<nav
|
|
60
|
+
aria-label={ariaLabel}
|
|
61
|
+
data-site-nav
|
|
62
|
+
className="grid gap-vsp-md"
|
|
63
|
+
style={{
|
|
64
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(min(18rem, 100%), 1fr))",
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{processedTree.map((node) => (
|
|
68
|
+
<div key={node.slug} className="min-w-0 border border-muted pl-hsp-sm py-vsp-2xs">
|
|
69
|
+
{node.children.length > 0 ? (
|
|
70
|
+
<CategoryNode node={node} depth={0} isLast={true} />
|
|
71
|
+
) : (
|
|
72
|
+
<LeafNode node={node} depth={0} isLast={true} />
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
))}
|
|
76
|
+
</nav>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
SiteTreeNav.displayName = "SiteTreeNav";
|
|
80
|
+
|
|
81
|
+
function NodeList({ nodes, depth }: { nodes: SidebarNavNode[]; depth: number }) {
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
{nodes.map((node, index) => {
|
|
85
|
+
const isLast = index === nodes.length - 1;
|
|
86
|
+
return node.children.length > 0 ? (
|
|
87
|
+
<CategoryNode
|
|
88
|
+
key={node.slug}
|
|
89
|
+
node={node}
|
|
90
|
+
depth={depth}
|
|
91
|
+
isLast={isLast}
|
|
92
|
+
/>
|
|
93
|
+
) : (
|
|
94
|
+
<LeafNode
|
|
95
|
+
key={node.slug}
|
|
96
|
+
node={node}
|
|
97
|
+
depth={depth}
|
|
98
|
+
isLast={isLast}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
})}
|
|
102
|
+
</>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function CategoryNode({
|
|
107
|
+
node,
|
|
108
|
+
depth,
|
|
109
|
+
isLast,
|
|
110
|
+
}: {
|
|
111
|
+
node: SidebarNavNode;
|
|
112
|
+
depth: number;
|
|
113
|
+
isLast: boolean;
|
|
114
|
+
}) {
|
|
115
|
+
const [open, setOpen] = useState(true);
|
|
116
|
+
const toggle = () => setOpen((prev) => !prev);
|
|
117
|
+
const paddingLeft = padLeft(depth);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className={`${depth >= 1 && !isLast ? "relative" : ""}`}>
|
|
121
|
+
{depth >= 1 && !isLast && open && (
|
|
122
|
+
<div
|
|
123
|
+
className="absolute border-l border-dashed border-muted z-local-1"
|
|
124
|
+
style={{
|
|
125
|
+
left: connectorLeft(depth),
|
|
126
|
+
top: 0,
|
|
127
|
+
bottom: 0,
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
<div className="relative">
|
|
132
|
+
<ConnectorLines
|
|
133
|
+
depth={depth}
|
|
134
|
+
isLast={isLast}
|
|
135
|
+
widthScale={2}
|
|
136
|
+
topPad="calc(0.15rem + var(--spacing-vsp-xs))"
|
|
137
|
+
/>
|
|
138
|
+
<div
|
|
139
|
+
className="flex w-full items-center justify-between text-small font-semibold pt-[0.15rem] text-fg"
|
|
140
|
+
style={{ paddingLeft }}
|
|
141
|
+
>
|
|
142
|
+
{node.href ? (
|
|
143
|
+
<a
|
|
144
|
+
href={node.href}
|
|
145
|
+
className="flex-1 flex items-start gap-hsp-xs py-vsp-xs text-fg hover:text-accent hover:underline focus:underline"
|
|
146
|
+
>
|
|
147
|
+
{depth === 0 && (
|
|
148
|
+
<span className="flex h-[1lh] items-center">
|
|
149
|
+
<CategoryLinkIcon className="w-[18px] 2xl:w-[24px]" />
|
|
150
|
+
</span>
|
|
151
|
+
)}
|
|
152
|
+
{node.label}
|
|
153
|
+
</a>
|
|
154
|
+
) : (
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
onClick={toggle}
|
|
158
|
+
className="flex-1 py-vsp-xs text-left hover:text-accent hover:underline focus:underline"
|
|
159
|
+
>
|
|
160
|
+
{node.label}
|
|
161
|
+
</button>
|
|
162
|
+
)}
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
onClick={toggle}
|
|
166
|
+
className="aspect-square flex items-center justify-center w-[1.75rem] border-y border-l border-muted hover:underline focus:underline"
|
|
167
|
+
aria-expanded={open}
|
|
168
|
+
aria-label={open ? `Collapse ${node.label}` : `Expand ${node.label}`}
|
|
169
|
+
>
|
|
170
|
+
<ChevronRight className={`h-icon-xs w-icon-xs transition-transform duration-150 ${open ? "rotate-90" : ""} text-muted`} />
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
{open && (
|
|
175
|
+
<div>
|
|
176
|
+
<NodeList nodes={node.children} depth={depth + 1} />
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function LeafNode({
|
|
184
|
+
node,
|
|
185
|
+
depth,
|
|
186
|
+
isLast,
|
|
187
|
+
}: {
|
|
188
|
+
node: SidebarNavNode;
|
|
189
|
+
depth: number;
|
|
190
|
+
isLast: boolean;
|
|
191
|
+
}) {
|
|
192
|
+
if (!node.href) return null;
|
|
193
|
+
const isRoot = depth === 0;
|
|
194
|
+
const paddingLeft = padLeft(depth);
|
|
195
|
+
|
|
196
|
+
const topPad = isRoot
|
|
197
|
+
? "calc(var(--spacing-vsp-xs) + 0.15rem)"
|
|
198
|
+
: "var(--spacing-vsp-2xs)";
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div>
|
|
202
|
+
<div className="relative">
|
|
203
|
+
<ConnectorLines depth={depth} isLast={isLast} widthScale={2} topPad={topPad} />
|
|
204
|
+
<a
|
|
205
|
+
href={node.href}
|
|
206
|
+
className={isRoot
|
|
207
|
+
? "flex items-start gap-hsp-xs py-[calc(var(--spacing-vsp-xs)+0.15rem)] text-small font-semibold text-fg hover:text-accent hover:underline focus:underline"
|
|
208
|
+
: `block py-vsp-2xs ${isLast ? "pb-vsp-xs" : ""} text-small text-fg hover:text-accent hover:underline focus:underline`
|
|
209
|
+
}
|
|
210
|
+
style={{ paddingLeft }}
|
|
211
|
+
>
|
|
212
|
+
{isRoot && (
|
|
213
|
+
<span className="flex h-[1lh] items-center">
|
|
214
|
+
<CategoryLinkIcon className="w-[18px] 2xl:w-[24px]" />
|
|
215
|
+
</span>
|
|
216
|
+
)}
|
|
217
|
+
{node.label}
|
|
218
|
+
</a>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takazudo/zudo-doc",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "zudo-doc framework primitives layer that sits on top of zfb's engine — sidebar, theme, TOC, breadcrumb, layouts, head injection, View Transitions, SSR-skip wrappers (per ADR-003).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -392,6 +392,10 @@
|
|
|
392
392
|
"types": "./dist/doc-body-end/index.d.ts",
|
|
393
393
|
"default": "./dist/doc-body-end/index.js"
|
|
394
394
|
},
|
|
395
|
+
"./doc-body-end-islands": {
|
|
396
|
+
"types": "./dist/doc-body-end-islands/index.d.ts",
|
|
397
|
+
"default": "./dist/doc-body-end-islands/index.js"
|
|
398
|
+
},
|
|
395
399
|
"./doc-history-area": {
|
|
396
400
|
"types": "./dist/doc-history-area/index.d.ts",
|
|
397
401
|
"default": "./dist/doc-history-area/index.js"
|
|
@@ -511,9 +515,9 @@
|
|
|
511
515
|
],
|
|
512
516
|
"peerDependencies": {
|
|
513
517
|
"preact": "^10.29.1",
|
|
514
|
-
"@takazudo/zfb": "^0.1.0-next.
|
|
515
|
-
"@takazudo/zfb-runtime": "^0.1.0-next.
|
|
516
|
-
"@takazudo/zudo-doc-history-server": "^1.0
|
|
518
|
+
"@takazudo/zfb": "^0.1.0-next.69",
|
|
519
|
+
"@takazudo/zfb-runtime": "^0.1.0-next.69",
|
|
520
|
+
"@takazudo/zudo-doc-history-server": "^1.1.0",
|
|
517
521
|
"@takazudo/zdtp": "^0.3.3",
|
|
518
522
|
"shiki": "^4.0.2",
|
|
519
523
|
"zod": "^4.3.6",
|
|
@@ -555,8 +559,8 @@
|
|
|
555
559
|
"typescript": "^5.0.0",
|
|
556
560
|
"vitest": "^4.1.0",
|
|
557
561
|
"zod": "^4.3.6",
|
|
558
|
-
"@takazudo/zfb": "0.1.0-next.
|
|
559
|
-
"@takazudo/zfb-runtime": "0.1.0-next.
|
|
562
|
+
"@takazudo/zfb": "0.1.0-next.69",
|
|
563
|
+
"@takazudo/zfb-runtime": "0.1.0-next.69"
|
|
560
564
|
},
|
|
561
565
|
"scripts": {
|
|
562
566
|
"build": "tsup && tsc -p tsconfig.build.json",
|
package/routes-src/404.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
HeadWithDefaults,
|
|
11
11
|
HeaderWithDefaults,
|
|
12
12
|
FooterWithDefaults,
|
|
13
|
-
|
|
13
|
+
BodyEndIslands,
|
|
14
14
|
composeMetaTitle,
|
|
15
15
|
} from "./_chrome.js";
|
|
16
16
|
|
|
@@ -31,7 +31,7 @@ export default function NotFoundPage(): JSX.Element {
|
|
|
31
31
|
sidebarOverride={<></>}
|
|
32
32
|
headerOverride={<HeaderWithDefaults lang={locale} />}
|
|
33
33
|
footerOverride={<FooterWithDefaults lang={locale} />}
|
|
34
|
-
bodyEndComponents={<
|
|
34
|
+
bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
|
|
35
35
|
enableClientRouter={settings.dynamicPageTransition}
|
|
36
36
|
>
|
|
37
37
|
<div class="min-h-[60vh] flex flex-col items-center justify-center px-hsp-2xl py-vsp-xl">
|
package/routes-src/_chrome.tsx
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// bindings — is A2's job (#2363). With `packageOwnedRoutes` off this module is
|
|
28
28
|
// never evaluated; it only needs to typecheck and import.
|
|
29
29
|
|
|
30
|
-
import type { JSX, VNode } from "preact";
|
|
30
|
+
import type { JSX, VNode, ComponentChildren } from "preact";
|
|
31
31
|
import {
|
|
32
32
|
settings,
|
|
33
33
|
defaultLocale,
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
firstRoutedHref,
|
|
53
53
|
collectTags,
|
|
54
54
|
stableDocs,
|
|
55
|
+
colorSchemes,
|
|
55
56
|
} from "./_context.js";
|
|
56
57
|
import type { CategoryMeta, DocNavNode } from "./_docs-helpers.js";
|
|
57
58
|
|
|
@@ -67,6 +68,7 @@ import { createFooterWithDefaults } from "@takazudo/zudo-doc/footer-with-default
|
|
|
67
68
|
import { createSidebarWithDefaults } from "@takazudo/zudo-doc/sidebar-with-defaults";
|
|
68
69
|
import { createSidebarPrepaint } from "@takazudo/zudo-doc/sidebar-prepaint";
|
|
69
70
|
import { createDocBodyEnd } from "@takazudo/zudo-doc/doc-body-end";
|
|
71
|
+
import { createBodyEndIslands } from "@takazudo/zudo-doc/doc-body-end-islands";
|
|
70
72
|
import { createDocPager } from "@takazudo/zudo-doc/doc-pager";
|
|
71
73
|
import { createDocPageShell } from "@takazudo/zudo-doc/doc-page-shell";
|
|
72
74
|
import { createDocContentHeader } from "@takazudo/zudo-doc/doc-content-header";
|
|
@@ -92,6 +94,11 @@ import {
|
|
|
92
94
|
getThemeDefaultMode as getThemeDefaultModeBase,
|
|
93
95
|
} from "@takazudo/zudo-doc/nav-data-prep";
|
|
94
96
|
import { buildSidebarForSection } from "@takazudo/zudo-doc/sidebar-utils";
|
|
97
|
+
import { Details } from "@takazudo/zudo-doc/details";
|
|
98
|
+
import {
|
|
99
|
+
HtmlPreviewWrapper,
|
|
100
|
+
type HtmlPreviewWrapperProps,
|
|
101
|
+
} from "@takazudo/zudo-doc/html-preview-wrapper";
|
|
95
102
|
|
|
96
103
|
// ---------------------------------------------------------------------------
|
|
97
104
|
// Package-default host-only bindings (see module header).
|
|
@@ -123,12 +130,12 @@ const docHistoryMeta: Record<string, never> = {};
|
|
|
123
130
|
* tree. The project `sidebars` config is host-bound (A2). */
|
|
124
131
|
const sidebarsConfig: Record<string, never> = {};
|
|
125
132
|
|
|
126
|
-
/** Package
|
|
127
|
-
*
|
|
128
|
-
* the
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
133
|
+
/** Package-default body-end islands — the package-island subset of the host's
|
|
134
|
+
* `BodyEndIslands` (AiChatModal / ImageEnlarge / MermaidEnlarge), reconstructed
|
|
135
|
+
* from the serializable virtual-module `settings` flags (#2406 / #2401(c)).
|
|
136
|
+
* The host-owned bootstraps (client-router / design-token-panel) cannot live
|
|
137
|
+
* in the package, so they are NOT included here — A2/S4 re-homes those. */
|
|
138
|
+
const BodyEndIslands = createBodyEndIslands({ settings });
|
|
132
139
|
|
|
133
140
|
/** Package no-op DocHistory island stub — renders an empty fragment (the
|
|
134
141
|
* `DocHistoryComponent` contract requires a VNode, not null). */
|
|
@@ -157,14 +164,38 @@ function SearchWidgetBound(props: {
|
|
|
157
164
|
|
|
158
165
|
export const composeMetaTitle = createComposeMetaTitle(settings.siteName);
|
|
159
166
|
|
|
167
|
+
// Scheme-selection: resolve the active color scheme(s) from the host
|
|
168
|
+
// `colorSchemes` map supplied via the virtual module. Falls back to
|
|
169
|
+
// `DEFAULT_SCHEME` for any missing key or when `colorSchemes` is absent.
|
|
170
|
+
// `generateCssCustomProperties` is called in single-scheme mode
|
|
171
|
+
// (`settings.colorMode` is false); `generateLightDarkCssProperties` in
|
|
172
|
+
// light/dark mode. Both delegate to DEFAULT_SCHEME when `colorSchemes` is null.
|
|
173
|
+
function resolveHostScheme(key: string): ColorScheme {
|
|
174
|
+
if (!colorSchemes) return DEFAULT_SCHEME;
|
|
175
|
+
return colorSchemes[key] ?? DEFAULT_SCHEME;
|
|
176
|
+
}
|
|
177
|
+
|
|
160
178
|
export const HeadWithDefaults = createHeadWithDefaults({
|
|
161
179
|
settings,
|
|
162
180
|
composeMetaTitle,
|
|
163
181
|
withBase,
|
|
164
182
|
absoluteUrl,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
183
|
+
// Called only in single-scheme mode (colorMode false): resolve via settings.colorScheme.
|
|
184
|
+
generateCssCustomProperties: () =>
|
|
185
|
+
generateCssCustomProperties(resolveHostScheme(settings.colorScheme)),
|
|
186
|
+
// Called only in light/dark mode (colorMode truthy): resolve the pair.
|
|
187
|
+
generateLightDarkCssProperties: () => {
|
|
188
|
+
const cm = settings.colorMode;
|
|
189
|
+
if (cm) {
|
|
190
|
+
return generateLightDarkCssProperties(
|
|
191
|
+
resolveHostScheme(cm.lightScheme),
|
|
192
|
+
resolveHostScheme(cm.darkScheme),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
// Should not be reached (head-with-defaults only calls this when colorMode
|
|
196
|
+
// is truthy), but guard the contract by falling back to DEFAULT_SCHEME.
|
|
197
|
+
return generateLightDarkCssProperties(DEFAULT_SCHEME, DEFAULT_SCHEME);
|
|
198
|
+
},
|
|
168
199
|
});
|
|
169
200
|
|
|
170
201
|
// ---------------------------------------------------------------------------
|
|
@@ -267,7 +298,7 @@ export const SidebarWithDefaults = createSidebarWithDefaults({
|
|
|
267
298
|
});
|
|
268
299
|
|
|
269
300
|
const SidebarPrepaint = createSidebarPrepaint({ sidebarToggle: settings.sidebarToggle });
|
|
270
|
-
const DocBodyEnd = createDocBodyEnd({ settings, BodyEndIslands
|
|
301
|
+
const DocBodyEnd = createDocBodyEnd({ settings, BodyEndIslands });
|
|
271
302
|
const DocPager = createDocPager({ t });
|
|
272
303
|
|
|
273
304
|
// ---------------------------------------------------------------------------
|
|
@@ -371,6 +402,49 @@ const SiteTreeNavWrapper = createSiteTreeNavWrapper({
|
|
|
371
402
|
getCategoryOrder,
|
|
372
403
|
});
|
|
373
404
|
|
|
405
|
+
/** HtmlPreview MDX binding (package default).
|
|
406
|
+
*
|
|
407
|
+
* Mirrors the host stub (`pages/_mdx-components.ts`): the global preview
|
|
408
|
+
* config is `settings.htmlPreview`, a SERIALIZABLE setting that DOES ride in
|
|
409
|
+
* the route-context virtual module, so the package can read it directly (no
|
|
410
|
+
* host config needed). `HtmlPreviewWrapper` wraps the bare preview in
|
|
411
|
+
* `<Island when="visible">`, which SSR-renders the preview tree — so an
|
|
412
|
+
* injected docs page using `<HtmlPreview html="…" />` renders correct HTML.
|
|
413
|
+
*
|
|
414
|
+
* Hydration caveat: the interactive code-toggle hydrates only if zfb's islands
|
|
415
|
+
* scanner reaches the `@takazudo/zfb` <Island> import inside
|
|
416
|
+
* `HtmlPreviewWrapper`. The esbuild islands bundler does NOT walk imports of
|
|
417
|
+
* modules whose realpath is under `node_modules` (the same gap the routes
|
|
418
|
+
* plugin stages around — see `plugins/routes.ts`; upstream report:
|
|
419
|
+
* Takazudo/zudo-front-builder, "islands bundler skips addVirtualModule /
|
|
420
|
+
* import-walk for node_modules route realpaths"). SSR output is unaffected. */
|
|
421
|
+
function HtmlPreviewBound(props: HtmlPreviewWrapperProps): JSX.Element {
|
|
422
|
+
return HtmlPreviewWrapper({
|
|
423
|
+
globalConfig: settings.htmlPreview ?? null,
|
|
424
|
+
...props,
|
|
425
|
+
}) as JSX.Element;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Island MDX binding (package default) — an SSR pass-through that renders its
|
|
429
|
+
* children, deliberately NOT the real `@takazudo/zfb` <Island>.
|
|
430
|
+
*
|
|
431
|
+
* Why a pass-through and not `Island → @takazudo/zfb` (the sub-issue's literal
|
|
432
|
+
* ask): the MDX corpus never uses `<Island>` as a real render tag (only inside
|
|
433
|
+
* changelog prose), and the host stub itself uses this same pass-through. The
|
|
434
|
+
* real zfb <Island> emits `data-zfb-island="<firstChildTagName>"` for generic
|
|
435
|
+
* MDX children, i.e. a hydration marker pointing at a non-existent manifest
|
|
436
|
+
* entry — and the islands scanner can't register it anyway from a package
|
|
437
|
+
* (node_modules) import (upstream report above). A pass-through renders the
|
|
438
|
+
* server-renderable children correctly with no bogus marker, which is the
|
|
439
|
+
* complete and correct behaviour for the corpus. `when` is ignored at SSR
|
|
440
|
+
* time (it only matters to the client hydration runtime). */
|
|
441
|
+
function IslandPassthrough(props: {
|
|
442
|
+
when?: "load" | "idle" | "visible" | "media";
|
|
443
|
+
children?: ComponentChildren;
|
|
444
|
+
}): ComponentChildren {
|
|
445
|
+
return props.children ?? null;
|
|
446
|
+
}
|
|
447
|
+
|
|
374
448
|
export function createMdxComponentsBound(lang: string = defaultLocale) {
|
|
375
449
|
return createMdxComponents({
|
|
376
450
|
settings,
|
|
@@ -380,9 +454,22 @@ export function createMdxComponentsBound(lang: string = defaultLocale) {
|
|
|
380
454
|
CategoryTreeNav: CategoryTreeNavWrapper as never,
|
|
381
455
|
SiteTreeNav: SiteTreeNavWrapper as never,
|
|
382
456
|
},
|
|
383
|
-
//
|
|
384
|
-
// (
|
|
457
|
+
// Package-owned content components wired here so an INJECTED docs route
|
|
458
|
+
// (packageOwnedRoutes, no host `pages/` stub) renders MDX using these tags
|
|
459
|
+
// without the "MDX requires '<X>' to be passed via the 'components' prop"
|
|
460
|
+
// error (sub-issue #2390 / supersedes #2377).
|
|
461
|
+
// - Details → fully functional (pure <details>, no client JS).
|
|
462
|
+
// - HtmlPreview → SSR-renders; see HtmlPreviewBound for the hydration
|
|
463
|
+
// caveat tied to the node_modules islands-scanner gap.
|
|
464
|
+
// - Island → SSR pass-through; see IslandPassthrough.
|
|
465
|
+
// PresetGenerator stays a package stub (render nothing): it is the
|
|
466
|
+
// showcase's project-bound interactive island and downstream projects stub
|
|
467
|
+
// it. The showcase keeps its host-owned docs catch-all routes (allowlisted)
|
|
468
|
+
// so its real PresetGenerator still renders.
|
|
385
469
|
extras: {
|
|
470
|
+
Details: Details as never,
|
|
471
|
+
HtmlPreview: HtmlPreviewBound as never,
|
|
472
|
+
Island: IslandPassthrough as never,
|
|
386
473
|
PresetGenerator: (_props: unknown) => null,
|
|
387
474
|
},
|
|
388
475
|
});
|
|
@@ -434,7 +521,7 @@ export const VersionsPageView = createVersionsPageView({
|
|
|
434
521
|
HeadWithDefaults,
|
|
435
522
|
HeaderWithDefaults,
|
|
436
523
|
FooterWithDefaults,
|
|
437
|
-
BodyEndIslands
|
|
524
|
+
BodyEndIslands,
|
|
438
525
|
},
|
|
439
526
|
});
|
|
440
527
|
|
|
@@ -452,15 +539,16 @@ const tagPages = createTagPages({
|
|
|
452
539
|
HeadWithDefaults,
|
|
453
540
|
HeaderWithDefaults,
|
|
454
541
|
FooterWithDefaults,
|
|
455
|
-
BodyEndIslands
|
|
542
|
+
BodyEndIslands,
|
|
456
543
|
DocHistoryArea: DocHistoryArea as never,
|
|
457
544
|
},
|
|
458
545
|
});
|
|
459
546
|
|
|
460
547
|
export const { collectTagMapForLocale, TagDetailPageView, TagsIndexPageView } = tagPages;
|
|
461
548
|
|
|
462
|
-
// Re-export the locale-aware site-tree wrapper
|
|
463
|
-
|
|
549
|
+
// Re-export the locale-aware site-tree wrapper + the package-default body-end
|
|
550
|
+
// islands for the index entrypoints.
|
|
551
|
+
export { SiteTreeNavWrapper, BodyEndIslands };
|
|
464
552
|
|
|
465
553
|
// Bring the doc-route-entries builder + types into the chrome surface so the
|
|
466
554
|
// doc entrypoints import a single module.
|
package/routes-src/_context.ts
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
import { routeContext } from "virtual:zudo-doc-route-context";
|
|
35
35
|
|
|
36
36
|
import type { Settings } from "@takazudo/zudo-doc/settings";
|
|
37
|
+
import type { ColorScheme } from "@takazudo/zudo-doc/color-scheme-utils";
|
|
37
38
|
import type { FactoryI18n } from "@takazudo/zudo-doc/factory-context";
|
|
38
39
|
import { makeUrlHelpers, type UrlHelpers } from "@takazudo/zudo-doc/url-helpers";
|
|
39
40
|
import {
|
|
@@ -84,6 +85,7 @@ export interface RouteContextPayload {
|
|
|
84
85
|
settings: Settings;
|
|
85
86
|
translations: Record<string, Record<string, string>>;
|
|
86
87
|
tagVocabulary: readonly TagVocabularyEntry[];
|
|
88
|
+
colorSchemes: Record<string, ColorScheme> | null;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
/** The serializable route-context (from the virtual module). */
|
|
@@ -91,6 +93,9 @@ export const ctx = routeContext as unknown as RouteContextPayload;
|
|
|
91
93
|
export const settings: Settings = ctx.settings;
|
|
92
94
|
const translations = ctx.translations;
|
|
93
95
|
const tagVocabulary = ctx.tagVocabulary;
|
|
96
|
+
/** Host color-scheme palette map. `null` when not supplied — `_chrome.tsx`
|
|
97
|
+
* falls back to `DEFAULT_SCHEME` in that case. */
|
|
98
|
+
export const colorSchemes: Record<string, ColorScheme> | null = ctx.colorSchemes;
|
|
94
99
|
|
|
95
100
|
// ---------------------------------------------------------------------------
|
|
96
101
|
// i18n — reconstruct a FactoryI18n from settings + translations (Decision 1).
|
package/routes-src/_virtual.d.ts
CHANGED
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
// — so this declaration gives the package route entrypoints a typed import.
|
|
5
5
|
//
|
|
6
6
|
// The payload is SERIALIZABLE DATA ONLY (ADR Decision 1): `settings`,
|
|
7
|
-
// `translations`, `tagVocabulary`. `_context.ts` narrows it
|
|
8
|
-
// `RouteContextPayload` at the seam.
|
|
7
|
+
// `translations`, `tagVocabulary`, `colorSchemes`. `_context.ts` narrows it
|
|
8
|
+
// to the concrete `RouteContextPayload` at the seam.
|
|
9
9
|
declare module "virtual:zudo-doc-route-context" {
|
|
10
10
|
export const routeContext: {
|
|
11
11
|
settings: unknown;
|
|
12
12
|
translations: Record<string, Record<string, string>>;
|
|
13
13
|
tagVocabulary: ReadonlyArray<Record<string, unknown>>;
|
|
14
|
+
/** Host color-scheme palette map. `null` when the caller did not pass
|
|
15
|
+
* `colorSchemes` to `zudoDocPreset` — `_chrome.tsx` falls back to
|
|
16
|
+
* `DEFAULT_SCHEME` in that case. */
|
|
17
|
+
colorSchemes: Record<string, unknown> | null;
|
|
14
18
|
};
|
|
15
19
|
}
|
package/routes-src/index.tsx
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
HeadWithDefaults,
|
|
29
29
|
HeaderWithDefaults,
|
|
30
30
|
FooterWithDefaults,
|
|
31
|
-
|
|
31
|
+
BodyEndIslands,
|
|
32
32
|
composeMetaTitle,
|
|
33
33
|
} from "./_chrome.js";
|
|
34
34
|
|
|
@@ -67,7 +67,7 @@ export default function IndexPage(): JSX.Element {
|
|
|
67
67
|
sidebarOverride={<></>}
|
|
68
68
|
headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase("/")} />}
|
|
69
69
|
footerOverride={<FooterWithDefaults lang={locale} />}
|
|
70
|
-
bodyEndComponents={<
|
|
70
|
+
bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
|
|
71
71
|
enableClientRouter={settings.dynamicPageTransition}
|
|
72
72
|
>
|
|
73
73
|
<div class="flex justify-center mb-vsp-xl">
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
HeadWithDefaults,
|
|
29
29
|
HeaderWithDefaults,
|
|
30
30
|
FooterWithDefaults,
|
|
31
|
-
|
|
31
|
+
BodyEndIslands,
|
|
32
32
|
composeMetaTitle,
|
|
33
33
|
} from "./_chrome.js";
|
|
34
34
|
|
|
@@ -83,7 +83,7 @@ export default function LocaleIndexPage({ params }: PageArgs): JSX.Element {
|
|
|
83
83
|
sidebarOverride={<></>}
|
|
84
84
|
headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase(`/${locale}/`)} />}
|
|
85
85
|
footerOverride={<FooterWithDefaults lang={locale} />}
|
|
86
|
-
bodyEndComponents={<
|
|
86
|
+
bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
|
|
87
87
|
enableClientRouter={settings.dynamicPageTransition}
|
|
88
88
|
>
|
|
89
89
|
<div class="flex justify-center mb-vsp-xl">
|