fix(ui): restore dark mode contrast

This commit is contained in:
Tiancheng-Xu 2026-09-21 19:52:39 -04:00 committed by Jake Turner
parent 52d12a222e
commit 8d821f3ada
3 changed files with 87 additions and 4 deletions

View File

@ -81,7 +81,7 @@ export default function BuilderTagSelector({
value={adjective}
onChange={(e) => handleAdjectiveChange(e.target.value)}
disabled={disabled}
className="px-3 py-2 bg-desert-stone-lighter border border-desert-stone-light rounded-lg text-desert-green font-medium focus:outline-none focus:ring-2 focus:ring-desert-green disabled:opacity-50"
className="px-3 py-2 bg-desert-stone-lighter border border-desert-stone-light rounded-lg text-text-primary font-medium focus:outline-none focus:ring-2 focus:ring-desert-green disabled:opacity-50"
>
{ADJECTIVES.map((adj) => (
<option key={adj} value={adj}>
@ -96,7 +96,7 @@ export default function BuilderTagSelector({
value={noun}
onChange={(e) => handleNounChange(e.target.value)}
disabled={disabled}
className="px-3 py-2 bg-desert-stone-lighter border border-desert-stone-light rounded-lg text-desert-green font-medium focus:outline-none focus:ring-2 focus:ring-desert-green disabled:opacity-50"
className="px-3 py-2 bg-desert-stone-lighter border border-desert-stone-light rounded-lg text-text-primary font-medium focus:outline-none focus:ring-2 focus:ring-desert-green disabled:opacity-50"
>
{NOUNS.map((n) => (
<option key={n} value={n}>
@ -107,7 +107,7 @@ export default function BuilderTagSelector({
<span className="text-desert-stone-dark font-bold">-</span>
<span className="px-3 py-2 bg-desert-stone-lighter border border-desert-stone-light rounded-lg text-desert-green font-mono font-bold">
<span className="px-3 py-2 bg-desert-stone-lighter border border-desert-stone-light rounded-lg text-text-primary font-mono font-bold">
{number}
</span>

View File

@ -66,8 +66,9 @@ body {
--color-desert-green-darker: #f7eedc;
--color-desert-green-dark: #e8dfc8;
/* Accent green: slightly brighter for dark bg visibility */
/* Accent green remains dark enough for white text on buttons */
--color-desert-green: #525530;
--color-desert-green-foreground: #babaaa;
/* Light variants → dark variants (hover bg, disabled states) */
--color-desert-green-light: #3a3c24;
@ -120,6 +121,11 @@ body {
color-scheme: dark;
}
/* Keep foreground accents readable without lightening green button backgrounds. */
[data-theme="dark"] .text-desert-green {
color: var(--color-desert-green-foreground);
}
/* MapLibre popup styling for dark mode */
[data-theme="dark"] .maplibregl-popup-content {
background: #2a2918;

View File

@ -0,0 +1,77 @@
import * as assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { test } from 'node:test'
const css = await readFile(new URL('../../inertia/css/app.css', import.meta.url), 'utf8')
const builderTagSelector = await readFile(
new URL('../../inertia/components/BuilderTagSelector.tsx', import.meta.url),
'utf8'
)
const darkTheme = css.match(/\[data-theme="dark"\]\s*\{(?<tokens>[\s\S]*?)\n\}/)?.groups?.tokens
if (!darkTheme) {
throw new Error('dark theme token block must exist')
}
function token(name: string) {
const value = darkTheme!.match(new RegExp(`${name}:\\s*(#[0-9a-f]{6})`, 'i'))?.[1]
assert.ok(value, `${name} must be defined in the dark theme`)
return value
}
function luminance(hex: string) {
const channels = hex
.slice(1)
.match(/.{2}/g)!
.map((channel) => Number.parseInt(channel, 16) / 255)
.map((channel) => (channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4))
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722
}
function contrast(foreground: string, background: string) {
const foregroundLuminance = luminance(foreground)
const backgroundLuminance = luminance(background)
const lighter = Math.max(foregroundLuminance, backgroundLuminance)
const darker = Math.min(foregroundLuminance, backgroundLuminance)
return (lighter + 0.05) / (darker + 0.05)
}
test('dark accent text clears WCAG AA without changing the accent background', () => {
const accent = token('--color-desert-green-foreground')
const surfaces = [
token('--color-desert-sand'),
token('--color-desert-white'),
token('--color-surface-secondary'),
]
for (const surface of surfaces) {
assert.ok(
contrast(accent, surface) >= 4.5,
`${accent} on ${surface} must have at least 4.5:1 contrast`
)
}
assert.match(
css,
/\[data-theme="dark"\] \.text-desert-green\s*\{\s*color:\s*var\(--color-desert-green-foreground\)/,
'dark text-desert-green utilities must use the foreground-only accent token'
)
assert.ok(
contrast('#ffffff', token('--color-desert-green')) >= 4.5,
'the unchanged accent background must retain readable white text'
)
})
test('Builder Tag controls pair their inverted background with semantic text', () => {
const controls = builderTagSelector.match(
/className="[^"]*bg-desert-stone-lighter[^"]*text-text-primary[^"]*"/g
)
assert.equal(controls?.length, 3, 'the two selects and number should use semantic text')
assert.ok(
contrast(token('--color-text-primary'), token('--color-desert-stone-lighter')) >= 4.5,
'Builder Tag text and background must have at least 4.5:1 contrast'
)
})