Skip to content

Commit 25b2e47

Browse files
fix(library): keep custom-colored elements visible in exports (#829)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dcc7af1 commit 25b2e47

4 files changed

Lines changed: 91 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tumaet/apollon": patch
3+
---
4+
5+
Fix custom-colored elements vanishing from exports. Picking a swatch line/fill/text colour stored it as a bare `var(--apollon-swatch-*)` with no fallback, and the compat export pipeline (SVG, PNG, PDF, PPTX, server, VS Code) had no static value for those tokens, so it resolved them to an empty string — the coloured stroke/fill disappeared and only the default-black text survived. The swatch palette now has export fallbacks, so custom-coloured elements render in every format and diagram type (using the light-theme swatch value, matching the rest of the export pipeline). A test keeps the fallbacks in lockstep with the palette so retuning a swatch can't silently drift the exported colour.

library/lib/constants.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,19 @@ export const CSS_VARIABLE_FALLBACKS: Readonly<Record<string, string>> =
9292
"--apollon-collaboration-color-8": "#1098ad",
9393
"--apollon-guide-vertical": "#d63031",
9494
"--apollon-guide-horizontal": "#0984e3",
95+
// Color-picker swatches are stored as a fallback-less `var(--apollon-swatch-*)`,
96+
// so without a value here a swatched element resolves to "" and vanishes from
97+
// headless/compat export (issue #828). Light-theme primitives, like the rest of
98+
// this map; kept in sync with tokens.css by cssVariableContract.test.ts.
99+
"--apollon-swatch-slate": "#64748b",
100+
"--apollon-swatch-red": "#dc2626",
101+
"--apollon-swatch-orange": "#ea580c",
102+
"--apollon-swatch-amber": "#d97706",
103+
"--apollon-swatch-green": "#16a34a",
104+
"--apollon-swatch-teal": "#0d9488",
105+
"--apollon-swatch-blue": "#2563eb",
106+
"--apollon-swatch-violet": "#7c3aed",
107+
"--apollon-swatch-pink": "#db2777",
95108
"--apollon-background": "#ffffff",
96109
"--apollon-background-variant": "#f8f9fa",
97110
"--apollon-hover-neutral":

library/tests/unit/cssVariableContract.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, it, expect } from "vitest"
22
import { readFileSync } from "node:fs"
33
import { resolve } from "node:path"
4+
import { SWATCH_NAMES } from "@tumaet/ui/lib/color-swatch-tokens"
45
import { CSS_VARIABLE_FALLBACKS } from "@/constants"
56

67
// Drift guard for the design-token contract.
@@ -63,3 +64,39 @@ describe("CSS variable contract: CSS_VARIABLE_FALLBACKS ⊆ THEMING.md ∩ token
6364
}
6465
)
6566
})
67+
68+
// The color-picker swatches are the one token family a user can paint directly
69+
// onto exported geometry (as `var(--apollon-swatch-*)` with no inline fallback),
70+
// so unlike the general one-directional guard above they MUST have a compat
71+
// fallback or the element vanishes from every headless/embed export (issue
72+
// #828). And because the fallback duplicates a hex that really lives in
73+
// tokens.css, guard the VALUE too: each fallback must equal the light-theme
74+
// `--primitive-swatch-*` the swatch resolves to, so retuning the palette can't
75+
// silently ship a stale export color.
76+
//
77+
// tokens.css layers `--apollon-swatch-red: var(--primitive-swatch-red)` and
78+
// `--primitive-swatch-red: #dc2626`; the hex lives in the primitive, defined
79+
// once for light (`:root`, authored first) and again for the dark override.
80+
// Compat export is always light (like every other entry in the fallback map),
81+
// so read the first — i.e. light — declaration of each primitive.
82+
const lightPrimitiveHex = (name: string): string | undefined =>
83+
tokensCss.match(
84+
new RegExp(`--primitive-swatch-${name}\\s*:\\s*(#[0-9a-fA-F]{3,8})`)
85+
)?.[1]
86+
87+
describe("swatch tokens ⊆ CSS_VARIABLE_FALLBACKS (export must never blank or drift a swatch)", () => {
88+
it.each(SWATCH_NAMES)(
89+
"--apollon-swatch-%s exports the light-theme primitive hex",
90+
(name) => {
91+
const fallback = CSS_VARIABLE_FALLBACKS[`--apollon-swatch-${name}`]
92+
expect(
93+
fallback,
94+
`--apollon-swatch-${name} has no CSS_VARIABLE_FALLBACKS entry; a swatch of this color would export invisible`
95+
).toBeDefined()
96+
expect(
97+
fallback,
98+
`--apollon-swatch-${name} fallback ${fallback} != light --primitive-swatch-${name} ${lightPrimitiveHex(name)} in tokens.css — retune both together`
99+
).toBe(lightPrimitiveHex(name))
100+
}
101+
)
102+
})

library/tests/unit/exportUtils.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,42 @@ describe("replaceCSSVariables", () => {
309309
})
310310
})
311311

312+
// ---------------------------------------------------------------------------
313+
// Swatch color export (issue #828)
314+
//
315+
// A chosen swatch is stored as a fallback-less `var(--apollon-swatch-*)`
316+
// (ColorButtons.tsx). While those tokens were absent from
317+
// CSS_VARIABLE_FALLBACKS the compat export resolved them to "", wiping the
318+
// element's stroke/fill and leaving only the default-black text. (All swatches
319+
// are guarded present in cssVariableContract.test.ts; here we assert the paint
320+
// actually survives the export path.)
321+
// ---------------------------------------------------------------------------
322+
describe("swatch color export (issue #828)", () => {
323+
let svg: SVGSVGElement
324+
325+
beforeEach(() => {
326+
svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
327+
})
328+
329+
it("resolves a fallback-less swatch color to its hex", () => {
330+
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect")
331+
rect.setAttribute("stroke", "var(--apollon-swatch-red)")
332+
svg.appendChild(rect)
333+
334+
replaceCSSVariables(svg)
335+
expect(rect.getAttribute("stroke")).toBe("#dc2626")
336+
})
337+
338+
it("leaves a native custom hex untouched", () => {
339+
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect")
340+
rect.setAttribute("stroke", "#123456")
341+
svg.appendChild(rect)
342+
343+
replaceCSSVariables(svg)
344+
expect(rect.getAttribute("stroke")).toBe("#123456")
345+
})
346+
})
347+
312348
// ---------------------------------------------------------------------------
313349
// convertStyleToAttributes
314350
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)