Implement dark-first design system with Geist typography (#28)
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:
@@ -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>
|
||||
|
||||
@@ -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">🥚</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} → {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} — {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} — {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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>✦</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>
|
||||
)
|
||||
|
||||
@@ -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">✦</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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 ?? '?'} · {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>
|
||||
|
||||
@@ -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'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}`
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' : ''
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user