Files
loto/web/docs/system-architecture.md
T
tiennm99 0bc27f2897 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.
2026-04-27 01:26:21 +07:00

4.6 KiB
Raw Blame History

System Architecture

Page Flow

Entry (+layout.svelte)
  ├─ 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 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
  │   ├─ [if settings.masterMode === true]
  │   │   └─ Mount MasterPanel (host controls + draw history)
  │   └─ PageFooter (tagline + miti99 link)
  │
  └─ /master (Host Page)
      ├─ MasterPanel (state, controls, draw history; same as above)
      ├─ Back link to /
      └─ PageFooter

State Model

Player Card (storagePrefix="loto")

grid: number[][]          // 9×9 numbers (0 = empty)
crossed: boolean[][]      // 9×9 marked state

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 (110)      // 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")

Same as player card; isolated by prefix to allow host to play.

localStorage Keys

Prefix Grid Key Crossed Key
"loto" loto_grid loto_crossed
"loto_master_card" loto_master_card_grid loto_master_card_crossed

(Special) loto_master stores { called, remaining } for the master state.

All keys are JSON stringified. Corruption is silent (returns null).

basePath & Asset Resolution

Production Mode

NODE_ENV=production
basePath="/loto"
Output: /loto/index.html, /loto/_next/...
GitHub Pages serves: https://user.github.io/loto

Development Mode (Local)

NEXT_DEV_PROFILE not set
basePath="" (empty)
Dev server: http://localhost:3000

Code-Server Mode

NEXT_DEV_PROFILE=codeserver
CODESERVER_HOST=<proxy-host>
CODESERVER_PORT=3000 (or env)
basePath="/absproxy/{PORT}"
HMR Origin: CODESERVER_HOST
Access: https://<proxy>/absproxy/3000/

Note: /absproxy/{port} preserves the basePath through the proxy. /proxy/{port} strips it before forwarding, breaking HMR.

Client-Only Architecture

All pages are client-only (no SSR) because:

  • ssr: false in +layout.js disables server-side rendering
  • localStorage is unavailable on server
  • State initialization (grid, crossed) must run in browser

Files that are client-only:

  • src/routes/+page.svelte (player page)
  • src/routes/master/+page.svelte (host page)
  • src/lib/PlayerBoard.svelte (shared component)

Data Flow: Mark a Cell

1. User clicks button in PlayerBoard
2. handleCellClick(row, col) fires
3. crossed = crossed.map(...) toggled state
4. $effect listens to crossed → saveCrossedState()
5. localStorage updated with new crossed state
6. $derived updates rowCompleteness matrix
7. If row complete → bingo popup; if waiting → toast

Data Flow: Initial Load

1. Component mounts
2. $effect runs initial loadGrid(storagePrefix)
3. If found, load crossed state (or initialize as all false)
4. Pre-populate celebratedRows + notifiedWaitingRows sets
5. Pass grid + crossed to UI render

Animations

Name Duration Use
fade-in 0.2s Modal background entry
pop-in 0.4s Bingo popup scale + scale-back
bounce-slow 1.5s infinite Emoji on bingo popup
spin-slow 3s infinite on bingo popup
spin-slow-reverse 3s infinite reverse 🎊 on bingo popup
toast 5s forwards "Chờ X" notification fade in/build
cell-crossed::after instant Red diagonal line in marked cells

Offline Capability

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)