package com.peachpasswords.ui.components 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.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.peachpasswords.sync.SyncIndicator import com.peachpasswords.ui.theme.LocalReduceMotion import com.peachpasswords.ui.theme.peachColors /** * The sync indicator light, shared by the vault list and the Sync settings card * so the two can never disagree. Green pulsing while a sync is running, solid * green on a healthy pair, red on a problem, unlit when there is nothing to * report — the same four states as the browser's `stk-pip`. * * [label] is not decoration: colour alone is never the signal, so it carries * the same claim in words for TalkBack and for a long-press tooltip. */ @Composable fun SyncIndicatorDot( indicator: SyncIndicator, label: String, modifier: Modifier = Modifier, size: Dp = 6.dp, ) { val color by animateColorAsState( targetValue = when (indicator) { // Busy is green too, not amber: it is the same healthy sync, in // progress. The pulse carries "in progress", the hue carries "well". SyncIndicator.BUSY, SyncIndicator.OK -> peachColors.SyncOk SyncIndicator.WARN -> peachColors.SyncWarn SyncIndicator.ERROR -> peachColors.SyncErr SyncIndicator.IDLE -> peachColors.T2 }, label = "sync-indicator", ) val pulsing = indicator == SyncIndicator.BUSY && !LocalReduceMotion.current val alpha = if (pulsing) { val transition = rememberInfiniteTransition(label = "sync-pulse") val value by transition.animateFloat( initialValue = 1f, targetValue = 0.3f, animationSpec = infiniteRepeatable( animation = tween(900), repeatMode = RepeatMode.Reverse, ), label = "sync-pulse-alpha", ) value } else { 1f } Box( modifier .size(size) .alpha(alpha) .clip(CircleShape) .background(color) .semantics { contentDescription = label }, ) }