@takazudo/zudo-doc 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/preset.d.ts +4 -0
- package/dist/preset.js +4 -1
- package/dist/routes/_chrome.d.ts +2 -2
- package/dist/routes/_chrome.js +28 -2
- package/dist/safelist.css +1 -1
- 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 +6 -6
- package/routes-src/_chrome.tsx +64 -3
|
@@ -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.1.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",
|
|
@@ -511,9 +511,9 @@
|
|
|
511
511
|
],
|
|
512
512
|
"peerDependencies": {
|
|
513
513
|
"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.
|
|
514
|
+
"@takazudo/zfb": "^0.1.0-next.67",
|
|
515
|
+
"@takazudo/zfb-runtime": "^0.1.0-next.67",
|
|
516
|
+
"@takazudo/zudo-doc-history-server": "^1.0.2",
|
|
517
517
|
"@takazudo/zdtp": "^0.3.3",
|
|
518
518
|
"shiki": "^4.0.2",
|
|
519
519
|
"zod": "^4.3.6",
|
|
@@ -555,8 +555,8 @@
|
|
|
555
555
|
"typescript": "^5.0.0",
|
|
556
556
|
"vitest": "^4.1.0",
|
|
557
557
|
"zod": "^4.3.6",
|
|
558
|
-
"@takazudo/zfb": "0.1.0-next.
|
|
559
|
-
"@takazudo/zfb-runtime": "0.1.0-next.
|
|
558
|
+
"@takazudo/zfb": "0.1.0-next.67",
|
|
559
|
+
"@takazudo/zfb-runtime": "0.1.0-next.67"
|
|
560
560
|
},
|
|
561
561
|
"scripts": {
|
|
562
562
|
"build": "tsup && tsc -p tsconfig.build.json",
|
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,
|
|
@@ -92,6 +92,11 @@ import {
|
|
|
92
92
|
getThemeDefaultMode as getThemeDefaultModeBase,
|
|
93
93
|
} from "@takazudo/zudo-doc/nav-data-prep";
|
|
94
94
|
import { buildSidebarForSection } from "@takazudo/zudo-doc/sidebar-utils";
|
|
95
|
+
import { Details } from "@takazudo/zudo-doc/details";
|
|
96
|
+
import {
|
|
97
|
+
HtmlPreviewWrapper,
|
|
98
|
+
type HtmlPreviewWrapperProps,
|
|
99
|
+
} from "@takazudo/zudo-doc/html-preview-wrapper";
|
|
95
100
|
|
|
96
101
|
// ---------------------------------------------------------------------------
|
|
97
102
|
// Package-default host-only bindings (see module header).
|
|
@@ -371,6 +376,49 @@ const SiteTreeNavWrapper = createSiteTreeNavWrapper({
|
|
|
371
376
|
getCategoryOrder,
|
|
372
377
|
});
|
|
373
378
|
|
|
379
|
+
/** HtmlPreview MDX binding (package default).
|
|
380
|
+
*
|
|
381
|
+
* Mirrors the host stub (`pages/_mdx-components.ts`): the global preview
|
|
382
|
+
* config is `settings.htmlPreview`, a SERIALIZABLE setting that DOES ride in
|
|
383
|
+
* the route-context virtual module, so the package can read it directly (no
|
|
384
|
+
* host config needed). `HtmlPreviewWrapper` wraps the bare preview in
|
|
385
|
+
* `<Island when="visible">`, which SSR-renders the preview tree — so an
|
|
386
|
+
* injected docs page using `<HtmlPreview html="…" />` renders correct HTML.
|
|
387
|
+
*
|
|
388
|
+
* Hydration caveat: the interactive code-toggle hydrates only if zfb's islands
|
|
389
|
+
* scanner reaches the `@takazudo/zfb` <Island> import inside
|
|
390
|
+
* `HtmlPreviewWrapper`. The esbuild islands bundler does NOT walk imports of
|
|
391
|
+
* modules whose realpath is under `node_modules` (the same gap the routes
|
|
392
|
+
* plugin stages around — see `plugins/routes.ts`; upstream report:
|
|
393
|
+
* Takazudo/zudo-front-builder, "islands bundler skips addVirtualModule /
|
|
394
|
+
* import-walk for node_modules route realpaths"). SSR output is unaffected. */
|
|
395
|
+
function HtmlPreviewBound(props: HtmlPreviewWrapperProps): JSX.Element {
|
|
396
|
+
return HtmlPreviewWrapper({
|
|
397
|
+
globalConfig: settings.htmlPreview ?? null,
|
|
398
|
+
...props,
|
|
399
|
+
}) as JSX.Element;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Island MDX binding (package default) — an SSR pass-through that renders its
|
|
403
|
+
* children, deliberately NOT the real `@takazudo/zfb` <Island>.
|
|
404
|
+
*
|
|
405
|
+
* Why a pass-through and not `Island → @takazudo/zfb` (the sub-issue's literal
|
|
406
|
+
* ask): the MDX corpus never uses `<Island>` as a real render tag (only inside
|
|
407
|
+
* changelog prose), and the host stub itself uses this same pass-through. The
|
|
408
|
+
* real zfb <Island> emits `data-zfb-island="<firstChildTagName>"` for generic
|
|
409
|
+
* MDX children, i.e. a hydration marker pointing at a non-existent manifest
|
|
410
|
+
* entry — and the islands scanner can't register it anyway from a package
|
|
411
|
+
* (node_modules) import (upstream report above). A pass-through renders the
|
|
412
|
+
* server-renderable children correctly with no bogus marker, which is the
|
|
413
|
+
* complete and correct behaviour for the corpus. `when` is ignored at SSR
|
|
414
|
+
* time (it only matters to the client hydration runtime). */
|
|
415
|
+
function IslandPassthrough(props: {
|
|
416
|
+
when?: "load" | "idle" | "visible" | "media";
|
|
417
|
+
children?: ComponentChildren;
|
|
418
|
+
}): ComponentChildren {
|
|
419
|
+
return props.children ?? null;
|
|
420
|
+
}
|
|
421
|
+
|
|
374
422
|
export function createMdxComponentsBound(lang: string = defaultLocale) {
|
|
375
423
|
return createMdxComponents({
|
|
376
424
|
settings,
|
|
@@ -380,9 +428,22 @@ export function createMdxComponentsBound(lang: string = defaultLocale) {
|
|
|
380
428
|
CategoryTreeNav: CategoryTreeNavWrapper as never,
|
|
381
429
|
SiteTreeNav: SiteTreeNavWrapper as never,
|
|
382
430
|
},
|
|
383
|
-
//
|
|
384
|
-
// (
|
|
431
|
+
// Package-owned content components wired here so an INJECTED docs route
|
|
432
|
+
// (packageOwnedRoutes, no host `pages/` stub) renders MDX using these tags
|
|
433
|
+
// without the "MDX requires '<X>' to be passed via the 'components' prop"
|
|
434
|
+
// error (sub-issue #2390 / supersedes #2377).
|
|
435
|
+
// - Details → fully functional (pure <details>, no client JS).
|
|
436
|
+
// - HtmlPreview → SSR-renders; see HtmlPreviewBound for the hydration
|
|
437
|
+
// caveat tied to the node_modules islands-scanner gap.
|
|
438
|
+
// - Island → SSR pass-through; see IslandPassthrough.
|
|
439
|
+
// PresetGenerator stays a package stub (render nothing): it is the
|
|
440
|
+
// showcase's project-bound interactive island and downstream projects stub
|
|
441
|
+
// it. The showcase keeps its host-owned docs catch-all routes (allowlisted)
|
|
442
|
+
// so its real PresetGenerator still renders.
|
|
385
443
|
extras: {
|
|
444
|
+
Details: Details as never,
|
|
445
|
+
HtmlPreview: HtmlPreviewBound as never,
|
|
446
|
+
Island: IslandPassthrough as never,
|
|
386
447
|
PresetGenerator: (_props: unknown) => null,
|
|
387
448
|
},
|
|
388
449
|
});
|