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

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>