diff --git a/AGENTS.md b/AGENTS.md index 4edb25685..671dcdf97 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1073,6 +1073,8 @@ There is **one** highlighter in the app: the Shiki instance `@pierre/diffs` alre **Theming:** fences resolve the SAME theme the diff pane resolves, via `resolveFenceTheme` / `resolveSyntaxTheme` in `packages/ui/utils/syntaxTheme.ts` (keyed on `(colorTheme, resolvedMode)`; `packages/review-editor/hooks/usePierreTheme.ts` re-exports them). `useFenceTheme()` (`packages/ui/hooks/useFenceTheme.ts`) feeds the components and re-highlights on palette or mode change. Palettes with no Shiki counterpart fall back to `@pierre/diffs`' own `pierre-dark` / `pierre-light`. Consequence: code blocks follow the active palette in both light and dark instead of always rendering github-dark, so **do not add per-theme `.hljs-*`-style token CSS** — pick the right Shiki theme in `SHIKI_THEME_MAP` instead. +**Mermaid runtime (12.x, lazy):** `@plannotator/ui` pins `mermaid` exactly (12.0.0 since ui 0.40.0; Mermaid 12 lays flowchart/state/class/ER/requirement diagrams out with ELK by default and targets Safari 17.4+ / ES2024 — we take its defaults rather than pinning the 11.x ones). The runtime is loaded lazily on the first diagram through `packages/ui/utils/mermaid.ts`'s own `import('mermaid')`: `packages/editor/App.tsx` deliberately does NOT import `@plannotator/ui/utils/mermaid-eager` (it did through ui 0.39.0), so a plan with no diagram never downloads the ~2 MB runtime + ELK on the share portal or in a host build, and `tests/entry-assets.test.ts` fails if the eager import creeps back into either app. The single-file builds (`apps/hook`, `apps/review`, opencode, the compiled binary) still inline the runtime through `inlineDynamicImports`, so the lazy import saves nothing there and resolves from the bundle itself; the Mermaid 12 cost in those builds is the runtime's own growth. `MermaidBlock` shows the source fence under a "Rendering diagram" status until the first render lands (never the error panel as a placeholder), and `applyMermaidTheme` is keyed on the runtime object, so the lazily loaded runtime is themed on its first render exactly like an eagerly registered one. `mermaid-eager` stays exported for hosts that want the old startup registration. + **Diagram theming (Mermaid):** diagrams follow the palette and mode the same way fences do, through ONE dynamic mapping rather than per-palette themes. `packages/ui/utils/mermaidTheme.ts` reads the live CSS tokens off the document element (`readThemeTokens`: `--background`, `--foreground`, `--card`, `--border`, `--muted`, `--muted-foreground`, `--primary`, the accent tokens, `--font-sans`), derives a complete Mermaid `themeVariables` set for every diagram family from them (`buildMermaidThemeVariables(tokens, mode)`, pure; base theme `dark` under a dark resolved mode, `default` under light; node fill from `card`, borders from `border`, edges and arrowheads from `muted-foreground`, text from `foreground`/`card-foreground`, clusters from `muted`, twelve categorical fills for pie/git/mindmap/journey seeded from `primary`, `accent`, `success`, `warning`, `destructive` and normalized to one lightness per page polarity), and `MermaidBlock` runs the global `mermaid.initialize` through `applyMermaidTheme` once per `(palette, mode)` key from `useTheme()` before each render, re-rendering mounted diagrams when the key changes. Every colour handed to Mermaid is opaque hex (its colour library does not read `oklch()`), and a contrast guard (`ensureContrast`) repairs any text-on-fill pair under WCAG 4.5:1 or line-on-canvas pair under 3:1 (plus 0.1 headroom) by the smallest OKLab step toward `foreground`, then `background`, then pure black/white — guarding page-level text and lines against every surface they can cross (the `bg-muted/30` canvas over the document card and over the bare page, `card`, `muted`, `popover`, ER rows), not the page background alone; `mermaidTheme.test.ts` sweeps every palette in `packages/ui/themes` in both modes against that rule, so a new palette cannot regress it. The pure toolkit behind it is `packages/ui/utils/cssColor.ts`. **Host fallback:** with no theme tokens on the document `readThemeTokens` returns `undefined` and the runtime keeps the static `MERMAID_CONFIG` (still `securityLevel: 'strict'`, pinned) with no extra `initialize`, so a host without `ThemeProvider`/`theme.css` renders exactly as before. Do not add per-palette Mermaid themes or per-theme `.mermaid` CSS — extend the token mapping instead. `GraphvizBlock` already rewrites its SVG to `var(--foreground)` / `var(--muted-foreground)` / `var(--muted)` and needs no equivalent. **Bundle note:** Pierre imports Shiki's full bundle, so every grammar and theme is already inlined in the single-file builds; reusing its shared highlighter costs no extra bytes and needs no CDN or runtime wasm fetch. The Oniguruma WASM engine is dead weight under `shiki-js` and is aliased to `build/shiki-wasm-stub.ts` in the review, hook and portal Vite configs (via `resolve.alias`, which — unlike `plugins` — is shared with Vite's worker build). diff --git a/apps/skills/extra/plannotator-visual-explainer/SKILL.md b/apps/skills/extra/plannotator-visual-explainer/SKILL.md index 01ad73f6b..afe1451a0 100644 --- a/apps/skills/extra/plannotator-visual-explainer/SKILL.md +++ b/apps/skills/extra/plannotator-visual-explainer/SKILL.md @@ -24,7 +24,7 @@ Three paths depending on content type. Each has its own references and structure Always deliver via Plannotator's annotation UI. Do NOT use `open` or `xdg-open`. -For any deliverable that uses Mermaid, render every diagram with Mermaid 11 in both the light +For any deliverable that uses Mermaid, render every diagram with Mermaid 12 in both the light and dark palettes before opening the annotation UI. Rendering is a hard gate: an exception, empty SVG, or error output such as `aria-roledescription="error"` or `Syntax error in text` means the explainer is not deliverable. Fix the diagram or theme configuration and rerun both diff --git a/apps/skills/extra/plannotator-visual-explainer/SKILL.test.ts b/apps/skills/extra/plannotator-visual-explainer/SKILL.test.ts index 798843a72..cfbac5f68 100644 --- a/apps/skills/extra/plannotator-visual-explainer/SKILL.test.ts +++ b/apps/skills/extra/plannotator-visual-explainer/SKILL.test.ts @@ -80,7 +80,7 @@ describe("plannotator-visual-explainer Mermaid theming", () => { ); }); - test("renders representative Mermaid 11 diagrams in both palettes", async () => { + test("renders representative Mermaid 12 diagrams in both palettes", async () => { const palettes = [captureMermaidConfig("light"), captureMermaidConfig("dark")]; const uiPackageDir = join(import.meta.dir, "../../../../packages/ui"); const renderProbe = String.raw` @@ -92,8 +92,8 @@ describe("plannotator-visual-explainer Mermaid theming", () => { import("mermaid/package.json", { with: { type: "json" } }), ]); - if (!String(mermaidPackage.version).startsWith("11.")) { - throw new Error("Expected Mermaid 11, received " + mermaidPackage.version); + if (!String(mermaidPackage.version).startsWith("12.")) { + throw new Error("Expected Mermaid 12, received " + mermaidPackage.version); } const palettes = JSON.parse(process.env.PLANNOTATOR_MERMAID_PALETTES ?? "[]"); @@ -184,12 +184,12 @@ describe("plannotator-visual-explainer Mermaid theming", () => { throw new Error(`Mermaid render probe failed:\n${stderr || stdout}`); } expect(stdout).toMatch( - /Rendered 4 Mermaid 11\.[0-9.]+\.[0-9]+ SVGs without error signatures/, + /Rendered 4 Mermaid 12\.[0-9.]+\.[0-9]+ SVGs without error signatures/, ); }, 20_000); test("keeps Mermaid rendering as a pre-delivery gate", () => { - expect(skill).toContain("render every diagram with Mermaid 11"); + expect(skill).toContain("render every diagram with Mermaid 12"); expect(skill).toContain('aria-roledescription="error"'); expect(skill).toContain("Syntax error in text"); expect(skill).toContain("the explainer is not deliverable"); diff --git a/bun.lock b/bun.lock index 735be3fe3..4a1cdee4c 100644 --- a/bun.lock +++ b/bun.lock @@ -191,7 +191,7 @@ }, "packages/core": { "name": "@plannotator/core", - "version": "0.25.2", + "version": "0.25.3", "devDependencies": { "typescript": "~5.8.2", }, @@ -290,7 +290,7 @@ }, "packages/ui": { "name": "@plannotator/ui", - "version": "0.39.0", + "version": "0.40.0", "dependencies": { "@base-ui/react": "^1.7.0", "@codemirror/autocomplete": "^6.20.3", @@ -312,7 +312,7 @@ "@lezer/highlight": "^1.2.3", "@pierre/diffs": "1.3.6", "@plannotator/atomic-editor": "^0.8.0", - "@plannotator/core": "0.25.2", + "@plannotator/core": "0.25.3", "@plannotator/markdown-editor": "^0.4.0", "@plannotator/web-highlighter": "^0.8.1", "@tanstack/react-table": "^8.21.3", @@ -324,7 +324,7 @@ "katex": "^0.16.47", "lucide-react": "^1.38.0", "marked": "^17.0.6", - "mermaid": "^11.17.2", + "mermaid": "12.0.0", "motion": "^12.38.0", "perfect-freehand": "^1.2.2", "tailwind-merge": "^3.6.0", @@ -349,6 +349,9 @@ }, }, }, + "overrides": { + "lodash-es": "4.18.1", + }, "packages": { "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], @@ -554,8 +557,16 @@ "@capsizecss/unpack": ["@capsizecss/unpack@4.0.1", "", { "dependencies": { "fontkitten": "^1.0.3" } }, "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ=="], + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.1.2", "", { "dependencies": { "@chevrotain/gast": "11.1.2", "@chevrotain/types": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q=="], + + "@chevrotain/gast": ["@chevrotain/gast@11.1.2", "", { "dependencies": { "@chevrotain/types": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g=="], + + "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.1.2", "", {}, "sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw=="], + "@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], + "@chevrotain/utils": ["@chevrotain/utils@11.1.2", "", {}, "sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA=="], + "@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="], "@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="], @@ -816,7 +827,7 @@ "@mariozechner/clipboard-win32-x64-msvc": ["@mariozechner/clipboard-win32-x64-msvc@0.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA=="], - "@mermaid-js/parser": ["@mermaid-js/parser@1.2.1", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw=="], + "@mermaid-js/parser": ["@mermaid-js/parser@2.0.0", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-K8BeapFUfrfxbRAUQAG5oBOCwo1+bNWzaVnPxTCutkfrTBn6T/j91FIhqxWJ32SUeQ9T3iy1zcmPZ5ROZEvDrg=="], "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], @@ -1416,6 +1427,8 @@ "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], + "chevrotain": ["chevrotain@11.1.2", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.1.2", "@chevrotain/gast": "11.1.2", "@chevrotain/regexp-to-ast": "11.1.2", "@chevrotain/types": "11.1.2", "@chevrotain/utils": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg=="], + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], @@ -1612,6 +1625,8 @@ "electron-to-chromium": ["electron-to-chromium@1.5.421", "", {}, "sha512-cUhfpHQy+PGbt+X90DMcAVazDCziIZr73hpxD4LRs4BGQoJCifPzTfQWa7S6c+uhokTOBe8tot09GBSEO6c9LA=="], + "elkjs": ["elkjs@0.9.3", "", {}, "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], @@ -1680,8 +1695,6 @@ "fast-xml-parser": ["fast-xml-parser@5.11.1", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.2", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw=="], - "fastdom": ["fastdom@1.0.12", "", { "dependencies": { "strictdom": "^1.0.1" } }, "sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg=="], - "fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -1968,7 +1981,7 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "mermaid": ["mermaid@11.17.2", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.21", "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", "fastdom": "1.0.12", "katex": "^0.16.47", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg=="], + "mermaid": ["mermaid@12.0.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^2.0.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "chevrotain": "~11.1.2", "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.21", "dompurify": "^3.4.12", "elkjs": "^0.9.3", "es-toolkit": "^1.45.1", "katex": "^0.16.47", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-/wQXC9iBxoGV8p3erbvaXs9h77VyLDBH6GdayVjj3hEcSQhFU4N1WUhUppotCEqlIxI2pRMwjwBSwTB1MfZBgQ=="], "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], @@ -2292,8 +2305,6 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "strictdom": ["strictdom@1.0.1", "", {}, "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -2652,6 +2663,8 @@ "parse-semver/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "plannotator-webview/@types/bun": ["@types/bun@1.4.2", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="], + "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], "read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], @@ -2784,6 +2797,8 @@ "normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "plannotator-webview/@types/bun/bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="], + "rimraf/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], diff --git a/package.json b/package.json index e23a7965a..bf4355987 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,10 @@ "bugs": { "url": "https://github.com/backnotprop/plannotator/issues" }, - "workspaces": ["apps/*", "packages/*"], + "workspaces": [ + "apps/*", + "packages/*" + ], "scripts": { "dev:hook": "bun run --cwd apps/hook dev", "dev:portal": "bun run --cwd apps/portal dev", @@ -51,5 +54,8 @@ "@types/node": "^25.5.2", "@types/turndown": "^5.0.6", "bun-types": "^1.4.0" + }, + "overrides": { + "lodash-es": "4.18.1" } } diff --git a/packages/core/package.json b/packages/core/package.json index fcf20a3e6..567520422 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@plannotator/core", - "version": "0.25.2", + "version": "0.25.3", "type": "module", "exports": { "./agents": "./agents.ts", diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index cbaefa155..03019346e 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -1,14 +1,16 @@ // Eager renderer registration (side-effect imports, evaluated before every -// other module below). These keep Plannotator's first paint, identity minting -// and failure surface byte-identical now that @plannotator/ui loads KaTeX, the -// username dictionary and the Mermaid runtime lazily for hosts: math is typeset -// on the first commit, names come from the full dictionary, and Mermaid stays -// in this app's entry chunk (the review editor never renders Mermaid and does -// not import that entry). Guarded by tests/entry-assets.test.ts; do not drop +// other module below). These keep Plannotator's first paint and identity +// minting byte-identical now that @plannotator/ui loads KaTeX and the username +// dictionary lazily for hosts: math is typeset on the first commit and names +// come from the full dictionary. The Mermaid runtime is deliberately NOT +// registered here: since Mermaid 12 (ELK layout by default) it loads on the +// first diagram through `utils/mermaid`'s own `import('mermaid')`, so a plan +// with no diagram never pays for it in a chunked build (the share portal, any +// @plannotator/ui host). The single-file builds still inline it through +// `inlineDynamicImports`. Guarded by tests/entry-assets.test.ts; do not drop // or reorder any of these lines. import '@plannotator/ui/utils/math-eager'; import '@plannotator/ui/utils/identity-tater'; -import '@plannotator/ui/utils/mermaid-eager'; import React, { useState, useEffect, useLayoutEffect, useMemo, useRef, useCallback } from 'react'; import { toast, Toaster } from 'sonner'; import { type Origin, getAgentName } from '@plannotator/shared/agents'; diff --git a/packages/ui/HANDOFF.md b/packages/ui/HANDOFF.md index 47fe32eb1..fc1e54b9d 100644 --- a/packages/ui/HANDOFF.md +++ b/packages/ui/HANDOFF.md @@ -218,8 +218,8 @@ We deliberately did **not** restructure the exports map in this PR (move-don't-r | `utils/math` (`loadMathRenderer`, `getMathRenderer`, `getMathRendererSource`, `setMathRenderer`, `setMathRendererLoader`, `getMathRendererLoader`, `resetMathRenderer`) and `utils/math-eager` | The math renderer slot and its eager KaTeX registration. Import `utils/math-eager` for synchronous typesetting on the first commit; call `loadMathRenderer()` to pre-warm the lazy path. `resetMathRenderer()` empties the slot and keeps the registered loader; `setMathRendererLoader(null)` drops it. See "Lazy renderers and eager entries". | | `utils/mermaid-math-slot` | Alias target only: what a host redirects Mermaid's own `katex` import to, so `$$` labels in diagrams typeset through the math slot and the host build carries one KaTeX chunk. Never import it yourself. See "Lazy renderers and eager entries", item 2. | | `utils/identity-tater` | Side-effect entry that registers the full username dictionary into the identity generator slot. Import it only if you rely on the default tater names and want the full dictionary; a host with `identityProvider` should not. | -| `utils/mermaid` (`loadMermaidRuntime`, `getMermaidRuntime`, `getMermaidRuntimeSource`, `setMermaidRuntime`, `MERMAID_CONFIG`) and `utils/mermaid-eager` | The Mermaid runtime slot and its eager registration. Import `utils/mermaid-eager` to keep Mermaid in your entry chunk as Plannotator does; omit it for the lazy path with retry. See "Lazy renderers and eager entries". | -| `utils/mermaidTheme` (`buildMermaidThemeVariables`, `readThemeTokens`, `applyMermaidTheme`, `mermaidThemeKey`, `buildMermaidConfig`, `ensureContrast`) and `utils/cssColor` | Theme-aware diagram configuration: the pure token-to-`themeVariables` mapping with its contrast guard, the document token reader, and the cached per-`(palette, mode)` `initialize` step `MermaidBlock` runs before each render. Additive; with no theme tokens on the document the static `MERMAID_CONFIG` stays in force. See "Theme-aware Mermaid diagrams (next publish)". | +| `utils/mermaid` (`loadMermaidRuntime`, `getMermaidRuntime`, `getMermaidRuntimeSource`, `setMermaidRuntime`, `MERMAID_CONFIG`) and `utils/mermaid-eager` | The Mermaid runtime slot and its eager registration. Omit `utils/mermaid-eager` for the lazy path with retry, which is what Plannotator itself does since 0.40.0 (Mermaid 12); import it to register the runtime in your entry chunk at startup. See "Lazy renderers and eager entries" and "Mermaid 12 (0.40.0)". | +| `utils/mermaidTheme` (`buildMermaidThemeVariables`, `readThemeTokens`, `applyMermaidTheme`, `mermaidThemeKey`, `buildMermaidConfig`, `ensureContrast`) and `utils/cssColor` | Theme-aware diagram configuration: the pure token-to-`themeVariables` mapping with its contrast guard, the document token reader, and the cached per-`(palette, mode)` `initialize` step `MermaidBlock` runs before each render. Additive; with no theme tokens on the document the static `MERMAID_CONFIG` stays in force. See "Theme-aware Mermaid diagrams (0.40.0)". | **AI is fully avoidable** — with one precision worth knowing. No AI *UI* is reachable from the supported components: `useAIChat` is imported only by `components/ai/DocumentAIChatPanel` and `useAIProviderConfig`, neither of which any supported component imports, and `CommentPopover`'s Ask-AI affordance exists only behind the optional `onAskAI` prop. `configure.ts` does statically import the `useAIChat` module (it needs `setAITransport`), but if you never use AI the hook is dead code and bundlers eliminate it — verified empirically: a standalone consumer's production bundle importing the full supported surface contains zero `/api/ai` strings. Don't import `components/ai/*` and don't pass `aiTransport`, and you ship no AI code. @@ -457,11 +457,11 @@ Behavior is pinned by `components/MarkdownEditor.embedPicker.test.ts`, the suppo ## Lazy renderers and eager entries (0.32.0) -Four modules that used to ride every document read for a host that bundles by route now load on demand: the Mermaid runtime, the Graphviz engine, KaTeX, and the username dictionary. Plannotator's own apps register KaTeX and the dictionary eagerly in both the plan editor and the review editor, and the plan editor also registers the Mermaid runtime eagerly (the review editor never renders a Mermaid block and deliberately does not), so every surface renders exactly as before; the single-file builds are unchanged in size and first paint and the portal entry chunk keeps Mermaid as on main (the built-HTML markers and the A/B proof live in `tests/entry-assets.test.ts` and the PR that shipped this). +Four modules that used to ride every document read for a host that bundles by route now load on demand: the Mermaid runtime, the Graphviz engine, KaTeX, and the username dictionary. Plannotator's own apps register KaTeX and the dictionary eagerly in both the plan editor and the review editor, and through 0.39.0 the plan editor also registered the Mermaid runtime eagerly (the review editor never renders a Mermaid block and deliberately does not), so every surface rendered exactly as before; the single-file builds were unchanged in size and first paint and the portal entry chunk kept Mermaid as on main (the built-HTML markers and the A/B proof live in `tests/entry-assets.test.ts` and the PR that shipped this). **Since 0.40.0 the plan editor takes the lazy Mermaid path too** — see "Mermaid 12 (0.40.0)" for why and for the host contract; the paragraph below describes the slot, which is unchanged. 1. **Graphviz: no seam, nothing to do.** `GraphvizBlock` imports `@viz-js/viz` inside its render effect. It already showed the source fence until the SVG landed, so the only change for a chunking host is that the first dot fence on a page fetches the engine. A failed import is dropped from the memo and re-attempted once with a fresh `import()` after a short delay; a persistently failing chunk surfaces as the existing error panel with the source, plus a Retry button that issues another fresh attempt (a diagram syntax error shows the panel exactly as before, without Retry). Hosts that aliased the specifier to a lazy shim can delete the shim. - **Mermaid: a runtime slot, filled eagerly by Plannotator.** `utils/mermaid` holds the slot (`getMermaidRuntime`, `setMermaidRuntime`, `getMermaidRuntimeSource`) and the one code path `MermaidBlock` uses, `loadMermaidRuntime()`: it resolves at once from a filled slot and otherwise imports `mermaid` lazily, initialized once with `MERMAID_CONFIG` (`securityLevel: 'strict'` pinned by test), with the same drop-on-rejection, one automatic re-attempt and Retry button as Graphviz. `utils/mermaid-eager` imports the runtime statically, initializes it at module evaluation (where the old module-scope `initialize` ran) and fills the slot; `packages/editor/App.tsx` imports it by policy, so Plannotator's plan surfaces keep Mermaid in their entry chunk and it can never fail separately from the app (on the share portal `mermaid.core` stays in the entry, as on main). The review editor does not import it because it never renders a Mermaid block. A host that wants the same adds `import '@plannotator/ui/utils/mermaid-eager'`; a host that omits it gets the lazy path. + **Mermaid: a runtime slot, filled eagerly by Plannotator.** `utils/mermaid` holds the slot (`getMermaidRuntime`, `setMermaidRuntime`, `getMermaidRuntimeSource`) and the one code path `MermaidBlock` uses, `loadMermaidRuntime()`: it resolves at once from a filled slot and otherwise imports `mermaid` lazily, initialized once with `MERMAID_CONFIG` (`securityLevel: 'strict'` pinned by test), with the same drop-on-rejection, one automatic re-attempt and Retry button as Graphviz. `utils/mermaid-eager` imports the runtime statically, initializes it at module evaluation (where the old module-scope `initialize` ran) and fills the slot; through 0.39.0 `packages/editor/App.tsx` imported it by policy, so Plannotator's plan surfaces kept Mermaid in their entry chunk and it could never fail separately from the app. Since 0.40.0 neither Plannotator app imports it (Mermaid 12's runtime is too large to ride every plan read; `tests/entry-assets.test.ts` now asserts the eager marker is ABSENT from both bundles). A host that wants startup registration adds `import '@plannotator/ui/utils/mermaid-eager'`; a host that omits it gets the lazy path, exactly like Plannotator. **Retry, honestly.** An in-page retry cannot recover a chunk whose first fetch failed: browsers record a failed module fetch in the module map for the page lifetime, so a fresh `import()` of the same URL rejects without a request, and package code cannot re-import under a new URL because Rollup minifies the chunk's export names. The retry therefore recovers failures after the fetch (engine instantiation, `initialize`) and hosts that version chunk URLs; a host that needs recovery from a failed first fetch uses versioned chunk URLs or a `vite:preloadError` reload at app level. The panel with the source is always shown, never a blank. @@ -486,7 +486,7 @@ Four modules that used to ride every document read for a host that bundles by ro With the alias the same consumer build emits zero chunks carrying the KaTeX body out of the package and the entry's only `import()` in that area is the host's own loader chunk. Do not alias without registering a loader: math would then render as TeX text forever. Plannotator's entries import `math-eager`, so the slot is filled before the first render and this branch is never reached there; the single-file builds inline the default through `inlineDynamicImports` as before (`tests/entry-assets.test.ts` pins the split: `utils/math` has no `import('katex')` site, `utils/math-default-loader` has the only one). - **Mermaid's own KaTeX, and the last shared chunk (0.34.0, from 0.33.0 adoption feedback).** The alias above is not the whole story once a page can render a Mermaid diagram. The Mermaid runtime (11.15.0 in this checkout) typesets `$$...$$` labels through its own `import("katex")`, inside `renderKatexUnsanitized`, and it offers nothing to turn that off: `legacyMathML` / `forceLegacyMathML` only choose the output mode, the guard around the import is the `@mermaid-js/tiny` build marker, and there is no hook to hand it a renderer. The import only *runs* for a label that matches Mermaid's `$$` test, but it is *emitted* regardless, so a host that registered a loader and aliased the default still built a `katex-*.js` chunk, and because that chunk then had two dynamic importers (the host's loader module and the Mermaid runtime) Rollup kept it separate from the host's loader chunk: a math document fetched two files, the 57-byte loader chunk plus the shared 261 KB KaTeX chunk. Measured on the scratch Vite 6 consumer of this checkout (loader registered, default aliased, a document with inline math, a display block and a flowchart with a `$$` label): one chunk carried the KaTeX body before, `katex-*.js`, imported by `mermaid.core-*.js` and by the host's loader chunk; 367 JS chunks in all. + **Mermaid's own KaTeX, and the last shared chunk (0.34.0, from 0.33.0 adoption feedback).** The alias above is not the whole story once a page can render a Mermaid diagram. The Mermaid runtime (11.15.0 when this was written; 12.0.0 since 0.40.0, same import) typesets `$$...$$` labels through its own `import("katex")`, inside `renderKatexUnsanitized`, and it offers nothing to turn that off: `legacyMathML` / `forceLegacyMathML` only choose the output mode, the guard around the import is the `@mermaid-js/tiny` build marker, and there is no hook to hand it a renderer. The import only *runs* for a label that matches Mermaid's `$$` test, but it is *emitted* regardless, so a host that registered a loader and aliased the default still built a `katex-*.js` chunk, and because that chunk then had two dynamic importers (the host's loader module and the Mermaid runtime) Rollup kept it separate from the host's loader chunk: a math document fetched two files, the 57-byte loader chunk plus the shared 261 KB KaTeX chunk. Measured on the scratch Vite 6 consumer of this checkout (loader registered, default aliased, a document with inline math, a display block and a flowchart with a `$$` label): one chunk carried the KaTeX body before, `katex-*.js`, imported by `mermaid.core-*.js` and by the host's loader chunk; 367 JS chunks in all. The fix is a bundler-facing redirect to a package module, `utils/mermaid-math-slot`, whose default export has the one method Mermaid calls (`renderToString`) and delegates to whatever fills the math slot, with Mermaid's own options (`throwOnError: true`, `displayMode: true`, the MathML `output` mode) passed through untouched, so a KaTeX renderer produces exactly the markup Mermaid produced from its direct import. Redirect the `katex` specifier for importers inside the `mermaid` package ONLY; a plain `resolve.alias` on `katex` would also rewrite your own loader's import and break math everywhere: @@ -505,7 +505,7 @@ Four modules that used to ride every document read for a host that bundles by ro // plugins: [mermaidKatexToSlot, react(), ...] ``` - Two details of that snippet are layout-proofing. The importer test is `node_modules/mermaid/` anywhere in the path, not a pattern for one install layout: a hoisted install puts the runtime at `node_modules/mermaid/`, Bun's isolated layout at `node_modules/.bun/mermaid@11.15.0/node_modules/mermaid/`, and pnpm's at `node_modules/.pnpm/mermaid@11.15.0/node_modules/mermaid/`; every one of them ends in that segment, and the trailing separator keeps `mermaid-something` packages out. The slot module is resolved from the host's own config file (`configFile`), not from the Mermaid importer: resolving from the importer walks up from Mermaid's location, which finds `@plannotator/ui` on hoisted and Bun-isolated installs but not under pnpm's strict `node_modules`, where the package is only visible from the host root. Resolving from the config file is the same lookup the host's own imports use; passing an absolute path to the file (`path.resolve(...)` of the installed `utils/mermaid-math-slot.ts`) works too. + Two details of that snippet are layout-proofing. The importer test is `node_modules/mermaid/` anywhere in the path, not a pattern for one install layout: a hoisted install puts the runtime at `node_modules/mermaid/`, Bun's isolated layout at `node_modules/.bun/mermaid@12.0.0/node_modules/mermaid/`, and pnpm's at `node_modules/.pnpm/mermaid@12.0.0/node_modules/mermaid/`; every one of them ends in that segment, and the trailing separator keeps `mermaid-something` packages out. The slot module is resolved from the host's own config file (`configFile`), not from the Mermaid importer: resolving from the importer walks up from Mermaid's location, which finds `@plannotator/ui` on hoisted and Bun-isolated installs but not under pnpm's strict `node_modules`, where the package is only visible from the host root. Resolving from the config file is the same lookup the host's own imports use; passing an absolute path to the file (`path.resolve(...)` of the installed `utils/mermaid-math-slot.ts`) works too. With the redirect the same consumer build emits one chunk carrying the KaTeX body, the host's own loader chunk (`host-katex-*.js`, 261 KB, reached only by the entry's `import()`), `mermaid.core-*.js` has no KaTeX import left, and the chunk count drops to 366: one KaTeX chunk, owned by the host, one file fetched. The slot must be filled by the time Mermaid asks, so `MermaidBlock` awaits `loadMathRenderer()` before rendering a diagram whose source carries a `$$` label (`hasMermaidMath`, Mermaid's own regex); on a filled slot that resolves at once, on the lazy path it runs your loader, and if that load fails the label throws a message naming the cause (`MERMAID_MATH_SLOT_EMPTY_MESSAGE`) which the block's error panel shows with the source. Do not import the module yourself; it exists to be resolved to. Plannotator does not redirect: its Mermaid keeps its direct KaTeX, inlined by the single-file builds with everything else, and the pre-render wait is a resolved promise there because `math-eager` filled the slot at startup. No test in the repo renders a real Mermaid diagram with a math label (Mermaid does not render under happy-dom), and nothing in Plannotator's own documents exercises `$$` labels; the bridge is pinned by `utils/mermaid-math-slot.test.ts` (delegation with Mermaid's exact options, KaTeX parity, the empty-slot error, the label regex) and the pre-render warm by the "Mermaid math labels warm the math slot" cases in `components/DiagramBlock.lazyRetry.test.tsx`. @@ -677,7 +677,7 @@ Additive only, but required: `@plannotator/ui` 0.32.0 imports the new `@plannota --- -## Theme-aware Mermaid diagrams (next publish) +## Theme-aware Mermaid diagrams (0.40.0) Mermaid diagrams used to render from one static config in every palette and both modes: `MERMAID_CONFIG` pinned Mermaid's `dark` base theme plus a slate `themeVariables` palette, so a diagram was blue-on-slate under GitHub Light and Catppuccin alike. Diagrams now follow the active colour theme and mode the way code fences already do (`resolveFenceTheme` / `useFenceTheme`), through ONE dynamic mapping rather than per-palette themes. @@ -689,9 +689,9 @@ Mermaid diagrams used to render from one static config in every palette and both **Fallback contract for hosts.** Nothing changes for a host that does not use the tokens: with no `--background`/`--foreground` on the document (no `ThemeProvider`, no `theme.css`), `readThemeTokens` returns `undefined`, `buildMermaidThemeVariables` returns `null`, `buildMermaidConfig(null)` is `MERMAID_CONFIG` itself, and `applyMermaidTheme` records the key without calling `initialize` at all, so the runtime keeps the static config the loader or the eager entry initialized it with and renders byte-identically to 0.39.0 (pinned by `utils/mermaidTheme.test.ts` and `components/MermaidBlock.theme.test.tsx`). Outside a `ThemeProvider`, `useTheme()` yields the default context (Plannotator dark), which only names the key. `MERMAID_CONFIG` keeps its value and meaning (`securityLevel: 'strict'` still pinned by `components/MermaidBlock.test.ts`; `flowchart.htmlLabels` and `curve` are carried into the dynamic config unchanged) and `loadMermaidRuntime` / the eager entry are untouched: the runtime is still initialized once at registration, and the theme apply is a second, cached `initialize` on top. A host that ships its own tokens under the same names gets themed diagrams for free; a host that wants the old slate look in a themed document can keep the tokens off the diagram's ancestors, since `readThemeTokens` reads the document element by default. New exports are additive; the only behaviour change is for documents that carry the tokens, where diagrams now follow them. -**Known limits.** `GraphvizBlock` already maps its output to `var(--foreground)` / `var(--muted-foreground)` / `var(--muted)` and needs nothing. Mermaid hardcodes a `#000000` stroke on the sequence `crosshead` marker (lost messages, `-x`), which no theme variable reaches; it stays as in every Mermaid theme. A Mermaid 12 upgrade is a separate follow-up that reuses this mapping unchanged. +**Known limits.** `GraphvizBlock` already maps its output to `var(--foreground)` / `var(--muted-foreground)` / `var(--muted)` and needs nothing. Mermaid hardcodes a `#000000` stroke on the sequence `crosshead` marker (lost messages, `-x`), which no theme variable reaches; it stays as in every Mermaid theme. The Mermaid 12 upgrade shipped in the same 0.40.0 publish and reuses this mapping unchanged; the re-sweep on 12 is in "Mermaid 12 (0.40.0)". -## Element context through the host seam (next publish) +## Element context through the host seam (0.40.0) `@plannotator/ui` 0.39.0 captured **element context** on raw-HTML and live-app pinpoints but deliberately left `@plannotator/core/html-anchor` untouched, so a host persisting through `buildPersistedHtmlAnchor` and reading through `projectHostThreads` dropped the field on save and never got it back on projection. That gap (#1521) is closed: the field now survives the whole host round trip. @@ -715,13 +715,61 @@ Behavior is pinned by `../core/html-anchor.test.ts` (the wire fingerprint of a c --- +## Mermaid 12 (0.40.0) + +`@plannotator/ui` 0.40.0 moves the diagram runtime from `mermaid` `^11.17.2` to an exact `mermaid` `12.0.0` and, in the same release, stops registering it eagerly in Plannotator's own plan editor. The theme mapping from 0.39.x (see "Theme-aware Mermaid diagrams") is unchanged and was re-swept on 12: all 52 palettes in `packages/ui/themes` in both modes (104 combinations, 15 diagrams each), 17,160 text-on-fill pairs and 9,880 line-on-canvas pairs measured from the rendered SVG in Chromium, **0 failures** against the 4.5:1 / 3:1 rule and 0 render errors. This section is the contract a host needs to adopt 0.40.0; the publish order is at the end. + +**What Mermaid 12 changes, and what we took.** We take 12's defaults rather than pinning the 11 ones (owner ruling). Concretely: + +- **ELK is the default layout** for flowchart (`graph`, `flowchart`), state, class, ER and requirement diagrams: orthogonal right-angle edge routing, tighter node boxes with more label wrapping, different subgraph packing, and a different rendered `viewBox` for the same source. ELK is now part of `mermaid` itself (`elkjs` is a dependency; the separate `@mermaid-js/layout-elk` package is gone) and is loaded by Mermaid's own internal `import()` on the first ELK layout, so in a chunked host build it is a separate `elk-*.js` chunk (1,435 KB, 438 KB gzip) whether or not you import `mermaid-eager`. `flowchart-elk` as a diagram id still parses and renders (`aria-roledescription="flowchart-elk"`). The `defaultRenderer` option under `flowchart` / `class` / `state` config is removed upstream; use the top-level `layout` option if you need dagre back (`mermaid.initialize({ ..., layout: 'dagre' })` on the runtime after ours; `MERMAID_CONFIG` does not set `layout`). +- **Per-diagram default theme/look** (`redux-color` theme and the `neo` look for flowchart, class, state, ER, requirement, sequence, use case, swimlane, Venn and agentflow) does not reach a Plannotator-themed document: `applyMermaidTheme` passes an explicit base theme (`dark` / `default`) and a complete `themeVariables` set, and `MERMAID_CONFIG` passes `theme: 'dark'`, so the rendered look is the classic one in the 0.39 sweep and in the 0.40 sweep alike. A host that renders with no theme tokens keeps `MERMAID_CONFIG` (still `theme: 'dark'`, still `securityLevel: 'strict'`, pinned by `components/MermaidBlock.test.ts`). +- **Legacy diagram ids** `flowchart`, `class`, `state` are gone from `detectType`; `flowchart-v2`, `classDiagram`, `stateDiagram` are what 11 already returned for the same sources, and every `aria-roledescription` in the sweep is identical 11 → 12. +- **Removed public exports** (`clearLayoutRenderState`, `createCommonLayoutRenderer`, `defaultMeasureLayout`, `paintLayoutData`, the `CommonLayout*` types) are internal layout helpers that nothing in `@plannotator/ui` used or re-exported. +- **Browser floor: Safari 17.4+ and ES2024.** Mermaid 12 is built to that target ("Mermaid is now built to target Safari 17.4+ and ES2024"; Node 22.12+ for anything that imports it server-side, e.g. a test harness). A host that must render diagrams on an older Safari stays on ui 0.39.x. Nothing else in `@plannotator/ui` moved its floor. + +**SVG ids are byte-identical 11 → 12** (measured from the real rendered SVG in the plan editor across 15 diagrams in 7 families, all 12 ids per diagram matched). `MermaidBlock` renders with `mermaid.render("mermaid-" + block.id, source)`, so every id below is prefixed by that render id (`{renderId}`), and a host that anchors on these keeps working: + +| Family | Anchor | Pattern (11 and 12) | +|---|---|---| +| flowchart / `graph` / `flowchart-elk` | node | `` | +| | edge | `` | +| | subgraph | `` under `g.clusters` | +| | markers | `` | +| state (`stateDiagram-v2`) | state | `{renderId}-state-{stateId}-{n}` (`.node.default.statediagram-state`); pseudo-states `{renderId}-state-{scope}_start-{n}` / `_end-{n}`; composite cluster `{renderId}-state-{stateId}-{n}` (`.statediagram-cluster`) | +| | transition | `` (note `edge{n}`, not `L_a_b_0`) | +| class | class box | `{renderId}-classId-{ClassName}-{n}` | +| | relation | ``; markers `{renderId}_classDiagram-{kind}[-margin]` | +| ER | entity | `{renderId}-entity-{ENTITY}-{n}` | +| | relationship | ``; attribute cells are class-only (`.attribute-type`, `.attribute-name`, `.attribute-keys`, `.row-rect-odd/even`) | +| requirement | node / relation | `{renderId}-{reqId}` / `` | +| sequence | lifeline / root | `line#actor{n}`, `g#root-{n}` — **global, un-prefixed**; messages, notes, activations and loops carry classes only (`.messageLine0/1`, `.messageText`, `.sequenceNumber`, `.note`, `.activation0`, `.loopLine`) and no ids | +| gitGraph / pie | — | no ids at all (`.commit`, `.commit{n}`, `.branch{n}`, `.pieCircle`, `.slice`); the per-commit hash class on gitGraph circles is derived from generated commit ids and was never stable | +| every family | gradient | one `` | + +Three class-level deltas, none of which breaks an id- or class-based selector: `g.edgePaths` gains a second class (``, additive, same element); empty `g.edgeLabel > g.label > div.labelBkg` placeholder groups are no longer emitted for edges without a label (a host that assumed one `.edgeLabel` per edge must count labeled edges only); and the gitGraph auto-hash class differs, as it always could. + +**One DOM-order change a host must know about.** Children of `g.edgePaths` are now in **declaration order**. Under 11/dagre the eval's state diagram emitted `edge0, edge1, edge5, edge6, edge10, edge2, edge3, edge4`; under 12/ELK it emits `edge0 … edge10` in source order. Anything that indexes edges by DOM position (`edgePaths.children[i]`, `:nth-child`, walking siblings to pair an edge with a label) breaks; anything that selects by id (`{renderId}-L_{from}_{to}_{n}`, `{renderId}-edge{n}`, `{renderId}-id_{From}_{To}_{n}`) does not. If you need a stable order, sort by id or by the numeric suffix, never by position. + +**Lazy-load contract.** Since 0.32.0 `utils/mermaid` has loaded the runtime through a slot: filled, it resolves at once; empty, `loadMermaidRuntime()` runs `import('mermaid')` on the first diagram, memoized, with a rejected load dropped so the block's automatic re-attempt and its Retry issue a fresh import (`utils/mermaid.test.ts`, `components/DiagramBlock.lazyRetry.test.tsx`). Through 0.39.0 Plannotator's plan editor filled the slot eagerly by importing `utils/mermaid-eager`, so a host copying its entry got the same. **0.40.0 removes that import**: with 12's runtime ~1.8 MB larger, a plan with no diagram must not pay for it, so Plannotator itself now takes the lazy path, and `tests/entry-assets.test.ts` fails if the eager import comes back into either app. What that means for a host: + +- Nothing to change to get the lazy behaviour: it is the default of the package and always was. The first diagram on a page fetches `mermaid.core-*.js` (625 KB, 148 KB gzip on this checkout's portal build) and, for an ELK family, Mermaid then fetches `elk-*.js` (1,435 KB, 438 KB gzip); until the SVG lands the block shows the source fence under a quiet `role="status"` line ("Rendering diagram…", `data-mermaid-pending`), never the error panel, and the panel with the source plus Retry appears only for a failure (pinned by the "Mermaid pending state" case in `DiagramBlock.lazyRetry.test.tsx`). +- To keep the 0.39 behaviour — runtime registered and initialized at startup, in your entry chunk, unable to fail separately from the app — add the one line Plannotator used to have, before the first render: `import '@plannotator/ui/utils/mermaid-eager';`. It statically imports `mermaid`, runs `mermaid.initialize(MERMAID_CONFIG)` at module evaluation and fills the slot (`setMermaidRuntime(mermaid, 'plannotator-mermaid-eager')`); the ELK chunk is still Mermaid's own lazy import and is not hoisted by this. +- Own loader: `setMermaidRuntime(runtime, 'host')` after your own `import('mermaid')` + `initialize(MERMAID_CONFIG)` fills the slot the same way; there is no `mermaidRuntimeLoader` seam on `configurePlannotatorUI` (the test hook `__setMermaidRuntimeLoaderForTests` is not host surface). +- `applyMermaidTheme(mermaid, key)` is keyed on the **runtime object** as well as the `(palette, mode)` key, so a lazily loaded runtime is themed on its first render exactly like an eagerly registered one, and a host that swaps runtimes gets a fresh `initialize` (pinned by `components/MermaidBlock.theme.test.tsx`). +- Single-file builds gain nothing from the lazy import: `inlineDynamicImports` inlines the `import('mermaid')` target (and ELK) into the one HTML file, and the import resolves from the bundle. Plannotator's own hook bundle is 21.74 MB → 23.50 MB (+1.76 MB, +8.1%; gzip 6.66 MB → 7.19 MB); that delta is Mermaid 12's own size, not the loading strategy, and the review bundle is unchanged (17.43 MB, byte-identical to main) because it never carried Mermaid. On the chunked share portal the entry chunk goes 4,688 KB → 4,044 KB (-645 KB, -13.7%) with `mermaid.core` moving to its own chunk. + +**Theming contract, restated for 12.** `MermaidBlock` calls `applyMermaidTheme(runtime, mermaidThemeKey(colorTheme, mode))` before every render. `readThemeTokens()` reads `--background`, `--foreground`, `--card`, `--card-foreground`, `--popover`, `--border`, `--muted`, `--muted-foreground`, `--primary`, `--primary-foreground`, `--secondary`, `--accent`, `--destructive`, `--success`, `--warning` and `--font-sans` off the document element (resolving `color-mix()` / `var()` chains through a probe element); `buildMermaidThemeVariables(tokens, mode)` turns them into the base theme (`dark` / `default`) plus a complete `themeVariables` set for every family (node fills ← `card`, text ← `foreground` / `card-foreground`, borders ← `border`, edges and arrowheads ← `muted-foreground`, clusters ← `muted`, twelve categorical fills seeded from `primary`, `accent`, `success`, `warning`, `destructive`, `secondary`; every colour opaque hex, every text-on-fill pair guarded to 4.5:1 and every line 3:1 against every surface it can cross), and `mermaid.initialize(buildMermaidConfig(spec))` runs once per key change. **Without tokens** (no `ThemeProvider`, no `theme.css`, or the tokens kept off the document element) `readThemeTokens` returns `undefined`, `buildMermaidThemeVariables` returns `null`, nothing is re-initialized, and the runtime keeps the static `MERMAID_CONFIG` it was initialized with (12's own `dark` base theme with the slate `themeVariables`, ELK layout). None of the variable names changed between 11 and 12; the 0.40 sweep (104 combinations, 17,160 text pairs, 9,880 line pairs, 0 failures) is the proof that the mapping holds under ELK's re-laid-out geometry. + +**Publish order.** `@plannotator/ui` 0.40.0 pins `@plannotator/core` `0.25.3` exactly, so **publish `core` 0.25.3 first, then `ui` 0.40.0** (`npm publish` in `packages/core`, then in `packages/ui`; both by hand from `main` after merge — CI never publishes these packages). Nothing under `packages/core` changed for Mermaid 12 itself. Core 0.25.3 also carries #1549 (`parseHtmlElementContext`, `MAX_ELEMENT_CONTEXT_BYTES`, `MAX_PAGE_URL_LENGTH` on `@plannotator/core/html-anchor`, and the element-context round trip; see "Element context through the host seam"), which is why the ui pin moves: a ui 0.40.0 on a published core 0.25.2 would fail to compile in a consumer. + +--- + ## Publishing & versioning -- **Unreleased (next `@plannotator/ui` publish, minor bump):** theme-aware Mermaid diagrams. New additive exports `utils/mermaidTheme` (`buildMermaidThemeVariables`, `readThemeTokens`, `applyMermaidTheme`, `mermaidThemeKey`, `buildMermaidConfig`, `ensureContrast`, `isDarkBackground`, `MERMAID_THEME_TOKEN_NAMES`) and `utils/cssColor` (parser + OKLab/contrast toolkit). `MermaidBlock` now calls `useTheme()` and `applyMermaidTheme` before each render; `MERMAID_CONFIG`, `loadMermaidRuntime`, `mermaid-eager` and `securityLevel: 'strict'` are unchanged. A host whose document carries no theme tokens renders diagrams byte-identically to 0.39.0; a host that mounts `ThemeProvider` with `theme.css` gets diagrams in its palette and mode with no configuration. No new peer dependencies; core unchanged. See "Theme-aware Mermaid diagrams (next publish)". -- **Unreleased (same publish, and it moves `core`): element context through the host seam (#1521).** `@plannotator/core/html-anchor` gains `parseHtmlElementContext`, `MAX_ELEMENT_CONTEXT_BYTES` and `MAX_PAGE_URL_LENGTH`; `PersistedHtmlAnchor.elementContext?` and `HtmlAnnotationTarget.context?` now round-trip through `buildPersistedHtmlAnchor` and `projectHostThreads`. **Core changes here, so bump and publish `core` first** and update UI's exact core dependency before packing ui — a ui build that imports these from an older published core fails to compile in a consumer, the 0.38.0 failure mode. `@plannotator/ui/components/html-viewer` re-exports the validator, so 0.39.0's import site is unchanged, and rows without context stay byte-identical on the wire. `utils/parser` gains `includeOutline` on `elementContextExportBlock` / `exportAnnotationEntry`, and `exportAnnotationEntry`'s `includeRoute` now defaults to true per field. See "Element context through the host seam". -- The current pair is `@plannotator/ui` `0.39.0` on `@plannotator/core` `0.25.2` (core unchanged; nothing under `packages/core` moved). UI 0.39.0 adds **element context** to raw-HTML and live-app pinpoint annotations (#1517, #1520): a new optional `Annotation.elementContext` (`HtmlElementContext` in `@plannotator/ui/types`) and `HtmlAnnotationTarget.context`, captured by the bridge at click time (tag, id, author classes, ancestor `path`, `role`, accessible `name`, an allowlisted `attrs` set with href/src scrubbed of query and fragment, rendered `text`, an adaptive collapsed HTML `outline`, child count, viewport `rect`, nearest `landmark` and `heading`, a `component` hint, and in live-app sessions `page`), hard-capped at 2 KiB serialized per primary and 1 KiB per extra target, and re-validated at the parent trust boundary by the new `parseHtmlElementContext` export of `@plannotator/ui/components/html-viewer`. New helpers on `@plannotator/ui/utils/parser`: `elementContextExportBlock(ann, { includeRoute })` (the fenced skeleton plus selector/path/role/name/attrs/text/box/near lines the full export now prints under a context-bearing comment) and `exportAnnotationEntry(ann, { includeRoute })` (one annotation as a standalone feedback entry, a pure helper for hosts; `AnnotationPanel`'s card chrome is unchanged from 0.38.2). The field is purely descriptive: `HtmlElementAnchor` and restore are untouched, no `BRIDGE_PROTOCOL_VERSION` bump, share links drop it like anchors, annotations without it export byte-identically, and the repaint path posts only anchors to the bridge. **Host persistence gap in 0.39.0 itself, closed in the next publish (#1521, #1549)**: as shipped, 0.39.0's `@plannotator/core/html-anchor` (`buildPersistedHtmlAnchor`, `projectHostThreads`) does not carry `elementContext`, so a host pinned to 0.39.0 that persists through those helpers drops it on save and must persist and project the field itself. The next publish carries it end to end — see "Element context through the host seam". Peer ranges are unchanged from 0.38.2: `react` / `react-dom` `^19.2.3`, `tailwindcss` as before, and `@codemirror/state ^6.7.2` beside `@codemirror/view ^6.43.10`. Decision-control change in the same window (#1516): the header primary reads `Send Feedback` / `Post Comments` with no inline count (`DecisionPrimary.count` removed; internal, not host-supported surface). -- The previous pair was `@plannotator/ui` `0.38.2` on `@plannotator/core` `0.25.2`. UI 0.38.2 keeps the type word in a titled alert's accessible name through a visually hidden `sr-only` span before the title instead of an `aria-label` on the title row (naming a generic `div` is prohibited by ARIA and WebKit drops it, so VoiceOver on Safari read only the bold title in 0.38.1), and loosens the React peer back to `^19.2.3` (0.38.1 declared `^19.2.8` only because the dependency batch moved it; nothing in the package needs a newer API). **Do not consume ui 0.38.0**: it imports `@plannotator/core/token-hover` (the hover-card trigger settings, #1462) but pins core 0.25.1, which never exported that subpath, so it fails to compile in any consumer; 0.38.1 is the same UI pinning core 0.25.2, which publishes `./token-hover`, the rotated `guide-viewer-manifest` pin, and the `config-types` hover fields (core 0.25.2 is the first core publish since 0.25.1 even though those changes landed over several releases; the package smoke now diffs the UI's core imports against the registry so an unpublished core subpath fails preflight instead of the consumer). UI 0.38.1 also aligns `@codemirror/state` to `^6.7.2` beside `@codemirror/view ^6.43.10`, so a consumer can no longer resolve two state copies. UI 0.38.0 also renders a GitHub alert's bold-only first body line as its title on the icon row (an emoji on that line becomes the icon; `` is stripped and resolved through the new `alertIconRenderer` seam, null by default; grammar in `utils/alertTitle`, importable by a host editor so it writes the bytes the reader parses; a fenced code block inside an alert body still renders as text, deferred because nesting a `CodeBlock` inside a block interacts with the positional annotation anchors and needs its own design). UI 0.38.0 carries the whole unified decision-control stack: the internal primitives (`DecisionControl`, `utils/decisionSpec`, `hooks/useDismissablePopover` — not host-supported surface, see the unsupported list; `useDismissablePopover` also replaced the hand-rolled dismissal inside `ActionMenu`/`ApproveDropdown`, both likewise unsupported) plus one blessed-barrel addition: `decisionControlShortcuts` on `@plannotator/ui/shortcuts` (pure scope data, fetch-free, same contract as the other scopes). The removal of `ToolbarButtons`' platform-mode `muted` prop is internal — `ToolbarButtons` is not host-supported surface. UI 0.37.0 added the Viewer-owned document-header seam (a new public API, hence the minor bump; 0.36.1 was reserved for it but never published) while retaining the `hideQuickLabel` and `StickyHeaderLane` seams from the 0.35.x and 0.36.0 releases; core 0.25.1 publishes the `annotation-threads` subpath already used by `AnnotationPanel` and `utils/parser`, and UI pins that corrected core exactly. -- Recent pairs, for the consumer's install matrix: ui 0.32.0 on core 0.25.0 (lockstep, `html-anchor`), ui 0.33.0 and ui 0.34.0 on core 0.25.0 (ui only), and ui 0.35.2, ui 0.36.0, and ui 0.37.0 on core 0.25.1 (0.36.1 was never published), and ui 0.38.1, ui 0.38.2, and ui 0.39.0 on core 0.25.2. Do not consume ui 0.35.0 externally because its published manifest contains `workspace:*`; do not consume ui 0.35.1 because its exact core 0.25.0 dependency lacks the `annotation-threads` export. Do not consume ui 0.38.0 because its exact core 0.25.1 dependency lacks the `token-hover` export. +- **The current pair is `@plannotator/ui` `0.40.0` on `@plannotator/core` `0.25.3`. Publish `core` 0.25.3 first, then `ui` 0.40.0** (both by hand from `main` after merge; CI never publishes these packages). Three things ship in 0.40.0: (1) **Mermaid 12.0.0**, pinned exactly (was `^11.17.2`): ELK layout by default for flowchart/state/class/ER/requirement, Safari 17.4+ / ES2024 floor, SVG ids byte-identical to 11 but `g.edgePaths` children now in declaration order, and the plan editor no longer imports `utils/mermaid-eager` (the lazy path is the default for everyone; hosts that want startup registration import the eager entry themselves) — see "Mermaid 12 (0.40.0)"; (2) **theme-aware Mermaid diagrams**: New additive exports `utils/mermaidTheme` (`buildMermaidThemeVariables`, `readThemeTokens`, `applyMermaidTheme`, `mermaidThemeKey`, `buildMermaidConfig`, `ensureContrast`, `isDarkBackground`, `MERMAID_THEME_TOKEN_NAMES`) and `utils/cssColor` (parser + OKLab/contrast toolkit). `MermaidBlock` now calls `useTheme()` and `applyMermaidTheme` before each render; `MERMAID_CONFIG`, `loadMermaidRuntime`, `mermaid-eager` and `securityLevel: 'strict'` are unchanged. A host whose document carries no theme tokens renders diagrams byte-identically to 0.39.0; a host that mounts `ThemeProvider` with `theme.css` gets diagrams in its palette and mode with no configuration. No new peer dependencies; core unchanged. See "Theme-aware Mermaid diagrams (0.40.0)".; (3) **element context through the host seam (#1521, #1549), which is what moves `core` to 0.25.3:** `@plannotator/core/html-anchor` gains `parseHtmlElementContext`, `MAX_ELEMENT_CONTEXT_BYTES` and `MAX_PAGE_URL_LENGTH`; `PersistedHtmlAnchor.elementContext?` and `HtmlAnnotationTarget.context?` now round-trip through `buildPersistedHtmlAnchor` and `projectHostThreads`. **Core changes here, so bump and publish `core` first** and update UI's exact core dependency before packing ui — a ui build that imports these from an older published core fails to compile in a consumer, the 0.38.0 failure mode. `@plannotator/ui/components/html-viewer` re-exports the validator, so 0.39.0's import site is unchanged, and rows without context stay byte-identical on the wire. `utils/parser` gains `includeOutline` on `elementContextExportBlock` / `exportAnnotationEntry`, and `exportAnnotationEntry`'s `includeRoute` now defaults to true per field. See "Element context through the host seam". +- The previous pair was `@plannotator/ui` `0.39.0` on `@plannotator/core` `0.25.2` (core unchanged; nothing under `packages/core` moved). UI 0.39.0 adds **element context** to raw-HTML and live-app pinpoint annotations (#1517, #1520): a new optional `Annotation.elementContext` (`HtmlElementContext` in `@plannotator/ui/types`) and `HtmlAnnotationTarget.context`, captured by the bridge at click time (tag, id, author classes, ancestor `path`, `role`, accessible `name`, an allowlisted `attrs` set with href/src scrubbed of query and fragment, rendered `text`, an adaptive collapsed HTML `outline`, child count, viewport `rect`, nearest `landmark` and `heading`, a `component` hint, and in live-app sessions `page`), hard-capped at 2 KiB serialized per primary and 1 KiB per extra target, and re-validated at the parent trust boundary by the new `parseHtmlElementContext` export of `@plannotator/ui/components/html-viewer`. New helpers on `@plannotator/ui/utils/parser`: `elementContextExportBlock(ann, { includeRoute })` (the fenced skeleton plus selector/path/role/name/attrs/text/box/near lines the full export now prints under a context-bearing comment) and `exportAnnotationEntry(ann, { includeRoute })` (one annotation as a standalone feedback entry, a pure helper for hosts; `AnnotationPanel`'s card chrome is unchanged from 0.38.2). The field is purely descriptive: `HtmlElementAnchor` and restore are untouched, no `BRIDGE_PROTOCOL_VERSION` bump, share links drop it like anchors, annotations without it export byte-identically, and the repaint path posts only anchors to the bridge. **Host persistence gap in 0.39.0 itself, closed in the next publish (#1521, #1549)**: as shipped, 0.39.0's `@plannotator/core/html-anchor` (`buildPersistedHtmlAnchor`, `projectHostThreads`) does not carry `elementContext`, so a host pinned to 0.39.0 that persists through those helpers drops it on save and must persist and project the field itself. The next publish carries it end to end — see "Element context through the host seam". Peer ranges are unchanged from 0.38.2: `react` / `react-dom` `^19.2.3`, `tailwindcss` as before, and `@codemirror/state ^6.7.2` beside `@codemirror/view ^6.43.10`. Decision-control change in the same window (#1516): the header primary reads `Send Feedback` / `Post Comments` with no inline count (`DecisionPrimary.count` removed; internal, not host-supported surface). +- Before that, `@plannotator/ui` `0.38.2` on `@plannotator/core` `0.25.2`. UI 0.38.2 keeps the type word in a titled alert's accessible name through a visually hidden `sr-only` span before the title instead of an `aria-label` on the title row (naming a generic `div` is prohibited by ARIA and WebKit drops it, so VoiceOver on Safari read only the bold title in 0.38.1), and loosens the React peer back to `^19.2.3` (0.38.1 declared `^19.2.8` only because the dependency batch moved it; nothing in the package needs a newer API). **Do not consume ui 0.38.0**: it imports `@plannotator/core/token-hover` (the hover-card trigger settings, #1462) but pins core 0.25.1, which never exported that subpath, so it fails to compile in any consumer; 0.38.1 is the same UI pinning core 0.25.2, which publishes `./token-hover`, the rotated `guide-viewer-manifest` pin, and the `config-types` hover fields (core 0.25.2 is the first core publish since 0.25.1 even though those changes landed over several releases; the package smoke now diffs the UI's core imports against the registry so an unpublished core subpath fails preflight instead of the consumer). UI 0.38.1 also aligns `@codemirror/state` to `^6.7.2` beside `@codemirror/view ^6.43.10`, so a consumer can no longer resolve two state copies. UI 0.38.0 also renders a GitHub alert's bold-only first body line as its title on the icon row (an emoji on that line becomes the icon; `` is stripped and resolved through the new `alertIconRenderer` seam, null by default; grammar in `utils/alertTitle`, importable by a host editor so it writes the bytes the reader parses; a fenced code block inside an alert body still renders as text, deferred because nesting a `CodeBlock` inside a block interacts with the positional annotation anchors and needs its own design). UI 0.38.0 carries the whole unified decision-control stack: the internal primitives (`DecisionControl`, `utils/decisionSpec`, `hooks/useDismissablePopover` — not host-supported surface, see the unsupported list; `useDismissablePopover` also replaced the hand-rolled dismissal inside `ActionMenu`/`ApproveDropdown`, both likewise unsupported) plus one blessed-barrel addition: `decisionControlShortcuts` on `@plannotator/ui/shortcuts` (pure scope data, fetch-free, same contract as the other scopes). The removal of `ToolbarButtons`' platform-mode `muted` prop is internal — `ToolbarButtons` is not host-supported surface. UI 0.37.0 added the Viewer-owned document-header seam (a new public API, hence the minor bump; 0.36.1 was reserved for it but never published) while retaining the `hideQuickLabel` and `StickyHeaderLane` seams from the 0.35.x and 0.36.0 releases; core 0.25.1 publishes the `annotation-threads` subpath already used by `AnnotationPanel` and `utils/parser`, and UI pins that corrected core exactly. +- Recent pairs, for the consumer's install matrix: ui 0.32.0 on core 0.25.0 (lockstep, `html-anchor`), ui 0.33.0 and ui 0.34.0 on core 0.25.0 (ui only), and ui 0.35.2, ui 0.36.0, and ui 0.37.0 on core 0.25.1 (0.36.1 was never published), ui 0.38.1, ui 0.38.2, and ui 0.39.0 on core 0.25.2, and ui 0.40.0 on core 0.25.3 (lockstep, `html-anchor` element context). Do not consume ui 0.35.0 externally because its published manifest contains `workspace:*`; do not consume ui 0.35.1 because its exact core 0.25.0 dependency lacks the `annotation-threads` export. Do not consume ui 0.38.0 because its exact core 0.25.1 dependency lacks the `token-hover` export. - When both packages change, **publish `core` first**: ui 0.32.0 imports the `@plannotator/core/html-anchor` subpath, which no earlier published core (0.24.0 and before) has, just as ui 0.29.0 needed core 0.23.0 for `@plannotator/core/annotatable`. Bump core, update UI's exact core dependency to the same new version, and run `bun install` so `bun.lock` records the new workspace versions before packing either package. - The HTML annotation seams also changed the guides.show viewer **stylesheet** (five utility rules from `HtmlSurfaceControls`; the viewer JS is unchanged), so `packages/core/guide-viewer-manifest.ts` now pins a CSS hash that exists on guides.show only after the deploy workflow has published this build's `/v1/` assets. A guide exported from this build before that deploy would pin a stylesheet the host does not serve yet: **deploy guides.show before any release that ships this manifest.** - UI declares the already published core version exactly in its source manifest. Do not replace it with `workspace:*`: direct publication can preserve that protocol and make the package impossible to install outside this repository. Bun links the local core workspace whenever its version matches the exact dependency. Before publishing, run `bun run --cwd packages/ui smoke:package`; it checks the source and packed manifests, required tarball subpaths, local Bun linking, and a real pnpm install in an external temporary consumer. When both packages change, publish **`core` first, then `ui`**. diff --git a/packages/ui/README.md b/packages/ui/README.md index b446f5838..72572710d 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -56,10 +56,10 @@ Building your own tooltip and removing the built-in double-click reset are host- The Mermaid runtime, the Graphviz engine, KaTeX and the username dictionary are off the static import graph of `Viewer`, so a host that bundles by route does not download them for a plain markdown read. Graphviz needs nothing from you (the block imports the engine inside its render effect and shows the source fence until the SVG lands, as it always did). Mermaid, KaTeX and the dictionary sit behind synchronous slots: - **Math.** Without registration, a math node renders its TeX as text in the same wrapper (same `data-math-tex` / `data-math-display` / `aria-label` / class names), loads KaTeX via `import('katex')`, and re-renders typeset. To keep math typeset on the very first commit, as Plannotator does, add one line to your entry: `import "@plannotator/ui/utils/math-eager";`. To put KaTeX and its stylesheet on one lazy chunk instead, pass `mathRendererLoader`. The stylesheet remains your job either way (see "Consuming it", step 3). The default `import('katex')` is the only runtime mention of `katex` in the package and lives in `utils/math-default-loader` (0.33.0), called only while no loader is registered; a registered loader is never backfilled by it, though a default load already in flight at registration still fills the slot (pre-existing), so register the loader before the first math render. Chunk emission is static, so a bundler still emits that chunk (never requested) unless you alias the module away; see HANDOFF.md "Lazy renderers and eager entries" for the two-line alias. The Mermaid runtime has its own `import("katex")` for `$$` labels, which leaves a second, shared KaTeX chunk in a host build even with the alias; since 0.34.0 a host redirects that one import (for importers inside the `mermaid` package only) to `@plannotator/ui/utils/mermaid-math-slot`, which typesets the labels through your registered renderer, so one KaTeX chunk remains and it is yours. Recipe and measurement in HANDOFF.md, same section. `resetMathRenderer()` empties the slot only and keeps a registered loader (0.34.0); `setMathRendererLoader(null)` is the explicit way back to the package default. -- **Mermaid.** Without registration, the first diagram on a page fetches the runtime through `import('mermaid')`; a failed import is dropped from the memo, re-attempted once after a short delay, and the error panel (with the source) offers Retry, which issues another fresh attempt. Plannotator keeps Mermaid eager by policy so it can never fail separately from the app: `import "@plannotator/ui/utils/mermaid-eager";` in your entry does the same for your bundle. **Diagrams follow the colour theme.** Before every render `MermaidBlock` calls `applyMermaidTheme` (`utils/mermaidTheme`), which reads the theme tokens off the document (`--background`, `--foreground`, `--card`, `--border`, `--muted`, `--muted-foreground`, `--primary`, the accent tokens and `--font-sans`; `readThemeTokens`), derives a complete `themeVariables` set for every diagram family from them (`buildMermaidThemeVariables(tokens, mode)`, pure; base theme `dark` under a dark resolved mode, `default` under light; every text-on-fill pair guarded to WCAG 4.5:1 and every line 3:1, rule in the module doc) and runs the global `mermaid.initialize` once per `(palette, mode)` key, re-rendering mounted diagrams when the key changes. The key comes from `useTheme()`, so a host that mounts `ThemeProvider` and ships `theme.css` gets diagrams in its palette with nothing to configure. **Fallback contract:** with no tokens on the document (no `ThemeProvider`, no `theme.css`) `readThemeTokens` returns `undefined`, nothing is re-initialized, and the runtime keeps the static `MERMAID_CONFIG` it was initialized with, so such a host renders byte-identically to 0.39.0. `MERMAID_CONFIG` keeps its value and meaning (`securityLevel: 'strict'` pinned); the new exports are additive. Honest limit of any in-page retry: a browser records a failed module fetch in its module map for the page lifetime, so a fresh `import()` of the same chunk URL rejects without a request; the retry recovers failures after the fetch (engine instantiation, initialize) and hosts that version chunk URLs. A host that needs recovery from a failed first fetch uses versioned chunk URLs or a `vite:preloadError` reload at app level. +- **Mermaid.** Without registration, the first diagram on a page fetches the runtime through `import('mermaid')` and the block shows the source fence under a "Rendering diagram" status until the SVG lands; a failed import is dropped from the memo, re-attempted once after a short delay, and the error panel (with the source) offers Retry, which issues another fresh attempt. **Since 0.40.0 (Mermaid 12.0.0, ELK layout by default, Safari 17.4+) this lazy path is Plannotator's own**: the plan editor no longer imports the eager entry, so a document with no diagram never downloads the runtime in a chunked build. `import "@plannotator/ui/utils/mermaid-eager";` in your entry registers and initializes the runtime at startup instead, if you would rather it never fail separately from the app. Mermaid 12 lays flowchart, state, class, ER and requirement diagrams out with ELK (orthogonal edges, different packing); every generated SVG `id` keeps its 11.x shape, but children of `g.edgePaths` are now in declaration order, so select edges by id (`{renderId}-L_{from}_{to}_{n}`, `{renderId}-edge{n}`), never by DOM position. Details in HANDOFF.md "Mermaid 12 (0.40.0)". **Diagrams follow the colour theme.** Before every render `MermaidBlock` calls `applyMermaidTheme` (`utils/mermaidTheme`), which reads the theme tokens off the document (`--background`, `--foreground`, `--card`, `--border`, `--muted`, `--muted-foreground`, `--primary`, the accent tokens and `--font-sans`; `readThemeTokens`), derives a complete `themeVariables` set for every diagram family from them (`buildMermaidThemeVariables(tokens, mode)`, pure; base theme `dark` under a dark resolved mode, `default` under light; every text-on-fill pair guarded to WCAG 4.5:1 and every line 3:1, rule in the module doc) and runs the global `mermaid.initialize` once per `(palette, mode)` key, re-rendering mounted diagrams when the key changes. The key comes from `useTheme()`, so a host that mounts `ThemeProvider` and ships `theme.css` gets diagrams in its palette with nothing to configure. **Fallback contract:** with no tokens on the document (no `ThemeProvider`, no `theme.css`) `readThemeTokens` returns `undefined`, nothing is re-initialized, and the runtime keeps the static `MERMAID_CONFIG` it was initialized with, so such a host renders byte-identically to 0.39.0. `MERMAID_CONFIG` keeps its value and meaning (`securityLevel: 'strict'` pinned); the new exports are additive. Honest limit of any in-page retry: a browser records a failed module fetch in its module map for the page lifetime, so a fresh `import()` of the same chunk URL rejects without a request; the retry recovers failures after the fetch (engine instantiation, initialize) and hosts that version chunk URLs. A host that needs recovery from a failed first fetch uses versioned chunk URLs or a `vite:preloadError` reload at app level. - **Identity.** With an `identityProvider` the generator is never called and the word lists stay out of your bundle. Without one, default names come from a small built-in pool of the same `adjective-noun-tater` shape; `import "@plannotator/ui/utils/identity-tater";` registers the full dictionary, or pass your own `identityGenerator`. -Plannotator's own entries import the eager modules (`math-eager` and `identity-tater` in both `packages/editor/App.tsx` and `packages/review-editor/App.tsx`; `mermaid-eager` in the plan editor only, since the review editor never renders a Mermaid block), which is what keeps its single-file builds byte-identical and its portal entry chunk shaped as before; `tests/entry-assets.test.ts` fails if any of them is dropped. See HANDOFF.md "Lazy renderers and eager entries". +Plannotator's own entries import the eager math and identity modules (`math-eager` and `identity-tater` in both `packages/editor/App.tsx` and `packages/review-editor/App.tsx`), which is what keeps math typeset on the first commit and names minted from the full dictionary; neither app imports `mermaid-eager` since 0.40.0, so the Mermaid runtime rides a lazy chunk on the share portal and is inlined by `inlineDynamicImports` in the single-file builds. `tests/entry-assets.test.ts` fails if an eager math/identity import is dropped or an eager Mermaid import creeps back. See HANDOFF.md "Lazy renderers and eager entries" and "Mermaid 12 (0.40.0)". ### Markdown editor extensions + wiki links (`MarkdownEditor` / `InlineMarkdown`) diff --git a/packages/ui/components/DiagramBlock.lazyRetry.test.tsx b/packages/ui/components/DiagramBlock.lazyRetry.test.tsx index 7900f9070..a616f6f83 100644 --- a/packages/ui/components/DiagramBlock.lazyRetry.test.tsx +++ b/packages/ui/components/DiagramBlock.lazyRetry.test.tsx @@ -102,6 +102,37 @@ const cases = [ }, ]; +/** + * Since Mermaid 12 the plan editor takes the lazy path itself, so the window + * between mount and the first render is now a user-visible state on every + * surface. What regresses: the block flashes the error panel (or nothing) + * while the runtime is still loading, instead of the source under a status. + */ +describe('Mermaid pending state', () => { + test.skipIf(!hasDom)('shows the source under a rendering status until the runtime lands, never the error panel', async () => { + let release: (() => void) | null = null; + __setMermaidRuntimeLoaderForTests( + () => new Promise((resolve) => { release = () => resolve(fakeMermaid); }), + { retryDelayMs: RETRY_DELAY_MS }, + ); + const el = await mount(); + await settle(RETRY_DELAY_MS); + + expect(el.querySelector('[data-mermaid-pending]')).not.toBeNull(); + expect(el.textContent).toContain('Rendering diagram'); + expect(el.textContent).toContain(mermaidBlock.content); + expect(el.textContent).not.toContain('Mermaid Error'); + expect(el.innerHTML).not.toContain('data-sentinel="diagram"'); + + await act(async () => { release!(); }); + await settle(RETRY_DELAY_MS * 3); + + expect(el.querySelector('[data-mermaid-pending]')).toBeNull(); + expect(el.textContent).not.toContain('Rendering diagram'); + expect(el.innerHTML).toContain('data-sentinel="diagram"'); + }); +}); + describe.each(cases)('$name lazy runtime', ({ install, runtime, element, source, errorTitle }) => { test.skipIf(!hasDom)('a runtime that fails once renders the diagram after the automatic re-attempt', async () => { const loader = flakyLoader(runtime, 1); diff --git a/packages/ui/components/MermaidBlock.tsx b/packages/ui/components/MermaidBlock.tsx index baa862b80..9e606e882 100644 --- a/packages/ui/components/MermaidBlock.tsx +++ b/packages/ui/components/MermaidBlock.tsx @@ -22,9 +22,12 @@ const mermaidRetryEpoch = createRuntimeRetryEpoch(); export { MERMAID_CONFIG, __setMermaidRuntimeLoaderForTests }; /** - * The runtime comes from the slot in utils/mermaid: filled eagerly by - * Plannotator (utils/mermaid-eager, imported by the editor App), loaded - * lazily otherwise. See that module for the retry contract. + * The runtime comes from the slot in utils/mermaid: loaded lazily on the + * first diagram (Plannotator's own path since Mermaid 12), or already filled + * by a host that imported utils/mermaid-eager. See that module for the retry + * contract. Until the first render lands the block shows the source fence + * under a "Rendering diagram" status; the error panel appears only for a + * failure, never as a placeholder. */ const getMermaid = loadMermaidRuntime; @@ -600,6 +603,25 @@ const MermaidBlockImpl: React.FC<{ block: Block }> = ({ block }) => { ); + // First render still in flight (the runtime import on the lazy path, then + // the render itself): the source stays readable under a quiet status line. + // A re-render for a theme change keeps the previous SVG, so this shows only + // before the first diagram lands. + const pendingSource = ( + <> +
+
+ {inlineSource} + + ); + const diagramBody = (
= ({ block }) => { <>
{!isExpanded && controls} - {showSource || !svg ? inlineSource : !isExpanded ? diagramBody :
} + {showSource ? inlineSource : !svg ? pendingSource : !isExpanded ? diagramBody :
}
{!showSource && svg && isExpanded && typeof document !== 'undefined' && createPortal( diff --git a/packages/ui/package.json b/packages/ui/package.json index a5df7f678..25d3ab0bc 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@plannotator/ui", - "version": "0.39.0", + "version": "0.40.0", "type": "module", "exports": { "./components/*": "./components/*.tsx", @@ -74,7 +74,7 @@ "@lezer/highlight": "^1.2.3", "@pierre/diffs": "1.3.6", "@plannotator/atomic-editor": "^0.8.0", - "@plannotator/core": "0.25.2", + "@plannotator/core": "0.25.3", "@plannotator/markdown-editor": "^0.4.0", "@plannotator/web-highlighter": "^0.8.1", "@tanstack/react-table": "^8.21.3", @@ -86,7 +86,7 @@ "katex": "^0.16.47", "lucide-react": "^1.38.0", "marked": "^17.0.6", - "mermaid": "^11.17.2", + "mermaid": "12.0.0", "motion": "^12.38.0", "perfect-freehand": "^1.2.2", "tailwind-merge": "^3.6.0", diff --git a/packages/ui/utils/mermaid-eager.ts b/packages/ui/utils/mermaid-eager.ts index e97b3f504..003e9de88 100644 --- a/packages/ui/utils/mermaid-eager.ts +++ b/packages/ui/utils/mermaid-eager.ts @@ -3,23 +3,25 @@ * at module evaluation (exactly where the old module-scope * `mermaid.initialize` ran) and fills the slot in `./mermaid`. * - * `packages/editor/App.tsx` imports this module for its side effect, by - * policy: Plannotator's own surfaces keep Mermaid in their entry chunk so it - * can never fail separately from the app (on the share portal this is what - * keeps `mermaid.core` out of a lazy chunk). The review editor does not import - * it because it never renders a Mermaid block; adding the runtime there would - * grow that bundle. A host that wants the same import adds: + * Plannotator does NOT import this module any more. Through @plannotator/ui + * 0.39.0 `packages/editor/App.tsx` imported it by policy so Mermaid stayed in + * the plan editor's entry chunk; with Mermaid 12 (ELK layout by default) the + * runtime is about 1.8 MB larger and the plan editor loads it on the first + * diagram instead, through the lazy path in `./mermaid`. The module is kept + * for hosts that want the runtime registered and initialized before the + * first render (a host that gates first paint on it, or that would rather + * not have a separate chunk that can fail on its own): * * import '@plannotator/ui/utils/mermaid-eager'; * * A host that does not import it gets the lazy path in `./mermaid`. * * The source tag passed below doubles as a build marker: the literal only - * reaches a bundle when this module is evaluated in it, so a dropped or - * tree-shaken side-effect import is caught by the built-HTML check in - * tests/entry-assets.test.ts (the runtime itself stays inlined in a - * single-file build through the loader's import(), so a Mermaid diagram id - * cannot prove registration). + * reaches a bundle when this module is evaluated in it, which is how + * tests/entry-assets.test.ts proves on the built HTML that neither of + * Plannotator's bundles registers the runtime eagerly (the runtime itself is + * still inlined in a single-file build through the loader's import(), so a + * Mermaid diagram id cannot prove or disprove registration). */ import mermaid from 'mermaid'; import { MERMAID_CONFIG, setMermaidRuntime } from './mermaid'; diff --git a/packages/ui/utils/mermaid-math-slot.test.ts b/packages/ui/utils/mermaid-math-slot.test.ts index 5870ce14b..697934c3d 100644 --- a/packages/ui/utils/mermaid-math-slot.test.ts +++ b/packages/ui/utils/mermaid-math-slot.test.ts @@ -42,7 +42,7 @@ afterEach(() => { if (savedRenderer) setMathRenderer(savedRenderer, savedSource ?? 'host'); }); -/** The exact call Mermaid 11 makes in `renderKatexUnsanitized`. */ +/** The exact call Mermaid makes in `renderKatexUnsanitized` (unchanged 11 -> 12). */ const MERMAID_OPTIONS = { throwOnError: true, displayMode: true, output: 'mathml' } as const; describe('default export (what Mermaid receives for `katex`)', () => { diff --git a/packages/ui/utils/mermaid.test.ts b/packages/ui/utils/mermaid.test.ts index e97007b57..b63bdd472 100644 --- a/packages/ui/utils/mermaid.test.ts +++ b/packages/ui/utils/mermaid.test.ts @@ -2,7 +2,7 @@ * Mermaid runtime slot (utils/mermaid.ts), sibling of math.test.ts. * * What regresses if these fail: - * - a filled slot no longer short-circuits the loader, so Plannotator's eager + * - a filled slot no longer short-circuits the loader, so a host's eager * entry (which initialized the runtime at module evaluation) would be * followed by a second import and a second initialize; * - the empty-slot path stops memoizing, so every diagram on a page imports diff --git a/packages/ui/utils/mermaid.ts b/packages/ui/utils/mermaid.ts index 4a0ed9f5f..fd371dc7b 100644 --- a/packages/ui/utils/mermaid.ts +++ b/packages/ui/utils/mermaid.ts @@ -2,14 +2,20 @@ * Mermaid runtime slot. * * ONE code path feeds `MermaidBlock`: `loadMermaidRuntime()`. It resolves at - * once from a filled slot and otherwise imports the runtime lazily. Plannotator - * fills the slot at module evaluation through `./mermaid-eager` (imported by - * `packages/editor/App.tsx`), which keeps the runtime in its entry chunk on the - * share portal exactly as it was with the static import, so it cannot fail - * separately from the app. A host that does not import the eager entry gets - * the lazy path: the runtime is fetched on the first diagram, a failed import - * is dropped from the memo so the next call issues a fresh `import()`, and - * the block re-attempts once and offers Retry. + * once from a filled slot and otherwise imports the runtime lazily: the + * runtime is fetched on the first diagram, a failed import is dropped from + * the memo so the next call issues a fresh `import()`, and the block + * re-attempts once and offers Retry. + * + * Since Mermaid 12 (ELK layout by default, about 1.8 MB more runtime than 11) + * the lazy path IS Plannotator's own path: `packages/editor/App.tsx` no longer + * imports `./mermaid-eager`, so a plan with no diagram never downloads the + * runtime in a chunked build (the share portal, any host that bundles by + * route). The single-file builds inline the `import('mermaid')` target through + * `inlineDynamicImports`, so there the lazy import resolves from the bundle + * itself and nothing is fetched. A host that wants the runtime registered at + * startup imports `./mermaid-eager`, which fills the slot at module + * evaluation; the slot then short-circuits this loader. * * This module has NO static import of `mermaid`; the only place the * dependency is named at runtime is the default loader's `import('mermaid')`. @@ -49,7 +55,8 @@ export const MERMAID_CONFIG: MermaidConfig = { /** * Who filled the slot. The eager value doubles as a build marker: the literal * only reaches a bundle when `./mermaid-eager` is evaluated in it, which is - * what `tests/entry-assets.test.ts` asserts on the built HTML. + * how `tests/entry-assets.test.ts` proves on the built HTML that Plannotator's + * own bundles do NOT register the runtime eagerly. */ export type MermaidRuntimeSource = 'plannotator-mermaid-eager' | 'loader' | 'host'; @@ -69,7 +76,9 @@ let pending: Promise | null = null; /** * Delay before the block's one automatic re-attempt after a failed lazy - * import. Only chunking hosts can fail here; a filled slot never loads. + * import. Only chunked builds can fail here (the share portal, a host that + * bundles by route); a single-file build resolves the import from itself and + * a filled slot never loads. */ let retryDelayMs = 750; diff --git a/tests/entry-assets.test.ts b/tests/entry-assets.test.ts index b6dad254e..a9c7add83 100644 --- a/tests/entry-assets.test.ts +++ b/tests/entry-assets.test.ts @@ -80,11 +80,15 @@ describe('review entry assets', () => { }, ); - // Mermaid is eager in the plan editor by policy (the portal entry chunk must - // keep it, as on main) and deliberately absent from the review editor, which - // never renders a Mermaid block: importing it there would grow that bundle. - test('only the plan editor registers the eager Mermaid runtime', () => { - expect(read('packages/editor/App.tsx')).toContain("import '@plannotator/ui/utils/mermaid-eager';"); + // Mermaid is LAZY in both apps by policy since Mermaid 12: the runtime (ELK + // layout by default, ~1.8 MB more than 11) loads on the first diagram + // through utils/mermaid's own import('mermaid'), so a plan with no diagram + // never pays for it in a chunked build (the share portal). Re-adding the + // eager entry to either app would put the whole runtime back into that + // entry chunk with no error anywhere; the review editor never renders a + // Mermaid block at all. + test('neither app registers the eager Mermaid runtime', () => { + expect(read('packages/editor/App.tsx')).not.toContain('mermaid-eager'); expect(read('packages/review-editor/App.tsx')).not.toContain('mermaid-eager'); }); @@ -128,23 +132,25 @@ describe('review entry assets', () => { // dictionary is imported ONLY by identity-tater). These are the guards for // a dropped or tree-shaken side-effect import (a future // `"sideEffects": false` would let Vite discard `import '.../math-eager'`, - // the slot would stay empty and every runtime would paint TeX for a frame; - // a dropped mermaid-eager would move Mermaid into a lazy portal chunk that - // can fail separately). Proven by removing each import and rebuilding: the - // registration marker count drops to zero while the presence markers stay. - // The review bundle must NOT carry the Mermaid marker: it never renders a - // Mermaid block and main's review bundle has no Mermaid in it. + // the slot would stay empty and every runtime would paint TeX for a frame). + // Proven by removing each import and rebuilding: the registration marker + // count drops to zero while the presence markers stay. The Mermaid + // registration marker is asserted ABSENT from both bundles: since Mermaid + // 12 the plan editor loads the runtime lazily by policy, and an eager + // import creeping back in would only show up as a bigger portal entry + // chunk. The review bundle carries no Mermaid at all. // - Presence markers (a KaTeX class name, a Mermaid diagram id, an // Emscripten symbol from Graphviz, the bridge global), which only say the // runtime is still inlined by inlineDynamicImports. KaTeX is inlined // through utils/math-default-loader.ts's import('katex') whether or not it is registered, - // so `katex-display` cannot prove registration and is not asked to. + // so `katex-display` cannot prove registration and is not asked to; the + // Mermaid diagram id in the plan bundle likewise proves inlining only. // // dist/ is gitignored, so this is skipped on an unbuilt checkout; the CI job // that builds the bundles runs it right after. const REGISTRATION_MARKERS = ['plannotator-math-eager', 'uniqueUsernameGenerator']; const markerExpectations: Array<[bundle: string, present: string[], absent: string[]]> = [ - ['apps/hook/dist/index.html', [...REGISTRATION_MARKERS, 'plannotator-mermaid-eager', 'katex-display', 'flowchart-v2', 'viz_set_y_invert', '__plannotatorLiveConfig'], []], + ['apps/hook/dist/index.html', [...REGISTRATION_MARKERS, 'katex-display', 'flowchart-v2', 'viz_set_y_invert', '__plannotatorLiveConfig'], ['plannotator-mermaid-eager']], ['apps/review/dist/index.html', [...REGISTRATION_MARKERS, 'katex-display', '__plannotatorLiveConfig'], ['plannotator-mermaid-eager', 'flowchart-v2']], ]; for (const [path, present, absent] of markerExpectations) {