Back to Pokédex
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.

index.html
<!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>