mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-10-17 05:14:19 +02:00
fix(ui): restore dark mode contrast
This commit is contained in:
parent
52d12a222e
commit
8d821f3ada
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
77
admin/tests/unit/dark_mode_contrast.spec.ts
Normal file
77
admin/tests/unit/dark_mode_contrast.spec.ts
Normal 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'
|
||||
)
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user