Phase 1 — Vietnamese-safe font + master empty state:
- Add @fontsource/roboto-condensed (700 weight, all subsets including
Vietnamese). font-display:swap. tan-tan-num now resolves the bundled
face on Android instead of system Arial Narrow.
- New MasterEmptyState.svelte: ghost 11×9 grid + "Chế độ Quản trò"
pill + readiness microcopy. Replaces the bare line of text in
MasterPanel's no-game state.
Phase 2 — Mode picker icons, color picker layout, header polish:
- Inline SVG glyphs above each mode button (player card / megaphone /
two stacked cards). Communicates role at a glance.
- Color picker wrapped in a single bordered card with "Tuỳ chỉnh"
and "Mẫu sẵn" sub-headers.
- "Mặc định" → "Đặt lại" with a bordered chip style — clearer
affordance than the previous near-invisible footer link.
- Header subline: drop tracking-[0.28em] all-caps SaaS look; replace
with dash-flanked lantern band ("— 🏮 Hội chợ TN1 —") for
fairground mood.
Phase 3 — Installable PWA + offline audio:
- @vite-pwa/sveltekit with autoUpdate. Precache app shell (~353 KB
→ 213 entries) PLUS the default voice's 92 clips so first-install
is fully offline-capable. Alternate voices fall through to a
CacheFirst runtime rule (cached on first play, 30-day TTL,
maxEntries: 400 for future-proofing).
- New static/manifest.webmanifest (Lô tô — Hội chợ TN1, theme #1565c0,
background #0a0f1f, standalone, vi).
- Icons: 192/512 standard + 512 maskable, generated from a single
rose-amber-gradient SVG source.
- app.html: manifest link, dual theme-color meta (light + dark),
apple-touch-icon, apple-mobile-web-app-capable for proper
standalone launch on iOS Safari.
- _headers: add manifest-src + worker-src to CSP; no-cache on /sw.js
and /manifest.webmanifest so deploys propagate.
Tests: 115/115 pass. Build clean (305 precache entries, 0 glob
warnings).
Reviewer concerns (addressed):
- maxEntries bumped 200 → 400 (was barely enough for 2 voices).
- Default voice precached so offline-first promise holds without
requiring users to play every clip online first.
- "do NOT add skipWaiting" comment added next to autoUpdate.
Lô tô
Bàn số của trò chơi "Lô tô" — SvelteKit app.
Single page (/). The host enables "Chế độ quản trò" in settings to
reveal the master panel inline below their player card; called numbers
get spoken aloud in Vietnamese using bundled MP3 clips (no runtime TTS).
See docs/ for architecture, code standards, and deployment.
Development
npm install
npm run dev
Inside code-server (reverse proxy)
cp .env.example .env.local
# edit .env.local: set CODESERVER_HOST and CODESERVER_PORT
npm run dev:codeserver
Open https://<CODESERVER_HOST>/absproxy/<CODESERVER_PORT>/.
Use /absproxy/{port}/, not /proxy/{port}/ — the latter strips the
path prefix and breaks the SvelteKit base path.
Build
npm run build # default — root basePath, for Cloudflare Pages
npm run build:gh # /loto basePath, for tiennm99.github.io/loto manual export
Static export to build/. Deployed to Cloudflare Pages from main
(set up via the CF dashboard — see docs/deployment-guide.md).
Regenerating audio
Vietnamese voice clips live in static/audio/{voiceId}/. Generated
once with edge-tts (free, no API
key) — runtime never calls TTS. To regenerate (e.g., to add a new voice
that Microsoft ships, or to change wording):
pip install edge-tts
python3 scripts/generate-audio.py
The script auto-discovers every vi-* voice and writes a manifest the
app reads on next reload.