Implement dark-first design system with Geist typography
All checks were successful
CI / backend-lint (pull_request) Successful in 9s
CI / actions-lint (pull_request) Successful in 14s
CI / frontend-lint (pull_request) Successful in 20s

Add Tailwind v4 @theme tokens for surfaces, accents, text, borders,
and status colors. Self-host Geist Sans/Mono variable fonts. Redesign
nav with backdrop blur and active states, home page with gradient hero.
Migrate all 50+ components from ad-hoc gray/blue Tailwind classes to
semantic theme tokens (surface-*, text-*, border-*, accent-*, status-*).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 20:47:26 +01:00
parent e3b3dc5317
commit 512d1bfce5
56 changed files with 1151 additions and 1067 deletions

View File

@@ -14,7 +14,7 @@ function GraveyardCard({ entry }: { entry: GraveyardEntry }) {
const isEvolved = entry.currentPokemon !== null
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 flex flex-col items-center text-center opacity-60 grayscale">
<div className="bg-surface-1 rounded-lg shadow p-4 flex flex-col items-center text-center opacity-60 grayscale">
{displayPokemon.spriteUrl ? (
<img
src={displayPokemon.spriteUrl}
@@ -23,20 +23,18 @@ function GraveyardCard({ entry }: { entry: GraveyardEntry }) {
loading="lazy"
/>
) : (
<div className="w-25 h-25 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-xl font-bold text-gray-600 dark:text-gray-300">
<div className="w-25 h-25 rounded-full bg-surface-3 flex items-center justify-center text-xl font-bold text-text-secondary">
{displayPokemon.name[0]?.toUpperCase()}
</div>
)}
<div className="mt-2 flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full shrink-0 bg-red-500" />
<span className="font-semibold text-gray-900 dark:text-gray-100 text-sm">
<span className="font-semibold text-text-primary text-sm">
{entry.nickname || displayPokemon.name}
</span>
</div>
{entry.nickname && (
<div className="text-xs text-gray-500 dark:text-gray-400">{displayPokemon.name}</div>
)}
{entry.nickname && <div className="text-xs text-text-tertiary">{displayPokemon.name}</div>}
<div className="flex flex-col items-center gap-0.5 mt-1">
{displayPokemon.types.map((type) => (
@@ -44,24 +42,22 @@ function GraveyardCard({ entry }: { entry: GraveyardEntry }) {
))}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
<div className="text-xs text-text-tertiary mt-1">
Lv. {entry.catchLevel} &rarr; {entry.faintLevel}
</div>
<div className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">{entry.routeName}</div>
<div className="text-xs text-text-muted mt-0.5">{entry.routeName}</div>
<div className="text-[10px] text-purple-600 dark:text-purple-400 mt-0.5 font-medium">
Leg {entry.legOrder} &mdash; {entry.gameName}
</div>
{isEvolved && (
<div className="text-[10px] text-gray-400 dark:text-gray-500 mt-0.5">
Originally: {entry.pokemon.name}
</div>
<div className="text-[10px] text-text-muted mt-0.5">Originally: {entry.pokemon.name}</div>
)}
{entry.deathCause && (
<div className="text-[10px] italic text-gray-400 dark:text-gray-500 mt-0.5 line-clamp-2">
<div className="text-[10px] italic text-text-muted mt-0.5 line-clamp-2">
{entry.deathCause}
</div>
)}
@@ -107,7 +103,7 @@ export function GenlockeGraveyard({ genlockeId }: GenlockeGraveyardProps) {
if (error) {
return (
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
Failed to load graveyard data.
</div>
)
@@ -115,7 +111,7 @@ export function GenlockeGraveyard({ genlockeId }: GenlockeGraveyardProps) {
if (!data || data.totalDeaths === 0) {
return (
<div className="rounded-lg bg-gray-50 dark:bg-gray-800/50 p-6 text-center text-gray-500 dark:text-gray-400">
<div className="rounded-lg bg-surface-1/50 p-6 text-center text-text-tertiary">
No deaths recorded across any leg.
</div>
)
@@ -125,11 +121,11 @@ export function GenlockeGraveyard({ genlockeId }: GenlockeGraveyardProps) {
<div className="space-y-4">
{/* Summary bar */}
<div className="flex flex-wrap items-center gap-4 text-sm">
<span className="font-semibold text-gray-900 dark:text-gray-100">
<span className="font-semibold text-text-primary">
{data.totalDeaths} total death{data.totalDeaths !== 1 ? 's' : ''}
</span>
{data.deadliestLeg && (
<span className="text-gray-500 dark:text-gray-400">
<span className="text-text-tertiary">
Deadliest: Leg {data.deadliestLeg.legOrder} &mdash; {data.deadliestLeg.gameName} (
{data.deadliestLeg.deathCount})
</span>
@@ -141,7 +137,7 @@ export function GenlockeGraveyard({ genlockeId }: GenlockeGraveyardProps) {
<select
value={filterLeg ?? ''}
onChange={(e) => setFilterLeg(e.target.value ? Number(e.target.value) : null)}
className="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
className="text-sm border border-border-default rounded-lg px-3 py-1.5 bg-surface-1 text-text-primary"
>
<option value="">All Legs</option>
{data.deathsPerLeg.map((leg) => (
@@ -154,7 +150,7 @@ export function GenlockeGraveyard({ genlockeId }: GenlockeGraveyardProps) {
<select
value={sortKey}
onChange={(e) => setSortKey(e.target.value as SortKey)}
className="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
className="text-sm border border-border-default rounded-lg px-3 py-1.5 bg-surface-1 text-text-primary"
>
<option value="leg">Sort by Leg</option>
<option value="level">Sort by Level</option>