feat(ui): big bundle — settings (theme/master/auto), purple default, mobile fit, master extraction

Settings expansion: 4 new keys (theme auto/light/dark, masterMode,
autoCallEnabled, autoCallSpeed 1-10) with per-key validators that
preserve old saved data. Default empty-cell color flipped from Tân
Tân blue to Excel Standard Purple #7030A0; preset palette swapped
to Office's 10 standard colors (5x2 grid). 15 new tests, 53 total.

Theme system: Tailwind v4 @variant dark (.dark *); applyTheme()
toggles <html class="dark"> based on settings.theme; auto mode
mirrors prefers-color-scheme via matchMedia listener (cleanly torn
down when switching modes). Existing dark-mode CSS converted from
@media to :where(.dark) selectors.

Mobile fit: PlayerBoard cells aspect-square on mobile, sm:aspect-[3/5]
desktop. Number text scales text-base sm:text-2xl md:text-3xl. Page
padding tightened (px-2 py-4 sm:px-3 sm:py-12). Container bumped
max-w-lg to max-w-2xl.

Header / footer: removed instructions toggle and "Trang quản trò"
link from player page. New PageFooter.svelte (tagline + Made by
miti99 with [SVG heart] link); also duplicated in PlayerBoard's
closing section-label band per request. Heart is inline SVG (red),
not emoji.

Master mode: extracted everything from /master route into reusable
MasterPanel.svelte. /master route slimmed to header + MasterPanel
+ footer. / mounts MasterPanel conditionally when settings.masterMode.
Storage prefixes unchanged.

Master tracking grid removed: per request, the 11x9 ones-digit
master board is gone. Host still gets controls, "Số vừa xổ" hero,
draw history list, and their own player card. Player card is enough.

Auto-call: single $effect lifecycle keyed on (autoRunning,
settings.autoCallSpeed, settings.autoCallEnabled). Setup / clear
setInterval cleanly across speed changes, master-mode toggle off,
component unmount, and "user disabled auto" mid-run. Button toggles
"Xổ số" -> "Bắt đầu / Dừng". Speed slider in Settings (only visible
when master mode is on). aria-label / aria-valuetext on slider.

Code-review nice-to-fixes applied: folded the two MasterPanel $effects
into one, removed muddled onkeydown on the SettingsButton modal
backdrop, added slider a11y attrs.

Docs: PDR / codebase-summary / system-architecture / development-roadmap
/ code-standards all synced to the new state.
This commit is contained in:
tiennm99 committed 2026-04-27 01:26:21 +07:00
1 parent 96a652f381
commit 0bc27f2897
23 files changed
+2056 -425

No files matched your search

+5 -4
View File
@@ -41,8 +41,8 @@ Use the `.svelte.js` extension for modules that export rune-based reactive state
### Utilities
- Utility-first: `className="px-4 py-2 rounded-lg text-white"`.
- Responsive: `sm:`, `md:`, `lg:` prefixes for breakpoints.
- Dark mode: `dark:bg-slate-800`, `dark:text-white`.
- Animations: Custom keyframes in `globals.css`, apply via `animate-fade-in`.
- Dark mode: Explicit `@variant dark (.dark *)` in `app.css` declares dark-mode selector; use `dark:bg-slate-800 dark:text-white`. Settings store toggles `<html class="dark">` rather than relying on `@media (prefers-color-scheme: dark)`.
- Animations: Custom keyframes in `app.css`, apply via `animate-fade-in`.
### Layout
- Flexbox for alignment: `flex flex-col items-center justify-center`.
@@ -167,8 +167,8 @@ Use inline event handlers (`onclick`, `onkeydown`). Svelte 5 handles click deleg
### Unit Tests (Implemented)
- **Framework**: Vitest 4.1.5 with happy-dom
- **Test Files**: `src/lib/game-logic.test.js` (26 tests), `src/lib/settings-store.test.js` (12 tests) — 38 total passing
- **Coverage**: Game logic (generateGrid shape/constraints, isRowComplete, getWaitingNumber, persistence with validators), settings (load/save/reset with error handling)
- **Test Files**: `src/lib/game-logic.test.js` (26 tests), `src/lib/settings-store.test.js` (27 tests) — 53 total passing
- **Coverage**: Game logic (generateGrid shape/constraints, isRowComplete, getWaitingNumber, persistence with validators), settings (load/save/reset with error handling, theme detection, master mode toggle, auto-call speed, color validation)
- **Scripts**: `npm test` (run once), `npm run test:watch` (continuous)
- **Pattern**: Use vitest's `describe` / `it` blocks, `expect()` assertions. Test both happy path and error cases (corrupt JSON, missing localStorage).
@@ -192,3 +192,4 @@ Set in `.env.local` (not committed).
- **basePath**: Dual-mode: `""` (Cloudflare, dev) or `/loto` (GitHub Pages via `BUILD_PROFILE=gh`).
Last reviewed: 2026-04-27
Last synced: 2026-04-27 (6-phase refactor)
+18 -14
View File
@@ -6,31 +6,33 @@
| File | Purpose |
|------|---------|
| `src/routes/+layout.svelte` | Root HTML layout. Sets Vietnamese lang, imports Geist font, applies global flex layout. |
| `src/routes/+page.svelte` | Player page (`/`). Instructions toggle, PlayerBoard component, indigo gradient branding. |
| `src/routes/master/+page.svelte` | Host page (`/master`). Controls (new game, draw number), 11×9 last-digit-aligned master board with circular tokens (pink ring for 1–49, green for 50–90; cream fill when called, dim when uncalled; red ring + scale on last-called) and draw-order overlay, host's player card. |
| `src/routes/+page.svelte` | Player page (`/`). Header + SettingsButton, PlayerBoard, conditional MasterPanel (when `settings.masterMode`), PageFooter. Indigo→purple gradient branding. |
| `src/routes/master/+page.svelte` | Host page (`/master`). Thin shell: header + back link + MasterPanel + PageFooter. All host logic lives in `MasterPanel.svelte`. |
### 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:5) cells with condensed bold black numbers (`tan-tan-num` font stack), white number cells, blue empty cells. Handles crossed state, bingo popup, "Chờ X" toast. Accepts `storagePrefix` prop for multi-card isolation. Empty cells use `--empty-cell-bg` CSS var from settings store. |
| `src/lib/SettingsButton.svelte` | Gear icon + modal. Color picker + 8 preset swatches for empty-cell color. Reset-to-default button. Mounted on both `/` and `/master` headers. |
| `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:5 on mobile; wider on sm+) cells with condensed bold black numbers (`tan-tan-num` font stack), white number cells, purple empty cells by default. Handles crossed state, bingo popup, "Chờ X" toast. Accepts `storagePrefix` prop for multi-card isolation. Empty cells use `--empty-cell-bg` CSS var from settings store. |
| `src/lib/SettingsButton.svelte` | Gear icon + modal. 4 fieldsets: Giao diện (theme auto/light/dark), Chế độ quản trò (master mode toggle), Tự động xổ (auto-call + speed 1–10s), Màu ô trống (10 Excel color swatches). Reset-to-default button. Mounted on both `/` and `/master` headers. |
| `src/lib/MasterPanel.svelte` | Host controls. New game / draw, "Số vừa xổ" hero token, "Thứ tự đã xổ" history list, host's own player card. No 11×9 tracking board (player card is enough). "Xổ số" / "Bắt đầu / Dừng" button bound to auto-call. Mounted conditionally on `/` when `settings.masterMode === true`, or directly on `/master`. |
| `src/lib/PageFooter.svelte` | Footer with tagline ("Made by miti99 with ❤️ SVG icon") + link. Mounted on `/` and `/master`. |
### Game Logic
| File | Purpose |
|------|---------|
| `src/lib/game-logic.js` | Stateless utilities: generateGrid (constraint-aware picker — exact 5 per row & per col, ascending-sorted columns), saveGrid, loadGrid, saveCrossedState, loadCrossedState, isRowComplete, getWaitingNumber. |
| `src/lib/settings-store.svelte.js` | Reactive global UI settings via Svelte 5 runes. `emptyCellColor` (hex) persisted to localStorage `loto_settings`. Pushes value to `--empty-cell-bg` CSS var on `:root`. |
| `src/lib/settings-store.svelte.js` | Reactive global UI settings via Svelte 5 runes. Stores 5 keys: `theme` (enum: "auto" / "light" / "dark"), `masterMode` (bool), `autoCallEnabled` (bool), `autoCallSpeed` (1–10), `emptyCellColor` (hex). Persisted to localStorage `loto_settings`. Pushes values to CSS vars and `<html class="dark">` on `:root`. Per-key validators preserve old data. |
### Styling
| File | Purpose |
|------|---------|
| `src/app.css` | Root styles: Tailwind @import, CSS variables (light/dark), `.loto-grid` & `.master-grid` (9-col), animations (fade-in, pop-in, bounce-slow, spin-slow, toast), `.cell-crossed` diagonal. |
| `src/app.css` | Root styles: Tailwind @import, CSS variables (light/dark), Tailwind v4 `@variant dark (.dark *)` for explicit dark-mode class selector, `.loto-grid` & `.master-grid` (9-col), animations (fade-in, pop-in, bounce-slow, spin-slow, toast), `.cell-crossed` diagonal. |
### Tests
| File | Purpose |
|------|---------|
| `src/lib/game-logic.test.js` | 26 unit tests: generateGrid shape (9×9, 5 per row/col, no duplicates), column ranges & ascending sort, row completion, waiting number detection, persistence (saveGrid/loadGrid/saveCrossedState/loadCrossedState with validators). |
| `src/lib/settings-store.test.js` | 12 unit tests: defaults, loadSettings (restore from localStorage, apply CSS var, handle empty/corrupt), saveSettings, resetSettings, color validation. |
| `src/lib/settings-store.test.js` | 27 unit tests: defaults, loadSettings (restore 5 keys from localStorage, apply CSS vars, toggle dark class, handle empty/corrupt), saveSettings, resetSettings, theme toggle (auto → OS pref detection), master mode, auto-call + speed, color validation. |
### Configuration
| File | Purpose |
@@ -58,19 +60,21 @@
| `loto_master` | Host's drawn/remaining numbers. |
| `loto_master_card_grid` | Host's player card numbers. |
| `loto_master_card_crossed` | Host's marked cells. |
| `loto_settings` | Global UI settings: `{ emptyCellColor: "#rrggbb" }`. |
| `loto_settings` | Global UI settings: `{ theme, masterMode, autoCallEnabled, autoCallSpeed, emptyCellColor }`. |
## Component Hierarchy
```
RootLayout
├── HomePage (/)
│ ├── Instructions toggle
│ └── PlayerBoard (storagePrefix="loto")
│ ├── PlayerBoard (storagePrefix="loto")
│ ├── [if settings.masterMode]
│ │ └── MasterPanel (state, controls, draw history)
│ └── PageFooter
└── MasterPage (/master)
├── Controls (new game, draw)
├── Master board (11×9, last-digit aligned, draw-order overlay)
└── PlayerBoard (storagePrefix="loto_master_card")
├── MasterPanel (state, controls, draw history)
├── Back link
└── PageFooter
```
## Key Functions
@@ -81,8 +85,8 @@ RootLayout
| `generateGrid()` | game-logic.js | Builds 9×9; ascending-sorted numbers per column. |
| `isRowComplete()` | game-logic.js | Boolean: all non-zero cells in row crossed? |
| `getWaitingNumber()` | game-logic.js | Returns the single uncrossed number in row, or null. |
| `buildBoard()` | master/+page.svelte | Builds 11×9 master grid; row = ones-digit, col = tens-digit; col 0 holds 1–9, col 8 holds 80–90. |
| `handleCellClick()` | PlayerBoard.svelte | Toggle crossed[row][col]. |
| `saveGrid()` / `loadGrid()` | game-logic.js | localStorage with prefix-based keys. |
Last reviewed: 2026-04-27
Last synced: 2026-04-27 (6-phase refactor)
+11 -6
View File
@@ -15,15 +15,21 @@ The app is fully functional for core gameplay (Lô tô hội chợ Tân Tân var
Kinh verification
- Player card rendered as 3 stacked Tân Tân mini-cards
(Minh Tân / Loại đặc biệt / Tấn tài tấn lộc) with cross-hatch dividers
- Settings modal: empty-cell color picker (color picker + 8 presets +
reset), applies to player + master grids, persisted to `loto_settings`
- Settings modal: 4 fieldsets (theme auto/light/dark, master mode toggle,
auto-call speed 1–10s, 10 color presets), persisted to `loto_settings`
- Theme switcher (auto detects OS pref, explicit light/dark modes)
- Master mode toggle (shows MasterPanel on `/`)
- Auto-call timer with speed slider
- Mobile-responsive cells (aspect-ratio + text scaling)
- MasterPanel extracted component (reused on `/` and `/master`)
- PageFooter with tagline + creator link
- Host's own player card (isolated instance)
- localStorage persistence
- Dark mode
- Mobile responsive
- Offline capable
- Unit tests (Vitest: 26 game-logic tests + 12 settings-store tests, 38 total passing)
covering constraint validation, persistence, and error handling
- Unit tests (Vitest: 26 game-logic tests + 27 settings-store tests, 53 total passing)
covering constraint validation, persistence, theme/master-mode/auto-call logic, and error handling
## Idea Phase
@@ -33,8 +39,6 @@ Play celebratory chime or "Kinh!" voice snippet when row completes. Could use We
### Undo Last Cell
Allow player to undo the most recent cross/uncross action. Requires change history or state snapshot. **Status**: Idea (low priority)
### Theme Switcher
Explicit light/dark toggle button instead of relying on OS preference. **Status**: Idea (Tailwind already supports OS toggle)
### PWA Install
Add service worker and manifest for "Install App" prompt on Android/iOS. **Status**: Idea (would require server-side components)
@@ -104,3 +108,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)
+16 -6
View File
@@ -18,8 +18,8 @@ That format is intentionally out of scope.
- **Visual layout**: card displayed as 3 stacked 3×9 mini-cards with
traditional separator labels — *Tân Tân*, *An khang thịnh vượng*,
*Tân Tân tốt nhất* — matching the physical Tân Tân BAMBOORAFT paper
sheet (tall non-square cells, condensed bold black numbers on white,
blue empty-cell background by default).
sheet (tall non-square cells on mobile, wider on sm+; condensed bold black numbers on white,
purple empty-cell background by default — Excel Standard Purple `#7030A0`).
- **Column ranges**: col 0 = 1–9, col 1 = 10–19, …, col 7 = 70–79,
col 8 = 80–90.
- **Within a column**: numbers placed top-to-bottom in **ascending** order.
@@ -31,8 +31,11 @@ That format is intentionally out of scope.
a master board with circular tokens (pink ring for 1–49, green for 50–90,
cream fill on called, dim on uncalled, red ring + scale on the most
recent draw), and may also play their own card.
- **Settings**: gear icon in headers opens a modal with a color picker for
the empty/blank cell color (default brown matches physical card).
- **Settings**: gear icon in headers opens a modal with 4 fieldsets:
- Giao diện: theme switcher (auto / light / dark; auto mirrors OS pref)
- Chế độ quản trò: toggle to show MasterPanel on `/`
- Tự động xổ: enable auto-call; speed slider (1–10 seconds)
- Màu ô trống: color picker + 10 Excel preset swatches
Persisted to `loto_settings`.
### Out of scope
@@ -96,7 +99,13 @@ State is entirely client-side. Each page/card instance uses a unique localStorag
- [x] Mobile-responsive (base + sm breakpoints).
- [x] HMR works on code-server via proxy.
- [x] Player card rendered as 3 stacked Tân Tân mini-cards with separator labels.
- [x] Settings modal with empty-cell color picker; applies to both player and master grids; persisted.
- [x] Settings modal with 4 fieldsets (theme, master mode, auto-call, color); applies to both grids; persisted.
- [x] Theme switcher (auto / light / dark) with explicit dark class selector on `<html>`.
- [x] Master mode toggle to show MasterPanel on player page.
- [x] Auto-call timer (1–10s speed) with start/stop button.
- [x] Mobile-responsive layout (aspect-square sm:aspect-[3/5] cells, text scaling).
- [x] MasterPanel extracted; mounted conditionally on `/` and directly on `/master`.
- [x] PageFooter with tagline + link on both pages.
## Visual Language
@@ -115,4 +124,5 @@ State is entirely client-side. Each page/card instance uses a unique localStorag
- Multiplayer sync (real-time via WebSocket)
- i18n beyond Vietnamese
Last reviewed: 2026-04-26 (scope locked: Lô tô hội chợ Tân Tân)
Last reviewed: 2026-04-27 (scope locked: Lô tô hội chợ Tân Tân + theme/master/auto-call)
Last synced: 2026-04-27 (6-phase refactor)
+21 -9
View File
@@ -4,22 +4,23 @@
```
Entry (+layout.svelte)
├─ onMount: loadSettings() — restore emptyCellColor from loto_settings,
│ apply to --empty-cell-bg CSS variable before pages render
├─ onMount: loadSettings() — restore all 5 settings keys from loto_settings,
│ apply CSS vars, toggle <html class="dark"> on theme/OS pref, setup auto-call effect
│
├─ / (Player Page)
│ ├─ Load loto_grid, loto_crossed from localStorage
│ ├─ Display 9×9 grid (empty cells use --empty-cell-bg from settings)
│ ├─ Display 9×9 PlayerBoard (empty cells use --empty-cell-bg from settings)
│ ├─ Generate new grid on button click
│ ├─ Mark/unmark cells on click
│ └─ Show bingo popup + "Chờ X" toasts
│ ├─ Show bingo popup + "Chờ X" toasts
│ ├─ [if settings.masterMode === true]
│ │ └─ Mount MasterPanel (host controls + draw history)
│ └─ PageFooter (tagline + miti99 link)
│
└─ /master (Host Page)
├─ Load loto_master (called/remaining)
├─ Display 11×9 master board (empty cells use --empty-cell-bg)
├─ Draw button shows next called number (highlighted in red)
├─ Display host's own card (loto_master_card prefix)
└─ New Game button resets called/remaining
├─ MasterPanel (state, controls, draw history; same as above)
├─ Back link to /
└─ PageFooter
```
## State Model
@@ -34,10 +35,20 @@ Each row has exactly 5 non-zero numbers AND each column has exactly 5
(constraint-aware picker — no slack). Numbers within a column are sorted
ascending top-to-bottom (lô tô hội chợ Tân Tân convention).
### Settings (`loto_settings`)
```
theme: "auto" | "light" | "dark" // Display mode
masterMode: boolean // Show MasterPanel on /
autoCallEnabled: boolean // Enable auto-call timer
autoCallSpeed: number (1–10) // Speed in seconds
emptyCellColor: "#rrggbb" // Hex color (default #7030A0 Excel Purple)
```
### Host State (`storagePrefix="loto_master"`)
```
called: number[] // [5, 23, 67, ...] — drawn in order
remaining: number[] // [1, 2, 3, ...] minus called — shuffled initially
autoRunning: boolean // Auto-call timer active
```
### Host's Card (`storagePrefix="loto_master_card"`)
@@ -134,3 +145,4 @@ Files that are client-only:
All state is localStorage. No API calls. Fully functional offline after initial load.
Last reviewed: 2026-04-27
Last synced: 2026-04-27 (6-phase refactor)