feat: UI polish v2 + installable PWA with offline audio

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.
This commit is contained in:
tiennm99 committed 2026-04-27 20:35:59 +07:00
1 parent f7cbb6e1c7
commit f899b09631
24 files changed
+5756 -68

No files matched your search

+16 -12
View File
@@ -11,8 +11,9 @@
### Shared Components
| File | Purpose |
|------|---------|
| `src/lib/PlayerBoard.svelte` | Reusable player card (9×9 grid rendered as 3 stacked 3×9 mini-cards: Tân Tân / An khang thịnh vượng / Tân Tân tốt nhất). Tall (3:4 on mobile; 3:5 on sm+) cells with condensed bold black numbers (`tan-tan-num` font stack), white number cells, purple empty cells (dark mode dims via `filter:brightness(0.85)`). Handles crossed state, animated cross-out (200 ms `cross-draw` keyframe), `active:scale-90` press, 10 ms haptic on tap. Two header actions: "Tạo bảng mới" / "Xoá đánh dấu". First-run state shows a faded preview card. Bingo popup tiers: row 1 = standard celebration; row 3+ = falling-emoji confetti rain via CSS `confetti-fall`. Toast "Chờ N" + audio. Accepts `storagePrefix` prop for multi-card isolation. |
| `src/lib/SettingsButton.svelte` | Gear icon + modal (responsive `max-w-sm sm:max-w-md`). 5 fieldsets: Giao diện (theme pills), Chế độ quản trò (switch row), Tự động xổ (switch + speed slider), Âm thanh (two switches + voice picker), Màu ô trống (10 Excel color swatches + custom picker). Boolean toggles use a shared `switchRow` snippet (`role="switch"` + keyboard support). Reset-to-default button. Mounted on `/`. |
| `src/lib/PlayerBoard.svelte` | Reusable player card (9×9 grid rendered as 3 stacked 3×9 mini-cards: Tân Tân / An khang thịnh vượng / Tân Tân tốt nhất). Tall (3:4 on mobile; 3:5 on sm+) cells with condensed bold black numbers (`tan-tan-num` font stack w/ self-hosted Roboto Condensed), white number cells, purple empty cells (dark mode dims via `filter:brightness(0.85)`). Handles crossed state, animated cross-out (200 ms `cross-draw` keyframe), `active:scale-90` press, 10 ms haptic on tap. Two header actions: "Tạo bảng mới" / "Xoá đánh dấu". First-run state shows a faded preview card. Bingo popup tiers: row 1 = standard celebration; row 3+ = falling-emoji confetti rain via CSS `confetti-fall`. Toast "Chờ N" + audio. Accepts `storagePrefix` prop for multi-card isolation. |
| `src/lib/SettingsButton.svelte` | Gear icon + modal (responsive `max-w-sm sm:max-w-md`). 6 fieldsets: Giao diện (theme pills), Chế độ (3-way mode picker w/ SVG glyphs: player/master/both), Chế độ quản trò (switch row), Tự động xổ (switch + speed slider), Âm thanh (two switches + voice picker), Màu ô trống (10 Excel swatches + custom input in bordered card w/ "Tuỳ chỉnh"/"Mẫu" sub-headers). Boolean toggles use a shared `switchRow` snippet (`role="switch"` + keyboard support). Reset-to-default button. Mounted on `/`. |
| `src/lib/MasterEmptyState.svelte` | Empty board placeholder for first-run master (mirrors PlayerBoard's preview UX). Displays faded 11×9 grid with "Ấn để bắt đầu ván mới" hint. |
| `src/lib/MasterPanel.svelte` | Host controls. New game / draw, large "Số vừa xổ" hero token (160 px mobile, 224 px sm+) with `aria-live="assertive"` + auto `scrollIntoView` on each new draw, "Thứ tự đã xổ" history list, 11×9 last-digit-aligned tracking grid (with circular tokens + draw-order overlay). Publishes draws to `call-bus` for player auto-tick. "Xổ số" / "Bắt đầu / Dừng" button bound to auto-call. Mounted conditionally on `/` when `settings.mode !== "player"`; the wrapping section uses `transition:slide` for smooth toggle-in. |
| `src/lib/PageFooter.svelte` | Footer with tagline ("Made by miti99 with ❤️ SVG icon") + link. Mounted on `/`. |
@@ -38,20 +39,23 @@
| `src/lib/settings-store.test.js` | 31 unit tests: defaults (incl. voice keys), loadSettings (restore 8 keys, apply CSS vars, toggle dark class, handle empty/corrupt), saveSettings, resetSettings, theme toggle (auto → OS pref detection), master mode, auto-call + speed, color validation, voice round-trip + invalid-id fallback. |
| `src/lib/vietnamese-number.test.js` | 40 unit tests: ones (0–9), teens (10–19 incl. mười lăm), 20–90 incl. mốt and lăm exceptions, out-of-range fall-through. |
### Configuration
### Configuration & PWA
| File | Purpose |
|------|---------|
| `svelte.config.js` | adapter-static (HTML export), dual basePath via BUILD_PROFILE env. |
| `vite.config.js` | Tailwind + SvelteKit plugins. codeserver HMR config (port, allowedHosts, hmr). |
| `package.json` | SvelteKit 2, Svelte 5 (runes), Tailwind 4, Vite. Scripts: dev, dev:codeserver, build, build:gh, lint, test, test:watch. |
| `eslint.config.mjs` | ESLint 9 flat config (@eslint/js + eslint-plugin-svelte). Declares Svelte 5 rune globals (lines 16–22). |
| `svelte.config.js` | adapter-static (HTML export), dual basePath via BUILD_PROFILE env, SvelteKit PWA plugin config. |
| `vite.config.js` | Tailwind + SvelteKit + PWA plugins. codeserver HMR config (port, allowedHosts, hmr). |
| `package.json` | SvelteKit 2, Svelte 5 (runes), Tailwind 4, Vite, @vite-pwa/sveltekit. Scripts: dev, dev:codeserver, build, build:gh, lint, test, test:watch. |
| `eslint.config.mjs` | ESLint 9 flat config (@eslint/js + eslint-plugin-svelte). Declares Svelte 5 rune globals. |
| `jsconfig.json` | Path alias `$lib`, no checkJs. |
| `.gitignore` | Excludes node_modules, build, .env.local, etc. |
| `.env.example` | codeserver profile vars (CODESERVER_HOST, CODESERVER_PORT). |
| `static/_redirects` | Cloudflare Pages: `/* / 301` — every unknown path 301-redirects to homepage. Static assets are served first so this only fires on misses. |
| `static/audio/{voiceId}/*.mp3` | Pre-generated Vietnamese voice clips (1–90 + cho + kinh per voice). Generated once by `scripts/generate-audio.py` (edge-tts). Runtime never calls TTS. |
| `static/audio/manifest.json` | Voice list `{ id, edgeName, label, gender }[]`. Written by the generator, imported by `audio-manifest.js`. |
| `scripts/generate-audio.py` | One-shot Python script: discovers every `vi-*` edge-tts voice, generates 92 clips per voice, writes manifest. Run after voice/wording changes. |
| `static/_redirects` | Cloudflare Pages: `/* / 301` — every unknown path 301-redirects to homepage. |
| `static/manifest.webmanifest` | PWA manifest: name, short_name, icons (192/512px), theme colors, display: standalone, scope, start_url. |
| `static/icons/{192,512}.png` | PWA icons (auto-generated by @vite-pwa/sveltekit from source). |
| `static/audio/{voiceId}/*.mp3` | Pre-generated Vietnamese voice clips (1–90 + cho + kinh per voice). Generated by `scripts/generate-audio.py`. SW caches at runtime (CacheFirst). |
| `static/audio/manifest.json` | Voice list `{ id, edgeName, label, gender }[]`. |
| `scripts/generate-audio.py` | One-shot Python script: generates 92 clips per voice, writes manifest. |
| Service Worker (auto) | @vite-pwa/sveltekit generates `/pwa-manifest.json`, `/sw.js`. App shell precache ~353 KB. Auto-update mode with reload toast. |
## Key Data Structures
@@ -94,4 +98,4 @@ RootLayout
| `saveGrid()` / `loadGrid()` | game-logic.js | localStorage with prefix-based keys. |
Last reviewed: 2026-04-27
Last synced: 2026-04-27 (6-phase refactor)
Last synced: 2026-04-27 (UI polish v2 + PWA: font, mode picker, color picker, empty state, PWA stack)
+21 -6
View File
@@ -35,6 +35,7 @@ All colors have corresponding `dark:` variants. Tailwind's `prefers-color-scheme
## Typography
- **Font**: Geist Sans (Google Fonts), fallback to Arial/Helvetica.
- **Grid numbers**: Roboto Condensed (self-hosted via @fontsource/roboto-condensed, font-display: swap) in `tan-tan-num` stack, bold weight.
- **Headings**: Extrabold (font-extrabold) for main title, bold (font-bold) for secondary.
- **Body**: Regular weight, slate-600 (light) / slate-400 (dark).
- **Emphasis**: `<strong>` for important copy (e.g., button labels in instructions).
@@ -65,12 +66,19 @@ All colors have corresponding `dark:` variants. Tailwind's `prefers-color-scheme
## Settings & UI Toggles
### Mode Picker (SettingsButton)
Three-way toggle in settings modal:
- **Player mode** (`mode: "player"`) — PlayerBoard only, default UX for players
- **Master mode** (`mode: "master"`) — MasterPanel only, host-only view (e.g., projector)
- **Both mode** (`mode: "both"`) — PlayerBoard + MasterPanel stacked inline, master auto-ticks player board on draw
Three-way segmented picker in settings modal with inline SVG glyphs:
- **Player mode** (rectangle icon) — PlayerBoard only, default UX for players
- **Master mode** (rounded ellipse icon) — MasterPanel only, host-only view (e.g., projector)
- **Both mode** (double-box icon) — PlayerBoard + MasterPanel stacked inline, master auto-ticks player board on draw
Mode selection is persisted to localStorage and applied immediately.
SVG icons hand-drawn from primitives (no icon library). Selection persisted to localStorage.
### Color Picker (SettingsButton)
Grouped card with two sub-sections:
- **Mẫu** (Presets): 10 Excel color swatches in a flex row (purple, blue, cyan, green, yellow, orange, red, pink, gray, black)
- **Tuỳ chỉnh** (Custom): Native `<input type="color">` for custom hex entry
Card has rounded border, shadow, padding. Compact layout fits mobile.
## Component Patterns
@@ -159,5 +167,12 @@ Keep emojis rare; use for celebration only.
- Focus states: Implicit via Tailwind (`focus:ring-2`), add if extending components.
- Contrast: Tailwind slate/indigo/emerald combos meet WCAG AA.
- Dark mode: Respects `prefers-color-scheme`, not forced.
- SVG glyphs: Simple, high-contrast shapes (no fill patterns); keyboard-navigable via fieldset + label.
Last reviewed: 2026-04-26
## PWA Features
- **Install Prompt**: Native browser-driven (no custom banner). Manifest enables Android/iOS install.
- **Offline**: App shell precache enables instant load. Audio cached on first play.
- **Auto-Update**: SW detects new version; user sees reload toast (non-intrusive).
Last reviewed: 2026-04-27
+9 -5
View File
@@ -12,15 +12,19 @@ now", see `docs/project-overview-pdr.md` and `docs/codebase-summary.md`.
- "Quản trò đọc số" voice now speaks Chờ/Kinh on master in "both" mode
- One-shot migration: `masterMode: true` → `mode: "both"`
### UI Polish v2 + Installable PWA (2026-04-27)
- **Font**: Roboto Condensed self-hosted via @fontsource (Latin + Vietnamese subsets), swapped into `tan-tan-num` stack
- **Empty State**: New `MasterEmptyState.svelte` component for first-run host (mirrors player UX)
- **Mode Picker**: 3-way segmented toggle with inline SVG glyphs (player/master/both)
- **Color Picker**: Bordered card with "Mẫu" (presets) + "Tuỳ chỉnh" (custom input) sections
- **Header Polish**: Decorative dashes flanking subline (replaced all-caps tracking)
- **PWA**: @vite-pwa/sveltekit generates SW + manifest. App shell precache (~353 KB). Audio runtime caching (CacheFirst). Auto-update w/ reload toast. Works offline at fairground (no signal venues)
## Idea Phase
### Undo Last Cell
Allow player to undo the most recent cross/uncross action. Requires change history or state snapshot. **Status**: Idea (low priority)
### PWA Install
Add service worker and manifest for "Install App" prompt on Android/iOS. **Status**: Idea (would require server-side components)
## Considered Phase
### Multiplayer Sync (Real-time)
@@ -86,4 +90,4 @@ All decisions follow **YAGNI** (You Aren't Gonna Need It), **KISS** (Keep It Sim
Future work gates on **real user demand**, not speculation.
Last reviewed: 2026-04-27
Last synced: 2026-04-27 (6-phase refactor)
Last synced: 2026-04-27 (UI polish v2 + PWA shipped)
+21 -3
View File
@@ -177,9 +177,27 @@ PlayerBoard:
Each draw creates a new object (even repeat numbers) to ensure reactive re-fire.
## Offline Capability
## Offline Capability & PWA
All state is localStorage. No API calls. Fully functional offline after initial load.
**Static Storage**: All game state in localStorage. No API calls. Fully functional offline.
**Service Worker**: @vite-pwa/sveltekit auto-generates `/sw.js` with:
- App shell precache (~353 KB): HTML, CSS, JS bundles for instant load
- Audio runtime caching: Voice clips cached on-demand (CacheFirst strategy)
- Navigation: NetworkFirst (try network, fallback to cache)
- Auto-update: Detects new versions, shows reload toast, skipWaiting on user action
**Manifest**: `/manifest.webmanifest` enables install prompt on Android/iOS:
- Display: `standalone` (fullscreen app-like mode, no browser chrome)
- Theme colors: Indigo primary, white background
- Icons: 192px + 512px PNG (auto-generated)
- Start URL: `/` with basePath awareness
**Installation Flow**:
1. App loads, registers SW (production only, not dev)
2. Browser detects manifest → shows install prompt (native, no custom banner)
3. User installs → app appears on home screen, launches in standalone mode
4. Updates: SW checks for new version; if found, toast appears; user can reload
Last reviewed: 2026-04-27
Last synced: 2026-04-27 (three-mode + auto-tick feature)
Last synced: 2026-04-27 (UI polish v2 + PWA: font, mode picker, color picker, empty state, PWA stack)