Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
"drizzle-orm": "^0.45.3"
},
"devDependencies": {
"@fontsource-variable/onest": "^5.3.1",
"@fontsource-variable/unbounded": "^5.3.0",
"@fontsource-variable/archivo": "^5.3.0",
"@internationalized/date": "^3.12.4",
"@lucide/svelte": "^1.48.0",
"@sveltejs/adapter-vercel": "^6.3.4",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/assets/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions src/lib/components/compare/stat-comparison.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{#each rows as row (row.label)}
{@const best = Math.max(...row.values)}
<figure class="rounded-2xl border bg-card p-4">
<figure class="rounded-lg border bg-card p-4">
<figcaption class="text-sm font-medium">{row.label}</figcaption>
<ul class="mt-3 grid gap-1.5">
{#each series as s, i (s.name)}
{@const value = row.values[i]}
<li class="group grid grid-cols-[1fr_2.5rem] items-center gap-2" title="{s.name}: {value} {row.label}">
<span class="h-2 overflow-hidden rounded-full bg-muted">
<span class="h-2 overflow-hidden rounded-sm bg-muted">
<span
class="block h-full rounded-full transition-[width] duration-500"
class="block h-full rounded-sm transition-[width] duration-500"
style:width="{(value / row.max) * 100}%"
style:background-color={s.color}
></span>
Expand Down
136 changes: 136 additions & 0 deletions src/lib/components/dex/dex-table.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<script lang="ts">
import ArrowDownIcon from "@lucide/svelte/icons/arrow-down";
import ArrowUpIcon from "@lucide/svelte/icons/arrow-up";
import PokemonArt from "$lib/components/pokemon/pokemon-art.svelte";
import TypeBadge from "$lib/components/pokemon/type-badge.svelte";
import { statTotal, type DexListEntry, type DexQuery, type SortKey } from "$lib/dex/list.js";
import { dexNumber } from "$lib/pokemon/format.js";
import { STAT_KEYS, STAT_LABELS, type StatKey } from "$lib/pokemon/types.js";
import { cn } from "$lib/utils.js";

let {
entries,
query,
onsort,
eager = 0,
}: {
entries: DexListEntry[];
query: DexQuery;
onsort: (sort: SortKey, desc: boolean) => void;
eager?: number;
} = $props();

// Narrow screens only have room for one figure, so show whichever stat is being sorted on.
const compactKey = $derived<StatKey | "total">(
(STAT_KEYS as readonly string[]).includes(query.sort) ? (query.sort as StatKey) : "total",
);
const compactLabel = $derived(compactKey === "total" ? "Total" : STAT_LABELS[compactKey].short);

function figure(entry: DexListEntry, key: StatKey | "total"): number {
return key === "total" ? statTotal(entry) : entry.stats[STAT_KEYS.indexOf(key)];
}

function sortBy(key: SortKey) {
// Figures read best largest first; dex number and name read best ascending.
const numeric = key !== "dex" && key !== "name";
onsort(key, query.sort === key ? !query.desc : numeric);
}

function ariaSort(key: SortKey) {
if (query.sort !== key) return undefined;
return query.desc ? "descending" : "ascending";
}
</script>

{#snippet header(key: SortKey, label: string, title: string, className: string)}
<th scope="col" aria-sort={ariaSort(key)} class={cn("p-0 font-medium", className)}>
<button
type="button"
{title}
onclick={() => sortBy(key)}
class={cn(
"inline-flex h-9 w-full items-center gap-1 px-2 hover:text-foreground",
key === "dex" || key === "name" ? "justify-start" : "justify-end",
query.sort === key && "text-foreground",
)}
>
{label}
{#if query.sort === key}
{#if query.desc}
<ArrowDownIcon class="size-3" aria-hidden="true" />
{:else}
<ArrowUpIcon class="size-3" aria-hidden="true" />
{/if}
{/if}
</button>
</th>
{/snippet}

<div class="overflow-clip rounded-lg border bg-card">
<table class="w-full text-sm">
<thead class="border-b bg-muted text-xs text-muted-foreground lg:sticky lg:top-14 lg:z-10">
<tr>
{@render header("dex", "No.", "Sort by dex number", "hidden w-20 pl-2 sm:table-cell")}
{@render header("name", "Pokémon", "Sort by name", "pl-2 text-left sm:pl-0")}
<th scope="col" class="hidden w-40 px-2 text-left font-medium md:table-cell">Type</th>
{#each STAT_KEYS as key (key)}
{@render header(key, STAT_LABELS[key].short, `Sort by ${STAT_LABELS[key].long}`, "hidden w-14 lg:table-cell")}
{/each}
{@render header("total", "Total", "Sort by base stat total", "hidden w-16 pr-2 lg:table-cell")}
{@render header(compactKey, compactLabel, "Sort by this figure", "w-16 pr-2 lg:hidden")}
</tr>
</thead>
<tbody class="divide-y">
{#each entries as entry, index (entry.id)}
<tr class="relative transition-colors hover:bg-muted/60" style:--tint="var(--type-{entry.types[0]})">
<td class="hidden py-1.5 pl-4 text-muted-foreground tabular font-condensed sm:table-cell">
{dexNumber(entry.speciesId)}
</td>
<td class="py-1.5 pr-2 pl-4 sm:pl-2">
<div class="flex items-center gap-3">
<span class="tint-field grid size-11 shrink-0 place-items-center rounded-md p-0.5">
<PokemonArt
id={entry.id}
name=""
width={96}
widths={[96, 192]}
sizes="44px"
eager={index < eager}
/>
</span>
<div class="min-w-0">
<p class="text-xs text-muted-foreground tabular font-condensed sm:hidden">{dexNumber(entry.speciesId)}</p>
<a
href="/pokemon/{entry.slug}"
class="font-medium after:absolute after:inset-0 hover:underline hover:underline-offset-4 focus-visible:outline-none focus-visible:after:outline-2 focus-visible:after:-outline-offset-2 focus-visible:after:outline-ring"
data-sveltekit-preload-data="hover"
>
{entry.name}
</a>
<div class="mt-1 flex gap-1 md:hidden">
{#each entry.types as type (type)}
<TypeBadge {type} size="sm" />
{/each}
</div>
</div>
</div>
</td>
<td class="hidden px-2 md:table-cell">
<div class="flex gap-1">
{#each entry.types as type (type)}
<TypeBadge {type} size="sm" />
{/each}
</div>
</td>
{#each STAT_KEYS as key, i (key)}
<td class={cn("hidden px-2 text-right tabular lg:table-cell", query.sort === key ? "font-semibold" : "text-muted-foreground")}>
{entry.stats[i]}
</td>
{/each}
<td class="hidden pr-4 pl-2 text-right font-semibold tabular lg:table-cell">{statTotal(entry)}</td>
<td class="pr-4 pl-2 text-right font-semibold tabular lg:hidden">{figure(entry, compactKey)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
17 changes: 6 additions & 11 deletions src/lib/components/dex/dex-tile.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,23 @@

<a
href="/pokemon/{entry.slug}"
class="group block rounded-2xl outline-offset-4"
class="group block rounded-lg border bg-card p-1.5 transition-colors hover:border-(--tint)"
style:--tint="var(--type-{entry.types[0]})"
data-sveltekit-preload-data="hover"
>
<div class="tint-field relative overflow-hidden rounded-2xl p-[8%]">
<PokemonArt
id={entry.id}
name={entry.name}
{eager}
class="transition-transform duration-300 ease-out group-hover:-translate-y-1 group-hover:scale-[1.04]"
/>
<div class="tint-field relative overflow-hidden rounded-md p-[8%]">
<PokemonArt id={entry.id} name={entry.name} {eager} />
{#if highlight}
<span
class="absolute top-2 right-2 rounded-full bg-background/80 px-2 py-0.5 text-xs font-medium tabular backdrop-blur"
class="absolute top-1.5 right-1.5 rounded-sm bg-card px-1.5 py-0.5 text-xs font-medium tabular"
>
<span class="text-muted-foreground">{highlight.label}</span>
{highlight.value}
</span>
{/if}
</div>
<div class="px-1 pt-2.5">
<p class="text-xs text-muted-foreground tabular">{dexNumber(entry.speciesId)}</p>
<div class="px-1.5 pt-2 pb-1">
<p class="text-xs text-muted-foreground tabular font-condensed">{dexNumber(entry.speciesId)}</p>
<p class="leading-snug font-medium text-balance group-hover:underline group-hover:underline-offset-4">
{entry.name}
</p>
Expand Down
6 changes: 3 additions & 3 deletions src/lib/components/dex/type-filter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<div
role="group"
aria-label="Filter by type"
class="-mx-4 flex gap-1.5 overflow-x-auto px-4 pb-1 [scrollbar-width:none] sm:mx-0 sm:flex-wrap sm:overflow-visible sm:px-0 sm:pb-0"
class="-mx-4 flex gap-1 overflow-x-auto px-4 pb-1 [scrollbar-width:none] sm:mx-0 sm:grid sm:grid-cols-6 sm:overflow-visible sm:px-0 sm:pb-0 lg:grid-cols-9"
>
{#each TYPES as type (type)}
{@const active = selected.includes(type)}
Expand All @@ -26,14 +26,14 @@
onclick={() => toggle(type)}
style:--chip="var(--type-{type})"
class={cn(
"inline-flex h-7 shrink-0 items-center gap-1.5 rounded-full border px-2.5 text-xs font-medium transition-colors",
"inline-flex h-8 shrink-0 items-center gap-2 rounded-sm border px-2.5 text-xs font-medium transition-colors",
active
? cn("border-transparent bg-(--chip)", typeTextClass(type))
: "bg-card text-muted-foreground hover:border-(--chip) hover:text-foreground",
)}
>
<span
class={cn("size-2 rounded-full", active ? "bg-current opacity-60" : "bg-(--chip)")}
class={cn("h-3 w-1 rounded-full", active ? "bg-current opacity-50" : "bg-(--chip)")}
aria-hidden="true"
></span>
{titleCase(type)}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/pokemon/damage-class-icon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</script>

<span
class={cn("inline-flex h-5 items-center gap-1 rounded-full px-2 text-[0.6875rem] font-medium", {
class={cn("inline-flex h-5 items-center gap-1 rounded-sm px-1.5 text-[0.6875rem] font-medium", {
"bg-[#f4d9cf] text-[#8a2f12] dark:bg-[#4a2418] dark:text-[#ffb59c]": value === "physical",
"bg-[#d7e2f7] text-[#233f86] dark:bg-[#1c2d52] dark:text-[#a8c2ff]": value === "special",
"bg-muted text-muted-foreground": value === "status",
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/pokemon/detail-section.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
$props();
</script>

<section {id} aria-labelledby="{id}-title" class="scroll-mt-32">
<section {id} aria-labelledby="{id}-title" class="scroll-mt-36">
<div class="flex flex-wrap items-end justify-between gap-x-6 gap-y-3">
<div>
<h2 id="{id}-title" class="font-display text-2xl font-semibold tracking-tight">{title}</h2>
<h2 id="{id}-title" class="section-title">{title}</h2>
{#if description}
<p class="mt-1 max-w-prose text-sm text-muted-foreground">{description}</p>
{/if}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/pokemon/evolution-chain.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
aria-current={current ? "page" : undefined}
style:--tint="var(--type-{node.types[0]})"
class={cn(
"group flex w-40 shrink-0 flex-col items-center gap-1 rounded-2xl border border-transparent p-3 text-center transition-colors hover:border-(--tint)",
"group flex w-40 shrink-0 flex-col items-center gap-1 rounded-lg border border-transparent p-3 text-center transition-colors hover:border-(--tint)",
current && "border-(--tint) bg-card",
)}
>
<div class="tint-field w-full rounded-xl p-2">
<div class="tint-field w-full rounded-md p-2">
<PokemonArt id={node.speciesId} name={node.name} width={192} widths={[192, 256]} sizes="140px" />
</div>
<span class="mt-1 text-xs text-muted-foreground tabular">{dexNumber(node.speciesId)}</span>
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/pokemon/form-list.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
aria-current={current ? "page" : undefined}
style:--tint="var(--type-{form.types[0]})"
class={cn(
"group flex h-full flex-col gap-2 rounded-2xl border p-3 transition-colors hover:border-(--tint)",
"group flex h-full flex-col gap-2 rounded-lg border p-3 transition-colors hover:border-(--tint)",
current && "border-(--tint) bg-card",
)}
>
<div class="tint-field rounded-xl p-2">
<div class="tint-field rounded-md p-2">
<PokemonArt id={form.id} name={form.name} width={192} widths={[192, 256]} sizes="160px" />
</div>
<p class="text-sm leading-snug font-medium">{form.form ?? "Default"}</p>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/pokemon/gender-ratio.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

{#if ratio}
<div class="grid gap-1.5">
<div class="flex h-2 overflow-hidden rounded-full bg-muted" aria-hidden="true">
<div class="flex h-2 overflow-hidden rounded-sm bg-muted" aria-hidden="true">
<div class="bg-[#3d7dd8]" style:width="{ratio.male}%"></div>
<div class="bg-[#e0569b]" style:width="{ratio.female}%"></div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/pokemon/learnset-table.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@

{#each methods as m (m)}
<Tabs.Content value={m} class="mt-4">
<div class="overflow-x-auto rounded-xl border bg-card {loading ? 'opacity-60' : ''}">
<div class="overflow-x-auto rounded-lg border bg-card {loading ? 'opacity-60' : ''}">
<table class="w-full min-w-[40rem] text-sm">
<thead class="border-b text-left text-xs text-muted-foreground">
<tr>
Expand Down
47 changes: 47 additions & 0 deletions src/lib/components/pokemon/pokemon-about.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts">
import type { PokemonDetail } from "$lib/pokemon/detail.js";
import { formatHeight, formatWeight } from "$lib/pokemon/format.js";
import FlavorText from "./flavor-text.svelte";
import GenderRatio from "./gender-ratio.svelte";

let { detail }: { detail: PokemonDetail } = $props();

const height = $derived(formatHeight(detail.entry.height));
const weight = $derived(formatWeight(detail.entry.weight));
</script>

<div class="grid gap-8 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)] lg:gap-12">
<div class="grid content-start gap-8">
<FlavorText entries={detail.flavor} />

<dl class="grid gap-4">
<dt class="text-sm text-muted-foreground">Abilities</dt>
{#each detail.abilities as ability (ability.slug)}
<dd>
<p class="font-medium">
<a href="/abilities/{ability.slug}" class="hover:underline hover:underline-offset-4">{ability.name}</a>
{#if ability.hidden}
<span class="ml-1 text-xs font-normal text-muted-foreground">Hidden ability</span>
{/if}
</p>
<p class="max-w-prose text-sm text-muted-foreground">{ability.effect}</p>
</dd>
{/each}
</dl>
</div>

<dl class="grid content-start divide-y rounded-lg border bg-card text-sm">
<div class="grid grid-cols-[7rem_1fr] items-baseline gap-4 px-4 py-3">
<dt class="text-muted-foreground">Height</dt>
<dd class="tabular"><span class="font-medium">{height.metric}</span> <span class="ml-2 text-muted-foreground">{height.imperial}</span></dd>
</div>
<div class="grid grid-cols-[7rem_1fr] items-baseline gap-4 px-4 py-3">
<dt class="text-muted-foreground">Weight</dt>
<dd class="tabular"><span class="font-medium">{weight.metric}</span> <span class="ml-2 text-muted-foreground">{weight.imperial}</span></dd>
</div>
<div class="grid grid-cols-[7rem_1fr] items-center gap-4 px-4 py-3">
<dt class="text-muted-foreground">Gender</dt>
<dd><GenderRatio rate={detail.breeding.genderRate} /></dd>
</div>
</dl>
</div>
Loading
Loading