ADR-0065: Diagrams — Excalidraw as the Manual Layer, Mermaid Stays for Text-First/UML¶
- Status: SUPERSEDED by ADR-0072 (2026-07-15 — self-hosted draw.io replaces Excalidraw; the integration shape here carries over, only the editor engine + artifact format changed). Originally: accepted (spike-gated — spike PASSED 2026-07-13, GO-WITH-CONDITIONS; conditions are engineering items in the implementation slice, not open decisions)
- Date: 2026-07-13
- Deciders: Nicolas Burri ("excalidraw sounds interesting… keeping mermaid for pure UML also sounds good to me. So we can use the tool that is best suited"), after the options analysis (mermaid-only ceiling, self-hosted draw.io embed, maxGraph self-build) and the spike evidence.
- Motivation: the company QMS carries many draw.io flowcharts; text DSLs (mermaid) cannot replace manual layout; AI-assisted drafting is a stated future goal.
- Relates to: ADR-0045 (content-part attachments — the storage vehicle), ADR-0057 (tree-as-record + documented projection degradation), ADR-0059 (the "path of least resistance to full-featured" doctrine), ADR-0062 (license posture the lane was audited against).
Decision¶
- Two diagram tools, by suitability: Mermaid (already integrated) remains for text-first / UML / AI-generated diagrams; Excalidraw becomes the manual/freeform flowchart layer.
- Storage (the house pattern): a
diagramnode in the ProseMirror tree references a content-part pair —scene.json(the editable Excalidraw source) + rendered.svg(display). Both ride ADR-0045 attachment machinery; frozen versions pin both parts. - Display = SVG-as-
<img>through the existingattachment:resolver (spike Q4): both evaluated paths proved XSS-inert, but the img path adds no new sanitizer trust surface and structurally scopes the SVG's@font-faceaway from the app document (the inline-SVG path would inject global-scope CSS). The sanitize authority stays untouched. - Editing: slash-command opens a lazily-loaded React island (react + react-dom + excalidraw confined to a lazy chunk; spike-measured main-bundle delta +42 bytes gzip, island ~452 KB gzip, zone-isolated, leak-free). On save: re-export scene + SVG.
- AI bridge:
mermaid-to-excalidraw(MIT) is the "AI drafts, human refines" path — AI emits mermaid, conversion yields fully editable elements, layout cleanup is manual (converter output is dense by design; documented limitation). - Projection: degrades to an image reference — the ADR-0057 known-limitations pattern; the tree + content parts remain lossless.
- draw.io compatibility valve: existing company charts enter as attached
.drawiofiles with an exported SVG preview — migration without commitment; no embedded draw.io editor.
Spike evidence (screenshots/spike-excalidraw-*.png; full report in the 2026-07-13 worklog)¶
License lane CLEAN over the full 202-package production tree (two khroma-style gate exclusions
needed: pako (MIT AND Zlib), khroma (MIT*)). React island measurements as in §4. Fonts
self-hosted; dark mode follows html[data-mode]. Scene JSON ~74 KB / SVG ~53 KB for a 20-node
chart. Hostile SVG injection inert on both display paths. Round-trip visually identical.
Conditions (implementation-slice obligations)¶
- C-1 (the driver):
exportToSvg's font harvester falls back to the esm.sh CDN for fonts it lacks locally — CSP blocks them (cosmetic: the exported SVG is verifiably self-contained and embeds the primary font), but a QMS tool must not EMIT blocked external requests. Suppress the fallback (restrict registered fonts / shim the harvester) and PIN "zero external requests on export" in the fullstack smoke (prod-bundle-gate discipline). - C-2: ci.yml license gate gains the
pako+khroma-pattern exclusions with reasons. - C-3: carry the react-dom
.defaultinterop normalization (prod-only optimizer behavior — dev works without it, prod throws; found live in the spike). - C-4: React type packages stay dev-only; the production lane is exactly react, react-dom, @excalidraw/excalidraw, @excalidraw/mermaid-to-excalidraw.