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

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