Release: test infrastructure, rules overhaul, and design refresh #30
@@ -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
|
||||
---
|
||||
@@ -44,12 +44,16 @@ for (const theme of themes) {
|
||||
id: v.id,
|
||||
impact: v.impact,
|
||||
description: v.description,
|
||||
nodes: v.nodes.length,
|
||||
nodes: v.nodes.map((n) => ({
|
||||
html: n.html,
|
||||
target: n.target,
|
||||
failureSummary: n.failureSummary,
|
||||
})),
|
||||
}))
|
||||
|
||||
expect(
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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} — {entry.gameName}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>✦</span>
|
||||
Shiny Box
|
||||
<span className="text-xs font-normal text-text-muted ml-1">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
}`
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -46,8 +46,9 @@
|
||||
|
||||
/* Text on dark */
|
||||
--color-text-primary: #e6edf3;
|
||||
--color-text-secondary: #7d8590;
|
||||
--color-text-tertiary: #484f58;
|
||||
--color-text-secondary: #9198a1;
|
||||
--color-text-tertiary: #8b949e;
|
||||
--color-text-muted: #8b949e;
|
||||
--color-text-link: #7eb0ce;
|
||||
|
||||
/* Borders */
|
||||
@@ -90,7 +91,8 @@ html[data-theme='light'] {
|
||||
/* Text */
|
||||
--color-text-primary: #1f2328;
|
||||
--color-text-secondary: #656d76;
|
||||
--color-text-tertiary: #8b949e;
|
||||
--color-text-tertiary: #596069;
|
||||
--color-text-muted: #596069;
|
||||
--color-text-link: #1a5068;
|
||||
|
||||
/* Borders */
|
||||
@@ -103,8 +105,8 @@ html[data-theme='light'] {
|
||||
--color-status-alive-bg: rgba(26, 127, 55, 0.1);
|
||||
--color-status-dead: #cf222e;
|
||||
--color-status-dead-bg: rgba(207, 34, 46, 0.1);
|
||||
--color-status-active: #1a7f37;
|
||||
--color-status-active-bg: rgba(26, 127, 55, 0.1);
|
||||
--color-status-active: #116b2b;
|
||||
--color-status-active-bg: rgba(17, 107, 43, 0.08);
|
||||
--color-status-completed: #0969da;
|
||||
--color-status-completed-bg: rgba(9, 105, 218, 0.1);
|
||||
--color-status-failed: #cf222e;
|
||||
|
||||
@@ -18,9 +18,9 @@ const statusRing: Record<RunStatus, string> = {
|
||||
}
|
||||
|
||||
const statusStyles: Record<RunStatus, string> = {
|
||||
active: 'bg-green-900/40 text-green-300',
|
||||
completed: 'bg-blue-900/40 text-blue-300',
|
||||
failed: 'bg-red-900/40 text-red-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 light:bg-blue-100 light:text-blue-800',
|
||||
failed: 'bg-red-900/40 text-red-300 light:bg-red-100 light:text-red-800',
|
||||
}
|
||||
|
||||
function LegIndicator({ leg }: { leg: GenlockeLegDetail }) {
|
||||
@@ -270,7 +270,7 @@ export function GenlockeDetail() {
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
showGraveyard
|
||||
? '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
|
||||
@@ -280,7 +280,7 @@ export function GenlockeDetail() {
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
showLineage
|
||||
? '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
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useGenlockes } from '../hooks/useGenlockes'
|
||||
import type { RunStatus } from '../types'
|
||||
|
||||
const statusStyles: Record<RunStatus, string> = {
|
||||
active: 'bg-green-900/40 text-green-300',
|
||||
completed: 'bg-blue-900/40 text-blue-300',
|
||||
failed: 'bg-red-900/40 text-red-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 light:bg-blue-100 light:text-blue-800',
|
||||
failed: 'bg-red-900/40 text-red-300 light:bg-red-100 light:text-red-800',
|
||||
}
|
||||
|
||||
export function GenlockeList() {
|
||||
|
||||
@@ -31,9 +31,9 @@ function sortEncounters(encounters: EncounterDetail[], key: TeamSortKey): Encoun
|
||||
}
|
||||
|
||||
const statusStyles: Record<RunStatus, string> = {
|
||||
active: 'bg-green-900/40 text-green-300',
|
||||
completed: 'bg-blue-900/40 text-blue-300',
|
||||
failed: 'bg-red-900/40 text-red-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 light:bg-blue-100 light:text-blue-800',
|
||||
failed: 'bg-red-900/40 text-red-300 light:bg-red-100 light:text-red-800',
|
||||
}
|
||||
|
||||
function formatDuration(start: string, end: string) {
|
||||
|
||||
@@ -59,9 +59,9 @@ function sortEncounters(encounters: EncounterDetail[], key: TeamSortKey): Encoun
|
||||
}
|
||||
|
||||
const statusStyles: Record<RunStatus, string> = {
|
||||
active: 'bg-green-900/40 text-green-300',
|
||||
completed: 'bg-blue-900/40 text-blue-300',
|
||||
failed: 'bg-red-900/40 text-red-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 light:bg-blue-100 light:text-blue-800',
|
||||
failed: 'bg-red-900/40 text-red-300 light:bg-red-100 light:text-red-800',
|
||||
}
|
||||
|
||||
function formatDuration(start: string, end: string) {
|
||||
@@ -801,7 +801,7 @@ export function RunEncounters() {
|
||||
})}
|
||||
</p>
|
||||
{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} —{' '}
|
||||
{run.genlocke.genlockeName}
|
||||
</p>
|
||||
@@ -811,7 +811,7 @@ export function RunEncounters() {
|
||||
{isActive && run.rules?.shinyClause && (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
✦ Log Shiny
|
||||
</button>
|
||||
@@ -1153,7 +1153,7 @@ export function RunEncounters() {
|
||||
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'}
|
||||
</button>
|
||||
@@ -1358,7 +1358,7 @@ export function RunEncounters() {
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
{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 ✓
|
||||
</span>
|
||||
) : isActive ? (
|
||||
|
||||
@@ -4,24 +4,24 @@ import { StatCard } from '../components'
|
||||
import type { PokemonRanking, StatsResponse } from '../types/stats'
|
||||
|
||||
const typeBarColors: Record<string, string> = {
|
||||
normal: 'bg-gray-400',
|
||||
fire: 'bg-red-500',
|
||||
water: 'bg-blue-500',
|
||||
electric: 'bg-yellow-400',
|
||||
grass: 'bg-green-500',
|
||||
ice: 'bg-cyan-300',
|
||||
fighting: 'bg-red-700',
|
||||
poison: 'bg-purple-500',
|
||||
ground: 'bg-amber-600',
|
||||
flying: 'bg-indigo-300',
|
||||
psychic: 'bg-pink-500',
|
||||
bug: 'bg-lime-500',
|
||||
rock: 'bg-amber-700',
|
||||
ghost: 'bg-purple-700',
|
||||
dragon: 'bg-indigo-600',
|
||||
dark: 'bg-gray-700',
|
||||
steel: 'bg-gray-400',
|
||||
fairy: 'bg-pink-300',
|
||||
normal: '#9ca3af',
|
||||
fire: '#ef4444',
|
||||
water: '#3b82f6',
|
||||
electric: '#facc15',
|
||||
grass: '#22c55e',
|
||||
ice: '#67e8f9',
|
||||
fighting: '#b91c1c',
|
||||
poison: '#a855f7',
|
||||
ground: '#d97706',
|
||||
flying: '#a5b4fc',
|
||||
psychic: '#ec4899',
|
||||
bug: '#84cc16',
|
||||
rock: '#b45309',
|
||||
ghost: '#7e22ce',
|
||||
dragon: '#4f46e5',
|
||||
dark: '#374151',
|
||||
steel: '#9ca3af',
|
||||
fairy: '#f9a8d4',
|
||||
}
|
||||
|
||||
function fmt(value: number | null, suffix = ''): string {
|
||||
@@ -74,44 +74,50 @@ function PokemonList({ title, pokemon }: { title: string; pokemon: PokemonRankin
|
||||
)
|
||||
}
|
||||
|
||||
function hexLuminance(hex: string): number {
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
return (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
||||
function srgbLuminance(hex: string): number {
|
||||
const toLinear = (c: number) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)
|
||||
const r = toLinear(parseInt(hex.slice(1, 3), 16) / 255)
|
||||
const g = toLinear(parseInt(hex.slice(3, 5), 16) / 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({
|
||||
label,
|
||||
value,
|
||||
max,
|
||||
color,
|
||||
colorHex,
|
||||
}: {
|
||||
label: string
|
||||
value: number
|
||||
max: number
|
||||
color?: string
|
||||
colorHex?: string
|
||||
colorHex: string
|
||||
}) {
|
||||
const width = max > 0 ? (value / max) * 100 : 0
|
||||
const isLight = colorHex ? hexLuminance(colorHex) > 0.55 : false
|
||||
const useDark = shouldUseDarkText(colorHex)
|
||||
return (
|
||||
<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={`h-full rounded-full ${color ?? ''}`}
|
||||
className="h-full rounded-full"
|
||||
style={{
|
||||
width: `${Math.max(width, 1)}%`,
|
||||
...(colorHex ? { backgroundColor: colorHex } : {}),
|
||||
backgroundColor: colorHex,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
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={{
|
||||
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}
|
||||
@@ -166,7 +172,7 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
|
||||
label={g.gameName}
|
||||
value={g.count}
|
||||
max={gameMax}
|
||||
{...(g.gameColor ? { colorHex: g.gameColor } : { color: 'bg-blue-500' })}
|
||||
colorHex={g.gameColor ?? '#3b82f6'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -258,7 +264,7 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
|
||||
label={t.type}
|
||||
value={t.count}
|
||||
max={typeMax}
|
||||
color={typeBarColors[t.type] ?? 'bg-gray-500'}
|
||||
colorHex={typeBarColors[t.type] ?? '#6b7280'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -75,9 +75,9 @@ export function AdminEvolutions() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={async () => {
|
||||
const data = await exportEvolutions()
|
||||
@@ -102,7 +102,7 @@ export function AdminEvolutions() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex items-center gap-4">
|
||||
<div className="mb-4 flex flex-wrap items-center gap-4">
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
/>
|
||||
<select
|
||||
aria-label="Filter by trigger"
|
||||
value={triggerFilter}
|
||||
onChange={(e) => {
|
||||
setTriggerFilter(e.target.value)
|
||||
|
||||
@@ -70,8 +70,9 @@ export function AdminGames() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex items-center gap-4">
|
||||
<div className="mb-4 flex flex-wrap items-center gap-4">
|
||||
<select
|
||||
aria-label="Filter by region"
|
||||
value={regionFilter}
|
||||
onChange={(e) => setRegionFilter(e.target.value)}
|
||||
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
@@ -84,6 +85,7 @@ export function AdminGames() {
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
aria-label="Filter by generation"
|
||||
value={genFilter}
|
||||
onChange={(e) => setGenFilter(e.target.value)}
|
||||
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<select
|
||||
aria-label="Filter by type"
|
||||
value={typeFilter}
|
||||
onChange={(e) => {
|
||||
setTypeFilter(e.target.value)
|
||||
|
||||
Reference in New Issue
Block a user