A token set colour with transparency loses it in the generated dark palette. The contrast audit also treats it as opaque, so faint text can pass a check it would fail as rendered.
Where, at development fd992ea:
lib/Service/ContrastService.php:245 matches only #rgb and #rrggbb, so #rgba and #rrggbbaa are "not a colour".
lib/Service/ContrastService.php:258-263 matches the alpha of rgba() and throws it away.
lib/Service/ContrastService.php:91-92 the contrast audit measures pairs through that parser.
lib/Service/DarkPaletteService.php:314-321 derives each dark token from the same parser, skipping what it cannot read.
lib/Service/DarkPaletteService.php:381-408 deriveColorToken() writes the dark value as an opaque hex.
|
public function parseColor(string $value): ?array { |
|
$value = trim($value); |
|
|
|
if (preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $value, $match) === 1) { |
|
$hex = $match[1]; |
|
if (strlen($hex) === 3) { |
|
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; |
|
} |
|
|
|
return [ |
|
hexdec(substr($hex, 0, 2)), |
|
hexdec(substr($hex, 2, 2)), |
|
hexdec(substr($hex, 4, 2)), |
|
]; |
|
} |
|
|
|
if (preg_match('/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*[\d.]+\s*)?\)$/', $value, $match) === 1) { |
|
$red = min(255, (int)$match[1]); |
|
$green = min(255, (int)$match[2]); |
|
$blue = min(255, (int)$match[3]); |
|
|
|
return [$red, $green, $blue]; |
|
} |
|
|
|
return null; |
|
}//end parseColor() |
|
$rgb = $this->contrast->parseColor(value: $literal); |
|
if ($rgb === null) { |
|
// Unparseable (gradient, keyword, size, font stack, url(), an |
|
// alias chain with no literal at the end) — skip. |
|
continue; |
|
} |
|
|
|
$dark[$token] = $this->deriveColorToken(token: $token, rgb: $rgb, lightDeclarations: $lightDeclarations); |
So an overlay like rgba(0, 0, 0, 0.5) becomes solid in dark mode and hides what it covers. An 8-digit hex token is never darkened at all. The audit compares the opaque colour, not the blend a user sees.
Specified fix: openspec/changes/authoring-token-value-types (requirements "Translucent colours are measured as they render" and "Derived dark values keep the light value's alpha", tasks 3.1 and 3.2).
Found by the OpenSpec pass on 27 Sep 2026 and re-read at fd992ea on 28 Sep.
Live check: add --nldesign-color-overlay: rgba(0, 0, 0, 0.5) to a token set, run occ nldesign:generate-dark-variants --force, and read an opaque hex in its dark file.
A token set colour with transparency loses it in the generated dark palette. The contrast audit also treats it as opaque, so faint text can pass a check it would fail as rendered.
Where, at development fd992ea:
lib/Service/ContrastService.php:245matches only#rgband#rrggbb, so#rgbaand#rrggbbaaare "not a colour".lib/Service/ContrastService.php:258-263matches the alpha ofrgba()and throws it away.lib/Service/ContrastService.php:91-92the contrast audit measures pairs through that parser.lib/Service/DarkPaletteService.php:314-321derives each dark token from the same parser, skipping what it cannot read.lib/Service/DarkPaletteService.php:381-408deriveColorToken()writes the dark value as an opaque hex.thematiq/lib/Service/ContrastService.php
Lines 242 to 267 in fd992ea
thematiq/lib/Service/DarkPaletteService.php
Lines 314 to 321 in fd992ea
So an overlay like
rgba(0, 0, 0, 0.5)becomes solid in dark mode and hides what it covers. An 8-digit hex token is never darkened at all. The audit compares the opaque colour, not the blend a user sees.Specified fix:
openspec/changes/authoring-token-value-types(requirements "Translucent colours are measured as they render" and "Derived dark values keep the light value's alpha", tasks 3.1 and 3.2).Found by the OpenSpec pass on 27 Sep 2026 and re-read at fd992ea on 28 Sep.
Live check: add
--nldesign-color-overlay: rgba(0, 0, 0, 0.5)to a token set, runocc nldesign:generate-dark-variants --force, and read an opaque hex in its dark file.