All three places where called numbers appear (hero, tracking grid
token, history list) — digit color now follows the ring: pink for
1–49, emerald for 50–90. Drops the rose-700 fallback that was
mismatched against both rings.
Match the hero "Số vừa xổ" weight. font-bold reads thin against the
cream-fill / red-text token; font-black gives the same chunky look
across all three places where called numbers appear.
The hero already stands alone with a thick colored border and a large
digit; the extra red ring-offset accent was overkill. Restored
dark:border-* on the colored border so it adapts cleanly in dark mode.
Drop dark:border-pink-400 / dark:border-emerald-400 on the outer ring.
The hero is already a focal point — single-mode colored border reads
the same in light and dark and avoids a lighter wash in dark mode.
Restore dark:text-rose-800 on the digit (light cream fill stays in dark).
Replaces the rounded-square orange→red gradient with the same circular
token treatment as the tracking grid: cream-yellow fill, thick ring
colored by value (pink ≤49 / green ≥50), dark rose digit. Keeps a
red ring-offset accent so the "just drawn" hero still stands out from
the same-style grid behind it. Bumped to w-24/sm:w-28 with text-5xl/
sm:text-6xl since the digit is the focal point.
Match the BAMBOORAFT physical sheet:
- Cell aspect changed from square to 3:5 (taller than wide), matching
the printed paper proportions
- Number cells rendered with .tan-tan-num: condensed system-font stack
(Arial Narrow / Avenir Next Condensed / Roboto Condensed), font-weight
900, negative letter-spacing for tight fall-back on platforms without
a true condensed face. Sized text-2xl / sm:text-3xl to fill the
taller cells. Black number on white cell.
- Default empty-cell color flipped from Minh Tân brown (#7a4a2b) to
BAMBOORAFT blue (#1e88e5). Settings store + test updated. Users who
picked a different color in settings keep their choice.
- Section dividers and labels recolored from orange to matching blue.
- Section labels relabelled to the BAMBOORAFT trio:
"Tân Tân" / "An khang thịnh vượng" / "Tân Tân tốt nhất"
Same pink/green-by-value treatment as the tracking grid: rounded-full,
3px ring (pink ≤49, green ≥50), cream fill, dark rose number. Bumped
size to w-8/9 + text-sm/base so two digits fit cleanly inside the ring.
- Bump number text to text-lg / sm:text-xl so digits read clearly
inside the circle
- Pink/green rings now indicate *called* state — uncalled numbers get
a neutral slate-gray ring instead, so the board's call progression
is the dominant signal at a glance
Replaces the orange/red filled-square cells on the master tracking
board with a circular token treatment inspired by the Hội chợ "Các
số đã ra" panel. Each numbered cell now renders a centered circle
with a colored ring — pink for 1–49, green for 50–90 — over a
cream-yellow fill when the number has been called, or dimmed/empty
when it has not. The most recent draw keeps its red accent via a
ring-offset highlight + slight scale, so the host can still find
"vừa xổ" at a glance. Draw-order superscript stays at the cell's
top-right corner, now in muted slate (visible against both the
cream-filled and dim-empty token states).
Adds vitest + happy-dom as devDependencies and npm scripts test
(one-shot) and test:watch. No SvelteKit/vite reconfig needed —
vitest auto-picks up the existing vite plugin chain.
game-logic.test.js (26 tests): generateGrid shape invariants
(9x9, exact 5/row, exact 5/col, no duplicates) over 200 random
trials; per-column number ranges (col 0 = 1-9, col 8 = 80-90);
ascending-within-column rule; isRowComplete/getWaitingNumber edge
cases; full save/load roundtrip for the persistence layer
including corrupt-JSON and wrong-shape rejection.
settings-store.test.js (12 tests): defaults frozen, load with
valid/invalid/corrupt payloads, hex regex rejects shorthand,
save persists to localStorage and pushes CSS var, reset returns
to brown default. Uses happy-dom env for localStorage and
documentElement.
code-standards.md: documents the rune globals declaration and the
.svelte.js convention for rune-using non-component modules.
Adds a gear button + modal on /, /master with a native color picker
and 8 preset swatches for the empty-cell background. The selected
color repaints both the player card AND the master tracking grid via
the --empty-cell-bg CSS variable. Default brown matches the physical
Minh Tân paper card.
settings-store.svelte.js: Svelte 5 rune-based reactive store, state
hydrated on layout mount, persisted to localStorage key loto_settings.
Validates hex (#rrggbb regex) before applying — keeps the CSS sink
safe and ignores corrupt or shorthand-3-digit payloads.
SettingsButton.svelte: gear icon, modal with picker + presets + reset
+ close, escape-to-close via window keydown listener (the trigger
button retains focus on open, so a dialog-scoped handler would miss).
eslint.config.mjs: declares Svelte 5 rune globals so .svelte.js stores
lint clean.
Docs synced: PDR, codebase-summary, system-architecture, roadmap.
Render the 9x9 player card as 3 stacked 3x9 mini-cards mirroring a
physical Minh Tân paper sheet, with traditional separator labels:
"Minh Tân", "Loại đặc biệt", "Tấn tài tấn lộc". Pure visual change —
underlying 9x9 data, generator, click handlers, win detection all
unchanged. Sectioned render uses absolute row index (startRow + r) so
crossed-state and rowCompleteness lookups stay correct.
Adds .section-divider (cross-hatch repeating gradient) and .section-label
styles to app.css. Defines --empty-cell-bg CSS variable with a brown
default matching the paper card; the runtime override lands with the
settings feature in a follow-up commit.
- master board: 11x9 last-digit aligned (col 0 = 1-9, col 8 = 80-90);
fixes missing ones-digit-9 row and aligns 1/11/21/.../81 horizontally
- master board: each called cell shows 1-based draw order so host can
glance across a winning row to verify "Kinh!"
- player card: constraint-aware picker guarantees exactly 5 numbers per
row AND per column (was loose weighted random allowing 4-6 per col)
- player card: numbers within each column now placed ascending
top-to-bottom per lô tô hội chợ tân tân convention
CF Pages reported: Error: Output directory 'public' not found. The
dashboard had defaulted to 'public' but SvelteKit adapter-static writes
to its own output dir.
wrangler.toml with pages_build_output_dir makes the project reproducible
from git — no manual dashboard tweak required for the output path.
The build command in the CF dashboard still controls how the build
runs; ensure it is set to `npm run b u i l d`.
Cloudflare Pages build failed with:
Error: Cannot find module '../lightningcss.linux-x64-gnu.node'
Tailwind 4 → @tailwindcss/vite → lightningcss has per-platform native
binaries shipped as optional npm packages. The lockfile generated on
this dev box (linux-arm64) only carried the arm64 binaries, so CF on
linux-x64 was missing the matching platform-specific package.
Adding lightningcss-linux-x64-gnu to optionalDependencies pins it into
the lockfile. On non-x64-linux machines npm skips it silently due to
the platform mismatch; CF Pages on linux-x64 will install it.
Pinned to 1.32.0 to match the parent dep already resolved.
Replace the full build+publish flow with a tiny inline shell step that
emits two static HTML pages (out/index.html and out/master/index.html).
Each one carries:
<meta http-equiv="refresh" content="0; url=https://loto.miti99.com/">
<script>location.replace("https://loto.miti99.com" + ...)</script>
The script preserves path / query / hash, so
/loto/ → loto.miti99.com/
/loto/master → loto.miti99.com/master
/loto/?x=1 → loto.miti99.com/?x=1
Cloudflare Pages stays canonical; this change just stops GH Pages from
serving a stale duplicate of the app and points old links at the live
domain instead.
The build:gh npm script is kept as a manual escape hatch for the rare
case where someone wants to deploy a real GH Pages copy by hand.
Symptom: 'Blocked request. This host (codeserver.sg.miti99.com) is not
allowed.' — the configs were reading process.env.CODESERVER_HOST, which
is empty at config-eval time because Vite reads .env.local at runtime,
not into process.env.
Switch both vite.config.js and svelte.config.js to use loadEnv() from
vite, which reads .env.local explicitly. Also lift allowedHosts out of
the codeserver profile so plain `npm run dev` is also reachable from
the codeserver proxy when CODESERVER_HOST is set in .env.local.
.github/workflows/deploy-github-pages.yml runs `npm run build:gh` on push
to main and uploads build/ via actions/upload-pages-artifact@v3 +
actions/deploy-pages@v4.
The build:gh script sets BUILD_PROFILE=gh, which switches svelte.config.js
basePath to /loto so assets resolve under tiennm99.github.io/loto.
Cloudflare Pages keeps deploying in parallel via the CF dashboard
(npm run build, root basePath, loto.miti99.com). Two URLs, no conflict.
- Default `npm run build` now produces a root-relative build (CF Pages
custom domain at loto.miti99.com). The /loto basePath is opt-in via
`npm run build:gh` for the rare manual GH Pages export.
- Removed both GitHub Actions deploy workflows (.github/workflows/) and
the dangling `build:cf` script (it was identical to `build` after the
default flip).
- next.config.mjs: simplified basePath logic — only `BUILD_PROFILE=gh`
toggles a non-empty path; everything else (CF, local dev) is root.
Two named build scripts replace the prior CF_PAGES auto-detect:
npm run build:gh → BUILD_PROFILE=gh, basePath /loto
target: https://tiennm99.github.io/loto
npm run build:cf → BUILD_PROFILE=cf, basePath ""
target: https://loto.miti99.com (CF Pages custom domain)
Both deploy workflows now use the matching profile script. The legacy
`npm run build` keeps its prior behaviour (defaults to /loto basePath) so
nothing else in the toolchain breaks. NEXT_BASE_PATH still wins for any
one-off custom-domain build.
CF_PAGES auto-detection removed — explicit profiles are clearer than
relying on the host injecting a magic env var, and dashboard users should
just set the build command to `npm run build:cf`.
next.config.mjs now detects CF_PAGES=1 (auto-injected by Cloudflare during
build) and switches basePath from /loto to "" — assets resolve at the
project root on loto.pages.dev. GH Pages keeps /loto unchanged.
New workflow .github/workflows/deploy-cloudflare-pages.yml mirrors the GH
Pages flow but publishes via cloudflare/wrangler-action@v3. Requires repo
secrets CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID; project name "loto"
is hardcoded — adjust --project-name= in the workflow if your CF Pages
project uses a different name.
Dashboard-based deploys also work without code changes thanks to the
CF_PAGES detection. See docs/deployment-guide.md for both paths.
After the JS+JSDoc conversion, some TS-flavored bits lingered. Removed:
- // @ts-check directives (TS-specific pragma)
- JSDoc annotations referencing TS-defined types: import('next').NextConfig,
React.MutableRefObject, React.Dispatch, React.SetStateAction
- jsconfig.json (TS-server-flavored config; only kept it for the @/* alias)
@/* imports replaced with relative paths so jsconfig is no longer needed.
Remaining JSDoc is plain @param / @returns — vanilla JS, no TS dependency.
Build, lint, dev profiles unchanged.
`app/` should hold route segments only. Game logic and the player-card
component are imported by both routes, so they belong outside:
app/loto-game-logic.ts -> lib/game-logic.ts
app/loto-player-board.tsx -> components/player-board.tsx
Imports use the existing @/* alias. Drops the redundant `loto-` prefix.
Also fixes the package name from the scaffold default.
Adds the standard ./docs/ structure (overview, codebase summary,
architecture, code standards, design guidelines, deployment guide,
roadmap) and the code-review report under ./plans/reports/.
README now points at the docs and covers the codeserver dev profile.
- Validate localStorage shape on load (typed parse, guarded set)
- Fix isRowComplete to require at least one number per row
- Split row-scan effect into bingo + waiting passes so updates to
later rows are not skipped by an early return
- Memoize per-row completeness; was recomputing 81 times per render
- Replace clickable divs with real <button>s; aria-label, aria-pressed,
focus ring, dialog role + escape on the bingo modal
- Freeze the master tracking board to make module-scope state safer
- Allow NEXT_BASE_PATH to override the prod default for forks /
custom-domain deploys
NEXT_DEV_PROFILE=codeserver wires basePath, assetPrefix, and
allowedDevOrigins from CODESERVER_HOST/PORT in .env.local. Use
/absproxy/{port} so code-server preserves the path prefix and Next's
basePath matches the incoming request.
Run with `npm run dev:codeserver` and access via
https://<host>/absproxy/<port>/.
Master page now embeds a separate playing card so the host can play along
while drawing numbers. Extracted shared grid logic into a reusable
PlayerBoard component with a configurable storage prefix.
Show animated congrats when all 5 numbers in a row are crossed.
Show "Chờ [number]" toast overlay when 4/5 crossed. Auto-fades
after 5s or dismissible by tap.
Replace vanilla HTML/JS/CSS with Next.js App Router, TypeScript,
and Tailwind CSS. Responsive design with dark mode support.
All original game logic preserved: grid generation, click-to-cross,
localStorage persistence.