Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1091,7 +1091,7 @@ There is **one** highlighter in the app: the Shiki instance `@pierre/diffs` alre

**Diagram engine (ui 0.41.0):** every Mermaid and Graphviz diagram renders through ONE renderer slot and ONE canvas, the viewer moved in from Workspaces (owner ruling: the new engine, not an option). `MermaidBlock` and `GraphvizBlock` are thin wrappers over `packages/ui/components/DiagramBlock.tsx`, which keeps the fence side (`diagramLanguages.ts`, the pending fence under "Rendering diagram…", the error panel with the source, the per-engine Retry epoch, the Show-source toggle, the natural-height box) and renders `DiagramViewer` (`packages/ui/components/diagram/`) inline and again at full size in `DiagramPopout` (the `TablePopout` chrome; one code path). The renderer slot is `packages/ui/utils/diagram-render.ts`: `renderDiagram(kind, renderId, source, theme)` loads the engine through its runtime slot (`utils/mermaid.ts` as before, with `applyMermaidTheme` per (palette, mode) before every render; `utils/graphviz.ts`, new, the same shape, `@viz-js/viz` pinned exactly `3.30.0`), sanitizes the output into a NODE (`sanitizeDiagramSvg` = DOMPurify `parseDiagramSvg` + the in-place `scrubDiagramSvg` belt: no script, no `on*`, no `javascript:`/`data:` reference, no `<a href>` — a Mermaid click binding must never turn a pinpoint click into a navigation), and pairs it with the engine's finder (`utils/diagram-anchor.ts` for Mermaid's id grammar, `utils/diagram-anchor-graphviz.ts` keyed on `g.node > title`, never `nodeN`). A load failure is a value (`runtimeUnavailable: true`), which is the one failure Retry can change; one automatic re-attempt runs after the slot's retry delay. The canvas (`DiagramCanvas`) mounts the node with `replaceChildren` inside a CSS-transformed wrapper — wheel/drag/`+`/`-`/`0`/arrow keys; click-to-select vs drag-to-pan on a pointer-type-aware threshold (4 px mouse, 10 px finger); NO hover targeting on a plain mouse-over (owner ruling: it read as messy and fought the pan hand — the ring under the pointer appears only while the platform modifier is held, `isModKeyHeld`, and disarms on its release, on any other key, and on blur); canvas keys ignore Meta/Ctrl/Alt so browser chords pass through; inline the canvas is `touch-action: pan-y` (a finger scrolls the page past it) and only the popout is `touch-none`; the zoom strip is `data-print-hide` while rings and badges print. Every edge gets an invisible 14 px hit path in ONE layer appended last in the svg root (`widenEdgeHitAreas`: bare geometry, ancestors' transforms composed, no id/class/`data-*`, `diagramHitSource` maps it back) — measured in Chromium, the only thing painted over an edge is its OWN label box, centred on its midpoint, so a label is a target that resolves to its edge, and because the hit layer sits over the nodes the canvas resolves a click by PRIORITY over `elementsFromPoint` (node, then edge, then cluster), never by `event.target`. A click that resolves no part comments on the WHOLE diagram (kind `diagram`), so a click never does nothing. The scrub also scopes every `<style>` to rules under the svg's own root id (`scopeDiagramCss`: no `@import`, no fetching `url(`, no rule that could restyle the page). And the overlay (`DiagramOverlay`) reprojects rings and badges from `getBBox` through `getScreenCTM` each frame (`utils/diagram-projection.ts`), so they keep constant pixel size at every zoom. `components/mermaidSvg.ts` is gone. Under happy-dom DOMPurify is not functional (foreign-realm fragment, mislabelled svg namespaces, a cached `Node.prototype.nodeName` getter), so DOM tests swap the parse step for an inert XML parse through `__setDiagramSvgParserForTests` (`packages/ui/test-setup/diagramSvg.ts`) while the scrub still runs; the DOMPurify parse is proven in Chromium (the parity run in the PR that landed it). Real rendered svgs for the codec tests live in `packages/ui/test-setup/fixtures/diagrams/` with Chromium-measured geometry beside them.

**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. Graphviz has no theme system, so the renderer slot's `themeGraphvizSvg` recolors the DOT DEFAULTS onto the tokens after the scrub (the white page polygon removed, `fill="none"` → `var(--card)`, default black strokes → `var(--foreground)` on nodes and edges and `var(--border)` on clusters, `lightgrey` → `var(--muted)`, text → `var(--foreground)`) and leaves every author color alone; it needs no per-palette equivalent.
**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, shadow amount)` 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`. **Node shadow:** 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))` grey, which reads as a halo on a themed page. The look is kept and that one filter is replaced: `buildMermaidThemeVariables` sets `themeVariables.dropShadow` from `buildMermaidShadow(ground, amount)` at `DEFAULT_MERMAID_SHADOW_AMOUNT` (**0.7**, i.e. 70 on the user-facing 0-100 scale, where 100 reproduces Mermaid's own geometry exactly and 0 publishes `dropShadow: false` — plus `nodeShadow: false`, the only thing that reaches a state diagram's inline-filtered start/end dots — which the neo rules render as `filter: none`). The colour is derived from the palette's own ground and its POLARITY follows the page, the same way Mermaid's `insertLookDefs` picks a white flood colour on dark themes: on a dark page the ground lifted 82% toward white at alpha `0.18 + 0.72a`, on a light page darkened 40% toward black at `0.25 + 0.3a`, each asserted to differ from the ground in the direction that reads (white/black as the fallback, Mermaid's grey when the palette yields no usable ground). A black shadow on a near-black ground is a valid filter that paints nothing, which is why polarity — not just alpha — follows the page. The amount is the cookie-only `diagramShadow` setting (Settings → Display → "Diagram Shadow", steps 0/40/70/100, `packages/ui/utils/diagramShadow.ts`), which rides into the renderer as `DiagramTheme.shadowAmount` and into the cache key; at the default the key is byte-identical to the old `(palette, mode)` one. It is a paint-time filter only: fills and label colours are identical at every amount (pinned in `mermaidTheme.test.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 — that config carries the same 70 geometry with a fixed light shadow colour, since its own slate theme is dark. A host that wants Mermaid's grey back sets its own `themeVariables.dropShadow` after ours; one that wants no shadow passes `shadowAmount: 0`. Do not add per-palette Mermaid themes or per-theme `.mermaid` CSS — extend the token mapping instead. Graphviz has no theme system, so the renderer slot's `themeGraphvizSvg` recolors the DOT DEFAULTS onto the tokens after the scrub (the white page polygon removed, `fill="none"` → `var(--card)`, default black strokes → `var(--foreground)` on nodes and edges and `var(--border)` on clusters, `lightgrey` → `var(--muted)`, text → `var(--foreground)`) and leaves every author color alone; it needs no per-palette equivalent.

**Printing (light half, one rule):** paper is white, so the whole page prints in
the **LIGHT half of the user's pair** — prose, code and diagrams together.
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/core/guide-viewer-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import type { GuideViewerAssets } from "./guide-format";

export const GUIDE_VIEWER_MANIFEST: Omit<GuideViewerAssets, "baseUrl"> = {
js: "viewer.CHc6x2n0.js",
js: "viewer.48O72WuZ.js",
css: "viewer.DilvVn0i.css",
jsIntegrity: "sha384-vVppc3Cfjf/9jt9zBzIFasRCLFfr0N+a45dr6KBjVTFhvdXkxnn2fQ88lIrXg2+w",
jsIntegrity: "sha384-XQrIafIUrC1JvOSLEFyhqNObv7YmydTDPwXCDJvSV7UcU2BMouDtQADclC1uvAGK",
cssIntegrity: "sha384-EvOFwI14FdyDrbnpahJjjIRkQXWl7ihnmSB4mDmFdIJCrdKTB3KfEAN+wfMNT9fL",
langs: {
"astro": "chunks/astro.Ts5EKq2l.js",
Expand Down
Loading
Loading