feat(ui): tone the Mermaid node shadow to 70 and derive its colour from the palette - #1563
Merged
Merged
Conversation
…Mermaid's own Mermaid 12's neo look shadows every node, cluster and actor from its own fixed `drop-shadow(1px 2px 2px rgba(185,185,185,1))`. On a themed page that grey reads as a halo around every node and follows no palette. Keep the look, replace the one filter. `buildMermaidThemeVariables` takes `options.shadowAmount` (0..1, default 0.7) and publishes `themeVariables.dropShadow` from the new pure `buildMermaidShadow(ground, amount)`. Geometry scales over a small floor, so 1 reproduces Mermaid's `1px 2px 2px` exactly and 0 publishes `dropShadow: false` (plus `nodeShadow: false`, the only thing that reaches the inline filter on a state diagram's small start/end dots), which the neo rules render as `filter: none`. The colour comes from the page's own ground and its POLARITY follows the page, the same rule Mermaid's `insertLookDefs` uses: dark page -> the ground lifted 82% toward white at alpha 0.18..0.90; light page -> darkened 40% toward black at 0.25..0.55; each asserted to differ from the ground in the direction that reads. Polarity matters more than alpha here: a black shadow on a near-black ground is a valid filter that paints nothing. `mermaidThemeKey` takes the amount and appends `#s<amount>` only when it is not the default, so the key for the shipped default is byte-identical to the `(palette, mode)` key it has always been. The static `MERMAID_CONFIG` a token-less host renders with gets the same 70 geometry with one fixed light colour, since its own slate palette is dark.
The amount reaches the renderer the way the palette does: `DiagramBlock` reads the cookie-only `diagramShadow` setting through `useConfigValue`, puts it on `DiagramTheme.shadowAmount`, and the renderer folds it into the theme key — so changing it re-initializes Mermaid and re-renders every mounted diagram, with no new context and no server write. The scale lives in `utils/diagramShadow` and is deliberately import-free: the settings registry is read on every surface (the guides.show viewer included) and must not pull the diagram theme mapping into that module graph. One trap worth the comment it carries: 0 is a VALID stored amount here, and `Number(null)` is 0, so `fromCookie` rejects an absent cookie BEFORE parsing. Without that, "never chosen" reads as "no shadow" and every diagram ships flat.
…scape hatches
AGENTS.md gets the rule (colour from the ground, polarity from the page, 70 by
default, the setting); README and HANDOFF get what a host has to know: the new
`options.shadowAmount`, that a host wanting Mermaid's grey passes its own
`themeVariables.dropShadow` after ours, that `{ shadowAmount: 0 }` is how to
ship none, and that the cache key is unchanged at the default.
…d 70 The shadow is a paint-time filter. Every variable but the two shadow keys must be identical with it off — swept over every shipped palette in both modes, plus the light/dark base pair explicitly.
The viewer bundles the diagram theme mapping, so its JS hash moved; the stylesheet did not. Regenerated with build:viewer && sync:manifest.
Palette-derived Mermaid node shadow at a default of 70% plus the Settings → Display "Diagram Shadow" control. Core stays pinned at 0.25.4 (unchanged from 0.41.1).
backnotprop
force-pushed
the
feat/mermaid-shadow-default
branch
from
September 18, 2026 01:53
fd826c7 to
3dfbf7a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The shipped look stays exactly as it is — Mermaid 12's neo look, ELK, our theme-aware palette mapping. Only the node drop shadow changes: it is toned down to 70 and its colour now comes from the palette instead of Mermaid's fixed grey.
Mermaid paints that shadow from
drop-shadow(1px 2px 2px rgba(185,185,185,1)), a constant that follows no palette and reads as a grey halo around every node on a themed page.What changed
buildMermaidThemeVariables(tokens, mode, options?)takesoptions.shadowAmount(0..1, defaultDEFAULT_MERMAID_SHADOW_AMOUNT= 0.7) and publishesthemeVariables.dropShadowfrom the new purebuildMermaidShadow(ground, amount):x = 0.3 + 0.7a,y = blur = 0.6 + 1.4a— so 1 reproduces Mermaid's1px 2px 2pxexactly and 0 publishesdropShadow: false(plusnodeShadow: false, the only thing that reaches the inlinefilter:url(#…-drop-shadow-small)on a state diagram's start/end dots), which the neo rules render asfilter: none.insertLookDefsdoes (floodColor = theme.includes('dark') ? '#FFFFFF' : '#000000'): dark page → the ground lifted 82% toward white at alpha0.18 + 0.72a; light page → darkened 40% toward black at0.25 + 0.3a; each asserted to differ from the ground in the direction that reads, falling back to white/black, and to Mermaid's grey when the palette yields no usable ground. Polarity matters more than alpha here: a black shadow on a near-black ground is a valid filter that paints nothing.diagramShadow, Settings → Display → "Diagram Shadow" (None / 40 / 70 / 100), next to Grid Background. It rides into the renderer asDiagramTheme.shadowAmountand into the Mermaid cache key, so changing it re-initializes and re-renders every mounted diagram. At the default the key is byte-identical to the old(palette, mode)key.Nothing in Mermaid itself is touched: this is
themeVariables.dropShadow(plusnodeShadowat 0), our colour derivation, and the Display setting. No version bumps — the@plannotator/ui/@plannotator/corebump is left for merge time, depending on the order with #1562.Before / after
Headless Chromium on
01-service-architecture.md, 1440×1000 @2x, palette pinned through the data dir anddocumentElement.classNameverified before measuring. Per diagram: % of pixels whose max channel delta exceeds 3, and the mean delta over those pixels.The last row is the calibration check: at amount 100 the palette-derived colour lands within ~4/255 of Mermaid's own grey on this palette, so everything the other rows measure is the toning down, not a colour shift. Computed filters off a live node: main
drop-shadow(rgb(185,185,185) 1px 2px 2px); branchdrop-shadow(rgba(203,205,207,0.682) 0.79px 1.58px 1.58px)(plannotator dark) andrgba(125,125,125,0.46)(one-light); amount 0 →none.Screenshots (main vs branch, both modes, plus 0 / 70 / 100) with a local index:
scratchpad/mermaid-shadow-pr/index.html.Tests
packages/ui/utils/mermaidTheme.test.ts: a default shadow present for every shipped palette in both modes, in the readable direction (lighter than the ground on a dark page, darker on a light one), never Mermaid's grey; absent at 0; geometry pinned at 0.7 and 1; the cache key carries the amount and the default key is the old one; and a fill/label fingerprint — every variable but the two shadow keys identical at amount 0 and 70, per palette, per mode.packages/ui/config/diagramShadowSetting.test.ts: the registry contract, including the trap that 0 is a valid stored amount whileNumber(null)is 0, so an absent cookie must be rejected before parsing (without it, "never chosen" reads as "no shadow" for everyone).packages/ui/components/MermaidBlock.theme.test.tsx: the setting actually reaches the runtime and re-renders mounted diagrams.bun run typecheck;bun test packages/ui/utils packages/ui/config packages/core(1036 pass);DOM_TESTS=1 bun test --isolate packages/ui/components/MermaidBlock* packages/ui/components/diagram packages/ui/components/Settings*(33 pass); the DOM allowlist test;bun run --cwd packages/ui smoke:package;build:viewer+check:manifest(repinned — the viewer bundles the mapping, so its JS hash moved; the stylesheet did not, and main was verified in sync first).The diagram engine is unaffected: the shadow is a paint-time filter on nodes, clusters and actors. The edge hit layer sits outside
.node, the overlay rings and badges are DOM outside the SVG, andsanitizeDiagramSvg's scoped-style rule keeps the[data-look="neo"]rules (they are under#renderId, and a literaldrop-shadow(…)has nourl(forcssFetchesto refuse).For hosts
@plannotator/uiconsumers get the toned-down default with no change. To keep Mermaid's grey, pass your ownthemeVariables.dropShadowafter ours; to ship none, pass{ shadowAmount: 0 }. The staticMERMAID_CONFIGa token-less host renders with carries the same 70 geometry with one fixed light colour (its own slate palette is dark) — that is the one byte that changed in the fallback config. Documented inpackages/ui/README.mdandHANDOFF.md.