mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-13 10:15:54 +00:00
feat(player-board): persistent Chờ ring + livelier confetti
Three player-board polish tweaks: 1. Per-section Chờ indicator. Each section's label band glows amber while any of its 3 rows is one cell from bingo. Uses the same amber as the toast so colour stays consistent. Animation respects prefers-reduced-motion. 2. Confetti threshold drops from 3+ bingos to: 2nd bingo OR 1st bingo while another row is in Chờ. The old threshold rarely fired on a 9-row card so most wins felt under-celebrated. 3. Confetti emoji set adds 🥢 🎋 🏮 (chopsticks, bamboo, lantern) for hội-chợ flavour, plus per-piece size jitter (1.5–2.4rem) via a --size CSS variable.
This commit is contained in:
1 parent
2b74b2e2e9
commit
a60ea08f37
5 files changed
+67
-8
No files matched your search
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: Per-row "Chờ" indicator
|
name: Per-row "Chờ" indicator
|
||||||
phase: 5
|
phase: 5
|
||||||
status: todo
|
status: completed
|
||||||
priority: medium
|
priority: medium
|
||||||
effort: 1h
|
effort: 1h
|
||||||
|
completed: 2026-04-28
|
||||||
---
|
---
|
||||||
|
|
||||||
# Phase 5 — Per-row "Chờ" visual indicator
|
# Phase 5 — Per-row "Chờ" visual indicator
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: Confetti polish
|
name: Confetti polish
|
||||||
phase: 6
|
phase: 6
|
||||||
status: todo
|
status: completed
|
||||||
priority: low
|
priority: low
|
||||||
effort: 30m
|
effort: 30m
|
||||||
|
completed: 2026-04-28
|
||||||
---
|
---
|
||||||
|
|
||||||
# Phase 6 — Confetti polish
|
# Phase 6 — Confetti polish
|
||||||
|
|||||||
@@ -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` |
|
| 2 | CI inline-script guard ✅ | `phase-02-ci-inline-script-guard.md` |
|
||||||
| 3 | Mode picker glyph redesign ✅ | `phase-03-mode-picker-glyphs.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` |
|
| 4 | Settings modal sticky on small screens ✅ | `phase-04-settings-modal-sticky.md` |
|
||||||
| 5 | Per-row "Chờ" indicator | `phase-05-cho-row-indicator.md` |
|
| 5 | Per-row "Chờ" indicator ✅ | `phase-05-cho-row-indicator.md` |
|
||||||
| 6 | Confetti polish (threshold + variety) | `phase-06-confetti-polish.md` |
|
| 6 | Confetti polish (threshold + variety) ✅ | `phase-06-confetti-polish.md` |
|
||||||
| 7 | Strict CSP via hashed inline script | `phase-07-strict-csp-hashed.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` |
|
| 8 | Audio cache LRU rule | `phase-08-audio-cache-lru.md` |
|
||||||
| 9 | PWA install verification checklist | `phase-09-pwa-verify-install.md` |
|
| 9 | PWA install verification checklist | `phase-09-pwa-verify-install.md` |
|
||||||
|
|||||||
+19
-1
@@ -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
|
/* Tân Tân BAMBOORAFT-style number — tall, condensed, heavy black. Stack
|
||||||
prefers a system condensed face; falls back to negative letter-spacing
|
prefers a system condensed face; falls back to negative letter-spacing
|
||||||
on platforms without one so the digits still read tall-and-tight. */
|
on platforms without one so the digits still read tall-and-tight. */
|
||||||
@@ -187,7 +205,7 @@ body {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: var(--x);
|
left: var(--x);
|
||||||
font-size: 2rem;
|
font-size: var(--size, 2rem);
|
||||||
animation: confetti-fall 1.6s ease-in var(--delay) forwards;
|
animation: confetti-fall 1.6s ease-in var(--delay) forwards;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@
|
|||||||
// 12 confetti emoji indices. Stable per-render — values don't matter,
|
// 12 confetti emoji indices. Stable per-render — values don't matter,
|
||||||
// only the count drives the {#each}.
|
// only the count drives the {#each}.
|
||||||
const CONFETTI = Array.from({ length: 12 }, (_, i) => i);
|
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)
|
// Plain refs (not reactive)
|
||||||
/** @type {ReturnType<typeof setTimeout> | null} */
|
/** @type {ReturnType<typeof setTimeout> | 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() {
|
function dismissToast() {
|
||||||
toast = null;
|
toast = null;
|
||||||
if (toastTimer) {
|
if (toastTimer) {
|
||||||
@@ -110,7 +124,19 @@
|
|||||||
notifiedWaitingRows.add(i);
|
notifiedWaitingRows.add(i);
|
||||||
congratsRow = i + 1;
|
congratsRow = i + 1;
|
||||||
showCongrats = true;
|
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();
|
if (announce) playBingo();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -219,6 +245,14 @@
|
|||||||
"TN1 (2014-2017)",
|
"TN1 (2014-2017)",
|
||||||
"Độc-Đỉnh-Điên",
|
"Độ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)
|
||||||
|
)
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-center gap-3 mb-6">
|
<div class="flex flex-wrap justify-center gap-3 mb-6">
|
||||||
@@ -257,7 +291,11 @@
|
|||||||
<div class="section-divider-vertical" aria-hidden="true"></div>
|
<div class="section-divider-vertical" aria-hidden="true"></div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
{#each SECTIONS as startRow, sectionIdx (sectionIdx)}
|
{#each SECTIONS as startRow, sectionIdx (sectionIdx)}
|
||||||
<div class="section-label">{SECTION_LABELS[sectionIdx]}</div>
|
<div
|
||||||
|
class="section-label {sectionHasWaiting[sectionIdx] ? 'section-label-waiting' : ''}"
|
||||||
|
>
|
||||||
|
{SECTION_LABELS[sectionIdx]}
|
||||||
|
</div>
|
||||||
<div class="loto-grid">
|
<div class="loto-grid">
|
||||||
{#each grid.slice(startRow, startRow + 3).flat() as num, idx (idx)}
|
{#each grid.slice(startRow, startRow + 3).flat() as num, idx (idx)}
|
||||||
{@const row = startRow + Math.floor(idx / 9)}
|
{@const row = startRow + Math.floor(idx / 9)}
|
||||||
@@ -378,6 +416,7 @@
|
|||||||
style:--x="{(i * 8.3 + (i % 3) * 11) % 100}%"
|
style:--x="{(i * 8.3 + (i % 3) * 11) % 100}%"
|
||||||
style:--delay="{(i * 37) % 400}ms"
|
style:--delay="{(i * 37) % 400}ms"
|
||||||
style:--rot="{(i * 67) % 360}deg"
|
style:--rot="{(i * 67) % 360}deg"
|
||||||
|
style:--size="{(1.5 + ((i * 13) % 11) / 10).toFixed(2)}rem"
|
||||||
>
|
>
|
||||||
{CONFETTI_EMOJI[i % CONFETTI_EMOJI.length]}
|
{CONFETTI_EMOJI[i % CONFETTI_EMOJI.length]}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in new issue
Block a user