Skip to content

Render mermaid diagrams in the theme's own colours - #8

Merged
Amoner merged 1 commit into
mainfrom
feat/mermaid
Aug 29, 2026
Merged

Amoner merged 1 commit into
mainfrom
feat/mermaid

Conversation

@Amoner

@Amoner Amoner commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Renders ```mermaid fences as diagrams, drawn in Inkwell's own colours.

Stacked on #7 — both change renderPreview, so this is based on
feat/local-images to keep the diff readable. Merge #7 first; GitHub will
retarget this to main and CI runs then.

Cost: +888 KB installed, nothing at startup

mermaid is ~3.4 MB of JavaScript, which sounds fatal for a "~6 MB" app. It is
not, for two measured reasons.

It is not in the entry chunk. The library is behind a dynamic
import("mermaid") that only fires the first time a document actually contains
a mermaid fence. The entry chunk is smaller than before (731 KB → 722 KB —
chunk-boundary churn, not a real saving). mermaid already code-splits per
diagram type, so a document with a flowchart never loads the gantt, cytoscape
or katex chunks.

Raw JS is not installed bytes. tauri = { version = "2" } keeps the default
compression feature, which brotli-compresses embedded frontend assets:

on #7 + mermaid delta
dist raw 1,936 KB 5,392 KB +3,456 KB
Inkwell.app 5,652 KB 6,540 KB +888 KB (+15.7%)

Local release builds run about 1 MB heavier than CI's — the shipped v0.2.4 in
/Applications is 4,540 KB against 5,572 KB for the same commit built here.
Applying the delta, a shipped build lands near 5.4 MB. README's "~6 MB" and
the "30x smaller than Electron" claim both survive untouched.

Theme

The ask was a style that reads on both day and night. Rather than freezing one
palette that compromises on both, mermaid's base theme is driven from
Inkwell's own CSS custom properties — --bg-preview, --bg-code-block,
--text-primary, --text-secondary, --border, --accent, --font-sans —
with darkMode set to match. A diagram is drawn in the palette of whichever
theme is showing, and toggling re-reads the variables, re-initialises mermaid
and redraws. The render cache is keyed by theme plus source, so a toggle can
never serve back a diagram drawn in the other palette.

One thing this exposed: diagrams that colour a set of things — pie slices,
gitGraph branches, timeline sections — cannot come from a single accent. Derived
from one colour, every pie slice rendered identically; the chart was unreadable
in both themes. Those slots now use a validated categorical set: eight hues in
fixed order, stepped separately per surface rather than flipped. Checked against
the real preview backgrounds (#fafafa / #252525): worst adjacent colour-blind
separation 9.1 light / 8.4 dark (OKLab ΔE, ≥8 target), worst normal-vision
separation 19.6 / 19.3 (≥15 floor). Past eight slots mermaid falls back to its
own derived colours rather than inventing a ninth hue.

--error is new in both theme files; there was no token for it, and the parse
error message needs one that reads on either background.

Rendering after sanitizing

Inkwell sanitizes with USE_PROFILES: { html: true }, which strips SVG.
Widening that profile to admit mermaid's output would weaken sanitizing for
every document, so the diagram is built after DOMPurify has run, replacing the
code block it produced. mermaid's own securityLevel: "strict" sanitizer is the
boundary for the SVG it generates — the documented contract, and the reason the
profile does not have to move.

Behaviour details

  • A cached diagram is swapped in synchronously, so typing elsewhere does not
    make every diagram flicker. Anything new stays a code block until ready.
  • Every DOM swap is guarded on pre.isConnected — a 150 ms debounce plus async
    rendering means a render can land on a preview that no longer exists.
  • The cache is bounded at 60 entries, oldest-first; editing a diagram produces a
    new entry per keystroke.
  • data-source-line moves from the <code> element onto the diagram, so
    click-to-edit and scroll sync keep working on the diagram itself. (The
    attribute is on <code>, not <pre> — markdown-it renders fence attrs there.)
  • A diagram that fails to parse shows the message and keeps the fence visible
    so the source stays editable. suppressErrorRendering stops mermaid injecting
    its own error nodes into the document — verified: zero stray nodes on body.
  • If the dynamic import fails, fences stay plain code blocks — exactly what they
    rendered as before this PR.

Verified

Headless WebKit against the built dist, serving the app's real CSP as an HTTP
header, plus the real .app on macOS:

  • 5 diagram types render (flowchart, sequence, state, class, pie) in both themes.
  • No CSP violations — mermaid needs no unsafe-eval; the policy is unchanged.
  • No page errors; the broken fence produces exactly one error message and keeps
    its source; a plain ```js fence is untouched.
  • Theme toggle measurably repaints: node fill #f6f8fa → #2d2d2d, node text
    #1a1a1a → #e0e0e0 — the --bg-code-block / --text-primary values.
  • data-source-line present on every rendered diagram.
  • Real .app on macOS: diagrams render in WKWebView under the shipped CSP, and
    the Theme button redraws them.

@Amoner
Amoner changed the base branch from feat/local-images to main August 29, 2026 03:51
Adds mermaid behind a dynamic import, so a document without a ```mermaid
fence never loads the library. The entry chunk does not grow (731 -> 722 KB);
mermaid arrives as its own chunks and already code-splits per diagram type.
Raw JS is not installed bytes — Tauri's default compression feature
brotli-compresses embedded assets, so 3.4 MB of JavaScript costs +888 KB in
Inkwell.app.

Diagrams are built after DOMPurify has run, replacing the code block it
produced. Inkwell sanitizes with USE_PROFILES: { html: true }, which strips
SVG; widening that profile to admit mermaid's output would weaken sanitizing
for every document, so mermaid's own strict-mode sanitizer is the boundary for
the SVG it generates instead.

Theming reads Inkwell's CSS custom properties and feeds them to mermaid's base
theme, so a diagram is drawn in the palette of whichever theme is showing
rather than in mermaid's default lavender. Toggling re-reads the variables and
redraws, and the render cache is keyed by theme plus source so a toggle can
never serve back a diagram drawn in the other palette.

Diagrams that colour a set of things — pie slices, gitGraph branches, timeline
sections — cannot be driven from a single accent, and deriving them from one
colour made every pie slice identical. They use a validated categorical set
instead: eight hues in fixed order, stepped separately per surface. Checked
against the real preview backgrounds, worst adjacent colour-blind separation
is 9.1 light / 8.4 dark and worst normal-vision separation 19.6 / 19.3.

Details:
- A cached diagram is swapped in synchronously so typing elsewhere does not
  make every diagram flicker; anything new stays a code block until ready.
- Every DOM swap is guarded on isConnected — a 150 ms debounce plus async
  rendering means a render can land on a preview that no longer exists.
- data-source-line moves from the <code> element onto the diagram, so
  click-to-edit and scroll sync keep working on the diagram itself.
- A diagram that fails to parse shows the message and keeps the fence visible.
  suppressErrorRendering stops mermaid injecting error nodes into the document.
- If the import fails, fences stay plain code blocks.

Adds --error to both themes; there was no token for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Amoner
Amoner merged commit 4dbadd9 into main Aug 29, 2026
4 checks passed
@Amoner
Amoner deleted the feat/mermaid branch August 29, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant