Compare commits

..

No commits in common. "f71f1842cd02fea031dc44ceeeeb4377e6d9e020" and "78df1a32e85fce56b4494a077aca88e1e224dd16" have entirely different histories.

9 changed files with 29 additions and 191 deletions

View File

@ -92,19 +92,19 @@ Working app with a type-themed UI:
snapshot; a pill switcher on the detail page rebuilds types / stats /
matchups / abilities / learnset / artwork for the chosen form. Purely
cosmetic variants (Unown's 27 letters, Vivillon's 19 patterns, Furfrou
trims, seasonal Deerling/Sawsbuck, Flabébé line colors, Alcremie's 62
trims, seasonal Deerling/Sawsbuck, Flabébé line colours, Alcremie's 62
decorations, Pikachu caps — 278 across 50 species) get a separate compact
"Appearance" dropdown that only swaps the sprite. Cards show a "+N forms"
badge.
- **Detail** — type-gradient hero + tabbed sheet (About / Stats / Evolution /
Moves / Locations); colored animated stat bars; defensive type matchups;
Moves / Locations); coloured animated stat bars; defensive type matchups;
evolution chain re-parented to the selected game's generation; learnset with
per-move power/type/accuracy/PP/effect (era-accurate via `past_values`,
incl. pre-Gen-4 physical/special-by-type), the TM/HM group ordered by
TM number for the selected game; wild encounter locations.
- **Game-aware** — abilities gated to Gen 3+ (hidden to Gen 5+); type chart
applies Gen 1 / pre-Gen 6 rules.
- **Games** — overlay picker with stylised version-color cover tiles,
- **Games** — overlay picker with stylised version-colour cover tiles,
sub-dex switch, and an "All games" (National, no gen limits) option.
- **Search** — tabbed lookup: Pokémon, Moves, Items and Abilities, all
browsable offline from the snapshot. Moves filter by type / damage class
@ -133,7 +133,7 @@ Working app with a type-themed UI:
chart (tap a type for its offensive + defensive breakdown; both
gen-aware).
- **Settings** — theme (System / Light / Dark / Black / Sepia) + accent
color, text size, an in-app reduce-motion override (on top of the OS
colour, text size, an in-app reduce-motion override (on top of the OS
preference, which is always respected too), sprite style, haptic
feedback on catch (Vibration API), which screen to land on at launch,
JSON export/import.

View File

@ -10,7 +10,7 @@ import { prettify } from '../data/pokedex-resolver.js';
/**
* One Pokémon in the dex feed. Tinted by its primary type: a soft top-down
* gradient, a type-colored spotlight behind an oversized sprite that lifts
* gradient, a type-coloured 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 } = {}) {

View File

@ -38,12 +38,7 @@ export function ProgressRing() {
const num = el('span', { class: 'ring__num' }, '0');
const tail = el('small', {});
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' },
svg,
el('div', { class: 'ring__label' }, value),
);
const node = el('div', { class: 'ring' }, svg, el('div', { class: 'ring__label' }, value));
let first = true;
let shown = 0;

View File

@ -1,7 +1,7 @@
/**
* Signature colors for each game, used to render a stylised "cover" tile in
* Signature colours for each game, used to render a stylised "cover" tile in
* the game picker (real box art is Nintendo's and not in the API). Two
* colors = the paired versions; games without a listed entry fall back to
* colours = the paired versions; games without a listed entry fall back to
* a per-generation hue.
*/
const GAME = {

View File

@ -1,60 +0,0 @@
import { el } from './dom.js';
/**
* A small centered modal with one or more choices. Resolves to the chosen
* option's `key`, or `null` if dismissed (backdrop, Escape, or a choice
* whose key is null).
*
* const pick = await chooseDialog({
* title: 'Import save',
* body: '84 seen, 40 caught.',
* choices: [
* { key: 'merge', label: 'Add to my Pokédex' },
* { key: 'replace', label: 'Replace mine', class: 'button--danger' },
* { key: null, label: 'Cancel', class: 'button--ghost' },
* ],
* });
*/
export function chooseDialog({ title, body, choices }) {
return new Promise((resolve) => {
const backdrop = el('div', {
class: 'sheet-backdrop dialog-backdrop is-open',
onclick: (e) => {
if (e.target === backdrop) done(null);
},
});
const panel = el(
'div',
{ class: 'dialog', role: 'dialog', 'aria-modal': 'true' },
el('h2', { class: 'dialog__title' }, title),
body ? el('p', { class: 'dialog__body' }, body) : null,
el(
'div',
{ class: 'dialog__actions' },
...choices.map((c) =>
el(
'button',
{
class: `button ${c.class || ''}`.trim(),
type: 'button',
onclick: () => done(c.key ?? null),
},
c.label,
),
),
),
);
const onKey = (e) => {
if (e.key === 'Escape') done(null);
};
function done(key) {
document.removeEventListener('keydown', onKey);
backdrop.remove();
resolve(key);
}
backdrop.append(panel);
document.body.append(backdrop);
document.addEventListener('keydown', onKey);
requestAnimationFrame(() => panel.querySelector('.button')?.focus());
});
}

View File

@ -1,5 +1,5 @@
// Resolve a Pokémon type to its hex color by reading the --type-* CSS
// custom properties once and memoising. Type colors do not change between
// Resolve a Pokémon type to its hex colour by reading the --type-* CSS
// custom properties once and memoising. Type colours don't change between
// light and dark, so the cache is safe for the session.
const TYPES = [
'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison',

View File

@ -60,29 +60,6 @@ export function importFlags({ seen = [], caught = [] } = {}) {
});
}
/**
* Set seen/caught to exactly the given lists favourites and notes are
* kept, everything else's seen/caught is cleared. For "replace with this
* save" imports.
*/
export function replaceFlags({ seen = [], caught = [] } = {}) {
selection.set((s) => {
const stamp = new Date().toISOString();
const pokemon = {};
for (const [id, e] of Object.entries(s.pokemon)) {
if (e.favorite || (e.note && e.note.trim())) {
pokemon[id] = { seen: false, caught: false, favorite: !!e.favorite, note: e.note || '', updatedAt: e.updatedAt || stamp };
}
}
const bump = (id, patch) => {
pokemon[id] = { ...(pokemon[id] || BLANK), ...patch, updatedAt: stamp };
};
for (const id of seen) bump(id, { seen: true });
for (const id of caught) bump(id, { seen: true, caught: true });
return { ...s, pokemon };
});
}
/** Aggregate seen/caught counts over an arbitrary list of national ids. */
export function stats(speciesIds) {
const p = selection.get().pokemon;

View File

@ -233,17 +233,6 @@
width: 60px;
height: 60px;
flex: none;
display: block;
text-decoration: none;
color: inherit;
border-radius: 50%;
transition: transform 0.12s var(--ease-spring);
}
.ring:hover {
transform: scale(1.06);
}
.ring:active {
transform: scale(0.95);
}
.ring__svg {
width: 100%;
@ -647,7 +636,7 @@
text-transform: capitalize;
}
/* Cover tiles: a clean split of the two version colors, no gloss. */
/* Cover tiles: a clean split of the two version colours, no gloss. */
.gamecard--cover {
position: relative;
overflow: hidden;
@ -1605,16 +1594,6 @@
.settings__install {
margin: 10px 0 0;
}
.settings__save {
margin: 10px 0 0;
}
.settings__save .button {
display: inline-flex;
}
.settings__save .settings__note {
margin: 6px 0 0;
font-size: 0.82rem;
}
.field {
display: flex;
flex-direction: column;
@ -3387,44 +3366,3 @@
animation-duration: 0.34s;
animation-timing-function: var(--ease-spring);
}
/* ---- Centered modal dialog ---------------------------------- */
.dialog-backdrop {
align-items: center;
padding: 20px;
}
.dialog {
width: 100%;
max-width: 420px;
background: var(--surface);
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
padding: 20px;
transform: translateY(12px) scale(0.97);
opacity: 0;
transition: transform 0.2s var(--ease-spring), opacity 0.2s ease;
}
.dialog-backdrop.is-open .dialog {
transform: none;
opacity: 1;
}
.dialog__title {
margin: 0;
font-size: 1.1rem;
}
.dialog__body {
margin: 8px 0 0;
font-size: 0.9rem;
color: var(--text-dim);
line-height: 1.5;
}
.dialog__actions {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 18px;
}
.dialog__actions .button {
width: 100%;
justify-content: center;
}

View File

@ -1,7 +1,6 @@
import { el, onTeardown } from '../lib/dom.js';
import { settings, applyTheme } from '../store/settings.js';
import { selection, importFlags, replaceFlags } from '../store/selection.js';
import { chooseDialog } from '../lib/dialog.js';
import { selection, importFlags } from '../store/selection.js';
import { team } from '../store/team.js';
import { formTracking } from '../store/formTracking.js';
import { shinyHunts } from '../store/shinyHunts.js';
@ -51,7 +50,7 @@ export async function SettingsView() {
}),
),
);
const accentField = el('div', { class: 'field' }, el('span', {}, 'Accent color'), accentRow);
const accentField = el('div', { class: 'field' }, el('span', {}, 'Accent colour'), accentRow);
const fontSizeField = selectField('Text size', st.fontScale || 'default', [
['small', 'Small'],
@ -124,7 +123,7 @@ export async function SettingsView() {
storageNote.textContent = 'Storage estimate not available in this browser.';
}
// Import the Pokédex out of a Gen 1/2/3 game save (.sav / .srm).
// Import the Pokédex out of a Gen 3 GBA save (.sav / .srm).
const saveNote = el('p', { class: 'settings__note' });
const saveInput = el('input', {
type: 'file',
@ -134,33 +133,22 @@ export async function SettingsView() {
const file = e.target.files[0];
if (!file) return;
saveNote.textContent = 'Reading…';
let dex;
try {
dex = parseSaveDex(await file.arrayBuffer());
} catch (err) {
saveNote.textContent = err.message || 'Could not read that save file.';
e.target.value = '';
return;
}
const dex = parseSaveDex(await file.arrayBuffer());
const who = dex.trainer || dex.game;
const choice = await chooseDialog({
title: `${dex.game}${dex.trainer ? ` · ${dex.trainer}` : ''}`,
body: `${dex.seen.length} seen · ${dex.caught.length} caught in this save.`,
choices: [
{ key: 'merge', label: 'Add to my Pokédex' },
{ key: 'replace', label: 'Replace mine with this', class: 'button--danger' },
{ key: null, label: 'Cancel', class: 'button--ghost' },
],
});
if (choice === 'merge') {
if (
confirm(
`${dex.game}${dex.trainer ? `${dex.trainer}` : ''}\n${dex.seen.length} seen, ${dex.caught.length} caught.\n\nMerge into your Pokédex? Nothing is removed.`,
)
) {
importFlags({ seen: dex.seen, caught: dex.caught });
saveNote.textContent = `Added ${dex.caught.length} caught / ${dex.seen.length} seen from ${who}.`;
} else if (choice === 'replace') {
replaceFlags({ seen: dex.seen, caught: dex.caught });
saveNote.textContent = `Replaced tracking with ${who}'s Pokédex (favourites and notes kept).`;
saveNote.textContent = `Merged ${dex.caught.length} caught / ${dex.seen.length} seen from ${who}.`;
} else {
saveNote.textContent = '';
}
} catch (err) {
saveNote.textContent = err.message || 'Could not read that save file.';
}
e.target.value = '';
},
});
@ -239,10 +227,10 @@ export async function SettingsView() {
el('a', { class: 'link', href: '#/shiny' }, 'Shiny hunts'),
),
el(
'div',
{ class: 'settings__save' },
'p',
{ class: 'settings__note' },
el('button', { class: 'button', type: 'button', onclick: () => saveInput.click() }, 'Import from a game save'),
el('p', { class: 'settings__note' }, 'A .sav / .srm from a Gen 13 game.'),
' — a .sav / .srm from Gen 1 (R/B/Y), Gen 2 (G/S/C) or Gen 3 (R/S/E, FR/LG). Reads the games own Pokédex; merges seen/caught, removes nothing.',
),
saveInput,
saveNote,