Implement dark-first design system with Geist typography (#28)
All checks were successful
CI / backend-lint (push) Successful in 10s
CI / actions-lint (push) Successful in 16s
CI / frontend-lint (push) Successful in 21s

Co-authored-by: Julian Tabel <juliantabel.jt@gmail.com>
Co-committed-by: Julian Tabel <juliantabel.jt@gmail.com>
This commit was merged in pull request #28.
This commit is contained in:
2026-02-17 20:48:42 +01:00
committed by TheFurya
parent e3b3dc5317
commit 42b66ee9a2
56 changed files with 1151 additions and 1067 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,92 @@
Copyright (c) 2023 Vercel, in collaboration with basement.studio
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -66,15 +66,15 @@ export function BossDefeatModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="relative bg-surface-1 rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="px-6 py-4 border-b border-border-default">
<h2 className="text-lg font-semibold">Battle: {boss.name}</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">{boss.location}</p>
<p className="text-sm text-text-tertiary">{boss.location}</p>
</div>
{/* Boss team preview */}
{boss.pokemon.length > 0 && (
<div className="px-6 py-3 border-b border-gray-200 dark:border-gray-700">
<div className="px-6 py-3 border-b border-border-default">
{showPills && (
<div className="flex gap-1 mb-2 flex-wrap">
{variantLabels.map((label) => (
@@ -85,7 +85,7 @@ export function BossDefeatModal({
className={`px-2 py-0.5 text-xs font-medium rounded-full transition-colors ${
selectedVariant === label
? 'bg-blue-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
: 'bg-surface-2 text-text-secondary hover:bg-surface-3'
}`}
>
{label}
@@ -101,14 +101,10 @@ export function BossDefeatModal({
{bp.pokemon.spriteUrl ? (
<img src={bp.pokemon.spriteUrl} alt={bp.pokemon.name} className="w-10 h-10" />
) : (
<div className="w-10 h-10 bg-gray-200 dark:bg-gray-700 rounded-full" />
<div className="w-10 h-10 bg-surface-3 rounded-full" />
)}
<span className="text-xs text-gray-500 dark:text-gray-400 capitalize">
{bp.pokemon.name}
</span>
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">
Lv.{bp.level}
</span>
<span className="text-xs text-text-tertiary capitalize">{bp.pokemon.name}</span>
<span className="text-xs font-medium text-text-secondary">Lv.{bp.level}</span>
<ConditionBadge condition={bp.conditionLabel} size="xs" />
</div>
))}
@@ -127,7 +123,7 @@ export function BossDefeatModal({
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md border transition-colors ${
result === 'won'
? 'bg-green-600 text-white border-green-600'
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'border-border-default hover:bg-surface-2'
}`}
>
Won
@@ -139,7 +135,7 @@ export function BossDefeatModal({
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md border transition-colors ${
result === 'lost'
? 'bg-red-600 text-white border-red-600'
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'border-border-default hover:bg-surface-2'
}`}
>
Lost
@@ -156,24 +152,24 @@ export function BossDefeatModal({
min={1}
value={attempts}
onChange={(e) => setAttempts(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
)}
</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3">
<div className="px-6 py-4 border-t border-border-default flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
>
Cancel
</button>
<button
type="submit"
disabled={isPending}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500 disabled:opacity-50"
>
{isPending ? 'Saving...' : 'Save Result'}
</button>

View File

@@ -76,17 +76,14 @@ export function EggEncounterModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white dark:bg-gray-800 border-b border-green-300 dark:border-green-600 px-6 py-4 rounded-t-xl">
<div className="relative bg-surface-1 rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-surface-1 border-b border-green-300 dark:border-green-600 px-6 py-4 rounded-t-xl">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
<h2 className="text-lg font-semibold text-text-primary flex items-center gap-2">
<span className="text-green-500">&#x1F95A;</span>
Log Egg Hatch
</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<button onClick={onClose} className="text-gray-400 hover:text-text-primary">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
@@ -97,7 +94,7 @@ export function EggEncounterModal({
</svg>
</button>
</div>
<p className="text-sm text-green-600 dark:text-green-400 mt-1">
<p className="text-sm text-status-active mt-1">
Egg hatches bypass the one-per-route rule
</p>
</div>
@@ -105,13 +102,13 @@ export function EggEncounterModal({
<div className="px-6 py-4 space-y-4">
{/* Route selector */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<label className="block text-sm font-medium text-text-secondary mb-1">
Hatch Location
</label>
<select
value={selectedRouteId ?? ''}
onChange={(e) => setSelectedRouteId(Number(e.target.value))}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500"
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-green-500"
>
<option value="">Select a location...</option>
{leafRoutes.map((r) => (
@@ -124,9 +121,7 @@ export function EggEncounterModal({
{/* Pokemon search */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Pokemon
</label>
<label className="block text-sm font-medium text-text-secondary mb-1">Pokemon</label>
{selectedPokemon ? (
<div className="flex items-center gap-3 p-3 rounded-lg border border-green-300 dark:border-green-600 bg-green-50 dark:bg-green-900/20">
{selectedPokemon.spriteUrl ? (
@@ -136,11 +131,11 @@ export function EggEncounterModal({
className="w-10 h-10"
/>
) : (
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-xs font-bold">
<div className="w-10 h-10 rounded-full bg-surface-3 flex items-center justify-center text-xs font-bold">
{selectedPokemon.name[0]?.toUpperCase()}
</div>
)}
<span className="font-medium text-gray-900 dark:text-gray-100 capitalize">
<span className="font-medium text-text-primary capitalize">
{selectedPokemon.name}
</span>
<button
@@ -149,7 +144,7 @@ export function EggEncounterModal({
setSearch('')
setSearchResults([])
}}
className="ml-auto text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
className="ml-auto text-sm text-gray-500 hover:text-text-secondary"
>
Change
</button>
@@ -161,7 +156,7 @@ export function EggEncounterModal({
placeholder="Search pokemon by name..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500"
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-green-500"
/>
{isSearching && (
<div className="flex items-center justify-center py-4">
@@ -175,16 +170,16 @@ export function EggEncounterModal({
key={p.id}
type="button"
onClick={() => setSelectedPokemon(p)}
className="flex flex-col items-center p-2 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-green-400 dark:hover:border-green-600 text-center transition-colors"
className="flex flex-col items-center p-2 rounded-lg border border-border-default hover:border-green-400 dark:hover:border-green-600 text-center transition-colors"
>
{p.spriteUrl ? (
<img src={p.spriteUrl} alt={p.name} className="w-10 h-10" />
) : (
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-xs font-bold">
<div className="w-10 h-10 rounded-full bg-surface-3 flex items-center justify-center text-xs font-bold">
{p.name[0]?.toUpperCase()}
</div>
)}
<span className="text-xs text-gray-700 dark:text-gray-300 mt-1 capitalize">
<span className="text-xs text-text-secondary mt-1 capitalize">
{p.name}
</span>
</button>
@@ -192,7 +187,7 @@ export function EggEncounterModal({
</div>
)}
{search.length >= 2 && !isSearching && searchResults.length === 0 && (
<p className="text-sm text-gray-500 dark:text-gray-400 py-2">No pokemon found</p>
<p className="text-sm text-text-tertiary py-2">No pokemon found</p>
)}
</>
)}
@@ -203,7 +198,7 @@ export function EggEncounterModal({
<div>
<label
htmlFor="egg-nickname"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Nickname
</label>
@@ -213,7 +208,7 @@ export function EggEncounterModal({
value={nickname}
onChange={(e) => setNickname(e.target.value)}
placeholder="Give it a name..."
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500"
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
)}
@@ -223,7 +218,7 @@ export function EggEncounterModal({
<div>
<label
htmlFor="egg-catch-level"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Hatch Level
</label>
@@ -235,17 +230,17 @@ export function EggEncounterModal({
value={catchLevel}
onChange={(e) => setCatchLevel(e.target.value)}
placeholder="1"
className="w-24 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500"
className="w-24 px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
)}
</div>
<div className="sticky bottom-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-6 py-4 rounded-b-xl flex justify-end gap-3">
<div className="sticky bottom-0 bg-surface-1 border-t border-border-default px-6 py-4 rounded-b-xl flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className="px-4 py-2 text-text-secondary bg-surface-2 rounded-lg font-medium hover:bg-surface-3 transition-colors"
>
Cancel
</button>

View File

@@ -55,8 +55,7 @@ const statusOptions: {
{
value: 'missed',
label: 'Missed / Ran',
color:
'bg-gray-100 text-gray-800 border-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600',
color: 'bg-surface-2 text-text-primary border-border-default',
},
]
@@ -261,16 +260,13 @@ export function EncounterModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 rounded-t-xl">
<div className="relative bg-surface-1 rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-surface-1 border-b border-border-default px-6 py-4 rounded-t-xl">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
<h2 className="text-lg font-semibold text-text-primary">
{isEditing ? 'Edit Encounter' : 'Log Encounter'}
</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<button onClick={onClose} className="text-gray-400 hover:text-text-primary">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
@@ -281,7 +277,7 @@ export function EncounterModal({
</svg>
</button>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">{route.name}</p>
<p className="text-sm text-text-tertiary mt-1">{route.name}</p>
</div>
<div className="px-6 py-4 space-y-4">
@@ -289,9 +285,7 @@ export function EncounterModal({
{!isEditing && (
<div>
<div className="flex items-center justify-between mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Pokemon
</label>
<label className="block text-sm font-medium text-text-secondary">Pokemon</label>
{!loadingPokemon && routePokemon && routePokemon.length > 0 && (
<button
type="button"
@@ -325,7 +319,7 @@ export function EncounterModal({
placeholder="Search pokemon..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full px-3 py-1.5 mb-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 py-1.5 mb-2 rounded-lg border border-border-default bg-surface-2 text-text-primary text-sm focus:outline-none focus:ring-2 focus:ring-accent-400"
/>
)}
{availableConditions.length > 0 && (
@@ -336,7 +330,7 @@ export function EncounterModal({
className={`px-2.5 py-1 text-xs font-medium rounded-full border transition-colors ${
selectedCondition === null
? 'bg-purple-100 border-purple-300 text-purple-800 dark:bg-purple-900/40 dark:border-purple-600 dark:text-purple-300'
: 'border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-purple-300 dark:hover:border-purple-600'
: 'border-border-default text-text-tertiary hover:border-purple-300 dark:hover:border-purple-600'
}`}
>
All
@@ -349,7 +343,7 @@ export function EncounterModal({
className={`px-2.5 py-1 text-xs font-medium rounded-full border transition-colors capitalize ${
selectedCondition === cond
? 'bg-purple-100 border-purple-300 text-purple-800 dark:bg-purple-900/40 dark:border-purple-600 dark:text-purple-300'
: 'border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-purple-300 dark:hover:border-purple-600'
: 'border-border-default text-text-tertiary hover:border-purple-300 dark:hover:border-purple-600'
}`}
>
{cond}
@@ -360,11 +354,9 @@ export function EncounterModal({
<div className="max-h-64 overflow-y-auto space-y-3">
{groupedPokemon.map(({ method, pokemon }, groupIdx) => (
<div key={method}>
{groupIdx > 0 && (
<div className="border-t border-gray-200 dark:border-gray-700 mb-3" />
)}
{groupIdx > 0 && <div className="border-t border-border-default mb-3" />}
{hasMultipleGroups && (
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1.5">
<div className="text-xs font-medium text-text-tertiary mb-1.5">
{getMethodLabel(method)}
</div>
)}
@@ -382,10 +374,10 @@ export function EncounterModal({
disabled={isDuped}
className={`flex flex-col items-center p-2 rounded-lg border text-center transition-colors ${
isDuped
? 'opacity-40 cursor-not-allowed border-gray-200 dark:border-gray-700'
? 'opacity-40 cursor-not-allowed border-border-default'
: isSelected
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
: 'border-border-default hover:border-border-default'
}`}
>
{rp.pokemon.spriteUrl ? (
@@ -395,11 +387,11 @@ export function EncounterModal({
className="w-10 h-10"
/>
) : (
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-xs font-bold">
<div className="w-10 h-10 rounded-full bg-surface-3 flex items-center justify-center text-xs font-bold">
{rp.pokemon.name[0]?.toUpperCase()}
</div>
)}
<span className="text-xs text-gray-700 dark:text-gray-300 mt-1 capitalize">
<span className="text-xs text-text-secondary mt-1 capitalize">
{rp.pokemon.name}
</span>
{isDuped && (
@@ -439,16 +431,14 @@ export function EncounterModal({
</div>
</>
) : (
<p className="text-sm text-gray-500 dark:text-gray-400 py-2">
No pokemon data for this route
</p>
<p className="text-sm text-text-tertiary py-2">No pokemon data for this route</p>
)}
</div>
)}
{/* Editing: show pokemon info */}
{isEditing && existing && (
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<div className="flex items-center gap-3 p-3 bg-surface-0/50 rounded-lg">
{existing.pokemon.spriteUrl ? (
<img
src={existing.pokemon.spriteUrl}
@@ -456,12 +446,12 @@ export function EncounterModal({
className="w-12 h-12"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-lg font-bold">
<div className="w-12 h-12 rounded-full bg-surface-3 flex items-center justify-center text-lg font-bold">
{existing.pokemon.name[0]?.toUpperCase()}
</div>
)}
<div>
<div className="font-medium text-gray-900 dark:text-gray-100 capitalize">
<div className="font-medium text-text-primary capitalize">
{existing.pokemon.name}
</div>
<div className="text-xs text-gray-500">
@@ -473,9 +463,7 @@ export function EncounterModal({
{/* Status */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<label className="block text-sm font-medium text-text-secondary mb-1">Status</label>
<div className="flex gap-2">
{statusOptions.map((opt) => (
<button
@@ -485,7 +473,7 @@ export function EncounterModal({
className={`flex-1 px-3 py-2 rounded-lg border text-sm font-medium transition-colors ${
status === opt.value
? opt.color
: 'border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-gray-300'
: 'border-border-default text-text-tertiary hover:border-gray-300'
}`}
>
{opt.label}
@@ -499,7 +487,7 @@ export function EncounterModal({
<div>
<label
htmlFor="nickname"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Nickname
</label>
@@ -509,19 +497,17 @@ export function EncounterModal({
value={nickname}
onChange={(e) => setNickname(e.target.value)}
placeholder="Give it a name..."
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-400"
/>
{showSuggestions && suggestions && suggestions.length > 0 && (
<div className="mt-2">
<div className="flex items-center justify-between mb-1.5">
<span className="text-xs text-gray-500 dark:text-gray-400">
Suggestions ({namingScheme})
</span>
<span className="text-xs text-text-tertiary">Suggestions ({namingScheme})</span>
<button
type="button"
onClick={() => regenerate()}
disabled={loadingSuggestions}
className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 disabled:opacity-50 transition-colors"
className="text-xs text-text-link hover:text-blue-700 dark:hover:text-blue-300 disabled:opacity-50 transition-colors"
>
{loadingSuggestions ? 'Loading...' : 'Regenerate'}
</button>
@@ -535,7 +521,7 @@ export function EncounterModal({
className={`px-2.5 py-1 text-xs rounded-full border transition-colors ${
nickname === name
? 'bg-blue-100 border-blue-300 text-blue-800 dark:bg-blue-900/40 dark:border-blue-600 dark:text-blue-300'
: 'border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:border-blue-300 dark:hover:border-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20'
: 'border-border-default text-text-secondary hover:border-blue-300 dark:hover:border-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20'
}`}
>
{name}
@@ -552,7 +538,7 @@ export function EncounterModal({
<div>
<label
htmlFor="catch-level"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Catch Level
</label>
@@ -568,7 +554,7 @@ export function EncounterModal({
? `${selectedPokemon.minLevel}${selectedPokemon.maxLevel}`
: 'Level'
}
className="w-24 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-24 px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-400"
/>
</div>
)}
@@ -579,7 +565,7 @@ export function EncounterModal({
<div>
<label
htmlFor="faint-level"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Faint Level <span className="font-normal text-gray-400">(mark as dead)</span>
</label>
@@ -591,13 +577,13 @@ export function EncounterModal({
value={faintLevel}
onChange={(e) => setFaintLevel(e.target.value)}
placeholder="Leave empty if still alive"
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-400"
/>
</div>
<div>
<label
htmlFor="death-cause"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Cause of Death <span className="font-normal text-gray-400">(optional)</span>
</label>
@@ -608,18 +594,18 @@ export function EncounterModal({
value={deathCause}
onChange={(e) => setDeathCause(e.target.value)}
placeholder="e.g. Crit from rival's Charizard"
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-400"
/>
</div>
</>
)}
</div>
<div className="sticky bottom-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-6 py-4 rounded-b-xl flex justify-end gap-3">
<div className="sticky bottom-0 bg-surface-1 border-t border-border-default px-6 py-4 rounded-b-xl flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className="px-4 py-2 text-text-secondary bg-surface-2 rounded-lg font-medium hover:bg-surface-3 transition-colors"
>
Cancel
</button>

View File

@@ -21,12 +21,12 @@ export function EndRunModal({ onConfirm, onClose, isPending, genlockeContext }:
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="relative bg-surface-1 rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="px-6 py-4 border-b border-border-default">
<h2 className="text-lg font-semibold">End Run</h2>
</div>
<div className="px-6 py-6">
<p className="text-gray-600 dark:text-gray-400 mb-6">How did your run end?</p>
<p className="text-text-tertiary mb-6">How did your run end?</p>
<div className="flex flex-col gap-3">
<button
onClick={() => onConfirm('completed')}
@@ -39,18 +39,18 @@ export function EndRunModal({ onConfirm, onClose, isPending, genlockeContext }:
<button
onClick={() => onConfirm('failed')}
disabled={isPending}
className="w-full px-4 py-3 rounded-lg font-medium text-left border-2 border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 hover:border-red-400 dark:hover:border-red-600 disabled:opacity-50 transition-colors"
className="w-full px-4 py-3 rounded-lg font-medium text-left border-2 border-red-200 dark:border-red-800 bg-status-failed-bg text-red-700 dark:text-red-300 hover:border-red-400 dark:hover:border-red-600 disabled:opacity-50 transition-colors"
>
<div className="font-semibold">Defeat</div>
<div className="text-sm opacity-80">{defeatDescription}</div>
</button>
</div>
</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end">
<div className="px-6 py-4 border-t border-border-default flex justify-end">
<button
onClick={onClose}
disabled={isPending}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
>
Cancel
</button>

View File

@@ -18,8 +18,8 @@ export function GameCard({ game, selected, onSelect }: GameCardProps) {
<button
type="button"
onClick={() => onSelect(game)}
className={`relative w-full rounded-lg overflow-hidden transition-all duration-200 hover:scale-105 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 ${
selected ? 'ring-2 ring-blue-500 scale-105 shadow-lg' : 'shadow'
className={`relative w-full rounded-lg overflow-hidden transition-all duration-200 hover:scale-105 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 dark:focus:ring-offset-gray-900 ${
selected ? 'ring-2 ring-accent-500 scale-105 shadow-lg' : 'shadow'
}`}
>
{imgIdx < boxArtSrcs.length ? (
@@ -38,14 +38,14 @@ export function GameCard({ game, selected, onSelect }: GameCardProps) {
</span>
</div>
)}
<div className="p-3 bg-white dark:bg-gray-800 text-left">
<h3 className="font-semibold text-gray-900 dark:text-gray-100">{game.name}</h3>
<div className="p-3 bg-surface-1 text-left">
<h3 className="font-semibold text-text-primary">{game.name}</h3>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
<span className="text-xs px-2 py-0.5 rounded-full bg-surface-2 text-text-tertiary">
{game.region.charAt(0).toUpperCase() + game.region.slice(1)}
</span>
{game.releaseYear && (
<span className="text-xs text-gray-500 dark:text-gray-400">{game.releaseYear}</span>
<span className="text-xs text-text-tertiary">{game.releaseYear}</span>
)}
</div>
</div>

View File

@@ -70,16 +70,14 @@ export function GameGrid({ games, selectedId, onSelect, runs }: GameGridProps) {
const pillClass = (active: boolean) =>
`px-3 py-1.5 text-sm font-medium rounded-full transition-colors ${
active
? 'bg-blue-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
active ? 'bg-blue-600 text-white' : 'bg-surface-2 text-text-secondary hover:bg-surface-3'
}`
return (
<div className="space-y-6">
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 mr-1">Gen:</span>
<span className="text-xs font-medium text-text-tertiary mr-1">Gen:</span>
<button
type="button"
onClick={() => setFilter(null)}
@@ -100,7 +98,7 @@ export function GameGrid({ games, selectedId, onSelect, runs }: GameGridProps) {
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 mr-1">Region:</span>
<span className="text-xs font-medium text-text-tertiary mr-1">Region:</span>
<button
type="button"
onClick={() => setRegionFilter(null)}
@@ -122,21 +120,21 @@ export function GameGrid({ games, selectedId, onSelect, runs }: GameGridProps) {
{runs && (
<div className="flex flex-wrap items-center gap-4">
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
<label className="flex items-center gap-2 text-sm text-text-secondary cursor-pointer">
<input
type="checkbox"
checked={hideWithActiveRun}
onChange={(e) => setHideWithActiveRun(e.target.checked)}
className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
className="rounded border-border-default text-blue-600 focus:ring-accent-400"
/>
Hide games with active run
</label>
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
<label className="flex items-center gap-2 text-sm text-text-secondary cursor-pointer">
<input
type="checkbox"
checked={hideCompleted}
onChange={(e) => setHideCompleted(e.target.checked)}
className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
className="rounded border-border-default text-blue-600 focus:ring-accent-400"
/>
Hide completed games
</label>
@@ -146,7 +144,7 @@ export function GameGrid({ games, selectedId, onSelect, runs }: GameGridProps) {
{grouped.map(({ generation, games }) => (
<div key={generation}>
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">
<h3 className="text-lg font-semibold text-text-primary mb-3">
{GENERATION_LABELS[generation] ?? `Generation ${generation}`}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">

View File

@@ -14,7 +14,7 @@ function GraveyardCard({ entry }: { entry: GraveyardEntry }) {
const isEvolved = entry.currentPokemon !== null
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 flex flex-col items-center text-center opacity-60 grayscale">
<div className="bg-surface-1 rounded-lg shadow p-4 flex flex-col items-center text-center opacity-60 grayscale">
{displayPokemon.spriteUrl ? (
<img
src={displayPokemon.spriteUrl}
@@ -23,20 +23,18 @@ function GraveyardCard({ entry }: { entry: GraveyardEntry }) {
loading="lazy"
/>
) : (
<div className="w-25 h-25 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-xl font-bold text-gray-600 dark:text-gray-300">
<div className="w-25 h-25 rounded-full bg-surface-3 flex items-center justify-center text-xl font-bold text-text-secondary">
{displayPokemon.name[0]?.toUpperCase()}
</div>
)}
<div className="mt-2 flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full shrink-0 bg-red-500" />
<span className="font-semibold text-gray-900 dark:text-gray-100 text-sm">
<span className="font-semibold text-text-primary text-sm">
{entry.nickname || displayPokemon.name}
</span>
</div>
{entry.nickname && (
<div className="text-xs text-gray-500 dark:text-gray-400">{displayPokemon.name}</div>
)}
{entry.nickname && <div className="text-xs text-text-tertiary">{displayPokemon.name}</div>}
<div className="flex flex-col items-center gap-0.5 mt-1">
{displayPokemon.types.map((type) => (
@@ -44,24 +42,22 @@ function GraveyardCard({ entry }: { entry: GraveyardEntry }) {
))}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
<div className="text-xs text-text-tertiary mt-1">
Lv. {entry.catchLevel} &rarr; {entry.faintLevel}
</div>
<div className="text-xs text-gray-400 dark:text-gray-500 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-600 dark:text-purple-400 mt-0.5 font-medium">
Leg {entry.legOrder} &mdash; {entry.gameName}
</div>
{isEvolved && (
<div className="text-[10px] text-gray-400 dark:text-gray-500 mt-0.5">
Originally: {entry.pokemon.name}
</div>
<div className="text-[10px] text-text-muted mt-0.5">Originally: {entry.pokemon.name}</div>
)}
{entry.deathCause && (
<div className="text-[10px] italic text-gray-400 dark:text-gray-500 mt-0.5 line-clamp-2">
<div className="text-[10px] italic text-text-muted mt-0.5 line-clamp-2">
{entry.deathCause}
</div>
)}
@@ -107,7 +103,7 @@ export function GenlockeGraveyard({ genlockeId }: GenlockeGraveyardProps) {
if (error) {
return (
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
Failed to load graveyard data.
</div>
)
@@ -115,7 +111,7 @@ export function GenlockeGraveyard({ genlockeId }: GenlockeGraveyardProps) {
if (!data || data.totalDeaths === 0) {
return (
<div className="rounded-lg bg-gray-50 dark:bg-gray-800/50 p-6 text-center text-gray-500 dark:text-gray-400">
<div className="rounded-lg bg-surface-1/50 p-6 text-center text-text-tertiary">
No deaths recorded across any leg.
</div>
)
@@ -125,11 +121,11 @@ export function GenlockeGraveyard({ genlockeId }: GenlockeGraveyardProps) {
<div className="space-y-4">
{/* Summary bar */}
<div className="flex flex-wrap items-center gap-4 text-sm">
<span className="font-semibold text-gray-900 dark:text-gray-100">
<span className="font-semibold text-text-primary">
{data.totalDeaths} total death{data.totalDeaths !== 1 ? 's' : ''}
</span>
{data.deadliestLeg && (
<span className="text-gray-500 dark:text-gray-400">
<span className="text-text-tertiary">
Deadliest: Leg {data.deadliestLeg.legOrder} &mdash; {data.deadliestLeg.gameName} (
{data.deadliestLeg.deathCount})
</span>
@@ -141,7 +137,7 @@ export function GenlockeGraveyard({ genlockeId }: GenlockeGraveyardProps) {
<select
value={filterLeg ?? ''}
onChange={(e) => setFilterLeg(e.target.value ? Number(e.target.value) : null)}
className="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
className="text-sm border border-border-default rounded-lg px-3 py-1.5 bg-surface-1 text-text-primary"
>
<option value="">All Legs</option>
{data.deathsPerLeg.map((leg) => (
@@ -154,7 +150,7 @@ export function GenlockeGraveyard({ genlockeId }: GenlockeGraveyardProps) {
<select
value={sortKey}
onChange={(e) => setSortKey(e.target.value as SortKey)}
className="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
className="text-sm border border-border-default rounded-lg px-3 py-1.5 bg-surface-1 text-text-primary"
>
<option value="leg">Sort by Leg</option>
<option value="level">Sort by Level</option>

View File

@@ -97,11 +97,11 @@ function TimelineGrid({
<div key={legOrder} className="flex justify-center relative" style={{ height: '20px' }}>
{/* Left half connector */}
{showLeftLine && (
<div className="absolute top-[9px] left-0 right-1/2 h-0.5 bg-gray-300 dark:bg-gray-600" />
<div className="absolute top-[9px] left-0 right-1/2 h-0.5 bg-surface-3" />
)}
{/* Right half connector */}
{showRightLine && (
<div className="absolute top-[9px] left-1/2 right-0 h-0.5 bg-gray-300 dark:bg-gray-600" />
<div className="absolute top-[9px] left-1/2 right-0 h-0.5 bg-surface-3" />
)}
{/* Dot or empty */}
{leg ? (
@@ -123,7 +123,7 @@ function LineageCard({ lineage, allLegOrders }: { lineage: LineageEntry; allLegO
const displayPokemon = firstLeg.currentPokemon ?? firstLeg.pokemon
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 flex items-center gap-4">
<div className="bg-surface-1 rounded-lg shadow p-4 flex items-center gap-4">
{/* Left: Pokemon sprite + nickname */}
<div className="flex flex-col items-center min-w-[80px]">
{displayPokemon.spriteUrl ? (
@@ -134,17 +134,15 @@ function LineageCard({ lineage, allLegOrders }: { lineage: LineageEntry; allLegO
loading="lazy"
/>
) : (
<div className="w-16 h-16 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-xl font-bold text-gray-600 dark:text-gray-300">
<div className="w-16 h-16 rounded-full bg-surface-3 flex items-center justify-center text-xl font-bold text-text-secondary">
{displayPokemon.name[0]?.toUpperCase()}
</div>
)}
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100 mt-1 text-center">
<span className="text-sm font-semibold text-text-primary mt-1 text-center">
{lineage.nickname || lineage.pokemon.name}
</span>
{lineage.nickname && (
<span className="text-[10px] text-gray-500 dark:text-gray-400">
{lineage.pokemon.name}
</span>
<span className="text-[10px] text-text-tertiary">{lineage.pokemon.name}</span>
)}
</div>
@@ -200,7 +198,7 @@ export function GenlockeLineage({ genlockeId }: GenlockeLineageProps) {
if (error) {
return (
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
Failed to load lineage data.
</div>
)
@@ -208,7 +206,7 @@ export function GenlockeLineage({ genlockeId }: GenlockeLineageProps) {
if (!data || data.totalLineages === 0) {
return (
<div className="rounded-lg bg-gray-50 dark:bg-gray-800/50 p-6 text-center text-gray-500 dark:text-gray-400">
<div className="rounded-lg bg-surface-1/50 p-6 text-center text-text-tertiary">
No Pokemon have been transferred between legs yet.
</div>
)
@@ -218,7 +216,7 @@ export function GenlockeLineage({ genlockeId }: GenlockeLineageProps) {
<div className="space-y-4">
{/* Summary bar */}
<div className="flex flex-wrap items-center gap-4 text-sm">
<span className="font-semibold text-gray-900 dark:text-gray-100">
<span className="font-semibold text-text-primary">
{data.totalLineages} lineage{data.totalLineages !== 1 ? 's' : ''} across{' '}
{allLegOrders.length} leg{allLegOrders.length !== 1 ? 's' : ''}
</span>
@@ -237,10 +235,10 @@ export function GenlockeLineage({ genlockeId }: GenlockeLineageProps) {
>
{allLegOrders.map((legOrder) => (
<div key={legOrder} className="flex flex-col items-center">
<span className="text-[10px] font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap">
<span className="text-[10px] font-medium text-text-tertiary whitespace-nowrap">
Leg {legOrder}
</span>
<span className="text-[9px] text-gray-400 dark:text-gray-500 whitespace-nowrap truncate max-w-[48px]">
<span className="text-[9px] text-text-muted whitespace-nowrap truncate max-w-[48px]">
{legGameNames.get(legOrder)}
</span>
</div>

View File

@@ -30,12 +30,10 @@ export function HofTeamModal({ alive, onSubmit, onSkip, isPending }: HofTeamModa
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-black/50" />
<div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 rounded-t-xl">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Hall of Fame Team
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
<div className="relative bg-surface-1 rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-surface-1 border-b border-border-default px-6 py-4 rounded-t-xl">
<h2 className="text-lg font-semibold text-text-primary">Hall of Fame Team</h2>
<p className="text-sm text-text-tertiary mt-1">
Select the Pokemon that entered the Hall of Fame (max 6)
</p>
</div>
@@ -57,8 +55,8 @@ export function HofTeamModal({ alive, onSubmit, onSkip, isPending }: HofTeamModa
isSelected
? 'border-yellow-500 bg-yellow-50 dark:bg-yellow-900/20'
: atMax
? 'border-gray-200 dark:border-gray-700 opacity-40 cursor-not-allowed'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
? 'border-border-default opacity-40 cursor-not-allowed'
: 'border-border-default hover:border-border-default'
}`}
>
{displayPokemon.spriteUrl ? (
@@ -68,11 +66,11 @@ export function HofTeamModal({ alive, onSubmit, onSkip, isPending }: HofTeamModa
className="w-14 h-14"
/>
) : (
<div className="w-14 h-14 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-lg font-bold">
<div className="w-14 h-14 rounded-full bg-surface-3 flex items-center justify-center text-lg font-bold">
{displayPokemon.name[0]?.toUpperCase()}
</div>
)}
<span className="text-xs font-medium text-gray-700 dark:text-gray-300 mt-1 capitalize">
<span className="text-xs font-medium text-text-secondary mt-1 capitalize">
{enc.nickname || displayPokemon.name}
</span>
{enc.nickname && (
@@ -84,19 +82,17 @@ export function HofTeamModal({ alive, onSubmit, onSkip, isPending }: HofTeamModa
</div>
</div>
<div className="sticky bottom-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-6 py-4 rounded-b-xl flex items-center justify-between">
<div className="sticky bottom-0 bg-surface-1 border-t border-border-default px-6 py-4 rounded-b-xl flex items-center justify-between">
<button
type="button"
onClick={onSkip}
disabled={isPending}
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 disabled:opacity-50"
className="text-sm text-text-tertiary hover:text-text-primary disabled:opacity-50"
>
Skip
</button>
<div className="flex items-center gap-3">
<span className="text-sm text-gray-400 dark:text-gray-500">
{selected.size}/6 selected
</span>
<span className="text-sm text-text-muted">{selected.size}/6 selected</span>
<button
type="button"
disabled={selected.size === 0 || isPending}

View File

@@ -1,61 +1,83 @@
import { useState } from 'react'
import { Link, Outlet } from 'react-router-dom'
import { Link, Outlet, useLocation } from 'react-router-dom'
const navLinks = [
{ to: '/runs/new', label: 'New Run' },
{ to: '/runs', label: 'My Runs' },
{ to: '/genlockes', label: 'Genlockes' },
{ to: '/stats', label: 'Stats' },
{ to: '/admin', label: 'Admin' },
]
function NavLink({
to,
active,
children,
onClick,
className = '',
}: {
to: string
active: boolean
children: React.ReactNode
onClick?: () => void
className?: string
}) {
return (
<Link
to={to}
onClick={onClick}
className={`${className} px-3 py-2 rounded-md text-sm font-medium transition-colors ${
active
? 'bg-accent-600/20 text-accent-300'
: 'text-text-secondary hover:text-text-primary hover:bg-surface-3'
}`}
>
{children}
</Link>
)
}
export function Layout() {
const [menuOpen, setMenuOpen] = useState(false)
const location = useLocation()
function isActive(to: string) {
if (to === '/runs/new') return location.pathname === '/runs/new'
return location.pathname.startsWith(to)
}
return (
<div className="min-h-screen flex flex-col bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<nav className="bg-white dark:bg-gray-800 shadow-sm">
<div className="min-h-screen flex flex-col bg-surface-0 text-text-primary">
<nav className="sticky top-0 z-40 bg-surface-1/80 backdrop-blur-lg border-b border-border-default">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<Link to="/" className="text-xl font-bold">
ANT
<div className="flex justify-between h-14">
<div className="flex items-center gap-2">
<Link to="/" className="flex items-center gap-2 group">
<img
src="/favicon.svg"
alt=""
className="w-7 h-7 transition-transform group-hover:scale-110"
/>
<span className="text-lg font-bold tracking-tight text-text-primary">ANT</span>
</Link>
</div>
{/* Desktop nav */}
<div className="hidden sm:flex items-center space-x-4">
<Link
to="/runs/new"
className="px-3 py-2 rounded-md text-sm font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
New Run
</Link>
<Link
to="/runs"
className="px-3 py-2 rounded-md text-sm font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
My Runs
</Link>
<Link
to="/genlockes"
className="px-3 py-2 rounded-md text-sm font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
Genlockes
</Link>
<Link
to="/stats"
className="px-3 py-2 rounded-md text-sm font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
Stats
</Link>
<Link
to="/admin"
className="px-3 py-2 rounded-md text-sm font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
Admin
</Link>
<div className="hidden sm:flex items-center gap-1">
{navLinks.map((link) => (
<NavLink key={link.to} to={link.to} active={isActive(link.to)}>
{link.label}
</NavLink>
))}
</div>
{/* Mobile hamburger */}
<div className="flex items-center sm:hidden">
<button
type="button"
onClick={() => setMenuOpen(!menuOpen)}
className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
className="p-2 rounded-md text-text-secondary hover:text-text-primary hover:bg-surface-3 transition-colors"
aria-label="Toggle menu"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{menuOpen ? (
<path
strokeLinecap="round"
@@ -78,43 +100,19 @@ export function Layout() {
</div>
{/* Mobile dropdown */}
{menuOpen && (
<div className="sm:hidden border-t border-gray-200 dark:border-gray-700">
<div className="sm:hidden border-t border-border-default">
<div className="px-2 pt-2 pb-3 space-y-1">
<Link
to="/runs/new"
onClick={() => setMenuOpen(false)}
className="block px-3 py-2 rounded-md text-base font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
New Run
</Link>
<Link
to="/runs"
onClick={() => setMenuOpen(false)}
className="block px-3 py-2 rounded-md text-base font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
My Runs
</Link>
<Link
to="/genlockes"
onClick={() => setMenuOpen(false)}
className="block px-3 py-2 rounded-md text-base font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
Genlockes
</Link>
<Link
to="/stats"
onClick={() => setMenuOpen(false)}
className="block px-3 py-2 rounded-md text-base font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
Stats
</Link>
<Link
to="/admin"
onClick={() => setMenuOpen(false)}
className="block px-3 py-2 rounded-md text-base font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
Admin
</Link>
{navLinks.map((link) => (
<NavLink
key={link.to}
to={link.to}
active={isActive(link.to)}
onClick={() => setMenuOpen(false)}
className="block"
>
{link.label}
</NavLink>
))}
</div>
</div>
)}
@@ -122,12 +120,12 @@ export function Layout() {
<main className="flex-1">
<Outlet />
</main>
<footer className="border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 text-center text-xs text-gray-500 dark:text-gray-400">
Pokémon encounter data from{' '}
<footer className="border-t border-border-default bg-surface-1/50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 text-center text-xs text-text-tertiary">
Encounter data from{' '}
<a
href="https://pokedb.org"
className="underline hover:text-gray-700 dark:hover:text-gray-300"
className="underline hover:text-text-secondary transition-colors"
target="_blank"
rel="noopener noreferrer"
>

View File

@@ -16,29 +16,27 @@ export function PokemonCard({ encounter, showFaintLevel, onClick }: PokemonCardP
return (
<div
onClick={onClick}
className={`bg-white dark:bg-gray-800 rounded-lg shadow p-4 flex flex-col items-center text-center ${
isDead ? 'opacity-60 grayscale' : ''
} ${onClick ? 'cursor-pointer hover:ring-2 hover:ring-blue-400 transition-shadow' : ''}`}
className={`bg-surface-1 rounded-xl border border-border-default p-4 flex flex-col items-center text-center transition-all ${
isDead ? 'opacity-50 grayscale' : ''
} ${onClick ? 'cursor-pointer hover:border-accent-400/30 hover:-translate-y-0.5' : ''}`}
>
{displayPokemon.spriteUrl ? (
<img src={displayPokemon.spriteUrl} alt={displayPokemon.name} className="w-25 h-25" />
) : (
<div className="w-25 h-25 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-xl font-bold text-gray-600 dark:text-gray-300">
<div className="w-25 h-25 rounded-full bg-surface-3 flex items-center justify-center text-xl font-bold text-text-secondary">
{displayPokemon.name[0]?.toUpperCase()}
</div>
)}
<div className="mt-2 flex items-center gap-1.5">
<span
className={`w-2 h-2 rounded-full shrink-0 ${isDead ? 'bg-red-500' : 'bg-green-500'}`}
className={`w-2 h-2 rounded-full shrink-0 ${isDead ? 'bg-status-dead' : 'bg-status-alive'}`}
/>
<span className="font-semibold text-gray-900 dark:text-gray-100 text-sm">
<span className="font-semibold text-text-primary text-sm">
{nickname || displayPokemon.name}
</span>
</div>
{nickname && (
<div className="text-xs text-gray-500 dark:text-gray-400">{displayPokemon.name}</div>
)}
{nickname && <div className="text-xs text-text-secondary">{displayPokemon.name}</div>}
<div className="flex flex-col items-center gap-0.5 mt-1">
{displayPokemon.types.map((type) => (
@@ -46,22 +44,20 @@ export function PokemonCard({ encounter, showFaintLevel, onClick }: PokemonCardP
))}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
<div className="text-xs font-mono text-text-secondary mt-1">
{showFaintLevel && isDead
? `Lv. ${catchLevel}${faintLevel}`
: `Lv. ${catchLevel ?? '?'}`}
</div>
<div className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">{route.name}</div>
<div className="text-xs text-text-tertiary mt-0.5">{route.name}</div>
{isEvolved && (
<div className="text-[10px] text-gray-400 dark:text-gray-500 mt-0.5">
Originally: {pokemon.name}
</div>
<div className="text-[10px] text-text-tertiary mt-0.5">Originally: {pokemon.name}</div>
)}
{isDead && deathCause && (
<div className="text-[10px] italic text-gray-400 dark:text-gray-500 mt-0.5 line-clamp-2">
<div className="text-[10px] italic text-text-tertiary mt-0.5 line-clamp-2">
{deathCause}
</div>
)}

View File

@@ -9,7 +9,7 @@ export function RuleBadges({ rules }: RuleBadgesProps) {
const enabledRules = RULE_DEFINITIONS.filter((def) => rules[def.key])
if (enabledRules.length === 0) {
return <span className="text-sm text-gray-500 dark:text-gray-400">No rules enabled</span>
return <span className="text-sm text-text-tertiary">No rules enabled</span>
}
return (

View File

@@ -11,13 +11,13 @@ export function RuleToggle({ name, description, enabled, onChange }: RuleToggleP
const [showTooltip, setShowTooltip] = useState(false)
return (
<div className="flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700 last:border-0">
<div className="flex items-center justify-between py-3 border-b border-border-default last:border-0">
<div className="flex-1 pr-4">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900 dark:text-gray-100">{name}</span>
<span className="font-medium text-text-primary">{name}</span>
<button
type="button"
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
className="text-gray-400 hover:text-text-secondary"
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
onClick={() => setShowTooltip(!showTooltip)}
@@ -33,17 +33,15 @@ export function RuleToggle({ name, description, enabled, onChange }: RuleToggleP
</svg>
</button>
</div>
{showTooltip && (
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{description}</p>
)}
{showTooltip && <p className="mt-1 text-sm text-text-tertiary">{description}</p>}
</div>
<button
type="button"
role="switch"
aria-checked={enabled}
onClick={() => onChange(!enabled)}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
enabled ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-600'
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 ${
enabled ? 'bg-blue-600' : 'bg-surface-3'
}`}
>
<span

View File

@@ -38,10 +38,8 @@ export function RulesConfiguration({
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
Rules Configuration
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
<h2 className="text-xl font-semibold text-text-primary">Rules Configuration</h2>
<p className="text-sm text-text-tertiary">
{enabledCount} of {totalCount} rules enabled
</p>
</div>
@@ -54,10 +52,10 @@ export function RulesConfiguration({
</button>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Core Rules</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
<div className="bg-surface-1 rounded-lg shadow">
<div className="px-4 py-3 border-b border-border-default">
<h3 className="text-lg font-medium text-text-primary">Core Rules</h3>
<p className="text-sm text-text-tertiary">
The fundamental rules of a Nuzlocke challenge
</p>
</div>
@@ -74,14 +72,10 @@ export function RulesConfiguration({
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
Difficulty Modifiers
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
Optional rules to increase the challenge
</p>
<div className="bg-surface-1 rounded-lg shadow">
<div className="px-4 py-3 border-b border-border-default">
<h3 className="text-lg font-medium text-text-primary">Difficulty Modifiers</h3>
<p className="text-sm text-text-tertiary">Optional rules to increase the challenge</p>
</div>
<div className="px-4">
{difficultyRules.map((rule) => (
@@ -97,12 +91,10 @@ export function RulesConfiguration({
</div>
{completionRules.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Completion</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
When is the run considered complete
</p>
<div className="bg-surface-1 rounded-lg shadow">
<div className="px-4 py-3 border-b border-border-default">
<h3 className="text-lg font-medium text-text-primary">Completion</h3>
<p className="text-sm text-text-tertiary">When is the run considered complete</p>
</div>
<div className="px-4">
{completionRules.map((rule) => (

View File

@@ -12,7 +12,7 @@ export function ShinyBox({ encounters, onEncounterClick }: ShinyBoxProps) {
<h3 className="text-sm font-semibold text-yellow-600 dark:text-yellow-400 mb-3 flex items-center gap-1.5">
<span>&#10022;</span>
Shiny Box
<span className="text-xs font-normal text-gray-400 dark:text-gray-500 ml-1">
<span className="text-xs font-normal text-text-muted ml-1">
{encounters.length} {encounters.length === 1 ? 'shiny' : 'shinies'}
</span>
</h3>
@@ -27,9 +27,7 @@ export function ShinyBox({ encounters, onEncounterClick }: ShinyBoxProps) {
))}
</div>
) : (
<p className="text-sm text-gray-400 dark:text-gray-500 text-center py-2">
No shinies found yet
</p>
<p className="text-sm text-text-muted text-center py-2">No shinies found yet</p>
)}
</div>
)

View File

@@ -92,17 +92,14 @@ export function ShinyEncounterModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white dark:bg-gray-800 border-b border-yellow-300 dark:border-yellow-600 px-6 py-4 rounded-t-xl">
<div className="relative bg-surface-1 rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-surface-1 border-b border-yellow-300 dark:border-yellow-600 px-6 py-4 rounded-t-xl">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
<h2 className="text-lg font-semibold text-text-primary flex items-center gap-2">
<span className="text-yellow-500">&#10022;</span>
Log Shiny Encounter
</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<button onClick={onClose} className="text-gray-400 hover:text-text-primary">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
@@ -121,13 +118,11 @@ export function ShinyEncounterModal({
<div className="px-6 py-4 space-y-4">
{/* Route selector */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Route
</label>
<label className="block text-sm font-medium text-text-secondary mb-1">Route</label>
<select
value={selectedRouteId ?? ''}
onChange={(e) => handleRouteChange(Number(e.target.value))}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-yellow-500"
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-yellow-500"
>
<option value="">Select a route...</option>
{leafRoutes.map((r) => (
@@ -141,9 +136,7 @@ export function ShinyEncounterModal({
{/* Pokemon Selection */}
{selectedRouteId && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Pokemon
</label>
<label className="block text-sm font-medium text-text-secondary mb-1">Pokemon</label>
{loadingPokemon ? (
<div className="flex items-center justify-center py-4">
<div className="w-6 h-6 border-2 border-yellow-500 border-t-transparent rounded-full animate-spin" />
@@ -156,17 +149,15 @@ export function ShinyEncounterModal({
placeholder="Search pokemon..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full px-3 py-1.5 mb-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-500"
className="w-full px-3 py-1.5 mb-2 rounded-lg border border-border-default bg-surface-2 text-text-primary text-sm focus:outline-none focus:ring-2 focus:ring-yellow-500"
/>
)}
<div className="max-h-64 overflow-y-auto space-y-3">
{groupedPokemon.map(({ method, pokemon }, groupIdx) => (
<div key={method}>
{groupIdx > 0 && (
<div className="border-t border-gray-200 dark:border-gray-700 mb-3" />
)}
{groupIdx > 0 && <div className="border-t border-border-default mb-3" />}
{hasMultipleGroups && (
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1.5">
<div className="text-xs font-medium text-text-tertiary mb-1.5">
{getMethodLabel(method)}
</div>
)}
@@ -179,7 +170,7 @@ export function ShinyEncounterModal({
className={`flex flex-col items-center p-2 rounded-lg border text-center transition-colors ${
selectedPokemon?.id === rp.id
? 'border-yellow-500 bg-yellow-50 dark:bg-yellow-900/30'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
: 'border-border-default hover:border-border-default'
}`}
>
{rp.pokemon.spriteUrl ? (
@@ -189,11 +180,11 @@ export function ShinyEncounterModal({
className="w-10 h-10"
/>
) : (
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-xs font-bold">
<div className="w-10 h-10 rounded-full bg-surface-3 flex items-center justify-center text-xs font-bold">
{rp.pokemon.name[0]?.toUpperCase()}
</div>
)}
<span className="text-xs text-gray-700 dark:text-gray-300 mt-1 capitalize">
<span className="text-xs text-text-secondary mt-1 capitalize">
{rp.pokemon.name}
</span>
{SPECIAL_METHODS.includes(rp.encounterMethod) && (
@@ -211,9 +202,7 @@ export function ShinyEncounterModal({
</div>
</>
) : (
<p className="text-sm text-gray-500 dark:text-gray-400 py-2">
No pokemon data for this route
</p>
<p className="text-sm text-text-tertiary py-2">No pokemon data for this route</p>
)}
</div>
)}
@@ -223,7 +212,7 @@ export function ShinyEncounterModal({
<div>
<label
htmlFor="shiny-nickname"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Nickname
</label>
@@ -233,7 +222,7 @@ export function ShinyEncounterModal({
value={nickname}
onChange={(e) => setNickname(e.target.value)}
placeholder="Give it a name..."
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-yellow-500"
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-yellow-500"
/>
</div>
)}
@@ -243,7 +232,7 @@ export function ShinyEncounterModal({
<div>
<label
htmlFor="shiny-catch-level"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Catch Level
</label>
@@ -259,17 +248,17 @@ export function ShinyEncounterModal({
? `${selectedPokemon.minLevel}${selectedPokemon.maxLevel}`
: 'Level'
}
className="w-24 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-yellow-500"
className="w-24 px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-yellow-500"
/>
</div>
)}
</div>
<div className="sticky bottom-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-6 py-4 rounded-b-xl flex justify-end gap-3">
<div className="sticky bottom-0 bg-surface-1 border-t border-border-default px-6 py-4 rounded-b-xl flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className="px-4 py-2 text-text-secondary bg-surface-2 rounded-lg font-medium hover:bg-surface-3 transition-colors"
>
Cancel
</button>

View File

@@ -6,26 +6,26 @@ interface StatCardProps {
}
const colorClasses: Record<string, string> = {
blue: 'border-blue-500',
green: 'border-green-500',
red: 'border-red-500',
blue: 'border-status-completed',
green: 'border-status-alive',
red: 'border-status-failed',
purple: 'border-purple-500',
amber: 'border-amber-500',
gray: 'border-gray-500',
gray: 'border-text-tertiary',
}
export function StatCard({ label, value, total, color }: StatCardProps) {
return (
<div
className={`bg-white dark:bg-gray-800 rounded-lg shadow p-4 border-l-4 ${colorClasses[color] ?? 'border-gray-500'}`}
className={`bg-surface-1 rounded-lg border border-border-default p-4 border-l-4 ${colorClasses[color] ?? 'border-text-tertiary'}`}
>
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
<div className="text-2xl font-bold font-mono text-text-primary">
{value}
{total !== undefined && (
<span className="text-sm font-normal text-gray-500 dark:text-gray-400"> / {total}</span>
<span className="text-sm font-normal font-sans text-text-secondary"> / {total}</span>
)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">{label}</div>
<div className="text-sm text-text-secondary">{label}</div>
</div>
)
}

View File

@@ -91,16 +91,13 @@ export function StatusChangeModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-sm w-full">
<div className="relative bg-surface-1 rounded-xl shadow-xl max-w-sm w-full">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
<div className="flex items-center justify-between px-6 py-4 border-b border-border-default">
<h2 className="text-lg font-semibold text-text-primary">
{isDead ? 'Death Details' : 'Pokemon Status'}
</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<button onClick={onClose} className="text-gray-400 hover:text-text-primary">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
@@ -122,51 +119,46 @@ export function StatusChangeModal({
className={`w-16 h-16 ${isDead ? 'grayscale opacity-60' : ''}`}
/>
) : (
<div className="w-16 h-16 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-xl font-bold text-gray-600 dark:text-gray-300">
<div className="w-16 h-16 rounded-full bg-surface-3 flex items-center justify-center text-xl font-bold text-text-secondary">
{displayPokemon.name[0]?.toUpperCase()}
</div>
)}
<div>
<div className="font-semibold text-gray-900 dark:text-gray-100">
<div className="font-semibold text-text-primary">
{nickname || displayPokemon.name}
</div>
{nickname && (
<div className="text-xs text-gray-500 dark:text-gray-400 capitalize">
{displayPokemon.name}
</div>
<div className="text-xs text-text-tertiary capitalize">{displayPokemon.name}</div>
)}
<div className="flex flex-col items-start gap-0.5 mt-1">
{displayPokemon.types.map((type) => (
<TypeBadge key={type} type={type} />
))}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
<div className="text-xs text-text-tertiary mt-1">
Lv. {catchLevel ?? '?'} &middot; {route.name}
</div>
{currentPokemon && (
<div className="text-[10px] text-gray-400 dark:text-gray-500 mt-0.5">
Originally: {pokemon.name}
</div>
<div className="text-[10px] text-text-muted mt-0.5">Originally: {pokemon.name}</div>
)}
</div>
</div>
{/* Dead pokemon: view-only details */}
{isDead && (
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-4 space-y-2">
<div className="flex items-center gap-2 text-red-700 dark:text-red-400 font-medium text-sm">
<div className="bg-status-failed-bg rounded-lg p-4 space-y-2">
<div className="flex items-center gap-2 text-status-failed font-medium text-sm">
<span className="w-2 h-2 rounded-full bg-red-500" />
Deceased
</div>
{faintLevel !== null && (
<div className="text-sm text-gray-700 dark:text-gray-300">
<span className="text-gray-500 dark:text-gray-400">Level at death:</span>{' '}
{faintLevel}
<div className="text-sm text-text-secondary">
<span className="text-text-tertiary">Level at death:</span> {faintLevel}
</div>
)}
{deathCause && (
<div className="text-sm text-gray-700 dark:text-gray-300">
<span className="text-gray-500 dark:text-gray-400">Cause:</span> {deathCause}
<div className="text-sm text-text-secondary">
<span className="text-text-tertiary">Cause:</span> {deathCause}
</div>
)}
</div>
@@ -205,22 +197,20 @@ export function StatusChangeModal({
{!isDead && showEvolve && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Evolve into:
</h3>
<h3 className="text-sm font-medium text-text-secondary">Evolve into:</h3>
<button
type="button"
onClick={() => setShowEvolve(false)}
className="text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
className="text-xs text-gray-500 hover:text-text-secondary"
>
Back
</button>
</div>
{evolutionsLoading && (
<p className="text-sm text-gray-500 dark:text-gray-400">Loading evolutions...</p>
<p className="text-sm text-text-tertiary">Loading evolutions...</p>
)}
{!evolutionsLoading && normalEvolutions.length === 0 && (
<p className="text-sm text-gray-500 dark:text-gray-400">No evolutions available</p>
<p className="text-sm text-text-tertiary">No evolutions available</p>
)}
{!evolutionsLoading && normalEvolutions.length > 0 && (
<div className="space-y-2">
@@ -230,7 +220,7 @@ export function StatusChangeModal({
type="button"
disabled={isPending}
onClick={() => handleEvolve(evo.toPokemon.id)}
className="w-full flex items-center gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:border-blue-300 dark:hover:border-blue-600 transition-colors disabled:opacity-50"
className="w-full flex items-center gap-3 p-3 rounded-lg border border-border-default hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:border-blue-300 dark:hover:border-blue-600 transition-colors disabled:opacity-50"
>
{evo.toPokemon.spriteUrl ? (
<img
@@ -239,15 +229,15 @@ export function StatusChangeModal({
className="w-10 h-10"
/>
) : (
<div className="w-10 h-10 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-sm font-bold text-gray-600 dark:text-gray-300">
<div className="w-10 h-10 rounded-full bg-surface-3 flex items-center justify-center text-sm font-bold text-text-secondary">
{evo.toPokemon.name[0]?.toUpperCase()}
</div>
)}
<div className="text-left">
<div className="font-medium text-gray-900 dark:text-gray-100 text-sm">
<div className="font-medium text-text-primary text-sm">
{evo.toPokemon.name}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
<div className="text-xs text-text-tertiary">
{formatEvolutionMethod(evo)}
</div>
</div>
@@ -262,9 +252,7 @@ export function StatusChangeModal({
{!isDead && showShedConfirm && shedCompanion && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Shed Evolution
</h3>
<h3 className="text-sm font-medium text-text-secondary">Shed Evolution</h3>
<button
type="button"
onClick={() => {
@@ -273,7 +261,7 @@ export function StatusChangeModal({
setShedNickname('')
setShowEvolve(true)
}}
className="text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
className="text-xs text-gray-500 hover:text-text-secondary"
>
Back
</button>
@@ -287,7 +275,7 @@ export function StatusChangeModal({
className="w-12 h-12"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-sm font-bold text-gray-600 dark:text-gray-300">
<div className="w-12 h-12 rounded-full bg-surface-3 flex items-center justify-center text-sm font-bold text-text-secondary">
{shedCompanion.toPokemon.name[0]?.toUpperCase()}
</div>
)}
@@ -300,7 +288,7 @@ export function StatusChangeModal({
<div>
<label
htmlFor="shed-nickname"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Nickname <span className="font-normal text-gray-400">(optional)</span>
</label>
@@ -311,7 +299,7 @@ export function StatusChangeModal({
value={shedNickname}
onChange={(e) => setShedNickname(e.target.value)}
placeholder={shedCompanion.toPokemon.name}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-amber-500"
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-amber-500"
/>
</div>
<div className="flex gap-3 pt-1">
@@ -319,7 +307,7 @@ export function StatusChangeModal({
type="button"
disabled={isPending}
onClick={() => applyEvolution(false)}
className="flex-1 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 transition-colors"
className="flex-1 px-4 py-2 bg-surface-2 text-text-secondary rounded-lg font-medium hover:bg-surface-3 disabled:opacity-50 transition-colors"
>
Skip
</button>
@@ -339,13 +327,11 @@ export function StatusChangeModal({
{!isDead && showFormChange && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Change form to:
</h3>
<h3 className="text-sm font-medium text-text-secondary">Change form to:</h3>
<button
type="button"
onClick={() => setShowFormChange(false)}
className="text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
className="text-xs text-gray-500 hover:text-text-secondary"
>
Back
</button>
@@ -358,19 +344,17 @@ export function StatusChangeModal({
type="button"
disabled={isPending}
onClick={() => handleEvolve(form.id)}
className="w-full flex items-center gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-purple-50 dark:hover:bg-purple-900/20 hover:border-purple-300 dark:hover:border-purple-600 transition-colors disabled:opacity-50"
className="w-full flex items-center gap-3 p-3 rounded-lg border border-border-default hover:bg-purple-50 dark:hover:bg-purple-900/20 hover:border-purple-300 dark:hover:border-purple-600 transition-colors disabled:opacity-50"
>
{form.spriteUrl ? (
<img src={form.spriteUrl} alt={form.name} className="w-10 h-10" />
) : (
<div className="w-10 h-10 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-sm font-bold text-gray-600 dark:text-gray-300">
<div className="w-10 h-10 rounded-full bg-surface-3 flex items-center justify-center text-sm font-bold text-text-secondary">
{form.name[0]?.toUpperCase()}
</div>
)}
<div className="text-left">
<div className="font-medium text-gray-900 dark:text-gray-100 text-sm">
{form.name}
</div>
<div className="font-medium text-text-primary text-sm">{form.name}</div>
<div className="flex gap-1">
{form.types.map((type) => (
<TypeBadge key={type} type={type} />
@@ -387,8 +371,8 @@ export function StatusChangeModal({
{/* Confirmation form */}
{!isDead && showConfirm && (
<div className="space-y-3">
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-3">
<p className="text-sm text-red-700 dark:text-red-400 font-medium">
<div className="bg-status-failed-bg rounded-lg p-3">
<p className="text-sm text-status-failed font-medium">
This cannot be undone (Nuzlocke rules).
</p>
</div>
@@ -396,7 +380,7 @@ export function StatusChangeModal({
<div>
<label
htmlFor="death-level"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Level at Death <span className="font-normal text-gray-400">(optional)</span>
</label>
@@ -408,14 +392,14 @@ export function StatusChangeModal({
value={deathLevel}
onChange={(e) => setDeathLevel(e.target.value)}
placeholder="Level"
className="w-24 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-red-500"
className="w-24 px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-red-500"
/>
</div>
<div>
<label
htmlFor="death-cause"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Cause of Death <span className="font-normal text-gray-400">(optional)</span>
</label>
@@ -426,7 +410,7 @@ export function StatusChangeModal({
value={cause}
onChange={(e) => setCause(e.target.value)}
placeholder="e.g. Crit from rival's Charizard"
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-red-500"
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-red-500"
/>
</div>
@@ -434,7 +418,7 @@ export function StatusChangeModal({
<button
type="button"
onClick={() => setShowConfirm(false)}
className="flex-1 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className="flex-1 px-4 py-2 bg-surface-2 text-text-secondary rounded-lg font-medium hover:bg-surface-3 transition-colors"
>
Cancel
</button>
@@ -454,11 +438,11 @@ export function StatusChangeModal({
{/* Footer for dead/no-confirm/no-evolve views */}
{(isDead ||
(!isDead && !showConfirm && !showEvolve && !showFormChange && !showShedConfirm)) && (
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end">
<div className="px-6 py-4 border-t border-border-default flex justify-end">
<button
type="button"
onClick={onClose}
className="px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className="px-4 py-2 bg-surface-2 text-text-secondary rounded-lg font-medium hover:bg-surface-3 transition-colors"
>
Close
</button>

View File

@@ -27,10 +27,10 @@ export function StepIndicator({
disabled={!isCompleted}
className={`flex items-center gap-2 text-sm font-medium ${
isCompleted
? 'text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-700 dark:hover:text-blue-300'
? 'text-text-link cursor-pointer hover:text-blue-700 dark:hover:text-blue-300'
: isCurrent
? 'text-gray-900 dark:text-gray-100'
: 'text-gray-400 dark:text-gray-500 cursor-default'
? 'text-text-primary'
: 'text-text-muted cursor-default'
}`}
>
<span
@@ -39,7 +39,7 @@ export function StepIndicator({
? 'bg-blue-600 text-white'
: isCurrent
? 'border-2 border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400'
: 'border-2 border-gray-300 dark:border-gray-600 text-gray-400 dark:text-gray-500'
: 'border-2 border-border-default text-text-muted'
}`}
>
{isCompleted ? (
@@ -61,7 +61,7 @@ export function StepIndicator({
{i < steps.length - 1 && (
<div
className={`flex-1 h-0.5 mx-3 ${
step < currentStep ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'
step < currentStep ? 'bg-blue-600' : 'bg-surface-3'
}`}
/>
)}

View File

@@ -26,12 +26,10 @@ export function TransferModal({ hofTeam, onSubmit, onSkip, isPending }: Transfer
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-black/50" />
<div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 rounded-t-xl">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Transfer Pokemon to Next Leg
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
<div className="relative bg-surface-1 rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-surface-1 border-b border-border-default px-6 py-4 rounded-t-xl">
<h2 className="text-lg font-semibold text-text-primary">Transfer Pokemon to Next Leg</h2>
<p className="text-sm text-text-tertiary mt-1">
Selected Pokemon will be bred down to their base form and appear as level 1 encounters
in the next leg.
</p>
@@ -51,7 +49,7 @@ export function TransferModal({ hofTeam, onSubmit, onSkip, isPending }: Transfer
className={`flex flex-col items-center p-3 rounded-lg border-2 text-center transition-colors ${
isSelected
? 'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
: 'border-border-default hover:border-border-default'
}`}
>
{displayPokemon.spriteUrl ? (
@@ -61,11 +59,11 @@ export function TransferModal({ hofTeam, onSubmit, onSkip, isPending }: Transfer
className="w-14 h-14"
/>
) : (
<div className="w-14 h-14 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-lg font-bold">
<div className="w-14 h-14 rounded-full bg-surface-3 flex items-center justify-center text-lg font-bold">
{displayPokemon.name[0]?.toUpperCase()}
</div>
)}
<span className="text-xs font-medium text-gray-700 dark:text-gray-300 mt-1 capitalize">
<span className="text-xs font-medium text-text-secondary mt-1 capitalize">
{enc.nickname || displayPokemon.name}
</span>
{enc.nickname && (
@@ -78,17 +76,17 @@ export function TransferModal({ hofTeam, onSubmit, onSkip, isPending }: Transfer
</div>
</div>
<div className="sticky bottom-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-6 py-4 rounded-b-xl flex items-center justify-between">
<div className="sticky bottom-0 bg-surface-1 border-t border-border-default px-6 py-4 rounded-b-xl flex items-center justify-between">
<button
type="button"
onClick={onSkip}
disabled={isPending}
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 disabled:opacity-50"
className="text-sm text-text-tertiary hover:text-text-primary disabled:opacity-50"
>
Skip (No Transfers)
</button>
<div className="flex items-center gap-3">
<span className="text-sm text-gray-400 dark:text-gray-500">
<span className="text-sm text-text-muted">
{selected.size}/{hofTeam.length} selected
</span>
<button

View File

@@ -23,7 +23,7 @@ export function AdminLayout() {
`block px-3 py-2 rounded-md text-sm font-medium whitespace-nowrap ${
isActive
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200'
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
: 'hover:bg-surface-2'
}`
}
>

View File

@@ -61,26 +61,26 @@ export function AdminTable<T>({
if (isLoading) {
return (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<div className="border border-border-default rounded-lg overflow-hidden">
<table className="min-w-full divide-y divide-border-default">
<thead className="bg-surface-1">
<tr>
{columns.map((col) => (
<th
key={col.header}
className={`px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider ${col.className ?? ''}`}
className={`px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider ${col.className ?? ''}`}
>
{col.header}
</th>
))}
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
<tbody className="bg-surface-0 divide-y divide-border-default">
{Array.from({ length: 5 }).map((_, i) => (
<tr key={i}>
{columns.map((col) => (
<td key={col.header} className={`px-4 py-3 ${col.className ?? ''}`}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
<div className="h-4 bg-surface-3 rounded animate-pulse" />
</td>
))}
</tr>
@@ -93,17 +93,17 @@ export function AdminTable<T>({
if (data.length === 0) {
return (
<div className="text-center py-8 text-gray-500 dark:text-gray-400 border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="text-center py-8 text-text-tertiary border border-border-default rounded-lg">
{emptyMessage}
</div>
)
}
return (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div className="border border-border-default rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<table className="min-w-full divide-y divide-border-default">
<thead className="bg-surface-1">
<tr>
{columns.map((col) => {
const sortable = !!col.sortKey
@@ -112,7 +112,7 @@ export function AdminTable<T>({
<th
key={col.header}
onClick={sortable ? () => handleSort(col.header) : undefined}
className={`px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider ${col.className ?? ''} ${sortable ? 'cursor-pointer select-none hover:text-gray-700 dark:hover:text-gray-200' : ''}`}
className={`px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider ${col.className ?? ''} ${sortable ? 'cursor-pointer select-none hover:text-text-primary' : ''}`}
>
<span className="inline-flex items-center gap-1">
{col.header}
@@ -127,14 +127,12 @@ export function AdminTable<T>({
})}
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
<tbody className="bg-surface-0 divide-y divide-border-default">
{sortedData.map((row) => (
<tr
key={keyFn(row)}
onClick={onRowClick ? () => onRowClick(row) : undefined}
className={
onRowClick ? 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800' : ''
}
className={onRowClick ? 'cursor-pointer hover:bg-surface-2' : ''}
>
{columns.map((col) => (
<td

View File

@@ -107,7 +107,7 @@ export function BossBattleFormModal({
<button
type="button"
onClick={onEditTeam}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
className="text-sm text-text-link hover:underline"
>
Edit Team ({boss?.pokemon.length ?? 0})
</button>
@@ -123,7 +123,7 @@ export function BossBattleFormModal({
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Brock"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -131,7 +131,7 @@ export function BossBattleFormModal({
<select
value={bossType}
onChange={(e) => setBossType(e.target.value as typeof bossType)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
{BOSS_TYPES.map((t) => (
<option key={t.value} value={t.value}>
@@ -145,7 +145,7 @@ export function BossBattleFormModal({
<select
value={specialtyType}
onChange={(e) => setSpecialtyType(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600 capitalize"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default capitalize"
>
<option value="">None</option>
{POKEMON_TYPES.map((t) => (
@@ -165,7 +165,7 @@ export function BossBattleFormModal({
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="e.g. Pewter City Gym"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
@@ -178,7 +178,7 @@ export function BossBattleFormModal({
min={1}
value={levelCap}
onChange={(e) => setLevelCap(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -189,7 +189,7 @@ export function BossBattleFormModal({
min={1}
value={order}
onChange={(e) => setOrder(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
</div>
@@ -202,7 +202,7 @@ export function BossBattleFormModal({
value={section}
onChange={(e) => setSection(e.target.value)}
placeholder="e.g. Main Story, Endgame"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
{games && games.length > 1 && (
@@ -211,7 +211,7 @@ export function BossBattleFormModal({
<select
value={gameId}
onChange={(e) => setGameId(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
<option value="">All games</option>
{games.map((g) => (
@@ -229,7 +229,7 @@ export function BossBattleFormModal({
<select
value={afterRouteId}
onChange={(e) => setAfterRouteId(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
<option value="">None</option>
{sortedRoutes.map((r) => (
@@ -248,7 +248,7 @@ export function BossBattleFormModal({
value={badgeName}
onChange={(e) => setBadgeName(e.target.value)}
placeholder="Optional"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -258,7 +258,7 @@ export function BossBattleFormModal({
value={badgeImageUrl}
onChange={(e) => setBadgeImageUrl(e.target.value)}
placeholder="Optional"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
</div>
@@ -270,7 +270,7 @@ export function BossBattleFormModal({
value={spriteUrl}
onChange={(e) => setSpriteUrl(e.target.value)}
placeholder="Optional"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
</FormModal>

View File

@@ -150,13 +150,13 @@ export function BossTeamEditor({ boss, onSave, onClose, isSaving }: BossTeamEdit
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="relative bg-surface-1 rounded-lg shadow-xl max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="px-6 py-4 border-b border-border-default">
<h2 className="text-lg font-semibold">{boss.name}'s Team</h2>
</div>
{/* Variant tabs */}
<div className="px-6 pt-3 flex items-center gap-1 flex-wrap border-b border-gray-200 dark:border-gray-700">
<div className="px-6 pt-3 flex items-center gap-1 flex-wrap border-b border-border-default">
{variants.map((v, i) => (
<button
key={v.label ?? '__default'}
@@ -164,8 +164,8 @@ export function BossTeamEditor({ boss, onSave, onClose, isSaving }: BossTeamEdit
onClick={() => setActiveTab(i)}
className={`px-3 py-1.5 text-sm font-medium rounded-t-md border border-b-0 transition-colors ${
activeTab === i
? 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100'
: 'bg-gray-50 dark:bg-gray-700 border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
? 'bg-surface-1 border-border-default text-text-primary'
: 'bg-surface-2 border-transparent text-text-tertiary hover:text-text-secondary'
}`}
>
{v.label ?? 'Default'}
@@ -187,7 +187,7 @@ export function BossTeamEditor({ boss, onSave, onClose, isSaving }: BossTeamEdit
<button
type="button"
onClick={() => setShowAddVariant(true)}
className="px-2 py-1.5 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
className="px-2 py-1.5 text-sm text-text-link hover:text-blue-700 dark:hover:text-blue-300"
title="Add variant"
>
+
@@ -206,13 +206,13 @@ export function BossTeamEditor({ boss, onSave, onClose, isSaving }: BossTeamEdit
if (e.key === 'Escape') setShowAddVariant(false)
}}
placeholder="Variant name..."
className="px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 w-40"
className="px-2 py-1 text-sm border rounded bg-surface-2 border-border-default w-40"
autoFocus
/>
<button
type="button"
onClick={addVariant}
className="px-2 py-1 text-sm text-blue-600 dark:text-blue-400"
className="px-2 py-1 text-sm text-text-link"
>
Add
</button>
@@ -247,7 +247,7 @@ export function BossTeamEditor({ boss, onSave, onClose, isSaving }: BossTeamEdit
max={100}
value={slot.level}
onChange={(e) => updateSlot(index, 'level', e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<button
@@ -265,25 +265,25 @@ export function BossTeamEditor({ boss, onSave, onClose, isSaving }: BossTeamEdit
<button
type="button"
onClick={addSlot}
className="w-full py-2 text-sm text-blue-600 dark:text-blue-400 border border-dashed border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700"
className="w-full py-2 text-sm text-text-link border border-dashed border-border-default rounded-md hover:bg-surface-2"
>
+ Add Pokemon
</button>
)}
</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3">
<div className="px-6 py-4 border-t border-border-default flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
>
Cancel
</button>
<button
type="submit"
disabled={isSaving}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500 disabled:opacity-50"
>
{isSaving ? 'Saving...' : 'Save Team'}
</button>

View File

@@ -53,8 +53,8 @@ export function BulkImportModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="relative bg-surface-1 rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="px-6 py-4 border-b border-border-default">
<h2 className="text-lg font-semibold">{title}</h2>
</div>
<form onSubmit={handleSubmit}>
@@ -66,12 +66,12 @@ export function BulkImportModal({
value={json}
onChange={(e) => setJson(e.target.value)}
placeholder={example}
className="w-full px-3 py-2 border rounded-md font-mono text-sm dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md font-mono text-sm bg-surface-2 border-border-default"
/>
</div>
{error && (
<div className="p-3 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-md text-sm">
<div className="p-3 bg-status-failed-bg text-status-failed rounded-md text-sm">
{error}
</div>
)}
@@ -82,7 +82,7 @@ export function BulkImportModal({
{createdLabel}: {result.created}, {updatedLabel}: {result.updated}
</p>
{result.errors.length > 0 && (
<ul className="mt-2 list-disc list-inside text-red-600 dark:text-red-400">
<ul className="mt-2 list-disc list-inside text-status-failed">
{result.errors.map((err, i) => (
<li key={i}>{err}</li>
))}
@@ -91,18 +91,18 @@ export function BulkImportModal({
</div>
)}
</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3">
<div className="px-6 py-4 border-t border-border-default flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
>
Close
</button>
<button
type="submit"
disabled={isSubmitting || !json.trim()}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500 disabled:opacity-50"
>
{isSubmitting ? 'Importing...' : 'Import'}
</button>

View File

@@ -18,17 +18,17 @@ export function DeleteConfirmModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onCancel} />
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="relative bg-surface-1 rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="px-6 py-4">
<h2 className="text-lg font-semibold text-red-600 dark:text-red-400">{title}</h2>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-300">{message}</p>
{error && <p className="mt-2 text-sm text-red-600 dark:text-red-400">{error}</p>}
<h2 className="text-lg font-semibold text-status-failed">{title}</h2>
<p className="mt-2 text-sm text-text-secondary">{message}</p>
{error && <p className="mt-2 text-sm text-status-failed">{error}</p>}
</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3">
<div className="px-6 py-4 border-t border-border-default flex justify-end gap-3">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
>
Cancel
</button>

View File

@@ -74,7 +74,7 @@ export function EvolutionFormModal({
<select
value={trigger}
onChange={(e) => setTrigger(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
{TRIGGER_OPTIONS.map((t) => (
<option key={t} value={t}>
@@ -92,7 +92,7 @@ export function EvolutionFormModal({
value={minLevel}
onChange={(e) => setMinLevel(e.target.value)}
placeholder="Optional"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -102,7 +102,7 @@ export function EvolutionFormModal({
value={item}
onChange={(e) => setItem(e.target.value)}
placeholder="e.g. thunder-stone"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -112,7 +112,7 @@ export function EvolutionFormModal({
value={heldItem}
onChange={(e) => setHeldItem(e.target.value)}
placeholder="Optional"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -122,7 +122,7 @@ export function EvolutionFormModal({
value={condition}
onChange={(e) => setCondition(e.target.value)}
placeholder="e.g. high-happiness, daytime"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -132,7 +132,7 @@ export function EvolutionFormModal({
value={region}
onChange={(e) => setRegion(e.target.value)}
placeholder="e.g. alola (blank = all regions)"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
</FormModal>

View File

@@ -33,14 +33,14 @@ export function FormModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="relative bg-surface-1 rounded-lg shadow-xl max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="px-6 py-4 border-b border-border-default">
<h2 className="text-lg font-semibold">{title}</h2>
{headerExtra}
</div>
<form onSubmit={onSubmit}>
<div className="px-6 py-4 space-y-4">{children}</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex items-center gap-3">
<div className="px-6 py-4 border-t border-border-default flex items-center gap-3">
{onDelete && (
<button
type="button"
@@ -53,7 +53,7 @@ export function FormModal({
}
}}
onBlur={() => setConfirmingDelete(false)}
className="px-4 py-2 text-sm font-medium rounded-md text-red-600 dark:text-red-400 border border-red-300 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 disabled:opacity-50"
className="px-4 py-2 text-sm font-medium rounded-md text-status-failed border border-red-300 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 disabled:opacity-50"
>
{isDeleting ? 'Deleting...' : confirmingDelete ? 'Confirm?' : 'Delete'}
</button>
@@ -62,14 +62,14 @@ export function FormModal({
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500 disabled:opacity-50"
>
{isSubmitting ? 'Saving...' : submitLabel}
</button>

View File

@@ -63,7 +63,7 @@ export function GameFormModal({
isDeleting={isDeleting}
headerExtra={
detailUrl ? (
<Link to={detailUrl} className="text-sm text-blue-600 dark:text-blue-400 hover:underline">
<Link to={detailUrl} className="text-sm text-text-link hover:underline">
View Routes & Bosses
</Link>
) : undefined
@@ -76,7 +76,7 @@ export function GameFormModal({
required
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -89,7 +89,7 @@ export function GameFormModal({
setSlug(e.target.value)
setAutoSlug(false)
}}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div className="grid grid-cols-2 gap-4">
@@ -101,7 +101,7 @@ export function GameFormModal({
min={1}
value={generation}
onChange={(e) => setGeneration(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -111,7 +111,7 @@ export function GameFormModal({
required
value={region}
onChange={(e) => setRegion(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
</div>
@@ -122,7 +122,7 @@ export function GameFormModal({
value={boxArtUrl}
onChange={(e) => setBoxArtUrl(e.target.value)}
placeholder="Optional"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -132,7 +132,7 @@ export function GameFormModal({
value={releaseYear}
onChange={(e) => setReleaseYear(e.target.value)}
placeholder="Optional"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
</FormModal>

View File

@@ -86,16 +86,16 @@ export function PokemonFormModal({
`px-3 py-1.5 text-sm font-medium rounded-t-md border-b-2 transition-colors ${
activeTab === tab
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
: 'border-transparent text-text-tertiary hover:text-text-secondary'
}`
return (
<>
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full mx-4 max-h-[90vh] flex flex-col">
<div className="relative bg-surface-1 rounded-lg shadow-xl max-w-lg w-full mx-4 max-h-[90vh] flex flex-col">
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 shrink-0">
<div className="px-6 py-4 border-b border-border-default shrink-0">
<h2 className="text-lg font-semibold">{pokemon ? 'Edit Pokemon' : 'Add Pokemon'}</h2>
{isEdit && (
<div className="flex gap-1 mt-2">
@@ -125,7 +125,7 @@ export function PokemonFormModal({
min={1}
value={pokeapiId}
onChange={(e) => setPokeapiId(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -136,7 +136,7 @@ export function PokemonFormModal({
min={1}
value={nationalDex}
onChange={(e) => setNationalDex(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -146,7 +146,7 @@ export function PokemonFormModal({
required
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -157,7 +157,7 @@ export function PokemonFormModal({
value={types}
onChange={(e) => setTypes(e.target.value)}
placeholder="Fire, Flying"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -167,11 +167,11 @@ export function PokemonFormModal({
value={spriteUrl}
onChange={(e) => setSpriteUrl(e.target.value)}
placeholder="Optional"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex items-center gap-3 shrink-0">
<div className="px-6 py-4 border-t border-border-default flex items-center gap-3 shrink-0">
{onDelete && (
<button
type="button"
@@ -184,7 +184,7 @@ export function PokemonFormModal({
}
}}
onBlur={() => setConfirmingDelete(false)}
className="px-4 py-2 text-sm font-medium rounded-md text-red-600 dark:text-red-400 border border-red-300 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 disabled:opacity-50"
className="px-4 py-2 text-sm font-medium rounded-md text-status-failed border border-red-300 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 disabled:opacity-50"
>
{isDeleting ? 'Deleting...' : confirmingDelete ? 'Confirm?' : 'Delete'}
</button>
@@ -193,14 +193,14 @@ export function PokemonFormModal({
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500 disabled:opacity-50"
>
{isSubmitting ? 'Saving...' : 'Save'}
</button>
@@ -212,11 +212,9 @@ export function PokemonFormModal({
{activeTab === 'evolutions' && (
<div className="flex flex-col min-h-0 flex-1">
<div className="px-6 py-4 overflow-y-auto">
{evolutionsLoading && (
<p className="text-sm text-gray-500 dark:text-gray-400">Loading...</p>
)}
{evolutionsLoading && <p className="text-sm text-text-tertiary">Loading...</p>}
{!evolutionsLoading && (!evolutionChain || evolutionChain.length === 0) && (
<p className="text-sm text-gray-500 dark:text-gray-400">No evolutions</p>
<p className="text-sm text-text-tertiary">No evolutions</p>
)}
{!evolutionsLoading && evolutionChain && evolutionChain.length > 0 && (
<div className="space-y-1">
@@ -225,22 +223,20 @@ export function PokemonFormModal({
key={evo.id}
type="button"
onClick={() => setEditingEvolution(evo)}
className="w-full text-left text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded px-2 py-1.5 -mx-2 transition-colors"
className="w-full text-left text-sm text-text-tertiary hover:bg-surface-2 rounded px-2 py-1.5 -mx-2 transition-colors"
>
{evo.fromPokemon.name} {evo.toPokemon.name}{' '}
<span className="text-gray-400 dark:text-gray-500">
({formatEvolutionMethod(evo)})
</span>
<span className="text-text-muted">({formatEvolutionMethod(evo)})</span>
</button>
))}
</div>
)}
</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end shrink-0">
<div className="px-6 py-4 border-t border-border-default flex justify-end shrink-0">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
>
Close
</button>
@@ -252,32 +248,30 @@ export function PokemonFormModal({
{activeTab === 'encounters' && (
<div className="flex flex-col min-h-0 flex-1">
<div className="px-6 py-4 overflow-y-auto">
{encountersLoading && (
<p className="text-sm text-gray-500 dark:text-gray-400">Loading...</p>
)}
{encountersLoading && <p className="text-sm text-text-tertiary">Loading...</p>}
{!encountersLoading && (!encounterLocations || encounterLocations.length === 0) && (
<p className="text-sm text-gray-500 dark:text-gray-400">No encounters</p>
<p className="text-sm text-text-tertiary">No encounters</p>
)}
{!encountersLoading && encounterLocations && encounterLocations.length > 0 && (
<div className="space-y-3">
{encounterLocations.map((game) => (
<div key={game.gameId}>
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<div className="text-sm font-medium text-text-secondary mb-1">
{game.gameName}
</div>
<div className="space-y-0.5 pl-2">
{game.encounters.map((enc, i) => (
<div
key={i}
className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1"
className="text-sm text-text-tertiary flex items-center gap-1"
>
<Link
to={`/admin/games/${game.gameId}/routes/${enc.routeId}`}
className="text-blue-600 dark:text-blue-400 hover:underline"
className="text-text-link hover:underline"
>
{enc.routeName}
</Link>
<span className="text-gray-400 dark:text-gray-500">
<span className="text-text-muted">
{enc.encounterMethod}, Lv. {enc.minLevel}{enc.maxLevel}
</span>
</div>
@@ -288,11 +282,11 @@ export function PokemonFormModal({
</div>
)}
</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end shrink-0">
<div className="px-6 py-4 border-t border-border-default flex justify-end shrink-0">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
>
Close
</button>

View File

@@ -44,11 +44,11 @@ export function PokemonSelector({
}}
onFocus={() => setOpen(true)}
placeholder="Search pokemon..."
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
{selectedId && <input type="hidden" name={label} value={selectedId} required />}
{open && pokemon.length > 0 && (
<ul className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-md shadow-lg max-h-48 overflow-y-auto">
<ul className="absolute z-10 mt-1 w-full bg-surface-1 border border-border-default rounded-md shadow-lg max-h-48 overflow-y-auto">
{pokemon.map((p) => (
<li
key={p.id}
@@ -57,7 +57,7 @@ export function PokemonSelector({
setSearch(p.name)
setOpen(false)
}}
className={`px-3 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2 ${
className={`px-3 py-2 cursor-pointer hover:bg-surface-2 text-sm flex items-center gap-2 ${
p.id === selectedId ? 'bg-blue-50 dark:bg-blue-900/30' : ''
}`}
>

View File

@@ -84,7 +84,7 @@ export function RouteEncounterFormModal({
setSelectedMethod(e.target.value)
if (e.target.value !== 'other') setCustomMethod('')
}}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
<option value="">Select method...</option>
{METHOD_ORDER.map((m) => (
@@ -108,7 +108,7 @@ export function RouteEncounterFormModal({
value={customMethod}
onChange={(e) => setCustomMethod(e.target.value)}
placeholder="Custom method name"
className="w-full mt-2 px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full mt-2 px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
)}
</div>
@@ -121,7 +121,7 @@ export function RouteEncounterFormModal({
max={100}
value={encounterRate}
onChange={(e) => setEncounterRate(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div className="grid grid-cols-2 gap-4">
@@ -134,7 +134,7 @@ export function RouteEncounterFormModal({
max={100}
value={minLevel}
onChange={(e) => setMinLevel(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -146,7 +146,7 @@ export function RouteEncounterFormModal({
max={100}
value={maxLevel}
onChange={(e) => setMaxLevel(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
</div>

View File

@@ -49,7 +49,7 @@ export function RouteFormModal({
isDeleting={isDeleting}
headerExtra={
detailUrl ? (
<Link to={detailUrl} className="text-sm text-blue-600 dark:text-blue-400 hover:underline">
<Link to={detailUrl} className="text-sm text-text-link hover:underline">
View Encounters
</Link>
) : undefined
@@ -62,7 +62,7 @@ export function RouteFormModal({
required
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -73,7 +73,7 @@ export function RouteFormModal({
min={0}
value={order}
onChange={(e) => setOrder(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
@@ -84,9 +84,9 @@ export function RouteFormModal({
value={pinwheelZone}
onChange={(e) => setPinwheelZone(e.target.value)}
placeholder="None"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
<p className="text-xs text-text-tertiary mt-1">
Routes in the same zone share an encounter when the Pinwheel Clause is active
</p>
</div>

View File

@@ -1 +1,85 @@
@import 'tailwindcss';
/* ── Geist font family (variable, self-hosted) ─────────────────── */
@font-face {
font-family: 'Geist Sans';
src: url('/fonts/GeistSans-Variable.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('/fonts/GeistMono-Variable.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
/* ── Tailwind v4 theme tokens ──────────────────────────────────── */
@theme {
/* Typography */
--font-sans: 'Geist Sans', ui-sans-serif, system-ui, sans-serif;
--font-mono: 'Geist Mono', ui-monospace, 'SFMono-Regular', monospace;
/* Brand surfaces (dark-first) */
--color-surface-0: #0d1117;
--color-surface-1: #161b22;
--color-surface-2: #1c2128;
--color-surface-3: #21262d;
--color-surface-4: #282e36;
/* Brand accent (steel blue from logo) */
--color-accent-50: #e8f4fa;
--color-accent-100: #c5e3f2;
--color-accent-200: #9dcde6;
--color-accent-300: #7eb0ce;
--color-accent-400: #5c95b8;
--color-accent-500: #4a7d9e;
--color-accent-600: #395e73;
--color-accent-700: #2d4a5c;
--color-accent-800: #1f3340;
--color-accent-900: #142129;
/* Text on dark */
--color-text-primary: #e6edf3;
--color-text-secondary: #7d8590;
--color-text-tertiary: #484f58;
--color-text-link: #7eb0ce;
/* Borders */
--color-border-default: rgba(255, 255, 255, 0.08);
--color-border-muted: rgba(255, 255, 255, 0.04);
--color-border-accent: rgba(126, 176, 206, 0.3);
/* Status (tuned for dark surfaces) */
--color-status-alive: #2ea043;
--color-status-alive-bg: rgba(46, 160, 67, 0.15);
--color-status-dead: #da3633;
--color-status-dead-bg: rgba(218, 54, 51, 0.15);
--color-status-active: #3fb950;
--color-status-active-bg: rgba(63, 185, 80, 0.15);
--color-status-completed: #58a6ff;
--color-status-completed-bg: rgba(88, 166, 255, 0.15);
--color-status-failed: #f85149;
--color-status-failed-bg: rgba(248, 81, 73, 0.15);
}
/* ── Base layer ────────────────────────────────────────────────── */
@layer base {
html {
color-scheme: dark;
}
body {
@apply bg-surface-0 text-text-primary font-sans antialiased;
}
::selection {
@apply bg-accent-600/50 text-white;
}
}

View File

@@ -12,7 +12,7 @@ const statusColors: Record<RunStatus, string> = {
}
const statusRing: Record<RunStatus, string> = {
completed: 'ring-blue-500',
completed: 'ring-accent-500',
active: 'ring-green-500 animate-pulse',
failed: 'ring-red-500',
}
@@ -32,18 +32,16 @@ function LegIndicator({ leg }: { leg: GenlockeLegDetail }) {
className={`w-4 h-4 rounded-full ${statusColors[status]} ring-2 ring-offset-2 ring-offset-white dark:ring-offset-gray-900 ${statusRing[status]}`}
/>
) : (
<div className="w-4 h-4 rounded-full bg-gray-300 dark:bg-gray-600" />
<div className="w-4 h-4 rounded-full bg-surface-3" />
)
const content = (
<div className="flex flex-col items-center gap-1 min-w-[80px]">
{dot}
<span className="text-xs font-medium text-gray-700 dark:text-gray-300 text-center leading-tight">
<span className="text-xs font-medium text-text-secondary text-center leading-tight">
{leg.game.name}
</span>
{status && (
<span className="text-[10px] text-gray-500 dark:text-gray-400 capitalize">{status}</span>
)}
{status && <span className="text-[10px] text-text-tertiary capitalize">{status}</span>}
</div>
)
@@ -72,7 +70,7 @@ function PokemonSprite({ pokemon }: { pokemon: RetiredPokemon }) {
}
return (
<div
className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-sm font-bold"
className="w-10 h-10 rounded-full bg-surface-3 flex items-center justify-center text-sm font-bold"
title={pokemon.name}
>
{pokemon.name[0]?.toUpperCase()}
@@ -137,7 +135,7 @@ export function GenlockeDetail() {
if (error || !genlocke) {
return (
<div className="max-w-4xl mx-auto p-8">
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
Failed to load genlocke. Please try again.
</div>
</div>
@@ -157,11 +155,11 @@ export function GenlockeDetail() {
<div className="max-w-4xl mx-auto p-8 space-y-8">
{/* Header */}
<div>
<Link to="/genlockes" className="text-sm text-blue-600 dark:text-blue-400 hover:underline">
<Link to="/genlockes" className="text-sm text-text-link hover:underline">
&larr; Back to Genlockes
</Link>
<div className="flex items-center gap-3 mt-2">
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">{genlocke.name}</h1>
<h1 className="text-3xl font-bold text-text-primary">{genlocke.name}</h1>
<span
className={`px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${statusStyles[genlocke.status]}`}
>
@@ -172,8 +170,8 @@ export function GenlockeDetail() {
{/* Progress Timeline */}
<section>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Progress</h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-text-primary mb-4">Progress</h2>
<div className="bg-surface-1 rounded-lg shadow p-6">
<div className="flex items-start gap-2 overflow-x-auto pb-2">
{genlocke.legs.map((leg, i) => (
<div key={leg.id} className="flex items-center">
@@ -181,7 +179,7 @@ export function GenlockeDetail() {
{i < genlocke.legs.length - 1 && (
<div
className={`h-0.5 w-6 mx-1 mt-[-16px] ${
leg.runStatus === 'completed' ? 'bg-blue-500' : 'bg-gray-300 dark:bg-gray-600'
leg.runStatus === 'completed' ? 'bg-blue-500' : 'bg-surface-3'
}`}
/>
)}
@@ -193,9 +191,7 @@ export function GenlockeDetail() {
{/* Cumulative Stats */}
<section>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Cumulative Stats
</h2>
<h2 className="text-lg font-semibold text-text-primary mb-4">Cumulative Stats</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<StatCard label="Encounters" value={genlocke.stats.totalEncounters} color="blue" />
<StatCard label="Deaths" value={genlocke.stats.totalDeaths} color="red" />
@@ -211,30 +207,24 @@ export function GenlockeDetail() {
{/* Configuration */}
<section>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Configuration
</h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 space-y-4">
<h2 className="text-lg font-semibold text-text-primary mb-4">Configuration</h2>
<div className="bg-surface-1 rounded-lg shadow p-6 space-y-4">
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
Genlocke Rules
</h3>
<h3 className="text-sm font-medium text-text-tertiary mb-2">Genlocke Rules</h3>
<div className="flex flex-wrap gap-1.5">
{genlocke.genlockeRules.retireHoF ? (
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300">
Retire HoF Teams
</span>
) : (
<span className="text-sm text-gray-500 dark:text-gray-400">
<span className="text-sm text-text-tertiary">
No genlocke-specific rules enabled
</span>
)}
</div>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
Nuzlocke Rules
</h3>
<h3 className="text-sm font-medium text-text-tertiary mb-2">Nuzlocke Rules</h3>
<RuleBadges rules={genlocke.nuzlockeRules} />
</div>
</div>
@@ -243,13 +233,11 @@ export function GenlockeDetail() {
{/* Retired Families */}
{genlocke.genlockeRules.retireHoF && retiredByLeg.length > 0 && (
<section>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Retired Families
</h2>
<h2 className="text-lg font-semibold text-text-primary mb-4">Retired Families</h2>
<div className="space-y-3">
{retiredByLeg.map((leg) => (
<div key={leg.legOrder} className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
<div key={leg.legOrder} className="bg-surface-1 rounded-lg shadow p-4">
<h3 className="text-sm font-medium text-text-tertiary mb-2">
Leg {leg.legOrder} &mdash; {leg.gameName}
</h3>
<div className="flex flex-wrap gap-1">
@@ -267,9 +255,7 @@ export function GenlockeDetail() {
{/* Quick Actions */}
<section>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Quick Actions
</h2>
<h2 className="text-lg font-semibold text-text-primary mb-4">Quick Actions</h2>
<div className="flex flex-wrap gap-3">
{activeLeg && (
<Link
@@ -284,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-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
: 'bg-surface-3 text-text-secondary hover:bg-surface-4'
}`}
>
Graveyard
@@ -293,8 +279,8 @@ export function GenlockeDetail() {
onClick={() => setShowLineage((v) => !v)}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
showLineage
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
? 'bg-accent-600 text-white hover:bg-accent-500'
: 'bg-surface-3 text-text-secondary hover:bg-surface-4'
}`}
>
Lineage
@@ -305,9 +291,7 @@ export function GenlockeDetail() {
{/* Graveyard */}
{showGraveyard && (
<section>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Cumulative Graveyard
</h2>
<h2 className="text-lg font-semibold text-text-primary mb-4">Cumulative Graveyard</h2>
<GenlockeGraveyard genlockeId={id} />
</section>
)}
@@ -315,9 +299,7 @@ export function GenlockeDetail() {
{/* Lineage */}
{showLineage && (
<section>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Pokemon Lineages
</h2>
<h2 className="text-lg font-semibold text-text-primary mb-4">Pokemon Lineages</h2>
<GenlockeLineage genlockeId={id} />
</section>
)}

View File

@@ -14,10 +14,10 @@ export function GenlockeList() {
return (
<div className="max-w-4xl mx-auto p-8">
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Your Genlockes</h1>
<h1 className="text-3xl font-bold text-text-primary">Your Genlockes</h1>
<Link
to="/genlockes/new"
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 transition-colors"
>
Start New Genlocke
</Link>
@@ -30,14 +30,14 @@ export function GenlockeList() {
)}
{error && (
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
Failed to load genlockes. Please try again.
</div>
)}
{genlockes && genlockes.length === 0 && (
<div className="text-center py-16">
<p className="text-lg text-gray-500 dark:text-gray-400 mb-4">
<p className="text-lg text-text-tertiary mb-4">
No genlockes yet. Start your first Generation Locke!
</p>
<Link
@@ -55,14 +55,12 @@ export function GenlockeList() {
<Link
key={g.id}
to={`/genlockes/${g.id}`}
className="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow p-4"
className="block bg-surface-1 rounded-lg shadow hover:shadow-md transition-shadow p-4"
>
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{g.name}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
<h2 className="text-lg font-semibold text-text-primary">{g.name}</h2>
<p className="text-sm text-text-tertiary">
{g.currentLegOrder !== null
? `Leg ${g.currentLegOrder} / ${g.totalLegs}`
: `${g.completedLegs} / ${g.totalLegs} legs completed`}

View File

@@ -3,9 +3,9 @@ import { useRuns } from '../hooks/useRuns'
import type { RunStatus } from '../types'
const statusStyles: Record<RunStatus, string> = {
active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300',
completed: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300',
failed: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300',
active: 'bg-status-active-bg text-status-active border border-status-active/20',
completed: 'bg-status-completed-bg text-status-completed border border-status-completed/20',
failed: 'bg-status-failed-bg text-status-failed border border-status-failed/20',
}
export function Home() {
@@ -16,42 +16,46 @@ export function Home() {
return (
<div className="max-w-4xl mx-auto p-8">
<div className="text-center py-12">
<h1 className="text-4xl font-bold text-gray-900 dark:text-gray-100 mb-2">
Another Nuzlocke Tracker
</h1>
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8">
Track your Nuzlocke runs with ease
</p>
<Link
to="/runs/new"
className="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg font-medium text-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
>
Start New Run
</Link>
{/* Hero */}
<div className="relative text-center py-16 mb-8 overflow-hidden rounded-2xl bg-gradient-to-b from-surface-2 to-surface-0 border border-border-default">
<img
src="/favicon.svg"
alt=""
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 opacity-[0.04] pointer-events-none select-none"
/>
<div className="relative">
<h1 className="text-4xl sm:text-5xl font-bold tracking-tight text-text-primary mb-3">
Another Nuzlocke Tracker
</h1>
<p className="text-lg text-text-secondary mb-8">Track your Nuzlocke runs with ease</p>
<Link
to="/runs/new"
className="inline-block px-6 py-3 bg-accent-600 text-white rounded-lg font-medium text-lg hover:bg-accent-500 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 focus:ring-offset-surface-0 transition-all active:scale-[0.98]"
>
Start New Run
</Link>
</div>
</div>
{isLoading && (
<div className="flex items-center justify-center py-8">
<div className="w-6 h-6 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-accent-400 border-t-transparent rounded-full animate-spin" />
</div>
)}
{activeRun && (
<div className="mb-8">
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
<h2 className="text-xs font-medium text-text-tertiary uppercase tracking-wider mb-3">
Continue Playing
</h2>
<Link
to={`/runs/${activeRun.id}`}
className="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow p-5 border-l-4 border-green-500"
className="block bg-surface-1 rounded-xl border border-border-default hover:border-accent-400/30 transition-all hover:-translate-y-0.5 p-5"
>
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{activeRun.name}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
<h3 className="text-lg font-semibold text-text-primary">{activeRun.name}</h3>
<p className="text-sm text-text-secondary">
Started{' '}
{new Date(activeRun.startedAt).toLocaleDateString(undefined, {
year: 'numeric',
@@ -60,9 +64,7 @@ export function Home() {
})}
</p>
</div>
<span className="text-blue-600 dark:text-blue-400 font-medium text-sm">
Resume &rarr;
</span>
<span className="text-accent-300 font-medium text-sm">Resume &rarr;</span>
</div>
</Link>
</div>
@@ -71,10 +73,13 @@ export function Home() {
{recentRuns && recentRuns.length > 0 && (
<div>
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
<h2 className="text-xs font-medium text-text-tertiary uppercase tracking-wider">
Recent Runs
</h2>
<Link to="/runs" className="text-sm text-blue-600 dark:text-blue-400 hover:underline">
<Link
to="/runs"
className="text-sm text-text-link hover:text-accent-200 transition-colors"
>
View all
</Link>
</div>
@@ -83,12 +88,12 @@ export function Home() {
<Link
key={run.id}
to={`/runs/${run.id}`}
className="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow p-4"
className="block bg-surface-1 rounded-xl border border-border-default hover:border-border-accent transition-all hover:-translate-y-0.5 p-4"
>
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-gray-900 dark:text-gray-100">{run.name}</h3>
<p className="text-xs text-gray-500 dark:text-gray-400">
<h3 className="font-medium text-text-primary">{run.name}</h3>
<p className="text-xs text-text-secondary">
{new Date(run.startedAt).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
@@ -109,7 +114,7 @@ export function Home() {
)}
{runs && runs.length === 0 && (
<p className="text-center text-gray-500 dark:text-gray-400">
<p className="text-center text-text-secondary">
No runs yet. Start your first Nuzlocke challenge above!
</p>
)}

View File

@@ -116,21 +116,19 @@ export function NewGenlocke() {
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">New Genlocke</h1>
<p className="text-gray-600 dark:text-gray-400 mb-6">Set up your generational challenge.</p>
<h1 className="text-3xl font-bold text-text-primary mb-2">New Genlocke</h1>
<p className="text-text-tertiary mb-6">Set up your generational challenge.</p>
<StepIndicator currentStep={step} onStepClick={setStep} steps={STEPS} />
{/* Step 1: Name */}
{step === 1 && (
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
Name Your Genlocke
</h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-xl font-semibold text-text-primary mb-4">Name Your Genlocke</h2>
<div className="bg-surface-1 rounded-lg shadow p-6">
<label
htmlFor="genlocke-name"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Genlocke Name
</label>
@@ -139,7 +137,7 @@ export function NewGenlocke() {
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-transparent"
placeholder="My Genlocke"
maxLength={100}
autoFocus
@@ -151,7 +149,7 @@ export function NewGenlocke() {
type="button"
disabled={!name.trim()}
onClick={() => setStep(2)}
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Next
</button>
@@ -162,9 +160,7 @@ export function NewGenlocke() {
{/* Step 2: Game Selection */}
{step === 2 && (
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
Select Games
</h2>
<h2 className="text-xl font-semibold text-text-primary mb-4">Select Games</h2>
{/* Preset buttons */}
<div className="flex gap-3 mb-6">
@@ -188,17 +184,15 @@ export function NewGenlocke() {
className={`flex-1 p-4 rounded-lg border-2 text-left transition-colors ${
isActive
? 'border-blue-600 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
: 'border-border-default hover:border-border-default'
}`}
>
<div
className={`font-medium ${isActive ? 'text-blue-600 dark:text-blue-400' : 'text-gray-900 dark:text-gray-100'}`}
className={`font-medium ${isActive ? 'text-text-link' : 'text-text-primary'}`}
>
{labels[type]}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{descriptions[type]}
</div>
<div className="text-sm text-text-tertiary mt-1">{descriptions[type]}</div>
</button>
)
})}
@@ -212,7 +206,7 @@ export function NewGenlocke() {
{/* Legs list */}
{legs.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow divide-y divide-gray-200 dark:divide-gray-700">
<div className="bg-surface-1 rounded-lg shadow divide-y divide-border-default">
{legs.map((leg, index) => (
<LegRow
key={`${leg.region}-${index}`}
@@ -246,7 +240,7 @@ export function NewGenlocke() {
<button
type="button"
onClick={() => setStep(1)}
className="px-6 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
className="px-6 py-2 text-text-secondary bg-surface-2 rounded-lg font-medium hover:bg-surface-3 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
>
Back
</button>
@@ -254,7 +248,7 @@ export function NewGenlocke() {
type="button"
disabled={legs.length === 0}
onClick={() => setStep(3)}
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Next
</button>
@@ -268,18 +262,16 @@ export function NewGenlocke() {
<RulesConfiguration rules={nuzlockeRules} onChange={setNuzlockeRules} />
{/* Genlocke-specific rules */}
<div className="mt-6 bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
Genlocke Rules
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
<div className="mt-6 bg-surface-1 rounded-lg shadow">
<div className="px-4 py-3 border-b border-border-default">
<h3 className="text-lg font-medium text-text-primary">Genlocke Rules</h3>
<p className="text-sm text-text-tertiary">
Rules specific to the generational challenge
</p>
</div>
<div className="px-4 py-4">
<fieldset>
<legend className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
<legend className="text-sm font-medium text-text-secondary mb-3">
Hall of Fame Pokemon
</legend>
<div className="space-y-3">
@@ -289,13 +281,11 @@ export function NewGenlocke() {
name="hofRule"
checked={!genlockeRules.retireHoF}
onChange={() => setGenlockeRules({ retireHoF: false })}
className="mt-0.5 w-4 h-4 text-blue-600 focus:ring-blue-500"
className="mt-0.5 w-4 h-4 text-blue-600 focus:ring-accent-400"
/>
<div>
<div className="font-medium text-gray-900 dark:text-gray-100">
Keep Hall of Fame
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
<div className="font-medium text-text-primary">Keep Hall of Fame</div>
<div className="text-sm text-text-tertiary">
Pokemon that beat the Elite Four can continue to the next leg
</div>
</div>
@@ -306,13 +296,11 @@ export function NewGenlocke() {
name="hofRule"
checked={genlockeRules.retireHoF}
onChange={() => setGenlockeRules({ retireHoF: true })}
className="mt-0.5 w-4 h-4 text-blue-600 focus:ring-blue-500"
className="mt-0.5 w-4 h-4 text-blue-600 focus:ring-accent-400"
/>
<div>
<div className="font-medium text-gray-900 dark:text-gray-100">
Retire Hall of Fame
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
<div className="font-medium text-text-primary">Retire Hall of Fame</div>
<div className="text-sm text-text-tertiary">
Pokemon that beat the Elite Four are retired and cannot be used in the next
leg
</div>
@@ -324,12 +312,10 @@ export function NewGenlocke() {
</div>
{/* Naming scheme */}
<div className="mt-6 bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
Naming Scheme
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
<div className="mt-6 bg-surface-1 rounded-lg shadow">
<div className="px-4 py-3 border-b border-border-default">
<h3 className="text-lg font-medium text-text-primary">Naming Scheme</h3>
<p className="text-sm text-text-tertiary">
Get nickname suggestions from a themed word list when catching Pokemon. Applied to
all legs.
</p>
@@ -338,7 +324,7 @@ export function NewGenlocke() {
<select
value={namingScheme ?? ''}
onChange={(e) => setNamingScheme(e.target.value || null)}
className="w-full max-w-xs px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full max-w-xs px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-400"
>
<option value="">None (manual nicknames)</option>
{namingCategories?.map((cat) => (
@@ -354,14 +340,14 @@ export function NewGenlocke() {
<button
type="button"
onClick={() => setStep(2)}
className="px-6 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
className="px-6 py-2 text-text-secondary bg-surface-2 rounded-lg font-medium hover:bg-surface-3 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
>
Back
</button>
<button
type="button"
onClick={() => setStep(4)}
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 transition-colors"
>
Next
</button>
@@ -372,32 +358,26 @@ export function NewGenlocke() {
{/* Step 4: Confirm */}
{step === 4 && (
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
Confirm & Start
</h2>
<h2 className="text-xl font-semibold text-text-primary mb-4">Confirm & Start</h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 space-y-4">
<div className="bg-surface-1 rounded-lg shadow p-6 space-y-4">
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Name</h3>
<p className="text-gray-900 dark:text-gray-100 font-medium">{name}</p>
<h3 className="text-sm font-medium text-text-tertiary mb-1">Name</h3>
<p className="text-text-primary font-medium">{name}</p>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
Legs ({legs.length})
</h3>
<div className="border-t border-border-default pt-4">
<h3 className="text-sm font-medium text-text-tertiary mb-2">Legs ({legs.length})</h3>
<ol className="space-y-2">
{legs.map((leg, i) => (
<li key={i} className="flex items-center gap-3">
<span className="text-sm text-gray-400 dark:text-gray-500 w-6 text-right font-mono">
<span className="text-sm text-text-muted w-6 text-right font-mono">
{i + 1}.
</span>
<GameThumb game={leg.game} />
<div>
<span className="text-gray-900 dark:text-gray-100 font-medium">
{leg.game.name}
</span>
<span className="text-sm text-gray-500 dark:text-gray-400 ml-2">
<span className="text-text-primary font-medium">{leg.game.name}</span>
<span className="text-sm text-text-tertiary ml-2">
{leg.region.charAt(0).toUpperCase() + leg.region.slice(1)}
</span>
</div>
@@ -406,24 +386,24 @@ export function NewGenlocke() {
</ol>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Rules</h3>
<div className="border-t border-border-default pt-4">
<h3 className="text-sm font-medium text-text-tertiary mb-1">Rules</h3>
<dl className="space-y-1 text-sm">
<div className="flex justify-between">
<dt className="text-gray-600 dark:text-gray-400">Nuzlocke Rules</dt>
<dd className="text-gray-900 dark:text-gray-100 font-medium">
<dt className="text-text-tertiary">Nuzlocke Rules</dt>
<dd className="text-text-primary font-medium">
{enabledRuleCount} of {totalRuleCount} enabled
</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-600 dark:text-gray-400">Hall of Fame</dt>
<dd className="text-gray-900 dark:text-gray-100 font-medium">
<dt className="text-text-tertiary">Hall of Fame</dt>
<dd className="text-text-primary font-medium">
{genlockeRules.retireHoF ? 'Retire' : 'Keep'}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-600 dark:text-gray-400">Naming Scheme</dt>
<dd className="text-gray-900 dark:text-gray-100 font-medium">
<dt className="text-text-tertiary">Naming Scheme</dt>
<dd className="text-text-primary font-medium">
{namingScheme
? namingScheme.charAt(0).toUpperCase() + namingScheme.slice(1)
: 'None'}
@@ -434,7 +414,7 @@ export function NewGenlocke() {
</div>
{createGenlocke.error && (
<div className="mt-4 rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
<div className="mt-4 rounded-lg bg-status-failed-bg p-4 text-status-failed">
Failed to create genlocke. Please try again.
</div>
)}
@@ -443,7 +423,7 @@ export function NewGenlocke() {
<button
type="button"
onClick={() => setStep(3)}
className="px-6 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
className="px-6 py-2 text-text-secondary bg-surface-2 rounded-lg font-medium hover:bg-surface-3 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
>
Back
</button>
@@ -451,7 +431,7 @@ export function NewGenlocke() {
type="button"
disabled={createGenlocke.isPending}
onClick={handleCreate}
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{createGenlocke.isPending ? 'Creating...' : 'Start Genlocke'}
</button>
@@ -486,12 +466,12 @@ function LegRow({
return (
<div className="flex items-center gap-3 px-4 py-3">
<span className="text-sm text-gray-400 dark:text-gray-500 w-6 text-right font-mono shrink-0">
<span className="text-sm text-text-muted w-6 text-right font-mono shrink-0">
{index + 1}.
</span>
<GameThumb game={leg.game} />
<div className="flex-1 min-w-0">
<div className="text-sm text-gray-500 dark:text-gray-400">
<div className="text-sm text-text-tertiary">
{leg.region.charAt(0).toUpperCase() + leg.region.slice(1)}
</div>
{games.length > 1 ? (
@@ -501,7 +481,7 @@ function LegRow({
const game = games.find((g) => g.id === Number(e.target.value))
if (game) onGameChange(game)
}}
className="mt-1 w-full max-w-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
className="mt-1 w-full max-w-xs px-2 py-1 rounded border border-border-default bg-surface-2 text-text-primary text-sm focus:outline-none focus:ring-2 focus:ring-accent-400"
>
{games.map((g) => (
<option key={g.id} value={g.id}>
@@ -510,7 +490,7 @@ function LegRow({
))}
</select>
) : (
<div className="text-gray-900 dark:text-gray-100 font-medium">{leg.game.name}</div>
<div className="text-text-primary font-medium">{leg.game.name}</div>
)}
</div>
<div className="flex items-center gap-1 shrink-0">
@@ -518,7 +498,7 @@ function LegRow({
type="button"
disabled={index === 0}
onClick={() => onMove('up')}
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:cursor-not-allowed"
className="p-1 text-gray-400 hover:text-text-secondary disabled:opacity-30 disabled:cursor-not-allowed"
title="Move up"
>
<svg
@@ -535,7 +515,7 @@ function LegRow({
type="button"
disabled={index === total - 1}
onClick={() => onMove('down')}
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:cursor-not-allowed"
className="p-1 text-gray-400 hover:text-text-secondary disabled:opacity-30 disabled:cursor-not-allowed"
title="Move down"
>
<svg
@@ -583,7 +563,7 @@ function AddLegDropdown({
<button
type="button"
onClick={() => setOpen(true)}
className="flex items-center gap-2 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium"
className="flex items-center gap-2 text-sm text-text-link hover:text-blue-700 dark:hover:text-blue-300 font-medium"
>
<svg
className="w-4 h-4"
@@ -600,10 +580,8 @@ function AddLegDropdown({
}
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-3">
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Select a region to add
</div>
<div className="bg-surface-1 rounded-lg shadow p-3">
<div className="text-sm font-medium text-text-secondary mb-2">Select a region to add</div>
<div className="flex flex-wrap gap-2">
{regions.map((region) => (
<button
@@ -613,7 +591,7 @@ function AddLegDropdown({
onAdd(region)
setOpen(false)
}}
className="px-3 py-1.5 rounded-md text-sm bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className="px-3 py-1.5 rounded-md text-sm bg-surface-2 text-text-primary hover:bg-surface-3 transition-colors"
>
{region.name.charAt(0).toUpperCase() + region.name.slice(1)}
</button>
@@ -621,7 +599,7 @@ function AddLegDropdown({
<button
type="button"
onClick={() => setOpen(false)}
className="px-3 py-1.5 rounded-md text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
className="px-3 py-1.5 rounded-md text-sm text-text-tertiary hover:text-text-secondary"
>
Cancel
</button>

View File

@@ -57,27 +57,23 @@ export function NewRun() {
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">New Nuzlocke Run</h1>
<p className="text-gray-600 dark:text-gray-400 mb-6">Set up your run in a few steps.</p>
<h1 className="text-3xl font-bold text-text-primary mb-2">New Nuzlocke Run</h1>
<p className="text-text-tertiary mb-6">Set up your run in a few steps.</p>
<StepIndicator currentStep={step} onStepClick={setStep} />
{step === 1 && (
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
Choose a Game
</h2>
<h2 className="text-xl font-semibold text-text-primary mb-4">Choose a Game</h2>
<div className="sticky top-0 z-10 bg-gray-50 dark:bg-gray-900 py-3 mb-4 border-b border-gray-200 dark:border-gray-700">
<div className="sticky top-0 z-10 bg-surface-0 py-3 mb-4 border-b border-border-default">
{selectedGame ? (
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<SelectedGameThumb game={selectedGame} />
<div>
<p className="font-medium text-gray-900 dark:text-gray-100">
{selectedGame.name}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
<p className="font-medium text-text-primary">{selectedGame.name}</p>
<p className="text-sm text-text-tertiary">
{selectedGame.region.charAt(0).toUpperCase() + selectedGame.region.slice(1)}
</p>
</div>
@@ -85,16 +81,14 @@ export function NewRun() {
<button
type="button"
onClick={() => setStep(2)}
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 transition-colors"
>
Next
</button>
</div>
) : (
<div className="flex items-center justify-between">
<p className="text-sm text-gray-500 dark:text-gray-400">
Select a game to continue
</p>
<p className="text-sm text-text-tertiary">Select a game to continue</p>
<button
type="button"
disabled
@@ -113,7 +107,7 @@ export function NewRun() {
)}
{error && (
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
Failed to load games. Please try again.
</div>
)}
@@ -137,14 +131,14 @@ export function NewRun() {
<button
type="button"
onClick={() => setStep(1)}
className="px-6 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
className="px-6 py-2 text-text-secondary bg-surface-2 rounded-lg font-medium hover:bg-surface-3 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
>
Back
</button>
<button
type="button"
onClick={() => setStep(3)}
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 transition-colors"
>
Next
</button>
@@ -154,15 +148,13 @@ export function NewRun() {
{step === 3 && (
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
Name Your Run
</h2>
<h2 className="text-xl font-semibold text-text-primary mb-4">Name Your Run</h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 space-y-4">
<div className="bg-surface-1 rounded-lg shadow p-6 space-y-4">
<div>
<label
htmlFor="run-name"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Run Name
</label>
@@ -171,7 +163,7 @@ export function NewRun() {
type="text"
value={runName}
onChange={(e) => setRunName(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-transparent"
placeholder="My Nuzlocke Run"
/>
</div>
@@ -180,7 +172,7 @@ export function NewRun() {
<div>
<label
htmlFor="naming-scheme"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
className="block text-sm font-medium text-text-secondary mb-1"
>
Naming Scheme
</label>
@@ -188,7 +180,7 @@ export function NewRun() {
id="naming-scheme"
value={namingScheme ?? ''}
onChange={(e) => setNamingScheme(e.target.value || null)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-transparent"
>
<option value="">None (manual nicknames)</option>
{namingCategories.map((cat) => (
@@ -197,37 +189,35 @@ export function NewRun() {
</option>
))}
</select>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
<p className="mt-1 text-xs text-text-tertiary">
Get nickname suggestions from a themed word list when catching Pokemon.
</p>
</div>
)}
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Summary</h3>
<div className="border-t border-border-default pt-4">
<h3 className="text-sm font-medium text-text-tertiary mb-2">Summary</h3>
<dl className="space-y-1 text-sm">
<div className="flex justify-between">
<dt className="text-gray-600 dark:text-gray-400">Game</dt>
<dd className="text-gray-900 dark:text-gray-100 font-medium">
{selectedGame?.name}
</dd>
<dt className="text-text-tertiary">Game</dt>
<dd className="text-text-primary font-medium">{selectedGame?.name}</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-600 dark:text-gray-400">Region</dt>
<dd className="text-gray-900 dark:text-gray-100 font-medium">
<dt className="text-text-tertiary">Region</dt>
<dd className="text-text-primary font-medium">
{selectedGame &&
selectedGame.region.charAt(0).toUpperCase() + selectedGame.region.slice(1)}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-600 dark:text-gray-400">Rules</dt>
<dd className="text-gray-900 dark:text-gray-100 font-medium">
<dt className="text-text-tertiary">Rules</dt>
<dd className="text-text-primary font-medium">
{enabledRuleCount} of {totalRuleCount} enabled
</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-600 dark:text-gray-400">Naming Scheme</dt>
<dd className="text-gray-900 dark:text-gray-100 font-medium">
<dt className="text-text-tertiary">Naming Scheme</dt>
<dd className="text-text-primary font-medium">
{namingScheme
? namingScheme.charAt(0).toUpperCase() + namingScheme.slice(1)
: 'None'}
@@ -238,7 +228,7 @@ export function NewRun() {
</div>
{createRun.error && (
<div className="mt-4 rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
<div className="mt-4 rounded-lg bg-status-failed-bg p-4 text-status-failed">
Failed to create run. Please try again.
</div>
)}
@@ -247,7 +237,7 @@ export function NewRun() {
<button
type="button"
onClick={() => setStep(2)}
className="px-6 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
className="px-6 py-2 text-text-secondary bg-surface-2 rounded-lg font-medium hover:bg-surface-3 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
>
Back
</button>
@@ -255,7 +245,7 @@ export function NewRun() {
type="button"
disabled={!runName.trim() || createRun.isPending}
onClick={handleCreate}
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{createRun.isPending ? 'Creating...' : 'Create Run'}
</button>

View File

@@ -86,7 +86,7 @@ export function RunDashboard() {
if (error || !run) {
return (
<div className="max-w-4xl mx-auto p-8">
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
Failed to load run. It may not exist.
</div>
<Link to="/runs" className="inline-block mt-4 text-blue-600 hover:underline">
@@ -106,14 +106,14 @@ export function RunDashboard() {
<div className="mb-6">
<Link
to="/runs"
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 mb-2 inline-block"
className="text-sm text-text-tertiary hover:text-text-primary mb-2 inline-block"
>
&larr; All Runs
</Link>
<div className="flex items-start justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">{run.name}</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
<h1 className="text-3xl font-bold text-text-primary">{run.name}</h1>
<p className="text-text-tertiary mt-1">
{run.game.name} &middot;{' '}
{run.game.region.charAt(0).toUpperCase() + run.game.region.slice(1)} &middot; Started{' '}
{new Date(run.startedAt).toLocaleDateString(undefined, {
@@ -137,7 +137,7 @@ export function RunDashboard() {
className={`rounded-lg p-4 mb-6 ${
run.status === 'completed'
? 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
: 'bg-status-failed-bg border border-red-200 dark:border-red-800'
}`}
>
<div className="flex items-center gap-3">
@@ -156,9 +156,7 @@ export function RunDashboard() {
</p>
<p
className={`text-sm ${
run.status === 'completed'
? 'text-blue-600 dark:text-blue-400'
: 'text-red-600 dark:text-red-400'
run.status === 'completed' ? 'text-text-link' : 'text-status-failed'
}`}
>
{run.completedAt && (
@@ -189,21 +187,19 @@ export function RunDashboard() {
{/* Rules */}
<div className="mb-6">
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Active Rules</h2>
<h2 className="text-sm font-medium text-text-tertiary mb-2">Active Rules</h2>
<RuleBadges rules={run.rules} />
</div>
{/* Naming Scheme */}
{namingCategories && namingCategories.length > 0 && (
<div className="mb-6">
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
Naming Scheme
</h2>
<h2 className="text-sm font-medium text-text-tertiary mb-2">Naming Scheme</h2>
{isActive ? (
<select
value={run.namingScheme ?? ''}
onChange={(e) => updateRun.mutate({ namingScheme: e.target.value || null })}
className="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
className="text-sm border border-border-default rounded-lg px-3 py-1.5 bg-surface-1 text-text-primary"
>
<option value="">None</option>
{namingCategories.map((cat) => (
@@ -213,7 +209,7 @@ export function RunDashboard() {
))}
</select>
) : (
<span className="text-sm text-gray-900 dark:text-gray-100">
<span className="text-sm text-text-primary">
{run.namingScheme
? run.namingScheme.charAt(0).toUpperCase() + run.namingScheme.slice(1)
: 'None'}
@@ -225,14 +221,14 @@ export function RunDashboard() {
{/* Active Team */}
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
<h2 className="text-lg font-semibold text-text-primary">
{isActive ? 'Active Team' : 'Final Team'}
</h2>
{alive.length > 1 && (
<select
value={teamSort}
onChange={(e) => setTeamSort(e.target.value as TeamSortKey)}
className="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
className="text-sm border border-border-default rounded-lg px-3 py-1.5 bg-surface-1 text-text-primary"
>
<option value="route">Route Order</option>
<option value="level">Catch Level</option>
@@ -242,7 +238,7 @@ export function RunDashboard() {
)}
</div>
{alive.length === 0 ? (
<p className="text-gray-500 dark:text-gray-400 text-sm">
<p className="text-text-tertiary text-sm">
No pokemon caught yet head to encounters to start building your team!
</p>
) : (
@@ -261,7 +257,7 @@ export function RunDashboard() {
{/* Graveyard */}
{dead.length > 0 && (
<div className="mb-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Graveyard</h2>
<h2 className="text-lg font-semibold text-text-primary mb-3">Graveyard</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{dead.map((enc) => (
<PokemonCard
@@ -281,13 +277,13 @@ export function RunDashboard() {
<>
<Link
to={`/runs/${runId}/encounters`}
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 transition-colors"
>
Log Encounter
</Link>
<button
onClick={() => setShowEndRun(true)}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg font-medium hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
className="px-4 py-2 border border-border-default rounded-lg font-medium hover:bg-surface-2 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
>
End Run
</button>

View File

@@ -93,9 +93,9 @@ const statusIndicator: Record<RouteStatus, { dot: string; label: string; bg: str
missed: {
dot: 'bg-gray-400',
label: 'Missed',
bg: 'bg-gray-50 dark:bg-gray-900/10',
bg: 'bg-surface-0/10',
},
none: { dot: 'bg-gray-300 dark:bg-gray-600', label: '', bg: '' },
none: { dot: 'bg-surface-3', label: '', bg: '' },
}
/**
@@ -222,7 +222,7 @@ function BossTeamPreview({
className={`px-2 py-0.5 text-xs font-medium rounded-full transition-colors ${
selectedVariant === label
? 'bg-blue-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
: 'bg-surface-2 text-text-secondary hover:bg-surface-3'
}`}
>
{label}
@@ -238,10 +238,10 @@ function BossTeamPreview({
{bp.pokemon.spriteUrl ? (
<img src={bp.pokemon.spriteUrl} alt={bp.pokemon.name} className="w-20 h-20" />
) : (
<div className="w-20 h-20 bg-gray-200 dark:bg-gray-700 rounded-full" />
<div className="w-20 h-20 bg-surface-3 rounded-full" />
)}
<div className="flex flex-col items-start gap-0.5">
<span className="text-xs text-gray-500 dark:text-gray-400">Lvl {bp.level}</span>
<span className="text-xs text-text-tertiary">Lvl {bp.level}</span>
<ConditionBadge condition={bp.conditionLabel} size="xs" />
</div>
</div>
@@ -302,20 +302,18 @@ function RouteGroup({
const hasGroupEncounter = groupEncounter !== null
return (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div className="border border-border-default rounded-lg overflow-hidden">
{/* Group header */}
<button
type="button"
onClick={onToggleExpand}
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-700/50 ${si.bg}`}
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-surface-2/50 ${si.bg}`}
>
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${si.dot}`} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 flex items-center gap-2">
<div className="text-sm font-medium text-text-primary flex items-center gap-2">
{group.name}
<span className="text-xs text-gray-400 dark:text-gray-500">
({group.children.length} areas)
</span>
<span className="text-xs text-text-muted">({group.children.length} areas)</span>
</div>
{groupEncounter && (
<div className="flex items-center gap-2 mt-0.5">
@@ -326,7 +324,7 @@ function RouteGroup({
className="w-10 h-10"
/>
)}
<span className="text-xs text-gray-500 dark:text-gray-400 capitalize">
<span className="text-xs text-text-tertiary capitalize">
{groupEncounter.nickname ?? groupEncounter.pokemon.name}
{groupEncounter.status === 'caught' &&
groupEncounter.faintLevel !== null &&
@@ -335,7 +333,7 @@ function RouteGroup({
</div>
)}
</div>
<span className="text-xs text-gray-400 dark:text-gray-500 shrink-0">{si.label}</span>
<span className="text-xs text-text-muted shrink-0">{si.label}</span>
<svg
className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
fill="none"
@@ -348,7 +346,7 @@ function RouteGroup({
{/* Expanded children */}
{isExpanded && (
<div className="border-t border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-800/50">
<div className="border-t border-border-default bg-surface-1/50">
{group.children.map((child) => {
const childEncounter = encounterByRoute.get(child.id)
const childStatus = getRouteStatus(childEncounter)
@@ -371,14 +369,12 @@ function RouteGroup({
onClick={() => !isDisabled && onRouteClick(child)}
disabled={isDisabled}
className={`w-full flex items-center gap-3 px-4 py-2 pl-8 text-left transition-colors ${
isDisabled
? 'opacity-50 cursor-not-allowed'
: 'hover:bg-gray-100 dark:hover:bg-gray-700/50'
isDisabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-surface-2/50'
} ${childSi.bg}`}
>
<span className={`w-2 h-2 rounded-full shrink-0 ${childSi.dot}`} />
<div className="flex-1 min-w-0">
<div className="text-sm text-gray-700 dark:text-gray-300">{child.name}</div>
<div className="text-sm text-text-secondary">{child.name}</div>
{!childEncounter && child.encounterMethods.length > 0 && (
<div className="flex flex-wrap gap-1 mt-0.5">
{child.encounterMethods.map((m) => (
@@ -387,12 +383,8 @@ function RouteGroup({
</div>
)}
</div>
{childEncounter && (
<span className="text-xs text-gray-400 dark:text-gray-500">{childSi.label}</span>
)}
{isDisabled && (
<span className="text-xs text-gray-400 dark:text-gray-500 italic">(locked)</span>
)}
{childEncounter && <span className="text-xs text-text-muted">{childSi.label}</span>}
{isDisabled && <span className="text-xs text-text-muted italic">(locked)</span>}
</button>
)
})}
@@ -674,7 +666,7 @@ export function RunEncounters() {
if (error || !run) {
return (
<div className="max-w-4xl mx-auto p-8">
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
Failed to load run.
</div>
<Link to="/runs" className="inline-block mt-4 text-blue-600 hover:underline">
@@ -792,14 +784,14 @@ export function RunEncounters() {
<div className="mb-6">
<Link
to="/runs"
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 mb-2 inline-block"
className="text-sm text-text-tertiary hover:text-text-primary mb-2 inline-block"
>
&larr; All Runs
</Link>
<div className="flex items-start justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">{run.name}</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
<h1 className="text-3xl font-bold text-text-primary">{run.name}</h1>
<p className="text-text-tertiary mt-1">
{run.game.name} &middot;{' '}
{run.game.region.charAt(0).toUpperCase() + run.game.region.slice(1)} &middot; Started{' '}
{new Date(run.startedAt).toLocaleDateString(undefined, {
@@ -827,7 +819,7 @@ export function RunEncounters() {
{isActive && (
<button
onClick={() => setShowEggModal(true)}
className="px-3 py-1 text-sm border border-green-400 dark:border-green-600 text-green-600 dark:text-green-400 rounded-full font-medium hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors"
className="px-3 py-1 text-sm border border-green-400 dark:border-green-600 text-status-active rounded-full font-medium hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors"
>
&#x1F95A; Log Egg
</button>
@@ -835,7 +827,7 @@ export function RunEncounters() {
{isActive && (
<button
onClick={() => setShowEndRun(true)}
className="px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded-full font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
className="px-3 py-1 text-sm border border-border-default rounded-full font-medium hover:bg-surface-2 transition-colors"
>
End Run
</button>
@@ -855,7 +847,7 @@ export function RunEncounters() {
className={`rounded-lg p-4 mb-6 ${
run.status === 'completed'
? 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
: 'bg-status-failed-bg border border-red-200 dark:border-red-800'
}`}
>
<div className="flex items-center justify-between">
@@ -881,9 +873,7 @@ export function RunEncounters() {
</p>
<p
className={`text-sm ${
run.status === 'completed'
? 'text-blue-600 dark:text-blue-400'
: 'text-red-600 dark:text-red-400'
run.status === 'completed' ? 'text-text-link' : 'text-status-failed'
}`}
>
{run.completedAt && (
@@ -926,7 +916,7 @@ export function RunEncounters() {
}
}}
disabled={advanceLeg.isPending}
className="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 transition-colors"
className="px-4 py-2 text-sm font-medium rounded-lg bg-accent-600 text-white hover:bg-accent-500 disabled:opacity-50 transition-colors"
>
{advanceLeg.isPending ? 'Advancing...' : 'Advance to Next Leg'}
</button>
@@ -936,7 +926,7 @@ export function RunEncounters() {
{run.status === 'completed' && (
<div className="mt-3 pt-3 border-t border-blue-200 dark:border-blue-800">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-blue-600 dark:text-blue-400 uppercase tracking-wider">
<span className="text-xs font-medium text-text-link uppercase tracking-wider">
Hall of Fame
</span>
<button
@@ -956,11 +946,11 @@ export function RunEncounters() {
{dp.spriteUrl ? (
<img src={dp.spriteUrl} alt={dp.name} className="w-12 h-12" />
) : (
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-sm font-bold">
<div className="w-12 h-12 rounded-full bg-surface-3 flex items-center justify-center text-sm font-bold">
{dp.name[0]?.toUpperCase()}
</div>
)}
<span className="text-[10px] text-blue-600 dark:text-blue-400 capitalize mt-0.5">
<span className="text-[10px] text-text-link capitalize mt-0.5">
{enc.nickname || dp.name}
</span>
</div>
@@ -987,24 +977,20 @@ export function RunEncounters() {
{/* Level Cap Bar */}
{run.rules?.levelCaps && sortedBosses.length > 0 && (
<div className="sticky top-0 z-10 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg px-4 py-3 mb-6 shadow-sm">
<div className="sticky top-0 z-10 bg-surface-0 border border-border-default rounded-lg px-4 py-3 mb-6 shadow-sm">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">
<span className="text-sm font-semibold text-text-primary">
Level Cap: {currentLevelCap ?? '—'}
</span>
{nextBoss && (
<span className="text-sm text-gray-500 dark:text-gray-400">
Next: {nextBoss.name}
</span>
<span className="text-sm text-text-tertiary">Next: {nextBoss.name}</span>
)}
{!nextBoss && (
<span className="text-sm text-green-600 dark:text-green-400">
All bosses defeated!
</span>
<span className="text-sm text-status-active">All bosses defeated!</span>
)}
</div>
<span className="text-xs text-gray-400 dark:text-gray-500">
<span className="text-xs text-text-muted">
{defeatedBossIds.size}/{sortedBosses.length} defeated
</span>
</div>
@@ -1032,7 +1018,7 @@ export function RunEncounters() {
className={`w-6 h-6 rounded-full border-2 flex items-center justify-center text-xs font-bold ${
earned
? 'border-yellow-500 bg-yellow-100 dark:bg-yellow-900/40 text-yellow-700 dark:text-yellow-300'
: 'border-gray-300 dark:border-gray-600 text-gray-400'
: 'border-border-default text-gray-400'
}`}
>
{boss.order}
@@ -1048,7 +1034,7 @@ export function RunEncounters() {
{/* Rules */}
<div className="mb-6">
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Active Rules</h2>
<h2 className="text-sm font-medium text-text-tertiary mb-2">Active Rules</h2>
<RuleBadges rules={run.rules} />
</div>
@@ -1061,10 +1047,10 @@ export function RunEncounters() {
onClick={() => setShowTeam(!showTeam)}
className="flex items-center gap-2 group"
>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
<h2 className="text-lg font-semibold text-text-primary">
{isActive ? 'Team' : 'Final Team'}
</h2>
<span className="text-xs text-gray-400 dark:text-gray-500">
<span className="text-xs text-text-muted">
{alive.length} alive
{dead.length > 0 ? `, ${dead.length} dead` : ''}
</span>
@@ -1086,7 +1072,7 @@ export function RunEncounters() {
<select
value={teamSort}
onChange={(e) => setTeamSort(e.target.value as TeamSortKey)}
className="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
className="text-sm border border-border-default rounded-lg px-3 py-1.5 bg-surface-1 text-text-primary"
>
<option value="route">Route Order</option>
<option value="level">Catch Level</option>
@@ -1110,9 +1096,7 @@ export function RunEncounters() {
)}
{dead.length > 0 && (
<>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
Graveyard
</h3>
<h3 className="text-sm font-medium text-text-tertiary mb-2">Graveyard</h3>
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2">
{dead.map((enc) => (
<PokemonCard
@@ -1162,7 +1146,7 @@ export function RunEncounters() {
<div className="mb-4">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-3">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Encounters</h2>
<h2 className="text-lg font-semibold text-text-primary">Encounters</h2>
{isActive && completedCount < totalLocations && (
<button
type="button"
@@ -1181,11 +1165,11 @@ export function RunEncounters() {
</button>
)}
</div>
<span className="text-sm text-gray-500 dark:text-gray-400">
<span className="text-sm text-text-tertiary">
{completedCount} / {totalLocations} locations
</span>
</div>
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div className="h-2 bg-surface-3 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 rounded-full transition-all"
style={{
@@ -1212,7 +1196,7 @@ export function RunEncounters() {
className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
filter === key
? 'bg-blue-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
: 'bg-surface-2 text-text-secondary hover:bg-surface-3'
}`}
>
{label}
@@ -1223,7 +1207,7 @@ export function RunEncounters() {
{/* Route list */}
<div className="space-y-1">
{filteredRoutes.length === 0 && (
<p className="text-gray-500 dark:text-gray-400 text-sm py-4 text-center">
<p className="text-text-tertiary text-sm py-4 text-center">
{filter === 'all'
? 'Click a route to log your first encounter'
: 'No routes match this filter — try a different one'}
@@ -1264,13 +1248,11 @@ export function RunEncounters() {
key={route.id}
type="button"
onClick={() => handleRouteClick(route)}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-700/50 ${si.bg}`}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-left transition-colors hover:bg-surface-2/50 ${si.bg}`}
>
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${si.dot}`} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
{route.name}
</div>
<div className="text-sm font-medium text-text-primary">{route.name}</div>
{encounter ? (
<div className="flex items-center gap-2 mt-0.5">
{encounter.pokemon.spriteUrl && (
@@ -1280,7 +1262,7 @@ export function RunEncounters() {
className="w-10 h-10"
/>
)}
<span className="text-xs text-gray-500 dark:text-gray-400 capitalize">
<span className="text-xs text-text-tertiary capitalize">
{encounter.nickname ?? encounter.pokemon.name}
{encounter.status === 'caught' &&
encounter.faintLevel !== null &&
@@ -1297,9 +1279,7 @@ export function RunEncounters() {
)
)}
</div>
<span className="text-xs text-gray-400 dark:text-gray-500 shrink-0">
{si.label}
</span>
<span className="text-xs text-text-muted shrink-0">{si.label}</span>
</button>
)
})()
@@ -1347,9 +1327,7 @@ export function RunEncounters() {
<div key={`boss-${boss.id}`}>
<div
className={`my-2 rounded-lg border-2 ${bossTypeColors[boss.bossType] ?? bossTypeColors['other']} ${
isDefeated
? 'bg-green-50/50 dark:bg-green-900/10'
: 'bg-white dark:bg-gray-800'
isDefeated ? 'bg-green-50/50 dark:bg-green-900/10' : 'bg-surface-1'
} px-4 py-3`}
>
<div
@@ -1371,15 +1349,15 @@ export function RunEncounters() {
)}
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">
<span className="text-sm font-semibold text-text-primary">
{boss.name}
</span>
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-surface-2 text-text-secondary">
{bossTypeLabel[boss.bossType] ?? boss.bossType}
</span>
{boss.specialtyType && <TypeBadge type={boss.specialtyType} />}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-xs text-text-tertiary">
{boss.location} &middot; Level Cap: {boss.levelCap}
</p>
</div>
@@ -1392,7 +1370,7 @@ export function RunEncounters() {
) : isActive ? (
<button
onClick={() => setSelectedBoss(boss)}
className="px-3 py-1 text-xs font-medium rounded-full bg-blue-600 text-white hover:bg-blue-700 transition-colors"
className="px-3 py-1 text-xs font-medium rounded-full bg-accent-600 text-white hover:bg-accent-500 transition-colors"
>
Battle
</button>
@@ -1406,11 +1384,11 @@ export function RunEncounters() {
</div>
{sectionAfter && (
<div className="flex items-center gap-3 my-4">
<div className="flex-1 h-px bg-gray-300 dark:bg-gray-600" />
<span className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<div className="flex-1 h-px bg-surface-3" />
<span className="text-sm font-semibold text-text-tertiary uppercase tracking-wider">
{sectionAfter}
</span>
<div className="flex-1 h-px bg-gray-300 dark:bg-gray-600" />
<div className="flex-1 h-px bg-surface-3" />
</div>
)}
</div>

View File

@@ -3,9 +3,9 @@ import { useRuns } from '../hooks/useRuns'
import type { RunStatus } from '../types'
const statusStyles: Record<RunStatus, string> = {
active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300',
completed: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300',
failed: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300',
active: 'bg-status-active-bg text-status-active border border-status-active/20',
completed: 'bg-status-completed-bg text-status-completed border border-status-completed/20',
failed: 'bg-status-failed-bg text-status-failed border border-status-failed/20',
}
export function RunList() {
@@ -14,10 +14,10 @@ export function RunList() {
return (
<div className="max-w-4xl mx-auto p-8">
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Your Runs</h1>
<h1 className="text-3xl font-bold text-text-primary">Your Runs</h1>
<Link
to="/runs/new"
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
className="px-4 py-2 bg-accent-600 text-white rounded-lg font-medium hover:bg-accent-500 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 focus:ring-offset-surface-0 transition-all active:scale-[0.98]"
>
Start New Run
</Link>
@@ -25,24 +25,24 @@ export function RunList() {
{isLoading && (
<div className="flex items-center justify-center py-12">
<div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-4 border-accent-400 border-t-transparent rounded-full animate-spin" />
</div>
)}
{error && (
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
<div className="rounded-lg bg-status-failed-bg border border-status-failed/20 p-4 text-status-failed">
Failed to load runs. Please try again.
</div>
)}
{runs && runs.length === 0 && (
<div className="text-center py-16">
<p className="text-lg text-gray-500 dark:text-gray-400 mb-4">
<p className="text-lg text-text-secondary mb-4">
No runs yet. Start your first Nuzlocke!
</p>
<Link
to="/runs/new"
className="inline-block px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
className="inline-block px-6 py-2 bg-accent-600 text-white rounded-lg font-medium hover:bg-accent-500 transition-all active:scale-[0.98]"
>
Start New Run
</Link>
@@ -50,19 +50,17 @@ export function RunList() {
)}
{runs && runs.length > 0 && (
<div className="space-y-3">
<div className="space-y-2">
{runs.map((run) => (
<Link
key={run.id}
to={`/runs/${run.id}`}
className="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow p-4"
className="block bg-surface-1 rounded-xl border border-border-default hover:border-border-accent transition-all hover:-translate-y-0.5 p-4"
>
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{run.name}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
<h2 className="text-lg font-semibold text-text-primary">{run.name}</h2>
<p className="text-sm text-text-secondary">
Started{' '}
{new Date(run.startedAt).toLocaleDateString(undefined, {
year: 'numeric',

View File

@@ -40,24 +40,22 @@ function PokemonList({ title, pokemon }: { title: string; pokemon: PokemonRankin
return (
<div>
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">{title}</h3>
<h3 className="text-sm font-semibold text-text-secondary mb-2">{title}</h3>
{pokemon.length === 0 ? (
<p className="text-sm text-gray-500 dark:text-gray-400">No data</p>
<p className="text-sm text-text-tertiary">No data</p>
) : (
<>
<div className="space-y-1.5">
{visible.map((p, i) => (
<div key={p.pokemonId} className="flex items-center gap-2 text-sm">
<span className="text-gray-400 dark:text-gray-500 w-5 text-right">{i + 1}.</span>
<span className="text-text-muted w-5 text-right">{i + 1}.</span>
{p.spriteUrl ? (
<img src={p.spriteUrl} alt={p.name} className="w-6 h-6" loading="lazy" />
) : (
<div className="w-6 h-6 bg-gray-200 dark:bg-gray-700 rounded" />
<div className="w-6 h-6 bg-surface-3 rounded" />
)}
<span className="capitalize text-gray-800 dark:text-gray-200">{p.name}</span>
<span className="ml-auto text-gray-500 dark:text-gray-400 font-medium">
{p.count}
</span>
<span className="capitalize text-text-primary">{p.name}</span>
<span className="ml-auto text-text-tertiary font-medium">{p.count}</span>
</div>
))}
</div>
@@ -65,7 +63,7 @@ function PokemonList({ title, pokemon }: { title: string; pokemon: PokemonRankin
<button
type="button"
onClick={() => setExpanded(!expanded)}
className="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline"
className="mt-2 text-xs text-text-link hover:underline"
>
{expanded ? 'Show less' : `Show all ${pokemon.length}`}
</button>
@@ -100,7 +98,7 @@ function HorizontalBar({
const isLight = colorHex ? hexLuminance(colorHex) > 0.55 : false
return (
<div className="flex items-center gap-2 text-sm">
<div className="flex-1 bg-gray-100 dark:bg-gray-700 rounded-full h-6 overflow-hidden relative">
<div className="flex-1 bg-surface-2 rounded-full h-6 overflow-hidden relative">
<div
className={`h-full rounded-full ${color ?? ''}`}
style={{
@@ -110,7 +108,7 @@ function HorizontalBar({
/>
<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-gray-700 dark:text-gray-200'
isLight ? 'text-gray-900 dark:text-gray-900' : 'text-text-primary'
}`}
style={{
textShadow: isLight ? '0 0 4px rgba(255,255,255,0.8)' : '0 0 4px rgba(0,0,0,0.3)',
@@ -119,17 +117,15 @@ function HorizontalBar({
{label}
</span>
</div>
<span className="w-8 text-right text-gray-700 dark:text-gray-300 font-medium shrink-0">
{value}
</span>
<span className="w-8 text-right text-text-secondary font-medium shrink-0">{value}</span>
</div>
)
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-4">{title}</h2>
<section className="bg-surface-1 rounded-lg shadow p-6">
<h2 className="text-lg font-bold text-text-primary mb-4">{title}</h2>
{children}
</section>
)
@@ -149,16 +145,13 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
<StatCard label="Completed" value={stats.completedRuns} color="blue" />
<StatCard label="Failed" value={stats.failedRuns} color="red" />
</div>
<div className="flex flex-wrap gap-x-6 gap-y-1 text-sm text-gray-600 dark:text-gray-400">
<div className="flex flex-wrap gap-x-6 gap-y-1 text-sm text-text-tertiary">
<span>
Win Rate:{' '}
<strong className="text-gray-800 dark:text-gray-200">{pct(stats.winRate)}</strong>
Win Rate: <strong className="text-text-primary">{pct(stats.winRate)}</strong>
</span>
<span>
Avg Duration:{' '}
<strong className="text-gray-800 dark:text-gray-200">
{fmt(stats.avgDurationDays, ' days')}
</strong>
<strong className="text-text-primary">{fmt(stats.avgDurationDays, ' days')}</strong>
</span>
</div>
</Section>
@@ -202,16 +195,13 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
color="gray"
/>
</div>
<div className="flex flex-wrap gap-x-6 gap-y-1 text-sm text-gray-600 dark:text-gray-400">
<div className="flex flex-wrap gap-x-6 gap-y-1 text-sm text-text-tertiary">
<span>
Catch Rate:{' '}
<strong className="text-gray-800 dark:text-gray-200">{pct(stats.catchRate)}</strong>
Catch Rate: <strong className="text-text-primary">{pct(stats.catchRate)}</strong>
</span>
<span>
Avg per Run:{' '}
<strong className="text-gray-800 dark:text-gray-200">
{fmt(stats.avgEncountersPerRun)}
</strong>
<strong className="text-text-primary">{fmt(stats.avgEncountersPerRun)}</strong>
</span>
</div>
</Section>
@@ -228,39 +218,29 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
<Section title="Team & Deaths">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4">
<StatCard label="Total Deaths" value={stats.totalDeaths} color="red" />
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 border-l-4 border-amber-500">
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{pct(stats.mortalityRate)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Mortality Rate</div>
<div className="bg-surface-1 rounded-lg shadow p-4 border-l-4 border-amber-500">
<div className="text-2xl font-bold text-text-primary">{pct(stats.mortalityRate)}</div>
<div className="text-sm text-text-tertiary">Mortality Rate</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 border-l-4 border-blue-500">
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{fmt(stats.avgCatchLevel)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Avg Catch Lv.</div>
<div className="bg-surface-1 rounded-lg shadow p-4 border-l-4 border-blue-500">
<div className="text-2xl font-bold text-text-primary">{fmt(stats.avgCatchLevel)}</div>
<div className="text-sm text-text-tertiary">Avg Catch Lv.</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 border-l-4 border-purple-500">
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{fmt(stats.avgFaintLevel)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Avg Faint Lv.</div>
<div className="bg-surface-1 rounded-lg shadow p-4 border-l-4 border-purple-500">
<div className="text-2xl font-bold text-text-primary">{fmt(stats.avgFaintLevel)}</div>
<div className="text-sm text-text-tertiary">Avg Faint Lv.</div>
</div>
</div>
{stats.topDeathCauses.length > 0 && (
<div className="mt-4">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Top Death Causes
</h3>
<h3 className="text-sm font-semibold text-text-secondary mb-2">Top Death Causes</h3>
<div className="space-y-1.5">
{stats.topDeathCauses.map((d, i) => (
<div key={d.cause} className="flex items-center gap-2 text-sm">
<span className="text-gray-400 dark:text-gray-500 w-5 text-right">{i + 1}.</span>
<span className="text-gray-800 dark:text-gray-200">{d.cause}</span>
<span className="ml-auto text-gray-500 dark:text-gray-400 font-medium">
{d.count}
</span>
<span className="text-text-muted w-5 text-right">{i + 1}.</span>
<span className="text-text-primary">{d.cause}</span>
<span className="ml-auto text-text-tertiary font-medium">{d.count}</span>
</div>
))}
</div>
@@ -293,7 +273,7 @@ export function Stats() {
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-6">Stats</h1>
<h1 className="text-3xl font-bold text-text-primary mb-6">Stats</h1>
{isLoading && (
<div className="flex justify-center py-12">
@@ -302,13 +282,13 @@ export function Stats() {
)}
{error && (
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
Failed to load stats: {error.message}
</div>
)}
{stats && stats.totalRuns === 0 && (
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
<div className="text-center py-12 text-text-tertiary">
<p className="text-lg mb-2">No data yet</p>
<p className="text-sm">Start a Nuzlocke run to see your stats here.</p>
</div>

View File

@@ -83,19 +83,19 @@ export function AdminEvolutions() {
const data = await exportEvolutions()
downloadJson(data, 'evolutions.json')
}}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
>
Export
</button>
<button
onClick={() => setShowBulkImport(true)}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
>
Bulk Import
</button>
<button
onClick={() => setShowCreate(true)}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
>
Add Evolution
</button>
@@ -111,7 +111,7 @@ export function AdminEvolutions() {
setPage(0)
}}
placeholder="Search by pokemon name, trigger, or item..."
className="w-full max-w-sm px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full max-w-sm px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
<select
value={triggerFilter}
@@ -119,7 +119,7 @@ export function AdminEvolutions() {
setTriggerFilter(e.target.value)
setPage(0)
}}
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
<option value="">All triggers</option>
{EVOLUTION_TRIGGERS.map((t) => (
@@ -135,14 +135,12 @@ export function AdminEvolutions() {
setTriggerFilter('')
setPage(0)
}}
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
className="text-sm text-text-tertiary hover:text-text-primary"
>
Clear filters
</button>
)}
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
{total} evolutions
</span>
<span className="text-sm text-text-tertiary whitespace-nowrap">{total} evolutions</span>
</div>
<AdminTable
@@ -156,38 +154,38 @@ export function AdminEvolutions() {
{totalPages > 1 && (
<div className="mt-4 flex items-center justify-between">
<div className="text-sm text-gray-500 dark:text-gray-400">
<div className="text-sm text-text-tertiary">
Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of {total}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setPage(0)}
disabled={page === 0}
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
>
First
</button>
<button
onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0}
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
>
Prev
</button>
<span className="text-sm text-gray-600 dark:text-gray-300 px-2">
<span className="text-sm text-text-secondary px-2">
Page {page + 1} of {totalPages}
</span>
<button
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
disabled={page >= totalPages - 1}
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
>
Next
</button>
<button
onClick={() => setPage(totalPages - 1)}
disabled={page >= totalPages - 1}
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
>
Last
</button>

View File

@@ -95,18 +95,15 @@ function SortableRouteGroup({
<tbody
ref={setNodeRef}
style={style}
className={`${isDragging ? 'opacity-50 bg-blue-50 dark:bg-blue-900/20' : ''} divide-y divide-gray-200 dark:divide-gray-700`}
className={`${isDragging ? 'opacity-50 bg-blue-50 dark:bg-blue-900/20' : ''} divide-y divide-border-default`}
>
<tr
className="hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
onClick={() => onClick(group)}
>
<tr className="hover:bg-surface-2 cursor-pointer" onClick={() => onClick(group)}>
<td className="px-4 py-3 text-sm w-12">
<button
{...attributes}
{...listeners}
onClick={(e) => e.stopPropagation()}
className="cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 touch-none"
className="cursor-grab active:cursor-grabbing text-gray-400 hover:text-text-secondary touch-none"
title="Drag to reorder"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
@@ -128,7 +125,7 @@ function SortableRouteGroup({
<Link
to={`/admin/games/${gameId}/routes/${group.id}`}
onClick={(e) => e.stopPropagation()}
className="text-blue-600 dark:text-blue-400 hover:underline"
className="text-text-link hover:underline"
>
Encounters
</Link>
@@ -137,15 +134,15 @@ function SortableRouteGroup({
{group.children.map((child) => (
<tr
key={child.id}
className="hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
className="hover:bg-surface-2 cursor-pointer"
onClick={() => onClick(child)}
>
<td className="px-4 py-3 text-sm w-12" />
<td className="px-4 py-3 text-sm whitespace-nowrap w-16 text-gray-400 dark:text-gray-500">
<td className="px-4 py-3 text-sm whitespace-nowrap w-16 text-text-muted">
{child.order}
</td>
<td className="px-4 py-3 text-sm whitespace-nowrap pl-8 text-gray-600 dark:text-gray-400">
<span className="text-gray-300 dark:text-gray-600 mr-1.5">{'\u2514'}</span>
<td className="px-4 py-3 text-sm whitespace-nowrap pl-8 text-text-tertiary">
<span className="text-text-muted mr-1.5">{'\u2514'}</span>
{child.name}
</td>
<td className="px-4 py-3 text-sm whitespace-nowrap text-center">
@@ -155,7 +152,7 @@ function SortableRouteGroup({
<Link
to={`/admin/games/${gameId}/routes/${child.id}`}
onClick={(e) => e.stopPropagation()}
className="text-blue-600 dark:text-blue-400 hover:underline"
className="text-text-link hover:underline"
>
Encounters
</Link>
@@ -192,7 +189,7 @@ function SortableBossRow({
<tr
ref={setNodeRef}
style={style}
className={`${isDragging ? 'opacity-50 bg-blue-50 dark:bg-blue-900/20' : ''} hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer`}
className={`${isDragging ? 'opacity-50 bg-blue-50 dark:bg-blue-900/20' : ''} hover:bg-surface-2 cursor-pointer`}
onClick={() => onClick(boss)}
>
<td className="px-4 py-3 text-sm w-12">
@@ -200,7 +197,7 @@ function SortableBossRow({
{...attributes}
{...listeners}
onClick={(e) => e.stopPropagation()}
className="cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 touch-none"
className="cursor-grab active:cursor-grabbing text-gray-400 hover:text-text-secondary touch-none"
title="Drag to reorder"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
@@ -220,7 +217,7 @@ function SortableBossRow({
(() => {
const g = games.find((g) => g.id === boss.gameId)
return g ? (
<span className="ml-2 text-xs px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400">
<span className="ml-2 text-xs px-1.5 py-0.5 rounded bg-surface-2 text-text-tertiary">
{g.name}
</span>
) : null
@@ -242,7 +239,7 @@ function SortableBossRow({
const value = e.target.value === '' ? null : Number(e.target.value)
onPositionChange(boss.id, value)
}}
className="text-sm border border-gray-300 dark:border-gray-600 rounded px-1.5 py-0.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 max-w-[180px]"
className="text-sm border border-border-default rounded px-1.5 py-0.5 bg-surface-1 text-text-primary max-w-[180px]"
>
<option value=""></option>
{routes.map((r) => (
@@ -343,30 +340,30 @@ export function AdminGameDetail() {
return (
<div>
<nav className="text-sm mb-4 text-gray-500 dark:text-gray-400">
<nav className="text-sm mb-4 text-text-tertiary">
<Link to="/admin/games" className="hover:underline">
Games
</Link>
{' / '}
<span className="text-gray-900 dark:text-gray-100">{game.name}</span>
<span className="text-text-primary">{game.name}</span>
</nav>
<div className="mb-6">
<h2 className="text-xl font-semibold">{game.name}</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
<p className="text-sm text-text-tertiary">
{game.region.charAt(0).toUpperCase() + game.region.slice(1)} &middot; Gen{' '}
{game.generation}
{game.releaseYear ? ` \u00b7 ${game.releaseYear}` : ''}
</p>
</div>
<div className="flex border-b border-gray-200 dark:border-gray-700 mb-4">
<div className="flex border-b border-border-default mb-4">
<button
onClick={() => setTab('routes')}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
tab === 'routes'
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
: 'border-transparent text-text-tertiary hover:text-text-secondary'
}`}
>
Routes ({routes.length})
@@ -376,7 +373,7 @@ export function AdminGameDetail() {
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
tab === 'bosses'
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
: 'border-transparent text-text-tertiary hover:text-text-secondary'
}`}
>
Boss Battles ({bosses?.length ?? 0})
@@ -391,19 +388,19 @@ export function AdminGameDetail() {
const result = await exportGameRoutes(id)
downloadJson(result.data, result.filename)
}}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
>
Export
</button>
<button
onClick={() => setShowBulkImportRoutes(true)}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
>
Bulk Import
</button>
<button
onClick={() => setShowCreate(true)}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
>
Add Route
</button>
@@ -421,26 +418,26 @@ export function AdminGameDetail() {
)}
{routes.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400 border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="text-center py-8 text-text-tertiary border border-border-default rounded-lg">
No routes yet. Add one to get started.
</div>
) : (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div className="border border-border-default rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<table className="min-w-full divide-y divide-border-default">
<thead className="bg-surface-1">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-12" />
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-16">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-12" />
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-16">
Order
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
Name
</th>
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-24">
<th className="px-4 py-3 text-center text-xs font-medium text-text-tertiary uppercase tracking-wider w-24">
Pinwheel
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-28">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-28">
Links
</th>
</tr>
@@ -513,19 +510,19 @@ export function AdminGameDetail() {
const result = await exportGameBosses(id)
downloadJson(result.data, result.filename)
}}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
>
Export
</button>
<button
onClick={() => setShowBulkImportBosses(true)}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
>
Bulk Import
</button>
<button
onClick={() => setShowCreateBoss(true)}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
>
Add Boss Battle
</button>
@@ -543,41 +540,41 @@ export function AdminGameDetail() {
)}
{!bosses || bosses.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400 border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="text-center py-8 text-text-tertiary border border-border-default rounded-lg">
No boss battles yet. Add one to get started.
</div>
) : (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div className="border border-border-default rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<table className="min-w-full divide-y divide-border-default">
<thead className="bg-surface-1">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-12" />
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-16">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-12" />
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-16">
Order
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
Name
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
Type
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
Specialty
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
Section
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
Location
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
Position
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-20">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-20">
Lv Cap
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-16">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-16">
Team
</th>
</tr>
@@ -591,7 +588,7 @@ export function AdminGameDetail() {
items={bosses.map((b) => b.id)}
strategy={verticalListSortingStrategy}
>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
<tbody className="bg-surface-0 divide-y divide-border-default">
{bosses.map((boss) => (
<SortableBossRow
key={boss.id}

View File

@@ -57,13 +57,13 @@ export function AdminGames() {
const data = await exportGames()
downloadJson(data, 'games.json')
}}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
>
Export
</button>
<button
onClick={() => setShowCreate(true)}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
>
Add Game
</button>
@@ -74,7 +74,7 @@ export function AdminGames() {
<select
value={regionFilter}
onChange={(e) => setRegionFilter(e.target.value)}
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
<option value="">All regions</option>
{regions.map((r) => (
@@ -86,7 +86,7 @@ export function AdminGames() {
<select
value={genFilter}
onChange={(e) => setGenFilter(e.target.value)}
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
<option value="">All generations</option>
{generations.map((g) => (
@@ -101,12 +101,12 @@ export function AdminGames() {
setRegionFilter('')
setGenFilter('')
}}
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
className="text-sm text-text-tertiary hover:text-text-primary"
>
Clear filters
</button>
)}
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
<span className="text-sm text-text-tertiary whitespace-nowrap">
{filteredGames.length} games
</span>
</div>

View File

@@ -67,36 +67,32 @@ export function AdminGenlockeDetail() {
return (
<div>
<nav className="text-sm mb-4 text-gray-500 dark:text-gray-400">
<nav className="text-sm mb-4 text-text-tertiary">
<Link to="/admin/genlockes" className="hover:underline">
Genlockes
</Link>
{' / '}
<span className="text-gray-900 dark:text-gray-100">{genlocke.name}</span>
<span className="text-text-primary">{genlocke.name}</span>
</nav>
{/* Header */}
<div className="mb-6 space-y-4">
<div className="flex flex-wrap items-end gap-4">
<div className="flex-1 min-w-[200px]">
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Name
</label>
<label className="block text-xs font-medium text-text-tertiary mb-1">Name</label>
<input
type="text"
value={editName}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Status
</label>
<label className="block text-xs font-medium text-text-tertiary mb-1">Status</label>
<select
value={editStatus}
onChange={(e) => setStatus(e.target.value)}
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
<option value="active">Active</option>
<option value="completed">Completed</option>
@@ -106,35 +102,35 @@ export function AdminGenlockeDetail() {
<button
onClick={handleSave}
disabled={!hasChanges || updateGenlocke.isPending}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500 disabled:opacity-50"
>
{updateGenlocke.isPending ? 'Saving...' : 'Save'}
</button>
<button
onClick={() => setShowDelete(true)}
className="px-4 py-2 text-sm font-medium rounded-md border border-red-300 dark:border-red-600 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20"
className="px-4 py-2 text-sm font-medium rounded-md border border-red-300 dark:border-red-600 text-status-failed hover:bg-red-50 dark:hover:bg-red-900/20"
>
Delete
</button>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400">
<p className="text-sm text-text-tertiary">
Created {new Date(genlocke.createdAt).toLocaleDateString()}
</p>
</div>
{/* Rules (read-only) */}
<div className="mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Rules</h3>
<div className="mb-6 p-4 bg-surface-1 rounded-lg">
<h3 className="text-sm font-semibold mb-2 text-text-secondary">Rules</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-500 dark:text-gray-400">Genlocke rules:</span>
<pre className="mt-1 text-xs bg-white dark:bg-gray-900 p-2 rounded border dark:border-gray-700 overflow-x-auto">
<span className="text-text-tertiary">Genlocke rules:</span>
<pre className="mt-1 text-xs bg-surface-0 p-2 rounded border border-border-default overflow-x-auto">
{JSON.stringify(genlocke.genlockeRules, null, 2)}
</pre>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400">Nuzlocke rules:</span>
<pre className="mt-1 text-xs bg-white dark:bg-gray-900 p-2 rounded border dark:border-gray-700 overflow-x-auto">
<span className="text-text-tertiary">Nuzlocke rules:</span>
<pre className="mt-1 text-xs bg-surface-0 p-2 rounded border border-border-default overflow-x-auto">
{JSON.stringify(genlocke.nuzlockeRules, null, 2)}
</pre>
</div>
@@ -147,18 +143,18 @@ export function AdminGenlockeDetail() {
<h3 className="text-lg font-semibold">Legs ({genlocke.legs.length})</h3>
<button
onClick={() => setAddingLeg(!addingLeg)}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
>
Add Leg
</button>
</div>
{addingLeg && (
<div className="mb-4 flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="mb-4 flex items-center gap-3 p-3 bg-surface-1 rounded-lg">
<select
value={selectedGameId}
onChange={(e) => setSelectedGameId(e.target.value ? Number(e.target.value) : '')}
className="flex-1 px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="flex-1 px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
<option value="">Select a game...</option>
{games.map((g) => (
@@ -179,7 +175,7 @@ export function AdminGenlockeDetail() {
setAddingLeg(false)
setSelectedGameId('')
}}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
>
Cancel
</button>
@@ -187,39 +183,39 @@ export function AdminGenlockeDetail() {
)}
{genlocke.legs.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400 border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="text-center py-8 text-text-tertiary border border-border-default rounded-lg">
No legs yet. Add one to get started.
</div>
) : (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div className="border border-border-default rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<table className="min-w-full divide-y divide-border-default">
<thead className="bg-surface-1">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-16">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-16">
Order
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
Game
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
Run
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
Status
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-20">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-20">
Enc.
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-20">
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-20">
Deaths
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-20">
<th className="px-4 py-3 text-right text-xs font-medium text-text-tertiary uppercase tracking-wider w-20">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
<tbody className="bg-surface-0 divide-y divide-border-default">
{genlocke.legs.map((leg) => (
<tr key={leg.id}>
<td className="px-4 py-3 text-sm whitespace-nowrap">{leg.legOrder}</td>
@@ -228,7 +224,7 @@ export function AdminGenlockeDetail() {
{leg.runId ? (
<Link
to={`/runs/${leg.runId}`}
className="text-blue-600 dark:text-blue-400 hover:underline"
className="text-text-link hover:underline"
>
Run #{leg.runId}
</Link>
@@ -241,10 +237,10 @@ export function AdminGenlockeDetail() {
<span
className={
leg.runStatus === 'active'
? 'text-green-600 dark:text-green-400'
? 'text-status-active'
: leg.runStatus === 'completed'
? 'text-blue-600 dark:text-blue-400'
: 'text-red-600 dark:text-red-400'
? 'text-text-link'
: 'text-status-failed'
}
>
{leg.runStatus}
@@ -264,7 +260,7 @@ export function AdminGenlockeDetail() {
? 'Cannot remove a leg with a linked run'
: 'Remove leg'
}
className="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 disabled:opacity-30 disabled:cursor-not-allowed"
className="text-status-failed hover:text-red-800 dark:hover:text-red-300 disabled:opacity-30 disabled:cursor-not-allowed"
>
Remove
</button>
@@ -279,25 +275,25 @@ export function AdminGenlockeDetail() {
</div>
{/* Stats */}
<div className="mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Stats</h3>
<div className="mb-6 p-4 bg-surface-1 rounded-lg">
<h3 className="text-sm font-semibold mb-2 text-text-secondary">Stats</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm">
<div>
<span className="text-gray-500 dark:text-gray-400">Legs</span>
<span className="text-text-tertiary">Legs</span>
<p className="text-lg font-semibold">
{genlocke.stats.legsCompleted} / {genlocke.stats.totalLegs}
</p>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400">Encounters</span>
<span className="text-text-tertiary">Encounters</span>
<p className="text-lg font-semibold">{genlocke.stats.totalEncounters}</p>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400">Deaths</span>
<span className="text-text-tertiary">Deaths</span>
<p className="text-lg font-semibold">{genlocke.stats.totalDeaths}</p>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400">Survival Rate</span>
<span className="text-text-tertiary">Survival Rate</span>
<p className="text-lg font-semibold">
{genlocke.stats.totalEncounters > 0
? `${Math.round(((genlocke.stats.totalEncounters - genlocke.stats.totalDeaths) / genlocke.stats.totalEncounters) * 100)}%`

View File

@@ -27,10 +27,10 @@ export function AdminGenlockes() {
<span
className={
g.status === 'active'
? 'text-green-600 dark:text-green-400'
? 'text-status-active'
: g.status === 'completed'
? 'text-blue-600 dark:text-blue-400'
: 'text-red-600 dark:text-red-400'
? 'text-text-link'
: 'text-status-failed'
}
>
{g.status}
@@ -60,7 +60,7 @@ export function AdminGenlockes() {
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
<option value="">All statuses</option>
<option value="active">Active</option>
@@ -70,12 +70,12 @@ export function AdminGenlockes() {
{statusFilter && (
<button
onClick={() => setStatusFilter('')}
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
className="text-sm text-text-tertiary hover:text-text-primary"
>
Clear filters
</button>
)}
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
<span className="text-sm text-text-tertiary whitespace-nowrap">
{filtered.length} genlockes
</span>
</div>

View File

@@ -68,7 +68,7 @@ export function AdminPokemon() {
p.spriteUrl ? (
<img src={p.spriteUrl} alt={p.name} className="w-8 h-8" />
) : (
<div className="w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded" />
<div className="w-8 h-8 bg-surface-3 rounded" />
),
},
{ header: 'Name', accessor: (p) => p.name },
@@ -85,19 +85,19 @@ export function AdminPokemon() {
const data = await exportPokemon()
downloadJson(data, 'pokemon.json')
}}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
>
Export
</button>
<button
onClick={() => setShowBulkImport(true)}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
>
Bulk Import
</button>
<button
onClick={() => setShowCreate(true)}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
>
Add Pokemon
</button>
@@ -113,7 +113,7 @@ export function AdminPokemon() {
setPage(0)
}}
placeholder="Search by name..."
className="w-full max-w-sm px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="w-full max-w-sm px-3 py-2 border rounded-md bg-surface-2 border-border-default"
/>
<select
value={typeFilter}
@@ -121,7 +121,7 @@ export function AdminPokemon() {
setTypeFilter(e.target.value)
setPage(0)
}}
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
<option value="">All types</option>
{POKEMON_TYPES.map((t) => (
@@ -137,14 +137,12 @@ export function AdminPokemon() {
setTypeFilter('')
setPage(0)
}}
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
className="text-sm text-text-tertiary hover:text-text-primary"
>
Clear filters
</button>
)}
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
{total} pokemon
</span>
<span className="text-sm text-text-tertiary whitespace-nowrap">{total} pokemon</span>
</div>
<AdminTable
@@ -159,38 +157,38 @@ export function AdminPokemon() {
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-4 flex items-center justify-between">
<div className="text-sm text-gray-500 dark:text-gray-400">
<div className="text-sm text-text-tertiary">
Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of {total}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setPage(0)}
disabled={page === 0}
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
>
First
</button>
<button
onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0}
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
>
Prev
</button>
<span className="text-sm text-gray-600 dark:text-gray-300 px-2">
<span className="text-sm text-text-secondary px-2">
Page {page + 1} of {totalPages}
</span>
<button
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
disabled={page >= totalPages - 1}
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
>
Next
</button>
<button
onClick={() => setPage(totalPages - 1)}
disabled={page >= totalPages - 1}
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
>
Last
</button>

View File

@@ -98,7 +98,7 @@ export function AdminRouteDetail() {
return (
<div>
<nav className="text-sm mb-4 text-gray-500 dark:text-gray-400">
<nav className="text-sm mb-4 text-text-tertiary">
<Link to="/admin/games" className="hover:underline">
Games
</Link>
@@ -108,7 +108,7 @@ export function AdminRouteDetail() {
</Link>
{' / '}
<select
className="text-gray-900 dark:text-gray-100 bg-transparent font-medium cursor-pointer hover:underline border-none p-0 text-sm"
className="text-text-primary bg-transparent font-medium cursor-pointer hover:underline border-none p-0 text-sm"
value={rId}
onChange={(e) => navigate(`/admin/games/${gId}/routes/${e.target.value}`)}
>
@@ -129,26 +129,26 @@ export function AdminRouteDetail() {
{prevRoute ? (
<Link
to={`/admin/games/${gId}/routes/${prevRoute.id}`}
className="px-2 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
className="px-2 py-1 text-sm rounded border border-border-default hover:bg-surface-2"
title={prevRoute.name}
>
&larr; Prev
</Link>
) : (
<span className="px-2 py-1 text-sm rounded border border-gray-200 dark:border-gray-700 text-gray-300 dark:text-gray-600 cursor-not-allowed">
<span className="px-2 py-1 text-sm rounded border border-border-default text-text-muted cursor-not-allowed">
&larr; Prev
</span>
)}
{nextRoute ? (
<Link
to={`/admin/games/${gId}/routes/${nextRoute.id}`}
className="px-2 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
className="px-2 py-1 text-sm rounded border border-border-default hover:bg-surface-2"
title={nextRoute.name}
>
Next &rarr;
</Link>
) : (
<span className="px-2 py-1 text-sm rounded border border-gray-200 dark:border-gray-700 text-gray-300 dark:text-gray-600 cursor-not-allowed">
<span className="px-2 py-1 text-sm rounded border border-border-default text-text-muted cursor-not-allowed">
Next &rarr;
</span>
)}
@@ -156,7 +156,7 @@ export function AdminRouteDetail() {
</div>
<button
onClick={() => setShowCreate(true)}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
>
Add Pokemon
</button>
@@ -212,26 +212,26 @@ export function AdminRouteDetail() {
<h3 className="text-lg font-semibold">Sub-areas ({childRoutes.length})</h3>
<button
onClick={() => setShowCreateChild(true)}
className="px-3 py-1.5 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
className="px-3 py-1.5 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
>
Add Sub-area
</button>
</div>
{childRoutes.length === 0 ? (
<p className="text-sm text-gray-500 dark:text-gray-400">No sub-areas for this route.</p>
<p className="text-sm text-text-tertiary">No sub-areas for this route.</p>
) : (
<div className="border rounded-md dark:border-gray-700 divide-y dark:divide-gray-700">
<div className="border border-border-default rounded-md divide-y divide-border-default">
{childRoutes.map((child) => (
<div key={child.id} className="flex items-center justify-between px-4 py-2">
<Link
to={`/admin/games/${gId}/routes/${child.id}`}
className="text-blue-600 dark:text-blue-400 hover:underline"
className="text-text-link hover:underline"
>
{child.name}
</Link>
<button
onClick={() => setDeletingChild(child)}
className="text-sm text-red-600 dark:text-red-400 hover:underline"
className="text-sm text-status-failed hover:underline"
>
Delete
</button>

View File

@@ -46,10 +46,10 @@ export function AdminRuns() {
<span
className={
r.status === 'active'
? 'text-green-600 dark:text-green-400'
? 'text-status-active'
: r.status === 'completed'
? 'text-blue-600 dark:text-blue-400'
: 'text-red-600 dark:text-red-400'
? 'text-text-link'
: 'text-status-failed'
}
>
{r.status}
@@ -74,7 +74,7 @@ export function AdminRuns() {
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
<option value="">All statuses</option>
<option value="active">Active</option>
@@ -84,7 +84,7 @@ export function AdminRuns() {
<select
value={gameFilter}
onChange={(e) => setGameFilter(e.target.value)}
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
>
<option value="">All games</option>
{runGames.map(([id, name]) => (
@@ -99,12 +99,12 @@ export function AdminRuns() {
setStatusFilter('')
setGameFilter('')
}}
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
className="text-sm text-text-tertiary hover:text-text-primary"
>
Clear filters
</button>
)}
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
<span className="text-sm text-text-tertiary whitespace-nowrap">
{filteredRuns.length} runs
</span>
</div>