Implement dark-first design system with Geist typography (#28)
Co-authored-by: Julian Tabel <juliantabel.jt@gmail.com> Co-committed-by: Julian Tabel <juliantabel.jt@gmail.com>
This commit was merged in pull request #28.
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user