Progress: manage/reset played games; Lighthouse polish
Progress view "Your games" now has an Edit mode: tap a game to open a dialog to **reset** its caught/seen (keeps it listed) or **remove** it from the list (also clears its bucket). The currently-selected game can't be removed (it'd re-add on next launch) — reset only. New `clearGame(vgKey)` in the selection store. Lighthouse pass (headless, local): SEO 100, a11y 96, best-practices 96. - add public/robots.txt (SEO 92 → 100) - ProgressRing: keep the aria-label in sync with the visible count (fixes label-content-name-mismatch) - .type-chip: text-shadow for white-on-bright-type-colour legibility, 0.68 → 0.7rem Still open (design calls, see notes): type-chip colour contrast is ~2–3.7:1 for the lighter types (white on the canonical type colours); some sub-12px labels; performance is dominated by the ~1 MB snapshot and the ~1000-card grid. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
parent
3a54cdcc53
commit
edef6bf879
@ -125,7 +125,8 @@ selected game. Query, scroll, tab and filters persist.
|
|||||||
Every game's seen/caught tally against its own dex, from its own per-game
|
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
|
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
|
imported a save for); the rest sit behind a toggle. The National Dex row is
|
||||||
the union across all games.
|
the union across all games. **Edit** the "Your games" list to reset a
|
||||||
|
game's caught/seen or drop it from the list.
|
||||||
|
|
||||||
### Shiny hunt tracker (`#/shiny`)
|
### Shiny hunt tracker (`#/shiny`)
|
||||||
|
|
||||||
|
|||||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
@ -62,6 +62,9 @@ export function ProgressRing() {
|
|||||||
String(CIRC * (1 - Math.max(seen, caught) / safeTotal)),
|
String(CIRC * (1 - Math.max(seen, caught) / safeTotal)),
|
||||||
);
|
);
|
||||||
tail.textContent = `/${total}`;
|
tail.textContent = `/${total}`;
|
||||||
|
// Keep the accessible name in sync with the visible count (avoids an
|
||||||
|
// aria-label / visible-text mismatch).
|
||||||
|
node.setAttribute('aria-label', `Progress by game: ${caught} of ${total} caught`);
|
||||||
countUp(num, caught, { from: shown, duration: first ? 700 : 350 });
|
countUp(num, caught, { from: shown, duration: first ? 700 : 350 });
|
||||||
shown = caught;
|
shown = caught;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -139,6 +139,16 @@ export function replaceFlags({ seen = [], caught = [], game } = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Drop one game's seen/caught bucket entirely (its own progress reset). */
|
||||||
|
export function clearGame(vgKey) {
|
||||||
|
selection.set((s) => {
|
||||||
|
if (!s.games[vgKey]) return s;
|
||||||
|
const games = { ...s.games };
|
||||||
|
delete games[vgKey];
|
||||||
|
return { ...s, games };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Aggregate seen/caught over a species list for one game (union when 'all'). */
|
/** Aggregate seen/caught over a species list for one game (union when 'all'). */
|
||||||
export function stats(speciesIds, vgKey = curGame()) {
|
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;
|
||||||
|
|||||||
@ -221,10 +221,13 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 0.68rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
/* The bright type colours are light; a soft shadow keeps the white text
|
||||||
|
readable on fire / grass / electric etc. */
|
||||||
|
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
|
||||||
background: var(--type-normal);
|
background: var(--type-normal);
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
@ -3275,6 +3278,31 @@
|
|||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
margin: 12px 0 2px;
|
margin: 12px 0 2px;
|
||||||
}
|
}
|
||||||
|
.prog__genrow {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.prog__edit {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.prog__row--manage {
|
||||||
|
box-shadow: inset 0 0 0 1px var(--border);
|
||||||
|
}
|
||||||
|
.prog__manage {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
.prog__row {
|
.prog__row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 8.5rem 1fr 3.6rem;
|
grid-template-columns: 8.5rem 1fr 3.6rem;
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { el, clear, onTeardown } from '../lib/dom.js';
|
import { el, clear, onTeardown } from '../lib/dom.js';
|
||||||
import { loadSnapshot } from '../data/snapshot.js';
|
import { loadSnapshot } from '../data/snapshot.js';
|
||||||
import { settings } from '../store/settings.js';
|
import { settings } from '../store/settings.js';
|
||||||
import { selection, stats, statsNational } from '../store/selection.js';
|
import { selection, stats, statsNational, clearGame } from '../store/selection.js';
|
||||||
import { ui } from '../store/ui.js';
|
import { ui } from '../store/ui.js';
|
||||||
import { playedGames, isPlayed } from '../store/playedGames.js';
|
import { playedGames, isPlayed, unmarkPlayed } from '../store/playedGames.js';
|
||||||
import { dexesForVersionGroup, versionGroupsByGeneration } from '../data/pokedex-resolver.js';
|
import { dexesForVersionGroup, versionGroupsByGeneration } from '../data/pokedex-resolver.js';
|
||||||
|
import { chooseDialog } from '../lib/dialog.js';
|
||||||
|
|
||||||
function bar(seen, caught, total) {
|
function bar(seen, caught, total) {
|
||||||
const pc = total ? (caught / total) * 100 : 0;
|
const pc = total ? (caught / total) * 100 : 0;
|
||||||
@ -30,21 +31,56 @@ export async function ProgressView() {
|
|||||||
return [...ids];
|
return [...ids];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let editing = false;
|
||||||
|
|
||||||
|
async function manageGame(vg) {
|
||||||
|
const g = stats(speciesIdsFor(vg.key), vg.key);
|
||||||
|
const isActive = settings.get().versionGroup === vg.key;
|
||||||
|
const choices = [{ key: 'reset', label: 'Reset caught / seen for this game' }];
|
||||||
|
if (!isActive) {
|
||||||
|
choices.push({ key: 'remove', label: 'Remove from your games', class: 'button--danger' });
|
||||||
|
}
|
||||||
|
choices.push({ key: null, label: 'Cancel', class: 'button--ghost' });
|
||||||
|
|
||||||
|
const pick = await chooseDialog({
|
||||||
|
title: vg.name,
|
||||||
|
body:
|
||||||
|
g.caught || g.seen
|
||||||
|
? `${g.caught} caught · ${g.seen} seen tracked here.` +
|
||||||
|
(isActive ? ' This is your selected game.' : '')
|
||||||
|
: 'Nothing tracked here yet.' + (isActive ? ' This is your selected game.' : ''),
|
||||||
|
choices,
|
||||||
|
});
|
||||||
|
if (pick === 'reset') {
|
||||||
|
clearGame(vg.key);
|
||||||
|
render();
|
||||||
|
} else if (pick === 'remove') {
|
||||||
|
clearGame(vg.key);
|
||||||
|
unmarkPlayed(vg.key);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function gameRow(vg, { played }) {
|
function gameRow(vg, { played }) {
|
||||||
const s = stats(speciesIdsFor(vg.key), vg.key);
|
const s = stats(speciesIdsFor(vg.key), vg.key);
|
||||||
|
const manage = played && editing;
|
||||||
return el(
|
return el(
|
||||||
'button',
|
'button',
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
class: `prog__row${played ? ' prog__row--played' : ''}`,
|
class: `prog__row${played ? ' prog__row--played' : ''}${manage ? ' prog__row--manage' : ''}`,
|
||||||
onclick: () => {
|
onclick: manage
|
||||||
settings.set({ versionGroup: vg.key, pokedex: null });
|
? () => manageGame(vg)
|
||||||
location.hash = '#/';
|
: () => {
|
||||||
},
|
settings.set({ versionGroup: vg.key, pokedex: null });
|
||||||
|
location.hash = '#/';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
el('span', { class: 'prog__name' }, vg.name),
|
el('span', { class: 'prog__name' }, vg.name),
|
||||||
bar(s.seen, s.caught, s.total),
|
bar(s.seen, s.caught, s.total),
|
||||||
el('span', { class: 'prog__count' }, `${s.caught} / ${s.total}`),
|
manage
|
||||||
|
? el('span', { class: 'prog__manage', 'aria-hidden': 'true' }, '⋯')
|
||||||
|
: el('span', { class: 'prog__count' }, `${s.caught} / ${s.total}`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,12 +108,32 @@ export async function ProgressView() {
|
|||||||
|
|
||||||
// Games you've played (imported a save for, or opened at least once).
|
// Games you've played (imported a save for, or opened at least once).
|
||||||
if (anyPlayed) {
|
if (anyPlayed) {
|
||||||
list.append(el('h3', { class: 'prog__gen' }, 'Your games'));
|
list.append(
|
||||||
|
el(
|
||||||
|
'div',
|
||||||
|
{ class: 'prog__genrow' },
|
||||||
|
el('h3', { class: 'prog__gen' }, 'Your games'),
|
||||||
|
el(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: 'prog__edit',
|
||||||
|
onclick: () => {
|
||||||
|
editing = !editing;
|
||||||
|
render();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editing ? 'Done' : 'Edit',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
for (const { versionGroups } of gens) {
|
for (const { versionGroups } of gens) {
|
||||||
for (const vg of versionGroups) {
|
for (const vg of versionGroups) {
|
||||||
if (isPlayed(vg.key)) list.append(gameRow(vg, { played: true }));
|
if (isPlayed(vg.key)) list.append(gameRow(vg, { played: true }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
editing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything else — behind a toggle, since a single unified caught
|
// Everything else — behind a toggle, since a single unified caught
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user