diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md index ea30db3..8f884e9 100644 --- a/docs/codebase-summary.md +++ b/docs/codebase-summary.md @@ -12,25 +12,33 @@ ### Shared Components | File | Purpose | |------|---------| -| `src/lib/PlayerBoard.svelte` | Reusable player card (9×9 grid). Handles crossed state, bingo popup, "Chờ X" toast. Accepts `storagePrefix` prop for multi-card isolation. | +| `src/lib/PlayerBoard.svelte` | Reusable player card (9×9 grid rendered as 3 stacked 3×9 mini-cards: Minh Tân / Loại đặc biệt / Tấn tài tấn lộc). 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. | ### 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`. | ### 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. | +### 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. | + ### Configuration | 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. | -| `eslint.config.mjs` | ESLint 9 flat config (@eslint/js + eslint-plugin-svelte). | +| `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). | | `jsconfig.json` | Path alias `$lib`, no checkJs. | | `.gitignore` | Excludes node_modules, build, .env.local, etc. | | `.env.example` | codeserver profile vars (CODESERVER_HOST, CODESERVER_PORT). | @@ -50,6 +58,7 @@ | `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" }`. | ## Component Hierarchy @@ -76,4 +85,4 @@ RootLayout | `handleCellClick()` | PlayerBoard.svelte | Toggle crossed[row][col]. | | `saveGrid()` / `loadGrid()` | game-logic.js | localStorage with prefix-based keys. | -Last reviewed: 2026-04-26 +Last reviewed: 2026-04-27 diff --git a/docs/development-roadmap.md b/docs/development-roadmap.md index 47ba202..36e8c3b 100644 --- a/docs/development-roadmap.md +++ b/docs/development-roadmap.md @@ -13,11 +13,17 @@ The app is fully functional for core gameplay (Lô tô hội chợ Tân Tân var - Host number drawing from 1–90 deck - 11×9 last-digit-aligned master board with draw-order overlay for fast 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` - 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 ## Idea Phase @@ -49,15 +55,9 @@ Internationalization (English, Chinese, etc.). Requires extraction of all Vietna ### Undo/Redo System Full undo/redo stack with history navigation. Adds complexity to state management. **Status**: Considered (YAGNI for now) -## Testing (Planned but Unstarted) +## Testing -### Unit Tests -- Game logic: `generateGrid()`, `isRowComplete()`, `getWaitingNumber()` -- localStorage helpers: `saveGrid()`, `loadGrid()`, etc. - -**Tech**: Vitest (Vite's native test runner) - -### Component Tests +### Component Tests (Planned) - PlayerBoard with mocked localStorage, $state/$derived verification - Master page with different game states @@ -96,10 +96,11 @@ European Bingo 90 patterns. **Out of scope** — Tân Tân uses single-line "Kin All decisions follow **YAGNI** (You Aren't Gonna Need It), **KISS** (Keep It Simple), **DRY** (Don't Repeat Yourself): - No multiplayer sync → adds server dependency, breaks static export (Cloudflare Pages) - No i18n → Vietnamese-only community, localizing adds complexity -- No testing → small codebase (<300 LOC), manual testing covers critical paths; add when code grows +- Unit tests implemented → critical paths (constraints, persistence, settings) now have automated coverage +- Component/E2E tests deferred → small codebase, manual testing sufficient for UI flows - No undo/redo → simple game, mistakes are play experience - Svelte 5 runes → cleaner reactivity than hooks, smaller mental model Future work gates on **real user demand**, not speculation. -Last reviewed: 2026-04-26 +Last reviewed: 2026-04-27 diff --git a/docs/project-overview-pdr.md b/docs/project-overview-pdr.md index c1379d0..0d32751 100644 --- a/docs/project-overview-pdr.md +++ b/docs/project-overview-pdr.md @@ -15,6 +15,9 @@ That format is intentionally out of scope. ### In scope (Lô tô hội chợ Tân Tân) - **Card**: 9 rows × 9 columns. Exactly **5 numbers per row** AND exactly **5 numbers per column** (45 numbers per card; 36 blanks). +- **Visual layout**: card displayed as 3 stacked 3×9 mini-cards with + traditional separator labels — *Minh Tân*, *Loại đặc biệt*, + *Tấn tài tấn lộc* — matching the physical Minh Tân paper sheet. - **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. @@ -24,6 +27,9 @@ That format is intentionally out of scope. - **Waiting state**: when a row needs only 1 number, app announces **"Chờ N"**. - **Host (quản trò)** draws numbers from a shuffled 1–90 deck, tracks them on a master board, 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). + Persisted to `loto_settings`. ### Out of scope - 3×9 / 15-number Bingo 90 / Tombola tickets (European format). @@ -85,6 +91,8 @@ State is entirely client-side. Each page/card instance uses a unique localStorag - [x] Dark mode support (Tailwind dark classes). - [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. ## Visual Language diff --git a/docs/system-architecture.md b/docs/system-architecture.md index 5943c51..9b2218a 100644 --- a/docs/system-architecture.md +++ b/docs/system-architecture.md @@ -3,19 +3,21 @@ ## Page Flow ``` -Entry +Entry (+layout.svelte) + ├─ onMount: loadSettings() — restore emptyCellColor from loto_settings, + │ apply to --empty-cell-bg CSS variable before pages render + │ ├─ / (Player Page) │ ├─ Load loto_grid, loto_crossed from localStorage - │ ├─ Display 9×9 grid + │ ├─ Display 9×9 grid (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 │ └─ /master (Host Page) ├─ Load loto_master (called/remaining) - ├─ Display 11×9 master board (numbers 1–90, aligned by ones-digit; - │ col 0 = 1–9, col 8 = 80–90; each called cell shows draw order) - ├─ Draw button shows next called number (highlighted in red on board) + ├─ 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 ``` @@ -131,4 +133,4 @@ Files that are client-only: All state is localStorage. No API calls. Fully functional offline after initial load. -Last reviewed: 2026-04-26 +Last reviewed: 2026-04-27 diff --git a/eslint.config.mjs b/eslint.config.mjs index cab1500..706e776 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,7 +9,18 @@ export default [ languageOptions: { ecmaVersion: "latest", sourceType: "module", - globals: { ...globals.browser, ...globals.node }, + globals: { + ...globals.browser, + ...globals.node, + // Svelte 5 runes — usable inside .svelte and .svelte.js files. + $state: "readonly", + $derived: "readonly", + $effect: "readonly", + $props: "readonly", + $bindable: "readonly", + $inspect: "readonly", + $host: "readonly", + }, }, }, { diff --git a/src/lib/SettingsButton.svelte b/src/lib/SettingsButton.svelte new file mode 100644 index 0000000..852454b --- /dev/null +++ b/src/lib/SettingsButton.svelte @@ -0,0 +1,174 @@ + + + + +{#if open} + +{/if} diff --git a/src/lib/settings-store.svelte.js b/src/lib/settings-store.svelte.js new file mode 100644 index 0000000..1251b35 --- /dev/null +++ b/src/lib/settings-store.svelte.js @@ -0,0 +1,62 @@ +/** + * Global UI settings, persisted to localStorage and reactive via Svelte 5 + * runes. Currently only `emptyCellColor` (paints blank cells across both + * the player card and the master tracking grid). Shape is intentionally + * an object so future keys can be added without breaking persistence. + * + * @module lib/settings-store + */ + +const STORAGE_KEY = "loto_settings"; +const HEX6 = /^#[0-9a-fA-F]{6}$/; + +export const DEFAULT_SETTINGS = Object.freeze({ + /** Brown — matches a Minh Tân paper card. */ + emptyCellColor: "#7a4a2b", +}); + +export const settings = $state({ ...DEFAULT_SETTINGS }); + +/** @param {unknown} v */ +function isValidColor(v) { + return typeof v === "string" && HEX6.test(v); +} + +function applyToDom() { + if (typeof document === "undefined") return; + document.documentElement.style.setProperty( + "--empty-cell-bg", + settings.emptyCellColor, + ); +} + +export function loadSettings() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) { + applyToDom(); + return; + } + const parsed = JSON.parse(raw); + if (parsed && isValidColor(parsed.emptyCellColor)) { + settings.emptyCellColor = parsed.emptyCellColor; + } + } catch { + /* private mode / quota / corrupt JSON — fall back to defaults */ + } + applyToDom(); +} + +export function saveSettings() { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...settings })); + } catch { + /* see loadSettings */ + } + applyToDom(); +} + +export function resetSettings() { + settings.emptyCellColor = DEFAULT_SETTINGS.emptyCellColor; + saveSettings(); +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index d701e67..d32d91e 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,7 +1,14 @@ {@render children()} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 0aba295..3824865 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,6 +1,7 @@ @@ -8,7 +9,10 @@
-
+
+
+ +

diff --git a/src/routes/master/+page.svelte b/src/routes/master/+page.svelte index c854c07..376729d 100644 --- a/src/routes/master/+page.svelte +++ b/src/routes/master/+page.svelte @@ -70,6 +70,7 @@