A quiet score pad for the board game Harmonies (Johan Benvenuto, Libellud, 2024). Count five landscapes, the animal cards and nature's spirit, save the sheet on your device, and keep a small record of every game at the table.
A mobile-first web app, installable as a PWA and ready to ship to Android and iOS through Capacitor. No account, no network, no backend: everything stays on the device.
| Route | Screen | State |
|---|---|---|
/ |
Score pad — game title, date, board side, one column per player with live totals, and a sticky rail of details and actions from 1280px | Complete |
/records |
Saved games, search, sort, density, JSON/CSV export, JSON import, delete with undo | Complete |
/games/:id |
Saved game detail: per-player landscape and animal breakdown, notes, winner, duplicate/export/delete | Complete |
/stats |
Games played, seats, best score, average winning score, per-player table and bars | Complete |
/help |
How to score, with the printed points tables and a section index | Complete |
Every route is wrapped by the shell: sticky top bar, page title, and a menu panel (score pad / records / stats / how to score, language, theme, data). Below 900px the navigation lives only in that drawer; there is no sideways-scrolling nav row.
- React 18 + TypeScript (strict) + Vite 5
- Tailwind CSS 3.4, tokens as CSS variables in
apps/web/src/index.css - zustand 5 with
persist(localStorage), versioned and migrated - i18next + react-i18next, three bundled locales:
en,zh-HK(default),zh-CN - motion (
motion/react) microinteractions, reduced-motion aware - Vitest + Testing Library for the scoring engine, store, derivations and i18n parity
- Capacitor 6 config only — no native folders are committed
The entry chunk carries the score pad; /records, /games/:id, /stats and /help are
loaded on first visit behind a component-level skeleton. React, the router, i18next and
the animation runtime sit in named vendor chunks, so a copy or artwork change never
invalidates them.
npm install # from the repository root
npm run dev # Vite dev server, http://localhost:5173
npm run build # tsc -b && vite build
npm run preview # serve dist on http://localhost:4173
npm test # vitest run (86 tests)
npm run typecheck # tsc --noEmit -p apps/web/tsconfig.json
npm run lint # eslint .Individual workspace commands work too:
npm run dev -w harmonies-score-web
npm test -w harmonies-score-webThe app implements the printed sheet exactly. apps/web/src/lib/scoring.ts is the single
source of truth; every screen reads its totals from there.
| Landscape | Points |
|---|---|
| Trees | height 1 = 1, height 2 = 3, height 3 = 7 |
| Mountains | height 1 = 1, height 2 = 3, height 3 = 7 (a lone mountain scores 0) |
| Fields | 5 per group of 2+ connected yellow tokens |
| Buildings | 5 each, only when 3+ different token colours touch it |
| Water, side A (River) | longest river only: 2 = 2, 3 = 5, 4 = 8, 5 = 11, 6 = 15, then +4 per extra token; 0–1 = 0 |
| Water, side B (Islands) | 5 per island |
| Animals | the points printed on each card's lowest empty cube space |
| Nature's spirit | optional variant, defaults to 0 |
Total = landscapes + animal cards + nature's spirit. Highest total wins; a tie is broken by the number of animal cubes placed.
Worked example on board side A:
| Sheet | Trees | Mountains | Fields | Buildings | River | Animals | Spirit | Total |
|---|---|---|---|---|---|---|---|---|
| 3×h2 + 1×h3 trees, 2×h1 mountains, 3 fields, 2 buildings, river 7, five cards (10/7/5/3/2), spirit 4 | 16 | 2 | 15 | 10 | 19 | 27 | 4 | 93 |
| 2×h1 + 2×h2 trees, 1×h3 mountain, 2 fields, 1 building, river 5, three cards (8/4/2) | 8 | 7 | 10 | 5 | 11 | 14 | 0 | 55 |
Everything stays on the device. The store persists to
localStorage["harmonies.scorepad.v1"] as { draft, saved } — a refresh never drops a
saved game. Records can be exported as JSON (exportJson) or CSV (toCsv, one row per
player, UTF-8 BOM for Excel) and re-imported (importJson, shape-validated with typed
ImportError codes). Importing merges: duplicate ids are skipped, saved games are never
replaced, and the toast states how many arrived.
Dates are stored as local wall-clock strings (2026-10-18T19:30) taken straight from
<input type="datetime-local">. No timezone conversion is applied anywhere.
See docs/DESIGN.md for the palette (with measured contrast), type scale, spacing, primitives, motion and art direction. The short version: warm paper, watercolour art, delicate ink rules, hexagonal landscape tokens, numbers in Fraunces, and one right edge for every column of figures.
npm run build && npm run preview # measure the built output, not the dev server
python3 "$HOME/.hermes/skills/kin/02-Creative/kin-ui/scripts/i18n_keys.py" apps/webThe translation audit must report MISSING=0, DRIFT=0 and UNUSED=0 (298 keys in each
locale). Layout and colour are checked on the built page at 360, 390, 768, 1024 and
1440px in both themes: no horizontal scroll, every text/background pair at or above 4.5:1
(3:1 for large text and control borders), no broken images, webfonts loaded. See
AGENTS.md for the full pre-flight list.
Capacitor reads apps/web/capacitor.config.ts (com.ashluc.harmonies,
Harmonies Scorepad, webDir: dist). The native folders are not committed; create
them once on the machine that will build the app.
# One-time: add a platform shell (creates apps/web/android or apps/web/ios)
npm run cap:add:android
npm run cap:add:ios # macOS + Xcode only
# Every time the web app changes: build, then copy dist + plugins into the shells
npm run build:mobile
# Open the native project
npm --workspace harmonies-score-web run android
npm --workspace harmonies-score-web run iosbuild:mobile runs the production build and then npx cap sync from apps/web. The
splash and status-bar defaults in capacitor.config.ts use the paper colour, and the
manifest and icon set are what the PWA install uses on the web.
Painted watercolour assets live in apps/web/public/art/ (WebP, 720×720 tokens,
2048×1152 / 1024×1536 heroes). The app falls back to its own hand-drawn SVG icons when a
file is absent, so the interface never shows a broken image. Icons are hand-authored in
src/components/icons.tsx; no icon library is bundled. The app icons in
apps/web/public/icons/ are rendered from the same leaf mark as the favicon.
Harmonies is designed by Johan Benvenuto and published by Libellud. This is an unofficial score pad made for a home table.