Release: test infrastructure, rules overhaul, and design refresh #30

Merged
TheFurya merged 43 commits from develop into main 2026-02-21 16:58:18 +01:00
56 changed files with 1151 additions and 1067 deletions
Showing only changes of commit 42b66ee9a2 - Show all commits

View File

@@ -1,11 +1,11 @@
--- ---
# nuzlocke-tracker-9c8d # nuzlocke-tracker-9c8d
title: Rebrand to Another Nuzlocke Tracker (ANT) title: Rebrand to Another Nuzlocke Tracker (ANT)
status: in-progress status: completed
type: task type: task
priority: normal priority: normal
created_at: 2026-02-10T14:46:09Z created_at: 2026-02-10T14:46:09Z
updated_at: 2026-02-17T19:08:18Z updated_at: 2026-02-17T19:17:22Z
--- ---
Adopt the new branding: **Another Nuzlocke Tracker**, abbreviated **ANT**. Adopt the new branding: **Another Nuzlocke Tracker**, abbreviated **ANT**.

View File

@@ -0,0 +1,104 @@
---
# nuzlocke-tracker-dpw7
title: Modernize website design and look-and-feel
status: in-progress
type: feature
priority: normal
created_at: 2026-02-17T19:16:39Z
updated_at: 2026-02-17T19:45:49Z
---
Overhaul the UI to a dark-first, techy aesthetic with a cohesive brand identity derived from the ANT steel ant logo.
## Design direction
**Dark & techy** — dark-first surfaces, subtle glow/accent effects, code-editor-influenced aesthetic. Think GitHub dark, Discord, or Linear dark mode. Light mode becomes the secondary theme.
## 1. Brand palette + Tailwind theme
Define custom Tailwind v4 theme tokens in `index.css` using `@theme`:
- **Surfaces:** dark navy/charcoal base (`#0f1117`, `#161b22`, `#1c2128`) with layered elevation (darker = further back, lighter = elevated)
- **Accent:** steel blue from the logo (`#395E73`, `#7EB0CE`) as the primary interactive color
- **Text:** off-white primary (`#e6edf3`), muted gray secondary (`#7d8590`)
- **Status colors:** keep green/red/blue semantics but shift to darker, more saturated variants that work on dark surfaces
- **Borders:** subtle (`rgba(255,255,255,0.08)`) instead of gray-200/700
Replace ad-hoc Tailwind color classes throughout all components with theme tokens.
## 2. Typography
Self-host **Geist** (or Inter/JetBrains Mono pairing):
- Geist Sans for UI text (headings, labels, body)
- Geist Mono for data-heavy elements (stats numbers, encounter rates, levels)
- Set up via `@font-face` in `index.css`, configure in Tailwind `@theme`
- Establish clear size/weight hierarchy: page titles (2xl bold), section headers (lg semibold), body (sm regular), labels (xs medium)
## 3. Navigation redesign
- Add the ant SVG logo mark next to "ANT" in the nav
- Active route indicator (accent-colored underline or background highlight)
- Subtle bottom border glow or gradient accent line
- Slightly translucent/backdrop-blur nav background for depth
- Better mobile menu transitions (slide or fade instead of instant toggle)
## 4. Home page hero
- Full-width dark gradient hero section with the ant logo as a subtle watermark/background element
- Tagline with stronger typography hierarchy
- Stats summary (total runs, completion rate) as glowing stat pills if the user has data
- CTA button with accent glow/gradient
## 5. Cards & surfaces
- Dark elevated cards (`bg-[#161b22]`) with subtle border (`border-white/[0.06]`)
- Hover state: slight border brightness increase + subtle shadow glow in accent color
- Active/selected states with accent border
- Pokemon cards: dark backgrounds make sprites pop better, accent ring on hover
- Stat cards: accent-colored left border or top gradient
- Modals: dark overlay with backdrop-blur, card-style modal surface
## 6. Status indicators & badges
- Status badges: more vibrant on dark backgrounds (alive=emerald glow, dead=red glow, caught=blue)
- Type badges: use the established Pokemon type colors but tuned for dark surfaces
- Encounter method badges: same treatment
- Pulse animation on active run indicators
## 7. Micro-interactions
- Smooth transitions on all interactive elements (`transition-all duration-150`)
- Hover lift on cards (`hover:-translate-y-0.5`)
- Button press feedback (`active:scale-[0.98]`)
- Loading spinners in accent color
- Skeleton loading states for data-heavy pages
## 8. Dark/light mode
- Dark is the default and primary design target
- Light mode: invert surfaces to white/gray-50, keep accent colors, adjust contrast
- Toggle in nav (sun/moon icon)
- Persist preference in localStorage, respect `prefers-color-scheme`
## Checklist
- [x] Define Tailwind v4 `@theme` tokens (colors, fonts, spacing) in `index.css`
- [x] Self-host Geist font family, configure in theme
- [x] Redesign nav bar (logo mark, active states, backdrop blur, dark surface)
- [x] Redesign home page hero section
- [x] Update card/surface styles globally (Layout, PokemonCard, StatCard, GameCard)
- [x] Update all page-level backgrounds and containers
- [ ] Update modal styles (EncounterModal, StatusChangeModal, etc.)
- [ ] Update badge/indicator styles (TypeBadge, RuleBadges, EncounterMethodBadge)
- [ ] Add dark/light mode toggle to nav
- [ ] Polish hover states and transitions across all interactive elements
- [ ] Verify mobile layout and touch targets
- [ ] Verify accessibility (contrast ratios, focus indicators)
## Constraints
- Tailwind-only (no additional CSS frameworks or component libraries)
- Self-hosted fonts only (no Google Fonts CDN)
- Maintain accessibility (WCAG AA contrast ratios, visible focus indicators)
- No performance regression (fonts loaded with `font-display: swap`, no layout shift)

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,12 +21,12 @@ export function EndRunModal({ onConfirm, onClose, isPending, genlockeContext }:
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center"> <div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onClose} /> <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="relative bg-surface-1 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="px-6 py-4 border-b border-border-default">
<h2 className="text-lg font-semibold">End Run</h2> <h2 className="text-lg font-semibold">End Run</h2>
</div> </div>
<div className="px-6 py-6"> <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"> <div className="flex flex-col gap-3">
<button <button
onClick={() => onConfirm('completed')} onClick={() => onConfirm('completed')}
@@ -39,18 +39,18 @@ export function EndRunModal({ onConfirm, onClose, isPending, genlockeContext }:
<button <button
onClick={() => onConfirm('failed')} onClick={() => onConfirm('failed')}
disabled={isPending} 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="font-semibold">Defeat</div>
<div className="text-sm opacity-80">{defeatDescription}</div> <div className="text-sm opacity-80">{defeatDescription}</div>
</button> </button>
</div> </div>
</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 <button
onClick={onClose} onClick={onClose}
disabled={isPending} 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 Cancel
</button> </button>

View File

@@ -18,8 +18,8 @@ export function GameCard({ game, selected, onSelect }: GameCardProps) {
<button <button
type="button" type="button"
onClick={() => onSelect(game)} 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 ${ 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-blue-500 scale-105 shadow-lg' : 'shadow' selected ? 'ring-2 ring-accent-500 scale-105 shadow-lg' : 'shadow'
}`} }`}
> >
{imgIdx < boxArtSrcs.length ? ( {imgIdx < boxArtSrcs.length ? (
@@ -38,14 +38,14 @@ export function GameCard({ game, selected, onSelect }: GameCardProps) {
</span> </span>
</div> </div>
)} )}
<div className="p-3 bg-white dark:bg-gray-800 text-left"> <div className="p-3 bg-surface-1 text-left">
<h3 className="font-semibold text-gray-900 dark:text-gray-100">{game.name}</h3> <h3 className="font-semibold text-text-primary">{game.name}</h3>
<div className="flex items-center gap-2 mt-1"> <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)} {game.region.charAt(0).toUpperCase() + game.region.slice(1)}
</span> </span>
{game.releaseYear && ( {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>
</div> </div>

View File

@@ -70,16 +70,14 @@ export function GameGrid({ games, selectedId, onSelect, runs }: GameGridProps) {
const pillClass = (active: boolean) => const pillClass = (active: boolean) =>
`px-3 py-1.5 text-sm font-medium rounded-full transition-colors ${ `px-3 py-1.5 text-sm font-medium rounded-full transition-colors ${
active active ? 'bg-blue-600 text-white' : 'bg-surface-2 text-text-secondary hover:bg-surface-3'
? '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'
}` }`
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-3"> <div className="space-y-3">
<div className="flex flex-wrap items-center gap-2"> <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 <button
type="button" type="button"
onClick={() => setFilter(null)} onClick={() => setFilter(null)}
@@ -100,7 +98,7 @@ export function GameGrid({ games, selectedId, onSelect, runs }: GameGridProps) {
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <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 <button
type="button" type="button"
onClick={() => setRegionFilter(null)} onClick={() => setRegionFilter(null)}
@@ -122,21 +120,21 @@ export function GameGrid({ games, selectedId, onSelect, runs }: GameGridProps) {
{runs && ( {runs && (
<div className="flex flex-wrap items-center gap-4"> <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 <input
type="checkbox" type="checkbox"
checked={hideWithActiveRun} checked={hideWithActiveRun}
onChange={(e) => setHideWithActiveRun(e.target.checked)} 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 Hide games with active run
</label> </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 <input
type="checkbox" type="checkbox"
checked={hideCompleted} checked={hideCompleted}
onChange={(e) => setHideCompleted(e.target.checked)} 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 Hide completed games
</label> </label>
@@ -146,7 +144,7 @@ export function GameGrid({ games, selectedId, onSelect, runs }: GameGridProps) {
{grouped.map(({ generation, games }) => ( {grouped.map(({ generation, games }) => (
<div key={generation}> <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}`} {GENERATION_LABELS[generation] ?? `Generation ${generation}`}
</h3> </h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">

View File

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

View File

@@ -97,11 +97,11 @@ function TimelineGrid({
<div key={legOrder} className="flex justify-center relative" style={{ height: '20px' }}> <div key={legOrder} className="flex justify-center relative" style={{ height: '20px' }}>
{/* Left half connector */} {/* Left half connector */}
{showLeftLine && ( {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 */} {/* Right half connector */}
{showRightLine && ( {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 */} {/* Dot or empty */}
{leg ? ( {leg ? (
@@ -123,7 +123,7 @@ function LineageCard({ lineage, allLegOrders }: { lineage: LineageEntry; allLegO
const displayPokemon = firstLeg.currentPokemon ?? firstLeg.pokemon const displayPokemon = firstLeg.currentPokemon ?? firstLeg.pokemon
return ( 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 */} {/* Left: Pokemon sprite + nickname */}
<div className="flex flex-col items-center min-w-[80px]"> <div className="flex flex-col items-center min-w-[80px]">
{displayPokemon.spriteUrl ? ( {displayPokemon.spriteUrl ? (
@@ -134,17 +134,15 @@ function LineageCard({ lineage, allLegOrders }: { lineage: LineageEntry; allLegO
loading="lazy" 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()} {displayPokemon.name[0]?.toUpperCase()}
</div> </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} {lineage.nickname || lineage.pokemon.name}
</span> </span>
{lineage.nickname && ( {lineage.nickname && (
<span className="text-[10px] text-gray-500 dark:text-gray-400"> <span className="text-[10px] text-text-tertiary">{lineage.pokemon.name}</span>
{lineage.pokemon.name}
</span>
)} )}
</div> </div>
@@ -200,7 +198,7 @@ export function GenlockeLineage({ genlockeId }: GenlockeLineageProps) {
if (error) { if (error) {
return ( 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. Failed to load lineage data.
</div> </div>
) )
@@ -208,7 +206,7 @@ export function GenlockeLineage({ genlockeId }: GenlockeLineageProps) {
if (!data || data.totalLineages === 0) { if (!data || data.totalLineages === 0) {
return ( 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. No Pokemon have been transferred between legs yet.
</div> </div>
) )
@@ -218,7 +216,7 @@ export function GenlockeLineage({ genlockeId }: GenlockeLineageProps) {
<div className="space-y-4"> <div className="space-y-4">
{/* Summary bar */} {/* Summary bar */}
<div className="flex flex-wrap items-center gap-4 text-sm"> <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{' '} {data.totalLineages} lineage{data.totalLineages !== 1 ? 's' : ''} across{' '}
{allLegOrders.length} leg{allLegOrders.length !== 1 ? 's' : ''} {allLegOrders.length} leg{allLegOrders.length !== 1 ? 's' : ''}
</span> </span>
@@ -237,10 +235,10 @@ export function GenlockeLineage({ genlockeId }: GenlockeLineageProps) {
> >
{allLegOrders.map((legOrder) => ( {allLegOrders.map((legOrder) => (
<div key={legOrder} className="flex flex-col items-center"> <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} Leg {legOrder}
</span> </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)} {legGameNames.get(legOrder)}
</span> </span>
</div> </div>

View File

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

View File

@@ -1,61 +1,83 @@
import { useState } from 'react' 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() { export function Layout() {
const [menuOpen, setMenuOpen] = useState(false) 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 ( return (
<div className="min-h-screen flex flex-col bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100"> <div className="min-h-screen flex flex-col bg-surface-0 text-text-primary">
<nav className="bg-white dark:bg-gray-800 shadow-sm"> <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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16"> <div className="flex justify-between h-14">
<div className="flex items-center"> <div className="flex items-center gap-2">
<Link to="/" className="text-xl font-bold"> <Link to="/" className="flex items-center gap-2 group">
ANT <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> </Link>
</div> </div>
{/* Desktop nav */} {/* Desktop nav */}
<div className="hidden sm:flex items-center space-x-4"> <div className="hidden sm:flex items-center gap-1">
<Link {navLinks.map((link) => (
to="/runs/new" <NavLink key={link.to} to={link.to} active={isActive(link.to)}>
className="px-3 py-2 rounded-md text-sm font-medium hover:bg-gray-100 dark:hover:bg-gray-700" {link.label}
> </NavLink>
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> </div>
{/* Mobile hamburger */} {/* Mobile hamburger */}
<div className="flex items-center sm:hidden"> <div className="flex items-center sm:hidden">
<button <button
type="button" type="button"
onClick={() => setMenuOpen(!menuOpen)} 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" 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 ? ( {menuOpen ? (
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -78,43 +100,19 @@ export function Layout() {
</div> </div>
{/* Mobile dropdown */} {/* Mobile dropdown */}
{menuOpen && ( {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"> <div className="px-2 pt-2 pb-3 space-y-1">
<Link {navLinks.map((link) => (
to="/runs/new" <NavLink
onClick={() => setMenuOpen(false)} key={link.to}
className="block px-3 py-2 rounded-md text-base font-medium hover:bg-gray-100 dark:hover:bg-gray-700" to={link.to}
> active={isActive(link.to)}
New Run onClick={() => setMenuOpen(false)}
</Link> className="block"
<Link >
to="/runs" {link.label}
onClick={() => setMenuOpen(false)} </NavLink>
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>
</div> </div>
</div> </div>
)} )}
@@ -122,12 +120,12 @@ export function Layout() {
<main className="flex-1"> <main className="flex-1">
<Outlet /> <Outlet />
</main> </main>
<footer className="border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800"> <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-gray-500 dark:text-gray-400"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 text-center text-xs text-text-tertiary">
Pokémon encounter data from{' '} Encounter data from{' '}
<a <a
href="https://pokedb.org" 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" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >

View File

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

View File

@@ -9,7 +9,7 @@ export function RuleBadges({ rules }: RuleBadgesProps) {
const enabledRules = RULE_DEFINITIONS.filter((def) => rules[def.key]) const enabledRules = RULE_DEFINITIONS.filter((def) => rules[def.key])
if (enabledRules.length === 0) { 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 ( return (

View File

@@ -11,13 +11,13 @@ export function RuleToggle({ name, description, enabled, onChange }: RuleToggleP
const [showTooltip, setShowTooltip] = useState(false) const [showTooltip, setShowTooltip] = useState(false)
return ( 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-1 pr-4">
<div className="flex items-center gap-2"> <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 <button
type="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)} onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)} onMouseLeave={() => setShowTooltip(false)}
onClick={() => setShowTooltip(!showTooltip)} onClick={() => setShowTooltip(!showTooltip)}
@@ -33,17 +33,15 @@ export function RuleToggle({ name, description, enabled, onChange }: RuleToggleP
</svg> </svg>
</button> </button>
</div> </div>
{showTooltip && ( {showTooltip && <p className="mt-1 text-sm text-text-tertiary">{description}</p>}
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{description}</p>
)}
</div> </div>
<button <button
type="button" type="button"
role="switch" role="switch"
aria-checked={enabled} aria-checked={enabled}
onClick={() => onChange(!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 ${ 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-gray-200 dark:bg-gray-600' enabled ? 'bg-blue-600' : 'bg-surface-3'
}`} }`}
> >
<span <span

View File

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

View File

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

View File

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

View File

@@ -6,26 +6,26 @@ interface StatCardProps {
} }
const colorClasses: Record<string, string> = { const colorClasses: Record<string, string> = {
blue: 'border-blue-500', blue: 'border-status-completed',
green: 'border-green-500', green: 'border-status-alive',
red: 'border-red-500', red: 'border-status-failed',
purple: 'border-purple-500', purple: 'border-purple-500',
amber: 'border-amber-500', amber: 'border-amber-500',
gray: 'border-gray-500', gray: 'border-text-tertiary',
} }
export function StatCard({ label, value, total, color }: StatCardProps) { export function StatCard({ label, value, total, color }: StatCardProps) {
return ( return (
<div <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} {value}
{total !== undefined && ( {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>
<div className="text-sm text-gray-600 dark:text-gray-400">{label}</div> <div className="text-sm text-text-secondary">{label}</div>
</div> </div>
) )
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,26 +61,26 @@ export function AdminTable<T>({
if (isLoading) { if (isLoading) {
return ( 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">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table className="min-w-full divide-y divide-border-default">
<thead className="bg-gray-50 dark:bg-gray-800"> <thead className="bg-surface-1">
<tr> <tr>
{columns.map((col) => ( {columns.map((col) => (
<th <th
key={col.header} 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} {col.header}
</th> </th>
))} ))}
</tr> </tr>
</thead> </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) => ( {Array.from({ length: 5 }).map((_, i) => (
<tr key={i}> <tr key={i}>
{columns.map((col) => ( {columns.map((col) => (
<td key={col.header} className={`px-4 py-3 ${col.className ?? ''}`}> <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> </td>
))} ))}
</tr> </tr>
@@ -93,17 +93,17 @@ export function AdminTable<T>({
if (data.length === 0) { if (data.length === 0) {
return ( 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} {emptyMessage}
</div> </div>
) )
} }
return ( 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"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table className="min-w-full divide-y divide-border-default">
<thead className="bg-gray-50 dark:bg-gray-800"> <thead className="bg-surface-1">
<tr> <tr>
{columns.map((col) => { {columns.map((col) => {
const sortable = !!col.sortKey const sortable = !!col.sortKey
@@ -112,7 +112,7 @@ export function AdminTable<T>({
<th <th
key={col.header} key={col.header}
onClick={sortable ? () => handleSort(col.header) : undefined} 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"> <span className="inline-flex items-center gap-1">
{col.header} {col.header}
@@ -127,14 +127,12 @@ export function AdminTable<T>({
})} })}
</tr> </tr>
</thead> </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) => ( {sortedData.map((row) => (
<tr <tr
key={keyFn(row)} key={keyFn(row)}
onClick={onRowClick ? () => onRowClick(row) : undefined} onClick={onRowClick ? () => onRowClick(row) : undefined}
className={ className={onRowClick ? 'cursor-pointer hover:bg-surface-2' : ''}
onRowClick ? 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800' : ''
}
> >
{columns.map((col) => ( {columns.map((col) => (
<td <td

View File

@@ -107,7 +107,7 @@ export function BossBattleFormModal({
<button <button
type="button" type="button"
onClick={onEditTeam} 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}) Edit Team ({boss?.pokemon.length ?? 0})
</button> </button>
@@ -123,7 +123,7 @@ export function BossBattleFormModal({
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
placeholder="e.g. Brock" 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>
<div> <div>
@@ -131,7 +131,7 @@ export function BossBattleFormModal({
<select <select
value={bossType} value={bossType}
onChange={(e) => setBossType(e.target.value as typeof 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) => ( {BOSS_TYPES.map((t) => (
<option key={t.value} value={t.value}> <option key={t.value} value={t.value}>
@@ -145,7 +145,7 @@ export function BossBattleFormModal({
<select <select
value={specialtyType} value={specialtyType}
onChange={(e) => setSpecialtyType(e.target.value)} 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> <option value="">None</option>
{POKEMON_TYPES.map((t) => ( {POKEMON_TYPES.map((t) => (
@@ -165,7 +165,7 @@ export function BossBattleFormModal({
value={location} value={location}
onChange={(e) => setLocation(e.target.value)} onChange={(e) => setLocation(e.target.value)}
placeholder="e.g. Pewter City Gym" 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> </div>
@@ -178,7 +178,7 @@ export function BossBattleFormModal({
min={1} min={1}
value={levelCap} value={levelCap}
onChange={(e) => setLevelCap(e.target.value)} 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>
<div> <div>
@@ -189,7 +189,7 @@ export function BossBattleFormModal({
min={1} min={1}
value={order} value={order}
onChange={(e) => setOrder(e.target.value)} 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>
</div> </div>
@@ -202,7 +202,7 @@ export function BossBattleFormModal({
value={section} value={section}
onChange={(e) => setSection(e.target.value)} onChange={(e) => setSection(e.target.value)}
placeholder="e.g. Main Story, Endgame" 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> </div>
{games && games.length > 1 && ( {games && games.length > 1 && (
@@ -211,7 +211,7 @@ export function BossBattleFormModal({
<select <select
value={gameId} value={gameId}
onChange={(e) => setGameId(e.target.value)} 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> <option value="">All games</option>
{games.map((g) => ( {games.map((g) => (
@@ -229,7 +229,7 @@ export function BossBattleFormModal({
<select <select
value={afterRouteId} value={afterRouteId}
onChange={(e) => setAfterRouteId(e.target.value)} 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> <option value="">None</option>
{sortedRoutes.map((r) => ( {sortedRoutes.map((r) => (
@@ -248,7 +248,7 @@ export function BossBattleFormModal({
value={badgeName} value={badgeName}
onChange={(e) => setBadgeName(e.target.value)} onChange={(e) => setBadgeName(e.target.value)}
placeholder="Optional" 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> <div>
@@ -258,7 +258,7 @@ export function BossBattleFormModal({
value={badgeImageUrl} value={badgeImageUrl}
onChange={(e) => setBadgeImageUrl(e.target.value)} onChange={(e) => setBadgeImageUrl(e.target.value)}
placeholder="Optional" 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> </div>
@@ -270,7 +270,7 @@ export function BossBattleFormModal({
value={spriteUrl} value={spriteUrl}
onChange={(e) => setSpriteUrl(e.target.value)} onChange={(e) => setSpriteUrl(e.target.value)}
placeholder="Optional" 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>
</FormModal> </FormModal>

View File

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

View File

@@ -53,8 +53,8 @@ export function BulkImportModal({
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center"> <div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onClose} /> <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="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-gray-200 dark:border-gray-700"> <div className="px-6 py-4 border-b border-border-default">
<h2 className="text-lg font-semibold">{title}</h2> <h2 className="text-lg font-semibold">{title}</h2>
</div> </div>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@@ -66,12 +66,12 @@ export function BulkImportModal({
value={json} value={json}
onChange={(e) => setJson(e.target.value)} onChange={(e) => setJson(e.target.value)}
placeholder={example} 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> </div>
{error && ( {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} {error}
</div> </div>
)} )}
@@ -82,7 +82,7 @@ export function BulkImportModal({
{createdLabel}: {result.created}, {updatedLabel}: {result.updated} {createdLabel}: {result.created}, {updatedLabel}: {result.updated}
</p> </p>
{result.errors.length > 0 && ( {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) => ( {result.errors.map((err, i) => (
<li key={i}>{err}</li> <li key={i}>{err}</li>
))} ))}
@@ -91,18 +91,18 @@ export function BulkImportModal({
</div> </div>
)} )}
</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 <button
type="button" type="button"
onClick={onClose} 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 Close
</button> </button>
<button <button
type="submit" type="submit"
disabled={isSubmitting || !json.trim()} 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'} {isSubmitting ? 'Importing...' : 'Import'}
</button> </button>

View File

@@ -18,17 +18,17 @@ export function DeleteConfirmModal({
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center"> <div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onCancel} /> <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"> <div className="px-6 py-4">
<h2 className="text-lg font-semibold text-red-600 dark:text-red-400">{title}</h2> <h2 className="text-lg font-semibold text-status-failed">{title}</h2>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-300">{message}</p> <p className="mt-2 text-sm text-text-secondary">{message}</p>
{error && <p className="mt-2 text-sm text-red-600 dark:text-red-400">{error}</p>} {error && <p className="mt-2 text-sm text-status-failed">{error}</p>}
</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 <button
type="button" type="button"
onClick={onCancel} 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 Cancel
</button> </button>

View File

@@ -74,7 +74,7 @@ export function EvolutionFormModal({
<select <select
value={trigger} value={trigger}
onChange={(e) => setTrigger(e.target.value)} 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) => ( {TRIGGER_OPTIONS.map((t) => (
<option key={t} value={t}> <option key={t} value={t}>
@@ -92,7 +92,7 @@ export function EvolutionFormModal({
value={minLevel} value={minLevel}
onChange={(e) => setMinLevel(e.target.value)} onChange={(e) => setMinLevel(e.target.value)}
placeholder="Optional" 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> <div>
@@ -102,7 +102,7 @@ export function EvolutionFormModal({
value={item} value={item}
onChange={(e) => setItem(e.target.value)} onChange={(e) => setItem(e.target.value)}
placeholder="e.g. thunder-stone" 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>
<div> <div>
@@ -112,7 +112,7 @@ export function EvolutionFormModal({
value={heldItem} value={heldItem}
onChange={(e) => setHeldItem(e.target.value)} onChange={(e) => setHeldItem(e.target.value)}
placeholder="Optional" 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> <div>
@@ -122,7 +122,7 @@ export function EvolutionFormModal({
value={condition} value={condition}
onChange={(e) => setCondition(e.target.value)} onChange={(e) => setCondition(e.target.value)}
placeholder="e.g. high-happiness, daytime" 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>
<div> <div>
@@ -132,7 +132,7 @@ export function EvolutionFormModal({
value={region} value={region}
onChange={(e) => setRegion(e.target.value)} onChange={(e) => setRegion(e.target.value)}
placeholder="e.g. alola (blank = all regions)" 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> </div>
</FormModal> </FormModal>

View File

@@ -33,14 +33,14 @@ export function FormModal({
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center"> <div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onClose} /> <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="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-gray-200 dark:border-gray-700"> <div className="px-6 py-4 border-b border-border-default">
<h2 className="text-lg font-semibold">{title}</h2> <h2 className="text-lg font-semibold">{title}</h2>
{headerExtra} {headerExtra}
</div> </div>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<div className="px-6 py-4 space-y-4">{children}</div> <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 && ( {onDelete && (
<button <button
type="button" type="button"
@@ -53,7 +53,7 @@ export function FormModal({
} }
}} }}
onBlur={() => setConfirmingDelete(false)} 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'} {isDeleting ? 'Deleting...' : confirmingDelete ? 'Confirm?' : 'Delete'}
</button> </button>
@@ -62,14 +62,14 @@ export function FormModal({
<button <button
type="button" type="button"
onClick={onClose} 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 Cancel
</button> </button>
<button <button
type="submit" type="submit"
disabled={isSubmitting} 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} {isSubmitting ? 'Saving...' : submitLabel}
</button> </button>

View File

@@ -63,7 +63,7 @@ export function GameFormModal({
isDeleting={isDeleting} isDeleting={isDeleting}
headerExtra={ headerExtra={
detailUrl ? ( 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 View Routes & Bosses
</Link> </Link>
) : undefined ) : undefined
@@ -76,7 +76,7 @@ export function GameFormModal({
required required
value={name} value={name}
onChange={(e) => setName(e.target.value)} 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>
<div> <div>
@@ -89,7 +89,7 @@ export function GameFormModal({
setSlug(e.target.value) setSlug(e.target.value)
setAutoSlug(false) 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>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@@ -101,7 +101,7 @@ export function GameFormModal({
min={1} min={1}
value={generation} value={generation}
onChange={(e) => setGeneration(e.target.value)} 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>
<div> <div>
@@ -111,7 +111,7 @@ export function GameFormModal({
required required
value={region} value={region}
onChange={(e) => setRegion(e.target.value)} 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>
</div> </div>
@@ -122,7 +122,7 @@ export function GameFormModal({
value={boxArtUrl} value={boxArtUrl}
onChange={(e) => setBoxArtUrl(e.target.value)} onChange={(e) => setBoxArtUrl(e.target.value)}
placeholder="Optional" 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> <div>
@@ -132,7 +132,7 @@ export function GameFormModal({
value={releaseYear} value={releaseYear}
onChange={(e) => setReleaseYear(e.target.value)} onChange={(e) => setReleaseYear(e.target.value)}
placeholder="Optional" 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>
</FormModal> </FormModal>

View File

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

View File

@@ -44,11 +44,11 @@ export function PokemonSelector({
}} }}
onFocus={() => setOpen(true)} onFocus={() => setOpen(true)}
placeholder="Search pokemon..." 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 />} {selectedId && <input type="hidden" name={label} value={selectedId} required />}
{open && pokemon.length > 0 && ( {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) => ( {pokemon.map((p) => (
<li <li
key={p.id} key={p.id}
@@ -57,7 +57,7 @@ export function PokemonSelector({
setSearch(p.name) setSearch(p.name)
setOpen(false) 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' : '' p.id === selectedId ? 'bg-blue-50 dark:bg-blue-900/30' : ''
}`} }`}
> >

View File

@@ -84,7 +84,7 @@ export function RouteEncounterFormModal({
setSelectedMethod(e.target.value) setSelectedMethod(e.target.value)
if (e.target.value !== 'other') setCustomMethod('') 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> <option value="">Select method...</option>
{METHOD_ORDER.map((m) => ( {METHOD_ORDER.map((m) => (
@@ -108,7 +108,7 @@ export function RouteEncounterFormModal({
value={customMethod} value={customMethod}
onChange={(e) => setCustomMethod(e.target.value)} onChange={(e) => setCustomMethod(e.target.value)}
placeholder="Custom method name" 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> </div>
@@ -121,7 +121,7 @@ export function RouteEncounterFormModal({
max={100} max={100}
value={encounterRate} value={encounterRate}
onChange={(e) => setEncounterRate(e.target.value)} 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>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@@ -134,7 +134,7 @@ export function RouteEncounterFormModal({
max={100} max={100}
value={minLevel} value={minLevel}
onChange={(e) => setMinLevel(e.target.value)} 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>
<div> <div>
@@ -146,7 +146,7 @@ export function RouteEncounterFormModal({
max={100} max={100}
value={maxLevel} value={maxLevel}
onChange={(e) => setMaxLevel(e.target.value)} 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>
</div> </div>

View File

@@ -49,7 +49,7 @@ export function RouteFormModal({
isDeleting={isDeleting} isDeleting={isDeleting}
headerExtra={ headerExtra={
detailUrl ? ( 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 View Encounters
</Link> </Link>
) : undefined ) : undefined
@@ -62,7 +62,7 @@ export function RouteFormModal({
required required
value={name} value={name}
onChange={(e) => setName(e.target.value)} 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>
<div> <div>
@@ -73,7 +73,7 @@ export function RouteFormModal({
min={0} min={0}
value={order} value={order}
onChange={(e) => setOrder(e.target.value)} 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>
<div> <div>
@@ -84,9 +84,9 @@ export function RouteFormModal({
value={pinwheelZone} value={pinwheelZone}
onChange={(e) => setPinwheelZone(e.target.value)} onChange={(e) => setPinwheelZone(e.target.value)}
placeholder="None" 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 Routes in the same zone share an encounter when the Pinwheel Clause is active
</p> </p>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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