SOURCE / PINNED RELEASE
Made of little things.
Pokédex
- Release
- 6ada05c191e1…
- Author-recorded commit
- Not provided
- License
- Check the original source
The hash-verified playable HTML, which may contain bundled or minified code. This view does not execute it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pokédex</title>
<meta name="description" content="Browse all 1025 Pokémon on your desktop: artwork, types, Pokédex entries and base stats, from PokéAPI.">
<meta name="napplet-id" content="pokedex">
<meta name="napplet-requires" content="resource storage">
<link rel="icon" href="data:image/png;base64,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">
<style>:root {
--red: #dc0a2d;
--red-dark: #8f0019;
--type: #a8a77a;
font-family: ui-rounded, 'SF Pro Rounded', 'Nunito', 'Inter Variable', ui-sans-serif, system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
height: 100%;
overflow: hidden;
background: transparent;
user-select: none;
}
button,
input {
font: inherit;
color: inherit;
}
.dex {
position: relative;
height: 100%;
padding: 12px;
border-radius: 24px;
background:
radial-gradient(120% 80% at 20% 0%, rgb(255 255 255 / 0.22), transparent 60%),
linear-gradient(160deg, #ef2b46, var(--red) 45%, #b0001f);
box-shadow: inset 0 0 0 2px rgb(0 0 0 / 0.18), inset 0 2px 0 rgb(255 255 255 / 0.25);
color: #fff;
display: grid;
gap: 10px;
}
/* The lens and the three little lights. */
.lights {
grid-area: lights;
display: flex;
align-items: center;
gap: 7px;
}
.lens {
width: 30px;
height: 30px;
border-radius: 50%;
border: 3px solid #fff;
background: radial-gradient(circle at 35% 30%, #c9f3ff, #29a8e0 35%, #0a5d9c 80%);
box-shadow: 0 0 0 2px rgb(0 0 0 / 0.25), 0 0 12px rgb(41 168 224 / 0.6);
margin-right: 4px;
}
.dex.loading .lens {
animation: blink 0.7s ease-in-out infinite alternate;
}
@keyframes blink {
to {
filter: brightness(1.6);
box-shadow: 0 0 0 2px rgb(0 0 0 / 0.25), 0 0 22px rgb(140 220 255 / 0.9);
}
}
.led {
width: 10px;
height: 10px;
border-radius: 50%;
box-shadow: inset 0 -2px 2px rgb(0 0 0 / 0.3), 0 0 0 1px rgb(0 0 0 / 0.3);
}
.led.red { background: #ff5964; }
.led.yellow { background: #ffd23f; }
.led.green { background: #5ce35c; }
/* The screen: a white bezel around the artwork. */
.screen {
grid-area: screen;
min-height: 0;
min-width: 0;
padding: 7px;
border-radius: 14px 14px 14px 26px;
background: #e8e8e8;
box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.15), 0 2px 0 rgb(0 0 0 / 0.2);
}
.glass {
position: relative;
height: 100%;
border-radius: 8px;
overflow: hidden;
background:
radial-gradient(circle at 50% 60%, color-mix(in srgb, var(--type) 55%, #fff) 0%, color-mix(in srgb, var(--type) 80%, #1a1a1a) 100%);
box-shadow: inset 0 0 0 2px rgb(0 0 0 / 0.25);
cursor: pointer;
transition: background 0.4s;
}
#sprite {
position: absolute;
inset: 6%;
width: 88%;
height: 88%;
object-fit: contain;
filter: drop-shadow(0 4px 6px rgb(0 0 0 / 0.35));
opacity: 0;
transform: scale(0.9);
transition: opacity 0.25s, transform 0.25s;
-webkit-user-drag: none;
}
#sprite.shown {
opacity: 1;
transform: none;
}
.status {
position: absolute;
inset: 0;
display: grid;
place-items: center;
padding: 8px;
text-align: center;
font-size: 12px;
font-weight: 600;
color: rgb(255 255 255 / 0.9);
text-shadow: 0 1px 2px rgb(0 0 0 / 0.5);
}
.shiny {
position: absolute;
top: 6px;
right: 6px;
width: 24px;
height: 24px;
padding: 0;
border: 0;
border-radius: 50%;
background: rgb(0 0 0 / 0.25);
color: rgb(255 255 255 / 0.7);
font-size: 13px;
line-height: 24px;
cursor: pointer;
}
.shiny[aria-pressed='true'] {
background: #ffd23f;
color: #7a5200;
box-shadow: 0 0 10px #ffd23f;
}
/* The caption on the screen, for the small size. */
.tag {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 14px 8px 6px;
background: linear-gradient(transparent, rgb(0 0 0 / 0.55));
font-size: 11px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0 1px 2px rgb(0 0 0 / 0.5);
}
.tag span {
opacity: 0.8;
font-variant-numeric: tabular-nums;
}
/* Name, types, entry and stats. */
.info {
grid-area: info;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: 6px;
overflow: hidden;
}
header {
line-height: 1.1;
}
.num {
font-size: 11px;
font-weight: 700;
opacity: 0.75;
font-variant-numeric: tabular-nums;
letter-spacing: 0.04em;
}
h1 {
margin: 1px 0 0;
font-size: 20px;
font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0 1px 0 var(--red-dark);
}
.genus {
display: block;
font-size: 11px;
opacity: 0.8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.types {
display: flex;
align-items: center;
gap: 6px;
}
#types {
display: flex;
gap: 4px;
}
.measure {
font-size: 10px;
font-weight: 600;
opacity: 0.8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.type {
padding: 2px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
text-shadow: 0 1px 1px rgb(0 0 0 / 0.35);
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.35), 0 1px 0 rgb(0 0 0 / 0.25);
}
.flavor {
margin: 0;
padding: 7px 9px;
border-radius: 8px;
background: #1f2a1f;
color: #b5f5a8;
font-family: ui-monospace, 'SF Mono', 'JetBrains Mono', monospace;
font-size: 10.5px;
line-height: 1.35;
box-shadow: inset 0 0 0 2px rgb(0 0 0 / 0.4);
flex: 0 1 auto;
min-height: 2.6em;
overflow-y: auto;
user-select: text;
mask-image: linear-gradient(#000 calc(100% - 9px), transparent calc(100% - 2px));
}
.flavor:empty {
display: none;
}
.stats {
grid-area: stats;
margin: 0;
display: grid;
grid-template-columns: auto 26px 1fr;
align-items: center;
gap: 2px 6px;
font-size: 10px;
font-weight: 700;
align-content: center;
}
.stats dt {
opacity: 0.85;
letter-spacing: 0.04em;
}
.stats dd {
margin: 0;
text-align: right;
font-variant-numeric: tabular-nums;
}
.bar {
height: 6px;
border-radius: 3px;
background: rgb(0 0 0 / 0.25);
overflow: hidden;
}
.bar i {
display: block;
height: 100%;
border-radius: 3px;
background: linear-gradient(90deg, #ffd23f, #5ce35c);
transition: width 0.4s;
}
/* Search and the d-pad. */
.search {
grid-area: search;
margin: 0;
min-width: 0;
}
.search input {
width: 100%;
height: 30px;
padding: 0 10px;
border: 0;
border-radius: 8px;
background: #1f2a1f;
color: #b5f5a8;
font-family: ui-monospace, 'SF Mono', 'JetBrains Mono', monospace;
font-size: 12px;
box-shadow: inset 0 0 0 2px rgb(0 0 0 / 0.4);
outline: none;
user-select: text;
}
.search input::placeholder {
color: rgb(181 245 168 / 0.5);
}
.search input:focus {
box-shadow: inset 0 0 0 2px #5ce35c;
}
.search.miss input {
animation: shake 0.3s;
}
@keyframes shake {
25% { transform: translateX(-4px); }
75% { transform: translateX(4px); }
}
.controls {
grid-area: controls;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 6px;
}
.controls button {
width: 30px;
height: 30px;
padding: 0;
border: 0;
border-radius: 8px;
background: #2b2b2b;
color: #eee;
font-size: 11px;
cursor: pointer;
box-shadow: 0 2px 0 #000, inset 0 1px 0 rgb(255 255 255 / 0.15);
}
.controls button:active {
transform: translateY(2px);
box-shadow: 0 0 0 #000;
}
#random {
border-radius: 50%;
display: grid;
place-items: center;
}
.ball {
width: 18px;
height: 18px;
border-radius: 50%;
background: linear-gradient(#ef2b46 0 45%, #111 45% 55%, #fff 55%);
box-shadow: 0 0 0 1.5px #111;
position: relative;
}
.ball::after {
content: '';
position: absolute;
inset: 6px;
border-radius: 50%;
background: #fff;
box-shadow: 0 0 0 1.5px #111;
}
.dex.loading .ball {
animation: wobble 0.6s ease-in-out infinite;
}
@keyframes wobble {
25% { transform: rotate(-20deg); }
75% { transform: rotate(20deg); }
}
button:focus-visible,
.glass:focus-visible {
outline: 2px solid #fff;
outline-offset: 2px;
}
/* Small: only the screen, with the controls over it on hover. */
body[data-shape='small'] .dex {
padding: 8px;
grid-template: 'screen' 1fr / 1fr;
}
body[data-shape='small'] .screen {
padding: 5px;
}
body[data-shape='small'] :is(.lights, .info, .search, .stats) {
display: none;
}
body[data-shape='small'] .controls {
position: absolute;
left: 0;
right: 0;
top: 50%;
translate: 0 -50%;
justify-content: space-between;
padding: 0 14px;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
}
body[data-shape='small'] .controls button {
pointer-events: auto;
width: 24px;
height: 24px;
opacity: 0.85;
}
body[data-shape='small'] #random {
visibility: hidden;
}
body[data-shape='small'] .dex:hover .controls {
opacity: 1;
}
body[data-shape='small'] .shiny {
display: none;
}
body:not([data-shape='small']) .tag {
display: none;
}
/* Wide: the screen, then the entry and the d-pad. */
body[data-shape='wide'] .dex {
grid-template:
'screen info' 1fr
'screen controls' auto
/ auto 1fr;
}
body[data-shape='wide'] .screen {
aspect-ratio: 1;
height: 100%;
}
body[data-shape='wide'] :is(.lights, .search, .stats) {
display: none;
}
body[data-shape='wide'] .genus {
display: none;
}
body[data-shape='wide'] .flavor {
padding: 5px 8px;
}
body[data-shape='wide'] .controls button {
width: 26px;
height: 26px;
}
/* Large: all of it. */
body[data-shape='large'] .dex {
grid-template:
'lights lights' auto
'screen info' minmax(0, 1.25fr)
'stats stats' minmax(0, 1fr)
'search controls' auto
/ 1fr 1fr;
}
/* Tall: stacked. */
body[data-shape='tall'] .dex {
grid-template:
'lights' auto
'screen' minmax(0, 1fr)
'info' minmax(0, 1fr)
'search' auto
'controls' auto
/ 1fr;
}
body[data-shape='tall'] .stats {
display: none;
}
body[data-shape='tall'] .controls {
justify-content: center;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition: none !important;
animation: none !important;
}
}
</style>
</head>
<body>
<main class="dex">
<div class="lights" aria-hidden="true">
<span class="lens"></span>
<span class="led red"></span>
<span class="led yellow"></span>
<span class="led green"></span>
</div>
<div class="screen">
<div class="glass" id="glass">
<img id="sprite" alt="">
<div class="status" id="status"></div>
<button class="shiny" id="shiny" title="Shiny" aria-pressed="false">✦</button>
<div class="tag"><span id="num">No. ---</span> <b id="name-small"></b></div>
</div>
</div>
<section class="info">
<header>
<span class="num" id="num-big">No. ---</span>
<h1 id="name">Pokédex</h1>
<span class="genus" id="genus"></span>
</header>
<div class="types"><span id="types"></span><span class="measure" id="measure"></span></div>
<p class="flavor" id="flavor"></p>
</section>
<dl class="stats" id="stats"></dl>
<form class="search" id="search" autocomplete="off">
<input id="query" type="search" placeholder="Name or number" aria-label="Find a Pokémon" spellcheck="false">
</form>
<nav class="controls">
<button id="prev" title="Previous" aria-label="Previous">◀</button>
<button id="random" title="Random" aria-label="Random"><span class="ball"></span></button>
<button id="next" title="Next" aria-label="Next">▶</button>
</nav>
</main>
<script>(() => {
const napplet = window.napplet ?? {};
const API = 'https://pokeapi.co/api/v2';
const ART = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork';
const COUNT = 1025;
const TYPES = {
normal: '#a8a77a', fire: '#ee8130', water: '#6390f0', electric: '#f7d02c',
grass: '#7ac74c', ice: '#96d9d6', fighting: '#c22e28', poison: '#a33ea1',
ground: '#e2bf65', flying: '#a98ff3', psychic: '#f95587', bug: '#a6b91a',
rock: '#b6a136', ghost: '#735797', dragon: '#6f35fc', dark: '#705746',
steel: '#b7b7ce', fairy: '#d685ad',
};
const STATS = {
hp: 'HP', attack: 'ATK', defense: 'DEF',
'special-attack': 'SpA', 'special-defense': 'SpD', speed: 'SPE',
};
const $ = (id) => document.getElementById(id);
const dex = document.querySelector('.dex');
const glass = $('glass');
const sprite = $('sprite');
const status = $('status');
const shinyButton = $('shiny');
const search = $('search');
const query = $('query');
let current = null;
let shiny = false;
let wanted = 0;
let spriteUrl = null;
let names = null;
// Storage, per copy of the widget, as JSON.
const store = {
async get(key, fallback) {
try {
const raw = await napplet.storage?.instance.getItem(key);
return raw == null ? fallback : JSON.parse(raw);
} catch {
return fallback;
}
},
set(key, value) {
napplet.storage?.instance.setItem(key, JSON.stringify(value)).catch(() => {});
},
};
async function bytes(url) {
if (!napplet.resource) throw new Error('offline');
return napplet.resource.bytes(url);
}
async function json(url) {
return JSON.parse(await (await bytes(url)).text());
}
const english = (list) => list.filter((e) => e.language.name === 'en');
const pad = (n) => String(n).padStart(4, '0');
const titleCase = (s) => s.replace(/(^|-)(\w)/g, (_, d, c) => (d ? ' ' : '') + c.toUpperCase());
async function fetchPokemon(id) {
const [p, s] = await Promise.all([json(`${API}/pokemon/${id}`), json(`${API}/pokemon-species/${id}`)]);
const flavor = english(s.flavor_text_entries).pop()?.flavor_text ?? '';
return {
id,
name: english(s.names)[0]?.name ?? titleCase(s.name),
genus: english(s.genera)[0]?.genus ?? '',
types: p.types.map((t) => t.type.name),
flavor: flavor.replace(/[\f\n\r\u00ad]+/g, ' ').replace(/\s+/g, ' ').trim(),
stats: p.stats.map((st) => [st.stat.name, st.base_stat]),
height: p.height / 10,
weight: p.weight / 10,
};
}
async function loadSprite(id) {
sprite.classList.remove('shown');
try {
const blob = await bytes(`${ART}${shiny ? '/shiny' : ''}/${id}.png`);
if (current?.id !== id) return;
if (spriteUrl) URL.revokeObjectURL(spriteUrl);
spriteUrl = URL.createObjectURL(blob);
sprite.src = spriteUrl;
sprite.alt = current.name;
await sprite.decode().catch(() => {});
sprite.classList.add('shown');
} catch {
if (current?.id === id) status.textContent = '?';
}
}
function render(p) {
const num = `No. ${pad(p.id)}`;
$('num').textContent = num;
$('num-big').textContent = num;
$('name').textContent = p.name;
$('name-small').textContent = p.name;
$('genus').textContent = p.genus;
$('measure').textContent = `${p.height} m · ${p.weight} kg`;
$('flavor').textContent = p.flavor;
$('types').replaceChildren(...p.types.map((t) => {
const el = document.createElement('span');
el.className = 'type';
el.textContent = t;
el.style.background = TYPES[t] ?? '#888';
return el;
}));
$('stats').replaceChildren(...p.stats.flatMap(([name, value]) => {
const dt = document.createElement('dt');
dt.textContent = STATS[name] ?? name;
const dd = document.createElement('dd');
dd.textContent = value;
const bar = document.createElement('div');
bar.className = 'bar';
const fill = document.createElement('i');
fill.style.width = `${Math.min(100, (value / 180) * 100)}%`;
bar.append(fill);
return [dt, dd, bar];
}));
glass.style.setProperty('--type', TYPES[p.types[0]] ?? '#a8a77a');
glass.title = `${p.name}: click for a random Pokémon`;
}
async function show(id) {
id = ((id - 1 + COUNT) % COUNT) + 1;
wanted = id;
dex.classList.add('loading');
status.textContent = '';
try {
const p = await fetchPokemon(id);
if (wanted !== id) return;
current = p;
render(p);
store.set('pokemon', p);
loadSprite(id);
} catch (e) {
if (wanted !== id) return;
status.textContent = e.message === 'offline' ? 'Turn on Internet access in About This Widget.' : 'Couldn’t reach the Pokédex.';
sprite.classList.remove('shown');
} finally {
if (wanted === id) dex.classList.remove('loading');
}
}
const random = () => {
let id;
do id = 1 + Math.floor(Math.random() * COUNT);
while (id === current?.id);
show(id);
};
// The species list, for finding a Pokémon by part of its name.
async function speciesNames() {
if (names) return names;
const cached = await napplet.storage?.getItem('species').catch(() => null);
if (cached) return (names = JSON.parse(cached));
const data = await json(`${API}/pokemon-species?limit=${COUNT}`);
names = data.results.map((r) => [Number(r.url.match(/\/(\d+)\/?$/)[1]), r.name]);
napplet.storage?.setItem('species', JSON.stringify(names)).catch(() => {});
return names;
}
async function find(text) {
const q = text.trim().toLowerCase().replace(/^#|^no\.?\s*/, '').replace(/[\s.'’:]+/g, '-').replace(/-+$/, '');
if (!q) return null;
if (/^\d+$/.test(q)) {
const n = Number(q);
return n >= 1 && n <= COUNT ? n : null;
}
const list = await speciesNames().catch(() => []);
const hit = list.find(([, n]) => n === q) ?? list.find(([, n]) => n.startsWith(q)) ?? list.find(([, n]) => n.includes(q));
return hit?.[0] ?? null;
}
search.addEventListener('submit', async (e) => {
e.preventDefault();
const id = await find(query.value);
if (id) {
query.value = '';
query.blur();
show(id);
} else {
search.classList.remove('miss');
void search.offsetWidth;
search.classList.add('miss');
}
});
query.addEventListener('focus', () => void speciesNames().catch(() => {}), { once: true });
$('prev').addEventListener('click', () => show((current?.id ?? wanted) - 1));
$('next').addEventListener('click', () => show((current?.id ?? wanted) + 1));
$('random').addEventListener('click', random);
glass.addEventListener('click', (e) => {
if (e.target !== shinyButton) random();
});
shinyButton.addEventListener('click', () => {
shiny = !shiny;
shinyButton.setAttribute('aria-pressed', String(shiny));
store.set('shiny', shiny);
if (current) loadSprite(current.id);
});
document.addEventListener('keydown', (e) => {
if (e.target === query) {
if (e.key === 'Escape') query.blur();
return;
}
if (e.key === 'ArrowLeft') $('prev').click();
else if (e.key === 'ArrowRight') $('next').click();
else if (e.key === 'r' || e.key === ' ') random();
else if (e.key === 's') shinyButton.click();
else if (e.key === '/') {
e.preventDefault();
query.focus();
}
});
// Small, wide, large or tall, from the box the widget is in.
function shape() {
const w = innerWidth;
const h = innerHeight;
document.body.dataset.shape =
w > h * 1.5 ? 'wide' : h > w * 1.5 && w >= 160 ? 'tall' : Math.min(w, h) >= 280 ? 'large' : 'small';
}
shape();
addEventListener('resize', shape);
(async () => {
shiny = await store.get('shiny', false);
shinyButton.setAttribute('aria-pressed', String(shiny));
const saved = await store.get('pokemon', null);
if (saved) {
current = saved;
render(saved);
wanted = saved.id;
loadSprite(saved.id);
} else {
// Pokémon of the day, to start.
const day = Math.floor(Date.now() / 86_400_000);
show(((day * 2654435761) % COUNT) + 1);
}
})();
})();
</script>
</body>
</html>
