Fix WCAG AA color contrast violations across all pages
All checks were successful
CI / backend-lint (push) Successful in 9s
CI / actions-lint (push) Successful in 15s
CI / frontend-lint (push) Successful in 21s

Replace incorrect perceived-brightness formula in Stats progress bars
with proper WCAG relative luminance calculation, and convert type bar
colors to hex values for reliable contrast detection. Add light: variant
classes to status badges, yellow/purple text, and admin nav links across
17 files. Darken light-mode status-active token and text-tertiary/muted
tokens. Add aria-labels to admin filter selects and flex-wrap for mobile
overflow on AdminEvolutions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 20:48:16 +01:00
parent a12478f24b
commit 4fbfcf9b29
18 changed files with 109 additions and 80 deletions

View File

@@ -43,12 +43,14 @@ const statusOptions: {
{
value: 'caught',
label: 'Caught',
color: 'bg-green-900/40 text-green-300 border-green-700',
color:
'bg-green-900/40 text-green-300 light:bg-green-100 light:text-green-800 border-green-700 light:border-green-300',
},
{
value: 'fainted',
label: 'Fainted',
color: 'bg-red-900/40 text-red-300 border-red-700',
color:
'bg-red-900/40 text-red-300 light:bg-red-100 light:text-red-800 border-red-700 light:border-red-300',
},
{
value: 'missed',
@@ -299,7 +301,7 @@ export function EncounterModal({
setSelectedPokemon(pickRandomPokemon(routePokemon, dupedPokemonIds))
}
}}
className="px-2.5 py-1 text-xs font-medium rounded-lg border border-purple-600 text-purple-400 hover:bg-purple-900/20 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
className="px-2.5 py-1 text-xs font-medium rounded-lg border border-purple-600 text-purple-400 light:text-purple-700 light:border-purple-500 hover:bg-purple-900/20 light:hover:bg-purple-50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
{selectedPokemon ? 'Re-roll' : 'Randomize'}
</button>
@@ -403,14 +405,14 @@ export function EncounterModal({
<EncounterMethodBadge method={rp.encounterMethod} />
)}
{!isDuped && displayRate !== null && displayRate !== undefined && (
<span className="text-[10px] text-purple-400 font-medium">
<span className="text-[10px] text-purple-400 light:text-purple-700 font-medium">
{displayRate}%
</span>
)}
{!isDuped &&
selectedCondition === null &&
conditions.length > 0 && (
<span className="text-[10px] text-purple-400">
<span className="text-[10px] text-purple-400 light:text-purple-700">
{conditions.join(', ')}
</span>
)}
@@ -518,7 +520,7 @@ export function EncounterModal({
onClick={() => setNickname(name)}
className={`px-2.5 py-1 text-xs rounded-full border transition-colors ${
nickname === name
? 'bg-accent-900/40 border-accent-600 text-accent-300'
? 'bg-accent-900/40 border-accent-600 text-accent-300 light:bg-accent-100 light:text-accent-700'
: 'border-border-default text-text-secondary hover:border-accent-600 hover:bg-accent-900/20'
}`}
>

View File

@@ -48,7 +48,7 @@ function GraveyardCard({ entry }: { entry: GraveyardEntry }) {
<div className="text-xs text-text-muted mt-0.5">{entry.routeName}</div>
<div className="text-[10px] text-purple-400 mt-0.5 font-medium">
<div className="text-[10px] text-purple-400 light:text-purple-700 mt-0.5 font-medium">
Leg {entry.legOrder} &mdash; {entry.gameName}
</div>

View File

@@ -48,18 +48,18 @@ function LegDot({ leg }: { leg: LineageLegEntry }) {
<div
className={`font-medium ${
leg.faintLevel !== null
? 'text-red-300'
? 'text-red-300 light:text-red-700'
: leg.wasTransferred
? 'text-blue-300'
? 'text-blue-300 light:text-blue-700'
: leg.enteredHof
? 'text-yellow-300'
: 'text-green-300'
? 'text-yellow-300 light:text-amber-700'
: 'text-green-300 light:text-green-700'
}`}
>
{label}
</div>
{leg.enteredHof && leg.faintLevel === null && (
<div className="text-yellow-300">Hall of Fame</div>
<div className="text-yellow-300 light:text-amber-700">Hall of Fame</div>
)}
</div>
<div className="w-2 h-2 bg-gray-900 dark:bg-gray-700 rotate-45 -mt-1" />
@@ -156,8 +156,8 @@ function LineageCard({ lineage, allLegOrders }: { lineage: LineageEntry; allLegO
<span
className={`px-2.5 py-1 rounded-full text-xs font-medium ${
lineage.status === 'alive'
? 'bg-green-900/40 text-green-300'
: 'bg-red-900/40 text-red-300'
? 'bg-green-900/40 text-green-300 light:bg-green-100 light:text-green-800'
: 'bg-red-900/40 text-red-300 light:bg-red-100 light:text-red-800'
}`}
>
{lineage.status === 'alive' ? 'Alive' : 'Dead'}

View File

@@ -9,7 +9,7 @@ interface ShinyBoxProps {
export function ShinyBox({ encounters, onEncounterClick }: ShinyBoxProps) {
return (
<div className="border-2 border-yellow-600 rounded-lg p-4">
<h3 className="text-sm font-semibold text-yellow-400 mb-3 flex items-center gap-1.5">
<h3 className="text-sm font-semibold text-yellow-400 light:text-amber-700 mb-3 flex items-center gap-1.5">
<span>&#10022;</span>
Shiny Box
<span className="text-xs font-normal text-text-muted ml-1">

View File

@@ -110,7 +110,7 @@ export function ShinyEncounterModal({
</svg>
</button>
</div>
<p className="text-sm text-yellow-400 mt-1">
<p className="text-sm text-yellow-400 light:text-amber-700 mt-1">
Shiny catches bypass the one-per-route rule
</p>
</div>

View File

@@ -21,7 +21,9 @@ export function AdminLayout() {
to={item.to}
className={({ isActive }) =>
`block px-3 py-2 rounded-md text-sm font-medium whitespace-nowrap ${
isActive ? 'bg-accent-900/40 text-accent-300' : 'hover:bg-surface-2'
isActive
? 'bg-accent-900/40 text-accent-300 light:bg-accent-100 light:text-accent-700'
: 'hover:bg-surface-2'
}`
}
>

View File

@@ -77,7 +77,7 @@ export function BulkImportModal({
)}
{result && (
<div className="p-3 bg-green-900/30 text-green-300 rounded-md text-sm">
<div className="p-3 bg-green-900/30 text-green-300 light:bg-green-100 light:text-green-800 rounded-md text-sm">
<p>
{createdLabel}: {result.created}, {updatedLabel}: {result.updated}
</p>