Easter egg: give the mini-game a real mobile trigger

.nav__brand is display:none below 1024px, so "tap the wordmark" was
desktop-only. Add a shared onTapCode() helper (src/lib/tap-code.js) and
put the 7-tap shortcut on the dex feed's game-title (.feed-head__id),
which is visible on every screen size. Keeps the wordmark tap for desktop.

3 tests for onTapCode (count, window reset, unbind).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
chris 2026-09-10 12:47:15 -04:00
parent fb299d11f9
commit 1125278f76
4 changed files with 83 additions and 24 deletions

View File

@ -1,6 +1,7 @@
import { el } from '../lib/dom.js'; import { el } from '../lib/dom.js';
import { openGameSheet } from './GameSheet.js'; import { openGameSheet } from './GameSheet.js';
import { settings } from '../store/settings.js'; import { settings } from '../store/settings.js';
import { onTapCode } from '../lib/tap-code.js';
const ITEMS = [ const ITEMS = [
{ label: 'Dex', icon: '▦', href: '#/', match: (h) => h === '#/' || h === '' || h === '#' }, { label: 'Dex', icon: '▦', href: '#/', match: (h) => h === '#/' || h === '' || h === '#' },
@ -25,20 +26,6 @@ export function Nav() {
const nav = el('nav', { class: 'nav', 'aria-label': 'Primary' }); const nav = el('nav', { class: 'nav', 'aria-label': 'Primary' });
let links = []; let links = [];
// Easter egg: seven quick taps on the wordmark opens the hidden game
// (the Konami code does the same on a keyboard).
let taps = 0;
let tapTimer = null;
function bumpBrand() {
taps += 1;
clearTimeout(tapTimer);
tapTimer = setTimeout(() => (taps = 0), 1500);
if (taps >= 7) {
taps = 0;
location.hash = '#/whos-that';
}
}
function build() { function build() {
const st = settings.get(); const st = settings.get();
const items = ITEMS.filter((it) => !it.optional || st[it.optional]); const items = ITEMS.filter((it) => !it.optional || st[it.optional]);
@ -55,10 +42,13 @@ export function Nav() {
) )
: el('a', { class: 'nav__link', href: item.href }, ...inner); : el('a', { class: 'nav__link', href: item.href }, ...inner);
}); });
nav.replaceChildren( // Easter egg: the wordmark only shows on wide screens; on mobile the
el('span', { class: 'nav__brand', onclick: bumpBrand }, 'Pocketdex'), // same shortcut lives on the dex feed's game title (see DexGrid).
...links, const brand = el('span', { class: 'nav__brand' }, 'Pocketdex');
); onTapCode(brand, () => {
if ((location.hash || '') !== '#/whos-that') location.hash = '#/whos-that';
});
nav.replaceChildren(brand, ...links);
nav._items = items; nav._items = items;
sync(); sync();
} }

23
src/lib/tap-code.js Normal file
View File

@ -0,0 +1,23 @@
/**
* Fire `fn` after `count` taps/clicks land on `node` within `windowMs` of
* each other a quiet way to hide a shortcut behind a bit of fidgeting.
* Returns an unbind.
*/
export function onTapCode(node, fn, { count = 7, windowMs = 1500 } = {}) {
let n = 0;
let timer = null;
const onClick = () => {
n += 1;
clearTimeout(timer);
timer = setTimeout(() => (n = 0), windowMs);
if (n >= count) {
n = 0;
fn();
}
};
node.addEventListener('click', onClick);
return () => {
clearTimeout(timer);
node.removeEventListener('click', onClick);
};
}

View File

@ -8,6 +8,7 @@ import { Card } from '../components/Card.js';
import { ProgressRing } from '../components/ProgressRing.js'; import { ProgressRing } from '../components/ProgressRing.js';
import { Sprite } from '../components/Sprite.js'; import { Sprite } from '../components/Sprite.js';
import { typesForGen } from '../lib/type-resolve.js'; import { typesForGen } from '../lib/type-resolve.js';
import { onTapCode } from '../lib/tap-code.js';
const TYPES = [ const TYPES = [
'normal', 'normal',
@ -429,16 +430,18 @@ export async function DexGrid() {
} }
syncToolsBadge(); syncToolsBadge();
// Easter egg: a flurry of taps on the game title opens the hidden game.
// (The Konami code does the same with a keyboard.)
const feedId = el('div', { class: 'feed-head__id' }, title, gameLabel);
onTapCode(feedId, () => {
if ((location.hash || '') !== '#/whos-that') location.hash = '#/whos-that';
});
view.append( view.append(
el( el(
'header', 'header',
{ class: 'feed-head' }, { class: 'feed-head' },
el( el('div', { class: 'feed-head__top' }, feedId, ring.node),
'div',
{ class: 'feed-head__top' },
el('div', { class: 'feed-head__id' }, title, gameLabel),
ring.node,
),
el( el(
'div', 'div',
{ class: 'field-search' }, { class: 'field-search' },

43
test/tap-code.test.js Normal file
View File

@ -0,0 +1,43 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { onTapCode } from '../src/lib/tap-code.js';
const tap = (node) => node.dispatchEvent(new Event('click'));
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
describe('onTapCode', () => {
it('fires after `count` taps inside the window', () => {
const node = new EventTarget();
const fn = vi.fn();
onTapCode(node, fn, { count: 7, windowMs: 1500 });
for (let i = 0; i < 6; i++) tap(node);
expect(fn).not.toHaveBeenCalled();
tap(node);
expect(fn).toHaveBeenCalledTimes(1);
});
it('resets when taps are too far apart', () => {
const node = new EventTarget();
const fn = vi.fn();
onTapCode(node, fn, { count: 3, windowMs: 500 });
tap(node);
tap(node);
vi.advanceTimersByTime(600); // window elapsed -> counter clears
tap(node);
tap(node);
expect(fn).not.toHaveBeenCalled();
tap(node);
expect(fn).toHaveBeenCalledTimes(1);
});
it('unbind stops it', () => {
const node = new EventTarget();
const fn = vi.fn();
const off = onTapCode(node, fn, { count: 2 });
off();
tap(node);
tap(node);
expect(fn).not.toHaveBeenCalled();
});
});