create-zudo-doc 0.2.21 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compose.d.ts +6 -3
- package/dist/compose.js +6 -4
- package/dist/features/image-enlarge.d.ts +22 -12
- package/dist/features/image-enlarge.js +23 -191
- package/dist/scaffold.d.ts +11 -0
- package/dist/scaffold.js +51 -24
- package/dist/settings-gen.js +4 -0
- package/dist/zfb-config-gen.d.ts +16 -10
- package/dist/zfb-config-gen.js +34 -239
- package/package.json +1 -1
- package/templates/base/pages/_mdx-components.ts +64 -185
- package/templates/base/pages/index.tsx +1 -1
- package/templates/base/pages/lib/_body-end-islands.tsx +6 -13
- package/templates/base/pages/lib/_category-nav.tsx +30 -115
- package/templates/base/pages/lib/_category-tree-nav.tsx +38 -65
- package/templates/base/pages/lib/_compose-meta-title.ts +2 -6
- package/templates/base/pages/lib/_doc-body-end.tsx +3 -35
- package/templates/base/pages/lib/_doc-content-header.tsx +10 -111
- package/templates/base/pages/lib/_doc-history-area.tsx +19 -211
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +10 -113
- package/templates/base/pages/lib/_doc-page-renderer.tsx +22 -183
- package/templates/base/pages/lib/_doc-page-shell.tsx +17 -251
- package/templates/base/pages/lib/_doc-pager.tsx +4 -74
- package/templates/base/pages/lib/_doc-route-entries.ts +43 -177
- package/templates/base/pages/lib/_doc-route-paths.ts +16 -101
- package/templates/base/pages/lib/_doc-tags-area.tsx +14 -77
- package/templates/base/pages/lib/_extract-headings.ts +21 -295
- package/templates/base/pages/lib/_footer-with-defaults.tsx +37 -225
- package/templates/base/pages/lib/_frontmatter-preview-data.ts +5 -31
- package/templates/base/pages/lib/_head-with-defaults.tsx +13 -138
- package/templates/base/pages/lib/_header-with-defaults.tsx +24 -324
- package/templates/base/pages/lib/_inline-version-switcher.tsx +16 -78
- package/templates/base/pages/lib/_math-block.tsx +4 -63
- package/templates/base/pages/lib/_nav-data-prep.ts +32 -51
- package/templates/base/pages/lib/_nav-source-cache.ts +12 -57
- package/templates/base/pages/lib/_nav-source-docs.ts +33 -233
- package/templates/base/pages/lib/_search-widget-script.ts +2 -470
- package/templates/base/pages/lib/_search-widget.tsx +9 -195
- package/templates/base/pages/lib/_sidebar-prepaint.tsx +6 -59
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +16 -118
- package/templates/base/pages/lib/_site-tree-nav.tsx +29 -80
- package/templates/base/pages/lib/doc-page-props.ts +26 -44
- package/templates/base/pages/lib/locale-merge.ts +32 -145
- package/templates/base/pages/lib/route-enumerators.ts +52 -286
- package/templates/base/src/components/ai-chat-modal.tsx +9 -8
- package/templates/base/src/components/content/code-group.tsx +3 -76
- package/templates/base/src/components/content/content-admonition.tsx +4 -56
- package/templates/base/src/components/doc-history.tsx +9 -8
- package/templates/base/src/components/image-enlarge.tsx +11 -8
- package/templates/base/src/components/sidebar-toggle.tsx +6 -170
- package/templates/base/src/components/sidebar-tree.tsx +6 -548
- package/templates/base/src/config/color-scheme-utils.ts +34 -158
- package/templates/base/src/config/i18n.ts +9 -0
- package/templates/base/src/config/settings-types.ts +34 -172
- package/templates/base/src/config/z-index-tokens.ts +5 -4
- package/templates/base/src/styles/global.css +40 -587
- package/templates/base/src/utils/base.ts +1 -1
- package/templates/base/src/utils/docs.ts +47 -16
- package/templates/base/src/utils/github.ts +12 -9
- package/templates/base/src/utils/nav-scope.ts +13 -42
- package/templates/base/src/utils/sidebar.ts +18 -86
- package/templates/base/src/utils/slug.ts +10 -53
- package/templates/base/src/utils/smart-break.tsx +12 -120
- package/templates/base/src/utils/tags.ts +25 -68
- package/templates/features/bodyFootUtil/files/src/utils/github.ts +12 -9
- package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +13 -39
- package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -636
- package/templates/features/docHistory/files/src/types/doc-history.ts +7 -23
- package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +35 -201
- package/templates/features/i18n/files/pages/[locale]/index.tsx +1 -1
- package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +8 -269
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +6 -99
- package/templates/features/tagGovernance/files/scripts/tags-audit.ts +67 -515
- package/templates/features/versioning/files/pages/lib/_versions-page.tsx +21 -73
- package/templates/base/pages/404.tsx +0 -61
- package/templates/base/pages/robots.txt.tsx +0 -29
- package/templates/base/pages/sitemap.xml.tsx +0 -59
- package/templates/base/plugins/connect-adapter.mjs +0 -169
- package/templates/base/plugins/copy-public-plugin.mjs +0 -58
- package/templates/base/plugins/search-index-plugin.mjs +0 -66
- package/templates/base/scripts/gen-z-index.mjs +0 -157
- package/templates/base/src/components/mermaid-enlarge.tsx +0 -490
- package/templates/base/src/components/site-tree-nav.tsx +0 -220
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +0 -47
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +0 -111
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +0 -59
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +0 -39
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +0 -43
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +0 -20
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +0 -93
- package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +0 -48
- package/templates/features/versioning/files/pages/docs/versions.tsx +0 -20
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// scripts/gen-z-index.mjs
|
|
3
|
-
//
|
|
4
|
-
// Codegen: rewrite the GENERATED:Z_INDEX marker block inside
|
|
5
|
-
// src/styles/global.css from the single source of truth in
|
|
6
|
-
// src/config/z-index-tokens.ts.
|
|
7
|
-
//
|
|
8
|
-
// The block is a Tailwind v4 `@theme { --z-index-<name>: <value>; }` for every
|
|
9
|
-
// tier, so Tailwind generates `z-<name>` utilities (e.g. `--z-index-toolbar: 20`
|
|
10
|
-
// → `.z-toolbar { z-index: 20 }`) and raw CSS can reference the same var via
|
|
11
|
-
// `z-index: var(--z-index-<name>)`.
|
|
12
|
-
//
|
|
13
|
-
// Pure Node (fs only — NO npm deps). Idempotent: running twice produces no diff.
|
|
14
|
-
//
|
|
15
|
-
// Usage:
|
|
16
|
-
// node scripts/gen-z-index.mjs # rewrite the block in global.css
|
|
17
|
-
// node scripts/gen-z-index.mjs --check # verify committed block is up to date
|
|
18
|
-
// # (exit 1 on drift, no write)
|
|
19
|
-
//
|
|
20
|
-
// MAINTENANCE: edit src/config/z-index-tokens.ts (the source of truth), then run
|
|
21
|
-
// `pnpm gen:z-index` and commit the regenerated global.css. Never hand-edit the
|
|
22
|
-
// block between the BEGIN/END markers.
|
|
23
|
-
|
|
24
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
25
|
-
import { resolve, dirname } from "node:path";
|
|
26
|
-
import { fileURLToPath } from "node:url";
|
|
27
|
-
|
|
28
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
-
const ROOT = resolve(__dirname, "..");
|
|
30
|
-
|
|
31
|
-
const TOKENS_PATH = resolve(ROOT, "src/config/z-index-tokens.ts");
|
|
32
|
-
const CSS_PATH = resolve(ROOT, "src/styles/global.css");
|
|
33
|
-
|
|
34
|
-
const BEGIN_MARKER = "GENERATED:Z_INDEX_BEGIN";
|
|
35
|
-
const END_MARKER = "GENERATED:Z_INDEX_END";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Parse the Z_INDEX_TIERS array out of z-index-tokens.ts WITHOUT importing it
|
|
39
|
-
* (this script is a dependency-free .mjs and cannot resolve TypeScript). Reads
|
|
40
|
-
* each `{ name: "...", value: <n>, ... }` object literal. Throws on a malformed
|
|
41
|
-
* source so drift between the parser and the file surfaces loudly.
|
|
42
|
-
*/
|
|
43
|
-
function parseTiers(src) {
|
|
44
|
-
const arrayMatch = src.match(
|
|
45
|
-
/export const Z_INDEX_TIERS[^=]*=\s*\[([\s\S]*?)\];/,
|
|
46
|
-
);
|
|
47
|
-
if (!arrayMatch) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
`Could not locate "export const Z_INDEX_TIERS = [ ... ]" in ${TOKENS_PATH}`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
const body = arrayMatch[1];
|
|
53
|
-
const tiers = [];
|
|
54
|
-
// Each tier is a `{ ... }` object literal; iterate top-level braces.
|
|
55
|
-
const objectRe = /\{([\s\S]*?)\}/g;
|
|
56
|
-
let m;
|
|
57
|
-
while ((m = objectRe.exec(body)) !== null) {
|
|
58
|
-
const obj = m[1];
|
|
59
|
-
const nameMatch = obj.match(/name:\s*"([^"]+)"/);
|
|
60
|
-
const valueMatch = obj.match(/value:\s*(-?\d+)/);
|
|
61
|
-
if (!nameMatch || !valueMatch) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`Malformed tier object in Z_INDEX_TIERS (missing name/value): ${obj.trim()}`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
tiers.push({ name: nameMatch[1], value: Number(valueMatch[1]) });
|
|
67
|
-
}
|
|
68
|
-
if (tiers.length === 0) {
|
|
69
|
-
throw new Error(`Z_INDEX_TIERS in ${TOKENS_PATH} parsed to an empty list`);
|
|
70
|
-
}
|
|
71
|
-
return tiers;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Build the full generated block (markers included). Two leading spaces of
|
|
76
|
-
* indentation match the surrounding `@theme` style in global.css.
|
|
77
|
-
*/
|
|
78
|
-
function buildBlock(tiers) {
|
|
79
|
-
const lines = [];
|
|
80
|
-
lines.push(` /* ${BEGIN_MARKER}`);
|
|
81
|
-
lines.push(
|
|
82
|
-
` * GENERATED:Z_INDEX — do not hand-edit; run pnpm gen:z-index.`,
|
|
83
|
-
);
|
|
84
|
-
lines.push(
|
|
85
|
-
` * Source of truth: src/config/z-index-tokens.ts. Tailwind v4 reads the`,
|
|
86
|
-
);
|
|
87
|
-
lines.push(
|
|
88
|
-
` * --z-index-<name> theme key and generates a z-<name> utility. */`,
|
|
89
|
-
);
|
|
90
|
-
lines.push(` @theme {`);
|
|
91
|
-
for (const tier of tiers) {
|
|
92
|
-
lines.push(` --z-index-${tier.name}: ${tier.value};`);
|
|
93
|
-
}
|
|
94
|
-
lines.push(` }`);
|
|
95
|
-
lines.push(` /* ${END_MARKER} */`);
|
|
96
|
-
return lines.join("\n");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Replace the existing BEGIN…END block in `css` with `block`. Throws if the
|
|
101
|
-
* markers are missing (the block must be seeded once by hand — see global.css).
|
|
102
|
-
*/
|
|
103
|
-
function replaceBlock(css, block) {
|
|
104
|
-
const beginIdx = css.indexOf(BEGIN_MARKER);
|
|
105
|
-
const endIdx = css.indexOf(END_MARKER);
|
|
106
|
-
if (beginIdx === -1 || endIdx === -1) {
|
|
107
|
-
throw new Error(
|
|
108
|
-
`Could not find ${BEGIN_MARKER} … ${END_MARKER} markers in ${CSS_PATH}.\n` +
|
|
109
|
-
`Seed the marker block once by hand, then re-run the generator.`,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
// Expand to the full comment line that opens the block (" /* GENERATED:...")
|
|
113
|
-
// and to the end of the closing "*/ " line so the whole region is replaced.
|
|
114
|
-
const lineStart = css.lastIndexOf("\n", beginIdx) + 1;
|
|
115
|
-
const afterEnd = css.indexOf("\n", endIdx);
|
|
116
|
-
const lineEnd = afterEnd === -1 ? css.length : afterEnd;
|
|
117
|
-
return css.slice(0, lineStart) + block + css.slice(lineEnd);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function main() {
|
|
121
|
-
const check = process.argv.includes("--check");
|
|
122
|
-
|
|
123
|
-
const tokensSrc = readFileSync(TOKENS_PATH, "utf8");
|
|
124
|
-
const css = readFileSync(CSS_PATH, "utf8");
|
|
125
|
-
|
|
126
|
-
const tiers = parseTiers(tokensSrc);
|
|
127
|
-
const block = buildBlock(tiers);
|
|
128
|
-
const next = replaceBlock(css, block);
|
|
129
|
-
|
|
130
|
-
if (check) {
|
|
131
|
-
if (next !== css) {
|
|
132
|
-
console.error(
|
|
133
|
-
"z-index codegen drift detected: src/styles/global.css is out of date.",
|
|
134
|
-
);
|
|
135
|
-
console.error("Run `pnpm gen:z-index` and commit the result.");
|
|
136
|
-
return 1;
|
|
137
|
-
}
|
|
138
|
-
console.log(
|
|
139
|
-
`OK — z-index @theme block is up to date (${tiers.length} tiers).`,
|
|
140
|
-
);
|
|
141
|
-
return 0;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (next === css) {
|
|
145
|
-
console.log(
|
|
146
|
-
`z-index @theme block already up to date (${tiers.length} tiers); no change.`,
|
|
147
|
-
);
|
|
148
|
-
return 0;
|
|
149
|
-
}
|
|
150
|
-
writeFileSync(CSS_PATH, next);
|
|
151
|
-
console.log(
|
|
152
|
-
`Wrote z-index @theme block to src/styles/global.css (${tiers.length} tiers).`,
|
|
153
|
-
);
|
|
154
|
-
return 0;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
process.exit(main());
|
|
@@ -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
|
-
}
|