From 671e72972a2320294a16ac9f6d54b5e8cb7705dc Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 10 Sep 2026 11:24:31 -0400 Subject: [PATCH] Apply Prettier to the whole tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure formatting — no behaviour change. Verified: build passes, all 14 routes render, service worker active, no console errors. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu --- .github/ISSUE_TEMPLATE/bug_report.md | 18 +- .github/ISSUE_TEMPLATE/feature_request.md | 4 +- .github/dependabot.yml | 6 +- AGENTS.md | 26 +- CODE_OF_CONDUCT.md | 20 +- CONTRIBUTING.md | 4 +- README.md | 47 +-- SECURITY.md | 4 +- docker-compose.yml | 2 +- index.html | 5 +- scripts/build-snapshot.mjs | 47 +-- src/components/Card.js | 15 +- src/components/EvolutionChain.js | 9 +- src/components/GameSheet.js | 10 +- src/components/MovesList.js | 30 +- src/components/Nav.js | 14 +- src/components/PokemonPicker.js | 6 +- src/components/ProgressRing.js | 7 +- src/components/Sprite.js | 3 +- src/components/skeletons.js | 9 +- src/data/game-sprites.js | 6 +- src/data/pokedex-resolver.js | 8 +- src/data/type-chart.js | 63 +++- src/lib/anim.js | 6 +- src/lib/damage-calc.js | 12 +- src/lib/savedex.js | 78 ++++- src/lib/swipe.js | 6 +- src/lib/type-color.js | 21 +- src/main.js | 4 +- src/router.js | 12 +- src/store/createStore.js | 5 +- src/store/selection.js | 8 +- src/store/settings.js | 10 +- src/store/shinyHunts.js | 10 +- src/store/team.js | 5 +- src/styles/layout.css | 359 ++++++++++++++++------ src/styles/tokens.css | 33 +- src/views/AbilityDetail.js | 4 +- src/views/BreedingView.js | 61 +++- src/views/CompareView.js | 12 +- src/views/DexGrid.js | 98 ++++-- src/views/ItemDetail.js | 6 +- src/views/MoveDetail.js | 9 +- src/views/NaturesView.js | 6 +- src/views/PokemonDetail.js | 96 ++++-- src/views/ProgressView.js | 10 +- src/views/SearchView.js | 128 ++++++-- src/views/SettingsView.js | 198 +++++++----- src/views/ShinyView.js | 56 +++- src/views/TeamView.js | 100 +++++- src/views/TypeChartView.js | 40 ++- vite.config.js | 6 +- 52 files changed, 1269 insertions(+), 493 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c9f6f60..6da89ee 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,24 +1,24 @@ --- name: Bug report about: Something isn't working right -title: "" +title: '' labels: bug -assignees: "" +assignees: '' --- -**What happened** - +**What happened** (and what you expected instead) **Steps to reproduce** -1. -2. -3. + +1. Go to … +2. … +3. … **Context** + - Browser / OS: - Installed as a PWA or in a browser tab: - Selected game (if relevant): - Console errors (DevTools → Console): -**Screenshot / clip** - +**Screenshot / clip** (optional but very helpful) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 4fa5af6..bb547fb 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,9 +1,9 @@ --- name: Feature request about: Suggest an idea -title: "" +title: '' labels: enhancement -assignees: "" +assignees: '' --- **The idea** diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c620f95..a70b26c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,18 +1,18 @@ version: 2 updates: - package-ecosystem: npm - directory: "/" + directory: '/' schedule: interval: weekly groups: workbox: patterns: - - "workbox-*" + - 'workbox-*' dev-dependencies: dependency-type: development open-pull-requests-limit: 5 - package-ecosystem: github-actions - directory: "/" + directory: '/' schedule: interval: monthly diff --git a/AGENTS.md b/AGENTS.md index 7e86f15..e4caf2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,18 +28,18 @@ npm run preview # serve the production build — REQUIRED to test the se ## Project structure -| Path | What | -| --- | --- | -| `src/main.js` | boot: theme, nav, router, SW registration, install prompt | -| `src/router.js` | hash router; `routes` array; each view is `() => Promise` | -| `src/views/*.js` | one file per route (`DexGrid`, `PokemonDetail`, `TeamView`, …) | -| `src/components/*.js` | reusable DOM pieces (`Card`, `Sprite`, `TypeChip`, `CompareTable`, …) | -| `src/store/*.js` | `createStore()` state, persisted to `localStorage` under `pdx.*` | -| `src/data/*.js` | snapshot loader, pokedex resolver, lazy PokéAPI client, type chart, natures | -| `src/lib/*.js` | app-agnostic helpers (`dom`, `anim`, `damage-calc`, `savedex`, `swipe`, …) | -| `scripts/build-snapshot.mjs` | the only thing that calls PokéAPI at build time | -| `src/styles/tokens.css` | palette, type colours, the five themes | -| `src/styles/layout.css` | everything else | +| Path | What | +| ---------------------------- | --------------------------------------------------------------------------- | +| `src/main.js` | boot: theme, nav, router, SW registration, install prompt | +| `src/router.js` | hash router; `routes` array; each view is `() => Promise` | +| `src/views/*.js` | one file per route (`DexGrid`, `PokemonDetail`, `TeamView`, …) | +| `src/components/*.js` | reusable DOM pieces (`Card`, `Sprite`, `TypeChip`, `CompareTable`, …) | +| `src/store/*.js` | `createStore()` state, persisted to `localStorage` under `pdx.*` | +| `src/data/*.js` | snapshot loader, pokedex resolver, lazy PokéAPI client, type chart, natures | +| `src/lib/*.js` | app-agnostic helpers (`dom`, `anim`, `damage-calc`, `savedex`, `swipe`, …) | +| `scripts/build-snapshot.mjs` | the only thing that calls PokéAPI at build time | +| `src/styles/tokens.css` | palette, type colours, the five themes | +| `src/styles/layout.css` | everything else | ## Conventions @@ -61,7 +61,7 @@ npm run preview # serve the production build — REQUIRED to test the se - Respect `prefersReducedMotion()` (from `src/store/settings.js`) for any animation. - 2-space indent, semicolons, single quotes, trailing commas in multi-line - literals. Match the surrounding file; keep comments about *why*. + literals. Match the surrounding file; keep comments about _why_. - No new runtime dependencies. Prefer adding to the snapshot over a new runtime fetch. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 295ae6b..d0a6761 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or advances of +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c9d03aa..0e08de2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,7 +65,7 @@ code**: - Stores: `set(fn)` **replaces** state — spread `...s` yourself. Read with `get()`, react with `subscribe()`, and unsubscribe in the view's teardown (`onTeardown(view, off)`). -- Keep comments about *why*, not *what*. Match the existing density. +- Keep comments about _why_, not _what_. Match the existing density. - Respect `prefersReducedMotion()` for any new animation. - Keep everything game-aware: if you touch typings, type effectiveness, learnsets or evolution, honour the selected game's generation @@ -105,7 +105,7 @@ Describe what you tested in the PR. - Branch off `main`. One focused change per PR. - Imperative commit subjects ("Add …", "Fix …"), with a body explaining - the *why* when it isn't obvious. + the _why_ when it isn't obvious. - Don't commit `dist/` or `src/data/snapshot.json` (both git-ignored). - Fill in the PR template. Screenshots or a short clip for anything visual. diff --git a/README.md b/README.md index d010522..5673162 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,12 @@ npm run snapshot # fetch the data snapshot from PokéAPI (one time) npm run dev # http://localhost:5173 ``` -| Script | What it does | -| --- | --- | +| Script | What it does | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `npm run snapshot` | Build `src/data/snapshot.json` from PokéAPI. Skips if the file is under 30 days old; `npm run snapshot -- --force` to rebuild anyway. | -| `npm run dev` | Vite dev server with HMR. | -| `npm run build` | Runs `snapshot` (as `prebuild`), then builds to `dist/`. | -| `npm run preview` | Serve the production build. **Use this to test the service worker / offline / install** — none of that runs under `npm run dev`. | +| `npm run dev` | Vite dev server with HMR. | +| `npm run build` | Runs `snapshot` (as `prebuild`), then builds to `dist/`. | +| `npm run preview` | Serve the production build. **Use this to test the service worker / offline / install** — none of that runs under `npm run dev`. | The snapshot is **git-ignored** — it's a build artifact. `npm install` doesn't create it; run `npm run snapshot` (or `npm run build`) first, or @@ -54,6 +54,7 @@ the app has no data to show. ## Features ### Dex grid + Type-tinted cards — official artwork over a type-coloured spotlight, ghost number, per-dex progress ring. Filter by name/number; chips for caught / missing / favourites / legendary / has-a-note; a filter drawer for @@ -65,6 +66,7 @@ version-exclusive badge ("Ruby only"), derived from baked wild-encounter data (so gift/trade-only exclusives are missed). ### Detail page + Type-gradient hero with a shared-element morph from the tapped card, then a tabbed sheet: @@ -87,9 +89,10 @@ compact **Appearance** dropdown, plus a per-species "form dex" checklist tracked independently of the main caught flag. ### Team builder (`#/team`) + A lineup of up to 6 (specific forms included). Three modes: -- **Coverage** — leads with a *weak spots* summary (types that hit 2+ +- **Coverage** — leads with a _weak spots_ summary (types that hit 2+ members, or that someone's weak to and nobody resists), then a full defensive matchup grid with a diverging resist ◀│▶ weak bar per type, offensive STAB gaps, and at-a-glance stats. @@ -108,6 +111,7 @@ and a **Breeding** helper (`#/breeding` — egg-group browser, compatibility check, and egg moves resolved to the base breeding stage). ### Search (`#/search`) + Tabbed lookup — Pokémon, Moves, Items, Abilities — all browsable offline from the snapshot. Moves filter by type / damage class / "TMs only" / "HMs only" for the selected game and sort by power / accuracy / recency, or @@ -117,28 +121,31 @@ move, ability and item detail pages each list the Pokémon connected to them selected game. Query, scroll, tab and filters persist. ### Progress by game (`#/progress`) + Every game's seen/caught tally against its own dex, from its own per-game bucket. Leads with the games you've played (selected at least once, or imported a save for); the rest sit behind a toggle. The National Dex row is the union across all games. ### Shiny hunt tracker (`#/shiny`) + Per-hunt counters with method-aware odds (full odds, Masuda, SOS, chain fishing, DexNav, radar, dynamax adventures, outbreaks, mass outbreaks) and Shiny Charm, cumulative-probability readout, notes, and a "found it" that marks the Pokémon caught. ### Save-file import + **Settings → Import from a game save.** Reads the game's own seen/owned bitfields — not an approximation — from: -| Gen | Games | Format | -| --- | --- | --- | -| 1 | Red / Blue / Yellow | 32 KB SRAM (`.sav` / `.srm`) | -| 2 | Gold / Silver / Crystal | 32 KB SRAM — checksum picks G/S vs Crystal | -| 3 | Ruby / Sapphire / Emerald, FireRed / LeafGreen | 128 KB — newer slot, rotated sections | -| 4 | Diamond / Pearl / Platinum, HeartGold / SoulSilver | 512 KB NDS — active slot, DeSmuME `.dsv` footer stripped | -| 5 | Black / White, Black 2 / White 2 | 512 KB NDS — per-form "seen" copies OR'd | +| Gen | Games | Format | +| --- | -------------------------------------------------- | -------------------------------------------------------- | +| 1 | Red / Blue / Yellow | 32 KB SRAM (`.sav` / `.srm`) | +| 2 | Gold / Silver / Crystal | 32 KB SRAM — checksum picks G/S vs Crystal | +| 3 | Ruby / Sapphire / Emerald, FireRed / LeafGreen | 128 KB — newer slot, rotated sections | +| 4 | Diamond / Pearl / Platinum, HeartGold / SoulSilver | 512 KB NDS — active slot, DeSmuME `.dsv` footer stripped | +| 5 | Black / White, Black 2 / White 2 | 512 KB NDS — per-form "seen" copies OR'd | Gen 1–3 are checksum-verified; Gen 4–5 offsets come from PKHeX and are validated structurally (nothing set past the last species; every caught @@ -150,6 +157,7 @@ Full local state (settings, per-game tracking, team, form tracking, shiny hunts, played games) also exports / imports as a single JSON file. ### Settings + Theme (System / Light / Dark / Black OLED / Sepia) + accent colour; text size; an in-app reduce-motion override (the OS preference is always respected too); sprite style (modern pixel / game-era pixel / official @@ -157,6 +165,7 @@ artwork / HOME); haptic feedback on catch (Vibration API); which screen to land on at launch; JSON export / import; cache and tracking resets. ### PWA + Workbox service worker: precache the app shell + snapshot; stale-while- revalidate for PokéAPI JSON (30-day TTL); cache-first LRU for sprites; in-app update toast. Installable with maskable icons, `beforeinstallprompt` @@ -169,12 +178,12 @@ Type chart. ### Data & storage -| Layer | Where | Notes | -| --- | --- | --- | -| Preferences + tracking | `localStorage` (`pdx.*` keys) | Tiny, synchronous, restored on reload. | -| Build-time snapshot | `src/data/snapshot.json` (~150 KB) | Every species (id, name, generation, current + past typings, base stats, height/weight, species flags), every regional Pokédex, every version group, plus move / item / ability name indexes and baked TM numbers and version-exclusivity. Precached by the SW. | -| Detail data | `pokeapi.co`, fetched lazily per page | Stats, abilities, flavour text, evolution, encounters. Cached by the SW (stale-while-revalidate). | -| Sprites | PokéAPI sprites repo, cache-first | Capped LRU. | +| Layer | Where | Notes | +| ---------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Preferences + tracking | `localStorage` (`pdx.*` keys) | Tiny, synchronous, restored on reload. | +| Build-time snapshot | `src/data/snapshot.json` (~150 KB) | Every species (id, name, generation, current + past typings, base stats, height/weight, species flags), every regional Pokédex, every version group, plus move / item / ability name indexes and baked TM numbers and version-exclusivity. Precached by the SW. | +| Detail data | `pokeapi.co`, fetched lazily per page | Stats, abilities, flavour text, evolution, encounters. Cached by the SW (stale-while-revalidate). | +| Sprites | PokéAPI sprites repo, cache-first | Capped LRU. | The list, search, sort, filters and game switching run **entirely off the snapshot** — no network until you open a detail page. diff --git a/SECURITY.md b/SECURITY.md index 6786be1..7d8b944 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,8 +10,8 @@ branches. Please **don't** open a public issue for a security problem. -Use GitHub's **private vulnerability reporting** (the *Report a -vulnerability* button under the repository's *Security* tab). Include what +Use GitHub's **private vulnerability reporting** (the _Report a +vulnerability_ button under the repository's _Security_ tab). Include what you found, how to reproduce it, and the impact you think it has. You'll get an acknowledgement as soon as possible. diff --git a/docker-compose.yml b/docker-compose.yml index aa769aa..3efb129 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,5 +6,5 @@ services: BASE_PATH: / image: pocketdex ports: - - "8080:80" + - '8080:80' restart: unless-stopped diff --git a/index.html b/index.html index 521c3fd..fb9dc1f 100644 --- a/index.html +++ b/index.html @@ -2,10 +2,7 @@ - + x.name), }); }); - versionGroups.sort( - (a, b) => a.generation - b.generation || a.key.localeCompare(b.key), - ); + versionGroups.sort((a, b) => a.generation - b.generation || a.key.localeCompare(b.key)); // ---- Species: the data-rich part ----------------------------- const speciesIndex = await api('pokemon-species?limit=100000'); - const ids = speciesIndex.results - .map((r) => idFromUrl(r.url)) - .sort((a, b) => a - b); + const ids = speciesIndex.results.map((r) => idFromUrl(r.url)).sort((a, b) => a - b); const statMap = (pk) => { const s = Object.fromEntries(pk.stats.map((x) => [x.stat.name, x.base_stat])); @@ -192,17 +186,21 @@ async function main() { }; }; const bstOf = (st) => st.hp + st.atk + st.def + st.spa + st.spd + st.spe; - const abilKey = (pk) => pk.abilities.map((a) => a.ability.name).sort().join(','); + const abilKey = (pk) => + pk.abilities + .map((a) => a.ability.name) + .sort() + .join(','); const statKey = (st) => `${st.hp}/${st.atk}/${st.def}/${st.spa}/${st.spd}/${st.spe}`; console.log(`Fetching ${ids.length} species (stats, typings, flags, forms) …`); const species = await mapLimit(ids, CONCURRENCY, async (id) => { - const [pk, sp] = await Promise.all([ - api(`pokemon/${id}`), - api(`pokemon-species/${id}`), - ]); + const [pk, sp] = await Promise.all([api(`pokemon/${id}`), api(`pokemon-species/${id}`)]); const stats = statMap(pk); - const baseTypes = pk.types.slice().sort((a, b) => a.slot - b.slot).map((t) => t.type.name); + const baseTypes = pk.types + .slice() + .sort((a, b) => a.slot - b.slot) + .map((t) => t.type.name); const baseAbil = abilKey(pk); const baseStatKey = statKey(stats); @@ -226,7 +224,10 @@ async function main() { continue; } const fstats = statMap(fp); - const ftypes = fp.types.slice().sort((a, b) => a.slot - b.slot).map((t) => t.type.name); + const ftypes = fp.types + .slice() + .sort((a, b) => a.slot - b.slot) + .map((t) => t.type.name); const cat = formCategory(slug, sp.name); const entry = { slug, @@ -296,7 +297,10 @@ async function main() { .map((t) => t.type.name), pastTypes: (pk.past_types || []).map((p) => ({ gen: idFromUrl(p.generation.url), - types: p.types.slice().sort((a, b) => a.slot - b.slot).map((t) => t.type.name), + types: p.types + .slice() + .sort((a, b) => a.slot - b.slot) + .map((t) => t.type.name), })), stats, bst: stats.hp + stats.atk + stats.def + stats.spa + stats.spd + stats.spe, @@ -326,7 +330,9 @@ async function main() { // exclusive there. Wild-only — gift/trade/evolution exclusives are missed. const versionToVg = {}; for (const vg of versionGroups) for (const v of vg.versions) versionToVg[v] = vg.key; - const vgVersionCount = Object.fromEntries(versionGroups.map((vg) => [vg.key, vg.versions.length])); + const vgVersionCount = Object.fromEntries( + versionGroups.map((vg) => [vg.key, vg.versions.length]), + ); console.log(`Fetching wild encounters for ${species.length} species …`); await mapLimit(species, CONCURRENCY, async (sp) => { let enc; @@ -410,10 +416,7 @@ async function main() { name: a.name, generation: idFromUrl(a.generation.url), isMainSeries: a.is_main_series, - effect: - (en && (en.short_effect || en.effect)) || - (flavour && flavour.flavor_text) || - '', + effect: (en && (en.short_effect || en.effect)) || (flavour && flavour.flavor_text) || '', pokemon: [ ...new Set(a.pokemon.map((x) => idFromUrl(x.pokemon.url)).filter((n) => n <= 100000)), ], diff --git a/src/components/Card.js b/src/components/Card.js index a2c9bbd..441a9c7 100644 --- a/src/components/Card.js +++ b/src/components/Card.js @@ -13,7 +13,11 @@ import { prettify } from '../data/pokedex-resolver.js'; * gradient, a type-colored spotlight behind an oversized sprite that lifts * above the card, a number chip, and a big ghost number in the corner. */ -export function Card(species, number, { spriteStyle = 'official', versionGroup, gen = 9, boxed = false, metric = null } = {}) { +export function Card( + species, + number, + { spriteStyle = 'official', versionGroup, gen = 9, boxed = false, metric = null } = {}, +) { const state = entry(species.id); const types = typesForGen(species, gen); const mainType = types[0] || 'normal'; @@ -61,7 +65,8 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup, onclick: () => { // Tag this sprite so the router's view transition morphs it into // the detail hero. - for (const n of document.querySelectorAll('.card__art.is-morph')) n.classList.remove('is-morph'); + for (const n of document.querySelectorAll('.card__art.is-morph')) + n.classList.remove('is-morph'); art.classList.add('is-morph'); }, }, @@ -85,7 +90,11 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup, (() => { const ex = versionGroup && versionGroup !== 'all' && species.exclusiveIn?.[versionGroup]; return ex - ? el('span', { class: 'card__excl', title: 'Version exclusive (wild)' }, `${ex.map(prettify).join(' & ')} only`) + ? el( + 'span', + { class: 'card__excl', title: 'Version exclusive (wild)' }, + `${ex.map(prettify).join(' & ')} only`, + ) : null; })(), metric ? el('span', { class: 'card__metric' }, metric) : null, diff --git a/src/components/EvolutionChain.js b/src/components/EvolutionChain.js index f3b519f..7db1187 100644 --- a/src/components/EvolutionChain.js +++ b/src/components/EvolutionChain.js @@ -34,7 +34,10 @@ function nodeEl(stage, style, versionGroup, depth) { * surviving ancestor — so Pikachu → Raichu still shows in Red/Blue even * though Pichu doesn't, and Bellossom never appears there. */ -export function EvolutionChain(chain, { style = 'default', versionGroup, genOf = () => 1, maxGen = 9 } = {}) { +export function EvolutionChain( + chain, + { style = 'default', versionGroup, genOf = () => 1, maxGen = 9 } = {}, +) { const nodes = new Map(); let pruned = false; @@ -95,9 +98,7 @@ export function EvolutionChain(chain, { style = 'default', versionGroup, genOf = roots.forEach((r) => renderNode(r, 0)); if (pruned) { - wrap.append( - el('p', { class: 'evo__note' }, 'Cross-generation stages hidden for this game.'), - ); + wrap.append(el('p', { class: 'evo__note' }, 'Cross-generation stages hidden for this game.')); } return wrap; } diff --git a/src/components/GameSheet.js b/src/components/GameSheet.js index 37e5d54..a09335e 100644 --- a/src/components/GameSheet.js +++ b/src/components/GameSheet.js @@ -96,9 +96,7 @@ export async function openGameSheet() { ); for (const { generation, versionGroups } of versionGroupsByGeneration(snap)) { - list.append( - el('h3', { class: 'gsheet__gen' }, `Gen ${generation.id} · ${generation.name}`), - ); + list.append(el('h3', { class: 'gsheet__gen' }, `Gen ${generation.id} · ${generation.name}`)); const grid = el('div', { class: 'gsheet__grid' }); for (const vg of versionGroups) { const active = vg.key === st.versionGroup; @@ -126,7 +124,11 @@ export async function openGameSheet() { 'span', { class: 'gamecard__foot' }, el('span', { class: 'gamecard__meta' }, vg.versions.map(prettify).join(' / ')), - el('span', { class: 'gamecard__dex' }, `${dexCount} ${dexCount === 1 ? 'dex' : 'dexes'}`), + el( + 'span', + { class: 'gamecard__dex' }, + `${dexCount} ${dexCount === 1 ? 'dex' : 'dexes'}`, + ), ), ), ); diff --git a/src/components/MovesList.js b/src/components/MovesList.js index ec7b211..1d941c3 100644 --- a/src/components/MovesList.js +++ b/src/components/MovesList.js @@ -6,8 +6,7 @@ import { TypeChip } from './TypeChip.js'; export async function machineNumber(moveData, versionGroupKey) { const list = moveData.machines || []; if (!list.length) return null; - const hit = - list.find((x) => x.version_group.name === versionGroupKey) || list[list.length - 1]; + const hit = list.find((x) => x.version_group.name === versionGroupKey) || list[list.length - 1]; try { const mc = await getMachine(Number(hit.machine.url.replace(/\/$/, '').split('/').pop())); const m = mc.item.name.match(/^([a-z]+)0*(\d+)$/i); @@ -53,7 +52,15 @@ function englishText(entries, key) { // Before Gen 4, a move's physical/special class was decided by its TYPE, // not set per move. const PHYSICAL_TYPES = new Set([ - 'normal', 'fighting', 'poison', 'ground', 'flying', 'bug', 'rock', 'ghost', 'steel', + 'normal', + 'fighting', + 'poison', + 'ground', + 'flying', + 'bug', + 'rock', + 'ghost', + 'steel', ]); export function forGeneration(d, gen, genOfVg) { @@ -145,7 +152,11 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = () const enrichers = []; for (const m of moves) { - const meta = el('span', { class: 'moverow__meta' }, el('span', { class: 'moverow__pending' }, '…')); + const meta = el( + 'span', + { class: 'moverow__meta' }, + el('span', { class: 'moverow__pending' }, '…'), + ); const body = el('div', { class: 'movebody', hidden: true }); const tmCell = method === 'machine' ? el('span', { class: 'moverow__tm' }) : null; let bodyFilled = false; @@ -179,7 +190,9 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = () fillBody(body, m.data, v); await fillTm(); } catch { - body.replaceChildren(el('span', { class: 'moverow__pending' }, 'Details unavailable.')); + body.replaceChildren( + el('span', { class: 'moverow__pending' }, 'Details unavailable.'), + ); } } }, @@ -268,9 +281,12 @@ function fillBody(body, d, v) { if (d.priority) bits.push(`Priority ${d.priority > 0 ? '+' : ''}${d.priority}`); if (d.target?.name) bits.push(`Target: ${d.target.name.replace(/-/g, ' ')}`); if (d.meta?.ailment?.name && d.meta.ailment.name !== 'none') { - bits.push(`May ${d.meta.ailment.name.replace(/-/g, ' ')}${d.meta.ailment_chance ? ` (${d.meta.ailment_chance}%)` : ''}`); + bits.push( + `May ${d.meta.ailment.name.replace(/-/g, ' ')}${d.meta.ailment_chance ? ` (${d.meta.ailment_chance}%)` : ''}`, + ); } - if (d.meta?.drain) bits.push(`${d.meta.drain > 0 ? 'Drains' : 'Recoil'} ${Math.abs(d.meta.drain)}%`); + if (d.meta?.drain) + bits.push(`${d.meta.drain > 0 ? 'Drains' : 'Recoil'} ${Math.abs(d.meta.drain)}%`); if (d.meta?.healing) bits.push(`Heals ${d.meta.healing}%`); if (d.meta?.crit_rate) bits.push(`+${d.meta.crit_rate} crit rate`); if (d.meta?.flinch_chance) bits.push(`Flinch ${d.meta.flinch_chance}%`); diff --git a/src/components/Nav.js b/src/components/Nav.js index 5cb2636..a5ef22f 100644 --- a/src/components/Nav.js +++ b/src/components/Nav.js @@ -4,7 +4,13 @@ import { settings } from '../store/settings.js'; const ITEMS = [ { label: 'Dex', icon: '▦', href: '#/', match: (h) => h === '#/' || h === '' || h === '#' }, - { label: 'Team', icon: '⛨', href: '#/team', match: (h) => h.startsWith('#/team'), optional: 'showTeamNav' }, + { + label: 'Team', + icon: '⛨', + href: '#/team', + match: (h) => h.startsWith('#/team'), + optional: 'showTeamNav', + }, { label: 'Games', icon: '◉', action: openGameSheet }, { label: 'Search', icon: '⌕', href: '#/search', match: (h) => h.startsWith('#/search') }, { label: 'Settings', icon: '⚙', href: '#/settings', match: (h) => h.startsWith('#/settings') }, @@ -28,7 +34,11 @@ export function Nav() { el('span', { class: 'nav__label' }, item.label), ]; return item.action - ? el('button', { class: 'nav__link', type: 'button', onclick: () => item.action() }, ...inner) + ? el( + 'button', + { class: 'nav__link', type: 'button', onclick: () => item.action() }, + ...inner, + ) : el('a', { class: 'nav__link', href: item.href }, ...inner); }); nav.replaceChildren(el('span', { class: 'nav__brand' }, 'Pocketdex'), ...links); diff --git a/src/components/PokemonPicker.js b/src/components/PokemonPicker.js index c41162a..0e78e02 100644 --- a/src/components/PokemonPicker.js +++ b/src/components/PokemonPicker.js @@ -88,7 +88,11 @@ export async function openPokemonPicker(onPick, { closeAfterPick = false } = {}) 'div', { class: 'gsheet__head' }, el('h2', {}, 'Add a Pokémon'), - el('button', { class: 'gsheet__close', type: 'button', 'aria-label': 'Close', onclick: close }, '✕'), + el( + 'button', + { class: 'gsheet__close', type: 'button', 'aria-label': 'Close', onclick: close }, + '✕', + ), ); dragToClose(panel, head, close); panel.append(head, input, results); diff --git a/src/components/ProgressRing.js b/src/components/ProgressRing.js index 27cd086..aae46a6 100644 --- a/src/components/ProgressRing.js +++ b/src/components/ProgressRing.js @@ -40,7 +40,12 @@ export function ProgressRing() { const value = el('span', { class: 'ring__value' }, num, tail); const node = el( 'a', - { class: 'ring', href: '#/progress', title: 'Progress by game', 'aria-label': 'Progress by game' }, + { + class: 'ring', + href: '#/progress', + title: 'Progress by game', + 'aria-label': 'Progress by game', + }, svg, el('div', { class: 'ring__label' }, value), ); diff --git a/src/components/Sprite.js b/src/components/Sprite.js index fef993c..08bf44c 100644 --- a/src/components/Sprite.js +++ b/src/components/Sprite.js @@ -1,8 +1,7 @@ import { el } from '../lib/dom.js'; import { gameSpriteUrl } from '../data/game-sprites.js'; -const SPRITES = - 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon'; +const SPRITES = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon'; function pixel(id, shiny) { return `${SPRITES}${shiny ? '/shiny' : ''}/${id}.png`; diff --git a/src/components/skeletons.js b/src/components/skeletons.js index 4ee5282..a7abe2d 100644 --- a/src/components/skeletons.js +++ b/src/components/skeletons.js @@ -2,8 +2,7 @@ import { el } from '../lib/dom.js'; import { Sprite } from './Sprite.js'; import { settings } from '../store/settings.js'; -const bar = (w, h = 14) => - el('span', { class: 'sk-bar', style: `width:${w};height:${h}px` }); +const bar = (w, h = 14) => el('span', { class: 'sk-bar', style: `width:${w};height:${h}px` }); const facts = () => el( @@ -43,11 +42,7 @@ export function detailSkeleton(match) { el( 'div', { class: 'psheet' }, - el( - 'div', - { class: 'psheet__tabs' }, - ...Array.from({ length: 5 }, () => bar('58px', 18)), - ), + el('div', { class: 'psheet__tabs' }, ...Array.from({ length: 5 }, () => bar('58px', 18))), el('div', { class: 'ppanel' }, bar('92%'), bar('84%'), bar('66%'), facts()), ), ); diff --git a/src/data/game-sprites.js b/src/data/game-sprites.js index 6069ff3..201d857 100644 --- a/src/data/game-sprites.js +++ b/src/data/game-sprites.js @@ -4,8 +4,7 @@ * group) to the closest sprite folder. Games newer than Gen 7 shipped no * pixel sprites — those return null and the caller falls back to HOME art. */ -const REPO = - 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions'; +const REPO = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions'; const PATH_BY_VERSION_GROUP = { 'red-blue': 'generation-i/red-blue', @@ -26,8 +25,7 @@ const PATH_BY_VERSION_GROUP = { 'omega-ruby-alpha-sapphire': 'generation-vi/omegaruby-alphasapphire', 'sun-moon': 'generation-vii/ultra-sun-ultra-moon', 'ultra-sun-ultra-moon': 'generation-vii/ultra-sun-ultra-moon', - 'brilliant-diamond-and-shining-pearl': - 'generation-viii/brilliant-diamond-shining-pearl', + 'brilliant-diamond-and-shining-pearl': 'generation-viii/brilliant-diamond-shining-pearl', }; /** URL for a game-era pixel sprite, or null if that game has none. */ diff --git a/src/data/pokedex-resolver.js b/src/data/pokedex-resolver.js index 2b6f1df..f8c16bc 100644 --- a/src/data/pokedex-resolver.js +++ b/src/data/pokedex-resolver.js @@ -4,18 +4,14 @@ */ export function prettify(key) { - return key - .replace(/-/g, ' ') - .replace(/\b\w/g, (c) => c.toUpperCase()); + return key.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); } /** Regional dexes belonging to a version group, in the API's order. */ export function dexesForVersionGroup(snap, versionGroupKey) { const vg = snap.versionGroupByKey.get(versionGroupKey); const keys = vg && vg.pokedexKeys.length ? vg.pokedexKeys : ['national']; - return keys - .map((k) => snap.pokedexByKey.get(k)) - .filter(Boolean); + return keys.map((k) => snap.pokedexByKey.get(k)).filter(Boolean); } /** diff --git a/src/data/type-chart.js b/src/data/type-chart.js index f9ef7be..968789f 100644 --- a/src/data/type-chart.js +++ b/src/data/type-chart.js @@ -5,9 +5,24 @@ * CHART[attacking][defending] holds only the non-1x multipliers. */ export const TYPES = [ - 'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison', - 'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark', - 'steel', 'fairy', + 'normal', + 'fire', + 'water', + 'electric', + 'grass', + 'ice', + 'fighting', + 'poison', + 'ground', + 'flying', + 'psychic', + 'bug', + 'rock', + 'ghost', + 'dragon', + 'dark', + 'steel', + 'fairy', ]; const CHART = { @@ -15,14 +30,48 @@ const CHART = { fire: { fire: 0.5, water: 0.5, grass: 2, ice: 2, bug: 2, rock: 0.5, dragon: 0.5, steel: 2 }, water: { fire: 2, water: 0.5, grass: 0.5, ground: 2, rock: 2, dragon: 0.5 }, electric: { water: 2, electric: 0.5, grass: 0.5, ground: 0, flying: 2, dragon: 0.5 }, - grass: { fire: 0.5, water: 2, grass: 0.5, poison: 0.5, ground: 2, flying: 0.5, bug: 0.5, rock: 2, dragon: 0.5, steel: 0.5 }, + grass: { + fire: 0.5, + water: 2, + grass: 0.5, + poison: 0.5, + ground: 2, + flying: 0.5, + bug: 0.5, + rock: 2, + dragon: 0.5, + steel: 0.5, + }, ice: { fire: 0.5, water: 0.5, grass: 2, ice: 0.5, ground: 2, flying: 2, dragon: 2, steel: 0.5 }, - fighting: { normal: 2, ice: 2, poison: 0.5, flying: 0.5, psychic: 0.5, bug: 0.5, rock: 2, ghost: 0, dark: 2, steel: 2, fairy: 0.5 }, + fighting: { + normal: 2, + ice: 2, + poison: 0.5, + flying: 0.5, + psychic: 0.5, + bug: 0.5, + rock: 2, + ghost: 0, + dark: 2, + steel: 2, + fairy: 0.5, + }, poison: { grass: 2, poison: 0.5, ground: 0.5, rock: 0.5, ghost: 0.5, steel: 0, fairy: 2 }, ground: { fire: 2, electric: 2, grass: 0.5, poison: 2, flying: 0, bug: 0.5, rock: 2, steel: 2 }, flying: { electric: 0.5, grass: 2, fighting: 2, bug: 2, rock: 0.5, steel: 0.5 }, psychic: { fighting: 2, poison: 2, psychic: 0.5, dark: 0, steel: 0.5 }, - bug: { fire: 0.5, grass: 2, fighting: 0.5, poison: 0.5, flying: 0.5, psychic: 2, ghost: 0.5, dark: 2, steel: 0.5, fairy: 0.5 }, + bug: { + fire: 0.5, + grass: 2, + fighting: 0.5, + poison: 0.5, + flying: 0.5, + psychic: 2, + ghost: 0.5, + dark: 2, + steel: 0.5, + fairy: 0.5, + }, rock: { fire: 2, ice: 2, fighting: 0.5, ground: 0.5, flying: 2, bug: 2, steel: 0.5 }, ghost: { normal: 0, psychic: 2, ghost: 2, dark: 0.5 }, dragon: { dragon: 2, steel: 0.5, fairy: 0 }, @@ -94,7 +143,7 @@ export function offensiveSummary(atkTypes, gen = 9) { * Returns { '4': [...types], '2': [...], '0.5': [...], '0.25': [...], '0': [...] } */ export function defensiveMatchups(defTypes, gen = 9) { - const buckets = { '4': [], '2': [], '0.5': [], '0.25': [], '0': [] }; + const buckets = { 4: [], 2: [], 0.5: [], 0.25: [], 0: [] }; // Types that didn't exist yet can't be attacking: Steel/Dark before Gen 2, // Fairy before Gen 6. const absent = new Set(gen < 2 ? ['steel', 'dark', 'fairy'] : gen < 6 ? ['fairy'] : []); diff --git a/src/lib/anim.js b/src/lib/anim.js index c304825..008de51 100644 --- a/src/lib/anim.js +++ b/src/lib/anim.js @@ -6,7 +6,11 @@ const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3); * Tween a number into `node`'s text. Snaps straight to the value when the * user prefers reduced motion. Returns a cancel fn. */ -export function countUp(node, to, { from = 0, duration = 550, decimals = 0, prefix = '', suffix = '', ease = easeOutCubic } = {}) { +export function countUp( + node, + to, + { from = 0, duration = 550, decimals = 0, prefix = '', suffix = '', ease = easeOutCubic } = {}, +) { const fmt = (v) => `${prefix}${decimals ? v.toFixed(decimals) : Math.round(v)}${suffix}`; if (prefersReducedMotion() || duration <= 0 || from === to) { node.textContent = fmt(to); diff --git a/src/lib/damage-calc.js b/src/lib/damage-calc.js index 16ee2dc..72c72c0 100644 --- a/src/lib/damage-calc.js +++ b/src/lib/damage-calc.js @@ -32,7 +32,17 @@ export function statAt(base, level, { iv = 31, ev = 0, nature = 'Hardy', statKey * * Returns null for status moves or 0-power moves. */ -export function calcDamage({ level, power, moveType, atkTypes, defTypes, atkStat, defStat, gen, crit = false }) { +export function calcDamage({ + level, + power, + moveType, + atkTypes, + defTypes, + atkStat, + defStat, + gen, + crit = false, +}) { if (!power) return null; const base = Math.floor((Math.floor((2 * level) / 5 + 2) * power * (atkStat / defStat)) / 50) + 2; const stab = atkTypes.includes(moveType) ? 1.5 : 1; diff --git a/src/lib/savedex.js b/src/lib/savedex.js index 0cd74bc..bb75c43 100644 --- a/src/lib/savedex.js +++ b/src/lib/savedex.js @@ -32,7 +32,16 @@ const GB_CHARS = (() => { t[0xa0 + i] = lo[i]; } for (let i = 0; i < 10; i++) t[0xf6 + i] = String(i); - Object.assign(t, { 0xe8: '.', 0xe3: '-', 0xf3: '/', 0xf4: ',', 0x9a: '(', 0x9b: ')', 0xe6: '?', 0xe7: '!' }); + Object.assign(t, { + 0xe8: '.', + 0xe3: '-', + 0xf3: '/', + 0xf4: ',', + 0x9a: '(', + 0x9b: ')', + 0xe6: '?', + 0xe7: '!', + }); return t; })(); @@ -47,7 +56,11 @@ const GBA_CHARS = (() => { t[0xd5 + i] = lo[i]; } Object.assign(t, { - 0xad: '.', 0xae: '-', 0xba: '/', 0xac: ',', 0xb8: '&', + 0xad: '.', + 0xae: '-', + 0xba: '/', + 0xac: ',', + 0xb8: '&', }); return t; })(); @@ -76,13 +89,21 @@ const sumBytes = (bytes, from, to) => { * Generation I — Red / Blue / Yellow (International, 32 KB SRAM) * ------------------------------------------------------------------ */ -const G1 = { name: 0x2598, owned: 0x25a3, seen: 0x25b6, species: 151, ckByte: 0x3523, ckFrom: 0x2598, ckTo: 0x3522 }; +const G1 = { + name: 0x2598, + owned: 0x25a3, + seen: 0x25b6, + species: 151, + ckByte: 0x3523, + ckFrom: 0x2598, + ckTo: 0x3522, +}; function tryGen1(bytes) { if (bytes.length < 0x8000) return null; const caught = bitsToList(bytes, G1.owned, G1.species); const seen = bitsToList(bytes, G1.seen, G1.species); - const checksumOk = ((~sumBytes(bytes, G1.ckFrom, G1.ckTo)) & 0xff) === bytes[G1.ckByte]; + const checksumOk = (~sumBytes(bytes, G1.ckFrom, G1.ckTo) & 0xff) === bytes[G1.ckByte]; if (!seen.length && !caught.length) return null; return { gen: 1, @@ -101,8 +122,26 @@ function tryGen1(bytes) { * ------------------------------------------------------------------ */ const G2 = { - gs: { name: 0x200b, owned: 0x2a4c, seen: 0x2a6c, ck: 0x2d69, ckFrom: 0x2009, ckTo: 0x2d68, label: 'Gold / Silver', vgs: ['gold-silver'] }, - cr: { name: 0x2009, owned: 0x2a27, seen: 0x2a47, ck: 0x2d0d, ckFrom: 0x2009, ckTo: 0x2b82, label: 'Crystal', vgs: ['crystal'] }, + gs: { + name: 0x200b, + owned: 0x2a4c, + seen: 0x2a6c, + ck: 0x2d69, + ckFrom: 0x2009, + ckTo: 0x2d68, + label: 'Gold / Silver', + vgs: ['gold-silver'], + }, + cr: { + name: 0x2009, + owned: 0x2a27, + seen: 0x2a47, + ck: 0x2d0d, + ckFrom: 0x2009, + ckTo: 0x2b82, + label: 'Crystal', + vgs: ['crystal'], + }, }; const G2_SPECIES = 251; @@ -145,7 +184,9 @@ const SECTION_SIZE = 4096; const SECTIONS_PER_SLOT = 14; const SLOT_SIZE = SECTION_SIZE * SECTIONS_PER_SLOT; // 57344 const SIGNATURE = 0x08012025; -const SECTION_DATA_SIZE = [3884, 3968, 3968, 3968, 3848, 3968, 3968, 3968, 3968, 3968, 3968, 3968, 3968, 2000]; +const SECTION_DATA_SIZE = [ + 3884, 3968, 3968, 3968, 3848, 3968, 3968, 3968, 3968, 3968, 3968, 3968, 3968, 2000, +]; const DEX_OFFSET = 0x18; const DEX_MAGIC = DEX_OFFSET + 2; // 0xDA = RSE national, 0xB9 = FRLG national const G3_OWNED = DEX_OFFSET + 16; @@ -179,7 +220,10 @@ function tryGen3(bytes) { if (bytes.length < SLOT_SIZE + SECTION_SIZE) return null; const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const a = readSlot(view, 0); - const b = bytes.length >= 2 * SLOT_SIZE ? readSlot(view, SLOT_SIZE) : { sections: {}, counter: -1, valid: 0 }; + const b = + bytes.length >= 2 * SLOT_SIZE + ? readSlot(view, SLOT_SIZE) + : { sections: {}, counter: -1, valid: 0 }; if (a.valid < 3 && b.valid < 3) return null; const slot = b.counter > a.counter ? b : a; const s0 = slot.sections[0]; @@ -222,8 +266,15 @@ function ndsNormalise(bytes) { let hasMarker = false; for (let i = 0; i <= tail.length - marker.length; i++) { let ok = true; - for (let j = 0; j < marker.length; j++) if (tail[i + j] !== marker.charCodeAt(j)) { ok = false; break; } - if (ok) { hasMarker = true; break; } + for (let j = 0; j < marker.length; j++) + if (tail[i + j] !== marker.charCodeAt(j)) { + ok = false; + break; + } + if (ok) { + hasMarker = true; + break; + } } if (hasMarker || bytes.length >= 0x80000) return bytes.subarray(0, 0x80000); } @@ -298,7 +349,12 @@ function readGen5Slot(bytes, slotBase, v) { const caught = bitsToList(bytes, p + GEN5.caught, GEN5.species); const seen = orSeen(bytes, p, GEN5.seen, GEN5.size, GEN5.species); if (!seen.length || caught.some((n) => !seen.includes(n))) return null; - return { seen, caught, label: v.label, vg: v.dex === GEN5.bw.dex ? 'black-white' : 'black-2-white-2' }; + return { + seen, + caught, + label: v.label, + vg: v.dex === GEN5.bw.dex ? 'black-white' : 'black-2-white-2', + }; } function tryGen5(raw) { diff --git a/src/lib/swipe.js b/src/lib/swipe.js index a899f8a..2c63102 100644 --- a/src/lib/swipe.js +++ b/src/lib/swipe.js @@ -20,7 +20,11 @@ export function onSwipe(target, { onLeft, onRight, threshold = 60 } = {}) { // the page? (moves list, tab strip, coverage grid…) `dir` < 0 means the // finger is moving left — that scrolls such an element to the right. const scrollsInside = (node, dir) => { - for (let n = node; n && n !== document.body && n !== target.parentElement; n = n.parentElement) { + for ( + let n = node; + n && n !== document.body && n !== target.parentElement; + n = n.parentElement + ) { if (n.scrollWidth - n.clientWidth > 2) { const ox = getComputedStyle(n).overflowX; if (ox === 'auto' || ox === 'scroll') { diff --git a/src/lib/type-color.js b/src/lib/type-color.js index 0105c48..62da9ca 100644 --- a/src/lib/type-color.js +++ b/src/lib/type-color.js @@ -2,9 +2,24 @@ // custom properties once and memoising. Type colors do not change between // light and dark, so the cache is safe for the session. const TYPES = [ - 'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison', - 'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark', - 'steel', 'fairy', + 'normal', + 'fire', + 'water', + 'electric', + 'grass', + 'ice', + 'fighting', + 'poison', + 'ground', + 'flying', + 'psychic', + 'bug', + 'rock', + 'ghost', + 'dragon', + 'dark', + 'steel', + 'fairy', ]; let cache; diff --git a/src/main.js b/src/main.js index 7f3dd36..7a678ec 100644 --- a/src/main.js +++ b/src/main.js @@ -41,9 +41,7 @@ settings.subscribe((s) => { if ((location.hash || '').startsWith('#/pokemon/')) rerender(); } }); -window - .matchMedia('(prefers-color-scheme: dark)') - .addEventListener('change', () => applyTheme()); +window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => applyTheme()); const app = document.getElementById('app'); app.replaceChildren(); diff --git a/src/router.js b/src/router.js index 4043644..7439d89 100644 --- a/src/router.js +++ b/src/router.js @@ -18,10 +18,18 @@ import { prefersReducedMotion } from './store/settings.js'; const routes = [ { pattern: /^#?\/?$/, view: () => DexGrid() }, - { pattern: /^#\/pokemon\/(\d+)$/, view: (m) => PokemonDetail(Number(m[1])), skeleton: detailSkeleton }, + { + pattern: /^#\/pokemon\/(\d+)$/, + view: (m) => PokemonDetail(Number(m[1])), + skeleton: detailSkeleton, + }, { pattern: /^#\/move\/(\d+)$/, view: (m) => MoveDetail(Number(m[1])), skeleton: lookupSkeleton }, { pattern: /^#\/item\/(\d+)$/, view: (m) => ItemDetail(Number(m[1])), skeleton: lookupSkeleton }, - { pattern: /^#\/ability\/(\d+)$/, view: (m) => AbilityDetail(Number(m[1])), skeleton: lookupSkeleton }, + { + pattern: /^#\/ability\/(\d+)$/, + view: (m) => AbilityDetail(Number(m[1])), + skeleton: lookupSkeleton, + }, { pattern: /^#\/team$/, view: () => TeamView() }, { pattern: /^#\/natures$/, view: () => NaturesView() }, { pattern: /^#\/types$/, view: () => TypeChartView() }, diff --git a/src/store/createStore.js b/src/store/createStore.js index 6ddd78f..56e7ceb 100644 --- a/src/store/createStore.js +++ b/src/store/createStore.js @@ -34,10 +34,7 @@ export function createStore(key, initial) { return { get: () => state, set(patch) { - state = - typeof patch === 'function' - ? patch(state) - : { ...state, ...patch }; + state = typeof patch === 'function' ? patch(state) : { ...state, ...patch }; save(); for (const fn of subscribers) fn(state); }, diff --git a/src/store/selection.js b/src/store/selection.js index 3e8f200..f2daa63 100644 --- a/src/store/selection.js +++ b/src/store/selection.js @@ -121,7 +121,8 @@ export function importFlags({ seen = [], caught = [], game } = {}) { const stamp = new Date().toISOString(); const bucket = { ...(s.games[vgKey] || {}) }; for (const id of seen) bucket[id] = { ...(bucket[id] || GBLANK), seen: true, updatedAt: stamp }; - for (const id of caught) bucket[id] = { ...(bucket[id] || GBLANK), seen: true, caught: true, updatedAt: stamp }; + for (const id of caught) + bucket[id] = { ...(bucket[id] || GBLANK), seen: true, caught: true, updatedAt: stamp }; return { ...s, games: { ...s.games, [vgKey]: bucket } }; }); } @@ -140,10 +141,7 @@ export function replaceFlags({ seen = [], caught = [], game } = {}) { /** Aggregate seen/caught over a species list for one game (union when 'all'). */ export function stats(speciesIds, vgKey = curGame()) { - const get = - vgKey === 'all' - ? unionEntry - : (id) => selection.get().games[vgKey]?.[id] || GBLANK; + const get = vgKey === 'all' ? unionEntry : (id) => selection.get().games[vgKey]?.[id] || GBLANK; let seen = 0; let caught = 0; for (const id of speciesIds) { diff --git a/src/store/settings.js b/src/store/settings.js index 3b67c22..8ff86e6 100644 --- a/src/store/settings.js +++ b/src/store/settings.js @@ -51,13 +51,10 @@ export function applyTheme(state = settings.get()) { if (meta) { const dark = DARKISH.has(theme) || - ((!theme || theme === 'system') && - window.matchMedia('(prefers-color-scheme: dark)').matches); + ((!theme || theme === 'system') && window.matchMedia('(prefers-color-scheme: dark)').matches); meta.setAttribute( 'content', - dark - ? '#0b0b0c' - : getComputedStyle(root).getPropertyValue('--accent').trim() || '#b3161a', + dark ? '#0b0b0c' : getComputedStyle(root).getPropertyValue('--accent').trim() || '#b3161a', ); } } @@ -65,7 +62,6 @@ export function applyTheme(state = settings.get()) { /** True if animations should be skipped — OS preference or the in-app override. */ export function prefersReducedMotion() { return ( - !!settings.get().reduceMotion || - window.matchMedia('(prefers-reduced-motion: reduce)').matches + !!settings.get().reduceMotion || window.matchMedia('(prefers-reduced-motion: reduce)').matches ); } diff --git a/src/store/shinyHunts.js b/src/store/shinyHunts.js index 6d6c6f1..969ab1f 100644 --- a/src/store/shinyHunts.js +++ b/src/store/shinyHunts.js @@ -14,7 +14,15 @@ export function addHunt({ speciesId, method, charm }) { shinyHunts.set((s) => ({ ...s, hunts: [ - { id: uid(), speciesId, method, charm: !!charm, count: 0, notes: '', startedAt: new Date().toISOString() }, + { + id: uid(), + speciesId, + method, + charm: !!charm, + count: 0, + notes: '', + startedAt: new Date().toISOString(), + }, ...s.hunts, ], })); diff --git a/src/store/team.js b/src/store/team.js index ba8538d..865dbb6 100644 --- a/src/store/team.js +++ b/src/store/team.js @@ -41,5 +41,8 @@ export function clearTeam() { } export function inTeam(id, form = null) { - return team.get().members.map(slot).some((m) => sameSlot(m, { id, form: form || null })); + return team + .get() + .members.map(slot) + .some((m) => sameSlot(m, { id, form: form || null })); } diff --git a/src/styles/layout.css b/src/styles/layout.css index ef194cb..138442c 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -142,7 +142,9 @@ font-weight: 600; text-decoration: none; line-height: 1; - transition: color 0.12s ease, background 0.12s ease; + transition: + color 0.12s ease, + background 0.12s ease; } .reflink:hover { color: var(--text); @@ -226,24 +228,64 @@ background: var(--type-normal); letter-spacing: 0.02em; } -.type-chip[data-type="normal"] { background: var(--type-normal); } -.type-chip[data-type="fire"] { background: var(--type-fire); } -.type-chip[data-type="water"] { background: var(--type-water); } -.type-chip[data-type="electric"] { background: var(--type-electric); color: #3a3a1a; } -.type-chip[data-type="grass"] { background: var(--type-grass); } -.type-chip[data-type="ice"] { background: var(--type-ice); color: #1c3b38; } -.type-chip[data-type="fighting"] { background: var(--type-fighting); } -.type-chip[data-type="poison"] { background: var(--type-poison); } -.type-chip[data-type="ground"] { background: var(--type-ground); } -.type-chip[data-type="flying"] { background: var(--type-flying); } -.type-chip[data-type="psychic"] { background: var(--type-psychic); } -.type-chip[data-type="bug"] { background: var(--type-bug); } -.type-chip[data-type="rock"] { background: var(--type-rock); color: #2e2a1c; } -.type-chip[data-type="ghost"] { background: var(--type-ghost); } -.type-chip[data-type="dragon"] { background: var(--type-dragon); } -.type-chip[data-type="dark"] { background: var(--type-dark); } -.type-chip[data-type="steel"] { background: var(--type-steel); } -.type-chip[data-type="fairy"] { background: var(--type-fairy); color: #3d1f3b; } +.type-chip[data-type='normal'] { + background: var(--type-normal); +} +.type-chip[data-type='fire'] { + background: var(--type-fire); +} +.type-chip[data-type='water'] { + background: var(--type-water); +} +.type-chip[data-type='electric'] { + background: var(--type-electric); + color: #3a3a1a; +} +.type-chip[data-type='grass'] { + background: var(--type-grass); +} +.type-chip[data-type='ice'] { + background: var(--type-ice); + color: #1c3b38; +} +.type-chip[data-type='fighting'] { + background: var(--type-fighting); +} +.type-chip[data-type='poison'] { + background: var(--type-poison); +} +.type-chip[data-type='ground'] { + background: var(--type-ground); +} +.type-chip[data-type='flying'] { + background: var(--type-flying); +} +.type-chip[data-type='psychic'] { + background: var(--type-psychic); +} +.type-chip[data-type='bug'] { + background: var(--type-bug); +} +.type-chip[data-type='rock'] { + background: var(--type-rock); + color: #2e2a1c; +} +.type-chip[data-type='ghost'] { + background: var(--type-ghost); +} +.type-chip[data-type='dragon'] { + background: var(--type-dragon); +} +.type-chip[data-type='dark'] { + background: var(--type-dark); +} +.type-chip[data-type='steel'] { + background: var(--type-steel); +} +.type-chip[data-type='fairy'] { + background: var(--type-fairy); + color: #3d1f3b; +} /* ---------- Dex feed header ------------------------------------- */ .feed-head { @@ -477,13 +519,21 @@ box-shadow: var(--shadow); content-visibility: auto; contain-intrinsic-size: auto 250px; - transition: transform 0.14s var(--ease-spring), box-shadow 0.14s ease; + transition: + transform 0.14s var(--ease-spring), + box-shadow 0.14s ease; animation: cardIn 0.4s var(--ease-spring) backwards; animation-delay: calc(var(--i, 0) * 18ms); } @keyframes cardIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: none; } + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: none; + } } .card:hover { transform: translateY(-4px); @@ -532,11 +582,11 @@ object-fit: contain; filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.28)); } -.card[data-sprite="default"] .card__art .sprite, -.card[data-sprite="game"] .card__art .sprite { +.card[data-sprite='default'] .card__art .sprite, +.card[data-sprite='game'] .card__art .sprite { image-rendering: pixelated; } -.card[data-sprite="game"] .card__art .sprite { +.card[data-sprite='game'] .card__art .sprite { width: 92px; height: 92px; } @@ -548,7 +598,9 @@ padding: 5px; border-radius: 12px; background: #f7f7f5; - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08), 0 2px 6px rgba(0, 0, 0, 0.14); + box-shadow: + inset 0 0 0 1px rgba(0, 0, 0, 0.08), + 0 2px 6px rgba(0, 0, 0, 0.14); filter: none; } .card__body { @@ -606,15 +658,15 @@ background: rgba(255, 255, 255, 0.75); backdrop-filter: blur(3px); } -.card__caught[aria-pressed="true"] { +.card__caught[aria-pressed='true'] { background: var(--good); color: #fff; } -.card__caught[aria-pressed="true"]::before { - content: "✓"; +.card__caught[aria-pressed='true']::before { + content: '✓'; } -.card__caught[aria-pressed="false"]::before { - content: "+"; +.card__caught[aria-pressed='false']::before { + content: '+'; } .card.is-caught { border-color: color-mix(in srgb, var(--good) 45%, var(--border)); @@ -677,7 +729,9 @@ cursor: pointer; font: inherit; box-shadow: var(--shadow); - transition: transform 0.14s var(--ease-spring), border-color 0.14s ease; + transition: + transform 0.14s var(--ease-spring), + border-color 0.14s ease; } .gamecard:hover { transform: translateY(-3px); @@ -710,7 +764,7 @@ box-shadow: var(--shadow); } .gamecard--cover::after { - content: ""; + content: ''; position: absolute; inset: 0; background: linear-gradient(to top, rgba(0, 0, 0, 0.28), transparent 55%); @@ -724,7 +778,9 @@ } .gamecard--cover.is-active { background: linear-gradient(105deg, var(--g1) 0%, var(--g1) 42%, var(--g2) 58%, var(--g2) 100%); - box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent); + box-shadow: + 0 0 0 2px var(--surface), + 0 0 0 4px var(--accent); } .gamecard--cover .gamecard__name { font-size: 1rem; @@ -774,10 +830,16 @@ inset: 0; z-index: -2; background: - radial-gradient(120% 80% at 80% 0%, color-mix(in srgb, var(--type-2) 55%, transparent), transparent 60%), - linear-gradient(160deg, + radial-gradient( + 120% 80% at 80% 0%, + color-mix(in srgb, var(--type-2) 55%, transparent), + transparent 60% + ), + linear-gradient( + 160deg, color-mix(in srgb, var(--type-main) 92%, #fff 8%) 0%, - color-mix(in srgb, var(--type-main) 62%, #000 38%) 100%); + color-mix(in srgb, var(--type-main) 62%, #000 38%) 100% + ); } .phero__ball { position: absolute; @@ -791,7 +853,7 @@ border-radius: 50%; } .phero__ball::before { - content: ""; + content: ''; position: absolute; left: -20px; right: -20px; @@ -801,7 +863,7 @@ background: #fff; } .phero__ball::after { - content: ""; + content: ''; position: absolute; left: 50%; top: 50%; @@ -912,15 +974,23 @@ padding: 14px; border-radius: 18px; background: #f7f7f5; - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.22), inset 0 0 0 1px rgba(0, 0, 0, 0.08); + box-shadow: + 0 10px 24px rgba(0, 0, 0, 0.22), + inset 0 0 0 1px rgba(0, 0, 0, 0.08); } .phero__art--boxed img { image-rendering: pixelated; filter: none; } @keyframes pop { - from { transform: scale(0.8); opacity: 0; } - to { transform: scale(1); opacity: 1; } + from { + transform: scale(0.8); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } } .phero__nav { width: 44px; @@ -1036,9 +1106,15 @@ animation: pnote-fade 1.6s ease forwards; } @keyframes pnote-fade { - 0% { opacity: 1; } - 70% { opacity: 1; } - 100% { opacity: 0; } + 0% { + opacity: 1; + } + 70% { + opacity: 1; + } + 100% { + opacity: 0; + } } .pnote__input { width: 100%; @@ -1092,8 +1168,14 @@ animation: fade 0.25s ease; } @keyframes fade { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: none; } + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: none; + } } .ppanel__flavor { font-size: 1rem; @@ -1131,8 +1213,14 @@ animation: flavorSlide 0.28s var(--ease-spring); } @keyframes flavorSlide { - from { opacity: 0; transform: translateX(14px); } - to { opacity: 1; transform: none; } + from { + opacity: 0; + transform: translateX(14px); + } + to { + opacity: 1; + transform: none; + } } .flavor__cap { display: block; @@ -1221,12 +1309,24 @@ border-radius: 999px; transition: width 0.7s var(--ease-spring); } -.statbar__fill[data-stat="hp"] { background: #ff6b6b; } -.statbar__fill[data-stat="attack"] { background: #ffa94d; } -.statbar__fill[data-stat="defense"] { background: #ffd43b; } -.statbar__fill[data-stat="special-attack"] { background: #74c0fc; } -.statbar__fill[data-stat="special-defense"] { background: #63e6be; } -.statbar__fill[data-stat="speed"] { background: #da77f2; } +.statbar__fill[data-stat='hp'] { + background: #ff6b6b; +} +.statbar__fill[data-stat='attack'] { + background: #ffa94d; +} +.statbar__fill[data-stat='defense'] { + background: #ffd43b; +} +.statbar__fill[data-stat='special-attack'] { + background: #74c0fc; +} +.statbar__fill[data-stat='special-defense'] { + background: #63e6be; +} +.statbar__fill[data-stat='speed'] { + background: #da77f2; +} .statbar--total { font-weight: 700; border-top: 1px solid var(--border); @@ -1378,7 +1478,7 @@ color: var(--text-dim); transition: transform 0.15s ease; } -.moverow[aria-expanded="true"] .moverow__chev { +.moverow[aria-expanded='true'] .moverow__chev { transform: rotate(180deg); } .movebody { @@ -1427,9 +1527,15 @@ letter-spacing: 0.03em; text-transform: uppercase; } -.moverow__cat[data-cat="physical"] { color: #e0762f; } -.moverow__cat[data-cat="special"] { color: #4b8fd6; } -.moverow__cat[data-cat="status"] { color: #8a8f98; } +.moverow__cat[data-cat='physical'] { + color: #e0762f; +} +.moverow__cat[data-cat='special'] { + color: #4b8fd6; +} +.moverow__cat[data-cat='status'] { + color: #8a8f98; +} .moverow__stat { font-variant-numeric: tabular-nums; white-space: nowrap; @@ -1530,7 +1636,9 @@ box-shadow: var(--shadow); text-decoration: none; color: inherit; - transition: transform 0.12s var(--ease-spring), border-color 0.12s ease; + transition: + transform 0.12s var(--ease-spring), + border-color 0.12s ease; } .search__row:hover { transform: translateX(3px); @@ -1704,7 +1812,9 @@ } .swatch.is-on { border-color: var(--text); - box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--sw); + box-shadow: + 0 0 0 2px var(--bg), + 0 0 0 4px var(--sw); } /* ---------- Toast ------------------------------------------- */ @@ -1802,7 +1912,7 @@ cursor: grabbing; } .gsheet__head::before { - content: ""; + content: ''; position: absolute; top: 6px; left: 50%; @@ -1868,8 +1978,14 @@ animation: viewIn 0.26s ease both; } @keyframes viewIn { - from { opacity: 0; transform: translateY(6px); } - to { opacity: 1; transform: none; } + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: none; + } } ::view-transition-old(root), @@ -1894,8 +2010,12 @@ animation: shimmer 1.25s linear infinite; } @keyframes shimmer { - from { background-position: 220% 0; } - to { background-position: -120% 0; } + from { + background-position: 220% 0; + } + to { + background-position: -120% 0; + } } .sk-hero { background: var(--surface-2); @@ -1963,7 +2083,7 @@ animation: viewIn 0.25s ease both; } .netpill::before { - content: ""; + content: ''; width: 7px; height: 7px; border-radius: 50%; @@ -2103,10 +2223,10 @@ transition: transform 0.15s ease; margin-left: auto; } -.feed-tools__toggle[aria-expanded="true"] .feed-tools__chev { +.feed-tools__toggle[aria-expanded='true'] .feed-tools__chev { transform: rotate(180deg); } -.feed-tools__toggle[data-active]:not([data-active=""])::before { +.feed-tools__toggle[data-active]:not([data-active=''])::before { content: attr(data-active); min-width: 17px; height: 17px; @@ -2314,11 +2434,24 @@ border-radius: 6px; font-variant-numeric: tabular-nums; } -.cov__cell.m4 { background: color-mix(in srgb, var(--danger) 60%, transparent); color: #fff; } -.cov__cell.m2 { background: color-mix(in srgb, var(--danger) 28%, transparent); } -.cov__cell.m05 { background: color-mix(in srgb, var(--good) 24%, transparent); } -.cov__cell.m025 { background: color-mix(in srgb, var(--good) 48%, transparent); color: #fff; } -.cov__cell.m0 { background: var(--surface-2); color: var(--text-dim); } +.cov__cell.m4 { + background: color-mix(in srgb, var(--danger) 60%, transparent); + color: #fff; +} +.cov__cell.m2 { + background: color-mix(in srgb, var(--danger) 28%, transparent); +} +.cov__cell.m05 { + background: color-mix(in srgb, var(--good) 24%, transparent); +} +.cov__cell.m025 { + background: color-mix(in srgb, var(--good) 48%, transparent); + color: #fff; +} +.cov__cell.m0 { + background: var(--surface-2); + color: var(--text-dim); +} .cov__row--threat { background: color-mix(in srgb, var(--danger) 12%, transparent); box-shadow: inset 3px 0 0 var(--danger); @@ -2965,7 +3098,9 @@ image-rendering: pixelated; filter: grayscale(1); opacity: 0.6; - transition: filter 0.15s, opacity 0.15s; + transition: + filter 0.15s, + opacity 0.15s; } .formdex__tile.is-caught .formdex__sprite { filter: none; @@ -2996,7 +3131,9 @@ text-align: center; opacity: 0; transform: scale(0.6); - transition: opacity 0.15s, transform 0.15s; + transition: + opacity 0.15s, + transform 0.15s; } .formdex__tile.is-caught .formdex__check { opacity: 1; @@ -3231,8 +3368,12 @@ border-radius: 999px; vertical-align: middle; } -.prog__key--caught { background: var(--good); } -.prog__key--seen { background: color-mix(in srgb, var(--accent) 35%, transparent); } +.prog__key--caught { + background: var(--good); +} +.prog__key--seen { + background: color-mix(in srgb, var(--accent) 35%, transparent); +} /* ---- Breeding helper --------------------------------------- */ .breedview__body { @@ -3403,15 +3544,44 @@ /* ================= Micro-interactions ============================ */ /* Tactile press feedback — low specificity so any element's own :active / transition wins if it has one. */ -:where(.button, .seg__btn, .feed-chip, .breed__chip, .formbar__pill, - .flavor__pill, .lineup__add, .lineup__clear, .shiny__step, .prog__row, - .tchart__row, .tchart__col, .gamecard, .picker__row, .weakspot__chip) { - transition: transform 0.09s var(--ease-spring), background-color 0.16s ease, - color 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease; +:where( + .button, + .seg__btn, + .feed-chip, + .breed__chip, + .formbar__pill, + .flavor__pill, + .lineup__add, + .lineup__clear, + .shiny__step, + .prog__row, + .tchart__row, + .tchart__col, + .gamecard, + .picker__row, + .weakspot__chip +) { + transition: + transform 0.09s var(--ease-spring), + background-color 0.16s ease, + color 0.16s ease, + box-shadow 0.16s ease, + border-color 0.16s ease; } -:where(.button, .seg__btn, .feed-chip, .breed__chip, .formbar__pill, - .flavor__pill, .lineup__add, .shiny__step, .prog__row, .tchart__row, - .tchart__col, .gamecard):active { +:where( + .button, + .seg__btn, + .feed-chip, + .breed__chip, + .formbar__pill, + .flavor__pill, + .lineup__add, + .shiny__step, + .prog__row, + .tchart__row, + .tchart__col, + .gamecard +):active { transform: scale(0.96); } .card:active { @@ -3429,8 +3599,13 @@ animation: heroFloat 4.2s ease-in-out infinite; } @keyframes heroFloat { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-6px); } + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-6px); + } } /* Sliding thumb behind the active segment (JS sets --seg-i / --seg-n). */ @@ -3446,7 +3621,9 @@ border-radius: 999px; background: var(--surface); box-shadow: var(--shadow); - transition: transform 0.28s var(--ease-spring), width 0.28s var(--ease-spring); + transition: + transform 0.28s var(--ease-spring), + width 0.28s var(--ease-spring); pointer-events: none; } .seg__btn { @@ -3482,7 +3659,9 @@ padding: 20px; transform: translateY(12px) scale(0.97); opacity: 0; - transition: transform 0.2s var(--ease-spring), opacity 0.2s ease; + transition: + transform 0.2s var(--ease-spring), + opacity 0.2s ease; } .dialog-backdrop.is-open .dialog { transform: none; diff --git a/src/styles/tokens.css b/src/styles/tokens.css index 4acd833..85ebc8b 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -23,7 +23,7 @@ --shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.08); --shadow-lg: 0 20px 50px -12px rgba(0, 0, 0, 0.28); - --font: "Poppins", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --font: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1); /* Pokémon type colors */ @@ -62,7 +62,7 @@ } } -:root[data-theme="dark"] { +:root[data-theme='dark'] { --bg: #0b0b0c; --surface: #17171a; --surface-2: #202024; @@ -75,7 +75,7 @@ } /* Pure black — for OLED screens. */ -:root[data-theme="black"] { +:root[data-theme='black'] { --bg: #000000; --surface: #0b0b0c; --surface-2: #17171a; @@ -89,7 +89,7 @@ } /* Sepia — a warm, low-glare light theme. */ -:root[data-theme="sepia"] { +:root[data-theme='sepia'] { --bg: #f2e8d5; --surface: #fbf4e4; --surface-2: #e9dbbe; @@ -101,11 +101,26 @@ } /* Accent overrides — layered on top of any theme. Red is the default. */ -:root[data-accent="blue"] { --accent: #2f6fed; --accent-text: #ffffff; } -:root[data-accent="green"] { --accent: #1f9d57; --accent-text: #ffffff; } -:root[data-accent="amber"] { --accent: #d98a00; --accent-text: #1a1200; } -:root[data-accent="violet"] { --accent: #7b52e0; --accent-text: #ffffff; } -:root[data-accent="rose"] { --accent: #e0417a; --accent-text: #ffffff; } +:root[data-accent='blue'] { + --accent: #2f6fed; + --accent-text: #ffffff; +} +:root[data-accent='green'] { + --accent: #1f9d57; + --accent-text: #ffffff; +} +:root[data-accent='amber'] { + --accent: #d98a00; + --accent-text: #1a1200; +} +:root[data-accent='violet'] { + --accent: #7b52e0; + --accent-text: #ffffff; +} +:root[data-accent='rose'] { + --accent: #e0417a; + --accent-text: #ffffff; +} * { box-sizing: border-box; diff --git a/src/views/AbilityDetail.js b/src/views/AbilityDetail.js index d6b9995..9a37f61 100644 --- a/src/views/AbilityDetail.js +++ b/src/views/AbilityDetail.js @@ -101,9 +101,7 @@ export async function AbilityDetail(id) { ) : null, mons.length > 24 ? filterInput : null, - mons.length - ? grid - : el('p', { class: 'detail__muted' }, 'None on record.'), + mons.length ? grid : el('p', { class: 'detail__muted' }, 'None on record.'), ), ); return view; diff --git a/src/views/BreedingView.js b/src/views/BreedingView.js index 14ded8e..09f1c96 100644 --- a/src/views/BreedingView.js +++ b/src/views/BreedingView.js @@ -9,9 +9,15 @@ import { openPokemonPicker } from '../components/PokemonPicker.js'; import { segThumb } from '../lib/anim.js'; const EGG_LABEL = { - water1: 'Water 1', water2: 'Water 2', water3: 'Water 3', - ground: 'Field', humanshape: 'Human-Like', indeterminate: 'Amorphous', - plant: 'Grass', 'no-eggs': 'Undiscovered', ditto: 'Ditto', + water1: 'Water 1', + water2: 'Water 2', + water3: 'Water 3', + ground: 'Field', + humanshape: 'Human-Like', + indeterminate: 'Amorphous', + plant: 'Grass', + 'no-eggs': 'Undiscovered', + ditto: 'Ditto', }; const eggLabel = (g) => EGG_LABEL[g] || prettify(g); const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop()); @@ -57,7 +63,14 @@ export async function BreedingView() { ), ), ); - const setThumb = segThumb(seg, SEG.length, Math.max(0, SEG.findIndex(([id]) => id === mode))); + const setThumb = segThumb( + seg, + SEG.length, + Math.max( + 0, + SEG.findIndex(([id]) => id === mode), + ), + ); const body = el('div', { class: 'breedview__body' }); @@ -125,7 +138,12 @@ export async function BreedingView() { ), }, target - ? el('span', {}, Sprite(target.id, { style, size: 40, alt: target.name }), ` ${prettify(target.name)}`) + ? el( + 'span', + {}, + Sprite(target.id, { style, size: 40, alt: target.name }), + ` ${prettify(target.name)}`, + ) : 'Choose a Pokémon…', ); body.append(slot); @@ -133,7 +151,13 @@ export async function BreedingView() { const tGroups = new Set(target.eggGroups || []); if (tGroups.has('no-eggs')) { - body.append(el('p', { class: 'detail__muted' }, `${prettify(target.name)} is in the Undiscovered egg group — it can’t be bred.`)); + body.append( + el( + 'p', + { class: 'detail__muted' }, + `${prettify(target.name)} is in the Undiscovered egg group — it can’t be bred.`, + ), + ); return; } const isDitto = tGroups.has('ditto'); @@ -141,7 +165,9 @@ export async function BreedingView() { let list; if (isDitto) { - list = snap.species.filter((s) => !s.eggGroups.includes('no-eggs') && !s.eggGroups.includes('ditto')); + list = snap.species.filter( + (s) => !s.eggGroups.includes('no-eggs') && !s.eggGroups.includes('ditto'), + ); } else if (genderless) { list = snap.species.filter((s) => (s.eggGroups || []).includes('ditto')); } else { @@ -168,12 +194,17 @@ export async function BreedingView() { if (!prev) break; base = prev; } - const eggMovesBox = el('div', { class: 'breed__eggmoves' }, el('p', { class: 'detail__muted' }, 'Loading egg moves…')); + const eggMovesBox = el( + 'div', + { class: 'breed__eggmoves' }, + el('p', { class: 'detail__muted' }, 'Loading egg moves…'), + ); getPokemon(base.id) .then((pk) => { const eggMoves = pk.moves.filter((m) => m.version_group_details.some( - (d) => d.move_learn_method.name === 'egg' && (vg === 'all' || d.version_group.name === vg), + (d) => + d.move_learn_method.name === 'egg' && (vg === 'all' || d.version_group.name === vg), ), ); clear(eggMovesBox); @@ -196,12 +227,20 @@ export async function BreedingView() { 'div', { class: 'breed__eggmove-list' }, ...eggMoves.map((m) => - el('a', { class: 'link', href: `#/move/${idFromUrl(m.move.url)}` }, m.move.name.replace(/-/g, ' ')), + el( + 'a', + { class: 'link', href: `#/move/${idFromUrl(m.move.url)}` }, + m.move.name.replace(/-/g, ' '), + ), ), ), ); }) - .catch(() => clear(eggMovesBox).append(el('p', { class: 'detail__muted' }, 'Egg moves unavailable offline.'))); + .catch(() => + clear(eggMovesBox).append( + el('p', { class: 'detail__muted' }, 'Egg moves unavailable offline.'), + ), + ); body.append( el('p', { class: 'breed__note' }, note), diff --git a/src/views/CompareView.js b/src/views/CompareView.js index be77d87..0afff2f 100644 --- a/src/views/CompareView.js +++ b/src/views/CompareView.js @@ -95,7 +95,11 @@ export async function CompareView() { body.append( mem.length >= 2 ? CompareTable(mem, { gen, style }) - : el('p', { class: 'detail__muted' }, 'Add two or more Pokémon to compare their types and base stats.'), + : el( + 'p', + { class: 'detail__muted' }, + 'Add two or more Pokémon to compare their types and base stats.', + ), ); } @@ -105,11 +109,7 @@ export async function CompareView() { 'header', { class: 'view__header' }, el('h1', {}, 'Compare'), - el( - 'p', - {}, - vg ? `Types shown for ${vg.name}.` : 'Types shown for the newest games.', - ), + el('p', {}, vg ? `Types shown for ${vg.name}.` : 'Types shown for the newest games.'), ), slots, body, diff --git a/src/views/DexGrid.js b/src/views/DexGrid.js index 6ab992d..8cdea27 100644 --- a/src/views/DexGrid.js +++ b/src/views/DexGrid.js @@ -10,8 +10,24 @@ import { Sprite } from '../components/Sprite.js'; import { typesForGen } from '../lib/type-resolve.js'; const TYPES = [ - 'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison', - 'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy', + 'normal', + 'fire', + 'water', + 'electric', + 'grass', + 'ice', + 'fighting', + 'poison', + 'ground', + 'flying', + 'psychic', + 'bug', + 'rock', + 'ghost', + 'dragon', + 'dark', + 'steel', + 'fairy', ]; const FILTERS = [ @@ -47,30 +63,55 @@ const SEARCH_ICON = function sortValue(row, key) { const sp = row.species; switch (key) { - case 'dex': return row.number ?? sp.id; - case 'name': return sp.name; - case 'bst': return sp.bst ?? 0; - case 'hp': case 'atk': case 'def': case 'spa': case 'spd': case 'spe': + case 'dex': + return row.number ?? sp.id; + case 'name': + return sp.name; + case 'bst': + return sp.bst ?? 0; + case 'hp': + case 'atk': + case 'def': + case 'spa': + case 'spd': + case 'spe': return sp.stats?.[key] ?? 0; - case 'height': return sp.height ?? 0; - case 'weight': return sp.weight ?? 0; - case 'baseExp': return sp.baseExp ?? 0; - case 'catchrate': return sp.captureRate ?? 0; - case 'caught': return gameEntry(sp.id).updatedAt || ''; - default: return 0; + case 'height': + return sp.height ?? 0; + case 'weight': + return sp.weight ?? 0; + case 'baseExp': + return sp.baseExp ?? 0; + case 'catchrate': + return sp.captureRate ?? 0; + case 'caught': + return gameEntry(sp.id).updatedAt || ''; + default: + return 0; } } function metricLabel(key, sp) { switch (key) { - case 'bst': return `BST ${sp.bst}`; - case 'hp': case 'atk': case 'def': case 'spa': case 'spd': case 'spe': + case 'bst': + return `BST ${sp.bst}`; + case 'hp': + case 'atk': + case 'def': + case 'spa': + case 'spd': + case 'spe': return `${STAT_SHORT[key]} ${sp.stats?.[key] ?? 0}`; - case 'height': return `${((sp.height ?? 0) / 10).toFixed(1)} m`; - case 'weight': return `${((sp.weight ?? 0) / 10).toFixed(1)} kg`; - case 'baseExp': return sp.baseExp != null ? `${sp.baseExp} EXP` : null; - case 'catchrate': return `Catch ${sp.captureRate ?? '?'}`; - default: return null; + case 'height': + return `${((sp.height ?? 0) / 10).toFixed(1)} m`; + case 'weight': + return `${((sp.weight ?? 0) / 10).toFixed(1)} kg`; + case 'baseExp': + return sp.baseExp != null ? `${sp.baseExp} EXP` : null; + case 'catchrate': + return `Catch ${sp.captureRate ?? '?'}`; + default: + return null; } } @@ -254,7 +295,9 @@ export async function DexGrid() { }, }, el('option', { value: '' }, 'Any egg group'), - ...eggGroupList.map((g) => el('option', { value: g, selected: g === filterEggGroup }, prettify(g))), + ...eggGroupList.map((g) => + el('option', { value: g, selected: g === filterEggGroup }, prettify(g)), + ), ); const minBstField = el( @@ -347,7 +390,13 @@ export async function DexGrid() { 'div', { class: `feed-tools${ui.get().toolsOpen ? ' is-open' : ''}` }, filterBar, - el('div', { class: 'feed-sort' }, el('span', { class: 'feed-sort__label' }, 'Sort'), sortSelect, dirBtn), + el( + 'div', + { class: 'feed-sort' }, + el('span', { class: 'feed-sort__label' }, 'Sort'), + sortSelect, + dirBtn, + ), el('div', { class: 'feed-sort feed-filters' }, typeSelect, genSelect, randomBtn), el('div', { class: 'feed-sort feed-filters' }, abilitySelect, eggGroupSelect, minBstField), el('div', { class: 'feed-sort feed-filters' }, fullyEvolvedCheck), @@ -431,7 +480,9 @@ export async function DexGrid() { const gen = snap.versionGroupByKey.get(st.versionGroup)?.generation ?? 9; const boxed = st.spriteStyle === 'game' && gen <= 2; const filterTest = FILTERS.find((f) => f.key === filterKey).test; - const abilityMon = filterAbility ? new Set(snap.abilityById.get(filterAbility)?.pokemon || []) : null; + const abilityMon = filterAbility + ? new Set(snap.abilityById.get(filterAbility)?.pokemon || []) + : null; let list = rows.filter(({ species, number }) => { if ( @@ -490,8 +541,7 @@ export async function DexGrid() { ids = rows.map((r) => r.species.id); title.textContent = prettify(dex.name || dex.key); - gameLabel.textContent = - st.versionGroup === 'all' ? 'All games' : prettify(st.versionGroup); + gameLabel.textContent = st.versionGroup === 'all' ? 'All games' : prettify(st.versionGroup); refreshMeta(); paintGrid(); } diff --git a/src/views/ItemDetail.js b/src/views/ItemDetail.js index adf7242..5a41704 100644 --- a/src/views/ItemDetail.js +++ b/src/views/ItemDetail.js @@ -86,7 +86,11 @@ export async function ItemDetail(id) { } if (shown.length > HOLDER_CAP) { holdersGrid.append( - el('p', { class: 'detail__muted' }, `+${shown.length - HOLDER_CAP} more — filter to narrow.`), + el( + 'p', + { class: 'detail__muted' }, + `+${shown.length - HOLDER_CAP} more — filter to narrow.`, + ), ); } } diff --git a/src/views/MoveDetail.js b/src/views/MoveDetail.js index 2b22c84..f377e84 100644 --- a/src/views/MoveDetail.js +++ b/src/views/MoveDetail.js @@ -60,7 +60,8 @@ export async function MoveDetail(id) { `May ${prettify(d.meta.ailment.name)}${d.meta.ailment_chance ? ` (${d.meta.ailment_chance}%)` : ''}`, ); } - if (d.meta?.drain) bits.push(`${d.meta.drain > 0 ? 'Drains' : 'Recoil'} ${Math.abs(d.meta.drain)}%`); + if (d.meta?.drain) + bits.push(`${d.meta.drain > 0 ? 'Drains' : 'Recoil'} ${Math.abs(d.meta.drain)}%`); if (d.meta?.healing) bits.push(`Heals ${d.meta.healing}%`); if (d.meta?.crit_rate) bits.push(`+${d.meta.crit_rate} crit rate`); if (d.meta?.flinch_chance) bits.push(`Flinch ${d.meta.flinch_chance}%`); @@ -128,7 +129,11 @@ export async function MoveDetail(id) { } if (shown.length > LEARNER_CAP) { learnersGrid.append( - el('p', { class: 'detail__muted' }, `+${shown.length - LEARNER_CAP} more — filter to narrow.`), + el( + 'p', + { class: 'detail__muted' }, + `+${shown.length - LEARNER_CAP} more — filter to narrow.`, + ), ); } } diff --git a/src/views/NaturesView.js b/src/views/NaturesView.js index bf95b98..b4f39e5 100644 --- a/src/views/NaturesView.js +++ b/src/views/NaturesView.js @@ -9,7 +9,11 @@ export function NaturesView() { 'header', { class: 'view__header' }, el('h1', {}, 'Natures'), - el('p', {}, 'Each nature raises one stat by 10% and lowers another by 10%. HP is never affected; five natures are neutral.'), + el( + 'p', + {}, + 'Each nature raises one stat by 10% and lowers another by 10%. HP is never affected; five natures are neutral.', + ), ), el( 'div', diff --git a/src/views/PokemonDetail.js b/src/views/PokemonDetail.js index 05794f4..fb889ad 100644 --- a/src/views/PokemonDetail.js +++ b/src/views/PokemonDetail.js @@ -67,17 +67,13 @@ function typesForGeneration(pokemon, gen) { .sort((a, b) => a._gen - b._gen); const era = past.find((p) => p._gen >= gen); const list = era ? era.types : pokemon.types; - return list.slice().sort((a, b) => a.slot - b.slot).map((t) => t.type.name); + return list + .slice() + .sort((a, b) => a.slot - b.slot) + .map((t) => t.type.name); } -const STAT_NAMES = [ - 'hp', - 'attack', - 'defense', - 'special-attack', - 'special-defense', - 'speed', -]; +const STAT_NAMES = ['hp', 'attack', 'defense', 'special-attack', 'special-defense', 'speed']; const NO_ENCOUNTER_DATA = new Set([ 'scarlet-violet', @@ -106,10 +102,7 @@ export async function PokemonDetail(nationalId) { let pokemon; let species; try { - [pokemon, species] = await Promise.all([ - getPokemon(nationalId), - getSpecies(nationalId), - ]); + [pokemon, species] = await Promise.all([getPokemon(nationalId), getSpecies(nationalId)]); } catch (err) { clear(view).append( el( @@ -272,7 +265,13 @@ export async function PokemonDetail(nationalId) { const on = inTeam(nationalId, teamForm()); const full = team.get().members.length >= MAX_TEAM; const label = activeForm ? `${activeForm.name} in team` : 'In team'; - teamBtn.textContent = on ? `✓ ${label}` : full ? 'Team full' : activeForm ? `+ ${activeForm.name}` : '+ Team'; + teamBtn.textContent = on + ? `✓ ${label}` + : full + ? 'Team full' + : activeForm + ? `+ ${activeForm.name}` + : '+ Team'; teamBtn.classList.toggle('is-on', on); teamBtn.disabled = !on && full; } @@ -361,8 +360,10 @@ export async function PokemonDetail(nationalId) { }), ); const evYield = - p.stats.filter((s) => s.effort > 0).map((s) => `${s.effort} ${STAT_LABEL[s.stat.name] || s.stat.name}`).join(', ') || - '—'; + p.stats + .filter((s) => s.effort > 0) + .map((s) => `${s.effort} ${STAT_LABEL[s.stat.name] || s.stat.name}`) + .join(', ') || '—'; const heldItems = p.held_items?.length ? el( 'span', @@ -388,7 +389,10 @@ export async function PokemonDetail(nationalId) { abilityList.length && vgGen >= 3 ? fact('Abilities', abilitiesNode) : null, fact('Introduced', prettify(species.generation.name)), species.genera?.length - ? fact('Category', (species.genera.find((g) => g.language.name === 'en') || {}).genus || '—') + ? fact( + 'Category', + (species.genera.find((g) => g.language.name === 'en') || {}).genus || '—', + ) : null, ), el('h3', { class: 'ppanel__sub' }, 'Training'), @@ -466,7 +470,9 @@ export async function PokemonDetail(nationalId) { el('div', { class: 'fact' }, el('dt', {}, 'Gender'), genderCell(species.gender_rate)), fact( 'Egg groups', - eggGroups.includes('Undiscovered') ? "Undiscovered (can't breed)" : eggGroups.join(', ') || '—', + eggGroups.includes('Undiscovered') + ? "Undiscovered (can't breed)" + : eggGroups.join(', ') || '—', ), fact( 'Egg cycles', @@ -492,13 +498,19 @@ export async function PokemonDetail(nationalId) { ); } else if (NO_ENCOUNTER_DATA.has(st.versionGroup)) { locPanel.replaceChildren( - el('p', { class: 'detail__muted' }, "PokéAPI doesn't have wild-encounter data for this game yet."), + el( + 'p', + { class: 'detail__muted' }, + "PokéAPI doesn't have wild-encounter data for this game yet.", + ), ); } else { getEncounters(nationalId) .then((data) => locPanel.replaceChildren(Locations(data, vg ? vg.versions : []))) .catch(() => - locPanel.replaceChildren(el('p', { class: 'detail__muted' }, 'Location data unavailable offline.')), + locPanel.replaceChildren( + el('p', { class: 'detail__muted' }, 'Location data unavailable offline.'), + ), ); } @@ -516,7 +528,12 @@ export async function PokemonDetail(nationalId) { // generic "Default". const DEFAULT_SLOT_NAME = { unown: 'A' }; const slots = [ - { slug: snapSpecies.name, id: nationalId, name: DEFAULT_SLOT_NAME[snapSpecies.name] || 'Default', sprite: null }, + { + slug: snapSpecies.name, + id: nationalId, + name: DEFAULT_SLOT_NAME[snapSpecies.name] || 'Default', + sprite: null, + }, ...cosmeticForms, ]; const countEl = el('span', { class: 'formdex__count' }); @@ -582,7 +599,9 @@ export async function PokemonDetail(nationalId) { // ---- Tabs ------------------------------------------- const TABS = [ { id: 'about', label: 'About', node: aboutPanel }, - formDexPanel ? { id: 'formdex', label: `${prettify(snapSpecies.name)} Dex`, node: formDexPanel } : null, + formDexPanel + ? { id: 'formdex', label: `${prettify(snapSpecies.name)} Dex`, node: formDexPanel } + : null, { id: 'stats', label: 'Stats', node: statsPanel }, { id: 'evo', label: 'Evolution', node: evoPanel }, { id: 'moves', label: 'Moves', node: movesPanel }, @@ -745,7 +764,11 @@ export async function PokemonDetail(nationalId) { { class: 'phero' }, el('div', { class: 'phero__bg' }), el('div', { class: 'phero__ball', 'aria-hidden': 'true' }), - el('span', { class: 'phero__bignum', 'aria-hidden': 'true' }, `#${String(nationalId).padStart(4, '0')}`), + el( + 'span', + { class: 'phero__bignum', 'aria-hidden': 'true' }, + `#${String(nationalId).padStart(4, '0')}`, + ), el( 'div', { class: 'phero__top' }, @@ -753,23 +776,34 @@ export async function PokemonDetail(nationalId) { el('span', { class: 'phero__num' }, numLabel), el('div', { class: 'phero__actions' }, cryBtn, shinyBtn, favBtn), ), - el( - 'div', - { class: 'phero__head' }, - heroName, - heroTypes, - ), + el('div', { class: 'phero__head' }, heroName, heroTypes), formBar, cosmeticBar, el( 'div', { class: 'phero__stage' }, prev - ? el('a', { class: 'phero__nav phero__nav--prev', href: `#/pokemon/${prev.species.id}`, title: prev.species.name }, '‹') + ? el( + 'a', + { + class: 'phero__nav phero__nav--prev', + href: `#/pokemon/${prev.species.id}`, + title: prev.species.name, + }, + '‹', + ) : el('span', { class: 'phero__nav' }), artHolder, next - ? el('a', { class: 'phero__nav phero__nav--next', href: `#/pokemon/${next.species.id}`, title: next.species.name }, '›') + ? el( + 'a', + { + class: 'phero__nav phero__nav--next', + href: `#/pokemon/${next.species.id}`, + title: next.species.name, + }, + '›', + ) : el('span', { class: 'phero__nav' }), ), ), diff --git a/src/views/ProgressView.js b/src/views/ProgressView.js index e7ed5ed..1dc23ca 100644 --- a/src/views/ProgressView.js +++ b/src/views/ProgressView.js @@ -53,7 +53,9 @@ export async function ProgressView() { clear(list); const gens = versionGroupsByGeneration(snap); - const anyPlayed = gens.some(({ versionGroups }) => versionGroups.some((vg) => isPlayed(vg.key))); + const anyPlayed = gens.some(({ versionGroups }) => + versionGroups.some((vg) => isPlayed(vg.key)), + ); const showAll = ui.get().progShowAll || !anyPlayed; // National total — caught in any game. @@ -115,7 +117,11 @@ export async function ProgressView() { render(); clear(view).append( - el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/settings' }, '‹ Settings')), + el( + 'nav', + { class: 'lookup__nav' }, + el('a', { class: 'link', href: '#/settings' }, '‹ Settings'), + ), el( 'header', { class: 'view__header' }, diff --git a/src/views/SearchView.js b/src/views/SearchView.js index ea3dd08..f460122 100644 --- a/src/views/SearchView.js +++ b/src/views/SearchView.js @@ -9,11 +9,31 @@ import { TypeChip } from '../components/TypeChip.js'; import { segThumb } from '../lib/anim.js'; const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop()); -const loose = (s) => s.toLowerCase().replace(/[-\s]+/g, ' ').trim(); +const loose = (s) => + s + .toLowerCase() + .replace(/[-\s]+/g, ' ') + .trim(); const DMG = { physical: 'Phys', special: 'Spec', status: 'Stat' }; const TYPES = [ - 'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison', - 'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy', + 'normal', + 'fire', + 'water', + 'electric', + 'grass', + 'ice', + 'fighting', + 'poison', + 'ground', + 'flying', + 'psychic', + 'bug', + 'rock', + 'ghost', + 'dragon', + 'dark', + 'steel', + 'fairy', ]; const itemSprite = (name) => `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/${name}.png`; @@ -30,7 +50,7 @@ const tmSortKey = (label) => { if (!label) return 1e9; const m = String(label).match(/^([A-Z]+)0*(\d+)$/); if (!m) return 1e9 - 1; - return (({ TM: 0, HM: 1, TR: 2 })[m[1]] ?? 3) * 1000 + Number(m[2]); + return ({ TM: 0, HM: 1, TR: 2 }[m[1]] ?? 3) * 1000 + Number(m[2]); }; export async function SearchView() { @@ -75,7 +95,14 @@ export async function SearchView() { ), ), ); - const setThumb = segThumb(seg, TABS.length, Math.max(0, TABS.findIndex((t) => t.id === tab))); + const setThumb = segThumb( + seg, + TABS.length, + Math.max( + 0, + TABS.findIndex((t) => t.id === tab), + ), + ); const filters = el('div', { class: 'lookup__filters' }); const note = el('p', { class: 'search__note' }); @@ -119,9 +146,7 @@ export async function SearchView() { const vg = settings.get().versionGroup; const gameHasKind = (kind) => snap.moves.some((m) => - m.machines.some( - (x) => (vg === 'all' || x.vg === vg) && (x.tm || '').startsWith(kind), - ), + m.machines.some((x) => (vg === 'all' || x.vg === vg) && (x.tm || '').startsWith(kind)), ); const hasTm = gameHasKind('TM'); const hasHm = gameHasKind('HM'); // false for Gen 8+ games, which dropped HMs @@ -137,13 +162,23 @@ export async function SearchView() { filters.append( ...[ - pill('mvType', [['', 'Any type'], ...TYPES.map((t) => [t, prettify(t)])], ui.get().mvType, (v) => { - ui.set({ mvType: v }); - run(); - }), + pill( + 'mvType', + [['', 'Any type'], ...TYPES.map((t) => [t, prettify(t)])], + ui.get().mvType, + (v) => { + ui.set({ mvType: v }); + run(); + }, + ), pill( 'mvClass', - [['', 'Any category'], ['physical', 'Physical'], ['special', 'Special'], ['status', 'Status']], + [ + ['', 'Any category'], + ['physical', 'Physical'], + ['special', 'Special'], + ['status', 'Status'], + ], ui.get().mvClass, (v) => { ui.set({ mvClass: v }); @@ -181,16 +216,28 @@ export async function SearchView() { run(); }, ), - pill('itSort', [['name', 'A–Z'], ['id', 'Dex order']], ui.get().itSort, (v) => { - ui.set({ itSort: v }); - run(); - }), + pill( + 'itSort', + [ + ['name', 'A–Z'], + ['id', 'Dex order'], + ], + ui.get().itSort, + (v) => { + ui.set({ itSort: v }); + run(); + }, + ), ); } else if (tab === 'abilities') { filters.append( pill( 'abSort', - [['name', 'A–Z'], ['count', 'Most Pokémon'], ['gen', 'Newest']], + [ + ['name', 'A–Z'], + ['count', 'Most Pokémon'], + ['gen', 'Newest'], + ], ui.get().abSort, (v) => { ui.set({ abSort: v }); @@ -228,12 +275,11 @@ export async function SearchView() { const matches = snap.species .filter( (s) => - loose(s.name).includes(q) || - String(s.id) === q || - (s.types || []).some((t) => t === q), + loose(s.name).includes(q) || String(s.id) === q || (s.types || []).some((t) => t === q), ) .slice(0, 60); - if (!matches.length) return void results.append(el('p', { class: 'search__empty' }, 'No matches.')); + if (!matches.length) + return void results.append(el('p', { class: 'search__empty' }, 'No matches.')); for (const s of matches) { const e = entry(s.id); results.append( @@ -289,8 +335,10 @@ export async function SearchView() { }); list.sort((a, b) => { - if (sort === 'power') return (b.power ?? -1) - (a.power ?? -1) || a.name.localeCompare(b.name); - if (sort === 'accuracy') return (b.accuracy ?? -1) - (a.accuracy ?? -1) || a.name.localeCompare(b.name); + if (sort === 'power') + return (b.power ?? -1) - (a.power ?? -1) || a.name.localeCompare(b.name); + if (sort === 'accuracy') + return (b.accuracy ?? -1) - (a.accuracy ?? -1) || a.name.localeCompare(b.name); if (sort === 'gen') return b.generation - a.generation || a.name.localeCompare(b.name); if (sort === 'tm') { return ( @@ -318,14 +366,24 @@ export async function SearchView() { 'span', { class: 'search__types' }, TypeChip(m.type), - el('span', { class: 'moverow__cat', dataset: { cat: m.damageClass || '' } }, DMG[m.damageClass] || '—'), + el( + 'span', + { class: 'moverow__cat', dataset: { cat: m.damageClass || '' } }, + DMG[m.damageClass] || '—', + ), el('span', { class: 'search__num' }, m.power != null ? `${m.power} pw` : ''), ), ), ); } if (list.length > shown.length) { - results.append(el('p', { class: 'search__empty' }, `Showing ${shown.length} of ${list.length} — refine to see more.`)); + results.append( + el( + 'p', + { class: 'search__empty' }, + `Showing ${shown.length} of ${list.length} — refine to see more.`, + ), + ); } } @@ -343,7 +401,12 @@ export async function SearchView() { note.textContent = `${list.length} item${list.length === 1 ? '' : 's'}`; const shown = list.slice(0, 400); for (const it of shown) { - const icon = el('img', { class: 'search__item-icon', loading: 'lazy', alt: '', src: itemSprite(it.name) }); + const icon = el('img', { + class: 'search__item-icon', + loading: 'lazy', + alt: '', + src: itemSprite(it.name), + }); icon.addEventListener('error', () => icon.remove(), { once: true }); results.append( el( @@ -356,7 +419,13 @@ export async function SearchView() { ); } if (list.length > shown.length) { - results.append(el('p', { class: 'search__empty' }, `Showing ${shown.length} of ${list.length} — refine to see more.`)); + results.append( + el( + 'p', + { class: 'search__empty' }, + `Showing ${shown.length} of ${list.length} — refine to see more.`, + ), + ); } } @@ -367,7 +436,8 @@ export async function SearchView() { (a) => !q || loose(a.name).includes(q) || a.effect.toLowerCase().includes(q), ); list.sort((a, b) => { - if (sort === 'count') return b.pokemon.length - a.pokemon.length || a.name.localeCompare(b.name); + if (sort === 'count') + return b.pokemon.length - a.pokemon.length || a.name.localeCompare(b.name); if (sort === 'gen') return b.generation - a.generation || a.name.localeCompare(b.name); return a.name.localeCompare(b.name); }); diff --git a/src/views/SettingsView.js b/src/views/SettingsView.js index 6e85efa..a19fb7f 100644 --- a/src/views/SettingsView.js +++ b/src/views/SettingsView.js @@ -14,16 +14,21 @@ export async function SettingsView() { const view = el('section', { class: 'view settings' }); const st = settings.get(); - const themeField = selectField('Theme', st.theme, [ - ['system', 'System'], - ['light', 'Light'], - ['dark', 'Dark'], - ['black', 'Black (OLED)'], - ['sepia', 'Sepia'], - ], (v) => { - settings.set({ theme: v }); - applyTheme(); - }); + const themeField = selectField( + 'Theme', + st.theme, + [ + ['system', 'System'], + ['light', 'Light'], + ['dark', 'Dark'], + ['black', 'Black (OLED)'], + ['sepia', 'Sepia'], + ], + (v) => { + settings.set({ theme: v }); + applyTheme(); + }, + ); const ACCENTS = [ ['red', '#b3161a'], @@ -55,16 +60,23 @@ export async function SettingsView() { ); const accentField = el('div', { class: 'field' }, el('span', {}, 'Accent color'), accentRow); - const fontSizeField = selectField('Text size', st.fontScale || 'default', [ - ['small', 'Small'], - ['default', 'Default'], - ['large', 'Large'], - ], (v) => { - settings.set({ fontScale: v }); - applyTheme(); - }); + const fontSizeField = selectField( + 'Text size', + st.fontScale || 'default', + [ + ['small', 'Small'], + ['default', 'Default'], + ['large', 'Large'], + ], + (v) => { + settings.set({ fontScale: v }); + applyTheme(); + }, + ); - const reduceMotionField = el('label', { class: 'field field--check' }, + const reduceMotionField = el( + 'label', + { class: 'field field--check' }, el('input', { type: 'checkbox', checked: !!st.reduceMotion, @@ -76,14 +88,21 @@ export async function SettingsView() { 'Reduce motion', ); - const spriteField = selectField('Sprite style', st.spriteStyle, [ - ['default', 'Pixel (modern)'], - ['game', 'Game era (pixel art from the selected game)'], - ['official', 'Official artwork'], - ['home', 'Pokémon HOME'], - ], (v) => settings.set({ spriteStyle: v })); + const spriteField = selectField( + 'Sprite style', + st.spriteStyle, + [ + ['default', 'Pixel (modern)'], + ['game', 'Game era (pixel art from the selected game)'], + ['official', 'Official artwork'], + ['home', 'Pokémon HOME'], + ], + (v) => settings.set({ spriteStyle: v }), + ); - const shinyField = el('label', { class: 'field field--check' }, + const shinyField = el( + 'label', + { class: 'field field--check' }, el('input', { type: 'checkbox', checked: st.showShiny, @@ -92,7 +111,9 @@ export async function SettingsView() { 'Default detail view to shiny sprites', ); - const teamNavField = el('label', { class: 'field field--check' }, + const teamNavField = el( + 'label', + { class: 'field field--check' }, el('input', { type: 'checkbox', checked: st.showTeamNav !== false, @@ -101,7 +122,9 @@ export async function SettingsView() { 'Show Team in the navigation bar', ); - const hapticsField = el('label', { class: 'field field--check' }, + const hapticsField = el( + 'label', + { class: 'field field--check' }, el('input', { type: 'checkbox', checked: st.haptics !== false, @@ -110,11 +133,16 @@ export async function SettingsView() { 'Haptic feedback on catch', ); - const defaultRouteField = selectField('Landing screen', st.defaultRoute || 'dex', [ - ['dex', 'Dex'], - ['team', 'Team'], - ['search', 'Search'], - ], (v) => settings.set({ defaultRoute: v })); + const defaultRouteField = selectField( + 'Landing screen', + st.defaultRoute || 'dex', + [ + ['dex', 'Dex'], + ['team', 'Team'], + ['search', 'Search'], + ], + (v) => settings.set({ defaultRoute: v }), + ); const storageNote = el('p', { class: 'settings__note' }, 'Calculating storage…'); if (navigator.storage?.estimate) { @@ -157,7 +185,11 @@ export async function SettingsView() { body: `${dex.seen.length} seen · ${dex.caught.length} caught in this save.`, choices: [ { key: 'merge', label: `Add to ${prettify(target)}` }, - { key: 'replace', label: `Replace ${prettify(target)} with this`, class: 'button--danger' }, + { + key: 'replace', + label: `Replace ${prettify(target)} with this`, + class: 'button--danger', + }, { key: null, label: 'Cancel', class: 'button--ghost' }, ], }); @@ -253,50 +285,80 @@ export async function SettingsView() { el( 'div', { class: 'settings__save' }, - el('button', { class: 'button', type: 'button', onclick: () => saveInput.click() }, 'Import from a game save'), + el( + 'button', + { class: 'button', type: 'button', onclick: () => saveInput.click() }, + 'Import from a game save', + ), el('p', { class: 'settings__note' }, 'A .sav / .srm / .dsv from a Gen 1–5 game.'), ), saveInput, saveNote, storageNote, - el('div', { class: 'settings__actions' }, - el('button', { - class: 'button', - type: 'button', - onclick: () => exportData(), - }, 'Export backup'), - el('button', { - class: 'button', - type: 'button', - onclick: () => importInput.click(), - }, 'Import backup'), + el( + 'div', + { class: 'settings__actions' }, + el( + 'button', + { + class: 'button', + type: 'button', + onclick: () => exportData(), + }, + 'Export backup', + ), + el( + 'button', + { + class: 'button', + type: 'button', + onclick: () => importInput.click(), + }, + 'Import backup', + ), importInput, - el('button', { - class: 'button button--danger', - type: 'button', - onclick: () => { - if (confirm('Clear all seen/caught/favorite tracking? This cannot be undone.')) { - selection.replace({ version: 3, pokemon: {}, games: {} }); - } + el( + 'button', + { + class: 'button button--danger', + type: 'button', + onclick: () => { + if (confirm('Clear all seen/caught/favorite tracking? This cannot be undone.')) { + selection.replace({ version: 3, pokemon: {}, games: {} }); + } + }, }, - }, 'Clear tracking data'), - el('button', { - class: 'button button--danger', - type: 'button', - onclick: async () => { - if (!('caches' in window)) return; - if (confirm('Clear cached PokéAPI data and sprites? They will re-download when online.')) { - for (const key of await caches.keys()) await caches.delete(key); - alert('Caches cleared.'); - } + 'Clear tracking data', + ), + el( + 'button', + { + class: 'button button--danger', + type: 'button', + onclick: async () => { + if (!('caches' in window)) return; + if ( + confirm('Clear cached PokéAPI data and sprites? They will re-download when online.') + ) { + for (const key of await caches.keys()) await caches.delete(key); + alert('Caches cleared.'); + } + }, }, - }, 'Clear cached API data'), + 'Clear cached API data', + ), ), el('h2', {}, 'About'), - el('p', { class: 'settings__note' }, + el( + 'p', + { class: 'settings__note' }, 'Data from ', - el('a', { href: 'https://pokeapi.co/', class: 'link', target: '_blank', rel: 'noreferrer' }, 'PokéAPI'), + el( + 'a', + { href: 'https://pokeapi.co/', class: 'link', target: '_blank', rel: 'noreferrer' }, + 'PokéAPI', + ), '. Tracking data and preferences are stored locally in your browser.', ), ); @@ -311,9 +373,7 @@ function selectField(label, value, options, onChange) { const select = el( 'select', { onchange: (e) => onChange(e.target.value) }, - ...options.map(([v, text]) => - el('option', { value: v, selected: v === value }, text), - ), + ...options.map(([v, text]) => el('option', { value: v, selected: v === value }, text)), ); return el('label', { class: 'field' }, el('span', {}, label), select); } diff --git a/src/views/ShinyView.js b/src/views/ShinyView.js index f5baa1e..68cb5d8 100644 --- a/src/views/ShinyView.js +++ b/src/views/ShinyView.js @@ -71,13 +71,28 @@ export async function ShinyView() { 'span', {}, el('span', { class: 'shiny__name' }, (sp?.name || `#${h.speciesId}`).replace(/-/g, ' ')), - el('span', { class: 'shiny__method' }, methodById(h.method).label + (h.charm ? ' · charm' : '')), + el( + 'span', + { class: 'shiny__method' }, + methodById(h.method).label + (h.charm ? ' · charm' : ''), + ), ), ), el( 'div', { class: 'shiny__counter' }, - el('button', { type: 'button', class: 'shiny__step', onclick: () => { bumpHunt(h.id, -1); refresh(); } }, '−'), + el( + 'button', + { + type: 'button', + class: 'shiny__step', + onclick: () => { + bumpHunt(h.id, -1); + refresh(); + }, + }, + '−', + ), count, el( 'button', @@ -137,13 +152,25 @@ export async function ShinyView() { } function chooseMethod(done) { - const backdrop = el('div', { class: 'sheet-backdrop is-open', onclick: (e) => e.target === backdrop && backdrop.remove() }); - const methodSel = el('select', { class: 'search__input' }, ...METHODS.map((m) => el('option', { value: m.id }, m.label))); + const backdrop = el('div', { + class: 'sheet-backdrop is-open', + onclick: (e) => e.target === backdrop && backdrop.remove(), + }); + const methodSel = el( + 'select', + { class: 'search__input' }, + ...METHODS.map((m) => el('option', { value: m.id }, m.label)), + ); const charmChk = el('input', { type: 'checkbox' }); const panel = el( 'div', { class: 'gsheet' }, - el('div', { class: 'gsheet__head' }, el('h2', {}, 'Hunt method'), el('button', { class: 'gsheet__close', onclick: () => backdrop.remove() }, '✕')), + el( + 'div', + { class: 'gsheet__head' }, + el('h2', {}, 'Hunt method'), + el('button', { class: 'gsheet__close', onclick: () => backdrop.remove() }, '✕'), + ), methodSel, el('label', { class: 'lookup__check', style: 'margin-top:12px' }, charmChk, 'Shiny Charm'), el( @@ -173,17 +200,30 @@ export async function ShinyView() { for (const h of hunts) list.append(huntCard(h)); } list.append( - el('button', { type: 'button', class: 'lineup__add shiny__new', onclick: newHunt }, '+', el('span', {}, 'New hunt')), + el( + 'button', + { type: 'button', class: 'lineup__add shiny__new', onclick: newHunt }, + '+', + el('span', {}, 'New hunt'), + ), ); } clear(view).append( - el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/settings' }, '‹ Settings')), + el( + 'nav', + { class: 'lookup__nav' }, + el('a', { class: 'link', href: '#/settings' }, '‹ Settings'), + ), el( 'header', { class: 'view__header' }, el('h1', {}, 'Shiny hunts'), - el('p', {}, 'Count encounters and watch the cumulative odds climb. “Found it” marks the Pokémon caught.'), + el( + 'p', + {}, + 'Count encounters and watch the cumulative odds climb. “Found it” marks the Pokémon caught.', + ), ), list, ); diff --git a/src/views/TeamView.js b/src/views/TeamView.js index a6bd596..8aecd2d 100644 --- a/src/views/TeamView.js +++ b/src/views/TeamView.js @@ -16,12 +16,26 @@ import { openPokemonPicker } from '../components/PokemonPicker.js'; import { segThumb } from '../lib/anim.js'; const prettify = (s) => s.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); -const loose = (s) => s.toLowerCase().replace(/[-\s]+/g, ' ').trim(); +const loose = (s) => + s + .toLowerCase() + .replace(/[-\s]+/g, ' ') + .trim(); const multClass = (m) => m === 0 ? 'm0' : m === 0.25 ? 'm025' : m === 0.5 ? 'm05' : m === 2 ? 'm2' : m === 4 ? 'm4' : 'm1'; const multText = (m) => (m === 0.25 ? '¼' : m === 0.5 ? '½' : m === 1 ? '' : `${m}×`); const effWord = (eff) => - eff === 0 ? 'No effect' : eff >= 4 ? '4× effective' : eff >= 2 ? 'Super effective' : eff <= 0.25 ? 'Barely effective' : eff <= 0.5 ? 'Not very effective' : null; + eff === 0 + ? 'No effect' + : eff >= 4 + ? '4× effective' + : eff >= 2 + ? 'Super effective' + : eff <= 0.25 + ? 'Barely effective' + : eff <= 0.5 + ? 'Not very effective' + : null; export async function TeamView() { const view = el('section', { class: 'view teamview' }); @@ -56,7 +70,14 @@ export async function TeamView() { ), ), ); - const setThumb = segThumb(seg, MODES.length, Math.max(0, MODES.findIndex(([id]) => id === mode))); + const setThumb = segThumb( + seg, + MODES.length, + Math.max( + 0, + MODES.findIndex(([id]) => id === mode), + ), + ); const lineup = el('div', { class: 'lineup' }); const body = el('div', { class: 'teamview__body' }); @@ -75,9 +96,24 @@ export async function TeamView() { el( 'nav', { class: 'teamview__refs', 'aria-label': 'Reference tools' }, - el('a', { class: 'reflink', href: '#/natures' }, el('span', { class: 'reflink__ico', 'aria-hidden': 'true' }, '✦'), 'Natures'), - el('a', { class: 'reflink', href: '#/types' }, el('span', { class: 'reflink__ico', 'aria-hidden': 'true' }, '▦'), 'Type chart'), - el('a', { class: 'reflink', href: '#/breeding' }, el('span', { class: 'reflink__ico', 'aria-hidden': 'true' }, '⬡'), 'Breeding'), + el( + 'a', + { class: 'reflink', href: '#/natures' }, + el('span', { class: 'reflink__ico', 'aria-hidden': 'true' }, '✦'), + 'Natures', + ), + el( + 'a', + { class: 'reflink', href: '#/types' }, + el('span', { class: 'reflink__ico', 'aria-hidden': 'true' }, '▦'), + 'Type chart', + ), + el( + 'a', + { class: 'reflink', href: '#/breeding' }, + el('span', { class: 'reflink__ico', 'aria-hidden': 'true' }, '⬡'), + 'Breeding', + ), ), ), lineup, @@ -213,9 +249,7 @@ export async function TeamView() { const threats = analysis .filter((a) => a.weakN >= 2 || (a.weakN >= 1 && a.resist === 0)) .sort((a, b) => b.weakN - a.weakN || a.resist - b.resist); - const unresisted = analysis - .filter((a) => a.weakN === 0 && a.resist === 0) - .map((a) => a.type); + const unresisted = analysis.filter((a) => a.weakN === 0 && a.resist === 0).map((a) => a.type); const weakSpots = threats.length ? el( @@ -282,7 +316,11 @@ export async function TeamView() { el( 'span', { class: 'cov__bal-side cov__bal-side--r' }, - el('span', { class: 'cov__bal-weak', style: `width:${Math.min(a.weakN, 6) * 10}px` }, a.weakN || ''), + el( + 'span', + { class: 'cov__bal-weak', style: `width:${Math.min(a.weakN, 6) * 10}px` }, + a.weakN || '', + ), ), ); return el( @@ -306,7 +344,9 @@ export async function TeamView() { .slice() .sort( (a, b) => - (b.stats?.hp || 0) + (b.stats?.def || 0) + (b.stats?.spd || 0) - + (b.stats?.hp || 0) + + (b.stats?.def || 0) + + (b.stats?.spd || 0) - ((a.stats?.hp || 0) + (a.stats?.def || 0) + (a.stats?.spd || 0)), )[0]; @@ -449,7 +489,11 @@ export async function TeamView() { Sprite(sp.spriteId ?? sp.id, { style, size: 48, alt: sp.name }), el('span', {}, prettify(sp.name)), ) - : el('button', { type: 'button', class: 'calc__pick calc__pick--empty', onclick: () => pick() }, `Choose ${sideLabel}…`), + : el( + 'button', + { type: 'button', class: 'calc__pick calc__pick--empty', onclick: () => pick() }, + `Choose ${sideLabel}…`, + ), ); } function pick() { @@ -482,7 +526,13 @@ export async function TeamView() { ), ) : null; - return el('div', { class: 'calc__side' }, el('h3', { class: 'ppanel__sub' }, sideLabel), holder, quick); + return el( + 'div', + { class: 'calc__side' }, + el('h3', { class: 'ppanel__sub' }, sideLabel), + holder, + quick, + ); } const atkSlot = slot('atk', 'Attacker'); @@ -550,12 +600,18 @@ export async function TeamView() { const v = forGeneration(raw, gen, genOfVg); moveNote.textContent = `${prettify(v.type)} · ${v.damage_class ? prettify(v.damage_class) : 'Status'}${v.power ? ` · ${v.power} power` : ''}`; if (!atk || !def) { - clear(result).append(el('p', { class: 'detail__muted' }, 'Choose an attacker and a defender.')); + clear(result).append( + el('p', { class: 'detail__muted' }, 'Choose an attacker and a defender.'), + ); return; } if (!v.damage_class || v.damage_class === 'status' || !v.power) { clear(result).append( - el('p', { class: 'detail__muted' }, `${prettify(state.move.name)} doesn't deal direct damage.`), + el( + 'p', + { class: 'detail__muted' }, + `${prettify(state.move.name)} doesn't deal direct damage.`, + ), ); return; } @@ -612,13 +668,23 @@ export async function TeamView() { el( 'p', { class: 'calc__sub' }, - hitsMin === hitsMax ? `${hitsMin} hit${hitsMin > 1 ? 's' : ''} to KO` : `${hitsMin}–${hitsMax} hits to KO`, + hitsMin === hitsMax + ? `${hitsMin} hit${hitsMin > 1 ? 's' : ''} to KO` + : `${hitsMin}–${hitsMax} hits to KO`, ), el( 'div', { class: 'calc__badges' }, dmg.stab ? el('span', { class: 'calc__badge calc__badge--stab' }, 'STAB') : null, - word ? el('span', { class: `calc__badge${dmg.eff >= 2 || dmg.eff === 4 ? ' calc__badge--up' : ' calc__badge--down'}` }, word) : null, + word + ? el( + 'span', + { + class: `calc__badge${dmg.eff >= 2 || dmg.eff === 4 ? ' calc__badge--up' : ' calc__badge--down'}`, + }, + word, + ) + : null, ), ); } diff --git a/src/views/TypeChartView.js b/src/views/TypeChartView.js index 645eac2..6d96fce 100644 --- a/src/views/TypeChartView.js +++ b/src/views/TypeChartView.js @@ -7,9 +7,24 @@ import { TypeChip } from '../components/TypeChip.js'; import { typeHex } from '../lib/type-color.js'; const ABBR = { - normal: 'NOR', fire: 'FIR', water: 'WAT', electric: 'ELE', grass: 'GRA', ice: 'ICE', - fighting: 'FIG', poison: 'POI', ground: 'GRD', flying: 'FLY', psychic: 'PSY', bug: 'BUG', - rock: 'ROC', ghost: 'GHO', dragon: 'DRA', dark: 'DRK', steel: 'STE', fairy: 'FAI', + normal: 'NOR', + fire: 'FIR', + water: 'WAT', + electric: 'ELE', + grass: 'GRA', + ice: 'ICE', + fighting: 'FIG', + poison: 'POI', + ground: 'GRD', + flying: 'FLY', + psychic: 'PSY', + bug: 'BUG', + rock: 'ROC', + ghost: 'GHO', + dragon: 'DRA', + dark: 'DRK', + steel: 'STE', + fairy: 'FAI', }; const cellText = (m) => (m === 0 ? '0' : m === 0.5 ? '½' : m === 2 ? '2' : ''); const cellClass = (m) => (m === 0 ? 'm0' : m === 0.5 ? 'm05' : m === 2 ? 'm2' : 'm1'); @@ -78,11 +93,7 @@ export async function TypeChartView() { const m = multiplier(atk, [def], gen); const hot = selected === atk || selected === def; grid.append( - el( - 'span', - { class: `tchart__cell ${cellClass(m)}${hot ? ' is-hot' : ''}` }, - cellText(m), - ), + el('span', { class: `tchart__cell ${cellClass(m)}${hot ? ' is-hot' : ''}` }, cellText(m)), ); } } @@ -90,7 +101,11 @@ export async function TypeChartView() { clear(summary); if (!selected) { summary.append( - el('p', { class: 'detail__muted' }, 'Tap a type on the edge to see everything it hits and everything that hits it.'), + el( + 'p', + { class: 'detail__muted' }, + 'Tap a type on the edge to see everything it hits and everything that hits it.', + ), ); return; } @@ -99,7 +114,12 @@ export async function TypeChartView() { const def = defensiveMatchups([selected], gen); const chipRow = (label, list) => list.length - ? el('div', { class: 'tchart__srow' }, el('span', { class: 'tchart__slabel' }, label), el('span', { class: 'matchups__types' }, ...list.map(TypeChip))) + ? el( + 'div', + { class: 'tchart__srow' }, + el('span', { class: 'tchart__slabel' }, label), + el('span', { class: 'matchups__types' }, ...list.map(TypeChip)), + ) : null; summary.append( el('h3', { class: 'ppanel__sub' }, `${prettify(selected)} — attacking`), diff --git a/vite.config.js b/vite.config.js index 053fc5b..82048ab 100644 --- a/vite.config.js +++ b/vite.config.js @@ -21,11 +21,7 @@ export default defineConfig({ // The bundled PokéAPI snapshot can be a few hundred KB. maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, }, - includeAssets: [ - 'icon.svg', - 'favicon.svg', - 'apple-touch-icon.png', - ], + includeAssets: ['icon.svg', 'favicon.svg', 'apple-touch-icon.png'], manifest: { name: 'Pocketdex', short_name: 'Pocketdex',