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

@@ -0,0 +1,9 @@
---
# nuzlocke-tracker-1qzo
title: Fix WCAG AA color contrast violations
status: completed
type: bug
priority: high
created_at: 2026-02-20T19:19:32Z
updated_at: 2026-02-20T19:20:25Z
---

View File

@@ -44,12 +44,16 @@ for (const theme of themes) {
id: v.id, id: v.id,
impact: v.impact, impact: v.impact,
description: v.description, description: v.description,
nodes: v.nodes.length, nodes: v.nodes.map((n) => ({
html: n.html,
target: n.target,
failureSummary: n.failureSummary,
})),
})) }))
expect( expect(
violations, violations,
`${name} (${theme}): ${violations.length} accessibility violations found:\n${JSON.stringify(violations, null, 2)}`, `${name} (${theme}): ${violations.length} violation(s):\n${JSON.stringify(violations, null, 2)}`,
).toHaveLength(0) ).toHaveLength(0)
}) })
} }

View File

@@ -43,12 +43,14 @@ const statusOptions: {
{ {
value: 'caught', value: 'caught',
label: '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', value: 'fainted',
label: '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', value: 'missed',
@@ -299,7 +301,7 @@ export function EncounterModal({
setSelectedPokemon(pickRandomPokemon(routePokemon, dupedPokemonIds)) 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'} {selectedPokemon ? 'Re-roll' : 'Randomize'}
</button> </button>
@@ -403,14 +405,14 @@ export function EncounterModal({
<EncounterMethodBadge method={rp.encounterMethod} /> <EncounterMethodBadge method={rp.encounterMethod} />
)} )}
{!isDuped && displayRate !== null && displayRate !== undefined && ( {!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}% {displayRate}%
</span> </span>
)} )}
{!isDuped && {!isDuped &&
selectedCondition === null && selectedCondition === null &&
conditions.length > 0 && ( conditions.length > 0 && (
<span className="text-[10px] text-purple-400"> <span className="text-[10px] text-purple-400 light:text-purple-700">
{conditions.join(', ')} {conditions.join(', ')}
</span> </span>
)} )}
@@ -518,7 +520,7 @@ export function EncounterModal({
onClick={() => setNickname(name)} onClick={() => setNickname(name)}
className={`px-2.5 py-1 text-xs rounded-full border transition-colors ${ className={`px-2.5 py-1 text-xs rounded-full border transition-colors ${
nickname === name 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' : '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-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} Leg {entry.legOrder} &mdash; {entry.gameName}
</div> </div>

View File

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

View File

@@ -9,7 +9,7 @@ interface ShinyBoxProps {
export function ShinyBox({ encounters, onEncounterClick }: ShinyBoxProps) { export function ShinyBox({ encounters, onEncounterClick }: ShinyBoxProps) {
return ( return (
<div className="border-2 border-yellow-600 rounded-lg p-4"> <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> <span>&#10022;</span>
Shiny Box Shiny Box
<span className="text-xs font-normal text-text-muted ml-1"> <span className="text-xs font-normal text-text-muted ml-1">

View File

@@ -110,7 +110,7 @@ export function ShinyEncounterModal({
</svg> </svg>
</button> </button>
</div> </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 Shiny catches bypass the one-per-route rule
</p> </p>
</div> </div>

View File

@@ -21,7 +21,9 @@ export function AdminLayout() {
to={item.to} to={item.to}
className={({ isActive }) => className={({ isActive }) =>
`block px-3 py-2 rounded-md text-sm font-medium whitespace-nowrap ${ `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 && ( {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> <p>
{createdLabel}: {result.created}, {updatedLabel}: {result.updated} {createdLabel}: {result.created}, {updatedLabel}: {result.updated}
</p> </p>

View File

@@ -46,8 +46,9 @@
/* Text on dark */ /* Text on dark */
--color-text-primary: #e6edf3; --color-text-primary: #e6edf3;
--color-text-secondary: #7d8590; --color-text-secondary: #9198a1;
--color-text-tertiary: #484f58; --color-text-tertiary: #8b949e;
--color-text-muted: #8b949e;
--color-text-link: #7eb0ce; --color-text-link: #7eb0ce;
/* Borders */ /* Borders */
@@ -90,7 +91,8 @@ html[data-theme='light'] {
/* Text */ /* Text */
--color-text-primary: #1f2328; --color-text-primary: #1f2328;
--color-text-secondary: #656d76; --color-text-secondary: #656d76;
--color-text-tertiary: #8b949e; --color-text-tertiary: #596069;
--color-text-muted: #596069;
--color-text-link: #1a5068; --color-text-link: #1a5068;
/* Borders */ /* Borders */
@@ -103,8 +105,8 @@ html[data-theme='light'] {
--color-status-alive-bg: rgba(26, 127, 55, 0.1); --color-status-alive-bg: rgba(26, 127, 55, 0.1);
--color-status-dead: #cf222e; --color-status-dead: #cf222e;
--color-status-dead-bg: rgba(207, 34, 46, 0.1); --color-status-dead-bg: rgba(207, 34, 46, 0.1);
--color-status-active: #1a7f37; --color-status-active: #116b2b;
--color-status-active-bg: rgba(26, 127, 55, 0.1); --color-status-active-bg: rgba(17, 107, 43, 0.08);
--color-status-completed: #0969da; --color-status-completed: #0969da;
--color-status-completed-bg: rgba(9, 105, 218, 0.1); --color-status-completed-bg: rgba(9, 105, 218, 0.1);
--color-status-failed: #cf222e; --color-status-failed: #cf222e;

View File

@@ -18,9 +18,9 @@ const statusRing: Record<RunStatus, string> = {
} }
const statusStyles: Record<RunStatus, string> = { const statusStyles: Record<RunStatus, string> = {
active: 'bg-green-900/40 text-green-300', active: 'bg-green-900/40 text-green-300 light:bg-green-100 light:text-green-800',
completed: 'bg-blue-900/40 text-blue-300', completed: 'bg-blue-900/40 text-blue-300 light:bg-blue-100 light:text-blue-800',
failed: 'bg-red-900/40 text-red-300', failed: 'bg-red-900/40 text-red-300 light:bg-red-100 light:text-red-800',
} }
function LegIndicator({ leg }: { leg: GenlockeLegDetail }) { function LegIndicator({ leg }: { leg: GenlockeLegDetail }) {
@@ -270,7 +270,7 @@ export function GenlockeDetail() {
className={`px-4 py-2 rounded-lg font-medium transition-colors ${ className={`px-4 py-2 rounded-lg font-medium transition-colors ${
showGraveyard showGraveyard
? 'bg-red-600 text-white hover:bg-red-700' ? 'bg-red-600 text-white hover:bg-red-700'
: 'bg-surface-3 text-text-secondary hover:bg-surface-4' : 'bg-surface-3 text-text-secondary light:text-text-primary hover:bg-surface-4'
}`} }`}
> >
Graveyard Graveyard
@@ -280,7 +280,7 @@ export function GenlockeDetail() {
className={`px-4 py-2 rounded-lg font-medium transition-colors ${ className={`px-4 py-2 rounded-lg font-medium transition-colors ${
showLineage showLineage
? 'bg-accent-600 text-white hover:bg-accent-500' ? 'bg-accent-600 text-white hover:bg-accent-500'
: 'bg-surface-3 text-text-secondary hover:bg-surface-4' : 'bg-surface-3 text-text-secondary light:text-text-primary hover:bg-surface-4'
}`} }`}
> >
Lineage Lineage

View File

@@ -3,9 +3,9 @@ import { useGenlockes } from '../hooks/useGenlockes'
import type { RunStatus } from '../types' import type { RunStatus } from '../types'
const statusStyles: Record<RunStatus, string> = { const statusStyles: Record<RunStatus, string> = {
active: 'bg-green-900/40 text-green-300', active: 'bg-green-900/40 text-green-300 light:bg-green-100 light:text-green-800',
completed: 'bg-blue-900/40 text-blue-300', completed: 'bg-blue-900/40 text-blue-300 light:bg-blue-100 light:text-blue-800',
failed: 'bg-red-900/40 text-red-300', failed: 'bg-red-900/40 text-red-300 light:bg-red-100 light:text-red-800',
} }
export function GenlockeList() { export function GenlockeList() {

View File

@@ -31,9 +31,9 @@ function sortEncounters(encounters: EncounterDetail[], key: TeamSortKey): Encoun
} }
const statusStyles: Record<RunStatus, string> = { const statusStyles: Record<RunStatus, string> = {
active: 'bg-green-900/40 text-green-300', active: 'bg-green-900/40 text-green-300 light:bg-green-100 light:text-green-800',
completed: 'bg-blue-900/40 text-blue-300', completed: 'bg-blue-900/40 text-blue-300 light:bg-blue-100 light:text-blue-800',
failed: 'bg-red-900/40 text-red-300', failed: 'bg-red-900/40 text-red-300 light:bg-red-100 light:text-red-800',
} }
function formatDuration(start: string, end: string) { function formatDuration(start: string, end: string) {

View File

@@ -59,9 +59,9 @@ function sortEncounters(encounters: EncounterDetail[], key: TeamSortKey): Encoun
} }
const statusStyles: Record<RunStatus, string> = { const statusStyles: Record<RunStatus, string> = {
active: 'bg-green-900/40 text-green-300', active: 'bg-green-900/40 text-green-300 light:bg-green-100 light:text-green-800',
completed: 'bg-blue-900/40 text-blue-300', completed: 'bg-blue-900/40 text-blue-300 light:bg-blue-100 light:text-blue-800',
failed: 'bg-red-900/40 text-red-300', failed: 'bg-red-900/40 text-red-300 light:bg-red-100 light:text-red-800',
} }
function formatDuration(start: string, end: string) { function formatDuration(start: string, end: string) {
@@ -801,7 +801,7 @@ export function RunEncounters() {
})} })}
</p> </p>
{run.genlocke && ( {run.genlocke && (
<p className="text-sm text-purple-400 mt-1 font-medium"> <p className="text-sm text-purple-400 light:text-purple-700 mt-1 font-medium">
Leg {run.genlocke.legOrder} of {run.genlocke.totalLegs} &mdash;{' '} Leg {run.genlocke.legOrder} of {run.genlocke.totalLegs} &mdash;{' '}
{run.genlocke.genlockeName} {run.genlocke.genlockeName}
</p> </p>
@@ -811,7 +811,7 @@ export function RunEncounters() {
{isActive && run.rules?.shinyClause && ( {isActive && run.rules?.shinyClause && (
<button <button
onClick={() => setShowShinyModal(true)} onClick={() => setShowShinyModal(true)}
className="px-3 py-1 text-sm border border-yellow-600 text-yellow-400 rounded-full font-medium hover:bg-yellow-900/20 transition-colors" className="px-3 py-1 text-sm border border-yellow-600 text-yellow-400 light:text-amber-700 light:border-amber-600 rounded-full font-medium hover:bg-yellow-900/20 light:hover:bg-amber-50 transition-colors"
> >
&#10022; Log Shiny &#10022; Log Shiny
</button> </button>
@@ -1153,7 +1153,7 @@ export function RunEncounters() {
bulkRandomize.mutate() bulkRandomize.mutate()
} }
}} }}
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"
> >
{bulkRandomize.isPending ? 'Randomizing...' : 'Randomize All'} {bulkRandomize.isPending ? 'Randomizing...' : 'Randomize All'}
</button> </button>
@@ -1358,7 +1358,7 @@ export function RunEncounters() {
</div> </div>
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
{isDefeated ? ( {isDefeated ? (
<span className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-full bg-green-900/40 text-green-300"> <span className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-full bg-green-900/40 text-green-300 light:bg-green-100 light:text-green-800">
Defeated &#10003; Defeated &#10003;
</span> </span>
) : isActive ? ( ) : isActive ? (

View File

@@ -4,24 +4,24 @@ import { StatCard } from '../components'
import type { PokemonRanking, StatsResponse } from '../types/stats' import type { PokemonRanking, StatsResponse } from '../types/stats'
const typeBarColors: Record<string, string> = { const typeBarColors: Record<string, string> = {
normal: 'bg-gray-400', normal: '#9ca3af',
fire: 'bg-red-500', fire: '#ef4444',
water: 'bg-blue-500', water: '#3b82f6',
electric: 'bg-yellow-400', electric: '#facc15',
grass: 'bg-green-500', grass: '#22c55e',
ice: 'bg-cyan-300', ice: '#67e8f9',
fighting: 'bg-red-700', fighting: '#b91c1c',
poison: 'bg-purple-500', poison: '#a855f7',
ground: 'bg-amber-600', ground: '#d97706',
flying: 'bg-indigo-300', flying: '#a5b4fc',
psychic: 'bg-pink-500', psychic: '#ec4899',
bug: 'bg-lime-500', bug: '#84cc16',
rock: 'bg-amber-700', rock: '#b45309',
ghost: 'bg-purple-700', ghost: '#7e22ce',
dragon: 'bg-indigo-600', dragon: '#4f46e5',
dark: 'bg-gray-700', dark: '#374151',
steel: 'bg-gray-400', steel: '#9ca3af',
fairy: 'bg-pink-300', fairy: '#f9a8d4',
} }
function fmt(value: number | null, suffix = ''): string { function fmt(value: number | null, suffix = ''): string {
@@ -74,44 +74,50 @@ function PokemonList({ title, pokemon }: { title: string; pokemon: PokemonRankin
) )
} }
function hexLuminance(hex: string): number { function srgbLuminance(hex: string): number {
const r = parseInt(hex.slice(1, 3), 16) const toLinear = (c: number) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)
const g = parseInt(hex.slice(3, 5), 16) const r = toLinear(parseInt(hex.slice(1, 3), 16) / 255)
const b = parseInt(hex.slice(5, 7), 16) const g = toLinear(parseInt(hex.slice(3, 5), 16) / 255)
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 const b = toLinear(parseInt(hex.slice(5, 7), 16) / 255)
return 0.2126 * r + 0.7152 * g + 0.0722 * b
}
function shouldUseDarkText(bgHex: string): boolean {
const bgL = srgbLuminance(bgHex)
const whiteContrast = 1.05 / (bgL + 0.05)
const blackContrast = (bgL + 0.05) / 0.05
return blackContrast > whiteContrast
} }
function HorizontalBar({ function HorizontalBar({
label, label,
value, value,
max, max,
color,
colorHex, colorHex,
}: { }: {
label: string label: string
value: number value: number
max: number max: number
color?: string colorHex: string
colorHex?: string
}) { }) {
const width = max > 0 ? (value / max) * 100 : 0 const width = max > 0 ? (value / max) * 100 : 0
const isLight = colorHex ? hexLuminance(colorHex) > 0.55 : false const useDark = shouldUseDarkText(colorHex)
return ( return (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<div className="flex-1 bg-surface-2 rounded-full h-6 overflow-hidden relative"> <div className="flex-1 bg-surface-2 rounded-full h-6 overflow-hidden relative">
<div <div
className={`h-full rounded-full ${color ?? ''}`} className="h-full rounded-full"
style={{ style={{
width: `${Math.max(width, 1)}%`, width: `${Math.max(width, 1)}%`,
...(colorHex ? { backgroundColor: colorHex } : {}), backgroundColor: colorHex,
}} }}
/> />
<span <span
className={`absolute inset-0 flex items-center px-3 text-xs font-medium capitalize truncate ${ className={`absolute inset-0 flex items-center px-3 text-xs font-medium capitalize truncate ${
isLight ? 'text-gray-900 dark:text-gray-900' : 'text-text-primary' useDark ? 'text-gray-900' : 'text-white'
}`} }`}
style={{ style={{
textShadow: isLight ? '0 0 4px rgba(255,255,255,0.8)' : '0 0 4px rgba(0,0,0,0.3)', textShadow: useDark ? '0 0 4px rgba(255,255,255,0.8)' : '0 0 4px rgba(0,0,0,0.5)',
}} }}
> >
{label} {label}
@@ -166,7 +172,7 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
label={g.gameName} label={g.gameName}
value={g.count} value={g.count}
max={gameMax} max={gameMax}
{...(g.gameColor ? { colorHex: g.gameColor } : { color: 'bg-blue-500' })} colorHex={g.gameColor ?? '#3b82f6'}
/> />
))} ))}
</div> </div>
@@ -258,7 +264,7 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
label={t.type} label={t.type}
value={t.count} value={t.count}
max={typeMax} max={typeMax}
color={typeBarColors[t.type] ?? 'bg-gray-500'} colorHex={typeBarColors[t.type] ?? '#6b7280'}
/> />
))} ))}
</div> </div>

View File

@@ -75,9 +75,9 @@ export function AdminEvolutions() {
return ( return (
<div> <div>
<div className="flex justify-between items-center mb-4"> <div className="flex flex-wrap justify-between items-center gap-2 mb-4">
<h2 className="text-xl font-semibold">Evolutions</h2> <h2 className="text-xl font-semibold">Evolutions</h2>
<div className="flex gap-2"> <div className="flex flex-wrap gap-2">
<button <button
onClick={async () => { onClick={async () => {
const data = await exportEvolutions() const data = await exportEvolutions()
@@ -102,7 +102,7 @@ export function AdminEvolutions() {
</div> </div>
</div> </div>
<div className="mb-4 flex items-center gap-4"> <div className="mb-4 flex flex-wrap items-center gap-4">
<input <input
type="text" type="text"
value={search} value={search}
@@ -114,6 +114,7 @@ export function AdminEvolutions() {
className="w-full max-w-sm px-3 py-2 border rounded-md bg-surface-2 border-border-default" className="w-full max-w-sm px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/> />
<select <select
aria-label="Filter by trigger"
value={triggerFilter} value={triggerFilter}
onChange={(e) => { onChange={(e) => {
setTriggerFilter(e.target.value) setTriggerFilter(e.target.value)

View File

@@ -70,8 +70,9 @@ export function AdminGames() {
</div> </div>
</div> </div>
<div className="mb-4 flex items-center gap-4"> <div className="mb-4 flex flex-wrap items-center gap-4">
<select <select
aria-label="Filter by region"
value={regionFilter} value={regionFilter}
onChange={(e) => setRegionFilter(e.target.value)} onChange={(e) => setRegionFilter(e.target.value)}
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default" className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
@@ -84,6 +85,7 @@ export function AdminGames() {
))} ))}
</select> </select>
<select <select
aria-label="Filter by generation"
value={genFilter} value={genFilter}
onChange={(e) => setGenFilter(e.target.value)} onChange={(e) => setGenFilter(e.target.value)}
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default" className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"

View File

@@ -116,6 +116,7 @@ export function AdminPokemon() {
className="w-full max-w-sm px-3 py-2 border rounded-md bg-surface-2 border-border-default" className="w-full max-w-sm px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/> />
<select <select
aria-label="Filter by type"
value={typeFilter} value={typeFilter}
onChange={(e) => { onChange={(e) => {
setTypeFilter(e.target.value) setTypeFilter(e.target.value)