From c9247cd677f2c438c1355f449c177cae35115fc7 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 27 Aug 2026 11:54:30 -0400 Subject: [PATCH] Redesign detail page: type-themed hero + tabbed sheet - Poppins, larger radii, softer shadows, spring easing tokens - Detail page: full-bleed type-gradient hero with official artwork, Poke Ball + dex-number watermarks, prev/next, shiny toggle - Rounded sheet overlaps hero; tabs: About / Stats / Evolution / Moves / Locations; colored + animated stat bars - All prior data logic (era-accurate stats, gen-gated abilities, re-parented evolution, matchups, encounters) preserved Co-Authored-By: Claude Sonnet 5 --- src/styles/layout.css | 369 +++++++++++++++++++++++++++---------- src/styles/tokens.css | 13 +- src/views/PokemonDetail.js | 366 ++++++++++++++++++++---------------- 3 files changed, 490 insertions(+), 258 deletions(-) diff --git a/src/styles/layout.css b/src/styles/layout.css index b4fe524..fdb1e62 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -407,145 +407,320 @@ text-transform: capitalize; } -/* ---------- Detail ---------------------------------------------- */ -.detail { - max-width: 1100px; +/* ================= Detail: type-themed hero + tabbed sheet ========= */ +.pdetail { + --ink: #fff; + max-width: 960px; margin: 0 auto; } -.detail__nav { - display: flex; - justify-content: space-between; - align-items: baseline; - gap: 8px 12px; - flex-wrap: wrap; - margin-bottom: 8px; - font-size: 0.9rem; - text-transform: capitalize; -} -.detail__nav a { - max-width: 45vw; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.detail__num { - margin: 0; - color: var(--text-dim); - font-size: 0.85rem; - text-transform: capitalize; -} -.detail__name { - margin: 2px 0 8px; - font-size: 2rem; - text-transform: capitalize; -} -.detail__types { - display: flex; - gap: 6px; -} - -.detail__panels { - display: grid; - gap: 16px; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - align-items: start; - margin-top: 16px; -} -.detail__section { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px; - margin: 0; - min-width: 0; -} -.detail__section h2 { - font-size: 0.95rem; - margin: 0 0 12px; -} -.detail__section--wide { - grid-column: 1 / -1; -} -.detail__section--media { - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; -} - -.detail__media { - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; -} -.detail__media .sprite { - width: min(240px, 60vw); - height: min(240px, 60vw); - object-fit: contain; -} -.detail__track { - display: flex; - gap: 8px; - justify-content: center; - flex-wrap: wrap; -} .detail__muted { color: var(--text-dim); margin: 0; } -.toggle { + +/* ---- Hero -------------------------------------------------------- */ +.phero { + position: relative; + overflow: hidden; + border-radius: var(--radius-lg); + padding: 16px 20px 96px; + color: var(--ink); + isolation: isolate; +} +.phero__bg { + position: absolute; + inset: 0; + z-index: -2; + background: + radial-gradient(120% 80% at 80% 0%, color-mix(in srgb, var(--type-2) 55%, transparent), transparent 60%), + linear-gradient(160deg, + color-mix(in srgb, var(--type-main) 92%, #fff 8%) 0%, + color-mix(in srgb, var(--type-main) 62%, #000 38%) 100%); +} +.phero__ball { + position: absolute; + top: -70px; + right: -60px; + width: 260px; + height: 260px; + z-index: -1; + opacity: 0.1; + border: 20px solid #fff; + border-radius: 50%; +} +.phero__ball::before { + content: ""; + position: absolute; + left: -20px; + right: -20px; + top: 50%; + height: 20px; + transform: translateY(-50%); + background: #fff; +} +.phero__ball::after { + content: ""; + position: absolute; + left: 50%; + top: 50%; + width: 84px; + height: 84px; + transform: translate(-50%, -50%); + border: 20px solid #fff; + border-radius: 50%; +} +.phero__bignum { + position: absolute; + right: 16px; + bottom: 64px; + z-index: -1; + font-size: 5rem; + font-weight: 800; + letter-spacing: -0.02em; + color: rgba(255, 255, 255, 0.16); + font-variant-numeric: tabular-nums; +} +.phero__top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + font-weight: 600; +} +.phero__back, +.phero__num { + color: var(--ink); + text-decoration: none; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + font-size: 0.9rem; +} +.phero__num { + opacity: 0.85; + text-transform: capitalize; +} +.phero__head { + margin-top: 14px; +} +.phero__name { + margin: 0; + font-size: 2.2rem; + font-weight: 700; + text-transform: capitalize; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); +} +.phero__types { + display: flex; + gap: 8px; + margin-top: 8px; +} +.phero__types .type-chip { + background: rgba(255, 255, 255, 0.25); + backdrop-filter: blur(4px); + color: var(--ink); + padding: 4px 12px; + font-size: 0.72rem; +} +.phero__stage { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-top: 4px; +} +.phero__art { + width: min(300px, 64vw); + height: min(300px, 64vw); + display: grid; + place-items: center; + filter: drop-shadow(0 16px 24px rgba(0, 0, 0, 0.28)); + animation: pop 0.5s var(--ease-spring); +} +.phero__art img { + width: 100%; + height: 100%; + object-fit: contain; +} +@keyframes pop { + from { transform: scale(0.8); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} +.phero__nav { + width: 44px; + height: 44px; + flex: none; + display: grid; + place-items: center; + border-radius: 50%; + font-size: 1.4rem; + color: var(--ink); + text-decoration: none; + background: rgba(255, 255, 255, 0.16); +} +.phero__nav:hover { + background: rgba(255, 255, 255, 0.3); +} +.phero__shiny { + position: absolute; + right: 20px; + bottom: 108px; + border: none; + border-radius: 999px; + padding: 6px 14px; + font: inherit; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + color: var(--ink); + background: rgba(255, 255, 255, 0.18); +} +.phero__shiny.is-on { + background: #fff; + color: var(--type-main); +} + +/* ---- Sheet ----------------------------------------------------- */ +.psheet { + position: relative; + margin-top: -72px; + background: var(--surface); + border-radius: var(--radius-lg) var(--radius-lg) var(--radius) var(--radius); + box-shadow: var(--shadow-lg); + padding: 18px clamp(16px, 4vw, 32px) 28px; +} +.ptrack { + display: flex; + justify-content: center; + gap: 10px; + margin-bottom: 6px; +} +.ptrack__btn { appearance: none; - border: 1px solid var(--border); + border: 1.5px solid var(--border); background: var(--surface); color: var(--text-dim); - padding: 8px 16px; + padding: 7px 20px; border-radius: 999px; font: inherit; font-weight: 600; + font-size: 0.85rem; cursor: pointer; + transition: transform 0.12s var(--ease-spring); } -.toggle.is-on { +.ptrack__btn:active { + transform: scale(0.94); +} +.ptrack__btn.is-on { background: var(--good); border-color: var(--good); color: #fff; } -.detail__flavor { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 14px 16px; - font-style: italic; +.ptrack__btn--favorite { + flex: none; + padding: 6px 12px; + font-size: 1rem; + line-height: 1; + color: var(--ink); + border-color: rgba(255, 255, 255, 0.4); + background: rgba(255, 255, 255, 0.16); +} +.ptrack__btn--favorite.is-on { + background: #fff; + border-color: #fff; + color: #ff4d6d; +} +.psheet__tabs { + display: flex; + gap: 4px; + overflow-x: auto; + scrollbar-width: none; + border-bottom: 1px solid var(--border); + margin: 10px 0 18px; +} +.psheet__tabs::-webkit-scrollbar { + display: none; +} +.psheet__tab { + appearance: none; + border: none; + background: none; + font: inherit; + font-weight: 600; + font-size: 0.9rem; color: var(--text-dim); - max-width: 60ch; + padding: 10px 14px; + cursor: pointer; + white-space: nowrap; + border-bottom: 2.5px solid transparent; + margin-bottom: -1px; +} +.psheet__tab.is-active { + color: var(--type-main); + border-bottom-color: var(--type-main); +} +.psheet__body { + animation: fade 0.25s ease; +} +@keyframes fade { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: none; } +} +.ppanel__flavor { + font-size: 1rem; + line-height: 1.6; + margin: 0 0 18px; overflow-wrap: anywhere; } +.ppanel__sub { + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-dim); + margin: 24px 0 10px; +} +.pfacts { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 14px 18px; + margin: 0; +} +.pstats { + margin-bottom: 4px; +} .statbar { display: grid; - grid-template-columns: 44px 44px 1fr; + grid-template-columns: 46px 44px 1fr; align-items: center; - gap: 10px; - margin: 6px 0; + gap: 12px; + margin: 9px 0; font-size: 0.85rem; } .statbar__label { color: var(--text-dim); + font-weight: 600; } .statbar__value { font-variant-numeric: tabular-nums; text-align: right; + font-weight: 600; } .statbar__track { - height: 8px; + height: 10px; background: var(--surface-2); border-radius: 999px; overflow: hidden; } .statbar__fill { height: 100%; - background: var(--accent); + background: var(--type-main, var(--accent)); border-radius: 999px; + transition: width 0.7s var(--ease-spring); } +.statbar__fill[data-stat="hp"] { background: #ff6b6b; } +.statbar__fill[data-stat="attack"] { background: #ffa94d; } +.statbar__fill[data-stat="defense"] { background: #ffd43b; } +.statbar__fill[data-stat="special-attack"] { background: #74c0fc; } +.statbar__fill[data-stat="special-defense"] { background: #63e6be; } +.statbar__fill[data-stat="speed"] { background: #da77f2; } .statbar--total { font-weight: 700; border-top: 1px solid var(--border); diff --git a/src/styles/tokens.css b/src/styles/tokens.css index 66bb14a..c8e4efe 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -1,5 +1,7 @@ /* Design tokens. Light palette on :root; dark overrides via media query and an explicit [data-theme="dark"] so the in-app toggle wins both ways. */ +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap'); + :root { --bg: #f4f4f6; --surface: #ffffff; @@ -12,14 +14,17 @@ --good: #2f9e44; --danger: #c92a2a; - --radius: 14px; - --radius-sm: 9px; + --radius: 20px; + --radius-sm: 12px; + --radius-lg: 30px; --gap: 16px; --nav-size: 64px; --maxw: 1440px; - --shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 6px 20px rgba(0, 0, 0, 0.06); - --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 20px 50px -12px rgba(0, 0, 0, 0.28); + --font: "Poppins", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1); /* Pokémon type colors */ --type-normal: #9099a1; diff --git a/src/views/PokemonDetail.js b/src/views/PokemonDetail.js index 82a2c7a..e58cad3 100644 --- a/src/views/PokemonDetail.js +++ b/src/views/PokemonDetail.js @@ -23,14 +23,28 @@ const STAT_NAMES = [ 'speed', ]; +const NO_ENCOUNTER_DATA = new Set([ + 'scarlet-violet', + 'the-teal-mask', + 'the-indigo-disk', + 'brilliant-diamond-and-shining-pearl', + 'legends-arceus', +]); + +function typeHex(type) { + const v = getComputedStyle(document.documentElement) + .getPropertyValue(`--type-${type}`) + .trim(); + return v || '#8a8f98'; +} + export async function PokemonDetail(nationalId) { - const view = el('section', { class: 'view detail' }); + const view = el('section', { class: 'view pdetail' }); const snap = await loadSnapshot(); const st = settings.get(); const vg = snap.versionGroupByKey.get(st.versionGroup); const vgGen = vg ? vg.generation : 9; - // Prev / next within the currently selected dex. const dex = resolvePokedex(snap, st); const rows = dexRows(snap, dex); const pos = rows.findIndex((r) => r.species.id === nationalId); @@ -60,64 +74,74 @@ export async function PokemonDetail(nationalId) { return view; } - // ---- Artwork + shiny toggle ----------------------------------------- + const types = pokemon.types + .slice() + .sort((a, b) => a.slot - b.slot) + .map((t) => t.type.name); + const mainType = types[0]; + view.style.setProperty('--type-main', typeHex(mainType)); + view.style.setProperty('--type-2', typeHex(types[1] || mainType)); + view.dataset.type = mainType; + + // ---- Artwork + shiny ---------------------------------------------- const artStyle = st.spriteStyle === 'default' ? 'official' : st.spriteStyle; let shiny = st.showShiny; - const media = el('div', { class: 'detail__media' }); - + const artHolder = el('div', { class: 'phero__art' }); + const shinyBtn = el('button', { + class: 'phero__shiny', + type: 'button', + title: 'Toggle shiny', + onclick: () => { + shiny = !shiny; + paintArt(); + }, + }); function paintArt() { - media.replaceChildren( + artHolder.replaceChildren( Sprite(nationalId, { style: artStyle, shiny, versionGroup: st.versionGroup, alt: species.name, - size: 240, + size: 320, }), - el( - 'button', - { - class: 'button button--ghost', - type: 'button', - onclick: () => { - shiny = !shiny; - paintArt(); - }, - }, - shiny ? 'Show normal' : 'Show shiny', - ), ); + shinyBtn.textContent = shiny ? '✦ shiny' : '✧ shiny'; + shinyBtn.classList.toggle('is-on', shiny); } paintArt(); - // ---- Seen / caught / favorite (writes the unified selection) -------- - const trackBar = el('div', { class: 'detail__track' }); - function renderTrack() { - const e = entry(nationalId); - trackBar.replaceChildren( - toggleButton('seen', 'Seen', e.seen), - toggleButton('caught', 'Caught', e.caught), - toggleButton('favorite', '★ Favorite', e.favorite), - ); - } - function toggleButton(field, label, on) { - return el( - 'button', - { - class: `toggle${on ? ' is-on' : ''}`, - type: 'button', - 'aria-pressed': String(on), - onclick: () => { - toggle(nationalId, field); - renderTrack(); - }, + // ---- Seen / caught / favorite ----------------------------------- + const favBtn = trackToggle('favorite', '♥'); + const seenBtn = trackToggle('seen', 'Seen'); + const caughtBtn = trackToggle('caught', 'Caught'); + function trackToggle(field, label) { + const b = el('button', { + class: `ptrack__btn ptrack__btn--${field}`, + type: 'button', + 'aria-pressed': String(entry(nationalId)[field]), + onclick: () => { + toggle(nationalId, field); + syncTrack(); }, - label, - ); + }); + b.append(label); + return b; } - renderTrack(); + function syncTrack() { + for (const [field, b] of [ + ['favorite', favBtn], + ['seen', seenBtn], + ['caught', caughtBtn], + ]) { + const on = entry(nationalId)[field]; + b.setAttribute('aria-pressed', String(on)); + b.classList.toggle('is-on', on); + } + } + syncTrack(); - // ---- Flavor text for the selected game ----------------------------- + // ---- About panel ----------------------------------------------- const wantedVersions = new Set(vg ? vg.versions : []); const englishFlavor = species.flavor_text_entries.filter( (f) => f.language.name === 'en', @@ -126,31 +150,60 @@ export async function PokemonDetail(nationalId) { englishFlavor.find((f) => wantedVersions.has(f.version.name)) || englishFlavor[englishFlavor.length - 1]; - // ---- Stats -------------------------------------------------------- - const stat = (name) => - pokemon.stats.find((s) => s.stat.name === name)?.base_stat ?? 0; - const statTotal = STAT_NAMES.reduce((sum, n) => sum + stat(n), 0); - - // ---- Abilities: Gen 3+ only; hidden abilities Gen 5+ ------------- const abilities = pokemon.abilities .filter((a) => !a.is_hidden || vgGen >= 5) .map((a) => prettify(a.ability.name) + (a.is_hidden ? ' (hidden)' : '')); - // ---- Evolution section (filled in async) ------------------------- - const evoSection = el( - 'section', - { class: 'detail__section' }, - el('h2', {}, 'Evolution'), - el('p', { class: 'detail__muted' }, 'Loading…'), + const aboutPanel = el( + 'div', + { class: 'ppanel' }, + flavor + ? el('p', { class: 'ppanel__flavor' }, flavor.flavor_text.replace(/\s+/g, ' ')) + : null, + el( + 'dl', + { class: 'pfacts' }, + fact('Height', `${(pokemon.height / 10).toFixed(1)} m`), + fact('Weight', `${(pokemon.weight / 10).toFixed(1)} kg`), + abilities.length && vgGen >= 3 ? fact('Abilities', abilities.join(', ')) : null, + fact('Introduced', prettify(species.generation.name)), + species.genera?.length + ? fact( + 'Category', + (species.genera.find((g) => g.language.name === 'en') || {}).genus || '—', + ) + : null, + ), ); - loadEvolution(species, st, snap, vgGen).then((node) => { - evoSection.replaceChildren(el('h2', {}, 'Evolution'), node); - }); - const movesSection = el( - 'section', - { class: 'detail__section detail__section--wide' }, - el('h2', {}, `Moves — ${prettify(st.versionGroup)}`), + // ---- Stats panel --------------------------------------------- + const stat = (name) => + pokemon.stats.find((s) => s.stat.name === name)?.base_stat ?? 0; + const statTotal = STAT_NAMES.reduce((sum, n) => sum + stat(n), 0); + const statBars = STAT_NAMES.map((n) => StatBar(n, stat(n))); + const statsPanel = el( + 'div', + { class: 'ppanel' }, + el('div', { class: 'pstats' }, ...statBars), + el( + 'div', + { class: 'statbar statbar--total' }, + el('span', { class: 'statbar__label' }, 'Total'), + el('span', { class: 'statbar__value' }, String(statTotal)), + el('div', { class: 'statbar__track' }), + ), + el('h3', { class: 'ppanel__sub' }, 'Type matchups (defending)'), + TypeMatchups(types, vgGen), + ); + + // ---- Evolution panel -------------------------------------- + const evoPanel = el('div', { class: 'ppanel' }, el('p', { class: 'detail__muted' }, 'Loading…')); + loadEvolution(species, st, snap, vgGen).then((node) => evoPanel.replaceChildren(node)); + + // ---- Moves panel ---------------------------------------- + const movesPanel = el( + 'div', + { class: 'ppanel' }, MovesList(pokemon.moves, { versionGroupKey: st.versionGroup, gen: vgGen, @@ -158,118 +211,117 @@ export async function PokemonDetail(nationalId) { }), ); - // PokéAPI has no wild-encounter data for these games yet. - const NO_ENCOUNTER_DATA = new Set([ - 'scarlet-violet', - 'the-teal-mask', - 'the-indigo-disk', - 'brilliant-diamond-and-shining-pearl', - 'legends-arceus', - ]); - const locHeading = () => el('h2', {}, `Where to find — ${prettify(st.versionGroup)}`); - const locSection = el( - 'section', - { class: 'detail__section detail__section--wide' }, - locHeading(), - el('p', { class: 'detail__muted' }, 'Loading…'), - ); + // ---- Locations panel ---------------------------------- + const locPanel = el('div', { class: 'ppanel' }, el('p', { class: 'detail__muted' }, 'Loading…')); if (NO_ENCOUNTER_DATA.has(st.versionGroup)) { - locSection.replaceChildren( - locHeading(), + locPanel.replaceChildren( el('p', { class: 'detail__muted' }, "PokéAPI doesn't have wild-encounter data for this game yet."), ); } else { getEncounters(nationalId) - .then((data) => { - locSection.replaceChildren(locHeading(), Locations(data, vg ? vg.versions : [])); - }) - .catch(() => { - locSection.replaceChildren( - locHeading(), - el('p', { class: 'detail__muted' }, 'Location data unavailable offline.'), - ); - }); + .then((data) => locPanel.replaceChildren(Locations(data, vg ? vg.versions : []))) + .catch(() => + locPanel.replaceChildren(el('p', { class: 'detail__muted' }, 'Location data unavailable offline.')), + ); } - // ---- Assemble --------------------------------------------------- + // ---- Tabs ------------------------------------------- + const TABS = [ + { id: 'about', label: 'About', node: aboutPanel }, + { id: 'stats', label: 'Stats', node: statsPanel }, + { id: 'evo', label: 'Evolution', node: evoPanel }, + { id: 'moves', label: 'Moves', node: movesPanel }, + { id: 'loc', label: `Locations`, node: locPanel }, + ]; + const body = el('div', { class: 'psheet__body' }); + const tabButtons = TABS.map((t) => + el( + 'button', + { + class: 'psheet__tab', + type: 'button', + onclick: () => selectTab(t.id), + }, + t.label, + ), + ); + function selectTab(id) { + TABS.forEach((t, i) => { + const active = t.id === id; + tabButtons[i].classList.toggle('is-active', active); + tabButtons[i].setAttribute('aria-selected', String(active)); + }); + const tab = TABS.find((t) => t.id === id); + body.replaceChildren(tab.node); + if (id === 'stats') animateStats(); + } + + function animateStats() { + requestAnimationFrame(() => { + for (const el2 of statBars) { + const fill = el2.querySelector('.statbar__fill'); + if (!fill || fill.dataset.animated) continue; + const target = fill.style.width; + fill.style.width = '0%'; + requestAnimationFrame(() => { + fill.style.width = target; + fill.dataset.animated = '1'; + }); + } + }); + } + + // ---- Assemble ------------------------------------- + const numLabel = + regionalNumber != null + ? `#${String(regionalNumber).padStart(3, '0')} · ${prettify(dex.name || dex.key)}` + : `#${String(nationalId).padStart(4, '0')} · National`; + clear(view).append( el( - 'nav', - { class: 'detail__nav' }, - prev - ? el('a', { class: 'link', href: `#/pokemon/${prev.species.id}` }, `‹ ${prev.species.name}`) - : el('span'), - el('a', { class: 'link', href: '#/' }, 'All'), - next - ? el('a', { class: 'link', href: `#/pokemon/${next.species.id}` }, `${next.species.name} ›`) - : el('span'), - ), - el( - 'header', - { class: 'detail__header' }, - el( - 'p', - { class: 'detail__num' }, - regionalNumber != null - ? `#${String(regionalNumber).padStart(3, '0')} · ${prettify(dex.name || dex.key)}` - : `#${String(nationalId).padStart(4, '0')} · National`, - ), - el('h1', { class: 'detail__name' }, species.name.replace(/-/g, ' ')), + 'div', + { class: 'phero' }, + el('div', { class: 'phero__bg' }), + el('div', { class: 'phero__ball', 'aria-hidden': 'true' }), + el('span', { class: 'phero__bignum', 'aria-hidden': 'true' }, `#${String(nationalId).padStart(4, '0')}`), el( 'div', - { class: 'detail__types' }, - ...pokemon.types.map((t) => TypeChip(t.type.name)), + { class: 'phero__top' }, + el('a', { class: 'phero__back', href: '#/' }, '‹ Dex'), + el('span', { class: 'phero__num' }, numLabel), + favBtn, ), + el( + 'div', + { class: 'phero__head' }, + el('h1', { class: 'phero__name' }, species.name.replace(/-/g, ' ')), + el('div', { class: 'phero__types' }, ...types.map(TypeChip)), + ), + el( + 'div', + { class: 'phero__stage' }, + prev + ? el('a', { class: 'phero__nav phero__nav--prev', href: `#/pokemon/${prev.species.id}`, title: prev.species.name }, '‹') + : el('span', { class: 'phero__nav' }), + artHolder, + next + ? el('a', { class: 'phero__nav phero__nav--next', href: `#/pokemon/${next.species.id}`, title: next.species.name }, '›') + : el('span', { class: 'phero__nav' }), + ), + shinyBtn, ), - flavor - ? el('p', { class: 'detail__flavor' }, flavor.flavor_text.replace(/\s+/g, ' ')) - : null, el( 'div', - { class: 'detail__panels' }, - el('section', { class: 'detail__section detail__section--media' }, media, trackBar), - el( - 'section', - { class: 'detail__section' }, - el('h2', {}, 'Base stats'), - ...STAT_NAMES.map((n) => StatBar(n, stat(n))), - el( - 'div', - { class: 'statbar statbar--total' }, - el('span', { class: 'statbar__label' }, 'Total'), - el('span', { class: 'statbar__value' }, String(statTotal)), - el('div', { class: 'statbar__track' }), - ), - ), - el( - 'section', - { class: 'detail__section' }, - el('h2', {}, 'Details'), - el( - 'dl', - { class: 'detail__facts' }, - fact('Height', `${(pokemon.height / 10).toFixed(1)} m`), - fact('Weight', `${(pokemon.weight / 10).toFixed(1)} kg`), - abilities.length && vgGen >= 3 ? fact('Abilities', abilities.join(', ')) : null, - fact('Introduced', prettify(species.generation.name)), - ), - ), - el( - 'section', - { class: 'detail__section' }, - el('h2', {}, 'Type matchups (defending)'), - TypeMatchups( - pokemon.types.map((t) => t.type.name), - vgGen, - ), - ), - evoSection, - locSection, - movesSection, + { class: 'psheet' }, + el('div', { class: 'ptrack' }, seenBtn, caughtBtn), + el('div', { class: 'psheet__tabs', role: 'tablist' }, ...tabButtons), + body, ), ); - const off = settings.subscribe(() => renderTrack()); + selectTab('about'); + + const off = settings.subscribe(() => syncTrack()); onTeardown(view, off); return view; }