Compare commits
No commits in common. "61ceb6abf33ba9f5460d1bf2e66544a0f0a479bd" and "01269fd35a7b7dc365dd79cce4c8553396b1d720" have entirely different histories.
61ceb6abf3
...
01269fd35a
@ -2,15 +2,13 @@
|
|||||||
/**
|
/**
|
||||||
* Build a compact, offline-first data snapshot from PokéAPI.
|
* Build a compact, offline-first data snapshot from PokéAPI.
|
||||||
*
|
*
|
||||||
* The app runs its list, search, sort, filters and game switching entirely
|
* The app needs three things to work fully offline before any lazy detail
|
||||||
* off this file — no network until you open a detail page. It carries, per
|
* fetch: the species list (id, name, generation, types), the regional
|
||||||
* species: name, generation, current + past typings, the six base stats and
|
* Pokédex lists (which species, in what order, with regional numbers), and
|
||||||
* their total, height, weight, base experience, and species flags
|
* the version groups (games) with their associated Pokédexes.
|
||||||
* (legendary / mythical / baby, capture rate, growth rate, colour, egg
|
|
||||||
* groups, gender rate). Plus every regional Pokédex and every version group.
|
|
||||||
*
|
*
|
||||||
* Richer detail (abilities, moves, flavour text, evolution, encounters) is
|
* Everything richer than that — stats, abilities, flavor text, evolution —
|
||||||
* fetched on demand by the app and cached by the service worker.
|
* is fetched on demand by the app and cached by the service worker.
|
||||||
*
|
*
|
||||||
* Output: src/data/snapshot.json (imported by src/data/snapshot.js)
|
* Output: src/data/snapshot.json (imported by src/data/snapshot.js)
|
||||||
* Usage: node scripts/build-snapshot.mjs [--force]
|
* Usage: node scripts/build-snapshot.mjs [--force]
|
||||||
@ -24,18 +22,19 @@ const OUT = resolve(__dirname, '../src/data/snapshot.json');
|
|||||||
const BASE = 'https://pokeapi.co/api/v2';
|
const BASE = 'https://pokeapi.co/api/v2';
|
||||||
const MAX_AGE_DAYS = 30;
|
const MAX_AGE_DAYS = 30;
|
||||||
const FORCE = process.argv.includes('--force');
|
const FORCE = process.argv.includes('--force');
|
||||||
const CONCURRENCY = 16;
|
const CONCURRENCY = 8;
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
||||||
const pretty = (k) => k.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
const pretty = (k) =>
|
||||||
|
k.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
|
||||||
const _cache = new Map();
|
const _cache = new Map();
|
||||||
async function api(path) {
|
async function api(path) {
|
||||||
if (_cache.has(path)) return _cache.get(path);
|
if (_cache.has(path)) return _cache.get(path);
|
||||||
const url = path.startsWith('http') ? path : `${BASE}/${path}`;
|
const url = path.startsWith('http') ? path : `${BASE}/${path}`;
|
||||||
let lastErr;
|
let lastErr;
|
||||||
for (let attempt = 0; attempt < 5; attempt++) {
|
for (let attempt = 0; attempt < 4; attempt++) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@ -56,19 +55,13 @@ async function api(path) {
|
|||||||
async function mapLimit(items, limit, fn) {
|
async function mapLimit(items, limit, fn) {
|
||||||
const out = new Array(items.length);
|
const out = new Array(items.length);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
let done = 0;
|
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
||||||
const total = items.length;
|
while (i < items.length) {
|
||||||
const workers = Array.from({ length: Math.min(limit, total) }, async () => {
|
|
||||||
while (i < total) {
|
|
||||||
const idx = i++;
|
const idx = i++;
|
||||||
out[idx] = await fn(items[idx], idx);
|
out[idx] = await fn(items[idx], idx);
|
||||||
if (++done % 200 === 0 || done === total) {
|
|
||||||
process.stdout.write(`\r … ${done}/${total} species`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await Promise.all(workers);
|
await Promise.all(workers);
|
||||||
if (total) process.stdout.write('\n');
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,29 +85,71 @@ async function main() {
|
|||||||
console.log('Building snapshot from PokéAPI …');
|
console.log('Building snapshot from PokéAPI …');
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
|
|
||||||
// ---- Type name list ------------------------------------------------
|
// ---- Types: build pokemonName -> [type, ...] -----------------------------
|
||||||
const typeIndex = await api('type?limit=100');
|
const typeIndex = await api('type?limit=100');
|
||||||
const types = typeIndex.results
|
const usableTypes = typeIndex.results
|
||||||
.map((r) => r.name)
|
.map((r) => r.name)
|
||||||
.filter((n) => !['unknown', 'shadow', 'stellar'].includes(n));
|
.filter((n) => !['unknown', 'shadow', 'stellar'].includes(n));
|
||||||
|
|
||||||
// ---- Generations (for display names / regions) ------------------
|
const typesByPokemon = new Map();
|
||||||
|
await mapLimit(usableTypes, CONCURRENCY, async (t) => {
|
||||||
|
const data = await api(`type/${t}`);
|
||||||
|
for (const { slot, pokemon } of data.pokemon) {
|
||||||
|
const arr = typesByPokemon.get(pokemon.name) || [];
|
||||||
|
arr.push({ slot, type: t });
|
||||||
|
typesByPokemon.set(pokemon.name, arr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const typesFor = (name) =>
|
||||||
|
(typesByPokemon.get(name) || [])
|
||||||
|
.sort((a, b) => a.slot - b.slot)
|
||||||
|
.map((x) => x.type);
|
||||||
|
const typesForSpecies = (name) => {
|
||||||
|
const direct = typesFor(name);
|
||||||
|
if (direct.length) return direct;
|
||||||
|
// Fall back to a default form variant, e.g. "deoxys" -> "deoxys-normal".
|
||||||
|
for (const key of typesByPokemon.keys()) {
|
||||||
|
if (key === name || key.startsWith(`${name}-`)) {
|
||||||
|
const t = typesFor(key);
|
||||||
|
if (t.length) return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Generations: species -> { name, generation } ----------------------
|
||||||
const genIndex = await api('generation?limit=50');
|
const genIndex = await api('generation?limit=50');
|
||||||
const generations = [];
|
const generations = [];
|
||||||
await mapLimit(genIndex.results, 8, async (g) => {
|
const speciesMeta = new Map();
|
||||||
|
await mapLimit(genIndex.results, CONCURRENCY, async (g) => {
|
||||||
const data = await api(`generation/${g.name}`);
|
const data = await api(`generation/${g.name}`);
|
||||||
generations.push({
|
generations.push({
|
||||||
id: data.id,
|
id: data.id,
|
||||||
name: pretty(data.main_region?.name || g.name),
|
name: pretty(data.main_region?.name || g.name),
|
||||||
region: data.main_region?.name || null,
|
region: data.main_region?.name || null,
|
||||||
});
|
});
|
||||||
|
for (const sp of data.pokemon_species) {
|
||||||
|
const id = idFromUrl(sp.url);
|
||||||
|
speciesMeta.set(id, { id, name: sp.name, generation: data.id });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
generations.sort((a, b) => a.id - b.id);
|
generations.sort((a, b) => a.id - b.id);
|
||||||
|
|
||||||
// ---- Pokédexes ----------------------------------------------------
|
const speciesIds = [...speciesMeta.keys()].sort((a, b) => a - b);
|
||||||
|
const species = speciesIds.map((id) => {
|
||||||
|
const m = speciesMeta.get(id);
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: m.name,
|
||||||
|
generation: m.generation,
|
||||||
|
types: typesForSpecies(m.name),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- Pokédexes --------------------------------------------------------
|
||||||
const dexIndex = await api('pokedex?limit=100');
|
const dexIndex = await api('pokedex?limit=100');
|
||||||
const pokedexes = [];
|
const pokedexes = [];
|
||||||
await mapLimit(dexIndex.results, 8, async (d) => {
|
await mapLimit(dexIndex.results, CONCURRENCY, async (d) => {
|
||||||
const data = await api(`pokedex/${d.name}`);
|
const data = await api(`pokedex/${d.name}`);
|
||||||
pokedexes.push({
|
pokedexes.push({
|
||||||
key: data.name,
|
key: data.name,
|
||||||
@ -129,10 +164,10 @@ async function main() {
|
|||||||
});
|
});
|
||||||
pokedexes.sort((a, b) => a.key.localeCompare(b.key));
|
pokedexes.sort((a, b) => a.key.localeCompare(b.key));
|
||||||
|
|
||||||
// ---- Version groups (the "games") -----------------------------
|
// ---- Version groups (the "games") -----------------------------------
|
||||||
const vgIndex = await api('version-group?limit=100');
|
const vgIndex = await api('version-group?limit=100');
|
||||||
const versionGroups = [];
|
const versionGroups = [];
|
||||||
await mapLimit(vgIndex.results, 8, async (v) => {
|
await mapLimit(vgIndex.results, CONCURRENCY, async (v) => {
|
||||||
const data = await api(`version-group/${v.name}`);
|
const data = await api(`version-group/${v.name}`);
|
||||||
versionGroups.push({
|
versionGroups.push({
|
||||||
key: data.name,
|
key: data.name,
|
||||||
@ -146,57 +181,7 @@ async function main() {
|
|||||||
(a, b) => a.generation - b.generation || a.key.localeCompare(b.key),
|
(a, b) => a.generation - b.generation || a.key.localeCompare(b.key),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---- Species: the data-rich part -----------------------------
|
// ---- Write ----------------------------------------------------------
|
||||||
const speciesIndex = await api('pokemon-species?limit=100000');
|
|
||||||
const ids = speciesIndex.results
|
|
||||||
.map((r) => idFromUrl(r.url))
|
|
||||||
.sort((a, b) => a - b);
|
|
||||||
|
|
||||||
console.log(`Fetching ${ids.length} species (stats, typings, flags) …`);
|
|
||||||
const species = await mapLimit(ids, CONCURRENCY, async (id) => {
|
|
||||||
const [pk, sp] = await Promise.all([
|
|
||||||
api(`pokemon/${id}`),
|
|
||||||
api(`pokemon-species/${id}`),
|
|
||||||
]);
|
|
||||||
const s = Object.fromEntries(pk.stats.map((x) => [x.stat.name, x.base_stat]));
|
|
||||||
const stats = {
|
|
||||||
hp: s.hp ?? 0,
|
|
||||||
atk: s.attack ?? 0,
|
|
||||||
def: s.defense ?? 0,
|
|
||||||
spa: s['special-attack'] ?? 0,
|
|
||||||
spd: s['special-defense'] ?? 0,
|
|
||||||
spe: s.speed ?? 0,
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name: sp.name,
|
|
||||||
generation: idFromUrl(sp.generation.url),
|
|
||||||
types: pk.types
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => a.slot - b.slot)
|
|
||||||
.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),
|
|
||||||
})),
|
|
||||||
stats,
|
|
||||||
bst: stats.hp + stats.atk + stats.def + stats.spa + stats.spd + stats.spe,
|
|
||||||
height: pk.height,
|
|
||||||
weight: pk.weight,
|
|
||||||
baseExp: pk.base_experience ?? null,
|
|
||||||
isLegendary: sp.is_legendary,
|
|
||||||
isMythical: sp.is_mythical,
|
|
||||||
isBaby: sp.is_baby,
|
|
||||||
captureRate: sp.capture_rate,
|
|
||||||
baseHappiness: sp.base_happiness,
|
|
||||||
growthRate: sp.growth_rate?.name || null,
|
|
||||||
color: sp.color?.name || null,
|
|
||||||
eggGroups: sp.egg_groups.map((g) => g.name),
|
|
||||||
genderRate: sp.gender_rate,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- Write ----------------------------------------------------
|
|
||||||
const snapshot = {
|
const snapshot = {
|
||||||
meta: {
|
meta: {
|
||||||
generatedAt: new Date().toISOString(),
|
generatedAt: new Date().toISOString(),
|
||||||
@ -207,7 +192,7 @@ async function main() {
|
|||||||
versionGroups: versionGroups.length,
|
versionGroups: versionGroups.length,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
types,
|
types: usableTypes,
|
||||||
generations,
|
generations,
|
||||||
species,
|
species,
|
||||||
pokedexes,
|
pokedexes,
|
||||||
@ -215,12 +200,14 @@ async function main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await mkdir(dirname(OUT), { recursive: true });
|
await mkdir(dirname(OUT), { recursive: true });
|
||||||
const body = JSON.stringify(snapshot);
|
await writeFile(OUT, JSON.stringify(snapshot));
|
||||||
await writeFile(OUT, body);
|
const kb = (JSON.stringify(snapshot).length / 1024).toFixed(0);
|
||||||
console.log(
|
console.log(
|
||||||
`Wrote ${OUT} — ${species.length} species, ${pokedexes.length} pokédexes, ` +
|
`Wrote ${OUT} — ${species.length} species, ${pokedexes.length} pokédexes, ` +
|
||||||
`${versionGroups.length} version groups (${(body.length / 1024).toFixed(0)} KB) in ` +
|
`${versionGroups.length} version groups (${kb} KB) in ${(
|
||||||
`${((Date.now() - t0) / 1000).toFixed(1)}s`,
|
(Date.now() - t0) /
|
||||||
|
1000
|
||||||
|
).toFixed(1)}s`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { typeHex } from '../lib/type-color.js';
|
|||||||
* gradient, a type-coloured 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.
|
* above the card, a number chip, and a big ghost number in the corner.
|
||||||
*/
|
*/
|
||||||
export function Card(species, number, { spriteStyle = 'official', versionGroup, boxed = false, metric = null } = {}) {
|
export function Card(species, number, { spriteStyle = 'official', versionGroup, boxed = false } = {}) {
|
||||||
const state = entry(species.id);
|
const state = entry(species.id);
|
||||||
const mainType = (species.types || [])[0] || 'normal';
|
const mainType = (species.types || [])[0] || 'normal';
|
||||||
const num = String(number ?? species.id).padStart(3, '0');
|
const num = String(number ?? species.id).padStart(3, '0');
|
||||||
@ -53,7 +53,6 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup,
|
|||||||
el('span', { class: 'card__num' }, `#${num}`),
|
el('span', { class: 'card__num' }, `#${num}`),
|
||||||
el('span', { class: 'card__name' }, species.name.replace(/-/g, ' ')),
|
el('span', { class: 'card__name' }, species.name.replace(/-/g, ' ')),
|
||||||
el('span', { class: 'card__types' }, ...(species.types || []).map(TypeChip)),
|
el('span', { class: 'card__types' }, ...(species.types || []).map(TypeChip)),
|
||||||
metric ? el('span', { class: 'card__metric' }, metric) : null,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -13,36 +13,23 @@ import { createStore } from './createStore.js';
|
|||||||
export const settings = createStore('pdx.settings', {
|
export const settings = createStore('pdx.settings', {
|
||||||
versionGroup: 'scarlet-violet',
|
versionGroup: 'scarlet-violet',
|
||||||
pokedex: null,
|
pokedex: null,
|
||||||
theme: 'system', // system | light | dark | black | sepia
|
theme: 'system',
|
||||||
accent: 'red', // red | blue | green | amber | violet | rose
|
|
||||||
spriteStyle: 'official',
|
spriteStyle: 'official',
|
||||||
showShiny: false,
|
showShiny: false,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
});
|
});
|
||||||
|
|
||||||
const DARKISH = new Set(['dark', 'black']);
|
export function applyTheme(theme = settings.get().theme) {
|
||||||
|
|
||||||
export function applyTheme(state = settings.get()) {
|
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
const { theme, accent } = state;
|
if (theme === 'system') root.removeAttribute('data-theme');
|
||||||
|
|
||||||
if (!theme || theme === 'system') root.removeAttribute('data-theme');
|
|
||||||
else root.setAttribute('data-theme', theme);
|
else root.setAttribute('data-theme', theme);
|
||||||
|
|
||||||
if (!accent || accent === 'red') root.removeAttribute('data-accent');
|
|
||||||
else root.setAttribute('data-accent', accent);
|
|
||||||
|
|
||||||
const meta = document.querySelector('meta[name="theme-color"]');
|
const meta = document.querySelector('meta[name="theme-color"]');
|
||||||
if (meta) {
|
if (meta) {
|
||||||
const dark =
|
const dark =
|
||||||
DARKISH.has(theme) ||
|
theme === 'dark' ||
|
||||||
((!theme || theme === 'system') &&
|
(theme === 'system' &&
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
meta.setAttribute(
|
meta.setAttribute('content', dark ? '#0b0b0c' : '#b3161a');
|
||||||
'content',
|
|
||||||
dark
|
|
||||||
? '#0b0b0c'
|
|
||||||
: getComputedStyle(root).getPropertyValue('--accent').trim() || '#b3161a',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,4 @@ export const ui = createStore('pdx.ui', {
|
|||||||
detailTab: 'about',
|
detailTab: 'about',
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
searchScroll: 0,
|
searchScroll: 0,
|
||||||
sort: 'dex',
|
|
||||||
sortDesc: false,
|
|
||||||
filter: 'all',
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -219,13 +219,29 @@
|
|||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
.feed-head__game {
|
.game-pill {
|
||||||
display: block;
|
display: inline-flex;
|
||||||
margin-top: 4px;
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 5px 10px 5px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
color: var(--text-dim);
|
||||||
|
font: inherit;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-dim);
|
text-decoration: none;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.game-pill:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.game-pill__chev {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ring {
|
.ring {
|
||||||
@ -339,42 +355,6 @@
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feed-sort {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
.feed-sort__label {
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-dim);
|
|
||||||
}
|
|
||||||
.feed-sort__select {
|
|
||||||
flex: 1;
|
|
||||||
max-width: 220px;
|
|
||||||
padding: 7px 12px;
|
|
||||||
border: 1.5px solid var(--border);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.84rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.feed-sort__dir {
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
flex: none;
|
|
||||||
border: 1.5px solid var(--border);
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text);
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
|
||||||
@ -517,17 +497,6 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
.card__metric {
|
|
||||||
margin-top: 6px;
|
|
||||||
align-self: flex-start;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: color-mix(in srgb, var(--type-main) 24%, var(--surface-2));
|
|
||||||
color: var(--text);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 700;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
.card__caught {
|
.card__caught {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
@ -709,14 +678,8 @@
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.phero__num {
|
.phero__num {
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
text-align: center;
|
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
.phero__head {
|
.phero__head {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
@ -791,23 +754,19 @@
|
|||||||
.phero__nav:hover {
|
.phero__nav:hover {
|
||||||
background: rgba(255, 255, 255, 0.3);
|
background: rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
.phero__actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
.phero__shiny {
|
.phero__shiny {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
bottom: 108px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 6px 12px;
|
padding: 6px 14px;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 0.74rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
background: rgba(255, 255, 255, 0.18);
|
background: rgba(255, 255, 255, 0.18);
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
.phero__shiny.is-on {
|
.phero__shiny.is-on {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@ -943,6 +902,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
.flavor__all {
|
.flavor__all {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
@ -1363,28 +1323,6 @@
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
.swatches {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.swatch {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid var(--border);
|
|
||||||
background: var(--sw);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
transition: transform 0.12s var(--ease-spring);
|
|
||||||
}
|
|
||||||
.swatch:hover {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
.swatch.is-on {
|
|
||||||
border-color: var(--text);
|
|
||||||
box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--sw);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Toast ------------------------------------------- */
|
/* ---------- Toast ------------------------------------------- */
|
||||||
.toast {
|
.toast {
|
||||||
|
|||||||
@ -47,9 +47,8 @@
|
|||||||
--type-fairy: #ec8fe6;
|
--type-fairy: #ec8fe6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* System default: follow the OS only when no explicit theme is chosen. */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root:not([data-theme]) {
|
:root:not([data-theme="light"]) {
|
||||||
--bg: #0b0b0c;
|
--bg: #0b0b0c;
|
||||||
--surface: #17171a;
|
--surface: #17171a;
|
||||||
--surface-2: #202024;
|
--surface-2: #202024;
|
||||||
@ -74,39 +73,6 @@
|
|||||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pure black — for OLED screens. */
|
|
||||||
:root[data-theme="black"] {
|
|
||||||
--bg: #000000;
|
|
||||||
--surface: #0b0b0c;
|
|
||||||
--surface-2: #17171a;
|
|
||||||
--text: #f4f4f6;
|
|
||||||
--text-dim: #9a9aa4;
|
|
||||||
--border: #222226;
|
|
||||||
--accent: #ff5a5f;
|
|
||||||
--accent-text: #16161a;
|
|
||||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.6), 0 10px 30px rgba(0, 0, 0, 0.55);
|
|
||||||
--shadow-lg: 0 20px 50px -12px rgba(0, 0, 0, 0.65);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sepia — a warm, low-glare light theme. */
|
|
||||||
:root[data-theme="sepia"] {
|
|
||||||
--bg: #f2e8d5;
|
|
||||||
--surface: #fbf4e4;
|
|
||||||
--surface-2: #e9dbbe;
|
|
||||||
--text: #3b3022;
|
|
||||||
--text-dim: #7c6c52;
|
|
||||||
--border: #dccbaa;
|
|
||||||
--accent: #a8410f;
|
|
||||||
--accent-text: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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; }
|
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,88 +2,40 @@ import { el, clear, onTeardown } from '../lib/dom.js';
|
|||||||
import { loadSnapshot } from '../data/snapshot.js';
|
import { loadSnapshot } from '../data/snapshot.js';
|
||||||
import { resolvePokedex, dexRows, prettify } from '../data/pokedex-resolver.js';
|
import { resolvePokedex, dexRows, prettify } from '../data/pokedex-resolver.js';
|
||||||
import { settings } from '../store/settings.js';
|
import { settings } from '../store/settings.js';
|
||||||
import { ui } from '../store/ui.js';
|
|
||||||
import { selection, stats } from '../store/selection.js';
|
import { selection, stats } from '../store/selection.js';
|
||||||
import { Card } from '../components/Card.js';
|
import { Card } from '../components/Card.js';
|
||||||
import { ProgressRing } from '../components/ProgressRing.js';
|
import { ProgressRing } from '../components/ProgressRing.js';
|
||||||
|
import { openGameSheet } from '../components/GameSheet.js';
|
||||||
|
|
||||||
const FILTERS = [
|
const FILTERS = [
|
||||||
{ key: 'all', label: 'All', test: () => true },
|
{ key: 'all', label: 'All', test: () => true },
|
||||||
{ key: 'caught', label: 'Caught', test: (e) => e.caught },
|
{ key: 'caught', label: 'Caught', test: (e) => e.caught },
|
||||||
{ key: 'missing', label: 'Missing', test: (e) => !e.caught },
|
{ key: 'missing', label: 'Missing', test: (e) => !e.caught },
|
||||||
{ key: 'favorite', label: 'Favorites', test: (e) => e.favorite },
|
{ key: 'favorite', label: 'Favorites', test: (e) => e.favorite },
|
||||||
{ key: 'legendary', label: 'Legendary', test: (e, sp) => sp.isLegendary || sp.isMythical },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const SORTS = [
|
|
||||||
{ key: 'dex', label: 'Dex number', desc: false },
|
|
||||||
{ key: 'name', label: 'Name (A–Z)', desc: false },
|
|
||||||
{ key: 'bst', label: 'Base stat total', desc: true },
|
|
||||||
{ key: 'hp', label: 'HP', desc: true },
|
|
||||||
{ key: 'atk', label: 'Attack', desc: true },
|
|
||||||
{ key: 'def', label: 'Defense', desc: true },
|
|
||||||
{ key: 'spa', label: 'Sp. Attack', desc: true },
|
|
||||||
{ key: 'spd', label: 'Sp. Defense', desc: true },
|
|
||||||
{ key: 'spe', label: 'Speed', desc: true },
|
|
||||||
{ key: 'height', label: 'Height', desc: true },
|
|
||||||
{ key: 'weight', label: 'Weight', desc: true },
|
|
||||||
{ key: 'baseExp', label: 'Base EXP', desc: true },
|
|
||||||
{ key: 'catchrate', label: 'Catch rate', desc: true },
|
|
||||||
{ key: 'caught', label: 'Recently caught', desc: true },
|
|
||||||
];
|
|
||||||
const STAT_SHORT = { hp: 'HP', atk: 'Atk', def: 'Def', spa: 'SpA', spd: 'SpD', spe: 'Spe' };
|
|
||||||
|
|
||||||
const SEARCH_ICON =
|
const SEARCH_ICON =
|
||||||
'<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>';
|
'<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>';
|
||||||
|
|
||||||
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':
|
|
||||||
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 selection.get().pokemon[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':
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DexGrid() {
|
export async function DexGrid() {
|
||||||
const view = el('section', { class: 'view dexgrid' });
|
const view = el('section', { class: 'view dexgrid' });
|
||||||
const snap = await loadSnapshot();
|
const snap = await loadSnapshot();
|
||||||
|
|
||||||
const grid = el('div', { class: 'grid' });
|
const grid = el('div', { class: 'grid' });
|
||||||
let query = '';
|
let query = '';
|
||||||
let filterKey = ui.get().filter || 'all';
|
let filterKey = 'all';
|
||||||
let sortKey = ui.get().sort || 'dex';
|
|
||||||
let sortDesc = !!ui.get().sortDesc;
|
|
||||||
let rows = [];
|
let rows = [];
|
||||||
let ids = [];
|
let ids = [];
|
||||||
|
|
||||||
const ring = ProgressRing();
|
const ring = ProgressRing();
|
||||||
|
|
||||||
const title = el('h1', {});
|
const title = el('h1', {});
|
||||||
// Which game is active — a quiet label, not a control. Change it from the
|
const gamePill = el(
|
||||||
// Games button in the nav.
|
'button',
|
||||||
const gameLabel = el('span', { class: 'feed-head__game' });
|
{ class: 'game-pill', type: 'button', onclick: () => openGameSheet() },
|
||||||
|
el('span', {}),
|
||||||
|
el('span', { class: 'game-pill__chev', 'aria-hidden': 'true' }, '⌄'),
|
||||||
|
);
|
||||||
|
|
||||||
const search = el('input', {
|
const search = el('input', {
|
||||||
class: 'field-search__input',
|
class: 'field-search__input',
|
||||||
@ -101,11 +53,10 @@ export async function DexGrid() {
|
|||||||
const btn = el(
|
const btn = el(
|
||||||
'button',
|
'button',
|
||||||
{
|
{
|
||||||
class: `feed-chip${f.key === filterKey ? ' is-active' : ''}`,
|
class: 'feed-chip',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
filterKey = f.key;
|
filterKey = f.key;
|
||||||
ui.set({ filter: f.key });
|
|
||||||
chipEls.forEach((c, i) => c.btn.classList.toggle('is-active', FILTERS[i].key === f.key));
|
chipEls.forEach((c, i) => c.btn.classList.toggle('is-active', FILTERS[i].key === f.key));
|
||||||
paintGrid();
|
paintGrid();
|
||||||
},
|
},
|
||||||
@ -116,37 +67,7 @@ export async function DexGrid() {
|
|||||||
filterBar.append(btn);
|
filterBar.append(btn);
|
||||||
return { btn, count };
|
return { btn, count };
|
||||||
});
|
});
|
||||||
|
chipEls[0].btn.classList.add('is-active');
|
||||||
const sortSelect = el(
|
|
||||||
'select',
|
|
||||||
{
|
|
||||||
class: 'feed-sort__select',
|
|
||||||
onchange: (e) => {
|
|
||||||
sortKey = e.target.value;
|
|
||||||
sortDesc = SORTS.find((s) => s.key === sortKey).desc;
|
|
||||||
ui.set({ sort: sortKey, sortDesc });
|
|
||||||
syncDirBtn();
|
|
||||||
paintGrid();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...SORTS.map((s) => el('option', { value: s.key, selected: s.key === sortKey }, s.label)),
|
|
||||||
);
|
|
||||||
const dirBtn = el('button', {
|
|
||||||
class: 'feed-sort__dir',
|
|
||||||
type: 'button',
|
|
||||||
title: 'Reverse order',
|
|
||||||
onclick: () => {
|
|
||||||
sortDesc = !sortDesc;
|
|
||||||
ui.set({ sortDesc });
|
|
||||||
syncDirBtn();
|
|
||||||
paintGrid();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
function syncDirBtn() {
|
|
||||||
dirBtn.textContent = sortDesc ? '↓' : '↑';
|
|
||||||
dirBtn.setAttribute('aria-label', sortDesc ? 'Descending' : 'Ascending');
|
|
||||||
}
|
|
||||||
syncDirBtn();
|
|
||||||
|
|
||||||
view.append(
|
view.append(
|
||||||
el(
|
el(
|
||||||
@ -155,7 +76,7 @@ export async function DexGrid() {
|
|||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'feed-head__top' },
|
{ class: 'feed-head__top' },
|
||||||
el('div', { class: 'feed-head__id' }, title, gameLabel),
|
el('div', { class: 'feed-head__id' }, title, gamePill),
|
||||||
ring.node,
|
ring.node,
|
||||||
),
|
),
|
||||||
el(
|
el(
|
||||||
@ -165,13 +86,6 @@ export async function DexGrid() {
|
|||||||
search,
|
search,
|
||||||
),
|
),
|
||||||
filterBar,
|
filterBar,
|
||||||
el(
|
|
||||||
'div',
|
|
||||||
{ class: 'feed-sort' },
|
|
||||||
el('span', { class: 'feed-sort__label' }, 'Sort'),
|
|
||||||
sortSelect,
|
|
||||||
dirBtn,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
grid,
|
grid,
|
||||||
);
|
);
|
||||||
@ -180,14 +94,13 @@ export async function DexGrid() {
|
|||||||
const p = selection.get().pokemon;
|
const p = selection.get().pokemon;
|
||||||
let caught = 0;
|
let caught = 0;
|
||||||
let favorite = 0;
|
let favorite = 0;
|
||||||
let legendary = 0;
|
for (const id of ids) {
|
||||||
for (const { species } of rows) {
|
const e = p[id];
|
||||||
const e = p[species.id];
|
if (!e) continue;
|
||||||
if (e?.caught) caught++;
|
if (e.caught) caught++;
|
||||||
if (e?.favorite) favorite++;
|
if (e.favorite) favorite++;
|
||||||
if (species.isLegendary || species.isMythical) legendary++;
|
|
||||||
}
|
}
|
||||||
return { all: rows.length, caught, missing: rows.length - caught, favorite, legendary };
|
return { all: ids.length, caught, missing: ids.length - caught, favorite };
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshMeta() {
|
function refreshMeta() {
|
||||||
@ -201,49 +114,38 @@ export async function DexGrid() {
|
|||||||
function paintGrid() {
|
function paintGrid() {
|
||||||
const st = settings.get();
|
const st = settings.get();
|
||||||
const pokemonState = selection.get().pokemon;
|
const pokemonState = selection.get().pokemon;
|
||||||
|
// Gen 1–2 game-era sprites have opaque white backgrounds — frame them.
|
||||||
const boxed =
|
const boxed =
|
||||||
st.spriteStyle === 'game' &&
|
st.spriteStyle === 'game' &&
|
||||||
(snap.versionGroupByKey.get(st.versionGroup)?.generation ?? 9) <= 2;
|
(snap.versionGroupByKey.get(st.versionGroup)?.generation ?? 9) <= 2;
|
||||||
const filterTest = FILTERS.find((f) => f.key === filterKey).test;
|
const filterTest = FILTERS.find((f) => f.key === filterKey).test;
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
let shown = 0;
|
||||||
|
|
||||||
let list = rows.filter(({ species, number }) => {
|
for (const { species, number } of rows) {
|
||||||
if (
|
if (
|
||||||
query &&
|
query &&
|
||||||
!species.name.includes(query) &&
|
!species.name.includes(query) &&
|
||||||
String(number) !== query &&
|
String(number) !== query &&
|
||||||
String(species.id) !== query
|
String(species.id) !== query
|
||||||
) {
|
) {
|
||||||
return false;
|
continue;
|
||||||
}
|
}
|
||||||
const e = pokemonState[species.id] || { seen: false, caught: false, favorite: false };
|
const e = pokemonState[species.id] || { seen: false, caught: false, favorite: false };
|
||||||
return filterTest(e, species);
|
if (!filterTest(e)) continue;
|
||||||
});
|
|
||||||
|
|
||||||
const dir = sortDesc ? -1 : 1;
|
|
||||||
list = list
|
|
||||||
.map((row) => ({ row, v: sortValue(row, sortKey) }))
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (a.v < b.v) return -dir;
|
|
||||||
if (a.v > b.v) return dir;
|
|
||||||
return (a.row.number ?? a.row.species.id) - (b.row.number ?? b.row.species.id);
|
|
||||||
})
|
|
||||||
.map((x) => x.row);
|
|
||||||
|
|
||||||
const frag = document.createDocumentFragment();
|
|
||||||
list.forEach(({ species, number }, i) => {
|
|
||||||
const card = Card(species, number, {
|
const card = Card(species, number, {
|
||||||
spriteStyle: st.spriteStyle,
|
spriteStyle: st.spriteStyle,
|
||||||
versionGroup: st.versionGroup,
|
versionGroup: st.versionGroup,
|
||||||
boxed,
|
boxed,
|
||||||
metric: metricLabel(sortKey, species),
|
|
||||||
});
|
});
|
||||||
if (i < 30) card.style.setProperty('--i', String(i));
|
if (shown < 30) card.style.setProperty('--i', String(shown));
|
||||||
frag.append(card);
|
frag.append(card);
|
||||||
});
|
shown++;
|
||||||
|
}
|
||||||
|
|
||||||
clear(grid);
|
clear(grid);
|
||||||
grid.append(
|
grid.append(
|
||||||
list.length === 0
|
shown === 0
|
||||||
? el('p', { class: 'grid__empty' }, 'Nothing matches those filters.')
|
? el('p', { class: 'grid__empty' }, 'Nothing matches those filters.')
|
||||||
: frag,
|
: frag,
|
||||||
);
|
);
|
||||||
@ -256,7 +158,7 @@ export async function DexGrid() {
|
|||||||
ids = rows.map((r) => r.species.id);
|
ids = rows.map((r) => r.species.id);
|
||||||
|
|
||||||
title.textContent = prettify(dex.name || dex.key);
|
title.textContent = prettify(dex.name || dex.key);
|
||||||
gameLabel.textContent =
|
gamePill.firstChild.textContent =
|
||||||
st.versionGroup === 'all' ? 'All games' : prettify(st.versionGroup);
|
st.versionGroup === 'all' ? 'All games' : prettify(st.versionGroup);
|
||||||
refreshMeta();
|
refreshMeta();
|
||||||
paintGrid();
|
paintGrid();
|
||||||
@ -267,8 +169,7 @@ export async function DexGrid() {
|
|||||||
const offSettings = settings.subscribe(rebuild);
|
const offSettings = settings.subscribe(rebuild);
|
||||||
const offSelection = selection.subscribe(() => {
|
const offSelection = selection.subscribe(() => {
|
||||||
refreshMeta();
|
refreshMeta();
|
||||||
if (filterKey !== 'all' && filterKey !== 'legendary') paintGrid();
|
if (filterKey !== 'all') paintGrid();
|
||||||
else if (sortKey === 'caught') paintGrid();
|
|
||||||
});
|
});
|
||||||
onTeardown(view, () => {
|
onTeardown(view, () => {
|
||||||
offSettings();
|
offSettings();
|
||||||
|
|||||||
@ -300,7 +300,7 @@ export async function PokemonDetail(nationalId) {
|
|||||||
{ class: 'phero__top' },
|
{ class: 'phero__top' },
|
||||||
el('a', { class: 'phero__back', href: '#/' }, '‹ Dex'),
|
el('a', { class: 'phero__back', href: '#/' }, '‹ Dex'),
|
||||||
el('span', { class: 'phero__num' }, numLabel),
|
el('span', { class: 'phero__num' }, numLabel),
|
||||||
el('div', { class: 'phero__actions' }, shinyBtn, favBtn),
|
favBtn,
|
||||||
),
|
),
|
||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
@ -319,6 +319,7 @@ export async function PokemonDetail(nationalId) {
|
|||||||
? 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' }),
|
: el('span', { class: 'phero__nav' }),
|
||||||
),
|
),
|
||||||
|
shinyBtn,
|
||||||
),
|
),
|
||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
|
|||||||
@ -10,43 +10,11 @@ export async function SettingsView() {
|
|||||||
['system', 'System'],
|
['system', 'System'],
|
||||||
['light', 'Light'],
|
['light', 'Light'],
|
||||||
['dark', 'Dark'],
|
['dark', 'Dark'],
|
||||||
['black', 'Black (OLED)'],
|
|
||||||
['sepia', 'Sepia'],
|
|
||||||
], (v) => {
|
], (v) => {
|
||||||
settings.set({ theme: v });
|
settings.set({ theme: v });
|
||||||
applyTheme();
|
applyTheme(v);
|
||||||
});
|
});
|
||||||
|
|
||||||
const ACCENTS = [
|
|
||||||
['red', '#b3161a'],
|
|
||||||
['blue', '#2f6fed'],
|
|
||||||
['green', '#1f9d57'],
|
|
||||||
['amber', '#d98a00'],
|
|
||||||
['violet', '#7b52e0'],
|
|
||||||
['rose', '#e0417a'],
|
|
||||||
];
|
|
||||||
const accentRow = el(
|
|
||||||
'div',
|
|
||||||
{ class: 'swatches' },
|
|
||||||
...ACCENTS.map(([key, hex]) =>
|
|
||||||
el('button', {
|
|
||||||
class: `swatch${st.accent === key ? ' is-on' : ''}`,
|
|
||||||
type: 'button',
|
|
||||||
style: `--sw:${hex}`,
|
|
||||||
title: key,
|
|
||||||
'aria-label': `${key} accent`,
|
|
||||||
onclick: () => {
|
|
||||||
settings.set({ accent: key });
|
|
||||||
applyTheme();
|
|
||||||
[...accentRow.children].forEach((b, i) =>
|
|
||||||
b.classList.toggle('is-on', ACCENTS[i][0] === key),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const accentField = el('div', { class: 'field' }, el('span', {}, 'Accent colour'), accentRow);
|
|
||||||
|
|
||||||
const spriteField = selectField('Sprite style', st.spriteStyle, [
|
const spriteField = selectField('Sprite style', st.spriteStyle, [
|
||||||
['default', 'Pixel (modern)'],
|
['default', 'Pixel (modern)'],
|
||||||
['game', 'Game era (pixel art from the selected game)'],
|
['game', 'Game era (pixel art from the selected game)'],
|
||||||
@ -93,7 +61,7 @@ export async function SettingsView() {
|
|||||||
|
|
||||||
view.append(
|
view.append(
|
||||||
el('header', { class: 'view__header' }, el('h1', {}, 'Settings')),
|
el('header', { class: 'view__header' }, el('h1', {}, 'Settings')),
|
||||||
el('div', { class: 'settings__group' }, themeField, accentField, spriteField, shinyField),
|
el('div', { class: 'settings__group' }, themeField, spriteField, shinyField),
|
||||||
|
|
||||||
el('h2', {}, 'Your data'),
|
el('h2', {}, 'Your data'),
|
||||||
storageNote,
|
storageNote,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user