Invalid theme values are clamped silently
The editor and AI assistant share one validation policy. The note also corrects its original description of the code's behavior before that policy was implemented.
Every write to the Active Theme passes one validation policy, untrusted by default — the Theme Editor and the assistant's Live Edit are not separated into trust tiers, because both ultimately carry unchecked strings. Out-of-gamut and malformed values reach it in practice, so the policy is to clamp into gamut and continue, never to reject the write and never to tell the visitor. The alternative — surfacing a toast when a value is clamped — was rejected because clamping a slider is invisible by design and the toast would be noise, and because the assistant announcing its own bad output mid-conversation is worse than quietly correcting it.
Consequences
A visitor who types or pastes an out-of-gamut colour into the Theme Editor sees a colour that differs from the one they entered, with no explanation. This is expected, not a bug.
Silence is the lesser of two invisible failures rather than a good outcome on its own: an unvalidated value makes setProperty a no-op, so the variable silently keeps its previous value. Both paths are quiet; only one of them lands somewhere close to what was asked for.
As built
This decision was accepted before anything implemented it, and the description of the existing behaviour it was accepted against was wrong. Recorded here rather than in a superseding ADR, because the decision did not change — only the code caught up with it.
Two things were true when this was written and are no longer:
validateAndFixThemesubstituted defaults, it did not clamp. A radius of5becameDEFAULT_THEME.radius, not1.5; an invalid colour became the default theme's colour. That is a third behaviour this ADR never considered, and the one its own reasoning argues hardest against — a substituted default does not land anywhere near what was asked for.- Out-of-gamut colours were not detected at all.
OKLCH_REGEXinvalidation.tschecked the shape of anoklch(...)string and nothing else, despite a comment claiming it bounded L to 0–1 and H to 0–360.oklch(99 99 999)passed validation untouched and went straight tosetProperty, where the browser clamped it at paint.
The policy now lives in clampColour and friends in lib/theme/active-theme.ts, applied at the operation boundary so that state never holds a value the clamp would have corrected — which in turn keeps share links, CSS export, and JSON export clean for free.
Two extensions to the decision fall out of "clamp toward what was asked for":
- Other colour spaces are converted, not rejected. A pasted
#3366cc,rgb(...), orhsl(...)is not out of gamut, it is in the wrong space, and there is nothing to clamp toward. Pasting a hex code is the single most likely thing a client does in that box, so it is honoured. Bounds are per-channel (L→[0,1], C→[0,0.4], H mod 360) rather than true hue-dependent sRGB gamut mapping; a flat chroma ceiling keeps the policy pure and fast, and everything it admits is renderable. - Genuinely unparseable input falls back to the previous value, not to the default. This is the one path where there is nothing closer to honour, and the previous value is at least something the visitor chose.