diff --git a/plans/260428-0927-implement-todo-backlog/phase-05-cho-row-indicator.md b/plans/260428-0927-implement-todo-backlog/phase-05-cho-row-indicator.md index 04ee225..b415b74 100644 --- a/plans/260428-0927-implement-todo-backlog/phase-05-cho-row-indicator.md +++ b/plans/260428-0927-implement-todo-backlog/phase-05-cho-row-indicator.md @@ -1,9 +1,10 @@ --- name: Per-row "Chờ" indicator phase: 5 -status: todo +status: completed priority: medium effort: 1h +completed: 2026-04-28 --- # Phase 5 — Per-row "Chờ" visual indicator diff --git a/plans/260428-0927-implement-todo-backlog/phase-06-confetti-polish.md b/plans/260428-0927-implement-todo-backlog/phase-06-confetti-polish.md index 2442c56..e0ddcf5 100644 --- a/plans/260428-0927-implement-todo-backlog/phase-06-confetti-polish.md +++ b/plans/260428-0927-implement-todo-backlog/phase-06-confetti-polish.md @@ -1,9 +1,10 @@ --- name: Confetti polish phase: 6 -status: todo +status: completed priority: low effort: 30m +completed: 2026-04-28 --- # Phase 6 — Confetti polish diff --git a/plans/260428-0927-implement-todo-backlog/plan.md b/plans/260428-0927-implement-todo-backlog/plan.md index efc5284..90c57e5 100644 --- a/plans/260428-0927-implement-todo-backlog/plan.md +++ b/plans/260428-0927-implement-todo-backlog/plan.md @@ -21,8 +21,8 @@ YAGNI — parking-lot features and upstream-blocked items skipped. | 2 | CI inline-script guard ✅ | `phase-02-ci-inline-script-guard.md` | | 3 | Mode picker glyph redesign ✅ | `phase-03-mode-picker-glyphs.md` | | 4 | Settings modal sticky on small screens ✅ | `phase-04-settings-modal-sticky.md` | -| 5 | Per-row "Chờ" indicator | `phase-05-cho-row-indicator.md` | -| 6 | Confetti polish (threshold + variety) | `phase-06-confetti-polish.md` | +| 5 | Per-row "Chờ" indicator ✅ | `phase-05-cho-row-indicator.md` | +| 6 | Confetti polish (threshold + variety) ✅ | `phase-06-confetti-polish.md` | | 7 | Strict CSP via hashed inline script | `phase-07-strict-csp-hashed.md` | | 8 | Audio cache LRU rule | `phase-08-audio-cache-lru.md` | | 9 | PWA install verification checklist | `phase-09-pwa-verify-install.md` | diff --git a/src/app.css b/src/app.css index 62150c6..315d084 100644 --- a/src/app.css +++ b/src/app.css @@ -133,6 +133,24 @@ body { ); } +/* Persistent visual cue when any of the section's 3 rows is in "Chờ" + (one cell from bingo). Amber matches the toast — `bg-amber-500/95` + in PlayerBoard — so users associate the same colour with the same + meaning. Inset ring so the label band keeps its natural width. */ +.section-label-waiting { + box-shadow: inset 0 0 0 2px rgb(245 158 11 / 0.6); + animation: section-pulse 2.4s ease-in-out infinite; +} + +@keyframes section-pulse { + 0%, 100% { box-shadow: inset 0 0 0 2px rgb(245 158 11 / 0.45); } + 50% { box-shadow: inset 0 0 0 2px rgb(245 158 11 / 0.85); } +} + +@media (prefers-reduced-motion: reduce) { + .section-label-waiting { animation: none; } +} + /* Tân Tân BAMBOORAFT-style number — tall, condensed, heavy black. Stack prefers a system condensed face; falls back to negative letter-spacing on platforms without one so the digits still read tall-and-tight. */ @@ -187,7 +205,7 @@ body { position: absolute; top: 0; left: var(--x); - font-size: 2rem; + font-size: var(--size, 2rem); animation: confetti-fall 1.6s ease-in var(--delay) forwards; will-change: transform, opacity; } diff --git a/src/lib/PlayerBoard.svelte b/src/lib/PlayerBoard.svelte index 677320f..dc30b15 100644 --- a/src/lib/PlayerBoard.svelte +++ b/src/lib/PlayerBoard.svelte @@ -32,7 +32,9 @@ // 12 confetti emoji indices. Stable per-render — values don't matter, // only the count drives the {#each}. const CONFETTI = Array.from({ length: 12 }, (_, i) => i); - const CONFETTI_EMOJI = ["🎊", "✨", "🎉", "🥳"]; + // Hội-chợ flavour added (lantern, bamboo, chopsticks) so the + // celebration set reads as Vietnamese fair, not generic party. + const CONFETTI_EMOJI = ["🎊", "✨", "🎉", "🥳", "🥢", "🎋", "🏮"]; // Plain refs (not reactive) /** @type {ReturnType | null} */ @@ -51,6 +53,18 @@ : [] ); + // Per-row "Chờ" flag — one cell away AND not already complete. Reuses + // rowCompleteness so we don't re-walk the row twice. + const waitingRows = $derived( + grid && crossed.length + ? grid.map( + (_, r) => + !rowCompleteness[r] && + getWaitingNumber(grid, crossed, r) !== null, + ) + : [] + ); + function dismissToast() { toast = null; if (toastTimer) { @@ -110,7 +124,19 @@ notifiedWaitingRows.add(i); congratsRow = i + 1; showCongrats = true; - celebrationTier = celebratedRows.size >= 3 ? 2 : 1; + // Tier 2 confetti: 2nd bingo, OR 1st bingo while another row + // is one cell away. The previous "3+ bingos" threshold rarely + // fired on a 9-row card so most wins felt under-celebrated. + const hasActiveCho = grid.some( + (_, r) => + !celebratedRows.has(r) && + getWaitingNumber(grid, crossed, r) !== null, + ); + celebrationTier = + celebratedRows.size >= 2 || + (celebratedRows.size >= 1 && hasActiveCho) + ? 2 + : 1; if (announce) playBingo(); break; } @@ -219,6 +245,14 @@ "TN1 (2014-2017)", "Độc-Đỉnh-Điên", ]; + + // Persistent ring on the section band when any of its 3 rows is + // waiting on a single number. Reduces reliance on the 5s toast. + const sectionHasWaiting = $derived( + SECTIONS.map((startRow) => + waitingRows.slice(startRow, startRow + 3).some(Boolean) + ) + );
@@ -257,7 +291,11 @@
{#each SECTIONS as startRow, sectionIdx (sectionIdx)} - +
{#each grid.slice(startRow, startRow + 3).flat() as num, idx (idx)} {@const row = startRow + Math.floor(idx / 9)} @@ -378,6 +416,7 @@ style:--x="{(i * 8.3 + (i % 3) * 11) % 100}%" style:--delay="{(i * 37) % 400}ms" style:--rot="{(i * 67) % 360}deg" + style:--size="{(1.5 + ((i * 13) % 11) / 10).toFixed(2)}rem" > {CONFETTI_EMOJI[i % CONFETTI_EMOJI.length]}