Modernise the dex feed; gate shiny to Gen 2+
- Feed header: large title, tappable game pill, circular progress ring, icon-in-field search, filter chips with live counts (All/Caught/Missing/Favorites) - Cards: left-aligned info, number chip, corner ghost number (no longer hidden behind the sprite), top-down type gradient, staggered entrance animation; caught toggle moved to top-left - Detail: hide the shiny toggle for Gen 1 games (shinies are Gen 2+); add Category (genus) to the About tab Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
b4f2460341
commit
9216a53932
@ -5,14 +5,14 @@ import { entry, toggle } from '../store/selection.js';
|
|||||||
import { typeHex } from '../lib/type-color.js';
|
import { typeHex } from '../lib/type-color.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One Pokémon in the dex grid. Tinted by its primary type: a soft gradient,
|
* One Pokémon in the dex feed. Tinted by its primary type: a soft top-down
|
||||||
* a type-coloured spotlight behind the sprite, and a big translucent number.
|
* gradient, a type-coloured spotlight behind an oversized sprite that lifts
|
||||||
* The card is a link to the detail route; the caught toggle is a real button
|
* above the card, a number chip, and a big ghost number in the corner.
|
||||||
* layered on top that updates itself so toggling never repaints the grid.
|
|
||||||
*/
|
*/
|
||||||
export function Card(species, number, { spriteStyle = 'official', versionGroup } = {}) {
|
export function Card(species, number, { spriteStyle = 'official', versionGroup } = {}) {
|
||||||
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 caughtBtn = el('button', {
|
const caughtBtn = el('button', {
|
||||||
class: 'card__caught',
|
class: 'card__caught',
|
||||||
@ -37,13 +37,21 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup }
|
|||||||
dataset: { type: mainType, sprite: spriteStyle },
|
dataset: { type: mainType, sprite: spriteStyle },
|
||||||
style: `--type-main:${typeHex(mainType)}`,
|
style: `--type-main:${typeHex(mainType)}`,
|
||||||
},
|
},
|
||||||
el('span', { class: 'card__bignum' }, String(number ?? species.id).padStart(3, '0')),
|
el('span', { class: 'card__ghost', 'aria-hidden': 'true' }, num),
|
||||||
el('span', { class: 'card__spot', 'aria-hidden': 'true' }),
|
el('span', { class: 'card__spot', 'aria-hidden': 'true' }),
|
||||||
Sprite(species.id, { style: spriteStyle, versionGroup, alt: species.name, size: 112 }),
|
el(
|
||||||
el('span', { class: 'card__num' }, `#${String(number ?? species.id).padStart(4, '0')}`),
|
'div',
|
||||||
|
{ class: 'card__art' },
|
||||||
|
Sprite(species.id, { style: spriteStyle, versionGroup, alt: species.name, size: 116 }),
|
||||||
|
),
|
||||||
|
caughtBtn,
|
||||||
|
el(
|
||||||
|
'div',
|
||||||
|
{ class: 'card__body' },
|
||||||
|
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)),
|
||||||
caughtBtn,
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
|
|||||||
48
src/components/ProgressRing.js
Normal file
48
src/components/ProgressRing.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { el } from '../lib/dom.js';
|
||||||
|
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
|
||||||
|
function circle(r, cls) {
|
||||||
|
const c = document.createElementNS(NS, 'circle');
|
||||||
|
c.setAttribute('cx', '26');
|
||||||
|
c.setAttribute('cy', '26');
|
||||||
|
c.setAttribute('r', String(r));
|
||||||
|
c.setAttribute('class', cls);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compact circular progress used in the dex feed header. Returns
|
||||||
|
* { node, update } — update({ caught, seen, total }) re-draws the arcs and
|
||||||
|
* the centre label without rebuilding.
|
||||||
|
*/
|
||||||
|
export function ProgressRing() {
|
||||||
|
const R = 22;
|
||||||
|
const CIRC = 2 * Math.PI * R;
|
||||||
|
|
||||||
|
const track = circle(R, 'ring__track');
|
||||||
|
const seenArc = circle(R, 'ring__seen');
|
||||||
|
const caughtArc = circle(R, 'ring__caught');
|
||||||
|
for (const arc of [seenArc, caughtArc]) {
|
||||||
|
arc.setAttribute('stroke-dasharray', String(CIRC));
|
||||||
|
arc.setAttribute('stroke-dashoffset', String(CIRC));
|
||||||
|
arc.setAttribute('transform', 'rotate(-90 26 26)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = document.createElementNS(NS, 'svg');
|
||||||
|
svg.setAttribute('viewBox', '0 0 52 52');
|
||||||
|
svg.setAttribute('class', 'ring__svg');
|
||||||
|
svg.append(track, seenArc, caughtArc);
|
||||||
|
|
||||||
|
const value = el('span', { class: 'ring__value' });
|
||||||
|
const node = el('div', { class: 'ring' }, svg, el('div', { class: 'ring__label' }, value));
|
||||||
|
|
||||||
|
function update({ caught = 0, seen = 0, total = 1 }) {
|
||||||
|
const safeTotal = Math.max(total, 1);
|
||||||
|
caughtArc.setAttribute('stroke-dashoffset', String(CIRC * (1 - caught / safeTotal)));
|
||||||
|
seenArc.setAttribute('stroke-dashoffset', String(CIRC * (1 - Math.max(seen, caught) / safeTotal)));
|
||||||
|
value.replaceChildren(String(caught), el('small', {}, `/${total}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { node, update };
|
||||||
|
}
|
||||||
@ -196,66 +196,162 @@
|
|||||||
.type-chip[data-type="steel"] { background: var(--type-steel); }
|
.type-chip[data-type="steel"] { background: var(--type-steel); }
|
||||||
.type-chip[data-type="fairy"] { background: var(--type-fairy); color: #3d1f3b; }
|
.type-chip[data-type="fairy"] { background: var(--type-fairy); color: #3d1f3b; }
|
||||||
|
|
||||||
/* ---------- Dex grid ---------------------------------------------- */
|
/* ---------- Dex feed header ------------------------------------- */
|
||||||
.progress-header {
|
.feed-head {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background: var(--bg);
|
margin: -8px 0 18px;
|
||||||
padding: 8px 0 12px;
|
padding: 14px 0 12px;
|
||||||
|
background: color-mix(in srgb, var(--bg) 82%, transparent);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
}
|
}
|
||||||
.progress-header__titles h1 {
|
.feed-head__top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.feed-head__id h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.7rem;
|
font-size: 1.9rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
line-height: 1.1;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
.progress-header__sub,
|
.game-pill {
|
||||||
.progress-header__counts {
|
display: inline-flex;
|
||||||
margin: 2px 0 0;
|
align-items: center;
|
||||||
color: var(--text-dim);
|
gap: 6px;
|
||||||
font-size: 0.9rem;
|
margin-top: 8px;
|
||||||
}
|
padding: 5px 8px 5px 12px;
|
||||||
.progress-header__dot {
|
|
||||||
margin: 0 6px;
|
|
||||||
}
|
|
||||||
.progress-header__track {
|
|
||||||
margin-top: 12px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
overflow: hidden;
|
color: var(--text-dim);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
.progress-header__bar {
|
.game-pill:hover {
|
||||||
height: 100%;
|
color: var(--text);
|
||||||
width: 0;
|
}
|
||||||
border-radius: 999px;
|
.game-pill__chev {
|
||||||
background: linear-gradient(90deg, var(--good), color-mix(in srgb, var(--good) 60%, #74c0fc));
|
font-size: 0.9rem;
|
||||||
transition: width 0.4s var(--ease-spring);
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dexgrid__controls {
|
.ring {
|
||||||
display: flex;
|
position: relative;
|
||||||
flex-direction: column;
|
width: 60px;
|
||||||
gap: 10px;
|
height: 60px;
|
||||||
margin: 8px 0 16px;
|
flex: none;
|
||||||
}
|
}
|
||||||
.dexgrid__search,
|
.ring__svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.ring__track {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--surface-2);
|
||||||
|
stroke-width: 5;
|
||||||
|
}
|
||||||
|
.ring__seen {
|
||||||
|
fill: none;
|
||||||
|
stroke: color-mix(in srgb, var(--good) 40%, var(--surface-2));
|
||||||
|
stroke-width: 5;
|
||||||
|
stroke-linecap: round;
|
||||||
|
transition: stroke-dashoffset 0.5s var(--ease-spring);
|
||||||
|
}
|
||||||
|
.ring__caught {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--good);
|
||||||
|
stroke-width: 5;
|
||||||
|
stroke-linecap: round;
|
||||||
|
transition: stroke-dashoffset 0.5s var(--ease-spring);
|
||||||
|
}
|
||||||
|
.ring__label {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.ring__value small {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-search {
|
||||||
|
position: relative;
|
||||||
|
margin: 14px 0 12px;
|
||||||
|
}
|
||||||
|
.field-search__icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.field-search__input,
|
||||||
.search__input {
|
.search__input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px 12px 44px;
|
||||||
border: 1.5px solid var(--border);
|
border: 1.5px solid var(--border);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
.dexgrid__search:focus,
|
.search__input {
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
.field-search__input:focus,
|
||||||
.search__input:focus {
|
.search__input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feed-chips {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.feed-chips::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.feed-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex: none;
|
||||||
|
appearance: none;
|
||||||
|
border: 1.5px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text-dim);
|
||||||
|
padding: 7px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.feed-chip.is-active {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent-text);
|
||||||
|
}
|
||||||
|
.feed-chip__count {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
.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));
|
||||||
@ -283,92 +379,109 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
padding: 16px 14px 14px;
|
||||||
padding: 16px 12px 14px;
|
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
165deg,
|
180deg,
|
||||||
color-mix(in srgb, var(--type-main) 26%, var(--surface)) 0%,
|
color-mix(in srgb, var(--type-main) 30%, var(--surface)) 0%,
|
||||||
var(--surface) 60%
|
color-mix(in srgb, var(--type-main) 9%, var(--surface)) 52%,
|
||||||
|
var(--surface) 100%
|
||||||
);
|
);
|
||||||
border: 1px solid color-mix(in srgb, var(--type-main) 22%, var(--border));
|
border: 1px solid color-mix(in srgb, var(--type-main) 20%, var(--border));
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
content-visibility: auto;
|
content-visibility: auto;
|
||||||
contain-intrinsic-size: 232px;
|
contain-intrinsic-size: auto 236px;
|
||||||
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);
|
||||||
}
|
}
|
||||||
.card > * {
|
@keyframes cardIn {
|
||||||
position: relative;
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
z-index: 1;
|
to { opacity: 1; transform: none; }
|
||||||
}
|
}
|
||||||
.card:hover {
|
.card:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
}
|
}
|
||||||
|
.card__ghost {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
right: 6px;
|
||||||
|
z-index: 0;
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: color-mix(in srgb, var(--type-main) 34%, transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.card__spot {
|
.card__spot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 30px;
|
top: 14px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
width: 118px;
|
width: 128px;
|
||||||
height: 118px;
|
height: 128px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
circle,
|
circle,
|
||||||
color-mix(in srgb, var(--type-main) 42%, transparent) 0%,
|
color-mix(in srgb, var(--type-main) 46%, transparent) 0%,
|
||||||
transparent 68%
|
transparent 66%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.card__bignum {
|
.card__art {
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: 74px;
|
z-index: 1;
|
||||||
left: 50%;
|
width: 120px;
|
||||||
transform: translate(-50%, -50%);
|
height: 120px;
|
||||||
z-index: 0;
|
|
||||||
font-size: 4.2rem;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
color: color-mix(in srgb, var(--type-main) 24%, transparent);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
.card .sprite {
|
.card__art .sprite {
|
||||||
width: 112px;
|
width: 100%;
|
||||||
height: 112px;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.22));
|
filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.28));
|
||||||
}
|
}
|
||||||
.card[data-sprite="default"] .sprite,
|
.card[data-sprite="default"] .card__art .sprite,
|
||||||
.card[data-sprite="game"] .sprite {
|
.card[data-sprite="game"] .card__art .sprite {
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
.card__body {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
align-self: stretch;
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
.card__num {
|
.card__num {
|
||||||
font-size: 0.72rem;
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-weight: 600;
|
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.card__name {
|
.card__name {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.95rem;
|
font-size: 0.98rem;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
text-align: center;
|
line-height: 1.15;
|
||||||
}
|
}
|
||||||
.card__types {
|
.card__types {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
.card__caught {
|
.card__caught {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
left: 10px;
|
||||||
z-index: 2;
|
z-index: 3;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@ -378,7 +491,7 @@
|
|||||||
place-items: center;
|
place-items: center;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
background: rgba(255, 255, 255, 0.72);
|
background: rgba(255, 255, 255, 0.75);
|
||||||
backdrop-filter: blur(3px);
|
backdrop-filter: blur(3px);
|
||||||
}
|
}
|
||||||
.card__caught[aria-pressed="true"] {
|
.card__caught[aria-pressed="true"] {
|
||||||
@ -394,6 +507,11 @@
|
|||||||
.card.is-caught {
|
.card.is-caught {
|
||||||
border-color: color-mix(in srgb, var(--good) 45%, var(--border));
|
border-color: color-mix(in srgb, var(--good) 45%, var(--border));
|
||||||
}
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.card {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Game picker ------------------------------------------- */
|
/* ---------- Game picker ------------------------------------------- */
|
||||||
.gamepicker__subdex {
|
.gamepicker__subdex {
|
||||||
|
|||||||
@ -4,15 +4,18 @@ import { resolvePokedex, dexRows, prettify } from '../data/pokedex-resolver.js';
|
|||||||
import { settings } from '../store/settings.js';
|
import { settings } from '../store/settings.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 { ProgressHeader } from '../components/ProgressHeader.js';
|
import { ProgressRing } from '../components/ProgressRing.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: 'uncaught', label: 'Not caught', 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 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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>';
|
||||||
|
|
||||||
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();
|
||||||
@ -20,9 +23,21 @@ export async function DexGrid() {
|
|||||||
const grid = el('div', { class: 'grid' });
|
const grid = el('div', { class: 'grid' });
|
||||||
let query = '';
|
let query = '';
|
||||||
let filterKey = 'all';
|
let filterKey = 'all';
|
||||||
|
let rows = [];
|
||||||
|
let ids = [];
|
||||||
|
|
||||||
|
const ring = ProgressRing();
|
||||||
|
|
||||||
|
const title = el('h1', {});
|
||||||
|
const gamePill = el(
|
||||||
|
'a',
|
||||||
|
{ class: 'game-pill', href: '#/games' },
|
||||||
|
el('span', {}),
|
||||||
|
el('span', { class: 'game-pill__chev', 'aria-hidden': 'true' }, '⌄'),
|
||||||
|
);
|
||||||
|
|
||||||
const search = el('input', {
|
const search = el('input', {
|
||||||
class: 'dexgrid__search',
|
class: 'field-search__input',
|
||||||
type: 'search',
|
type: 'search',
|
||||||
placeholder: 'Filter this Pokédex…',
|
placeholder: 'Filter this Pokédex…',
|
||||||
oninput: (e) => {
|
oninput: (e) => {
|
||||||
@ -31,37 +46,69 @@ export async function DexGrid() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterBar = el(
|
const filterBar = el('div', { class: 'feed-chips' });
|
||||||
'div',
|
const chipEls = FILTERS.map((f) => {
|
||||||
{ class: 'chips' },
|
const count = el('span', { class: 'feed-chip__count' });
|
||||||
...FILTERS.map((f) =>
|
const btn = el(
|
||||||
el(
|
|
||||||
'button',
|
'button',
|
||||||
{
|
{
|
||||||
class: `chip${f.key === filterKey ? ' is-active' : ''}`,
|
class: 'feed-chip',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
filterKey = f.key;
|
filterKey = f.key;
|
||||||
[...filterBar.children].forEach((c, i) =>
|
chipEls.forEach((c, i) => c.btn.classList.toggle('is-active', FILTERS[i].key === f.key));
|
||||||
c.classList.toggle('is-active', FILTERS[i].key === filterKey),
|
|
||||||
);
|
|
||||||
paintGrid();
|
paintGrid();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
f.label,
|
el('span', {}, f.label),
|
||||||
),
|
count,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
filterBar.append(btn);
|
||||||
|
return { btn, count };
|
||||||
|
});
|
||||||
|
chipEls[0].btn.classList.add('is-active');
|
||||||
|
|
||||||
const header = ProgressHeader({ title: '', subtitle: '' });
|
|
||||||
view.append(
|
view.append(
|
||||||
header.node,
|
el(
|
||||||
el('div', { class: 'dexgrid__controls' }, search, filterBar),
|
'header',
|
||||||
|
{ class: 'feed-head' },
|
||||||
|
el(
|
||||||
|
'div',
|
||||||
|
{ class: 'feed-head__top' },
|
||||||
|
el('div', { class: 'feed-head__id' }, title, gamePill),
|
||||||
|
ring.node,
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
'div',
|
||||||
|
{ class: 'field-search' },
|
||||||
|
el('span', { class: 'field-search__icon', html: SEARCH_ICON, 'aria-hidden': 'true' }),
|
||||||
|
search,
|
||||||
|
),
|
||||||
|
filterBar,
|
||||||
|
),
|
||||||
grid,
|
grid,
|
||||||
);
|
);
|
||||||
|
|
||||||
let rows = [];
|
function counts() {
|
||||||
let ids = [];
|
const p = selection.get().pokemon;
|
||||||
|
let caught = 0;
|
||||||
|
let favorite = 0;
|
||||||
|
for (const id of ids) {
|
||||||
|
const e = p[id];
|
||||||
|
if (!e) continue;
|
||||||
|
if (e.caught) caught++;
|
||||||
|
if (e.favorite) favorite++;
|
||||||
|
}
|
||||||
|
return { all: ids.length, caught, missing: ids.length - caught, favorite };
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshMeta() {
|
||||||
|
ring.update(stats(ids));
|
||||||
|
const c = counts();
|
||||||
|
chipEls.forEach(({ count }, i) => {
|
||||||
|
count.textContent = c[FILTERS[i].key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function paintGrid() {
|
function paintGrid() {
|
||||||
const st = settings.get();
|
const st = settings.get();
|
||||||
@ -79,27 +126,23 @@ export async function DexGrid() {
|
|||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const e = pokemonState[species.id] || {
|
const e = pokemonState[species.id] || { seen: false, caught: false, favorite: false };
|
||||||
seen: false,
|
|
||||||
caught: false,
|
|
||||||
favorite: false,
|
|
||||||
};
|
|
||||||
if (!filterTest(e)) continue;
|
if (!filterTest(e)) continue;
|
||||||
frag.append(
|
const card = Card(species, number, {
|
||||||
Card(species, number, {
|
|
||||||
spriteStyle: st.spriteStyle,
|
spriteStyle: st.spriteStyle,
|
||||||
versionGroup: st.versionGroup,
|
versionGroup: st.versionGroup,
|
||||||
}),
|
});
|
||||||
);
|
if (shown < 30) card.style.setProperty('--i', String(shown));
|
||||||
|
frag.append(card);
|
||||||
shown++;
|
shown++;
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(grid);
|
clear(grid);
|
||||||
if (shown === 0) {
|
grid.append(
|
||||||
grid.append(el('p', { class: 'grid__empty' }, 'Nothing matches those filters.'));
|
shown === 0
|
||||||
} else {
|
? el('p', { class: 'grid__empty' }, 'Nothing matches those filters.')
|
||||||
grid.append(frag);
|
: frag,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuild() {
|
function rebuild() {
|
||||||
@ -108,28 +151,19 @@ export async function DexGrid() {
|
|||||||
rows = dexRows(snap, dex);
|
rows = dexRows(snap, dex);
|
||||||
ids = rows.map((r) => r.species.id);
|
ids = rows.map((r) => r.species.id);
|
||||||
|
|
||||||
header.setTitles(
|
title.textContent = prettify(dex.name || dex.key);
|
||||||
prettify(dex.name || dex.key),
|
gamePill.firstChild.textContent = prettify(st.versionGroup);
|
||||||
el(
|
refreshMeta();
|
||||||
'span',
|
|
||||||
{},
|
|
||||||
`${prettify(st.versionGroup)} · `,
|
|
||||||
el('a', { href: '#/games', class: 'link' }, 'change game'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
header.update(stats(ids));
|
|
||||||
paintGrid();
|
paintGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
rebuild();
|
rebuild();
|
||||||
|
|
||||||
const offSettings = settings.subscribe(rebuild);
|
const offSettings = settings.subscribe(rebuild);
|
||||||
// Selection changes only affect counts + which cards pass the filter.
|
|
||||||
const offSelection = selection.subscribe(() => {
|
const offSelection = selection.subscribe(() => {
|
||||||
header.update(stats(ids));
|
refreshMeta();
|
||||||
if (filterKey !== 'all') paintGrid();
|
if (filterKey !== 'all') paintGrid();
|
||||||
});
|
});
|
||||||
|
|
||||||
onTeardown(view, () => {
|
onTeardown(view, () => {
|
||||||
offSettings();
|
offSettings();
|
||||||
offSelection();
|
offSelection();
|
||||||
|
|||||||
@ -77,11 +77,14 @@ export async function PokemonDetail(nationalId) {
|
|||||||
view.style.setProperty('--type-2', typeHex(types[1] || mainType));
|
view.style.setProperty('--type-2', typeHex(types[1] || mainType));
|
||||||
view.dataset.type = mainType;
|
view.dataset.type = mainType;
|
||||||
|
|
||||||
// ---- Artwork + shiny ----------------------------------------------
|
// ---- Artwork + shiny --------------------------------------------
|
||||||
|
// Shiny Pokémon were introduced in Gen 2 — no toggle for Gen 1 games.
|
||||||
|
const shinyAvailable = vgGen >= 2;
|
||||||
const artStyle = st.spriteStyle === 'default' ? 'official' : st.spriteStyle;
|
const artStyle = st.spriteStyle === 'default' ? 'official' : st.spriteStyle;
|
||||||
let shiny = st.showShiny;
|
let shiny = shinyAvailable && st.showShiny;
|
||||||
const artHolder = el('div', { class: 'phero__art' });
|
const artHolder = el('div', { class: 'phero__art' });
|
||||||
const shinyBtn = el('button', {
|
const shinyBtn = shinyAvailable
|
||||||
|
? el('button', {
|
||||||
class: 'phero__shiny',
|
class: 'phero__shiny',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
title: 'Toggle shiny',
|
title: 'Toggle shiny',
|
||||||
@ -89,7 +92,8 @@ export async function PokemonDetail(nationalId) {
|
|||||||
shiny = !shiny;
|
shiny = !shiny;
|
||||||
paintArt();
|
paintArt();
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
: null;
|
||||||
function paintArt() {
|
function paintArt() {
|
||||||
artHolder.replaceChildren(
|
artHolder.replaceChildren(
|
||||||
Sprite(nationalId, {
|
Sprite(nationalId, {
|
||||||
@ -100,9 +104,11 @@ export async function PokemonDetail(nationalId) {
|
|||||||
size: 320,
|
size: 320,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
if (shinyBtn) {
|
||||||
shinyBtn.textContent = shiny ? '✦ shiny' : '✧ shiny';
|
shinyBtn.textContent = shiny ? '✦ shiny' : '✧ shiny';
|
||||||
shinyBtn.classList.toggle('is-on', shiny);
|
shinyBtn.classList.toggle('is-on', shiny);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
paintArt();
|
paintArt();
|
||||||
|
|
||||||
// ---- Seen / caught / favorite -----------------------------------
|
// ---- Seen / caught / favorite -----------------------------------
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user