feat: answer reveal, auth screens, challenges, onboarding, pairing, paywall, wheel, settings, components

This commit is contained in:
null 2026-06-22 19:18:49 -05:00
parent b42544bafb
commit 125a24eb85
31 changed files with 339 additions and 175 deletions

View File

@ -25,7 +25,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
@ -141,7 +141,7 @@ private fun AnswerRevealContent(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp) horizontalArrangement = Arrangement.spacedBy(14.dp)
) { ) {
CircularProgressIndicator(color = Color(0xFFB98AF4)) CloserHeartLoader(size = 32.dp)
Text("Loading reveal") Text("Loading reveal")
} }
} }
@ -451,7 +451,7 @@ private fun ReleasingKeyState() {
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp) horizontalArrangement = Arrangement.spacedBy(14.dp)
) { ) {
CircularProgressIndicator(color = Color(0xFFB98AF4)) CloserHeartLoader(size = 32.dp)
Text( Text(
text = "Opening reveal…", text = "Opening reveal…",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,

View File

@ -20,7 +20,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -157,11 +157,7 @@ fun ForgotPasswordScreen(
contentColor = AuthOnPrimary contentColor = AuthOnPrimary
) )
) { ) {
if (state.isLoading) CircularProgressIndicator( if (state.isLoading) CloserHeartLoader(size = 22.dp)
modifier = Modifier.size(20.dp),
color = AuthOnPrimary,
strokeWidth = 2.dp
)
else Text("Send reset email", style = MaterialTheme.typography.labelLarge) else Text("Send reset email", style = MaterialTheme.typography.labelLarge)
} }
} }

View File

@ -26,7 +26,6 @@ import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -58,6 +57,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import app.closer.core.navigation.AppRoute import app.closer.core.navigation.AppRoute
import app.closer.ui.components.BrandMessageRotator import app.closer.ui.components.BrandMessageRotator
import app.closer.ui.components.CloserHeartLoader
@Composable @Composable
fun LoginScreen( fun LoginScreen(
@ -176,7 +176,7 @@ fun LoginScreen(
contentColor = AuthOnPrimary contentColor = AuthOnPrimary
) )
) { ) {
if (state.isLoading) CircularProgressIndicator(color = AuthOnPrimary, strokeWidth = 2.dp) if (state.isLoading) CloserHeartLoader(size = 22.dp)
else Text("Sign in", style = MaterialTheme.typography.labelLarge) else Text("Sign in", style = MaterialTheme.typography.labelLarge)
} }

View File

@ -20,7 +20,7 @@ import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -179,7 +179,7 @@ fun SignUpScreen(
contentColor = AuthOnPrimary contentColor = AuthOnPrimary
) )
) { ) {
if (state.isLoading) CircularProgressIndicator(color = AuthOnPrimary, strokeWidth = 2.dp) if (state.isLoading) CloserHeartLoader(size = 22.dp)
else Text("Create account", style = MaterialTheme.typography.labelLarge) else Text("Create account", style = MaterialTheme.typography.labelLarge)
} }

View File

@ -29,7 +29,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -241,7 +241,7 @@ fun ConnectionChallengesScreen(
@Composable @Composable
private fun ChallengesLoadingScreen() { private fun ChallengesLoadingScreen() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(color = CloserPalette.PurpleDeep) CloserHeartLoader()
} }
} }

View File

@ -1,29 +1,35 @@
package app.closer.ui.components package app.closer.ui.components
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerCardColor import app.closer.ui.theme.closerCardColor
import androidx.compose.animation.core.LinearEasing import android.provider.Settings
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.PathParser
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
@ -31,8 +37,6 @@ fun LoadingState(
message: String = "Loading…", message: String = "Loading…",
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val shimmer = closerSkeletonBrush()
CloserCard( CloserCard(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
containerColor = closerCardColor(alpha = 0.8f) containerColor = closerCardColor(alpha = 0.8f)
@ -41,23 +45,10 @@ fun LoadingState(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(CloserSpacing.Xxxl), .padding(CloserSpacing.Xxxl),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(CloserSpacing.Md) verticalArrangement = Arrangement.spacedBy(CloserSpacing.Md)
) { ) {
SkeletonLine( CloserHeartLoader()
brush = shimmer,
modifier = Modifier.fillMaxWidth(0.42f),
height = 18.dp
)
SkeletonLine(
brush = shimmer,
modifier = Modifier.fillMaxWidth(),
height = 13.dp
)
SkeletonLine(
brush = shimmer,
modifier = Modifier.fillMaxWidth(0.72f),
height = 13.dp
)
Text( Text(
text = message, text = message,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
@ -73,39 +64,90 @@ fun LoadingState(
} }
@Composable @Composable
private fun closerSkeletonBrush(): Brush { fun CloserHeartLoader(
val transition = rememberInfiniteTransition(label = "closerSkeleton") modifier: Modifier = Modifier,
val shimmerOffset = transition.animateFloat( size: Dp = 76.dp
initialValue = -320f, ) {
targetValue = 640f, val context = LocalContext.current
val reducedMotion = remember {
Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
1f
) == 0f
}
val transition = rememberInfiniteTransition(label = "closerHeartLoader")
val animatedFill = transition.animateFloat(
initialValue = 0.08f,
targetValue = 1f,
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1200, easing = LinearEasing), animation = tween(durationMillis = 1500, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Restart repeatMode = RepeatMode.Restart
), ),
label = "closerSkeletonOffset" label = "closerHeartFill"
) )
val animatedPulse = transition.animateFloat(
return Brush.linearGradient( initialValue = 0.96f,
colors = listOf( targetValue = 1.04f,
CloserPalette.PurpleMist.copy(alpha = 0.55f), animationSpec = infiniteRepeatable(
CloserPalette.PurpleSoft.copy(alpha = 0.95f), animation = tween(durationMillis = 900, easing = FastOutSlowInEasing),
CloserPalette.PinkMist.copy(alpha = 0.72f) repeatMode = RepeatMode.Reverse
), ),
start = Offset(shimmerOffset.value, 0f), label = "closerHeartPulse"
end = Offset(shimmerOffset.value + 280f, 0f)
) )
}
@Composable val fillProgress = if (reducedMotion) 1f else animatedFill.value
private fun SkeletonLine( val pulse = if (reducedMotion) 1f else animatedPulse.value
brush: Brush, val shadowPath = remember {
modifier: Modifier = Modifier, PathParser().parsePathString(
height: androidx.compose.ui.unit.Dp "M54,89C48,82 25,65 20,50C15,35 23,22 37,22C45,22 51,26 54,33C57,26 63,22 71,22C85,22 93,35 88,50C83,65 60,82 54,89Z"
) { ).toPath()
Box( }
val leftPath = remember {
PathParser().parsePathString(
"M54,85C49,79 27,62 22,48C17,35 24,24 37,24C45,24 51,28 54,35Z"
).toPath()
}
val rightPath = remember {
PathParser().parsePathString(
"M54,85C59,79 81,62 86,48C91,35 84,24 71,24C63,24 57,28 54,35Z"
).toPath()
}
val leftHighlight = remember {
PathParser().parsePathString(
"M27,42C28,32 34,27 42,27C48,27 52,30 54,35L54,41C47,36 37,36 27,42Z"
).toPath()
}
val rightHighlight = remember {
PathParser().parsePathString(
"M54,35C57,30 62,27 69,27C78,27 84,32 85,42C75,36 65,36 54,41Z"
).toPath()
}
Canvas(
modifier = modifier modifier = modifier
.height(height) .size(size)
.clip(RoundedCornerShape(CloserRadii.Pill)) .graphicsLayer {
.background(brush) scaleX = pulse
) scaleY = pulse
}
.clearAndSetSemantics {}
) {
val scaleX = this.size.width / 108f
val scaleY = this.size.height / 108f
withTransform({
scale(scaleX = scaleX, scaleY = scaleY, pivot = Offset.Zero)
}) {
drawPath(shadowPath, color = Color(0xFF24122F).copy(alpha = 0.10f))
drawPath(leftPath, color = Color(0xFFF7C8E4).copy(alpha = 0.22f))
drawPath(rightPath, color = Color(0xFFD9B8FF).copy(alpha = 0.22f))
clipRect(top = 108f * (1f - fillProgress), bottom = 108f) {
drawPath(leftPath, color = Color(0xFFF7C8E4))
drawPath(rightPath, color = Color(0xFFD9B8FF))
drawPath(leftHighlight, color = Color(0xFFFFF4FA).copy(alpha = 0.68f))
drawPath(rightHighlight, color = Color(0xFFF3E8FF).copy(alpha = 0.52f))
}
}
}
} }

View File

@ -33,7 +33,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -370,9 +370,8 @@ fun DesireSyncScreen(
.background(closerBackgroundBrush()) .background(closerBackgroundBrush())
) { ) {
when (state.phase) { when (state.phase) {
DesireSyncPhase.LOADING -> CircularProgressIndicator( DesireSyncPhase.LOADING -> CloserHeartLoader(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center)
color = CloserPalette.Romantic
) )
DesireSyncPhase.ERROR -> DSErrorScreen( DesireSyncPhase.ERROR -> DSErrorScreen(
message = state.error ?: "Something didn't load. Go back and try again.", message = state.error ?: "Something didn't load. Go back and try again.",
@ -553,7 +552,7 @@ private fun DSWaitingScreen(partnerName: String, onBack: () -> Unit, onAbandon:
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
CircularProgressIndicator(color = CloserPalette.Romantic, strokeWidth = 3.dp) CloserHeartLoader(size = 48.dp)
BrandMessageRotator(style = MaterialTheme.typography.bodySmall) BrandMessageRotator(style = MaterialTheme.typography.bodySmall)
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
OutlinedButton( OutlinedButton(
@ -1020,9 +1019,8 @@ fun DSReplayScreen(
.background(closerBackgroundBrush()) .background(closerBackgroundBrush())
) { ) {
when (phase) { when (phase) {
is DSReplayPhase.Loading -> CircularProgressIndicator( is DSReplayPhase.Loading -> CloserHeartLoader(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center)
color = CloserPalette.Romantic
) )
is DSReplayPhase.Error -> Column( is DSReplayPhase.Error -> Column(
modifier = Modifier modifier = Modifier

View File

@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -130,10 +130,7 @@ fun WaitingForPartnerScreen(
) { ) {
when { when {
state.isLoading -> { state.isLoading -> {
CircularProgressIndicator( CloserHeartLoader(size = 64.dp)
modifier = Modifier.size(48.dp),
color = MaterialTheme.colorScheme.primary
)
Text( Text(
text = "Loading...", text = "Loading...",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,

View File

@ -34,7 +34,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -414,9 +414,8 @@ fun HowWellScreen(
.background(closerBackgroundBrush()) .background(closerBackgroundBrush())
) { ) {
when (state.phase) { when (state.phase) {
HowWellPhase.LOADING -> CircularProgressIndicator( HowWellPhase.LOADING -> CloserHeartLoader(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center)
color = CloserPalette.PurpleDeep
) )
HowWellPhase.ERROR -> HowWellErrorScreen( HowWellPhase.ERROR -> HowWellErrorScreen(
message = state.error ?: "Something didn't load. Go back and try again.", message = state.error ?: "Something didn't load. Go back and try again.",
@ -622,7 +621,7 @@ private fun HowWellWaitingScreen(amSubject: Boolean, partnerName: String, onBack
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
CircularProgressIndicator(color = CloserPalette.PurpleDeep, strokeWidth = 3.dp) CloserHeartLoader(size = 48.dp)
BrandMessageRotator(style = MaterialTheme.typography.bodySmall) BrandMessageRotator(style = MaterialTheme.typography.bodySmall)
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
OutlinedButton( OutlinedButton(
@ -1202,9 +1201,8 @@ fun HowWellReplayScreen(
.background(closerBackgroundBrush()) .background(closerBackgroundBrush())
) { ) {
when (phase) { when (phase) {
is HWReplayPhase.Loading -> CircularProgressIndicator( is HWReplayPhase.Loading -> CloserHeartLoader(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center)
color = CloserPalette.PurpleDeep
) )
is HWReplayPhase.Error -> Column( is HWReplayPhase.Error -> Column(
modifier = Modifier modifier = Modifier

View File

@ -33,7 +33,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -231,7 +231,7 @@ fun MemoryLaneScreen(
) { ) {
when (state.phase) { when (state.phase) {
MemoryLanePhase.LOADING -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { MemoryLanePhase.LOADING -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(color = CloserPalette.PurpleDeep) CloserHeartLoader()
} }
MemoryLanePhase.LIST -> CapsuleListScreen( MemoryLanePhase.LIST -> CapsuleListScreen(
capsules = state.capsules, capsules = state.capsules,
@ -494,7 +494,7 @@ private fun CapsuleCreateScreen(
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep) colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
) { ) {
if (state.isSaving) CircularProgressIndicator(color = Color.White, modifier = Modifier.size(20.dp), strokeWidth = 2.dp) if (state.isSaving) CloserHeartLoader(size = 22.dp)
else Text("Seal the capsule 📦", color = Color.White, style = MaterialTheme.typography.labelLarge) else Text("Seal the capsule 📦", color = Color.White, style = MaterialTheme.typography.labelLarge)
} }

View File

@ -34,7 +34,7 @@ import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.PhotoLibrary import androidx.compose.material.icons.filled.PhotoLibrary
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -255,7 +255,7 @@ private fun NameStep(
contentColor = AuthOnPrimary contentColor = AuthOnPrimary
) )
) { ) {
if (state.isLoading) CircularProgressIndicator(color = AuthOnPrimary, strokeWidth = 2.dp) if (state.isLoading) CloserHeartLoader(size = 22.dp)
else Text("Continue", style = MaterialTheme.typography.labelLarge) else Text("Continue", style = MaterialTheme.typography.labelLarge)
} }
} }
@ -312,7 +312,7 @@ private fun SexStep(
contentColor = AuthOnPrimary contentColor = AuthOnPrimary
) )
) { ) {
if (state.isLoading) CircularProgressIndicator(color = AuthOnPrimary, strokeWidth = 2.dp) if (state.isLoading) CloserHeartLoader(size = 22.dp)
else Text("Continue", style = MaterialTheme.typography.labelLarge) else Text("Continue", style = MaterialTheme.typography.labelLarge)
} }
} }
@ -440,7 +440,7 @@ private fun PhotoStep(
contentColor = AuthOnPrimary contentColor = AuthOnPrimary
) )
) { ) {
if (state.isLoading) CircularProgressIndicator(color = AuthOnPrimary, strokeWidth = 2.dp) if (state.isLoading) CloserHeartLoader(size = 22.dp)
else Text("Continue", style = MaterialTheme.typography.labelLarge) else Text("Continue", style = MaterialTheme.typography.labelLarge)
} }
} }

View File

@ -24,7 +24,6 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -55,6 +54,7 @@ import app.closer.ui.auth.AuthOnPrimary
import app.closer.ui.auth.AuthPrimary import app.closer.ui.auth.AuthPrimary
import app.closer.ui.auth.AuthPrimaryDeep import app.closer.ui.auth.AuthPrimaryDeep
import app.closer.ui.components.BrandMessageRotator import app.closer.ui.components.BrandMessageRotator
import app.closer.ui.components.CloserHeartLoader
import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.CloserPalette
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -85,11 +85,7 @@ fun OnboardingScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp) verticalArrangement = Arrangement.spacedBy(20.dp)
) { ) {
CircularProgressIndicator( CloserHeartLoader()
modifier = Modifier.size(36.dp),
color = AuthPrimary,
strokeWidth = 3.dp
)
BrandMessageRotator(color = AuthMuted) BrandMessageRotator(color = AuthMuted)
} }
} else { } else {

View File

@ -20,7 +20,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.TrendingUp import androidx.compose.material.icons.automirrored.filled.TrendingUp
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -98,7 +98,7 @@ fun YourProgressScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
CircularProgressIndicator() CloserHeartLoader()
} }
} }
state.error != null -> { state.error != null -> {

View File

@ -26,7 +26,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -166,11 +166,7 @@ fun AcceptInviteScreen(
contentColor = SettingsOnPrimary contentColor = SettingsOnPrimary
) )
) { ) {
if (state.isLoading) CircularProgressIndicator( if (state.isLoading) CloserHeartLoader(size = 22.dp)
modifier = Modifier.size(20.dp),
color = SettingsOnPrimary,
strokeWidth = 2.dp
)
else Text("Continue", style = MaterialTheme.typography.labelLarge) else Text("Continue", style = MaterialTheme.typography.labelLarge)
} }

View File

@ -29,7 +29,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -125,7 +125,7 @@ fun CreateInviteScreen(
) { ) {
if (state.isLoading) { if (state.isLoading) {
Spacer(Modifier.height(160.dp)) Spacer(Modifier.height(160.dp))
CircularProgressIndicator(modifier = Modifier.size(40.dp)) CloserHeartLoader(size = 64.dp)
} else if (state.inviteCode != null) { } else if (state.inviteCode != null) {
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))

View File

@ -16,7 +16,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -38,6 +37,7 @@ import app.closer.data.remote.FirestoreCoupleDataSource
import app.closer.domain.repository.AuthRepository import app.closer.domain.repository.AuthRepository
import app.closer.domain.repository.CoupleRepository import app.closer.domain.repository.CoupleRepository
import app.closer.ui.components.BrandMessageRotator import app.closer.ui.components.BrandMessageRotator
import app.closer.ui.components.CloserHeartLoader
import app.closer.ui.components.StatusGlyph import app.closer.ui.components.StatusGlyph
import app.closer.ui.settings.SettingsBackgroundBrush import app.closer.ui.settings.SettingsBackgroundBrush
import app.closer.ui.settings.SettingsInk import app.closer.ui.settings.SettingsInk
@ -181,7 +181,7 @@ fun EncryptionUpgradeScreen(
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
CircularProgressIndicator(color = SettingsPrimary) CloserHeartLoader()
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
BrandMessageRotator(style = MaterialTheme.typography.bodySmall) BrandMessageRotator(style = MaterialTheme.typography.bodySmall)
} }

View File

@ -20,7 +20,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -107,7 +107,7 @@ fun InviteConfirmScreen(
) { ) {
if (state.isLoading) { if (state.isLoading) {
Spacer(Modifier.height(160.dp)) Spacer(Modifier.height(160.dp))
CircularProgressIndicator(modifier = Modifier.size(40.dp)) CloserHeartLoader(size = 64.dp)
} else { } else {
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
@ -156,11 +156,7 @@ fun InviteConfirmScreen(
contentColor = SettingsOnPrimary contentColor = SettingsOnPrimary
) )
) { ) {
if (state.isConfirming) CircularProgressIndicator( if (state.isConfirming) CloserHeartLoader(size = 22.dp)
modifier = Modifier.size(20.dp),
color = SettingsOnPrimary,
strokeWidth = 2.dp
)
else Text("Pair up", style = MaterialTheme.typography.labelLarge) else Text("Pair up", style = MaterialTheme.typography.labelLarge)
} }

View File

@ -20,7 +20,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.OutlinedTextFieldDefaults
@ -141,11 +141,7 @@ fun RecoveryScreen(
) )
) { ) {
if (state.isLoading) { if (state.isLoading) {
CircularProgressIndicator( CloserHeartLoader(size = 22.dp)
modifier = Modifier.size(20.dp),
color = SettingsOnPrimary,
strokeWidth = 2.dp
)
} else { } else {
Text("Unlock answers", style = MaterialTheme.typography.labelLarge) Text("Unlock answers", style = MaterialTheme.typography.labelLarge)
} }

View File

@ -32,7 +32,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
@ -133,10 +133,7 @@ fun PaywallScreen(
BenefitsCard() BenefitsCard()
if (uiState.purchaseState is BillingState.Loading) { if (uiState.purchaseState is BillingState.Loading) {
CircularProgressIndicator( CloserHeartLoader(size = 40.dp)
modifier = Modifier.size(28.dp),
color = CloserPalette.PurpleRich
)
} }
ActionButtons( ActionButtons(

View File

@ -22,7 +22,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -401,7 +401,7 @@ private fun CategoryLoadingCard() {
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp) horizontalArrangement = Arrangement.spacedBy(14.dp)
) { ) {
CircularProgressIndicator(color = Color(0xFFB98AF4)) CloserHeartLoader(size = 32.dp)
Text( Text(
text = "Loading prompts", text = "Loading prompts",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,

View File

@ -26,7 +26,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -424,7 +424,7 @@ private fun LoadingPackCard() {
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp) horizontalArrangement = Arrangement.spacedBy(14.dp)
) { ) {
CircularProgressIndicator(color = Color(0xFFB98AF4)) CloserHeartLoader(size = 32.dp)
Text( Text(
text = "Loading question packs", text = "Loading question packs",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,

View File

@ -33,7 +33,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -251,7 +251,7 @@ fun DeleteAccountScreen(
) )
) { ) {
if (state.isReauthing) { if (state.isReauthing) {
CircularProgressIndicator(modifier = Modifier.size(16.dp), color = Color.White, strokeWidth = 2.dp) CloserHeartLoader(size = 18.dp)
} else { } else {
Text("Confirm", color = Color.White) Text("Confirm", color = Color.White)
} }
@ -328,7 +328,7 @@ fun DeleteAccountScreen(
) { ) {
if (state.isDeleting) { if (state.isDeleting) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White, strokeWidth = 2.dp) CloserHeartLoader(size = 20.dp)
Text("Deleting…", color = Color.White) Text("Deleting…", color = Color.White)
} }
} else { } else {

View File

@ -34,7 +34,7 @@ import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.PhotoLibrary import androidx.compose.material.icons.filled.PhotoLibrary
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -175,7 +175,7 @@ fun EditProfileContent(
) { ) {
if (state.isLoading) { if (state.isLoading) {
Spacer(Modifier.height(64.dp)) Spacer(Modifier.height(64.dp))
CircularProgressIndicator(color = SettingsPrimary) CloserHeartLoader()
} else { } else {
val imageUri = state.photoUri ?: state.photoUrl.takeIf { it.isNotBlank() } val imageUri = state.photoUri ?: state.photoUrl.takeIf { it.isNotBlank() }
Box( Box(
@ -356,7 +356,7 @@ fun EditProfileContent(
contentColor = SettingsOnPrimary contentColor = SettingsOnPrimary
) )
) { ) {
if (state.isSaving) CircularProgressIndicator(color = SettingsOnPrimary, strokeWidth = 2.dp) if (state.isSaving) CloserHeartLoader(size = 22.dp)
else Text("Save changes", style = MaterialTheme.typography.labelLarge) else Text("Save changes", style = MaterialTheme.typography.labelLarge)
} }

View File

@ -32,7 +32,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -190,7 +190,7 @@ fun RelationshipSettingsScreen(
) { ) {
if (state.isLeaving) { if (state.isLeaving) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White, strokeWidth = 2.dp) CloserHeartLoader(size = 20.dp)
Text("Leaving…", color = Color.White) Text("Leaving…", color = Color.White)
} }
} else { } else {

View File

@ -35,7 +35,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -226,7 +226,7 @@ fun SettingsScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
CircularProgressIndicator() CloserHeartLoader()
} }
} else { } else {
var showBaselineOutcomeDialog by remember { mutableStateOf(false) } var showBaselineOutcomeDialog by remember { mutableStateOf(false) }
@ -499,11 +499,7 @@ fun SettingsScreen(
contentColor = SettingsDanger contentColor = SettingsDanger
) )
) { ) {
if (state.isSigningOut) CircularProgressIndicator( if (state.isSigningOut) CloserHeartLoader(size = 22.dp)
modifier = Modifier.size(20.dp),
color = SettingsDanger,
strokeWidth = 2.dp
)
else Text("Sign out", style = MaterialTheme.typography.labelLarge) else Text("Sign out", style = MaterialTheme.typography.labelLarge)
} }

View File

@ -37,7 +37,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -417,9 +417,8 @@ fun ThisOrThatScreen(
.background(closerBackgroundBrush()) .background(closerBackgroundBrush())
) { ) {
when (state.phase) { when (state.phase) {
TotPhase.LOADING -> CircularProgressIndicator( TotPhase.LOADING -> CloserHeartLoader(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center)
color = CloserPalette.PurpleDeep
) )
TotPhase.ERROR -> ErrorState( TotPhase.ERROR -> ErrorState(
message = state.error ?: "Something didn't load. Go back and try again.", message = state.error ?: "Something didn't load. Go back and try again.",
@ -447,9 +446,8 @@ fun ThisOrThatScreen(
TotPhase.PLAYING -> { TotPhase.PLAYING -> {
val question = state.questions.getOrNull(state.currentIndex) val question = state.questions.getOrNull(state.currentIndex)
if (question == null) { if (question == null) {
CircularProgressIndicator( CloserHeartLoader(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center)
color = CloserPalette.PurpleDeep
) )
} else { } else {
val config = question.answerConfig as? ThisOrThatAnswerConfigImpl val config = question.answerConfig as? ThisOrThatAnswerConfigImpl
@ -909,7 +907,7 @@ private fun WaitingForRevealScreen(
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
CircularProgressIndicator(color = CloserPalette.PurpleDeep, strokeWidth = 3.dp) CloserHeartLoader(size = 48.dp)
BrandMessageRotator(style = MaterialTheme.typography.bodySmall) BrandMessageRotator(style = MaterialTheme.typography.bodySmall)
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
@ -1272,9 +1270,8 @@ fun ThisOrThatReplayScreen(
.background(closerBackgroundBrush()) .background(closerBackgroundBrush())
) { ) {
when (phase) { when (phase) {
is TotReplayPhase.Loading -> CircularProgressIndicator( is TotReplayPhase.Loading -> CloserHeartLoader(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center)
color = CloserPalette.PurpleDeep
) )
is TotReplayPhase.Error -> Column( is TotReplayPhase.Error -> Column(
modifier = Modifier modifier = Modifier

View File

@ -22,7 +22,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -104,7 +104,7 @@ private fun CategoryPickerContent(
horizontalArrangement = Arrangement.spacedBy(14.dp), horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CircularProgressIndicator(color = CloserPalette.PurpleDeep) CloserHeartLoader(size = 28.dp)
Text( Text(
"Loading categories…", "Loading categories…",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,

View File

@ -32,7 +32,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -262,7 +262,7 @@ private fun SpinWheelContent(
} }
} }
} }
state.isLoading -> CircularProgressIndicator(color = CloserPalette.PurpleDeep) state.isLoading -> CloserHeartLoader()
else -> { else -> {
Text( Text(
text = "Spin to discover a random category and 10 questions", text = "Spin to discover a random category and 10 questions",

View File

@ -26,7 +26,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -197,9 +197,8 @@ fun WheelCompleteScreen(
.background(closerBackgroundBrush()) .background(closerBackgroundBrush())
) { ) {
when (state.phase) { when (state.phase) {
WheelRevealPhase.LOADING -> CircularProgressIndicator( WheelRevealPhase.LOADING -> CloserHeartLoader(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center)
color = CloserPalette.PurpleDeep
) )
WheelRevealPhase.WAITING -> WheelWaitingContent( WheelRevealPhase.WAITING -> WheelWaitingContent(
partnerName = state.partnerName, partnerName = state.partnerName,
@ -254,7 +253,7 @@ private fun WheelWaitingContent(
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
CircularProgressIndicator(color = CloserPalette.PurpleDeep, strokeWidth = 3.dp) CloserHeartLoader(size = 48.dp)
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))

View File

@ -26,7 +26,7 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.CircularProgressIndicator import app.closer.ui.components.CloserHeartLoader
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
@ -114,7 +114,7 @@ private fun WheelSessionContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
CircularProgressIndicator(color = Color(0xFF56306F)) CloserHeartLoader()
} }
return@Column return@Column
} }

View File

@ -7,9 +7,7 @@ struct LoadingView: View {
var body: some View { var body: some View {
VStack(spacing: CloserSpacing.lg) { VStack(spacing: CloserSpacing.lg) {
ProgressView() CloserHeartLoader()
.tint(.closerPrimary)
.scaleEffect(1.2)
Text(message) Text(message)
.font(CloserFont.callout) .font(CloserFont.callout)
.foregroundColor(.closerTextSecondary) .foregroundColor(.closerTextSecondary)
@ -18,6 +16,168 @@ struct LoadingView: View {
} }
} }
struct CloserHeartLoader: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var fillProgress: CGFloat = 0.08
@State private var isPulsing = false
var size: CGFloat = 76
var body: some View {
ZStack {
CloserHeartShape()
.fill(
LinearGradient(
colors: [
Color(hex: "F7C8E4").opacity(0.22),
Color(hex: "D9B8FF").opacity(0.22)
],
startPoint: .leading,
endPoint: .trailing
)
)
HStack(spacing: 0) {
Color(hex: "F7C8E4")
Color(hex: "D9B8FF")
}
.mask(CloserHeartShape())
.mask(alignment: .bottom) {
Rectangle()
.frame(height: size * (reduceMotion ? 1 : fillProgress))
}
CloserHeartHighlightShape(side: .left)
.fill(Color(hex: "FFF4FA").opacity(reduceMotion ? 0.68 : 0.68 * fillProgress))
CloserHeartHighlightShape(side: .right)
.fill(Color(hex: "F3E8FF").opacity(reduceMotion ? 0.52 : 0.52 * fillProgress))
}
.frame(width: size, height: size)
.scaleEffect(reduceMotion ? 1 : (isPulsing ? 1.04 : 0.96))
.accessibilityHidden(true)
.onAppear {
guard !reduceMotion else {
fillProgress = 1
isPulsing = false
return
}
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: false)) {
fillProgress = 1
}
withAnimation(.easeInOut(duration: 0.9).repeatForever(autoreverses: true)) {
isPulsing = true
}
}
.onChange(of: reduceMotion) { _, newValue in
if newValue {
fillProgress = 1
isPulsing = false
}
}
}
}
private struct CloserHeartShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: point(54, 85, in: rect))
path.addCurve(
to: point(22, 48, in: rect),
control1: point(49, 79, in: rect),
control2: point(27, 62, in: rect)
)
path.addCurve(
to: point(37, 24, in: rect),
control1: point(17, 35, in: rect),
control2: point(24, 24, in: rect)
)
path.addCurve(
to: point(54, 35, in: rect),
control1: point(45, 24, in: rect),
control2: point(51, 28, in: rect)
)
path.addCurve(
to: point(71, 24, in: rect),
control1: point(57, 28, in: rect),
control2: point(63, 24, in: rect)
)
path.addCurve(
to: point(86, 48, in: rect),
control1: point(84, 24, in: rect),
control2: point(91, 35, in: rect)
)
path.addCurve(
to: point(54, 85, in: rect),
control1: point(81, 62, in: rect),
control2: point(59, 79, in: rect)
)
path.closeSubpath()
return path
}
}
private struct CloserHeartHighlightShape: Shape {
enum Side {
case left
case right
}
let side: Side
func path(in rect: CGRect) -> Path {
var path = Path()
switch side {
case .left:
path.move(to: point(27, 42, in: rect))
path.addCurve(
to: point(42, 27, in: rect),
control1: point(28, 32, in: rect),
control2: point(34, 27, in: rect)
)
path.addCurve(
to: point(54, 35, in: rect),
control1: point(48, 27, in: rect),
control2: point(52, 30, in: rect)
)
path.addLine(to: point(54, 41, in: rect))
path.addCurve(
to: point(27, 42, in: rect),
control1: point(47, 36, in: rect),
control2: point(37, 36, in: rect)
)
path.closeSubpath()
case .right:
path.move(to: point(54, 35, in: rect))
path.addCurve(
to: point(69, 27, in: rect),
control1: point(57, 30, in: rect),
control2: point(62, 27, in: rect)
)
path.addCurve(
to: point(85, 42, in: rect),
control1: point(78, 27, in: rect),
control2: point(84, 32, in: rect)
)
path.addCurve(
to: point(54, 41, in: rect),
control1: point(75, 36, in: rect),
control2: point(65, 36, in: rect)
)
path.closeSubpath()
}
return path
}
}
private func point(_ x: CGFloat, _ y: CGFloat, in rect: CGRect) -> CGPoint {
CGPoint(
x: rect.minX + rect.width * (x / 108),
y: rect.minY + rect.height * (y / 108)
)
}
// MARK: - Error View // MARK: - Error View
struct ErrorView: View { struct ErrorView: View {