Unify Android theme tokens

Add shared experience and status color tokens, migrate Android game/shared UI away from raw palettes, expand preview coverage, and strengthen the theme scanner with strict raw-color and design-system drift checks.
This commit is contained in:
null 2026-07-16 02:31:37 -05:00
parent 8a9bd41fe5
commit fc5d709174
33 changed files with 905 additions and 340 deletions

View File

@ -42,9 +42,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -54,7 +51,7 @@ import app.closer.core.navigation.AppRoute
import app.closer.domain.model.LocalAnswer import app.closer.domain.model.LocalAnswer
import app.closer.ui.components.CategoryGlyph import app.closer.ui.components.CategoryGlyph
import app.closer.ui.components.EmptyState import app.closer.ui.components.EmptyState
import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.closerStatusColors
import app.closer.ui.questions.displayCategoryName import app.closer.ui.questions.displayCategoryName
import app.closer.ui.components.CloserGlyphs import app.closer.ui.components.CloserGlyphs
@ -94,6 +91,7 @@ private fun AnswerHistoryContent(
) { ) {
var pendingDelete by remember { mutableStateOf<LocalAnswer?>(null) } var pendingDelete by remember { mutableStateOf<LocalAnswer?>(null) }
var selectedFilter by remember { mutableStateOf(AnswerHistoryFilter.ALL) } var selectedFilter by remember { mutableStateOf(AnswerHistoryFilter.ALL) }
val statusColors = closerStatusColors()
val visibleAnswers = remember(state.answers, selectedFilter) { val visibleAnswers = remember(state.answers, selectedFilter) {
state.answers.filter { answer -> state.answers.filter { answer ->
when (selectedFilter) { when (selectedFilter) {
@ -118,8 +116,8 @@ private fun AnswerHistoryContent(
pendingDelete = null pendingDelete = null
}, },
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF8D2D35), containerColor = statusColors.danger,
contentColor = Color.White contentColor = MaterialTheme.colorScheme.onError
) )
) { ) {
Text("Remove") Text("Remove")
@ -358,7 +356,7 @@ private fun AnswerHistoryCard(
) { ) {
Text( Text(
text = "Remove", text = "Remove",
color = Color(0xFF8D2D35) color = closerStatusColors().danger
) )
} }
} }
@ -369,8 +367,9 @@ private fun AnswerHistoryCard(
@Composable @Composable
private fun HistoryStateBadge(isRevealed: Boolean) { private fun HistoryStateBadge(isRevealed: Boolean) {
val tint = if (isRevealed) MaterialTheme.colorScheme.onPrimaryContainer else CloserPalette.Evergreen val statusColors = closerStatusColors()
val container = if (isRevealed) closerSoftPurpleColor() else CloserPalette.Evergreen.copy(alpha = 0.10f) val tint = if (isRevealed) MaterialTheme.colorScheme.onPrimaryContainer else statusColors.success
val container = if (isRevealed) closerSoftPurpleColor() else statusColors.successContainer
Surface( Surface(
shape = RoundedCornerShape(999.dp), shape = RoundedCornerShape(999.dp),
color = container color = container

View File

@ -17,10 +17,8 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.ui.res.painterResource
import app.closer.R import app.closer.R
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
@ -66,6 +64,9 @@ import app.closer.domain.DailyModeResolver
import app.closer.ui.questions.displayCategoryName import app.closer.ui.questions.displayCategoryName
import app.closer.ui.questions.displayQuestionType import app.closer.ui.questions.displayQuestionType
import app.closer.ui.components.BrandMessageRotator import app.closer.ui.components.BrandMessageRotator
import app.closer.ui.components.BrandIllustration
import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.closerToneColors
@Composable @Composable
fun AnswerRevealScreen( fun AnswerRevealScreen(
@ -265,6 +266,8 @@ private fun NoAnswerState(
onAnswerQuestion: () -> Unit, onAnswerQuestion: () -> Unit,
onHome: () -> Unit onHome: () -> Unit
) { ) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
RevealMessageCard { RevealMessageCard {
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
RevealPill("No answer yet") RevealPill("No answer yet")
@ -291,8 +294,8 @@ private fun NoAnswerState(
.heightIn(min = 48.dp), .heightIn(min = 48.dp),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4), containerColor = revealTone.accent,
contentColor = Color(0xFF24122F) contentColor = revealTone.onAccent
) )
) { ) {
Text("Answer") Text("Answer")
@ -319,6 +322,8 @@ private fun ReadyToRevealState(
onReveal: () -> Unit, onReveal: () -> Unit,
onHistory: () -> Unit onHistory: () -> Unit
) { ) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
RevealMessageCard { RevealMessageCard {
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
RevealPill("Private answer saved") RevealPill("Private answer saved")
@ -347,8 +352,8 @@ private fun ReadyToRevealState(
.heightIn(min = 48.dp), .heightIn(min = 48.dp),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4), containerColor = revealTone.accent,
contentColor = Color(0xFF24122F) contentColor = revealTone.onAccent
) )
) { ) {
Text("Reveal answer") Text("Reveal answer")
@ -432,15 +437,18 @@ private fun BothAnsweredSealedState(
onReveal: () -> Unit, onReveal: () -> Unit,
onHistory: () -> Unit onHistory: () -> Unit
) { ) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
RevealMessageCard { RevealMessageCard {
Column( Column(
verticalArrangement = Arrangement.spacedBy(14.dp), verticalArrangement = Arrangement.spacedBy(14.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Image( BrandIllustration(
painter = painterResource(R.drawable.illustration_reveal_celebration), res = R.drawable.illustration_reveal_celebration,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(140.dp) modifier = Modifier.size(140.dp),
tile = false
) )
RevealPill("Both answers are in") RevealPill("Both answers are in")
Text( Text(
@ -464,8 +472,8 @@ private fun BothAnsweredSealedState(
modifier = Modifier.weight(1f).heightIn(min = 48.dp), modifier = Modifier.weight(1f).heightIn(min = 48.dp),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4), containerColor = revealTone.accent,
contentColor = Color(0xFF24122F) contentColor = revealTone.onAccent
) )
) { ) {
Text("Reveal answer") Text("Reveal answer")
@ -504,6 +512,8 @@ private fun WaitingForPartnerState(
onRefresh: () -> Unit, onRefresh: () -> Unit,
onHome: () -> Unit onHome: () -> Unit
) { ) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
RevealMessageCard { RevealMessageCard {
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
RevealPill("Almost there") RevealPill("Almost there")
@ -528,8 +538,8 @@ private fun WaitingForPartnerState(
modifier = Modifier.weight(1f).heightIn(min = 48.dp), modifier = Modifier.weight(1f).heightIn(min = 48.dp),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4), containerColor = revealTone.accent,
contentColor = Color(0xFF24122F) contentColor = revealTone.onAccent
) )
) { ) {
Text("Check again") Text("Check again")
@ -595,6 +605,8 @@ private fun RevealedState(
loreSaved: Boolean = false, loreSaved: Boolean = false,
wasSealed: Boolean = false wasSealed: Boolean = false
) { ) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
val matchLine = if (partnerAnswer != null) { val matchLine = if (partnerAnswer != null) {
val ownIds = answer.selectedOptionIds.toSet() val ownIds = answer.selectedOptionIds.toSet()
val partnerIds = partnerAnswer.selectedOptionIds.toSet() val partnerIds = partnerAnswer.selectedOptionIds.toSet()
@ -616,7 +628,7 @@ private fun RevealedState(
Text( Text(
text = matchLine, text = matchLine,
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF56306F), color = revealTone.accent,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
@ -639,8 +651,8 @@ private fun RevealedState(
.heightIn(min = 48.dp), .heightIn(min = 48.dp),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4), containerColor = revealTone.accent,
contentColor = Color(0xFF24122F) contentColor = revealTone.onAccent
) )
) { ) {
Text("Saved answers") Text("Saved answers")
@ -663,7 +675,7 @@ private fun RevealedState(
Text( Text(
"Save to Couple Lore", "Save to Couple Lore",
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = Color(0xFF56306F).copy(alpha = 0.75f) color = revealTone.accent.copy(alpha = 0.75f)
) )
} }
} else { } else {
@ -684,6 +696,8 @@ private fun TinyActionCard(
mode: DailyModeResolver.DailyMode, mode: DailyModeResolver.DailyMode,
onDoThis: () -> Unit onDoThis: () -> Unit
) { ) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(28.dp), shape = RoundedCornerShape(28.dp),
@ -711,8 +725,8 @@ private fun TinyActionCard(
.heightIn(min = 40.dp), .heightIn(min = 40.dp),
shape = RoundedCornerShape(14.dp), shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4), containerColor = revealTone.accent,
contentColor = Color(0xFF24122F) contentColor = revealTone.onAccent
) )
) { ) {
Text("Do this tonight", style = MaterialTheme.typography.labelMedium) Text("Do this tonight", style = MaterialTheme.typography.labelMedium)
@ -769,6 +783,8 @@ private fun FollowUpChip(
label: String, label: String,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
Surface( Surface(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -796,7 +812,7 @@ private fun FollowUpChip(
Text( Text(
text = "", text = "",
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = Color(0xFFB98AF4), color = revealTone.accent,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }

View File

@ -51,6 +51,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -68,8 +69,11 @@ import app.closer.domain.model.ConnectionChallenge
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 java.time.LocalDate import java.time.LocalDate
import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerStatusColors
import app.closer.ui.theme.closerToneColors
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -403,6 +407,9 @@ private fun ChallengePickCard(
onPick: (ConnectionChallenge) -> Unit, onPick: (ConnectionChallenge) -> Unit,
onPaywall: () -> Unit onPaywall: () -> Unit
) { ) {
val challengeTone = closerToneColors(CloserExperienceTone.Challenge)
val statusColors = closerStatusColors()
Card( Card(
onClick = { if (challenge.isPremium && !hasPremium) onPaywall() else onPick(challenge) }, onClick = { if (challenge.isPremium && !hasPremium) onPaywall() else onPick(challenge) },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -419,7 +426,7 @@ private fun ChallengePickCard(
) { ) {
Surface( Surface(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
color = CloserPalette.PurpleDeep.copy(alpha = 0.10f), color = challengeTone.accentContainer,
modifier = Modifier.size(52.dp) modifier = Modifier.size(52.dp)
) { ) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
@ -451,27 +458,27 @@ private fun ChallengePickCard(
if (challenge.isPremium && !hasPremium) { if (challenge.isPremium && !hasPremium) {
Surface( Surface(
shape = RoundedCornerShape(999.dp), shape = RoundedCornerShape(999.dp),
color = CloserPalette.Gold.copy(alpha = 0.15f) color = statusColors.premiumContainer
) { ) {
Row( Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(3.dp) horizontalArrangement = Arrangement.spacedBy(3.dp)
) { ) {
Icon(CloserGlyphs.Lock, contentDescription = null, tint = CloserPalette.Gold, modifier = Modifier.size(10.dp)) Icon(CloserGlyphs.Lock, contentDescription = null, tint = statusColors.premium, modifier = Modifier.size(10.dp))
Text("Premium", style = MaterialTheme.typography.labelSmall, color = CloserPalette.Gold, fontWeight = FontWeight.SemiBold) Text("Premium", style = MaterialTheme.typography.labelSmall, color = statusColors.premium, fontWeight = FontWeight.SemiBold)
} }
} }
} else { } else {
Surface( Surface(
shape = RoundedCornerShape(999.dp), shape = RoundedCornerShape(999.dp),
color = CloserPalette.PurpleDeep.copy(alpha = 0.10f) color = challengeTone.accentContainer
) { ) {
Text( Text(
text = "${challenge.durationDays} days", text = "${challenge.durationDays} days",
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = CloserPalette.PurpleDeep, color = challengeTone.onAccentContainer,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }
@ -487,7 +494,7 @@ private fun ChallengePickCard(
Text( Text(
text = challenge.category, text = challenge.category,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = CloserPalette.PurpleDeep.copy(alpha = 0.7f) color = challengeTone.accent.copy(alpha = 0.7f)
) )
} }
} }
@ -509,6 +516,9 @@ private fun ChallengesActiveScreen(
onConfirmAbandon: () -> Unit, onConfirmAbandon: () -> Unit,
onDismissAbandon: () -> Unit onDismissAbandon: () -> Unit
) { ) {
val challengeTone = closerToneColors(CloserExperienceTone.Challenge)
val statusColors = closerStatusColors()
if (showAbandonConfirm) { if (showAbandonConfirm) {
AlertDialog( AlertDialog(
onDismissRequest = onDismissAbandon, onDismissRequest = onDismissAbandon,
@ -585,13 +595,13 @@ private fun ChallengesActiveScreen(
if (progress.jointStreak > 0 && !isComplete) { if (progress.jointStreak > 0 && !isComplete) {
Surface( Surface(
shape = RoundedCornerShape(999.dp), shape = RoundedCornerShape(999.dp),
color = CloserPalette.PurpleDeep.copy(alpha = 0.12f) color = challengeTone.accentContainer
) { ) {
Text( Text(
text = "🔥 ${progress.jointStreak}", text = "🔥 ${progress.jointStreak}",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = CloserPalette.PurpleDeep, color = challengeTone.onAccentContainer,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }
@ -650,13 +660,13 @@ private fun ChallengesActiveScreen(
) { ) {
Surface( Surface(
shape = RoundedCornerShape(999.dp), shape = RoundedCornerShape(999.dp),
color = CloserPalette.PurpleDeep.copy(alpha = 0.10f) color = challengeTone.accentContainer
) { ) {
Text( Text(
text = "Day $currentDay", text = "Day $currentDay",
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = CloserPalette.PurpleDeep, color = challengeTone.onAccentContainer,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }
@ -687,8 +697,8 @@ private fun ChallengesActiveScreen(
Surface( Surface(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
color = when { color = when {
isBothDone -> CloserPalette.Evergreen.copy(alpha = 0.10f) isBothDone -> statusColors.successContainer
isWaiting -> CloserPalette.PurpleDeep.copy(alpha = 0.07f) isWaiting -> challengeTone.accentContainer.copy(alpha = 0.55f)
else -> MaterialTheme.colorScheme.surface.copy(alpha = 0.6f) else -> MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)
} }
) { ) {
@ -704,14 +714,14 @@ private fun ChallengesActiveScreen(
else -> null else -> null
} }
if (icon != null) { if (icon != null) {
Icon(icon, contentDescription = null, tint = CloserPalette.Evergreen, modifier = Modifier.size(18.dp)) Icon(icon, contentDescription = null, tint = statusColors.success, modifier = Modifier.size(18.dp))
} else { } else {
Text(if (isWaiting) "" else "💬", style = MaterialTheme.typography.bodySmall) Text(if (isWaiting) "" else "💬", style = MaterialTheme.typography.bodySmall)
} }
Text( Text(
text = stateCopy, text = stateCopy,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = if (isBothDone) CloserPalette.Evergreen else MaterialTheme.colorScheme.onSurfaceVariant, color = if (isBothDone) statusColors.success else MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = if (isBothDone) FontWeight.Medium else FontWeight.Normal fontWeight = if (isBothDone) FontWeight.Medium else FontWeight.Normal
) )
} }
@ -729,14 +739,15 @@ private fun ChallengesActiveScreen(
.heightIn(min = 54.dp), .heightIn(min = 54.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = CloserPalette.PurpleDeep, containerColor = challengeTone.accent,
disabledContainerColor = CloserPalette.PurpleDeep.copy(alpha = 0.35f) contentColor = challengeTone.onAccent,
disabledContainerColor = challengeTone.accent.copy(alpha = 0.35f),
disabledContentColor = challengeTone.onAccent.copy(alpha = 0.65f)
) )
) { ) {
Text( Text(
text = ctaLabel, text = ctaLabel,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge
color = Color.White
) )
} }
} }
@ -781,13 +792,13 @@ private fun ChallengesActiveScreen(
.heightIn(min = 54.dp), .heightIn(min = 54.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = CloserPalette.PurpleDeep containerColor = challengeTone.accent,
contentColor = challengeTone.onAccent
) )
) { ) {
Text( Text(
text = "View game history", text = "View game history",
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge
color = Color.White
) )
} }
} }
@ -819,6 +830,8 @@ private fun DayTrackerStrip(
partnerCompletedDays: List<Int>, partnerCompletedDays: List<Int>,
currentDay: Int currentDay: Int
) { ) {
val challengeTone = closerToneColors(CloserExperienceTone.Challenge)
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
@ -830,9 +843,9 @@ private fun DayTrackerStrip(
val isCurrent = day == currentDay.coerceAtMost(totalDays) && !myDone val isCurrent = day == currentDay.coerceAtMost(totalDays) && !myDone
val bg = when { val bg = when {
jointDone -> CloserPalette.PurpleDeep jointDone -> challengeTone.accent
myDone -> CloserPalette.PurpleDeep.copy(alpha = 0.45f) myDone -> challengeTone.accent.copy(alpha = 0.45f)
isCurrent -> CloserPalette.PurpleDeep.copy(alpha = 0.15f) isCurrent -> challengeTone.accentContainer
else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
} }
@ -845,12 +858,14 @@ private fun DayTrackerStrip(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (jointDone) { if (jointDone) {
Icon(CloserGlyphs.Check, contentDescription = null, tint = Color.White, modifier = Modifier.size(14.dp)) Icon(CloserGlyphs.Check, contentDescription = null, tint = challengeTone.onAccent, modifier = Modifier.size(14.dp))
} else { } else {
Text( Text(
text = "$day", text = "$day",
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = if (myDone || isCurrent) Color.White else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), color = if (myDone) challengeTone.onAccent
else if (isCurrent) challengeTone.onAccentContainer
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }
@ -858,3 +873,31 @@ private fun DayTrackerStrip(
} }
} }
} }
@Composable
private fun ChallengesPreviewBody() {
Box(
modifier = Modifier
.fillMaxSize()
.background(closerBackgroundBrush())
) {
ChallengesPickScreen(
hasPremium = false,
onBack = {},
onPick = {},
onPaywall = {}
)
}
}
@Preview(name = "Challenges • light")
@Composable
private fun ChallengesPreviewLight() {
CloserTheme(darkTheme = false) { ChallengesPreviewBody() }
}
@Preview(name = "Challenges • dark")
@Composable
private fun ChallengesPreviewDark() {
CloserTheme(darkTheme = true) { ChallengesPreviewBody() }
}

View File

@ -46,7 +46,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -56,7 +55,9 @@ import app.closer.domain.model.GameType
import app.closer.notifications.GamePromptController import app.closer.notifications.GamePromptController
import app.closer.notifications.GamePromptKind import app.closer.notifications.GamePromptKind
import app.closer.notifications.IncomingGamePrompt import app.closer.notifications.IncomingGamePrompt
import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.closerStatusColors
import app.closer.ui.theme.closerToneColors
import coil.compose.AsyncImage import coil.compose.AsyncImage
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -179,6 +180,8 @@ private fun GamePromptCard(
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
val style = styleFor(prompt) val style = styleFor(prompt)
val promptTone = closerToneColors(CloserExperienceTone.Wheel)
val statusColors = closerStatusColors()
var dragUp by remember(prompt.kind, prompt.gameSessionId) { mutableStateOf(0f) } var dragUp by remember(prompt.kind, prompt.gameSessionId) { mutableStateOf(0f) }
Row( Row(
modifier = Modifier modifier = Modifier
@ -189,10 +192,10 @@ private fun GamePromptCard(
.clip(RoundedCornerShape(20.dp)) .clip(RoundedCornerShape(20.dp))
.background( .background(
Brush.linearGradient( Brush.linearGradient(
listOf(CloserPalette.PurpleDeep, CloserPalette.PurpleRich) listOf(promptTone.accent, promptTone.accentContainer)
) )
) )
.border(1.dp, Color.White.copy(alpha = 0.18f), RoundedCornerShape(20.dp)) .border(1.dp, promptTone.onAccent.copy(alpha = 0.18f), RoundedCornerShape(20.dp))
// Whole card taps act; flick up to dismiss (mirrors the chat bubble's drag-to-dismiss). // Whole card taps act; flick up to dismiss (mirrors the chat bubble's drag-to-dismiss).
.clickable(onClick = onAction) .clickable(onClick = onAction)
.pointerInput(prompt.gameSessionId) { .pointerInput(prompt.gameSessionId) {
@ -214,8 +217,8 @@ private fun GamePromptCard(
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
.clip(CircleShape) .clip(CircleShape)
.background(Color.White.copy(alpha = 0.18f)) .background(promptTone.onAccent.copy(alpha = 0.18f))
.border(1.5.dp, Color.White.copy(alpha = 0.5f), CircleShape), .border(1.5.dp, promptTone.onAccent.copy(alpha = 0.5f), CircleShape),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
val avatar = prompt.avatarUrl val avatar = prompt.avatarUrl
@ -230,7 +233,7 @@ private fun GamePromptCard(
Icon( Icon(
imageVector = CloserGlyphs.Play, imageVector = CloserGlyphs.Play,
contentDescription = null, contentDescription = null,
tint = Color.White, tint = promptTone.onAccent,
modifier = Modifier.size(22.dp) modifier = Modifier.size(22.dp)
) )
} }
@ -240,8 +243,8 @@ private fun GamePromptCard(
modifier = Modifier modifier = Modifier
.size(12.dp) .size(12.dp)
.clip(CircleShape) .clip(CircleShape)
.background(Color(0xFF3DD68C)) // live "here now" green .background(statusColors.success)
.border(2.dp, CloserPalette.PurpleDeep, CircleShape) .border(2.dp, promptTone.accent, CircleShape)
) )
} }
} }
@ -250,20 +253,20 @@ private fun GamePromptCard(
Text( Text(
text = style.line1, text = style.line1,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = Color.White.copy(alpha = 0.85f) color = promptTone.onAccent.copy(alpha = 0.85f)
) )
Text( Text(
text = style.line2, text = style.line2,
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold), style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold),
color = Color.White color = promptTone.onAccent
) )
} }
Button( Button(
onClick = onAction, onClick = onAction,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color.White, containerColor = promptTone.onAccent,
contentColor = CloserPalette.PurpleDeep contentColor = promptTone.accent
), ),
shape = RoundedCornerShape(14.dp) shape = RoundedCornerShape(14.dp)
) { ) {
@ -279,7 +282,7 @@ private fun GamePromptCard(
Icon( Icon(
imageVector = CloserGlyphs.Close, imageVector = CloserGlyphs.Close,
contentDescription = "Dismiss", contentDescription = "Dismiss",
tint = Color.White.copy(alpha = 0.8f), tint = promptTone.onAccent.copy(alpha = 0.8f),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
} }

View File

@ -122,6 +122,12 @@ private fun CloserMarkChipLoader(
size: Dp, size: Dp,
reducedMotion: Boolean reducedMotion: Boolean
) { ) {
val chipColors = listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.primaryContainer,
MaterialTheme.colorScheme.secondary
)
val ringColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.10f)
val transition = rememberInfiniteTransition(label = "closerMarkLoader") val transition = rememberInfiniteTransition(label = "closerMarkLoader")
val fill = if (reducedMotion) 1f else transition.animateFloat( val fill = if (reducedMotion) 1f else transition.animateFloat(
initialValue = 0f, initialValue = 0f,
@ -160,7 +166,7 @@ private fun CloserMarkChipLoader(
// Aubergine gradient chip (matches ic_launcher_background palette). // Aubergine gradient chip (matches ic_launcher_background palette).
drawRoundRect( drawRoundRect(
brush = Brush.linearGradient( brush = Brush.linearGradient(
colors = listOf(Color(0xFF5A2F74), Color(0xFF3A1D4D), Color(0xFF24122F)), colors = chipColors,
start = Offset(0f, 0f), start = Offset(0f, 0f),
end = Offset(w, h) end = Offset(w, h)
), ),
@ -190,7 +196,7 @@ private fun CloserMarkChipLoader(
} }
// Faint ring so the dark chip separates on dark backgrounds. // Faint ring so the dark chip separates on dark backgrounds.
drawRoundRect( drawRoundRect(
color = Color.White.copy(alpha = 0.10f), color = ringColor,
cornerRadius = corner, cornerRadius = corner,
style = Stroke(width = 1.dp.toPx()) style = Stroke(width = 1.dp.toPx())
) )

View File

@ -27,16 +27,16 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.closer.ui.theme.CloserExperienceTone
private val Purple = Color(0xFF56306F) import app.closer.ui.theme.closerToneColors
private val PurpleLight = Color(0xFFF0EDF9)
private val PinkPill = Color(0xFFB98AF4)
@Composable @Composable
fun SpecialDatesSection( fun SpecialDatesSection(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
compact: Boolean = false compact: Boolean = false
) { ) {
val dateTone = closerToneColors(CloserExperienceTone.Date)
Card( Card(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(if (compact) 24.dp else 28.dp), shape = RoundedCornerShape(if (compact) 24.dp else 28.dp),
@ -73,7 +73,7 @@ fun SpecialDatesSection(
horizontalArrangement = Arrangement.spacedBy(14.dp), horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
DateBlock(day = "14", month = "Jul", tint = Purple, compact = compact) DateBlock(day = "14", month = "Jul", tint = dateTone.accent, compact = compact)
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@ -102,7 +102,7 @@ fun SpecialDatesSection(
Icon( Icon(
imageVector = CloserGlyphs.CardGiftcard, imageVector = CloserGlyphs.CardGiftcard,
contentDescription = null, contentDescription = null,
tint = Purple, tint = dateTone.accent,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
} }
@ -127,12 +127,14 @@ fun SpecialDatesSection(
@Composable @Composable
private fun BirthdayRow(name: String, day: String, month: String) { private fun BirthdayRow(name: String, day: String, month: String) {
val dateTone = closerToneColors(CloserExperienceTone.Date)
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(14.dp), horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
DateBlock(day = day, month = month, tint = Purple, compact = true) DateBlock(day = day, month = month, tint = dateTone.accent, compact = true)
Text( Text(
text = "$name's Birthday", text = "$name's Birthday",
@ -144,7 +146,7 @@ private fun BirthdayRow(name: String, day: String, month: String) {
Icon( Icon(
imageVector = CloserGlyphs.Cake, imageVector = CloserGlyphs.Cake,
contentDescription = null, contentDescription = null,
tint = Purple.copy(alpha = 0.7f), tint = dateTone.accent.copy(alpha = 0.7f),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
} }
@ -184,15 +186,17 @@ private fun DateBlock(
@Composable @Composable
private fun TodayPill() { private fun TodayPill() {
val dateTone = closerToneColors(CloserExperienceTone.Date)
Surface( Surface(
shape = RoundedCornerShape(999.dp), shape = RoundedCornerShape(999.dp),
color = PinkPill color = dateTone.accent
) { ) {
Text( Text(
text = "Today", text = "Today",
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = Color.White, color = dateTone.onAccent,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }
@ -200,15 +204,17 @@ private fun TodayPill() {
@Composable @Composable
private fun DateCountPill() { private fun DateCountPill() {
val dateTone = closerToneColors(CloserExperienceTone.Date)
Surface( Surface(
shape = RoundedCornerShape(999.dp), shape = RoundedCornerShape(999.dp),
color = PurpleLight color = dateTone.accentContainer
) { ) {
Text( Text(
text = "3 saved", text = "3 saved",
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = Purple, color = dateTone.onAccentContainer,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }

View File

@ -59,6 +59,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import app.closer.ui.components.CloserGlyphs import app.closer.ui.components.CloserGlyphs
import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.closerToneColors
@Composable @Composable
fun DateBuilderScreen( fun DateBuilderScreen(
@ -354,6 +356,8 @@ private fun BudgetField(
onBudgetChange: (Int) -> Unit, onBudgetChange: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val dateTone = closerToneColors(CloserExperienceTone.Date)
var textValue by remember { mutableStateOf(budget.toString()) } var textValue by remember { mutableStateOf(budget.toString()) }
Column( Column(
@ -386,7 +390,7 @@ private fun BudgetField(
Text( Text(
text = "$budget", text = "$budget",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF56306F), color = dateTone.accent,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
@ -465,13 +469,15 @@ private fun SaveButton(
onSave: () -> Unit, onSave: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val dateTone = closerToneColors(CloserExperienceTone.Date)
Button( Button(
onClick = onSave, onClick = onSave,
modifier = modifier, modifier = modifier,
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4), containerColor = dateTone.accent,
contentColor = MaterialTheme.colorScheme.onPrimary contentColor = dateTone.onAccent
) )
) { ) {
Text("Create Plan") Text("Create Plan")

View File

@ -69,6 +69,9 @@ import app.closer.ui.components.ErrorState
import app.closer.ui.components.LoadingState import app.closer.ui.components.LoadingState
import kotlin.math.roundToInt import kotlin.math.roundToInt
import app.closer.ui.components.CloserGlyphs import app.closer.ui.components.CloserGlyphs
import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.closerStatusColors
import app.closer.ui.theme.closerToneColors
@Composable @Composable
fun DateMatchScreen( fun DateMatchScreen(
@ -493,6 +496,9 @@ private fun ActionButtons(
onMaybe: () -> Unit, onMaybe: () -> Unit,
onLove: () -> Unit onLove: () -> Unit
) { ) {
val dateTone = closerToneColors(CloserExperienceTone.Date)
val statusColors = closerStatusColors()
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
@ -501,23 +507,23 @@ private fun ActionButtons(
CircularActionButton( CircularActionButton(
icon = CloserGlyphs.Close, icon = CloserGlyphs.Close,
contentDescription = "Skip", contentDescription = "Skip",
color = MaterialTheme.colorScheme.errorContainer, color = statusColors.dangerContainer,
iconColor = Color(0xFF8D2D35), iconColor = statusColors.danger,
onClick = onSkip onClick = onSkip
) )
CircularActionButton( CircularActionButton(
icon = CloserGlyphs.Star, icon = CloserGlyphs.Star,
contentDescription = "Maybe", contentDescription = "Maybe",
color = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.74f), color = statusColors.warningContainer,
iconColor = Color(0xFF6B5D00), iconColor = statusColors.warning,
onClick = onMaybe, onClick = onMaybe,
size = 72.dp size = 72.dp
) )
CircularActionButton( CircularActionButton(
icon = CloserGlyphs.Heart, icon = CloserGlyphs.Heart,
contentDescription = "Love", contentDescription = "Love",
color = closerSoftPinkColor(), color = dateTone.accentContainer,
iconColor = Color(0xFF9B1B5A), iconColor = dateTone.accent,
onClick = onLove onClick = onLove
) )
} }
@ -563,6 +569,8 @@ private fun MatchOverlay(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onViewMatches: () -> Unit onViewMatches: () -> Unit
) { ) {
val dateTone = closerToneColors(CloserExperienceTone.Date)
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -619,8 +627,8 @@ private fun MatchOverlay(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFF3E8FF), containerColor = dateTone.accentContainer,
contentColor = Color(0xFF56306F) contentColor = dateTone.onAccentContainer
) )
) { ) {
Text("Keep swiping") Text("Keep swiping")
@ -630,8 +638,8 @@ private fun MatchOverlay(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4), containerColor = dateTone.accent,
contentColor = MaterialTheme.colorScheme.onPrimary contentColor = dateTone.onAccent
) )
) { ) {
Text("View matches") Text("View matches")

View File

@ -57,6 +57,9 @@ import app.closer.ui.components.EmptyState
import app.closer.ui.components.ErrorState import app.closer.ui.components.ErrorState
import app.closer.ui.components.LoadingState import app.closer.ui.components.LoadingState
import app.closer.ui.components.CloserGlyphs import app.closer.ui.components.CloserGlyphs
import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.closerStatusColors
import app.closer.ui.theme.closerToneColors
@Composable @Composable
fun DateMatchesScreen( fun DateMatchesScreen(
@ -87,6 +90,9 @@ private fun DateMatchesContent(
onMarkCompleted: (DateMatch) -> Unit = {}, onMarkCompleted: (DateMatch) -> Unit = {},
onMemories: () -> Unit = {} onMemories: () -> Unit = {}
) { ) {
val dateTone = closerToneColors(CloserExperienceTone.Date)
val statusColors = closerStatusColors()
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -171,7 +177,7 @@ private fun DateMatchesContent(
title = "Mutual love", title = "Mutual love",
count = state.mutualMatches.size, count = state.mutualMatches.size,
icon = CloserGlyphs.Heart, icon = CloserGlyphs.Heart,
iconColor = Color(0xFF9B1B5A), iconColor = dateTone.accent,
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = 8.dp)
) )
} }
@ -186,7 +192,7 @@ private fun DateMatchesContent(
title = "Maybe together", title = "Maybe together",
count = state.maybeMatches.size, count = state.maybeMatches.size,
icon = CloserGlyphs.Star, icon = CloserGlyphs.Star,
iconColor = Color(0xFF6B5D00), iconColor = statusColors.warning,
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = 8.dp)
) )
} }
@ -248,6 +254,8 @@ private fun SectionHeader(
@Composable @Composable
private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) { private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) {
val idea = match.dateIdea ?: return val idea = match.dateIdea ?: return
val dateTone = closerToneColors(CloserExperienceTone.Date)
IdeaCard( IdeaCard(
idea = idea, idea = idea,
action = { action = {
@ -256,7 +264,7 @@ private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) {
imageVector = CloserGlyphs.Heart, imageVector = CloserGlyphs.Heart,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
tint = Color(0xFF9B1B5A) tint = dateTone.accent
) )
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
Text("We did this", fontWeight = FontWeight.SemiBold) Text("We did this", fontWeight = FontWeight.SemiBold)
@ -276,12 +284,12 @@ private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) {
imageVector = CloserGlyphs.Heart, imageVector = CloserGlyphs.Heart,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(14.dp), modifier = Modifier.size(14.dp),
tint = Color(0xFF9B1B5A) tint = dateTone.accent
) )
Text( Text(
text = "Matched", text = "Matched",
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = Color(0xFF9B1B5A), color = dateTone.accent,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }
@ -292,6 +300,8 @@ private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) {
@Composable @Composable
private fun SuggestionCard(suggestion: DateMatchSuggestion) { private fun SuggestionCard(suggestion: DateMatchSuggestion) {
val statusColors = closerStatusColors()
IdeaCard( IdeaCard(
idea = suggestion.dateIdea, idea = suggestion.dateIdea,
badge = { badge = {
@ -308,12 +318,12 @@ private fun SuggestionCard(suggestion: DateMatchSuggestion) {
imageVector = CloserGlyphs.Star, imageVector = CloserGlyphs.Star,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(14.dp), modifier = Modifier.size(14.dp),
tint = Color(0xFF6B5D00) tint = statusColors.warning
) )
Text( Text(
text = if (suggestion.partnerAction == SwipeAction.LOVE) "They love it" else "Maybe match", text = if (suggestion.partnerAction == SwipeAction.LOVE) "They love it" else "Maybe match",
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = Color(0xFF6B5D00), color = statusColors.warning,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }

View File

@ -45,8 +45,8 @@ import app.closer.ui.components.BrandIllustration
import app.closer.ui.components.CloserCard import app.closer.ui.components.CloserCard
import app.closer.ui.components.CloserHeartLoader import app.closer.ui.components.CloserHeartLoader
import app.closer.ui.settings.SettingsSubpage import app.closer.ui.settings.SettingsSubpage
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerCardColor import app.closer.ui.theme.closerCardColor
import app.closer.ui.theme.closerStatusColors
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -216,10 +216,11 @@ private fun DateMemoryCard(row: DateMemoryRow, onClick: () -> Unit, onLongClick:
@Composable @Composable
private fun ReflectionChip(state: DateReflectionState) { private fun ReflectionChip(state: DateReflectionState) {
val statusColors = closerStatusColors()
val (label, color) = when (state) { val (label, color) = when (state) {
DateReflectionState.NONE -> "Reflect" to MaterialTheme.colorScheme.primary DateReflectionState.NONE -> "Reflect" to MaterialTheme.colorScheme.primary
DateReflectionState.AWAITING_PARTNER -> "Waiting" to CloserPalette.Gold DateReflectionState.AWAITING_PARTNER -> "Waiting" to statusColors.warning
DateReflectionState.BOTH_DONE -> "View" to CloserPalette.Evergreen DateReflectionState.BOTH_DONE -> "View" to statusColors.success
} }
Surface(shape = RoundedCornerShape(50), color = color.copy(alpha = 0.14f)) { Surface(shape = RoundedCornerShape(50), color = color.copy(alpha = 0.14f)) {
Text( Text(

View File

@ -49,6 +49,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
@ -69,8 +70,10 @@ import app.closer.ui.games.GameCopy
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import app.closer.ui.components.BrandMessageRotator import app.closer.ui.components.BrandMessageRotator
import app.closer.ui.components.StatusGlyph import app.closer.ui.components.StatusGlyph
import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerToneColors
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -526,6 +529,8 @@ private fun DSSetupScreen(
onStart: () -> Unit, onStart: () -> Unit,
onBack: () -> Unit onBack: () -> Unit
) { ) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -558,16 +563,16 @@ private fun DSSetupScreen(
onClick = { onLengthSelected(len) }, onClick = { onLengthSelected(len) },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
color = if (len == selectedLength) CloserPalette.Romantic else Color.Transparent, color = if (len == selectedLength) desireTone.accent else Color.Transparent,
border = if (len != selectedLength) border = if (len != selectedLength)
BorderStroke(1.dp, CloserPalette.Romantic.copy(alpha = 0.4f)) BorderStroke(1.dp, desireTone.accent.copy(alpha = 0.4f))
else null else null
) { ) {
Text( Text(
text = len.label, text = len.label,
modifier = Modifier.padding(vertical = 10.dp), modifier = Modifier.padding(vertical = 10.dp),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = if (len == selectedLength) Color.White else CloserPalette.Romantic, color = if (len == selectedLength) desireTone.onAccent else desireTone.accent,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
@ -578,7 +583,10 @@ private fun DSSetupScreen(
onClick = onStart, onClick = onStart,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.Romantic) colors = ButtonDefaults.buttonColors(
containerColor = desireTone.accent,
contentColor = desireTone.onAccent
)
) { ) {
Text("Create session") Text("Create session")
} }
@ -587,6 +595,8 @@ private fun DSSetupScreen(
@Composable @Composable
private fun DSIntroScreen(total: Int, onReady: () -> Unit) { private fun DSIntroScreen(total: Int, onReady: () -> Unit) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -598,8 +608,8 @@ private fun DSIntroScreen(total: Int, onReady: () -> Unit) {
) { ) {
StatusGlyph( StatusGlyph(
icon = CloserGlyphs.HeartOutline, icon = CloserGlyphs.HeartOutline,
tint = CloserPalette.Romantic, tint = desireTone.accent,
container = CloserPalette.Romantic.copy(alpha = 0.12f) container = desireTone.accentContainer
) )
Spacer(Modifier.height(20.dp)) Spacer(Modifier.height(20.dp))
Text( Text(
@ -620,13 +630,18 @@ private fun DSIntroScreen(total: Int, onReady: () -> Unit) {
onClick = onReady, onClick = onReady,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.Romantic) colors = ButtonDefaults.buttonColors(
) { Text("I'm ready", color = Color.White) } containerColor = desireTone.accent,
contentColor = desireTone.onAccent
)
) { Text("I'm ready") }
} }
} }
@Composable @Composable
private fun DSWaitingScreen(partnerName: String, onBack: () -> Unit, onAbandon: () -> Unit = {}) { private fun DSWaitingScreen(partnerName: String, onBack: () -> Unit, onAbandon: () -> Unit = {}) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -639,8 +654,8 @@ private fun DSWaitingScreen(partnerName: String, onBack: () -> Unit, onAbandon:
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
StatusGlyph( StatusGlyph(
icon = CloserGlyphs.Heart, icon = CloserGlyphs.Heart,
tint = CloserPalette.Romantic, tint = desireTone.accent,
container = CloserPalette.Romantic.copy(alpha = 0.12f) container = desireTone.accentContainer
) )
Text( Text(
text = "Your answers are in!", text = "Your answers are in!",
@ -708,6 +723,7 @@ private fun DSAnswerScreen(
) { ) {
val config = question.answerConfig as? ChoiceAnswerConfigImpl val config = question.answerConfig as? ChoiceAnswerConfigImpl
val options = config?.config?.options?.take(2) ?: return val options = config?.config?.options?.take(2) ?: return
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Column( Column(
modifier = Modifier modifier = Modifier
@ -724,13 +740,13 @@ private fun DSAnswerScreen(
) { ) {
Surface( Surface(
shape = RoundedCornerShape(999.dp), shape = RoundedCornerShape(999.dp),
color = CloserPalette.Romantic.copy(alpha = 0.12f) color = desireTone.accentContainer
) { ) {
Text( Text(
text = "${index + 1} / $total", text = "${index + 1} / $total",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = CloserPalette.Romantic, color = desireTone.onAccentContainer,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }
@ -769,7 +785,7 @@ private fun DSAnswerScreen(
options.forEach { option -> options.forEach { option ->
val isSelected = pendingSelection == option.id val isSelected = pendingSelection == option.id
val isPositive = option.id.lowercase() in POSITIVE_IDS val isPositive = option.id.lowercase() in POSITIVE_IDS
val selectedColor = if (isPositive) CloserPalette.Romantic else Color(0xFF6B6B8A) val selectedColor = if (isPositive) desireTone.accent else MaterialTheme.colorScheme.outline
val targetColor = when { val targetColor = when {
isSelected -> selectedColor isSelected -> selectedColor
pendingSelection != null -> MaterialTheme.colorScheme.surface.copy(alpha = 0.5f) pendingSelection != null -> MaterialTheme.colorScheme.surface.copy(alpha = 0.5f)
@ -791,7 +807,11 @@ private fun DSAnswerScreen(
Text( Text(
text = option.text, text = option.text,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = if (isSelected) Color.White else MaterialTheme.colorScheme.onSurface, color = if (isSelected) {
if (isPositive) desireTone.onAccent else MaterialTheme.colorScheme.inverseOnSurface
} else {
MaterialTheme.colorScheme.onSurface
},
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
@ -811,6 +831,8 @@ private fun DSRevealScreen(
onOpenDaily: () -> Unit = {}, onOpenDaily: () -> Unit = {},
showNextBeat: Boolean = false showNextBeat: Boolean = false
) { ) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -827,8 +849,8 @@ private fun DSRevealScreen(
) { ) {
StatusGlyph( StatusGlyph(
icon = if (matches.isEmpty()) CloserGlyphs.HeartOutline else CloserGlyphs.Heart, icon = if (matches.isEmpty()) CloserGlyphs.HeartOutline else CloserGlyphs.Heart,
tint = CloserPalette.Romantic, tint = desireTone.accent,
container = CloserPalette.Romantic.copy(alpha = 0.12f), container = desireTone.accentContainer,
size = 82.dp, size = 82.dp,
iconSize = 40.dp iconSize = 40.dp
) )
@ -855,7 +877,7 @@ private fun DSRevealScreen(
Text( Text(
text = "You both said yes to", text = "You both said yes to",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = CloserPalette.Romantic color = desireTone.accent
) )
} }
@ -890,8 +912,11 @@ private fun DSRevealScreen(
onClick = onPlayAgain, onClick = onPlayAgain,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.Romantic) colors = ButtonDefaults.buttonColors(
) { Text("Play again", color = Color.White) } containerColor = desireTone.accent,
contentColor = desireTone.onAccent
)
) { Text("Play again") }
OutlinedButton( OutlinedButton(
onClick = onHome, onClick = onHome,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
@ -910,18 +935,20 @@ private fun DesireProgressPill(
progress: Float, progress: Float,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Box( Box(
modifier = modifier modifier = modifier
.height(8.dp) .height(8.dp)
.clip(RoundedCornerShape(999.dp)) .clip(RoundedCornerShape(999.dp))
.background(CloserPalette.Romantic.copy(alpha = 0.15f)) .background(desireTone.accentContainer)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.fillMaxWidth(progress.coerceIn(0f, 1f)) .fillMaxWidth(progress.coerceIn(0f, 1f))
.clip(RoundedCornerShape(999.dp)) .clip(RoundedCornerShape(999.dp))
.background(CloserPalette.Romantic) .background(desireTone.accent)
) )
} }
} }
@ -932,6 +959,8 @@ private fun DesireRevealMeter(
total: Int, total: Int,
partnerName: String partnerName: String
) { ) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(22.dp), shape = RoundedCornerShape(22.dp),
@ -960,8 +989,8 @@ private fun DesireRevealMeter(
) )
StatusGlyph( StatusGlyph(
icon = CloserGlyphs.Sync, icon = CloserGlyphs.Sync,
tint = CloserPalette.Romantic, tint = desireTone.accent,
container = CloserPalette.Romantic.copy(alpha = 0.12f), container = desireTone.accentContainer,
size = 38.dp, size = 38.dp,
iconSize = 20.dp iconSize = 20.dp
) )
@ -978,7 +1007,7 @@ private fun DesireRevealMeter(
Text( Text(
text = "$matches shared, ${total - matches} kept private", text = "$matches shared, ${total - matches} kept private",
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = CloserPalette.Romantic, color = desireTone.accent,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@ -993,10 +1022,12 @@ private fun DesirePrivacyTile(
value: String, value: String,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Surface( Surface(
modifier = modifier, modifier = modifier,
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
color = CloserPalette.Romantic.copy(alpha = 0.08f) color = desireTone.accentContainer.copy(alpha = 0.55f)
) { ) {
Column( Column(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
@ -1013,7 +1044,7 @@ private fun DesirePrivacyTile(
Text( Text(
text = value, text = value,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = CloserPalette.Romantic, color = desireTone.accent,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
@ -1024,11 +1055,13 @@ private fun DesirePrivacyTile(
@Composable @Composable
private fun DesireMatchCard(match: DesireMatch) { private fun DesireMatchCard(match: DesireMatch) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = CloserPalette.Romantic.copy(alpha = 0.10f) containerColor = desireTone.accentContainer.copy(alpha = 0.62f)
), ),
elevation = CardDefaults.cardElevation(0.dp) elevation = CardDefaults.cardElevation(0.dp)
) { ) {
@ -1039,15 +1072,15 @@ private fun DesireMatchCard(match: DesireMatch) {
) { ) {
StatusGlyph( StatusGlyph(
icon = CloserGlyphs.Heart, icon = CloserGlyphs.Heart,
tint = CloserPalette.Romantic, tint = desireTone.accent,
container = CloserPalette.Romantic.copy(alpha = 0.12f), container = desireTone.accentContainer,
size = 34.dp, size = 34.dp,
iconSize = 18.dp iconSize = 18.dp
) )
Text( Text(
text = match.question.text, text = match.question.text,
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium), style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium),
// Was a hardcoded dark plum (Color(0xFF3D1F2E)) — fine on the light card in // Was a hardcoded dark plum — fine on the light card in
// light mode, but dim/low-contrast on the dark-tinted card in dark mode // light mode, but dim/low-contrast on the dark-tinted card in dark mode
// (C-DS-001). onSurface adapts: dark text on light, light text on dark. // (C-DS-001). onSurface adapts: dark text on light, light text on dark.
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
@ -1059,6 +1092,29 @@ private fun DesireMatchCard(match: DesireMatch) {
} }
} }
@Composable
private fun DesireSyncPreviewBody() {
Box(
modifier = Modifier
.fillMaxSize()
.background(closerBackgroundBrush())
) {
DSIntroScreen(total = 5, onReady = {})
}
}
@Preview(name = "DesireSync • light")
@Composable
private fun DesireSyncPreviewLight() {
CloserTheme(darkTheme = false) { DesireSyncPreviewBody() }
}
@Preview(name = "DesireSync • dark")
@Composable
private fun DesireSyncPreviewDark() {
CloserTheme(darkTheme = true) { DesireSyncPreviewBody() }
}
// ── History Replay ──────────────────────────────────────────────────────────── // ── History Replay ────────────────────────────────────────────────────────────
sealed interface DSReplayPhase { sealed interface DSReplayPhase {

View File

@ -282,7 +282,7 @@ private fun PartnerHomeContent(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = CloserPalette.PurpleDeep, containerColor = CloserPalette.PurpleDeep,
contentColor = Color.White contentColor = MaterialTheme.colorScheme.onPrimary
) )
) { ) {
Text("View today's question", fontWeight = FontWeight.SemiBold) Text("View today's question", fontWeight = FontWeight.SemiBold)

View File

@ -107,31 +107,31 @@ fun HomeActionTone.actionColors(): HomeActionColors {
soft = CloserPalette.PinkSoft, soft = CloserPalette.PinkSoft,
accent = CloserPalette.PinkBright, accent = CloserPalette.PinkBright,
deep = CloserPalette.PinkAccentDeep, deep = CloserPalette.PinkAccentDeep,
onAccent = Color(0xFF24122F) onAccent = MaterialTheme.colorScheme.onSecondaryContainer
) )
HomeActionTone.Reflection -> HomeActionColors( HomeActionTone.Reflection -> HomeActionColors(
soft = CloserPalette.PurpleGlow, soft = CloserPalette.PurpleGlow,
accent = CloserPalette.PurpleRich, accent = CloserPalette.PurpleRich,
deep = CloserPalette.PurpleDeep, deep = CloserPalette.PurpleDeep,
onAccent = Color(0xFF24122F) onAccent = MaterialTheme.colorScheme.onPrimaryContainer
) )
HomeActionTone.Ritual -> HomeActionColors( HomeActionTone.Ritual -> HomeActionColors(
soft = CloserPalette.PurpleSoft, soft = CloserPalette.PurpleSoft,
accent = CloserPalette.PinkBright, accent = CloserPalette.PinkBright,
deep = CloserPalette.PinkAccentDeep, deep = CloserPalette.PinkAccentDeep,
onAccent = Color(0xFF24122F) onAccent = MaterialTheme.colorScheme.onSecondaryContainer
) )
HomeActionTone.Starter -> HomeActionColors( HomeActionTone.Starter -> HomeActionColors(
soft = CloserPalette.PinkSoft, soft = CloserPalette.PinkSoft,
accent = CloserPalette.PinkBright, accent = CloserPalette.PinkBright,
deep = CloserPalette.PinkAccentDeep, deep = CloserPalette.PinkAccentDeep,
onAccent = Color(0xFF24122F) onAccent = MaterialTheme.colorScheme.onSecondaryContainer
) )
HomeActionTone.Pack -> HomeActionColors( HomeActionTone.Pack -> HomeActionColors(
soft = CloserPalette.PinkSoft, soft = CloserPalette.PinkSoft,
accent = CloserPalette.PinkBright, accent = CloserPalette.PinkBright,
deep = CloserPalette.PinkAccentDeep, deep = CloserPalette.PinkAccentDeep,
onAccent = Color(0xFF24122F) onAccent = MaterialTheme.colorScheme.onSecondaryContainer
) )
HomeActionTone.Utility -> HomeActionColors( HomeActionTone.Utility -> HomeActionColors(
soft = CloserPalette.PurpleSoft, soft = CloserPalette.PurpleSoft,

View File

@ -26,7 +26,6 @@ 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.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
@ -125,7 +124,7 @@ private fun PartnerHeaderBubble(
) { ) {
// Brand gradient ring around the avatar — signals "your partner" + that it's tappable. // Brand gradient ring around the avatar — signals "your partner" + that it's tappable.
val ringBrush = Brush.linearGradient( val ringBrush = Brush.linearGradient(
listOf(CloserPalette.PurpleRich, CloserPalette.PinkBright) listOf(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.secondary)
) )
val label = when { val label = when {
!isPaired -> "Invite your partner" !isPaired -> "Invite your partner"
@ -327,12 +326,12 @@ private fun PartnerSheetAction(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (enabled) 1f else 0.5f) color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (enabled) 1f else 0.5f)
) )
if (trailing != null) { if (trailing != null) {
Surface(shape = CircleShape, color = CloserPalette.PinkBright) { Surface(shape = CircleShape, color = MaterialTheme.colorScheme.secondaryContainer) {
Text( Text(
text = trailing, text = trailing,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp), modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = Color.White color = MaterialTheme.colorScheme.onSecondaryContainer
) )
} }
} }

View File

@ -1,6 +1,5 @@
package app.closer.ui.home.components package app.closer.ui.home.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -15,12 +14,12 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
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.window.Dialog import androidx.compose.ui.window.Dialog
import app.closer.R import app.closer.R
import app.closer.ui.components.BrandIllustration
import app.closer.ui.components.CelebrationOverlay import app.closer.ui.components.CelebrationOverlay
import app.closer.ui.components.CloserActionButton import app.closer.ui.components.CloserActionButton
import app.closer.ui.settings.SettingsInk import app.closer.ui.settings.SettingsInk
@ -48,10 +47,11 @@ internal fun StreakMilestoneDialog(
modifier = Modifier.padding(horizontal = 28.dp, vertical = 30.dp), modifier = Modifier.padding(horizontal = 28.dp, vertical = 30.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Image( BrandIllustration(
painter = painterResource(R.drawable.illustration_streak_milestone), res = R.drawable.illustration_streak_milestone,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(156.dp) modifier = Modifier.size(156.dp),
tile = false
) )
Spacer(Modifier.height(18.dp)) Spacer(Modifier.height(18.dp))
Text( Text(

View File

@ -53,6 +53,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
@ -78,8 +79,11 @@ import app.closer.ui.games.GameCopy
import app.closer.ui.components.BrandMessageRotator import app.closer.ui.components.BrandMessageRotator
import app.closer.ui.components.ResultGlyph import app.closer.ui.components.ResultGlyph
import app.closer.ui.components.StatusGlyph import app.closer.ui.components.StatusGlyph
import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerStatusColors
import app.closer.ui.theme.closerToneColors
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.abs import kotlin.math.abs
@ -585,6 +589,8 @@ private fun HWSetupScreen(
onStart: () -> Unit, onStart: () -> Unit,
onBack: () -> Unit onBack: () -> Unit
) { ) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -617,16 +623,16 @@ private fun HWSetupScreen(
onClick = { onLengthSelected(len) }, onClick = { onLengthSelected(len) },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
color = if (len == selectedLength) CloserPalette.PurpleDeep else Color.Transparent, color = if (len == selectedLength) howWellTone.accent else Color.Transparent,
border = if (len != selectedLength) border = if (len != selectedLength)
BorderStroke(1.dp, CloserPalette.PurpleDeep.copy(alpha = 0.4f)) BorderStroke(1.dp, howWellTone.accent.copy(alpha = 0.4f))
else null else null
) { ) {
Text( Text(
text = len.label, text = len.label,
modifier = Modifier.padding(vertical = 10.dp), modifier = Modifier.padding(vertical = 10.dp),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = if (len == selectedLength) Color.White else CloserPalette.PurpleDeep, color = if (len == selectedLength) howWellTone.onAccent else howWellTone.accent,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
@ -637,7 +643,10 @@ private fun HWSetupScreen(
onClick = onStart, onClick = onStart,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep) colors = ButtonDefaults.buttonColors(
containerColor = howWellTone.accent,
contentColor = howWellTone.onAccent
)
) { ) {
Text("Create session") Text("Create session")
} }
@ -651,6 +660,8 @@ private fun PlayerIntroScreen(
total: Int, total: Int,
onReady: () -> Unit onReady: () -> Unit
) { ) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -662,8 +673,8 @@ private fun PlayerIntroScreen(
) { ) {
StatusGlyph( StatusGlyph(
icon = if (amSubject) CloserGlyphs.Person else CloserGlyphs.Psychology, icon = if (amSubject) CloserGlyphs.Person else CloserGlyphs.Psychology,
tint = CloserPalette.PurpleDeep, tint = howWellTone.accent,
container = CloserPalette.PurpleMist container = howWellTone.accentContainer
) )
Spacer(Modifier.height(20.dp)) Spacer(Modifier.height(20.dp))
Text( Text(
@ -691,13 +702,18 @@ private fun PlayerIntroScreen(
onClick = onReady, onClick = onReady,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep) colors = ButtonDefaults.buttonColors(
) { Text("I'm ready", color = Color.White) } containerColor = howWellTone.accent,
contentColor = howWellTone.onAccent
)
) { Text("I'm ready") }
} }
} }
@Composable @Composable
private fun HowWellWaitingScreen(amSubject: Boolean, partnerName: String, onBack: () -> Unit, onAbandon: () -> Unit = {}) { private fun HowWellWaitingScreen(amSubject: Boolean, partnerName: String, onBack: () -> Unit, onAbandon: () -> Unit = {}) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -710,8 +726,8 @@ private fun HowWellWaitingScreen(amSubject: Boolean, partnerName: String, onBack
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
StatusGlyph( StatusGlyph(
icon = CloserGlyphs.Sync, icon = CloserGlyphs.Sync,
tint = CloserPalette.PurpleDeep, tint = howWellTone.accent,
container = CloserPalette.PurpleMist container = howWellTone.accentContainer
) )
Text( Text(
text = "All done on your side!", text = "All done on your side!",
@ -786,6 +802,7 @@ private fun AnswerScreen(
onQuit: () -> Unit onQuit: () -> Unit
) { ) {
val hasSelection = selectedOptionId != null || selectedScale != null val hasSelection = selectedOptionId != null || selectedScale != null
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Column( Column(
modifier = Modifier modifier = Modifier
@ -800,12 +817,12 @@ private fun AnswerScreen(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Surface(shape = RoundedCornerShape(999.dp), color = CloserPalette.PurpleMist) { Surface(shape = RoundedCornerShape(999.dp), color = howWellTone.accentContainer) {
Text( Text(
text = "${if (isGuesser) "Your guess" else "About you"} · ${index + 1} / $total", text = "${if (isGuesser) "Your guess" else "About you"} · ${index + 1} / $total",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = CloserPalette.PurpleDeep, color = howWellTone.onAccentContainer,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }
@ -831,12 +848,12 @@ private fun AnswerScreen(
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
if (isGuesser) { if (isGuesser) {
Surface(shape = RoundedCornerShape(999.dp), color = CloserPalette.PurpleMist) { Surface(shape = RoundedCornerShape(999.dp), color = howWellTone.accentContainer) {
Text( Text(
text = "How did $partnerName answer?", text = "How did $partnerName answer?",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 5.dp), modifier = Modifier.padding(horizontal = 12.dp, vertical = 5.dp),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = CloserPalette.PurpleDeep, color = howWellTone.onAccentContainer,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }
@ -881,11 +898,13 @@ private fun AnswerScreen(
enabled = hasSelection, enabled = hasSelection,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep) colors = ButtonDefaults.buttonColors(
containerColor = howWellTone.accent,
contentColor = howWellTone.onAccent
)
) { ) {
Text( Text(
text = if (index + 1 >= total) "Done →" else "Confirm →", text = if (index + 1 >= total) "Done →" else "Confirm →"
color = Color.White
) )
} }
} }
@ -904,6 +923,8 @@ private fun CompleteScreen(
onOpenDaily: () -> Unit = {}, onOpenDaily: () -> Unit = {},
showNextBeat: Boolean = false showNextBeat: Boolean = false
) { ) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -963,8 +984,11 @@ private fun CompleteScreen(
onClick = onSwapRoles, onClick = onSwapRoles,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep) colors = ButtonDefaults.buttonColors(
) { Text("Your turn — let $partnerName guess you", color = Color.White) } containerColor = howWellTone.accent,
contentColor = howWellTone.onAccent
)
) { Text("Your turn — let $partnerName guess you") }
OutlinedButton( OutlinedButton(
onClick = onPlayAgain, onClick = onPlayAgain,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
@ -975,8 +999,11 @@ private fun CompleteScreen(
onClick = onPlayAgain, onClick = onPlayAgain,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep) colors = ButtonDefaults.buttonColors(
) { Text("Play again", color = Color.White) } containerColor = howWellTone.accent,
contentColor = howWellTone.onAccent
)
) { Text("Play again") }
} }
OutlinedButton( OutlinedButton(
onClick = onHome, onClick = onHome,
@ -996,18 +1023,20 @@ private fun HowWellProgressPill(
progress: Float, progress: Float,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Box( Box(
modifier = modifier modifier = modifier
.height(8.dp) .height(8.dp)
.clip(RoundedCornerShape(999.dp)) .clip(RoundedCornerShape(999.dp))
.background(CloserPalette.PurpleMist) .background(howWellTone.accentContainer)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.fillMaxWidth(progress.coerceIn(0f, 1f)) .fillMaxWidth(progress.coerceIn(0f, 1f))
.clip(RoundedCornerShape(999.dp)) .clip(RoundedCornerShape(999.dp))
.background(CloserPalette.PurpleDeep) .background(howWellTone.accent)
) )
} }
} }
@ -1019,6 +1048,8 @@ private fun HowWellScoreRing(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val progress = if (total == 0) 0f else score.toFloat() / total val progress = if (total == 0) 0f else score.toFloat() / total
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Box( Box(
modifier = modifier.semantics(mergeDescendants = true) { modifier = modifier.semantics(mergeDescendants = true) {
contentDescription = "You guessed $score of $total correctly" contentDescription = "You guessed $score of $total correctly"
@ -1028,14 +1059,14 @@ private fun HowWellScoreRing(
Canvas(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier.fillMaxSize()) {
val strokeWidth = 11.dp.toPx() val strokeWidth = 11.dp.toPx()
drawArc( drawArc(
color = CloserPalette.PurpleMist, color = howWellTone.accentContainer,
startAngle = -90f, startAngle = -90f,
sweepAngle = 360f, sweepAngle = 360f,
useCenter = false, useCenter = false,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round) style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
) )
drawArc( drawArc(
color = CloserPalette.PurpleDeep, color = howWellTone.accent,
startAngle = -90f, startAngle = -90f,
sweepAngle = 360f * progress.coerceIn(0f, 1f), sweepAngle = 360f * progress.coerceIn(0f, 1f),
useCenter = false, useCenter = false,
@ -1046,7 +1077,7 @@ private fun HowWellScoreRing(
Text( Text(
text = "$score", text = "$score",
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold), style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
color = CloserPalette.PurpleDeep color = howWellTone.accent
) )
Text( Text(
text = "of $total", text = "of $total",
@ -1060,14 +1091,12 @@ private fun HowWellScoreRing(
@Composable @Composable
private fun BreakdownRow(result: HowWellResult) { private fun BreakdownRow(result: HowWellResult) {
// Match-row colors come as a container+content PAIR per theme: the old fixed pale-green val statusColors = closerStatusColors()
// container kept theme-driven onSurfaceVariant text, which is near-invisible in dark mode. val matchColor = statusColors.success
val dark = androidx.compose.foundation.isSystemInDarkTheme() val closeColor = statusColors.warning
val matchColor = if (dark) Color(0xFF81C995) else Color(0xFF2E7D32) val missColor = statusColors.danger
val closeColor = if (dark) Color(0xFFF6C453) else Color(0xFFF57F17) val matchContainer = statusColors.successContainer
val missColor = if (dark) Color(0xFFE58A82) else Color(0xFFC62828) val questionColor = if (result.isMatch) statusColors.success else MaterialTheme.colorScheme.onSurfaceVariant
val matchContainer = if (dark) Color(0xFF223A2A) else Color(0xFFE8F5E9)
val questionColor = if (result.isMatch && dark) Color(0xFFB9CBBE) else MaterialTheme.colorScheme.onSurfaceVariant
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp), shape = RoundedCornerShape(14.dp),
@ -1136,6 +1165,8 @@ private fun SingleChoiceInput(
selectedId: String?, selectedId: String?,
onSelect: (String) -> Unit onSelect: (String) -> Unit
) { ) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
options.forEach { option -> options.forEach { option ->
val selected = selectedId == option.id val selected = selectedId == option.id
@ -1144,7 +1175,7 @@ private fun SingleChoiceInput(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp), shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = if (selected) CloserPalette.PurpleDeep else MaterialTheme.colorScheme.surface containerColor = if (selected) howWellTone.accent else MaterialTheme.colorScheme.surface
), ),
elevation = CardDefaults.cardElevation(if (selected) 6.dp else 2.dp) elevation = CardDefaults.cardElevation(if (selected) 6.dp else 2.dp)
) { ) {
@ -1152,7 +1183,7 @@ private fun SingleChoiceInput(
text = option.text, text = option.text,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium), style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium),
color = if (selected) Color.White else MaterialTheme.colorScheme.onSurface, color = if (selected) howWellTone.onAccent else MaterialTheme.colorScheme.onSurface,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
@ -1168,6 +1199,8 @@ private fun BinaryChoiceInput(
selectedId: String?, selectedId: String?,
onSelect: (String) -> Unit onSelect: (String) -> Unit
) { ) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
listOf(optionA to "A", optionB to "B").forEach { (option, label) -> listOf(optionA to "A", optionB to "B").forEach { (option, label) ->
val selected = selectedId == option.id val selected = selectedId == option.id
@ -1176,7 +1209,7 @@ private fun BinaryChoiceInput(
modifier = Modifier.weight(1f).heightIn(min = 80.dp), modifier = Modifier.weight(1f).heightIn(min = 80.dp),
shape = RoundedCornerShape(14.dp), shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = if (selected) CloserPalette.PurpleDeep else MaterialTheme.colorScheme.surface containerColor = if (selected) howWellTone.accent else MaterialTheme.colorScheme.surface
), ),
elevation = CardDefaults.cardElevation(if (selected) 6.dp else 2.dp) elevation = CardDefaults.cardElevation(if (selected) 6.dp else 2.dp)
) { ) {
@ -1189,12 +1222,12 @@ private fun BinaryChoiceInput(
text = label, text = label,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = if (selected) Color.White.copy(alpha = 0.7f) else MaterialTheme.colorScheme.onSurfaceVariant color = if (selected) howWellTone.onAccent.copy(alpha = 0.7f) else MaterialTheme.colorScheme.onSurfaceVariant
) )
Text( Text(
text = option.text, text = option.text,
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
color = if (selected) Color.White else MaterialTheme.colorScheme.onSurface, color = if (selected) howWellTone.onAccent else MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
@ -1214,6 +1247,8 @@ private fun ScaleInput(
selected: Int?, selected: Int?,
onSelect: (Int) -> Unit onSelect: (Int) -> Unit
) { ) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -1224,7 +1259,7 @@ private fun ScaleInput(
Surface( Surface(
onClick = { onSelect(value) }, onClick = { onSelect(value) },
shape = CircleShape, shape = CircleShape,
color = if (isSelected) CloserPalette.PurpleDeep else MaterialTheme.colorScheme.surface, color = if (isSelected) howWellTone.accent else MaterialTheme.colorScheme.surface,
modifier = Modifier.size(52.dp), modifier = Modifier.size(52.dp),
shadowElevation = if (isSelected) 6.dp else 2.dp shadowElevation = if (isSelected) 6.dp else 2.dp
) { ) {
@ -1232,7 +1267,7 @@ private fun ScaleInput(
Text( Text(
text = "$value", text = "$value",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = if (isSelected) Color.White else MaterialTheme.colorScheme.onSurface color = if (isSelected) howWellTone.onAccent else MaterialTheme.colorScheme.onSurface
) )
} }
} }
@ -1255,6 +1290,34 @@ private fun scoreLabel(score: Int, total: Int): String = when {
else -> "Room to grow" else -> "Room to grow"
} }
@Composable
private fun HowWellPreviewBody() {
Box(
modifier = Modifier
.fillMaxSize()
.background(closerBackgroundBrush())
) {
PlayerIntroScreen(
amSubject = false,
partnerName = "Sam",
total = 5,
onReady = {}
)
}
}
@Preview(name = "HowWell • light")
@Composable
private fun HowWellPreviewLight() {
CloserTheme(darkTheme = false) { HowWellPreviewBody() }
}
@Preview(name = "HowWell • dark")
@Composable
private fun HowWellPreviewDark() {
CloserTheme(darkTheme = true) { HowWellPreviewBody() }
}
// ── History Replay ──────────────────────────────────────────────────────────── // ── History Replay ────────────────────────────────────────────────────────────
sealed interface HWReplayPhase { sealed interface HWReplayPhase {

View File

@ -71,8 +71,10 @@ import app.closer.data.remote.FirestoreCapsuleDataSource
import app.closer.domain.model.TimeCapsule import app.closer.domain.model.TimeCapsule
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.theme.CloserExperienceTone
import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerBackgroundBrush import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerToneColors
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@ -445,6 +447,8 @@ fun MemoryLaneScreen(
@Composable @Composable
private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) { private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) {
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -454,12 +458,12 @@ private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Surface(shape = RoundedCornerShape(50.dp), color = CloserPalette.Romantic.copy(alpha = 0.12f)) { Surface(shape = RoundedCornerShape(50.dp), color = premiumTone.accentContainer) {
Icon( Icon(
CloserGlyphs.Lock, CloserGlyphs.Lock,
contentDescription = null, contentDescription = null,
modifier = Modifier.padding(20.dp).size(32.dp), modifier = Modifier.padding(20.dp).size(32.dp),
tint = CloserPalette.Romantic tint = premiumTone.accent
) )
} }
Spacer(Modifier.height(20.dp)) Spacer(Modifier.height(20.dp))
@ -467,8 +471,16 @@ private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) {
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
Text("Write notes to your future selves — sealed until the date you choose.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center) Text("Write notes to your future selves — sealed until the date you choose.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
Spacer(Modifier.height(28.dp)) Spacer(Modifier.height(28.dp))
Button(onClick = onUnlock, modifier = Modifier.fillMaxWidth().heightIn(min = 54.dp), shape = RoundedCornerShape(18.dp), colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.Romantic)) { Button(
Text("Unlock premium", color = Color.White, style = MaterialTheme.typography.labelLarge) onClick = onUnlock,
modifier = Modifier.fillMaxWidth().heightIn(min = 54.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(
containerColor = premiumTone.accent,
contentColor = premiumTone.onAccent
)
) {
Text("Unlock premium", style = MaterialTheme.typography.labelLarge)
} }
TextButton(onClick = onBack) { Text("Go back") } TextButton(onClick = onBack) { Text("Go back") }
} }
@ -478,6 +490,8 @@ private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) {
@Composable @Composable
private fun MemoryLaneErrorScreen(message: String, onBack: () -> Unit, onRetry: () -> Unit) { private fun MemoryLaneErrorScreen(message: String, onBack: () -> Unit, onRetry: () -> Unit) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -491,8 +505,15 @@ private fun MemoryLaneErrorScreen(message: String, onBack: () -> Unit, onRetry:
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text(message, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center) Text(message, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
Button(onClick = onRetry, shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)) { Button(
Text("Try again", color = Color.White) onClick = onRetry,
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = revealTone.accent,
contentColor = revealTone.onAccent
)
) {
Text("Try again")
} }
TextButton(onClick = onBack) { Text("Go back") } TextButton(onClick = onBack) { Text("Go back") }
} }
@ -507,6 +528,8 @@ private fun CapsuleListScreen(
onNew: () -> Unit, onNew: () -> Unit,
onOpen: (TimeCapsule) -> Unit onOpen: (TimeCapsule) -> Unit
) { ) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -529,8 +552,8 @@ private fun CapsuleListScreen(
Text("Notes that open on a future date.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) Text("Notes that open on a future date.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
IconButton(onClick = onNew) { IconButton(onClick = onNew) {
Surface(shape = RoundedCornerShape(12.dp), color = CloserPalette.PurpleDeep) { Surface(shape = RoundedCornerShape(12.dp), color = revealTone.accent) {
Icon(CloserGlyphs.Add, contentDescription = "New capsule", tint = Color.White, modifier = Modifier.padding(6.dp).size(20.dp)) Icon(CloserGlyphs.Add, contentDescription = "New capsule", tint = revealTone.onAccent, modifier = Modifier.padding(6.dp).size(20.dp))
} }
} }
} }
@ -553,8 +576,15 @@ private fun CapsuleListScreen(
Text("No capsules yet", style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), color = MaterialTheme.colorScheme.onSurface) Text("No capsules yet", style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), color = MaterialTheme.colorScheme.onSurface)
Text("Write a note to your future selves — it'll stay sealed until the date you choose.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center) Text("Write a note to your future selves — it'll stay sealed until the date you choose.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Button(onClick = onNew, shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)) { Button(
Text("Write your first capsule", color = Color.White) onClick = onNew,
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = revealTone.accent,
contentColor = revealTone.onAccent
)
) {
Text("Write your first capsule")
} }
} }
} }
@ -635,6 +665,8 @@ private fun CapsuleCreateScreen(
onHideDatePicker: () -> Unit, onHideDatePicker: () -> Unit,
onSave: () -> Unit onSave: () -> Unit
) { ) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
if (state.showDatePicker) { if (state.showDatePicker) {
val pickerState = rememberDatePickerState( val pickerState = rememberDatePickerState(
initialSelectedDateMillis = state.customUnlockMs ?: (System.currentTimeMillis() + UnlockPreset.ONE_YEAR.ms) initialSelectedDateMillis = state.customUnlockMs ?: (System.currentTimeMillis() + UnlockPreset.ONE_YEAR.ms)
@ -774,10 +806,13 @@ private fun CapsuleCreateScreen(
enabled = !state.isSaving, enabled = !state.isSaving,
modifier = Modifier.fillMaxWidth().heightIn(min = 54.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 54.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep) colors = ButtonDefaults.buttonColors(
containerColor = revealTone.accent,
contentColor = revealTone.onAccent
)
) { ) {
if (state.isSaving) CloserHeartLoader(size = 22.dp) if (state.isSaving) CloserHeartLoader(size = 22.dp)
else Text(if (isEditing) "Save changes" else "Seal the capsule 📦", color = Color.White, style = MaterialTheme.typography.labelLarge) else Text(if (isEditing) "Save changes" else "Seal the capsule 📦", style = MaterialTheme.typography.labelLarge)
} }
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))

View File

@ -56,6 +56,7 @@ 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.components.CloserHeartLoader
import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerStatusColors
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
private const val CTA_PAGE = 3 private const val CTA_PAGE = 3
@ -388,6 +389,8 @@ private fun RevealCard(label: String, preview: String, revealed: Boolean, color:
@Composable @Composable
private fun GrowerVisual() { private fun GrowerVisual() {
val statusColors = closerStatusColors()
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
@ -400,7 +403,7 @@ private fun GrowerVisual() {
GrowthPill("Desire Sync", CloserPalette.PinkMist, CloserPalette.Romantic) GrowthPill("Desire Sync", CloserPalette.PinkMist, CloserPalette.Romantic)
GrowthPill("Memory Lane", CloserPalette.PurpleGlow, CloserPalette.PurpleDeep) GrowthPill("Memory Lane", CloserPalette.PurpleGlow, CloserPalette.PurpleDeep)
} }
GrowthPill("Question threads", Color(0xFFE8F5E9), Color(0xFF2E7D32)) GrowthPill("Question threads", statusColors.successContainer, statusColors.success)
} }
} }

View File

@ -47,6 +47,7 @@ import app.closer.ui.settings.SettingsMuted
import app.closer.ui.settings.SettingsPrimaryDeep import app.closer.ui.settings.SettingsPrimaryDeep
import app.closer.ui.settings.SettingsSoft import app.closer.ui.settings.SettingsSoft
import app.closer.ui.components.CloserGlyphs import app.closer.ui.components.CloserGlyphs
import app.closer.ui.theme.closerStatusColors
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -206,9 +207,10 @@ private fun DeltaCard(baseline: Outcome, latest: Outcome) {
@Composable @Composable
private fun DeltaRow(label: String, delta: Int) { private fun DeltaRow(label: String, delta: Int) {
val statusColors = closerStatusColors()
val color = when { val color = when {
delta > 0 -> SettingsPrimaryDeep delta > 0 -> SettingsPrimaryDeep
delta < 0 -> Color(0xFFB00020) delta < 0 -> statusColors.danger
else -> SettingsMuted else -> SettingsMuted
} }
val sign = when { val sign = when {

View File

@ -326,7 +326,7 @@ private fun PairAvatar(url: String, modifier: Modifier = Modifier) {
error = rememberVectorPainter(CloserGlyphs.Person), error = rememberVectorPainter(CloserGlyphs.Person),
modifier = modifier modifier = modifier
.clip(CircleShape) .clip(CircleShape)
.border(4.dp, Color.White, CircleShape) .border(4.dp, MaterialTheme.colorScheme.surface, CircleShape)
.background(SettingsSoft) .background(SettingsSoft)
) )
} else { } else {
@ -334,7 +334,7 @@ private fun PairAvatar(url: String, modifier: Modifier = Modifier) {
Box( Box(
modifier = modifier modifier = modifier
.clip(CircleShape) .clip(CircleShape)
.border(4.dp, Color.White, CircleShape) .border(4.dp, MaterialTheme.colorScheme.surface, CircleShape)
.background(SettingsSoft), .background(SettingsSoft),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {

View File

@ -6,7 +6,6 @@ import app.closer.ui.theme.closerSoftSurfaceColor
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -49,12 +48,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment 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.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import app.closer.ui.components.BrandIllustration import app.closer.ui.components.BrandIllustration
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -67,7 +62,8 @@ import app.closer.core.navigation.ExternalLinks
import app.closer.domain.repository.BillingState import app.closer.domain.repository.BillingState
import app.closer.ui.components.ErrorState import app.closer.ui.components.ErrorState
import app.closer.ui.components.LoadingState import app.closer.ui.components.LoadingState
import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.closerToneColors
import com.revenuecat.purchases.Package import com.revenuecat.purchases.Package
import app.closer.ui.brand.CloserCopy import app.closer.ui.brand.CloserCopy
import app.closer.ui.components.CloserGlyphs import app.closer.ui.components.CloserGlyphs
@ -184,6 +180,8 @@ private fun HeaderSection(
onClose: () -> Unit, onClose: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
Row( Row(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(14.dp), horizontalArrangement = Arrangement.spacedBy(14.dp),
@ -215,7 +213,7 @@ private fun HeaderSection(
Icon( Icon(
imageVector = CloserGlyphs.Close, imageVector = CloserGlyphs.Close,
contentDescription = "Close", contentDescription = "Close",
tint = Color(0xFF9B8AA6) tint = premiumTone.muted
) )
} }
} }
@ -254,10 +252,12 @@ private fun BenefitsCard(modifier: Modifier = Modifier) {
@Composable @Composable
private fun BenefitPill(label: String) { private fun BenefitPill(label: String) {
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
Row( Row(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(999.dp)) .clip(RoundedCornerShape(999.dp))
.background(CloserPalette.PurpleMist) .background(premiumTone.accentContainer)
.padding(horizontal = 10.dp, vertical = 7.dp), .padding(horizontal = 10.dp, vertical = 7.dp),
horizontalArrangement = Arrangement.spacedBy(7.dp), horizontalArrangement = Arrangement.spacedBy(7.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -265,16 +265,13 @@ private fun BenefitPill(label: String) {
Icon( Icon(
imageVector = CloserGlyphs.Check, imageVector = CloserGlyphs.Check,
contentDescription = null, contentDescription = null,
tint = CloserPalette.PurpleDeep, tint = premiumTone.onAccentContainer,
modifier = Modifier.size(15.dp) modifier = Modifier.size(15.dp)
) )
Text( Text(
text = label, text = label,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
// The pill background is always the light PurpleMist (not theme-adaptive), so the color = premiumTone.onAccentContainer
// text needs a fixed dark brand color — onSurface is near-white in dark mode and
// rendered the labels invisible (C-PW-001).
color = CloserPalette.PurpleDeep
) )
} }
} }
@ -286,10 +283,12 @@ private fun PlanOptions(
onSelect: (Package) -> Unit, onSelect: (Package) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
Card( Card(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(28.dp), shape = RoundedCornerShape(28.dp),
colors = CardDefaults.cardColors(containerColor = CloserPalette.PurpleSoft), colors = CardDefaults.cardColors(containerColor = premiumTone.accentContainer.copy(alpha = 0.56f)),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) { ) {
Column( Column(
@ -306,7 +305,7 @@ private fun PlanOptions(
Text( Text(
text = CloserCopy.Paywall.coupleShared, text = CloserCopy.Paywall.coupleShared,
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium), style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium),
color = Color(0xFF56306F) color = premiumTone.onAccentContainer
) )
if (packages.isEmpty()) { if (packages.isEmpty()) {
@ -336,13 +335,15 @@ private fun PlanRow(
onSelect: () -> Unit, onSelect: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
Row( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(18.dp)) .clip(RoundedCornerShape(18.dp))
.background( .background(
if (isSelected) CloserPalette.PurpleRich.copy(alpha = 0.20f) if (isSelected) premiumTone.accentContainer
else Color.White.copy(alpha = 0.64f) else MaterialTheme.colorScheme.surface.copy(alpha = 0.64f)
) )
.selectable( .selectable(
selected = isSelected, selected = isSelected,
@ -357,8 +358,8 @@ private fun PlanRow(
selected = isSelected, selected = isSelected,
onClick = null, onClick = null,
colors = RadioButtonDefaults.colors( colors = RadioButtonDefaults.colors(
selectedColor = Color(0xFFB98AF4), selectedColor = premiumTone.accent,
unselectedColor = Color(0xFF9B8AA6) unselectedColor = premiumTone.muted
) )
) )
@ -374,14 +375,14 @@ private fun PlanRow(
Text( Text(
text = pricing.offer ?: pkg.product.description, text = pricing.offer ?: pkg.product.description,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = if (pricing.offer != null) Color(0xFF56306F) else MaterialTheme.colorScheme.onSurfaceVariant color = if (pricing.offer != null) premiumTone.accent else MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
Text( Text(
text = pricing.price, text = pricing.price,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF56306F) color = premiumTone.accent
) )
} }
} }
@ -394,6 +395,8 @@ private fun ActionButtons(
onRestore: () -> Unit, onRestore: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
Column( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
@ -407,10 +410,10 @@ private fun ActionButtons(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = CloserPalette.PurpleDeep, containerColor = premiumTone.accent,
contentColor = MaterialTheme.colorScheme.onPrimary, contentColor = premiumTone.onAccent,
disabledContainerColor = CloserPalette.PurpleRich.copy(alpha = 0.40f), disabledContainerColor = premiumTone.accent.copy(alpha = 0.40f),
disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.54f) disabledContentColor = premiumTone.onAccent.copy(alpha = 0.54f)
) )
) { ) {
Text("Continue", fontWeight = FontWeight.SemiBold) Text("Continue", fontWeight = FontWeight.SemiBold)
@ -438,7 +441,7 @@ private fun ActionButtons(
TextButton(onClick = onRestore) { TextButton(onClick = onRestore) {
Text( Text(
text = "Restore", text = "Restore",
color = CloserPalette.PurpleDeep, color = premiumTone.accent,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }
@ -452,6 +455,8 @@ private fun LegalLinks(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current val context = LocalContext.current
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
Column( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@ -461,10 +466,10 @@ private fun LegalLinks(
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally) horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally)
) { ) {
TextButton(onClick = { ExternalLinks.openUrl(context, ExternalLinks.PRIVACY_POLICY) }) { TextButton(onClick = { ExternalLinks.openUrl(context, ExternalLinks.PRIVACY_POLICY) }) {
Text("Privacy Policy", style = MaterialTheme.typography.labelSmall, color = Color(0xFF9B8AA6)) Text("Privacy Policy", style = MaterialTheme.typography.labelSmall, color = premiumTone.muted)
} }
TextButton(onClick = { ExternalLinks.openUrl(context, ExternalLinks.TERMS_OF_SERVICE) }) { TextButton(onClick = { ExternalLinks.openUrl(context, ExternalLinks.TERMS_OF_SERVICE) }) {
Text("Terms of Service", style = MaterialTheme.typography.labelSmall, color = Color(0xFF9B8AA6)) Text("Terms of Service", style = MaterialTheme.typography.labelSmall, color = premiumTone.muted)
} }
} }
@ -472,7 +477,7 @@ private fun LegalLinks(
Text( Text(
"Subscription terms apply", "Subscription terms apply",
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = Color(0xFF9B8AA6) color = premiumTone.muted
) )
} }
} }
@ -480,6 +485,8 @@ private fun LegalLinks(
@Composable @Composable
private fun ThankYouOverlay(onDismiss: () -> Unit) { private fun ThankYouOverlay(onDismiss: () -> Unit) {
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -498,7 +505,7 @@ private fun ThankYouOverlay(onDismiss: () -> Unit) {
Icon( Icon(
imageVector = CloserGlyphs.Check, imageVector = CloserGlyphs.Check,
contentDescription = null, contentDescription = null,
tint = Color(0xFF56306F), tint = premiumTone.accent,
modifier = Modifier.size(48.dp) modifier = Modifier.size(48.dp)
) )
Text( Text(
@ -516,8 +523,8 @@ private fun ThankYouOverlay(onDismiss: () -> Unit) {
onClick = onDismiss, onClick = onDismiss,
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4), containerColor = premiumTone.accent,
contentColor = MaterialTheme.colorScheme.onPrimary contentColor = premiumTone.onAccent
) )
) { ) {
Text("Continue") Text("Continue")

View File

@ -1,6 +1,5 @@
package app.closer.ui.play package app.closer.ui.play
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -25,8 +24,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -37,6 +34,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import app.closer.R import app.closer.R
import app.closer.core.navigation.AppRoute import app.closer.core.navigation.AppRoute
import app.closer.ui.components.BrandIllustration
import app.closer.ui.components.CategoryGlyph import app.closer.ui.components.CategoryGlyph
import app.closer.ui.components.CloserActionButton import app.closer.ui.components.CloserActionButton
import app.closer.ui.components.CloserButtonStyle import app.closer.ui.components.CloserButtonStyle
@ -45,6 +43,7 @@ import app.closer.ui.components.CloserElevations
import app.closer.ui.components.CloserPill import app.closer.ui.components.CloserPill
import app.closer.ui.components.UnpairedLockedCard import app.closer.ui.components.UnpairedLockedCard
import app.closer.ui.components.CloserRadii import app.closer.ui.components.CloserRadii
import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerPlayCardBrush import app.closer.ui.theme.closerPlayCardBrush
import app.closer.ui.components.CloserGlyphs import app.closer.ui.components.CloserGlyphs
@ -802,18 +801,29 @@ private fun WheelGlyph(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = Modifier.padding(5.dp) modifier = Modifier.padding(5.dp)
) { ) {
Image( BrandIllustration(
painter = painterResource(R.drawable.illustration_spin_wheel), res = R.drawable.illustration_spin_wheel,
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Fit, modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize() tile = false
) )
} }
} }
} }
@Preview
@Composable @Composable
fun PlayHubScreenPreview() { private fun PlayHubScreenPreviewBody() {
PlayHubContent(onNavigate = {}, hasPremium = true) PlayHubContent(onNavigate = {}, hasPremium = true)
} }
@Preview(name = "Play hub • light")
@Composable
fun PlayHubScreenPreviewLight() {
CloserTheme(darkTheme = false) { PlayHubScreenPreviewBody() }
}
@Preview(name = "Play hub • dark")
@Composable
fun PlayHubScreenPreviewDark() {
CloserTheme(darkTheme = true) { PlayHubScreenPreviewBody() }
}

View File

@ -191,10 +191,10 @@ fun DeleteAccountScreen(
enabled = state.canDelete, enabled = state.canDelete,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = SettingsDanger, containerColor = SettingsDanger,
contentColor = Color.White contentColor = MaterialTheme.colorScheme.onError
) )
) { ) {
Text("Delete", color = Color.White) Text("Delete")
} }
}, },
dismissButton = { dismissButton = {
@ -249,13 +249,13 @@ fun DeleteAccountScreen(
enabled = !state.isReauthing, enabled = !state.isReauthing,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = SettingsDanger, containerColor = SettingsDanger,
contentColor = Color.White contentColor = MaterialTheme.colorScheme.onError
) )
) { ) {
if (state.isReauthing) { if (state.isReauthing) {
CloserHeartLoader(size = 18.dp) CloserHeartLoader(size = 18.dp)
} else { } else {
Text("Confirm", color = Color.White) Text("Confirm")
} }
} }
} }
@ -332,16 +332,16 @@ fun DeleteAccountScreen(
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = SettingsDanger, containerColor = SettingsDanger,
contentColor = Color.White contentColor = MaterialTheme.colorScheme.onError
) )
) { ) {
if (state.isDeleting) { if (state.isDeleting) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
CloserHeartLoader(size = 20.dp) CloserHeartLoader(size = 20.dp)
Text("Deleting…", color = Color.White) Text("Deleting…")
} }
} else { } else {
Text("Delete my account", color = Color.White, fontWeight = FontWeight.SemiBold) Text("Delete my account", fontWeight = FontWeight.SemiBold)
} }
} }
} }

View File

@ -114,10 +114,10 @@ fun RelationshipSettingsScreen(
enabled = !state.isLeaving, enabled = !state.isLeaving,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = SettingsDanger, containerColor = SettingsDanger,
contentColor = Color.White contentColor = MaterialTheme.colorScheme.onError
) )
) { ) {
Text("Leave", color = Color.White) Text("Leave")
} }
}, },
dismissButton = { dismissButton = {
@ -180,16 +180,16 @@ fun RelationshipSettingsScreen(
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = SettingsDanger, containerColor = SettingsDanger,
contentColor = Color.White contentColor = MaterialTheme.colorScheme.onError
) )
) { ) {
if (state.isLeaving) { if (state.isLeaving) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
CloserHeartLoader(size = 20.dp) CloserHeartLoader(size = 20.dp)
Text("Leaving…", color = Color.White) Text("Leaving…")
} }
} else { } else {
Text("Leave this couple", color = Color.White, fontWeight = FontWeight.SemiBold) Text("Leave this couple", fontWeight = FontWeight.SemiBold)
} }
} }
} }

View File

@ -112,13 +112,13 @@ fun SettingsSubpage(
fun SettingsSection( fun SettingsSection(
title: String? = null, title: String? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
accent: Color = Color(0xFFF7C8E4), accent: Color? = null,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val sectionAccent = if (isCloserDarkTheme()) { val sectionAccent = if (isCloserDarkTheme()) {
MaterialTheme.colorScheme.primaryContainer MaterialTheme.colorScheme.primaryContainer
} else { } else {
accent accent ?: MaterialTheme.colorScheme.secondaryContainer
} }
Card( Card(
modifier = modifier modifier = modifier
@ -480,7 +480,7 @@ fun SettingsScreen(
} }
} }
SettingsSection(title = "For the two of you", accent = Color(0xFFF7C8E4)) { SettingsSection(title = "For the two of you", accent = MaterialTheme.colorScheme.secondaryContainer) {
SettingsRow( SettingsRow(
icon = CloserGlyphs.Heart, icon = CloserGlyphs.Heart,
label = "Together", label = "Together",
@ -520,7 +520,7 @@ fun SettingsScreen(
) )
} }
SettingsSection(title = "Your rhythm", accent = Color(0xFFD9B8FF)) { SettingsSection(title = "Your rhythm", accent = MaterialTheme.colorScheme.primaryContainer) {
SettingsRow( SettingsRow(
icon = CloserGlyphs.Bell, icon = CloserGlyphs.Bell,
label = "Notifications", label = "Notifications",
@ -536,7 +536,7 @@ fun SettingsScreen(
) )
} }
SettingsSection(title = "Premium", accent = Color(0xFFE7A2D1)) { SettingsSection(title = "Premium", accent = MaterialTheme.colorScheme.tertiaryContainer) {
SettingsRow( SettingsRow(
icon = ImageVector.vectorResource(R.drawable.glyph_couple_premium), icon = ImageVector.vectorResource(R.drawable.glyph_couple_premium),
label = "Subscription", label = "Subscription",
@ -546,7 +546,7 @@ fun SettingsScreen(
) )
} }
SettingsSection(title = "Privacy and safety", accent = Color(0xFFD9B8FF)) { SettingsSection(title = "Privacy and safety", accent = MaterialTheme.colorScheme.primaryContainer) {
SettingsRow( SettingsRow(
icon = ImageVector.vectorResource(R.drawable.glyph_privacy_lock), icon = ImageVector.vectorResource(R.drawable.glyph_privacy_lock),
label = "Security", label = "Security",
@ -562,7 +562,7 @@ fun SettingsScreen(
) )
} }
SettingsSection(title = "Account", accent = Color(0xFFFFD9E8)) { SettingsSection(title = "Account", accent = MaterialTheme.colorScheme.errorContainer) {
SettingsRow( SettingsRow(
icon = ImageVector.vectorResource(R.drawable.glyph_delete_account), icon = ImageVector.vectorResource(R.drawable.glyph_delete_account),
label = "Delete account", label = "Delete account",
@ -679,14 +679,14 @@ private fun SettingsRow(
androidx.compose.foundation.layout.Box( androidx.compose.foundation.layout.Box(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(999.dp)) .clip(RoundedCornerShape(999.dp))
.background(Color(0xFF8F67C5)) .background(MaterialTheme.colorScheme.primary)
.padding(horizontal = 8.dp, vertical = 2.dp), .padding(horizontal = 8.dp, vertical = 2.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = if (badgeCount > 9) "9+" else badgeCount.toString(), text = if (badgeCount > 9) "9+" else badgeCount.toString(),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = Color.White color = MaterialTheme.colorScheme.onPrimary
) )
} }
} }

View File

@ -0,0 +1,126 @@
package app.closer.ui.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
enum class CloserExperienceTone {
Wheel,
HowWell,
ThisOrThat,
DesireSync,
Challenge,
Reveal,
Date,
Premium
}
@Immutable
data class CloserToneColors(
val accent: Color,
val onAccent: Color,
val accentContainer: Color,
val onAccentContainer: Color,
val muted: Color
)
@Immutable
data class CloserStatusColors(
val success: Color,
val successContainer: Color,
val warning: Color,
val warningContainer: Color,
val danger: Color,
val dangerContainer: Color,
val premium: Color,
val premiumContainer: Color
)
@Composable
fun closerToneColors(tone: CloserExperienceTone): CloserToneColors {
val scheme = MaterialTheme.colorScheme
val muted = scheme.onSurfaceVariant
fun colors(
accent: Color,
onAccent: Color,
accentContainer: Color,
onAccentContainer: Color
) = CloserToneColors(
accent = accent,
onAccent = onAccent,
accentContainer = accentContainer,
onAccentContainer = onAccentContainer,
muted = muted
)
return when (tone) {
CloserExperienceTone.Wheel,
CloserExperienceTone.HowWell,
CloserExperienceTone.Challenge,
CloserExperienceTone.Reveal -> colors(
accent = scheme.primary,
onAccent = scheme.onPrimary,
accentContainer = scheme.primaryContainer,
onAccentContainer = scheme.onPrimaryContainer
)
CloserExperienceTone.ThisOrThat,
CloserExperienceTone.DesireSync -> colors(
accent = scheme.secondary,
onAccent = scheme.onSecondary,
accentContainer = scheme.secondaryContainer,
onAccentContainer = scheme.onSecondaryContainer
)
CloserExperienceTone.Date -> colors(
accent = scheme.tertiary,
onAccent = scheme.onTertiary,
accentContainer = scheme.tertiaryContainer,
onAccentContainer = scheme.onTertiaryContainer
)
CloserExperienceTone.Premium -> if (isCloserDarkTheme()) {
colors(
accent = Color(0xFFFFD77A),
onAccent = Color(0xFF2F2100),
accentContainer = Color(0xFF4C3A06),
onAccentContainer = Color(0xFFFFE8A6)
)
} else {
colors(
accent = CloserPalette.Gold,
onAccent = Color.White,
accentContainer = Color(0xFFFFF2BF),
onAccentContainer = CloserPalette.Gold
)
}
}
}
@Composable
fun closerStatusColors(): CloserStatusColors =
if (isCloserDarkTheme()) {
CloserStatusColors(
success = Color(0xFF81C995),
successContainer = Color(0xFF223A2A),
warning = Color(0xFFF6C453),
warningContainer = Color(0xFF493811),
danger = Color(0xFFE58A82),
dangerContainer = MaterialTheme.colorScheme.errorContainer,
premium = Color(0xFFFFD77A),
premiumContainer = Color(0xFF4C3A06)
)
} else {
CloserStatusColors(
success = Color(0xFF2E7D32),
successContainer = Color(0xFFE8F5E9),
warning = Color(0xFFF57F17),
warningContainer = Color(0xFFFFF3D7),
danger = Color(0xFFC62828),
dangerContainer = MaterialTheme.colorScheme.errorContainer,
premium = CloserPalette.Gold,
premiumContainer = Color(0xFFFFF2BF)
)
}

View File

@ -87,9 +87,11 @@ import app.closer.ui.components.BrandMessageRotator
import android.content.Context import android.content.Context
import android.provider.Settings import android.provider.Settings
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.CloserTheme import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerToneColors
import app.closer.ui.theme.isCloserDarkTheme import app.closer.ui.theme.isCloserDarkTheme
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@ -933,7 +935,7 @@ private fun OptionCard(
) { ) {
Surface( Surface(
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
color = if (isSelected) Color.White.copy(alpha = 0.22f) else accent.copy(alpha = 0.16f), color = if (isSelected) onSelectedContainer.copy(alpha = 0.22f) else accent.copy(alpha = 0.16f),
modifier = Modifier.size(32.dp) modifier = Modifier.size(32.dp)
) { ) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
@ -1065,6 +1067,7 @@ private fun ThisOrThatReveal(
showNextBeat: Boolean = false showNextBeat: Boolean = false
) { ) {
val ratio = if (total > 0) matched.toFloat() / total else 0f val ratio = if (total > 0) matched.toFloat() / total else 0f
val thisOrThatTone = closerToneColors(CloserExperienceTone.ThisOrThat)
val headline = when { val headline = when {
ratio >= 0.8f -> "Two peas in a pod 🫛" ratio >= 0.8f -> "Two peas in a pod 🫛"
ratio >= 0.5f -> "Lots in common 💛" ratio >= 0.5f -> "Lots in common 💛"
@ -1115,9 +1118,12 @@ private fun ThisOrThatReveal(
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 56.dp), .heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep) colors = ButtonDefaults.buttonColors(
containerColor = thisOrThatTone.accent,
contentColor = thisOrThatTone.onAccent
)
) { ) {
Text("Play again", color = Color.White) Text("Play again")
} }
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
OutlinedButton( OutlinedButton(

View File

@ -40,6 +40,7 @@ import app.closer.domain.model.QuestionCategory
import app.closer.ui.components.CategoryGlyph import app.closer.ui.components.CategoryGlyph
import app.closer.ui.questions.displayCategoryName import app.closer.ui.questions.displayCategoryName
import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.components.CloserGlyphs import app.closer.ui.components.CloserGlyphs
@ -316,9 +317,8 @@ private fun CategoryPill(label: String) {
} }
} }
@Preview
@Composable @Composable
fun CategoryPickerScreenPreview() { private fun CategoryPickerScreenPreviewBody() {
CategoryPickerContent( CategoryPickerContent(
state = CategoryPickerUiState( state = CategoryPickerUiState(
isLoading = false, isLoading = false,
@ -340,3 +340,15 @@ fun CategoryPickerScreenPreview() {
onRetry = {} onRetry = {}
) )
} }
@Preview(name = "Wheel categories • light")
@Composable
fun CategoryPickerScreenPreviewLight() {
CloserTheme(darkTheme = false) { CategoryPickerScreenPreviewBody() }
}
@Preview(name = "Wheel categories • dark")
@Composable
fun CategoryPickerScreenPreviewDark() {
CloserTheme(darkTheme = true) { CategoryPickerScreenPreviewBody() }
}

View File

@ -10,7 +10,6 @@ import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -51,8 +50,6 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -60,7 +57,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import app.closer.R import app.closer.R
import app.closer.ui.components.BrandIllustration
import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.components.CloserGlyphs import app.closer.ui.components.CloserGlyphs
@ -389,13 +388,13 @@ private fun WheelSpinner(
.scale(if (isSpinning) 1f else idlePulse), .scale(if (isSpinning) 1f else idlePulse),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Image( BrandIllustration(
painter = painterResource(R.drawable.illustration_spin_wheel), res = R.drawable.illustration_spin_wheel,
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier modifier = Modifier
.size(236.dp) .size(236.dp)
.rotate(wheelAngle) .rotate(wheelAngle),
tile = false
) )
Canvas(modifier = Modifier.size(236.dp)) { Canvas(modifier = Modifier.size(236.dp)) {
@ -446,9 +445,8 @@ private fun WheelSpinner(
} }
} }
@Preview
@Composable @Composable
fun SpinWheelScreenPreview() { private fun SpinWheelScreenPreviewBody() {
SpinWheelContent( SpinWheelContent(
state = SpinWheelUiState(isLoading = false, categoryName = "Trust"), state = SpinWheelUiState(isLoading = false, categoryName = "Trust"),
onSpin = {}, onSpin = {},
@ -459,3 +457,15 @@ fun SpinWheelScreenPreview() {
onBack = {} onBack = {}
) )
} }
@Preview(name = "Wheel spin • light")
@Composable
fun SpinWheelScreenPreviewLight() {
CloserTheme(darkTheme = false) { SpinWheelScreenPreviewBody() }
}
@Preview(name = "Wheel spin • dark")
@Composable
fun SpinWheelScreenPreviewDark() {
CloserTheme(darkTheme = true) { SpinWheelScreenPreviewBody() }
}

View File

@ -55,9 +55,12 @@ import app.closer.data.remote.FirestoreWheelAnswerDataSource
import app.closer.data.remote.WheelRevealDoc import app.closer.data.remote.WheelRevealDoc
import app.closer.domain.usecase.GameSessionManager import app.closer.domain.usecase.GameSessionManager
import app.closer.ui.components.StatusGlyph import app.closer.ui.components.StatusGlyph
import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerCardColor import app.closer.ui.theme.closerCardColor
import app.closer.ui.theme.closerToneColors
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -266,6 +269,8 @@ private fun WheelWaitingContent(
onHome: () -> Unit, onHome: () -> Unit,
onAbandon: () -> Unit = {} onAbandon: () -> Unit = {}
) { ) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -279,8 +284,8 @@ private fun WheelWaitingContent(
StatusGlyph( StatusGlyph(
icon = CloserGlyphs.Check, icon = CloserGlyphs.Check,
tint = CloserPalette.PurpleDeep, tint = wheelTone.accent,
container = CloserPalette.PurpleMist, container = wheelTone.accentContainer,
size = 82.dp, size = 82.dp,
iconSize = 40.dp iconSize = 40.dp
) )
@ -330,6 +335,8 @@ private fun WheelRevealContent(
onOpenDaily: () -> Unit = {}, onOpenDaily: () -> Unit = {},
showNextBeat: Boolean = false showNextBeat: Boolean = false
) { ) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -346,8 +353,8 @@ private fun WheelRevealContent(
) { ) {
StatusGlyph( StatusGlyph(
icon = CloserGlyphs.Check, icon = CloserGlyphs.Check,
tint = CloserPalette.PurpleDeep, tint = wheelTone.accent,
container = CloserPalette.PurpleMist, container = wheelTone.accentContainer,
size = 72.dp, size = 72.dp,
iconSize = 34.dp iconSize = 34.dp
) )
@ -380,9 +387,12 @@ private fun WheelRevealContent(
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 56.dp), .heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep) colors = ButtonDefaults.buttonColors(
containerColor = wheelTone.accent,
contentColor = wheelTone.onAccent
)
) { ) {
Text("Spin again", color = Color.White) Text("Spin again")
} }
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
OutlinedButton( OutlinedButton(
@ -404,6 +414,8 @@ private fun WheelRevealContent(
@Composable @Composable
private fun WheelRevealRow(item: WheelRevealItem, partnerName: String) { private fun WheelRevealRow(item: WheelRevealItem, partnerName: String) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
@ -421,7 +433,7 @@ private fun WheelRevealRow(item: WheelRevealItem, partnerName: String) {
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
AnswerBlock(label = "You", text = item.myDisplay, accent = CloserPalette.PurpleDeep) AnswerBlock(label = "You", text = item.myDisplay, accent = wheelTone.accent)
AnswerBlock(label = partnerName, text = item.partnerDisplay, accent = CloserPalette.PinkAccentDeep) AnswerBlock(label = partnerName, text = item.partnerDisplay, accent = CloserPalette.PinkAccentDeep)
} }
} }
@ -467,6 +479,8 @@ private fun WheelErrorContent(
onRetry: () -> Unit, onRetry: () -> Unit,
onHome: () -> Unit onHome: () -> Unit
) { ) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -494,9 +508,12 @@ private fun WheelErrorContent(
onClick = onRetry, onClick = onRetry,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep) colors = ButtonDefaults.buttonColors(
containerColor = wheelTone.accent,
contentColor = wheelTone.onAccent
)
) { ) {
Text("Try again", color = Color.White) Text("Try again")
} }
OutlinedButton( OutlinedButton(
onClick = onHome, onClick = onHome,
@ -508,10 +525,9 @@ private fun WheelErrorContent(
} }
} }
@Preview
@Composable @Composable
fun WheelRevealPreview() { private fun WheelRevealPreviewBody() {
Box(Modifier.background(Color(0xFFFFFBFE))) { Box(Modifier.background(MaterialTheme.colorScheme.background)) {
WheelRevealContent( WheelRevealContent(
categoryName = "Trust", categoryName = "Trust",
partnerName = "Sam", partnerName = "Sam",
@ -532,3 +548,15 @@ fun WheelRevealPreview() {
) )
} }
} }
@Preview(name = "Wheel reveal • light")
@Composable
fun WheelRevealPreviewLight() {
CloserTheme(darkTheme = false) { WheelRevealPreviewBody() }
}
@Preview(name = "Wheel reveal • dark")
@Composable
fun WheelRevealPreviewDark() {
CloserTheme(darkTheme = true) { WheelRevealPreviewBody() }
}

View File

@ -51,6 +51,8 @@ import app.closer.ui.components.EmptyState
import app.closer.ui.components.ErrorState import app.closer.ui.components.ErrorState
import app.closer.ui.components.LoadingState import app.closer.ui.components.LoadingState
import app.closer.ui.questions.displayCategoryName import app.closer.ui.questions.displayCategoryName
import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.closerToneColors
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -183,6 +185,8 @@ fun GameHistoryScreen(
@Composable @Composable
private fun WheelSessionCard(session: QuestionSession, onClick: (() -> Unit)?) { private fun WheelSessionCard(session: QuestionSession, onClick: (() -> Unit)?) {
val replayable = onClick != null val replayable = onClick != null
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Card( Card(
onClick = onClick ?: {}, onClick = onClick ?: {},
enabled = replayable, enabled = replayable,
@ -214,7 +218,7 @@ private fun WheelSessionCard(session: QuestionSession, onClick: (() -> Unit)?) {
Text( Text(
text = SimpleDateFormat("d MMM", Locale.getDefault()).format(Date(ts)), text = SimpleDateFormat("d MMM", Locale.getDefault()).format(Date(ts)),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = Color(0xFFB98AF4) color = wheelTone.accent
) )
} }
} }
@ -234,6 +238,8 @@ private fun HistorySectionHeader(title: String) {
@Composable @Composable
private fun ChallengeHistoryCard(entry: CompletedChallengeEntry, onClick: (() -> Unit)? = null) { private fun ChallengeHistoryCard(entry: CompletedChallengeEntry, onClick: (() -> Unit)? = null) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Card( Card(
onClick = onClick ?: {}, onClick = onClick ?: {},
enabled = onClick != null, enabled = onClick != null,
@ -265,7 +271,7 @@ private fun ChallengeHistoryCard(entry: CompletedChallengeEntry, onClick: (() ->
Text( Text(
text = SimpleDateFormat("d MMM", java.util.Locale.getDefault()).format(Date(entry.startedAt)), text = SimpleDateFormat("d MMM", java.util.Locale.getDefault()).format(Date(entry.startedAt)),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = Color(0xFFB98AF4) color = wheelTone.accent
) )
} }
} }
@ -274,6 +280,8 @@ private fun ChallengeHistoryCard(entry: CompletedChallengeEntry, onClick: (() ->
@Composable @Composable
private fun CapsuleHistoryCard(capsule: TimeCapsule, onClick: (() -> Unit)? = null) { private fun CapsuleHistoryCard(capsule: TimeCapsule, onClick: (() -> Unit)? = null) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Card( Card(
onClick = onClick ?: {}, onClick = onClick ?: {},
enabled = onClick != null, enabled = onClick != null,
@ -307,7 +315,7 @@ private fun CapsuleHistoryCard(capsule: TimeCapsule, onClick: (() -> Unit)? = nu
Text( Text(
text = SimpleDateFormat("d MMM", java.util.Locale.getDefault()).format(Date(capsule.unlockAt)), text = SimpleDateFormat("d MMM", java.util.Locale.getDefault()).format(Date(capsule.unlockAt)),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = Color(0xFFB98AF4) color = wheelTone.accent
) )
} }
} }
@ -340,6 +348,8 @@ private fun sessionReplayRoute(session: QuestionSession): String? = when (sessio
@Composable @Composable
private fun GameHistoryLockedCard(onUnlock: () -> Unit) { private fun GameHistoryLockedCard(onUnlock: () -> Unit) {
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp), shape = RoundedCornerShape(24.dp),
@ -384,11 +394,11 @@ private fun GameHistoryLockedCard(onUnlock: () -> Unit) {
.heightIn(min = 52.dp), .heightIn(min = 52.dp),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4), containerColor = premiumTone.accent,
contentColor = MaterialTheme.colorScheme.onPrimary contentColor = premiumTone.onAccent
) )
) { ) {
Text("Unlock premium", color = MaterialTheme.colorScheme.onPrimary) Text("Unlock premium")
} }
} }
} }

View File

@ -1,8 +1,11 @@
package app.closer.ui.wheel package app.closer.ui.wheel
import app.closer.ui.theme.closerCardColor import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerCardColor
import app.closer.ui.theme.closerSoftPurpleColor import app.closer.ui.theme.closerSoftPurpleColor
import app.closer.ui.theme.closerToneColors
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -48,7 +51,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.LiveRegionMode import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
@ -104,6 +106,8 @@ private fun WheelSessionContent(
onRetry: () -> Unit, onRetry: () -> Unit,
onAbandon: () -> Unit onAbandon: () -> Unit
) { ) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -169,7 +173,7 @@ private fun WheelSessionContent(
Text( Text(
text = "${current + 1} / $total", text = "${current + 1} / $total",
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = Color(0xFF9B8AA6), color = wheelTone.muted,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
@ -262,11 +266,13 @@ private fun WheelSessionContent(
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 56.dp), .heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF56306F)) colors = ButtonDefaults.buttonColors(
containerColor = wheelTone.accent,
contentColor = wheelTone.onAccent
)
) { ) {
Text( Text(
text = if (state.showCompletionPrompt || current + 1 >= total) "Finish" else "Next question", text = if (state.showCompletionPrompt || current + 1 >= total) "Finish" else "Next question"
color = Color.White
) )
} }
Row( Row(
@ -288,7 +294,7 @@ private fun WheelSessionContent(
.weight(1f) .weight(1f)
.heightIn(min = 48.dp) .heightIn(min = 48.dp)
) { ) {
Text("Finish now", color = Color(0xFF9B8AA6)) Text("Finish now", color = wheelTone.muted)
} }
} }
// Escape hatch: leave without finishing (force-completes the shared session so it // Escape hatch: leave without finishing (force-completes the shared session so it
@ -299,7 +305,7 @@ private fun WheelSessionContent(
) { ) {
Text( Text(
"Quit game", "Quit game",
color = Color(0xFF9B8AA6).copy(alpha = 0.7f), color = wheelTone.muted.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium style = MaterialTheme.typography.labelMedium
) )
} }
@ -318,6 +324,8 @@ private fun AnswerOptions(
onScaleChanged: (Int) -> Unit, onScaleChanged: (Int) -> Unit,
onWrittenTextChanged: (String) -> Unit onWrittenTextChanged: (String) -> Unit
) { ) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
when (question.type) { when (question.type) {
"written" -> { "written" -> {
val config = question.answerConfig as? WrittenAnswerConfigImpl val config = question.answerConfig as? WrittenAnswerConfigImpl
@ -339,7 +347,7 @@ private fun AnswerOptions(
maxLines = 6, maxLines = 6,
shape = RoundedCornerShape(14.dp), shape = RoundedCornerShape(14.dp),
colors = OutlinedTextFieldDefaults.colors( colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF56306F), focusedBorderColor = wheelTone.accent,
unfocusedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f), unfocusedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f),
focusedContainerColor = MaterialTheme.colorScheme.surface, focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface unfocusedContainerColor = MaterialTheme.colorScheme.surface
@ -359,11 +367,11 @@ private fun AnswerOptions(
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
border = BorderStroke( border = BorderStroke(
width = if (isSelected) 2.dp else 1.dp, width = if (isSelected) 2.dp else 1.dp,
color = if (isSelected) Color(0xFF56306F) color = if (isSelected) wheelTone.accent
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f) else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f)
), ),
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
containerColor = if (isSelected) Color(0xFFF0EDF9) containerColor = if (isSelected) wheelTone.accentContainer
else MaterialTheme.colorScheme.surface, else MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface contentColor = MaterialTheme.colorScheme.onSurface
) )
@ -376,7 +384,7 @@ private fun AnswerOptions(
selected = isSelected, selected = isSelected,
onClick = null, onClick = null,
colors = RadioButtonDefaults.colors( colors = RadioButtonDefaults.colors(
selectedColor = Color(0xFF56306F) selectedColor = wheelTone.accent
) )
) )
Text( Text(
@ -403,11 +411,11 @@ private fun AnswerOptions(
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
border = BorderStroke( border = BorderStroke(
width = if (isChecked) 2.dp else 1.dp, width = if (isChecked) 2.dp else 1.dp,
color = if (isChecked) Color(0xFF56306F) color = if (isChecked) wheelTone.accent
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f) else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f)
), ),
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
containerColor = if (isChecked) Color(0xFFF0EDF9) containerColor = if (isChecked) wheelTone.accentContainer
else MaterialTheme.colorScheme.surface, else MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface contentColor = MaterialTheme.colorScheme.onSurface
) )
@ -420,7 +428,7 @@ private fun AnswerOptions(
checked = isChecked, checked = isChecked,
onCheckedChange = null, onCheckedChange = null,
colors = CheckboxDefaults.colors( colors = CheckboxDefaults.colors(
checkedColor = Color(0xFF56306F) checkedColor = wheelTone.accent
) )
) )
Text( Text(
@ -446,7 +454,7 @@ private fun AnswerOptions(
Text( Text(
text = "$selectedScaleValue", text = "$selectedScaleValue",
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold), style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),
color = Color(0xFF56306F) color = wheelTone.accent
) )
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@ -457,8 +465,8 @@ private fun AnswerOptions(
steps = if (cfg.maxScale > cfg.minScale) (cfg.maxScale - cfg.minScale - 1).coerceAtLeast(0) else 0, steps = if (cfg.maxScale > cfg.minScale) (cfg.maxScale - cfg.minScale - 1).coerceAtLeast(0) else 0,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = SliderDefaults.colors( colors = SliderDefaults.colors(
thumbColor = Color(0xFF56306F), thumbColor = wheelTone.accent,
activeTrackColor = Color(0xFF56306F), activeTrackColor = wheelTone.accent,
inactiveTrackColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f) inactiveTrackColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f)
) )
) )
@ -495,9 +503,9 @@ private fun AnswerOptions(
.height(96.dp), .height(96.dp),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = if (isSelected) Color(0xFF56306F) containerColor = if (isSelected) wheelTone.accent
else MaterialTheme.colorScheme.surfaceVariant, else MaterialTheme.colorScheme.surfaceVariant,
contentColor = if (isSelected) Color.White contentColor = if (isSelected) wheelTone.onAccent
else MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant
), ),
elevation = ButtonDefaults.buttonElevation( elevation = ButtonDefaults.buttonElevation(
@ -550,6 +558,8 @@ private fun EmptySessionCard() {
@Composable @Composable
private fun SubmitErrorCard(message: String, onRetry: () -> Unit) { private fun SubmitErrorCard(message: String, onRetry: () -> Unit) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp), shape = RoundedCornerShape(24.dp),
@ -574,17 +584,19 @@ private fun SubmitErrorCard(message: String, onRetry: () -> Unit) {
onClick = onRetry, onClick = onRetry,
modifier = Modifier.fillMaxWidth().heightIn(min = 52.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 52.dp),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF56306F)) colors = ButtonDefaults.buttonColors(
containerColor = wheelTone.accent,
contentColor = wheelTone.onAccent
)
) { ) {
Text("Retry", color = Color.White) Text("Retry")
} }
} }
} }
} }
@Preview
@Composable @Composable
fun WheelSessionScreenPreview() { private fun WheelSessionScreenPreviewBody() {
WheelSessionContent( WheelSessionContent(
state = WheelSessionUiState( state = WheelSessionUiState(
isLoading = false, isLoading = false,
@ -605,3 +617,15 @@ fun WheelSessionScreenPreview() {
onAbandon = {} onAbandon = {}
) )
} }
@Preview(name = "Wheel session • light")
@Composable
fun WheelSessionScreenPreviewLight() {
CloserTheme(darkTheme = false) { WheelSessionScreenPreviewBody() }
}
@Preview(name = "Wheel session • dark")
@Composable
fun WheelSessionScreenPreviewDark() {
CloserTheme(darkTheme = true) { WheelSessionScreenPreviewBody() }
}

View File

@ -9,7 +9,11 @@
# #
# Exclusions: # Exclusions:
# - Color.Transparent (intentional) # - Color.Transparent (intentional)
# - Theme.kt (the theme definition itself) # - ui/theme/* (the theme and token definitions themselves)
# - Google sign-in brand colors in AuthVisuals.kt
# - Debug-only ArtPreviewScreen.kt raw illustration previews
# - BrandIllustration.kt alpha masks
# - PackArtwork.kt, MessageBubbleOverlay.kt, and ChatComponents.kt media/scrim overlays
# - @Preview composables — a hardcoded color inside an `@Preview` function only affects # - @Preview composables — a hardcoded color inside an `@Preview` function only affects
# Android Studio's design-time preview pane, never the shipped app, so it is NOT a theme # Android Studio's design-time preview pane, never the shipped app, so it is NOT a theme
# defect. (Added 2026-06-28 after WheelCompleteScreen.kt `WheelRevealPreview` was filed as # defect. (Added 2026-06-28 after WheelCompleteScreen.kt `WheelRevealPreview` was filed as
@ -18,6 +22,7 @@
# #
# Usage: # Usage:
# ./scripts/theme-scan.sh > /tmp/claude-theme-scan-$(date +%Y%m%d).md # ./scripts/theme-scan.sh > /tmp/claude-theme-scan-$(date +%Y%m%d).md
# ./scripts/theme-scan.sh --strict /tmp/closer-theme-scan-strict.md
# ./scripts/theme-scan.sh --json # (future improvement — machine-readable) # ./scripts/theme-scan.sh --json # (future improvement — machine-readable)
# #
set -euo pipefail set -euo pipefail
@ -25,10 +30,21 @@ set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
UI_DIR="$PROJECT_ROOT/app/src/main/java/app/closer/ui" UI_DIR="$PROJECT_ROOT/app/src/main/java/app/closer/ui"
THEME_FILE="$PROJECT_ROOT/app/src/main/java/app/closer/ui/theme/Theme.kt" THEME_FILE="$PROJECT_ROOT/app/src/main/java/app/closer/ui/theme/Theme.kt"
SCAN_OUTPUT="${1:-}" STRICT=false
if [[ "${1:-}" == "--strict" ]]; then
STRICT=true
SCAN_OUTPUT="${2:-}"
else
SCAN_OUTPUT="${1:-}"
fi
if [[ -z "$SCAN_OUTPUT" ]]; then if [[ -z "$SCAN_OUTPUT" ]]; then
SCAN_OUTPUT="/tmp/claude-theme-scan-$(date +%Y%m%d).md" if [[ "$STRICT" == true ]]; then
SCAN_OUTPUT="/tmp/claude-theme-scan-strict-$(date +%Y%m%d).md"
else
SCAN_OUTPUT="/tmp/claude-theme-scan-$(date +%Y%m%d).md"
fi
fi fi
: > "$SCAN_OUTPUT" : > "$SCAN_OUTPUT"
@ -63,8 +79,35 @@ drop_previews() {
done done
} }
allowed_exception() {
local file="$1"
case "$file" in
"$UI_DIR"/theme/*) return 0 ;;
"$UI_DIR"/auth/AuthVisuals.kt) return 0 ;;
"$UI_DIR"/debug/ArtPreviewScreen.kt) return 0 ;;
"$UI_DIR"/components/BrandIllustration.kt) return 0 ;;
"$UI_DIR"/questions/PackArtwork.kt) return 0 ;;
"$UI_DIR"/components/MessageBubbleOverlay.kt) return 0 ;;
"$UI_DIR"/messages/components/ChatComponents.kt) return 0 ;;
esac
return 1
}
drop_allowed() {
local line file lineno
while IFS= read -r line; do
file=$(echo "$line" | cut -d: -f1)
lineno=$(echo "$line" | cut -d: -f2)
allowed_exception "$file" "$lineno" && continue
echo "$line"
done
}
log "# CloserApp Theme-Mismatch Scan — $(date)" log "# CloserApp Theme-Mismatch Scan — $(date)"
log "Project: $PROJECT_ROOT" log "Project: $PROJECT_ROOT"
if [[ "$STRICT" == true ]]; then
log "Mode: strict"
fi
log "" log ""
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ -82,11 +125,12 @@ scan_after_container() {
grep -rnE "$pattern" "$UI_DIR" --include="*.kt" > "$tmp_containers" || true grep -rnE "$pattern" "$UI_DIR" --include="*.kt" > "$tmp_containers" || true
while IFS= read -r container; do while IFS= read -r container; do
local file=$(echo "$container" | cut -d: -f1) local file=$(echo "$container" | cut -d: -f1)
local lineno=$(echo "$container" | cut -d: -f2) local lineno=$(echo "$container" | cut -d: -f2)
if [[ ! -f "$file" ]]; then continue; fi if [[ ! -f "$file" ]]; then continue; fi
if in_preview "$file" "$lineno"; then continue; fi if in_preview "$file" "$lineno"; then continue; fi
local segment=$(sed -n "${lineno},$((lineno+7))p" "$file") if allowed_exception "$file" "$lineno"; then continue; fi
local segment=$(sed -n "${lineno},$((lineno+7))p" "$file")
local match=$(echo "$segment" | grep -E '^\s*(color|containerColor|background)\s*=\s*Color(\.|\()' | grep -ivE "$exclude" | head -1 || true) local match=$(echo "$segment" | grep -E '^\s*(color|containerColor|background)\s*=\s*Color(\.|\()' | grep -ivE "$exclude" | head -1 || true)
if [[ -n "$match" ]]; then if [[ -n "$match" ]]; then
log "${label} ${file}:${lineno}" log "${label} ${file}:${lineno}"
@ -107,6 +151,7 @@ grep -rnE 'Modifier\.background\(Color(\.|\()' "$UI_DIR" --include="*.kt" \
| grep -ivE 'Color\.Transparent' \ | grep -ivE 'Color\.Transparent' \
| grep -ivE 'Theme\.kt' \ | grep -ivE 'Theme\.kt' \
| drop_previews \ | drop_previews \
| drop_allowed \
| while IFS= read -r line; do | while IFS= read -r line; do
log "🔴 CRITICAL $line" log "🔴 CRITICAL $line"
done || true done || true
@ -117,6 +162,7 @@ grep -rnE '(buttonColors|TextFieldDefaults\.colors|TabRowDefaults\.colors|Switch
| grep -iE 'Color(\.|\()' \ | grep -iE 'Color(\.|\()' \
| grep -ivE 'Color\.Transparent' \ | grep -ivE 'Color\.Transparent' \
| drop_previews \ | drop_previews \
| drop_allowed \
| while IFS= read -r line; do | while IFS= read -r line; do
log "🟠 MAJOR $line" log "🟠 MAJOR $line"
done || true done || true
@ -124,6 +170,7 @@ grep -rnE '(buttonColors|TextFieldDefaults\.colors|TabRowDefaults\.colors|Switch
grep -rnE '(Divider|HorizontalDivider)\s*\(' "$UI_DIR" --include="*.kt" \ grep -rnE '(Divider|HorizontalDivider)\s*\(' "$UI_DIR" --include="*.kt" \
| grep -iE 'color = Color(\.|\()' \ | grep -iE 'color = Color(\.|\()' \
| drop_previews \ | drop_previews \
| drop_allowed \
| while IFS= read -r line; do | while IFS= read -r line; do
log "🟠 MAJOR $line" log "🟠 MAJOR $line"
done || true done || true
@ -133,6 +180,7 @@ log "## Tier 1D — Text/Icon color hardcoded on themed surfaces (REVIEW)"
grep -rnE '^\s*color = Color\.(White|Black|(0x[0-9A-F]{8}))' "$UI_DIR" --include="*.kt" \ grep -rnE '^\s*color = Color\.(White|Black|(0x[0-9A-F]{8}))' "$UI_DIR" --include="*.kt" \
| grep -ivE 'Theme\.kt' \ | grep -ivE 'Theme\.kt' \
| drop_previews \ | drop_previews \
| drop_allowed \
| while IFS= read -r line; do | while IFS= read -r line; do
log "🟡 REVIEW $line" log "🟡 REVIEW $line"
done || true done || true
@ -141,6 +189,7 @@ log ""
log "## Tier 1E — Direct painterResource that bypasses BrandIllustration (MAJOR)" log "## Tier 1E — Direct painterResource that bypasses BrandIllustration (MAJOR)"
grep -rnE 'painterResource\(R\.drawable\.(illustration_|pack_art_)' "$UI_DIR" --include="*.kt" \ grep -rnE 'painterResource\(R\.drawable\.(illustration_|pack_art_)' "$UI_DIR" --include="*.kt" \
| drop_previews \ | drop_previews \
| drop_allowed \
| while IFS= read -r line; do | while IFS= read -r line; do
log "🟠 MAJOR $line" log "🟠 MAJOR $line"
done || true done || true
@ -150,6 +199,7 @@ log "## Tier 1F — Hardcoded border colors (REVIEW)"
grep -rnE 'Modifier\.border\([^)]*Color(\.|\()' "$UI_DIR" --include="*.kt" \ grep -rnE 'Modifier\.border\([^)]*Color(\.|\()' "$UI_DIR" --include="*.kt" \
| grep -ivE 'Color\.Transparent' \ | grep -ivE 'Color\.Transparent' \
| drop_previews \ | drop_previews \
| drop_allowed \
| while IFS= read -r line; do | while IFS= read -r line; do
log "🟡 REVIEW $line" log "🟡 REVIEW $line"
done || true done || true
@ -157,12 +207,38 @@ grep -rnE 'Modifier\.border\([^)]*Color(\.|\()' "$UI_DIR" --include="*.kt" \
log "" log ""
log "## Tier 1G — Hardcoded Brush/gradient stops (REVIEW)" log "## Tier 1G — Hardcoded Brush/gradient stops (REVIEW)"
grep -rnE 'Brush\.(linear|vertical|horizontal|radial)Gradient' "$UI_DIR" --include="*.kt" \ grep -rnE 'Brush\.(linear|vertical|horizontal|radial)Gradient' "$UI_DIR" --include="*.kt" \
| grep -vE 'BrandIllustration\.kt' \
| drop_previews \ | drop_previews \
| drop_allowed \
| while IFS= read -r line; do | while IFS= read -r line; do
log "🟡 REVIEW $line" file=$(echo "$line" | cut -d: -f1)
lineno=$(echo "$line" | cut -d: -f2)
segment=$(sed -n "${lineno},$((lineno+8))p" "$file")
if echo "$segment" | grep -qE 'Color(\.|\()|CloserPalette'; then
log "🟡 REVIEW $line"
fi
done || true done || true
if [[ "$STRICT" == true ]]; then
log ""
log "## Tier 1H — Strict raw colors outside theme/allowlist (MAJOR)"
grep -rnE 'Color\(0x[0-9A-Fa-f]{6,8}\)|Color\.(White|Black)' "$UI_DIR" --include="*.kt" \
| drop_previews \
| drop_allowed \
| while IFS= read -r line; do
log "🟠 MAJOR $line"
done || true
log ""
log "## Tier 2A — Strict shape/elevation drift (REVIEW)"
grep -rnE 'RoundedCornerShape\([0-9]+\.dp|CardDefaults\.cardElevation\([^)]*[0-9]+\.dp|shadow\([^)]*[0-9]+\.dp' "$UI_DIR" --include="*.kt" \
| grep -vE 'CloserPrimitives\.kt' \
| drop_previews \
| drop_allowed \
| while IFS= read -r line; do
log "🟡 REVIEW $line"
done || true
fi
log "" log ""
log "## Summary" log "## Summary"
critical=$(grep -c "🔴 CRITICAL" "$SCAN_OUTPUT" || true) critical=$(grep -c "🔴 CRITICAL" "$SCAN_OUTPUT" || true)