Skip to content

fix(contrast): measure translucent colours as they render, and keep alpha in dark mode - #703

Merged
rubenvdlinde merged 1 commit into
developmentfrom
fix/696-translucent-colours
Sep 28, 2026
Merged

rubenvdlinde merged 1 commit into
developmentfrom
fix/696-translucent-colours

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

A translucent token colour lost its transparency in the generated dark palette, and the contrast audit measured it as opaque, so faint text could pass a check it fails as rendered.

The defect

ContrastService::parseColor() read only #rgb and #rrggbb and dropped the alpha of rgba(). An 8-digit hex was "not a colour", so the dark palette skipped it and the audit marked it unevaluated. An rgba() value was measured as its opaque colour, and DarkPaletteService::deriveColorToken() wrote every dark value as an opaque hex, so an overlay like rgba(0, 0, 0, 0.5) became solid in dark mode.

The tests that were red

  • ContrastServiceTest::testFaintTextFailsAfterBlend, testEightDigitHexIsEvaluated, testTranslucentBackgroundIsBlendedOverThePage, testEvaluateBlendsATranslucentCandidate, testParseColorWithAlphaReadsEveryForm
  • DarkPaletteServiceTest::testTranslucentSurfaceKeepsAlpha, testEightDigitHexIsDarkenedAndKeepsAlpha

The fix

  • ContrastService reads #rgba, #rrggbbaa and the rgba() alpha (number or percentage) through a new parseColorWithAlpha(). parseColor() keeps returning the opaque channels for callers that want a colour.
  • A new measure() blends a translucent background over the page background (--nldesign-color-background, else white) and a translucent foreground over that, then takes the ratio. check(), evaluate(), the shipped-set audit, the compliance report and the dark palette's brand-primary check measure through it. Opaque colours measure exactly as before.
  • DarkPaletteService derives the dark channels from the opaque colour as before and keeps a light alpha below 1 as #rrggbbaa. The generator version moves from 2 to 3, and all 46 css/tokens/dark/*.css files are regenerated with the real generator (the equivalent of occ nldesign:generate-dark-variants --force). Installations regenerate on upgrade through the existing repair step, because a v2 header is no longer fresh.

What the regeneration changes, beyond the header: translucent light values now keep their alpha in dark (for example the conduction-new footer text and focus ring), 8-digit hex tokens are now darkened instead of left out, and a few sets gain a token the v2 files did not carry (for example --nldesign-component-heading-color in conduction-new), which is what the current generator produces from unchanged sources.

Not part of this change: hsl()/hsla() are still not parsed (they stay unevaluated, as today), a foreground the dark repair loop rewrites is written opaque, and the token set converter's own ramp and guard helpers still read colours through parseColor(). Blending a translucent primary before it reaches Nextcloud's theming is task 3.3 of the same change.

Verified

  • composer check:strict: exit 0 (its test:all skips in a bare clone, so PHPUnit ran separately).
  • PHPUnit with the server's lib/private on the autoloader: 868 tests, the same 11 errors and 7 failures as on development (missing Symfony and server classes outside a Nextcloud tree). DarkPaletteServiceTest::testRealShippedSetsPassContrastAfterGeneration passes.
  • npm run lint, format, test:l10n, check:l10n-js, check:manifest, test:unit: all exit 0. test:guest-css, test:component-scopes, test:lasuite-tokens: exit 0.
  • Hydra gates --scope-to-diff: exit 0.

Fixes #696

…lpha in dark mode

ContrastService read only #rgb and #rrggbb and dropped the alpha of
rgba(), so an 8-digit hex was 'not a colour' and faint text was measured
as opaque. It now reads #rgba, #rrggbbaa and the rgba() alpha, and the
contrast audit, the shipped-set audit, the compliance report and the dark
palette's brand check measure a translucent background over the page
background and a translucent foreground over that.

DarkPaletteService derived every dark value as an opaque hex and skipped
8-digit hex tokens. The dark channels still come from the opaque colour,
and a light alpha below 1 is kept as #rrggbbaa. The generator version is
now 3 and every css/tokens/dark file is regenerated with it.

Fixes #696
@rubenvdlinde
rubenvdlinde merged commit a85fc76 into development Sep 28, 2026
32 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/thematiq @ 5e051f7

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ✅
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-manifest ✅
test-l10n ✅
format ✅
composer ✅ ✅ 107/107
npm ✅ ✅ 2/2
app:check-code ⏭️
info.xml ✅
REUSE ✅
lockfile sync ✅
PHPUnit ✅
Newman ✅
Playwright ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it on any branch from the Actions tab, or locally with npx playwright test.
Hydra gates ✅

Quality workflow — 2026-09-28 15:30 UTC

Download the full PDF report from the workflow artifacts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant