create-zudo-doc 0.2.21 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/features/image-enlarge.d.ts +10 -2
  2. package/dist/features/image-enlarge.js +10 -2
  3. package/dist/scaffold.js +20 -22
  4. package/dist/zfb-config-gen.d.ts +16 -10
  5. package/dist/zfb-config-gen.js +34 -239
  6. package/package.json +1 -1
  7. package/templates/base/pages/lib/_body-end-islands.tsx +6 -13
  8. package/templates/base/pages/lib/_category-nav.tsx +30 -115
  9. package/templates/base/pages/lib/_category-tree-nav.tsx +38 -65
  10. package/templates/base/pages/lib/_compose-meta-title.ts +2 -6
  11. package/templates/base/pages/lib/_doc-body-end.tsx +3 -35
  12. package/templates/base/pages/lib/_doc-content-header.tsx +10 -111
  13. package/templates/base/pages/lib/_doc-history-area.tsx +19 -211
  14. package/templates/base/pages/lib/_doc-metainfo-area.tsx +10 -113
  15. package/templates/base/pages/lib/_doc-page-renderer.tsx +22 -183
  16. package/templates/base/pages/lib/_doc-page-shell.tsx +17 -251
  17. package/templates/base/pages/lib/_doc-pager.tsx +4 -74
  18. package/templates/base/pages/lib/_doc-route-entries.ts +43 -177
  19. package/templates/base/pages/lib/_doc-route-paths.ts +16 -101
  20. package/templates/base/pages/lib/_doc-tags-area.tsx +14 -77
  21. package/templates/base/pages/lib/_footer-with-defaults.tsx +37 -225
  22. package/templates/base/pages/lib/_frontmatter-preview-data.ts +5 -31
  23. package/templates/base/pages/lib/_head-with-defaults.tsx +13 -138
  24. package/templates/base/pages/lib/_header-with-defaults.tsx +24 -324
  25. package/templates/base/pages/lib/_inline-version-switcher.tsx +16 -78
  26. package/templates/base/pages/lib/_nav-data-prep.ts +32 -51
  27. package/templates/base/pages/lib/_nav-source-cache.ts +12 -57
  28. package/templates/base/pages/lib/_nav-source-docs.ts +33 -233
  29. package/templates/base/pages/lib/_search-widget-script.ts +2 -470
  30. package/templates/base/pages/lib/_search-widget.tsx +9 -195
  31. package/templates/base/pages/lib/_sidebar-prepaint.tsx +6 -59
  32. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +16 -118
  33. package/templates/base/pages/lib/_site-tree-nav.tsx +29 -80
  34. package/templates/base/pages/lib/doc-page-props.ts +26 -44
  35. package/templates/base/pages/lib/locale-merge.ts +32 -145
  36. package/templates/base/pages/lib/route-enumerators.ts +52 -286
  37. package/templates/base/pages/robots.txt.tsx +2 -26
  38. package/templates/base/src/components/ai-chat-modal.tsx +9 -8
  39. package/templates/base/src/components/doc-history.tsx +9 -8
  40. package/templates/base/src/components/image-enlarge.tsx +11 -8
  41. package/templates/base/src/components/sidebar-toggle.tsx +6 -170
  42. package/templates/base/src/components/sidebar-tree.tsx +6 -548
  43. package/templates/base/src/components/site-tree-nav.tsx +6 -220
  44. package/templates/base/src/config/color-scheme-utils.ts +34 -158
  45. package/templates/base/src/config/i18n.ts +9 -0
  46. package/templates/base/src/config/z-index-tokens.ts +5 -4
  47. package/templates/base/src/styles/global.css +5 -579
  48. package/templates/base/src/utils/base.ts +1 -1
  49. package/templates/base/src/utils/docs.ts +47 -16
  50. package/templates/base/src/utils/github.ts +12 -9
  51. package/templates/base/src/utils/nav-scope.ts +13 -42
  52. package/templates/base/src/utils/sidebar.ts +18 -86
  53. package/templates/base/src/utils/slug.ts +10 -53
  54. package/templates/base/src/utils/smart-break.tsx +12 -120
  55. package/templates/base/src/utils/tags.ts +25 -68
  56. package/templates/features/bodyFootUtil/files/src/utils/github.ts +12 -9
  57. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +13 -39
  58. package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -636
  59. package/templates/features/docHistory/files/src/types/doc-history.ts +7 -23
  60. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +35 -201
  61. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +8 -269
  62. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +6 -99
  63. package/templates/features/tagGovernance/files/scripts/tags-audit.ts +67 -515
  64. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +21 -73
  65. package/templates/base/plugins/connect-adapter.mjs +0 -169
  66. package/templates/base/plugins/search-index-plugin.mjs +0 -66
  67. package/templates/base/scripts/gen-z-index.mjs +0 -157
  68. package/templates/base/src/components/mermaid-enlarge.tsx +0 -490
  69. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +0 -47
  70. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +0 -111
  71. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +0 -93
@@ -1,490 +0,0 @@
1
- "use client";
2
-
3
- // Mermaid-enlarge island — adds an "enlarge" affordance to client-rendered
4
- // mermaid diagrams plus a Google-Maps-style zoom/pan dialog.
5
- //
6
- // Unlike images (which the MDX paragraph override SSR-wraps in
7
- // `<figure class="zd-enlargeable">` with the enlarge button already in the
8
- // markup — see pages/_mdx-components.ts), mermaid diagrams render CLIENT-SIDE:
9
- // the mermaid init script (packages/zudo-doc/src/code-syntax/mermaid-init-script.ts)
10
- // imports mermaid from a CDN, runs `mermaid.run()`, then sets
11
- // `data-mermaid-rendered` on each `.mermaid` container and injects the `<svg>`.
12
- // So there is no server markup to wrap — this island must INJECT the enlarge
13
- // button into each diagram container after it renders.
14
- //
15
- // Lifecycle coupling with the mermaid init script:
16
- // * Primary trigger: a MutationObserver on the content scope watching for the
17
- // `data-mermaid-rendered` attribute appearing (and the `<svg>` child).
18
- // * SPA hops: AFTER_NAVIGATE_EVENT re-scans the (swapped) content body.
19
- // * Theme/tweak re-render: the init script's `reinitMermaid` REMOVES and
20
- // regenerates the `<svg>` (debounced 300ms). The button lives on the
21
- // `.mermaid` CONTAINER (which persists), and the dedupe guard is keyed by
22
- // the container — so the button is neither dropped nor duplicated when the
23
- // svg is regenerated. If the dialog is open during a re-render, the open
24
- // diagram's fresh `<svg>` is re-cloned.
25
-
26
- // Use `preact/compat` so the bundle resolves to Preact's React-shim at runtime
27
- // (zfb's esbuild step doesn't alias bare `react` to `preact/compat`). Mirrors
28
- // image-enlarge.tsx.
29
- import type { JSX } from "preact";
30
- import { useState, useEffect, useRef, useCallback } from "preact/compat";
31
- import { AFTER_NAVIGATE_EVENT } from "@takazudo/zudo-doc/transitions";
32
-
33
- // ---------------------------------------------------------------------------
34
- // Shared dialog shell constants
35
- //
36
- // The hydrated component and the SSR fallback below render into the same Island
37
- // container, so they MUST agree on class string and inline style — otherwise
38
- // the dist HTML and the post-hydration DOM disagree and the first interaction
39
- // flashes. Sourcing both from the same constants closes that drift gap.
40
- //
41
- // The dialog itself is intentionally transform-FREE (centered via
42
- // position:fixed; inset:0; margin:auto). The zoom/pan transform lives on an
43
- // INNER wrapper — a transform on the `<dialog>` would establish a containing
44
- // block for its `position: fixed` descendants, re-anchoring the fixed close
45
- // button to the dialog corner instead of the viewport. Mirrors image-enlarge.
46
- //
47
- // z-modal / backdrop:z-modal-backdrop are defense-in-depth for the SPA-swap
48
- // window: a still-open showModal() dialog can lose top-layer promotion when the
49
- // page body is swapped, so the explicit modal-tier z-index keeps it above all
50
- // chrome. Intentionally redundant in the normal top-layer case.
51
- // ---------------------------------------------------------------------------
52
- const DIALOG_CLASS =
53
- "zd-mermaid-dialog z-modal mx-auto h-[90vh] max-h-[90vh] w-[90vw] max-w-[90vw] overflow-hidden border border-muted bg-surface p-0 backdrop:z-modal-backdrop";
54
- const DIALOG_STYLE = {
55
- position: "fixed",
56
- inset: "0",
57
- margin: "auto",
58
- } as const;
59
-
60
- // Selector for the content-scope root. The button injector scans within this
61
- // scope with `.querySelectorAll(".mermaid")` — no deeper constant needed.
62
- const CONTENT_SCOPE_SELECTOR = "main .zd-content";
63
-
64
- // The diagram svg is a DIRECT child of the `.mermaid` container; the injected
65
- // enlarge button's own icon svg is a grandchild. Selecting `:scope > svg` (not a
66
- // descendant `svg`) so we never pick up the button icon — which matters during a
67
- // theme/tweak re-render, when the diagram svg is briefly removed and a bare
68
- // `querySelector("svg")` would fall back to the button's icon.
69
- const DIAGRAM_SVG_SELECTOR = ":scope > svg";
70
-
71
- // Container-keyed dedupe marker. Set on the `.mermaid` container (which persists
72
- // across theme/tweak re-renders) once its enlarge button is injected, so the
73
- // re-render that regenerates the inner `<svg>` doesn't drop or duplicate it.
74
- const BTN_INJECTED_ATTR = "data-mermaid-enlarge-ready";
75
-
76
- // Zoom step + clamps. scale 1 = diagram fits the dialog (contain).
77
- const ZOOM_STEP = 1.25;
78
- const MIN_SCALE = 1;
79
- const MAX_SCALE = 4;
80
-
81
- // The enlarge button's 4-corner-arrows icon (same shape as ENLARGE_SVG in
82
- // pages/_mdx-components.ts) is injected as an innerHTML string in injectButton()
83
- // below — the button itself is created via document.createElement because the
84
- // mermaid container is plain DOM, not part of this island's render tree.
85
-
86
- // Toolbar icons. currentColor + aria-hidden so they inherit the toolbar button
87
- // color and are skipped by assistive tech (the buttons carry aria-labels).
88
- function PlusIcon() {
89
- return (
90
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true" focusable="false">
91
- <line x1="12" y1="5" x2="12" y2="19" />
92
- <line x1="5" y1="12" x2="19" y2="12" />
93
- </svg>
94
- );
95
- }
96
-
97
- function MinusIcon() {
98
- return (
99
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true" focusable="false">
100
- <line x1="5" y1="12" x2="19" y2="12" />
101
- </svg>
102
- );
103
- }
104
-
105
- function PanIcon() {
106
- return (
107
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round" aria-hidden="true" focusable="false">
108
- <path d="M9 11V5.5a1.5 1.5 0 0 1 3 0V11" />
109
- <path d="M12 11V4.5a1.5 1.5 0 0 1 3 0V11" />
110
- <path d="M15 11V6a1.5 1.5 0 0 1 3 0v6.5a6.5 6.5 0 0 1-6.5 6.5h-1a6 6 0 0 1-4.6-2.16l-2.2-2.86a1.5 1.5 0 0 1 2.3-1.92L9 13" />
111
- <path d="M9 11V8a1.5 1.5 0 0 0-3 0v5" />
112
- </svg>
113
- );
114
- }
115
-
116
- interface OpenDiagram {
117
- /** The live `.mermaid` container being shown — used to re-clone on re-render. */
118
- container: HTMLElement;
119
- /** The cloned `<svg>` outerHTML to render inside the pan viewport. */
120
- svgHtml: string;
121
- }
122
-
123
- export default function MermaidEnlarge() {
124
- const [open, setOpen] = useState<OpenDiagram | null>(null);
125
-
126
- // Zoom/pan state. scale 1 = diagram fits the dialog (contain); translate 0,0.
127
- const [scale, setScale] = useState(1);
128
- const [translate, setTranslate] = useState({ x: 0, y: 0 });
129
- const [panActive, setPanActive] = useState(false);
130
-
131
- const dialogRef = useRef<HTMLDialogElement>(null);
132
- const innerRef = useRef<HTMLDivElement>(null);
133
- // Pointer-drag bookkeeping (refs so the handlers don't re-create on each move).
134
- const dragState = useRef<{
135
- dragging: boolean;
136
- startX: number;
137
- startY: number;
138
- originX: number;
139
- originY: number;
140
- }>({ dragging: false, startX: 0, startY: 0, originX: 0, originY: 0 });
141
-
142
- // -----------------------------------------------------------------------
143
- // Button injection: scan the content scope, inject one enlarge button into
144
- // each RENDERED diagram container, and add `.zd-mermaid-enlargeable` (which
145
- // makes the container position:relative so the absolutely-positioned button
146
- // anchors to it).
147
- // -----------------------------------------------------------------------
148
- useEffect(() => {
149
- let mutationObserver: MutationObserver | null = null;
150
-
151
- function injectButton(container: HTMLElement) {
152
- // Container-keyed guard: the theme/tweak re-render regenerates the inner
153
- // <svg> but the container (and this marker) persists.
154
- if (container.hasAttribute(BTN_INJECTED_ATTR)) return;
155
- // Only inject once the diagram has actually rendered — the button is
156
- // meaningless before there's an <svg> to enlarge.
157
- const rendered =
158
- container.hasAttribute("data-mermaid-rendered") ||
159
- container.querySelector(DIAGRAM_SVG_SELECTOR) !== null;
160
- if (!rendered) return;
161
-
162
- container.setAttribute(BTN_INJECTED_ATTR, "");
163
- container.classList.add("zd-mermaid-enlargeable");
164
-
165
- const btn = document.createElement("button");
166
- btn.type = "button";
167
- btn.className = "zd-enlarge-btn";
168
- btn.setAttribute("aria-label", "Enlarge diagram");
169
- // 4-corner-arrows icon (matches ENLARGE_SVG used by image-enlarge).
170
- btn.innerHTML =
171
- '<svg viewBox="0 0 38.99 38.99" fill="currentColor" focusable="false" aria-hidden="true">' +
172
- '<polygon points="16.2 13.74 5.92 3.47 11.2 3.47 11.2 0 3.47 0 0 0 0 3.47 0 11.2 3.47 11.2 3.47 5.92 13.74 16.2 16.2 13.74" />' +
173
- '<polygon points="25.24 16.2 35.52 5.92 35.52 11.2 38.99 11.2 38.99 3.47 38.99 0 35.52 0 27.79 0 27.79 3.47 33.07 3.47 22.79 13.74 25.24 16.2" />' +
174
- '<polygon points="22.79 25.24 33.07 35.52 27.79 35.52 27.79 38.99 35.52 38.99 38.99 38.99 38.99 35.52 38.99 27.79 35.52 27.79 35.52 33.07 25.24 22.79 22.79 25.24" />' +
175
- '<polygon points="13.74 22.79 3.47 33.07 3.47 27.79 0 27.79 0 35.52 0 38.99 3.47 38.99 11.2 38.99 11.2 35.52 5.92 35.52 16.2 25.24 13.74 22.79" />' +
176
- "</svg>";
177
- container.appendChild(btn);
178
- }
179
-
180
- function scan() {
181
- const scope = document.querySelector(CONTENT_SCOPE_SELECTOR);
182
- if (!scope) return;
183
- scope
184
- .querySelectorAll<HTMLElement>(".mermaid")
185
- .forEach((el) => injectButton(el));
186
- }
187
-
188
- function startObserving() {
189
- const scope = document.querySelector(CONTENT_SCOPE_SELECTOR);
190
- if (scope) {
191
- // Watch for: new `.mermaid` containers (childList), the
192
- // `data-mermaid-rendered` attribute flipping on (attributes), and the
193
- // svg child appearing (childList subtree). Any of these means a diagram
194
- // is now eligible for the button.
195
- mutationObserver = new MutationObserver(() => scan());
196
- mutationObserver.observe(scope, {
197
- childList: true,
198
- subtree: true,
199
- attributes: true,
200
- attributeFilter: ["data-mermaid-rendered"],
201
- });
202
- }
203
- scan();
204
- }
205
-
206
- function handleAfterNavigate() {
207
- mutationObserver?.disconnect();
208
- mutationObserver = null;
209
- startObserving();
210
- }
211
-
212
- startObserving();
213
- document.addEventListener(AFTER_NAVIGATE_EVENT, handleAfterNavigate);
214
-
215
- return () => {
216
- mutationObserver?.disconnect();
217
- document.removeEventListener(AFTER_NAVIGATE_EVENT, handleAfterNavigate);
218
- };
219
- }, []);
220
-
221
- // -----------------------------------------------------------------------
222
- // Delegated open handler: a click on an injected `.zd-enlarge-btn` inside a
223
- // `.zd-mermaid-enlargeable` clones that diagram's `<svg>` into the dialog.
224
- // Mirrors image-enlarge's document-level delegated click.
225
- // -----------------------------------------------------------------------
226
- useEffect(() => {
227
- function handleDocumentClick(e: MouseEvent) {
228
- const target = e.target as Element;
229
- const container = target.closest(".zd-mermaid-enlargeable") as HTMLElement | null;
230
- if (!container) return;
231
- if (!target.closest(".zd-enlarge-btn")) return;
232
- const svg = container.querySelector(DIAGRAM_SVG_SELECTOR);
233
- if (!svg) return;
234
- // Reset zoom/pan on every open.
235
- setScale(1);
236
- setTranslate({ x: 0, y: 0 });
237
- setPanActive(false);
238
- setOpen({ container, svgHtml: svg.outerHTML });
239
- }
240
- document.addEventListener("click", handleDocumentClick);
241
- return () => document.removeEventListener("click", handleDocumentClick);
242
- }, []);
243
-
244
- // -----------------------------------------------------------------------
245
- // Re-clone the fresh `<svg>` if the underlying diagram re-renders while the
246
- // dialog is open (theme/tweak flip removes & regenerates the svg).
247
- // -----------------------------------------------------------------------
248
- useEffect(() => {
249
- if (!open) return;
250
- const { container } = open;
251
- const observer = new MutationObserver(() => {
252
- const svg = container.querySelector(DIAGRAM_SVG_SELECTOR);
253
- if (svg && svg.outerHTML !== open.svgHtml) {
254
- setOpen({ container, svgHtml: svg.outerHTML });
255
- }
256
- });
257
- observer.observe(container, { childList: true, subtree: true });
258
- return () => observer.disconnect();
259
- }, [open]);
260
-
261
- const handleClose = useCallback(() => setOpen(null), []);
262
-
263
- // -----------------------------------------------------------------------
264
- // Dialog open/close sync (inlined from useModalDialog — template projects
265
- // do not ship src/hooks/use-modal-dialog.ts, so the hook is not available).
266
- // -----------------------------------------------------------------------
267
- const isOpen = open !== null;
268
-
269
- // Sync dialog open/close with state.
270
- useEffect(() => {
271
- const dialog = dialogRef.current;
272
- if (!dialog) return;
273
- if (isOpen && !dialog.open) {
274
- dialog.showModal();
275
- } else if (!isOpen && dialog.open) {
276
- dialog.close();
277
- }
278
- }, [isOpen]);
279
-
280
- // Fire handleClose when the dialog is closed natively (Escape key).
281
- useEffect(() => {
282
- const dialog = dialogRef.current;
283
- if (!dialog) return;
284
- function onDialogClose() {
285
- if (isOpen) handleClose();
286
- }
287
- dialog.addEventListener("close", onDialogClose);
288
- return () => dialog.removeEventListener("close", onDialogClose);
289
- }, [isOpen, handleClose]);
290
-
291
- // Close on SPA navigation when dialog is open.
292
- useEffect(() => {
293
- function handleNavigation() {
294
- const dialog = dialogRef.current;
295
- if (dialog?.open) {
296
- dialog.close();
297
- handleClose();
298
- }
299
- }
300
- document.addEventListener(AFTER_NAVIGATE_EVENT, handleNavigation);
301
- return () => document.removeEventListener(AFTER_NAVIGATE_EVENT, handleNavigation);
302
- }, [handleClose]);
303
-
304
- // Backdrop-click handler: close when the click target is the dialog itself.
305
- // Preact-native event type so the template does not depend on @types/react
306
- // being present in the scaffolded project.
307
- function handleBackdropClick(e: JSX.TargetedMouseEvent<HTMLDialogElement>): void {
308
- const dialog = dialogRef.current;
309
- if (!dialog) return;
310
- if (e.target === dialog) dialog.close();
311
- }
312
-
313
- // -----------------------------------------------------------------------
314
- // Zoom controls
315
- // -----------------------------------------------------------------------
316
- const zoomIn = useCallback(() => {
317
- setScale((s) => Math.min(MAX_SCALE, s * ZOOM_STEP));
318
- }, []);
319
-
320
- const zoomOut = useCallback(() => {
321
- setScale((s) => {
322
- const next = Math.max(MIN_SCALE, s / ZOOM_STEP);
323
- // At full width, recenter (translate has no meaning when not zoomed in)
324
- // and turn off pan mode.
325
- if (next <= MIN_SCALE) {
326
- setTranslate({ x: 0, y: 0 });
327
- setPanActive(false);
328
- }
329
- return next;
330
- });
331
- }, []);
332
-
333
- const togglePan = useCallback(() => {
334
- setPanActive((p) => !p);
335
- }, []);
336
-
337
- // Clamp a candidate translate so the diagram can't be dragged fully
338
- // offscreen — keep the scaled content overlapping the viewport. The max
339
- // offset on each axis is half the overflow (scaled size minus base size).
340
- const clampTranslate = useCallback(
341
- (x: number, y: number, s: number) => {
342
- const inner = innerRef.current;
343
- if (!inner) return { x, y };
344
- const rect = inner.getBoundingClientRect();
345
- // rect already reflects the current transform; derive the un-scaled base
346
- // from the applied scale so the bound is stable across repeated drags.
347
- const baseW = rect.width / s;
348
- const baseH = rect.height / s;
349
- const maxX = Math.max(0, (baseW * s - baseW) / 2);
350
- const maxY = Math.max(0, (baseH * s - baseH) / 2);
351
- return {
352
- x: Math.max(-maxX, Math.min(maxX, x)),
353
- y: Math.max(-maxY, Math.min(maxY, y)),
354
- };
355
- },
356
- [],
357
- );
358
-
359
- // -----------------------------------------------------------------------
360
- // Pan drag (pointer events) — active only when pan mode is on and zoomed in.
361
- // -----------------------------------------------------------------------
362
- const onPointerDown = useCallback(
363
- (e: PointerEvent) => {
364
- if (!panActive || scale <= MIN_SCALE) return;
365
- const d = dragState.current;
366
- d.dragging = true;
367
- d.startX = e.clientX;
368
- d.startY = e.clientY;
369
- d.originX = translate.x;
370
- d.originY = translate.y;
371
- (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
372
- },
373
- [panActive, scale, translate],
374
- );
375
-
376
- const onPointerMove = useCallback(
377
- (e: PointerEvent) => {
378
- const d = dragState.current;
379
- if (!d.dragging) return;
380
- const nextX = d.originX + (e.clientX - d.startX);
381
- const nextY = d.originY + (e.clientY - d.startY);
382
- setTranslate(clampTranslate(nextX, nextY, scale));
383
- },
384
- [scale, clampTranslate],
385
- );
386
-
387
- const onPointerUp = useCallback((e: PointerEvent) => {
388
- const d = dragState.current;
389
- if (!d.dragging) return;
390
- d.dragging = false;
391
- (e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId);
392
- }, []);
393
-
394
- const zoomed = scale > MIN_SCALE;
395
- const atMax = scale >= MAX_SCALE;
396
-
397
- return (
398
- <dialog
399
- ref={dialogRef}
400
- onClick={handleBackdropClick}
401
- aria-label="Enlarged diagram"
402
- className={DIALOG_CLASS}
403
- style={DIALOG_STYLE}
404
- >
405
- {open && (
406
- <>
407
- <div
408
- className="zd-mermaid-viewport"
409
- // Pointer handlers live on the viewport so a drag started anywhere
410
- // over the diagram pans it. `touch-action: none` (in CSS) lets the
411
- // pointer events fire on touch without the browser scrolling.
412
- onPointerDown={onPointerDown}
413
- onPointerMove={onPointerMove}
414
- onPointerUp={onPointerUp}
415
- onPointerCancel={onPointerUp}
416
- data-pan-active={panActive && zoomed ? "" : undefined}
417
- >
418
- <div
419
- ref={innerRef}
420
- className="zd-mermaid-transform"
421
- style={{
422
- transform: `translate(${translate.x}px, ${translate.y}px) scale(${scale})`,
423
- transformOrigin: "center",
424
- }}
425
- // The cloned mermaid svg is trusted markup produced by the local
426
- // mermaid render; re-cloned here verbatim.
427
- dangerouslySetInnerHTML={{ __html: open.svgHtml }}
428
- />
429
- </div>
430
-
431
- <div className="zd-mermaid-toolbar" role="toolbar" aria-label="Diagram zoom controls">
432
- <button
433
- type="button"
434
- className="zd-mermaid-tool-btn"
435
- aria-label="Zoom in"
436
- onClick={zoomIn}
437
- disabled={atMax}
438
- >
439
- <PlusIcon />
440
- </button>
441
- <button
442
- type="button"
443
- className="zd-mermaid-tool-btn"
444
- aria-label="Zoom out"
445
- onClick={zoomOut}
446
- disabled={!zoomed}
447
- >
448
- <MinusIcon />
449
- </button>
450
- <button
451
- type="button"
452
- className="zd-mermaid-tool-btn"
453
- aria-label="Toggle pan mode"
454
- aria-pressed={panActive}
455
- onClick={togglePan}
456
- disabled={!zoomed}
457
- >
458
- <PanIcon />
459
- </button>
460
- </div>
461
-
462
- <button
463
- type="button"
464
- onClick={() => dialogRef.current?.close()}
465
- className="zd-enlarge-dialog-close"
466
- aria-label="Close enlarged diagram"
467
- >
468
- <svg viewBox="0 0 161.03 161.03" fill="currentColor" aria-hidden="true" focusable="false">
469
- <polygon points="161.03 10.27 150.76 0 80.51 70.24 10.27 0 0 10.27 70.24 80.51 0 150.76 10.27 161.03 80.51 90.78 150.76 161.03 161.03 150.76 90.78 80.51 161.03 10.27" />
470
- </svg>
471
- </button>
472
- </>
473
- )}
474
- </dialog>
475
- );
476
- }
477
-
478
- /**
479
- * Static SSR fallback for the {@link MermaidEnlarge} island.
480
- *
481
- * The body-end Island wrapper renders this on the server so the dist HTML
482
- * carries an empty, closed `<dialog class="zd-mermaid-dialog ...">` even before
483
- * hydration (a `<dialog>` without `open` is `display:none` per UA stylesheet).
484
- * The classes and inline style come from the shared `DIALOG_CLASS` /
485
- * `DIALOG_STYLE` constants so the SSR fallback cannot drift from the hydrated
486
- * `<dialog>` above. Mirrors `ImageEnlargeSsrFallback`.
487
- */
488
- export function MermaidEnlargeSsrFallback() {
489
- return <dialog aria-label="Enlarged diagram" className={DIALOG_CLASS} style={DIALOG_STYLE} />;
490
- }
@@ -1,47 +0,0 @@
1
- // @ts-check
2
- // zfb plugin module: claude-resources.
3
- //
4
- // Wires `runClaudeResourcesPreStep` (from
5
- // `@takazudo/zudo-doc/integrations/claude-resources`) into zfb's
6
- // `preBuild` lifecycle hook.
7
- //
8
- // Previously this shim spawned a `tsx` subprocess because the integration
9
- // package shipped only TypeScript source (no build step) and `gray-matter`
10
- // pulled in a CJS `require("fs")` that esbuild's ESM-only config-loader
11
- // bundle could not satisfy. Both constraints are now lifted: the package
12
- // ships compiled `dist/` and the plugin host is plain Node (not an esbuild
13
- // bundle), so the runner can be imported directly.
14
-
15
- /** @import { ZfbBuildHookContext, ZfbPlugin } from "@takazudo/zfb/plugins" */
16
-
17
- import { runClaudeResourcesPreStep } from "@takazudo/zudo-doc/integrations/claude-resources";
18
-
19
- const PLUGIN_NAME = "@takazudo/zudo-doc-claude-resources";
20
-
21
- /** @type {ZfbPlugin} */
22
- export default {
23
- name: PLUGIN_NAME,
24
-
25
- /** @param {ZfbBuildHookContext} ctx */
26
- async preBuild(ctx) {
27
- const claudeDir = ctx.options["claudeDir"];
28
- if (typeof claudeDir !== "string" || claudeDir.length === 0) {
29
- throw new Error(
30
- `[${PLUGIN_NAME}] preBuild: options.claudeDir must be a non-empty string (got ${JSON.stringify(claudeDir)})`,
31
- );
32
- }
33
- const projectRootOpt = ctx.options["projectRoot"];
34
- const docsDirOpt = ctx.options["docsDir"];
35
- const result = await runClaudeResourcesPreStep({
36
- claudeDir,
37
- projectRoot:
38
- typeof projectRootOpt === "string" ? projectRootOpt : ctx.projectRoot,
39
- docsDir: typeof docsDirOpt === "string" ? docsDirOpt : "src/content/docs",
40
- });
41
- // Surface a one-line summary so build logs make the generation
42
- // observable.
43
- ctx.logger.info(
44
- `claude-resources: ${result.claudemd} CLAUDE.md, ${result.commands} commands, ${result.skills} skills, ${result.agents} agents`,
45
- );
46
- },
47
- };
@@ -1,111 +0,0 @@
1
- // @ts-check
2
- // zfb plugin module: doc-history.
3
- //
4
- // Wires three lifecycle hooks for the doc-history integration:
5
- //
6
- // preBuild — emits `.zfb/doc-history-meta.json` (per-page git metadata
7
- // consumed at bundle time). Calls `runDocHistoryMetaStep`
8
- // directly; honours `SKIP_DOC_HISTORY=1` via the runner's
9
- // own env check.
10
- //
11
- // postBuild — invokes `runDocHistoryPostBuild` to write
12
- // `<outDir>/doc-history/<slug>.json` files. Skipped by default
13
- // on local builds (opt in with `GEN_DOC_HISTORY=1`); always runs
14
- // in CI; `SKIP_DOC_HISTORY=1` suppresses it everywhere. The
15
- // gating lives in `shouldGeneratePostBuild` (#1986).
16
- //
17
- // devMiddleware — reverse-proxies `/doc-history/*` requests to the
18
- // standalone `@takazudo/zudo-doc-history-server` on port 4322.
19
- //
20
- // Inline functions are not supported by zfb's plugin runtime — see
21
- // `@takazudo/zfb/plugins` source comment. Plugins must be authored as
22
- // standalone modules and referenced from `zfb.config.ts` by `name`.
23
-
24
- /** @import { ZfbBuildHookContext, ZfbDevMiddlewareContext, ZfbPlugin } from "@takazudo/zfb/plugins" */
25
-
26
- import { runDocHistoryMetaStep } from "@takazudo/zudo-doc/integrations/doc-history";
27
- import { runDocHistoryPostBuild } from "@takazudo/zudo-doc/integrations/doc-history";
28
- import { createDocHistoryDevMiddleware } from "@takazudo/zudo-doc/integrations/doc-history";
29
- import { connectToZfbHandler } from "./connect-adapter.mjs";
30
-
31
- /** @type {ZfbPlugin} */
32
- export default {
33
- name: "doc-history",
34
-
35
- /** @param {ZfbBuildHookContext} ctx */
36
- async preBuild(ctx) {
37
- const { docsDir, locales } = ctx.options;
38
- // Validate each locale entry before passing downstream so a misconfigured
39
- // locales map surfaces a clear error instead of a confusing runtime crash.
40
- if (locales != null) {
41
- for (const [key, entry] of Object.entries(/** @type {Record<string, unknown>} */ (locales))) {
42
- if (
43
- entry == null ||
44
- typeof entry !== "object" ||
45
- typeof /** @type {any} */ (entry).dir !== "string" ||
46
- /** @type {any} */ (entry).dir.length === 0
47
- ) {
48
- throw new Error(
49
- `[doc-history] invalid locales entry "${key}": expected { dir: string }`,
50
- );
51
- }
52
- }
53
- }
54
- await runDocHistoryMetaStep({
55
- projectRoot: ctx.projectRoot,
56
- docsDir: typeof docsDir === "string" ? docsDir : "src/content/docs",
57
- locales:
58
- locales != null
59
- ? /** @type {Record<string,{dir:string}>} */ (locales)
60
- : undefined,
61
- });
62
- },
63
-
64
- /** @param {ZfbBuildHookContext} ctx */
65
- async postBuild(ctx) {
66
- try {
67
- await runDocHistoryPostBuild(
68
- /** @type {import("@takazudo/zudo-doc/integrations/doc-history").DocHistoryOptions} */ (/** @type {unknown} */ (ctx.options)),
69
- { outDir: ctx.outDir, logger: ctx.logger },
70
- );
71
- } catch (err) {
72
- // postBuild dropdown JSON is redundant: the parallel build-history CI job
73
- // is the deployed source of truth. Downgrade failures to a warning so a
74
- // transient git/CLI error does not red the build-site job.
75
- const msg = err instanceof Error ? err.message : String(err);
76
- if (ctx.logger?.warn) {
77
- ctx.logger.warn(`[doc-history] postBuild failed (non-fatal): ${msg}`);
78
- } else {
79
- console.warn(`[doc-history] postBuild failed (non-fatal): ${msg}`);
80
- }
81
- }
82
- },
83
-
84
- /** @param {ZfbDevMiddlewareContext} ctx */
85
- devMiddleware(ctx) {
86
- const middleware = createDocHistoryDevMiddleware(
87
- /** @type {import("@takazudo/zudo-doc/integrations/doc-history").DocHistoryOptions} */ (/** @type {unknown} */ (ctx.options)),
88
- ctx.logger,
89
- );
90
- // zfb's `register(path, handler)` matches against the FULL request
91
- // URL (no base-stripping). For a non-root base (e.g. "/my-docs/"),
92
- // requests arrive as `/my-docs/doc-history/foo.json`, so we register
93
- // at the full base-prefixed route. For base="/", the prefix is empty
94
- // and the route is `/doc-history` as expected. The v2 middleware
95
- // itself is base-tolerant (matches via `url.includes("/doc-history/")`)
96
- // and slices from `/doc-history/` onward when proxying upstream.
97
- const basePrefix = stripTrailingSlash(
98
- typeof ctx.options["base"] === "string" ? ctx.options["base"] : "",
99
- );
100
- ctx.register(`${basePrefix}/doc-history`, connectToZfbHandler(middleware));
101
- },
102
- };
103
-
104
- /**
105
- * @param {string} s
106
- * @returns {string}
107
- */
108
- function stripTrailingSlash(s) {
109
- if (typeof s !== "string" || s.length === 0) return "";
110
- return s.endsWith("/") ? s.slice(0, -1) : s;
111
- }