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

+9 -1
View File
@@ -1,6 +1,14 @@
/*
Content-Security-Policy: default-src 'self'; img-src 'self' data:; media-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self'; font-src 'self' data:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
Content-Security-Policy: default-src 'self'; img-src 'self' data:; media-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self'; font-src 'self' data:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; manifest-src 'self'; worker-src 'self'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), microphone=(), payment=(), usb=()
X-Frame-Options: DENY
# Service worker must always re-validate so fresh deploys propagate.
/sw.js
Cache-Control: no-cache
/manifest.webmanifest
Cache-Control: no-cache
Content-Type: application/manifest+json
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+26
View File
@@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f43f5e"/>
<stop offset="50%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#f43f5e"/>
</linearGradient>
<linearGradient id="text" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#fff8e7"/>
<stop offset="100%" stop-color="#ffe9b8"/>
</linearGradient>
</defs>
<rect width="512" height="512" rx="96" fill="url(#bg)"/>
<rect x="56" y="56" width="400" height="400" rx="64" fill="#0a0f1f" opacity="0.18"/>
<text
x="256" y="312"
text-anchor="middle"
font-family="Roboto Condensed, Arial Narrow, system-ui, sans-serif"
font-weight="900"
font-size="240"
font-style="italic"
letter-spacing="-8"
fill="url(#text)"
style="paint-order:stroke;stroke:#0a0f1f;stroke-width:8;stroke-opacity:0.18"
>Lô tô</text>
</svg>

After

Width:  |  Height:  |  Size: 996 B

+30
View File
@@ -0,0 +1,30 @@
{
"name": "Lô tô — Hội chợ TN1",
"short_name": "Lô tô",
"description": "Bàn số của trò chơi Lô tô",
"start_url": ".",
"scope": ".",
"display": "standalone",
"orientation": "portrait",
"theme_color": "#1565c0",
"background_color": "#0a0f1f",
"lang": "vi",
"icons": [
{
"src": "icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}