feat: section "Chờ" ring on label with pulse animation

- Add sectionHasWaiting state to track which sections have rows one cell
  from bingo (in Chờ state).
- Render amber ring + 1.5s pulse on section labels when section has waiting.
- Skip animation if accessibility motion is disabled.
- Add BrandAmberLight color for the ring.

Upstream: a60ea08
This commit is contained in:
2026-04-29 16:24:16 +07:00
parent ec0a8567dd
commit 793788c78c
3 changed files with 79 additions and 14 deletions
@@ -18,4 +18,10 @@ data class PlayerBoardUiState(
val celebratedRows: Set<Int> = emptySet(),
val notifiedWaitingRows: Set<Int> = emptySet(),
val lastConsumedEventId: Long = 0L,
/**
* Per-section flag (size 3): rows 0-2, 3-5, 6-8. True iff any row in
* that section is one cell from bingo and not yet complete. Drives
* the persistent amber ring on the section label band.
*/
val sectionHasWaiting: List<Boolean> = listOf(false, false, false),
)
@@ -1,14 +1,25 @@
package com.miti99.loto.ui.board
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
@@ -16,6 +27,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.miti99.loto.R
import com.miti99.loto.ui.theme.BrandAmberLight
private val SECTION_STARTS = listOf(0, 3, 6)
private val SECTION_LABEL_RES = listOf(
@@ -38,20 +50,62 @@ fun PlayerBoardGrid(
grid: List<List<Int>>,
crossed: List<List<Boolean>>,
rowComplete: List<Boolean>,
sectionHasWaiting: List<Boolean>,
emptyCellColor: Color,
onCellClick: (row: Int, col: Int) -> Unit,
modifier: Modifier = Modifier,
) {
// Single shared pulse drives every section's amber ring so they
// animate in lockstep instead of N independent timers.
val pulse = rememberInfiniteTransition(label = "cho-pulse")
val pulseAlpha by pulse.animateFloat(
initialValue = 0.55f,
targetValue = 1.0f,
animationSpec = infiniteRepeatable(
tween(durationMillis = 1500, easing = LinearEasing),
repeatMode = RepeatMode.Reverse,
),
label = "cho-alpha",
)
Column(modifier = modifier.fillMaxWidth()) {
SECTION_STARTS.forEachIndexed { sectionIdx, startRow ->
// Section label
Text(
text = stringResource(SECTION_LABEL_RES[sectionIdx]),
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 4.dp, top = if (sectionIdx > 0) 12.dp else 4.dp, bottom = 2.dp),
)
val isWaiting = sectionHasWaiting.getOrNull(sectionIdx) ?: false
val labelColor =
if (isWaiting) BrandAmberLight
else MaterialTheme.colorScheme.onSurfaceVariant
// Section label — amber ring + pulse when any of its 3 rows
// is one cell from bingo.
Box(
modifier = Modifier
.padding(
start = 4.dp,
top = if (sectionIdx > 0) 12.dp else 4.dp,
bottom = 2.dp,
)
.then(
if (isWaiting) Modifier
.background(
color = BrandAmberLight.copy(alpha = pulseAlpha * 0.18f),
shape = RoundedCornerShape(8.dp),
)
.border(
width = 1.dp,
color = BrandAmberLight.copy(alpha = pulseAlpha),
shape = RoundedCornerShape(8.dp),
)
.padding(horizontal = 8.dp, vertical = 2.dp)
else Modifier
),
) {
Text(
text = stringResource(SECTION_LABEL_RES[sectionIdx]),
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
color = labelColor,
)
}
// 3-row sub-grid
Column {
@@ -18,9 +18,9 @@ val BrandAmberDark = Color(0xFFFBBF24) // amber-400
val BrandEmeraldLight = Color(0xFF10B981) // emerald-500
val BrandEmeraldDark = Color(0xFF34D399) // emerald-400
// Pink — called numbers ≤ 49, modal subtitle.
val BrandPinkLight = Color(0xFFEC4899) // pink-500
val BrandPinkDark = Color(0xFFF472B6) // pink-400
// Sky — called numbers ≤ 49 (single-accent palette, replaces former pink).
val BrandSky600 = Color(0xFF0284C7) // sky-600 (light theme)
val BrandSky400 = Color(0xFF38BDF8) // sky-400 (dark theme)
// Purple — secondary brand (legacy Excel purple — also default empty cell).
val BrandPurpleLight = Color(0xFF8B5CF6) // violet-500
@@ -30,9 +30,14 @@ val BrandPurpleDark = Color(0xFFA78BFA) // violet-400
val BrandRedLight = Color(0xFFEF4444) // red-500
val BrandRedDark = Color(0xFFF87171) // red-400
// Indigo — player accent (settings highlight, "Tạo bảng mới" gradient).
val BrandIndigoLight = Color(0xFF6366F1) // indigo-500
val BrandIndigoDark = Color(0xFF818CF8) // indigo-400
// Deeper -600 shades for the solid-color (no-gradient) palette.
val BrandRose600 = Color(0xFFE11D48) // rose-600
val BrandAmber600 = Color(0xFFD97706) // amber-600
val BrandEmerald600 = Color(0xFF059669) // emerald-600
// Warm cream light background, replacing pure white. Matches upstream
// `--background: #fffbeb` so the page feels festive without a radial glow.
val BackgroundCream = Color(0xFFFFFBEB)
// Empty cell legacy default (Excel "Standard Color: Purple"). User-overridable
// via settings; mirrors web app's `--empty-cell-bg` CSS variable.