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

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

View File

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