diff --git a/CONTEXT.md b/CONTEXT.md index 9d42611ef..f77a4ee0a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -54,11 +54,11 @@ The classification of a Vertex Type's configured icon into what it takes to rend _Avoid_: Icon type (ambiguous with `iconImageType`, the stored MIME string) **Icon Registry**: -The single store of resolved icons, keyed by Icon Source Id and shared by every Icon Surface. Holds a color-free artifact — a sanitized SVG string or a raster url — so applying a Vertex Type's color stays a pure transform at the point of use. A plain external store outside React/Jotai, bridged by `useSyncExternalStore`; explicitly **not** TanStack Query, because a per-hook subscription scaled with Vertex Type count and locked up the Schema View at 10k. Resolves a raster url synchronously, allows a failed icon three attempts in total, and never stores a failure as a result. See `docs/adr/20260813-icon-registry-not-react-query.md`. +The single store of resolved icons, keyed by Icon Source Id and shared by every Icon Surface. Holds a color-free artifact — a sanitized SVG string or a raster url — so applying a Vertex Type's color stays a pure transform at the point of use. Every stored SVG is guaranteed to carry a `viewBox`, synthesized from `width`/`height` when the source omits one, because the surfaces fit icons by `preserveAspectRatio` and an SVG with no `viewBox` has no ratio to fit (issue #2108). A plain external store outside React/Jotai, bridged by `useSyncExternalStore`; explicitly **not** TanStack Query, because a per-hook subscription scaled with Vertex Type count and locked up the Schema View at 10k. Resolves a raster url synchronously, allows a failed icon three attempts in total, and never stores a failure as a result. See `docs/adr/20260813-icon-registry-not-react-query.md`. _Avoid_: Icon cache (it is the source of truth for resolution, not a layer in front of one) **Icon Surface**: -One of the three places an icon is drawn, which differ in how color is applied and how much they trust the markup. The **canvas** (`useBackgroundImageMap` → cytoscape `background-image`) and the **sandboxed DOM** (`VertexSymbolIcon` → ``) both render the icon as a separate image document, so CSS cannot reach it: an SVG is passed as a `data:` uri with the color baked into the markup, a raster as its plain url. **Inline DOM** (`VertexIcon`, and the lucide branch of `VertexSymbolIcon`) renders live elements that inherit `color` through `currentColor`, making recolor free. Only trusted lucide geometry is inlined by `VertexSymbolIcon`; `VertexIcon` also inlines sanitized user SVG, which predates that rule and is the known outlier. +One of the three places an icon is drawn, which differ in how color is applied and how much they trust the markup. The **canvas** (`useBackgroundImageMap` → cytoscape `background-image`) and the **sandboxed DOM** (`VertexSymbolIcon` → ``) both render the icon as a separate image document, so CSS cannot reach it: an SVG is passed as a `data:` uri with the color baked into the markup, a raster as its plain url — `VertexSymbolIcon` places either directly. Both surfaces inset the icon to 60% of the node and fit it with `preserveAspectRatio`, but only `VertexSymbolIcon` can do so directly, in its own SVG coordinates; cytoscape cannot both preserve a ratio and inset, so the canvas instead wraps whatever `toIconImageUrl` returned — the plain raster url included — in its own padded square SVG and lets the nested `` fit itself (issue #2108). That wrapper is applied on the canvas path alone — adding it inside `toIconImageUrl` would inset twice on the DOM side. **Inline DOM** (`VertexIcon`, and the lucide branch of `VertexSymbolIcon`) renders live elements that inherit `color` through `currentColor`, making recolor free. Only trusted lucide geometry is inlined by `VertexSymbolIcon`; `VertexIcon` also inlines sanitized user SVG, which predates that rule and is the known outlier. _Avoid_: Icon renderer (three similarly-named components — `VertexIcon`, `VertexSymbol`, `VertexSymbolIcon` — differ by surface, so name the surface) **Neighbors**: diff --git a/docs/adr/20260813-icon-registry-not-react-query.md b/docs/adr/20260813-icon-registry-not-react-query.md index 67bdb8a11..89c827c58 100644 --- a/docs/adr/20260813-icon-registry-not-react-query.md +++ b/docs/adr/20260813-icon-registry-not-react-query.md @@ -31,18 +31,20 @@ Separately, the DOM surface paid for a workaround it did not need. `VertexSymbol **2. Icons render per kind, on the surface's own terms.** -| kind | canvas | DOM | -| -------- | -------------- | ------------------------------------------ | -| Lucide | baked data uri | ``, live DOM, color inherited | -| user SVG | baked data uri | ``, color baked | -| raster | url | `` | +| kind | canvas | DOM | +| -------- | -------------------------------------- | ------------------------------------------ | +| Lucide | baked data uri, wrapped for aspect fit | ``, live DOM, color inherited | +| user SVG | baked data uri, wrapped for aspect fit | ``, color baked | +| raster | plain url, wrapped for aspect fit | `` | Lucide markup is trusted bundled geometry with no ids, defs, or script, so inlining it costs nothing and recoloring becomes synchronous. -Untrusted SVG is deliberately **not** inlined on these surfaces. `` renders it as a script-disabled image document (W3C SVG Integration §3.4/§3.6 — an image context disables both script execution and external references). Inlining would trade that browser-enforced boundary for DOMPurify alone, and add id collisions with the `useId()`-generated `clipPath` ids and unsanitized `