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.ui.Alignment
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.style.TextOverflow
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.ui.components.CategoryGlyph
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.components.CloserGlyphs
@ -94,6 +91,7 @@ private fun AnswerHistoryContent(
) {
var pendingDelete by remember { mutableStateOf<LocalAnswer?>(null) }
var selectedFilter by remember { mutableStateOf(AnswerHistoryFilter.ALL) }
val statusColors = closerStatusColors()
val visibleAnswers = remember(state.answers, selectedFilter) {
state.answers.filter { answer ->
when (selectedFilter) {
@ -118,8 +116,8 @@ private fun AnswerHistoryContent(
pendingDelete = null
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF8D2D35),
contentColor = Color.White
containerColor = statusColors.danger,
contentColor = MaterialTheme.colorScheme.onError
)
) {
Text("Remove")
@ -358,7 +356,7 @@ private fun AnswerHistoryCard(
) {
Text(
text = "Remove",
color = Color(0xFF8D2D35)
color = closerStatusColors().danger
)
}
}
@ -369,8 +367,9 @@ private fun AnswerHistoryCard(
@Composable
private fun HistoryStateBadge(isRevealed: Boolean) {
val tint = if (isRevealed) MaterialTheme.colorScheme.onPrimaryContainer else CloserPalette.Evergreen
val container = if (isRevealed) closerSoftPurpleColor() else CloserPalette.Evergreen.copy(alpha = 0.10f)
val statusColors = closerStatusColors()
val tint = if (isRevealed) MaterialTheme.colorScheme.onPrimaryContainer else statusColors.success
val container = if (isRevealed) closerSoftPurpleColor() else statusColors.successContainer
Surface(
shape = RoundedCornerShape(999.dp),
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.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.ui.res.painterResource
import app.closer.R
import androidx.compose.foundation.layout.heightIn
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.displayQuestionType
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
fun AnswerRevealScreen(
@ -265,6 +266,8 @@ private fun NoAnswerState(
onAnswerQuestion: () -> Unit,
onHome: () -> Unit
) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
RevealMessageCard {
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
RevealPill("No answer yet")
@ -291,8 +294,8 @@ private fun NoAnswerState(
.heightIn(min = 48.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = Color(0xFF24122F)
containerColor = revealTone.accent,
contentColor = revealTone.onAccent
)
) {
Text("Answer")
@ -319,6 +322,8 @@ private fun ReadyToRevealState(
onReveal: () -> Unit,
onHistory: () -> Unit
) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
RevealMessageCard {
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
RevealPill("Private answer saved")
@ -347,8 +352,8 @@ private fun ReadyToRevealState(
.heightIn(min = 48.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = Color(0xFF24122F)
containerColor = revealTone.accent,
contentColor = revealTone.onAccent
)
) {
Text("Reveal answer")
@ -432,15 +437,18 @@ private fun BothAnsweredSealedState(
onReveal: () -> Unit,
onHistory: () -> Unit
) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
RevealMessageCard {
Column(
verticalArrangement = Arrangement.spacedBy(14.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.illustration_reveal_celebration),
BrandIllustration(
res = R.drawable.illustration_reveal_celebration,
contentDescription = null,
modifier = Modifier.size(140.dp)
modifier = Modifier.size(140.dp),
tile = false
)
RevealPill("Both answers are in")
Text(
@ -464,8 +472,8 @@ private fun BothAnsweredSealedState(
modifier = Modifier.weight(1f).heightIn(min = 48.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = Color(0xFF24122F)
containerColor = revealTone.accent,
contentColor = revealTone.onAccent
)
) {
Text("Reveal answer")
@ -504,6 +512,8 @@ private fun WaitingForPartnerState(
onRefresh: () -> Unit,
onHome: () -> Unit
) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
RevealMessageCard {
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
RevealPill("Almost there")
@ -528,8 +538,8 @@ private fun WaitingForPartnerState(
modifier = Modifier.weight(1f).heightIn(min = 48.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = Color(0xFF24122F)
containerColor = revealTone.accent,
contentColor = revealTone.onAccent
)
) {
Text("Check again")
@ -595,6 +605,8 @@ private fun RevealedState(
loreSaved: Boolean = false,
wasSealed: Boolean = false
) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
val matchLine = if (partnerAnswer != null) {
val ownIds = answer.selectedOptionIds.toSet()
val partnerIds = partnerAnswer.selectedOptionIds.toSet()
@ -616,7 +628,7 @@ private fun RevealedState(
Text(
text = matchLine,
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF56306F),
color = revealTone.accent,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@ -639,8 +651,8 @@ private fun RevealedState(
.heightIn(min = 48.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = Color(0xFF24122F)
containerColor = revealTone.accent,
contentColor = revealTone.onAccent
)
) {
Text("Saved answers")
@ -663,7 +675,7 @@ private fun RevealedState(
Text(
"Save to Couple Lore",
style = MaterialTheme.typography.labelMedium,
color = Color(0xFF56306F).copy(alpha = 0.75f)
color = revealTone.accent.copy(alpha = 0.75f)
)
}
} else {
@ -684,6 +696,8 @@ private fun TinyActionCard(
mode: DailyModeResolver.DailyMode,
onDoThis: () -> Unit
) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(28.dp),
@ -711,8 +725,8 @@ private fun TinyActionCard(
.heightIn(min = 40.dp),
shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = Color(0xFF24122F)
containerColor = revealTone.accent,
contentColor = revealTone.onAccent
)
) {
Text("Do this tonight", style = MaterialTheme.typography.labelMedium)
@ -769,6 +783,8 @@ private fun FollowUpChip(
label: String,
onClick: () -> Unit
) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
Surface(
modifier = Modifier
.fillMaxWidth()
@ -796,7 +812,7 @@ private fun FollowUpChip(
Text(
text = "",
style = MaterialTheme.typography.titleMedium,
color = Color(0xFFB98AF4),
color = revealTone.accent,
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.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.CoupleRepository
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.closerStatusColors
import app.closer.ui.theme.closerToneColors
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Job
@ -403,6 +407,9 @@ private fun ChallengePickCard(
onPick: (ConnectionChallenge) -> Unit,
onPaywall: () -> Unit
) {
val challengeTone = closerToneColors(CloserExperienceTone.Challenge)
val statusColors = closerStatusColors()
Card(
onClick = { if (challenge.isPremium && !hasPremium) onPaywall() else onPick(challenge) },
modifier = Modifier.fillMaxWidth(),
@ -419,7 +426,7 @@ private fun ChallengePickCard(
) {
Surface(
shape = RoundedCornerShape(16.dp),
color = CloserPalette.PurpleDeep.copy(alpha = 0.10f),
color = challengeTone.accentContainer,
modifier = Modifier.size(52.dp)
) {
Box(contentAlignment = Alignment.Center) {
@ -451,27 +458,27 @@ private fun ChallengePickCard(
if (challenge.isPremium && !hasPremium) {
Surface(
shape = RoundedCornerShape(999.dp),
color = CloserPalette.Gold.copy(alpha = 0.15f)
color = statusColors.premiumContainer
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(3.dp)
) {
Icon(CloserGlyphs.Lock, contentDescription = null, tint = CloserPalette.Gold, modifier = Modifier.size(10.dp))
Text("Premium", style = MaterialTheme.typography.labelSmall, color = CloserPalette.Gold, fontWeight = FontWeight.SemiBold)
Icon(CloserGlyphs.Lock, contentDescription = null, tint = statusColors.premium, modifier = Modifier.size(10.dp))
Text("Premium", style = MaterialTheme.typography.labelSmall, color = statusColors.premium, fontWeight = FontWeight.SemiBold)
}
}
} else {
Surface(
shape = RoundedCornerShape(999.dp),
color = CloserPalette.PurpleDeep.copy(alpha = 0.10f)
color = challengeTone.accentContainer
) {
Text(
text = "${challenge.durationDays} days",
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
style = MaterialTheme.typography.labelSmall,
color = CloserPalette.PurpleDeep,
color = challengeTone.onAccentContainer,
fontWeight = FontWeight.SemiBold
)
}
@ -487,7 +494,7 @@ private fun ChallengePickCard(
Text(
text = challenge.category,
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,
onDismissAbandon: () -> Unit
) {
val challengeTone = closerToneColors(CloserExperienceTone.Challenge)
val statusColors = closerStatusColors()
if (showAbandonConfirm) {
AlertDialog(
onDismissRequest = onDismissAbandon,
@ -585,13 +595,13 @@ private fun ChallengesActiveScreen(
if (progress.jointStreak > 0 && !isComplete) {
Surface(
shape = RoundedCornerShape(999.dp),
color = CloserPalette.PurpleDeep.copy(alpha = 0.12f)
color = challengeTone.accentContainer
) {
Text(
text = "🔥 ${progress.jointStreak}",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium,
color = CloserPalette.PurpleDeep,
color = challengeTone.onAccentContainer,
fontWeight = FontWeight.SemiBold
)
}
@ -650,13 +660,13 @@ private fun ChallengesActiveScreen(
) {
Surface(
shape = RoundedCornerShape(999.dp),
color = CloserPalette.PurpleDeep.copy(alpha = 0.10f)
color = challengeTone.accentContainer
) {
Text(
text = "Day $currentDay",
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
color = CloserPalette.PurpleDeep,
color = challengeTone.onAccentContainer,
fontWeight = FontWeight.SemiBold
)
}
@ -687,8 +697,8 @@ private fun ChallengesActiveScreen(
Surface(
shape = RoundedCornerShape(16.dp),
color = when {
isBothDone -> CloserPalette.Evergreen.copy(alpha = 0.10f)
isWaiting -> CloserPalette.PurpleDeep.copy(alpha = 0.07f)
isBothDone -> statusColors.successContainer
isWaiting -> challengeTone.accentContainer.copy(alpha = 0.55f)
else -> MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)
}
) {
@ -704,14 +714,14 @@ private fun ChallengesActiveScreen(
else -> 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 {
Text(if (isWaiting) "" else "💬", style = MaterialTheme.typography.bodySmall)
}
Text(
text = stateCopy,
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
)
}
@ -729,14 +739,15 @@ private fun ChallengesActiveScreen(
.heightIn(min = 54.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(
containerColor = CloserPalette.PurpleDeep,
disabledContainerColor = CloserPalette.PurpleDeep.copy(alpha = 0.35f)
containerColor = challengeTone.accent,
contentColor = challengeTone.onAccent,
disabledContainerColor = challengeTone.accent.copy(alpha = 0.35f),
disabledContentColor = challengeTone.onAccent.copy(alpha = 0.65f)
)
) {
Text(
text = ctaLabel,
style = MaterialTheme.typography.labelLarge,
color = Color.White
style = MaterialTheme.typography.labelLarge
)
}
}
@ -781,13 +792,13 @@ private fun ChallengesActiveScreen(
.heightIn(min = 54.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(
containerColor = CloserPalette.PurpleDeep
containerColor = challengeTone.accent,
contentColor = challengeTone.onAccent
)
) {
Text(
text = "View game history",
style = MaterialTheme.typography.labelLarge,
color = Color.White
style = MaterialTheme.typography.labelLarge
)
}
}
@ -819,6 +830,8 @@ private fun DayTrackerStrip(
partnerCompletedDays: List<Int>,
currentDay: Int
) {
val challengeTone = closerToneColors(CloserExperienceTone.Challenge)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
@ -830,9 +843,9 @@ private fun DayTrackerStrip(
val isCurrent = day == currentDay.coerceAtMost(totalDays) && !myDone
val bg = when {
jointDone -> CloserPalette.PurpleDeep
myDone -> CloserPalette.PurpleDeep.copy(alpha = 0.45f)
isCurrent -> CloserPalette.PurpleDeep.copy(alpha = 0.15f)
jointDone -> challengeTone.accent
myDone -> challengeTone.accent.copy(alpha = 0.45f)
isCurrent -> challengeTone.accentContainer
else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
}
@ -845,12 +858,14 @@ private fun DayTrackerStrip(
contentAlignment = Alignment.Center
) {
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 {
Text(
text = "$day",
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
)
}
@ -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.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
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.GamePromptKind
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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
@ -179,6 +180,8 @@ private fun GamePromptCard(
onDismiss: () -> Unit
) {
val style = styleFor(prompt)
val promptTone = closerToneColors(CloserExperienceTone.Wheel)
val statusColors = closerStatusColors()
var dragUp by remember(prompt.kind, prompt.gameSessionId) { mutableStateOf(0f) }
Row(
modifier = Modifier
@ -189,10 +192,10 @@ private fun GamePromptCard(
.clip(RoundedCornerShape(20.dp))
.background(
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).
.clickable(onClick = onAction)
.pointerInput(prompt.gameSessionId) {
@ -214,8 +217,8 @@ private fun GamePromptCard(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.18f))
.border(1.5.dp, Color.White.copy(alpha = 0.5f), CircleShape),
.background(promptTone.onAccent.copy(alpha = 0.18f))
.border(1.5.dp, promptTone.onAccent.copy(alpha = 0.5f), CircleShape),
contentAlignment = Alignment.Center
) {
val avatar = prompt.avatarUrl
@ -230,7 +233,7 @@ private fun GamePromptCard(
Icon(
imageVector = CloserGlyphs.Play,
contentDescription = null,
tint = Color.White,
tint = promptTone.onAccent,
modifier = Modifier.size(22.dp)
)
}
@ -240,8 +243,8 @@ private fun GamePromptCard(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(Color(0xFF3DD68C)) // live "here now" green
.border(2.dp, CloserPalette.PurpleDeep, CircleShape)
.background(statusColors.success)
.border(2.dp, promptTone.accent, CircleShape)
)
}
}
@ -250,20 +253,20 @@ private fun GamePromptCard(
Text(
text = style.line1,
style = MaterialTheme.typography.labelMedium,
color = Color.White.copy(alpha = 0.85f)
color = promptTone.onAccent.copy(alpha = 0.85f)
)
Text(
text = style.line2,
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold),
color = Color.White
color = promptTone.onAccent
)
}
Button(
onClick = onAction,
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = CloserPalette.PurpleDeep
containerColor = promptTone.onAccent,
contentColor = promptTone.accent
),
shape = RoundedCornerShape(14.dp)
) {
@ -279,7 +282,7 @@ private fun GamePromptCard(
Icon(
imageVector = CloserGlyphs.Close,
contentDescription = "Dismiss",
tint = Color.White.copy(alpha = 0.8f),
tint = promptTone.onAccent.copy(alpha = 0.8f),
modifier = Modifier.size(20.dp)
)
}

View File

@ -122,6 +122,12 @@ private fun CloserMarkChipLoader(
size: Dp,
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 fill = if (reducedMotion) 1f else transition.animateFloat(
initialValue = 0f,
@ -160,7 +166,7 @@ private fun CloserMarkChipLoader(
// Aubergine gradient chip (matches ic_launcher_background palette).
drawRoundRect(
brush = Brush.linearGradient(
colors = listOf(Color(0xFF5A2F74), Color(0xFF3A1D4D), Color(0xFF24122F)),
colors = chipColors,
start = Offset(0f, 0f),
end = Offset(w, h)
),
@ -190,7 +196,7 @@ private fun CloserMarkChipLoader(
}
// Faint ring so the dark chip separates on dark backgrounds.
drawRoundRect(
color = Color.White.copy(alpha = 0.10f),
color = ringColor,
cornerRadius = corner,
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.unit.dp
import androidx.compose.ui.unit.sp
private val Purple = Color(0xFF56306F)
private val PurpleLight = Color(0xFFF0EDF9)
private val PinkPill = Color(0xFFB98AF4)
import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.closerToneColors
@Composable
fun SpecialDatesSection(
modifier: Modifier = Modifier,
compact: Boolean = false
) {
val dateTone = closerToneColors(CloserExperienceTone.Date)
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(if (compact) 24.dp else 28.dp),
@ -73,7 +73,7 @@ fun SpecialDatesSection(
horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.CenterVertically
) {
DateBlock(day = "14", month = "Jul", tint = Purple, compact = compact)
DateBlock(day = "14", month = "Jul", tint = dateTone.accent, compact = compact)
Column(
modifier = Modifier.weight(1f),
@ -102,7 +102,7 @@ fun SpecialDatesSection(
Icon(
imageVector = CloserGlyphs.CardGiftcard,
contentDescription = null,
tint = Purple,
tint = dateTone.accent,
modifier = Modifier.size(24.dp)
)
}
@ -127,12 +127,14 @@ fun SpecialDatesSection(
@Composable
private fun BirthdayRow(name: String, day: String, month: String) {
val dateTone = closerToneColors(CloserExperienceTone.Date)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.CenterVertically
) {
DateBlock(day = day, month = month, tint = Purple, compact = true)
DateBlock(day = day, month = month, tint = dateTone.accent, compact = true)
Text(
text = "$name's Birthday",
@ -144,7 +146,7 @@ private fun BirthdayRow(name: String, day: String, month: String) {
Icon(
imageVector = CloserGlyphs.Cake,
contentDescription = null,
tint = Purple.copy(alpha = 0.7f),
tint = dateTone.accent.copy(alpha = 0.7f),
modifier = Modifier.size(20.dp)
)
}
@ -184,15 +186,17 @@ private fun DateBlock(
@Composable
private fun TodayPill() {
val dateTone = closerToneColors(CloserExperienceTone.Date)
Surface(
shape = RoundedCornerShape(999.dp),
color = PinkPill
color = dateTone.accent
) {
Text(
text = "Today",
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
style = MaterialTheme.typography.labelSmall,
color = Color.White,
color = dateTone.onAccent,
fontWeight = FontWeight.SemiBold
)
}
@ -200,15 +204,17 @@ private fun TodayPill() {
@Composable
private fun DateCountPill() {
val dateTone = closerToneColors(CloserExperienceTone.Date)
Surface(
shape = RoundedCornerShape(999.dp),
color = PurpleLight
color = dateTone.accentContainer
) {
Text(
text = "3 saved",
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
style = MaterialTheme.typography.labelSmall,
color = Purple,
color = dateTone.onAccentContainer,
fontWeight = FontWeight.SemiBold
)
}

View File

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

View File

@ -69,6 +69,9 @@ import app.closer.ui.components.ErrorState
import app.closer.ui.components.LoadingState
import kotlin.math.roundToInt
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
fun DateMatchScreen(
@ -493,6 +496,9 @@ private fun ActionButtons(
onMaybe: () -> Unit,
onLove: () -> Unit
) {
val dateTone = closerToneColors(CloserExperienceTone.Date)
val statusColors = closerStatusColors()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
@ -501,23 +507,23 @@ private fun ActionButtons(
CircularActionButton(
icon = CloserGlyphs.Close,
contentDescription = "Skip",
color = MaterialTheme.colorScheme.errorContainer,
iconColor = Color(0xFF8D2D35),
color = statusColors.dangerContainer,
iconColor = statusColors.danger,
onClick = onSkip
)
CircularActionButton(
icon = CloserGlyphs.Star,
contentDescription = "Maybe",
color = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.74f),
iconColor = Color(0xFF6B5D00),
color = statusColors.warningContainer,
iconColor = statusColors.warning,
onClick = onMaybe,
size = 72.dp
)
CircularActionButton(
icon = CloserGlyphs.Heart,
contentDescription = "Love",
color = closerSoftPinkColor(),
iconColor = Color(0xFF9B1B5A),
color = dateTone.accentContainer,
iconColor = dateTone.accent,
onClick = onLove
)
}
@ -563,6 +569,8 @@ private fun MatchOverlay(
onDismiss: () -> Unit,
onViewMatches: () -> Unit
) {
val dateTone = closerToneColors(CloserExperienceTone.Date)
Box(
modifier = Modifier
.fillMaxSize()
@ -619,8 +627,8 @@ private fun MatchOverlay(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFF3E8FF),
contentColor = Color(0xFF56306F)
containerColor = dateTone.accentContainer,
contentColor = dateTone.onAccentContainer
)
) {
Text("Keep swiping")
@ -630,8 +638,8 @@ private fun MatchOverlay(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = MaterialTheme.colorScheme.onPrimary
containerColor = dateTone.accent,
contentColor = dateTone.onAccent
)
) {
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.LoadingState
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
fun DateMatchesScreen(
@ -87,6 +90,9 @@ private fun DateMatchesContent(
onMarkCompleted: (DateMatch) -> Unit = {},
onMemories: () -> Unit = {}
) {
val dateTone = closerToneColors(CloserExperienceTone.Date)
val statusColors = closerStatusColors()
Box(
modifier = Modifier
.fillMaxSize()
@ -171,7 +177,7 @@ private fun DateMatchesContent(
title = "Mutual love",
count = state.mutualMatches.size,
icon = CloserGlyphs.Heart,
iconColor = Color(0xFF9B1B5A),
iconColor = dateTone.accent,
modifier = Modifier.padding(top = 8.dp)
)
}
@ -186,7 +192,7 @@ private fun DateMatchesContent(
title = "Maybe together",
count = state.maybeMatches.size,
icon = CloserGlyphs.Star,
iconColor = Color(0xFF6B5D00),
iconColor = statusColors.warning,
modifier = Modifier.padding(top = 8.dp)
)
}
@ -248,6 +254,8 @@ private fun SectionHeader(
@Composable
private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) {
val idea = match.dateIdea ?: return
val dateTone = closerToneColors(CloserExperienceTone.Date)
IdeaCard(
idea = idea,
action = {
@ -256,7 +264,7 @@ private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) {
imageVector = CloserGlyphs.Heart,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = Color(0xFF9B1B5A)
tint = dateTone.accent
)
Spacer(Modifier.width(6.dp))
Text("We did this", fontWeight = FontWeight.SemiBold)
@ -276,12 +284,12 @@ private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) {
imageVector = CloserGlyphs.Heart,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = Color(0xFF9B1B5A)
tint = dateTone.accent
)
Text(
text = "Matched",
style = MaterialTheme.typography.labelSmall,
color = Color(0xFF9B1B5A),
color = dateTone.accent,
fontWeight = FontWeight.SemiBold
)
}
@ -292,6 +300,8 @@ private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) {
@Composable
private fun SuggestionCard(suggestion: DateMatchSuggestion) {
val statusColors = closerStatusColors()
IdeaCard(
idea = suggestion.dateIdea,
badge = {
@ -308,12 +318,12 @@ private fun SuggestionCard(suggestion: DateMatchSuggestion) {
imageVector = CloserGlyphs.Star,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = Color(0xFF6B5D00)
tint = statusColors.warning
)
Text(
text = if (suggestion.partnerAction == SwipeAction.LOVE) "They love it" else "Maybe match",
style = MaterialTheme.typography.labelSmall,
color = Color(0xFF6B5D00),
color = statusColors.warning,
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.CloserHeartLoader
import app.closer.ui.settings.SettingsSubpage
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerCardColor
import app.closer.ui.theme.closerStatusColors
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -216,10 +216,11 @@ private fun DateMemoryCard(row: DateMemoryRow, onClick: () -> Unit, onLongClick:
@Composable
private fun ReflectionChip(state: DateReflectionState) {
val statusColors = closerStatusColors()
val (label, color) = when (state) {
DateReflectionState.NONE -> "Reflect" to MaterialTheme.colorScheme.primary
DateReflectionState.AWAITING_PARTNER -> "Waiting" to CloserPalette.Gold
DateReflectionState.BOTH_DONE -> "View" to CloserPalette.Evergreen
DateReflectionState.AWAITING_PARTNER -> "Waiting" to statusColors.warning
DateReflectionState.BOTH_DONE -> "View" to statusColors.success
}
Surface(shape = RoundedCornerShape(50), color = color.copy(alpha = 0.14f)) {
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.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.SavedStateHandle
@ -69,8 +70,10 @@ import app.closer.ui.games.GameCopy
import dagger.hilt.android.qualifiers.ApplicationContext
import app.closer.ui.components.BrandMessageRotator
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.closerToneColors
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Job
@ -526,6 +529,8 @@ private fun DSSetupScreen(
onStart: () -> Unit,
onBack: () -> Unit
) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Column(
modifier = Modifier
.fillMaxSize()
@ -558,16 +563,16 @@ private fun DSSetupScreen(
onClick = { onLengthSelected(len) },
modifier = Modifier.weight(1f),
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)
BorderStroke(1.dp, CloserPalette.Romantic.copy(alpha = 0.4f))
BorderStroke(1.dp, desireTone.accent.copy(alpha = 0.4f))
else null
) {
Text(
text = len.label,
modifier = Modifier.padding(vertical = 10.dp),
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
)
}
@ -578,7 +583,10 @@ private fun DSSetupScreen(
onClick = onStart,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.Romantic)
colors = ButtonDefaults.buttonColors(
containerColor = desireTone.accent,
contentColor = desireTone.onAccent
)
) {
Text("Create session")
}
@ -587,6 +595,8 @@ private fun DSSetupScreen(
@Composable
private fun DSIntroScreen(total: Int, onReady: () -> Unit) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Column(
modifier = Modifier
.fillMaxSize()
@ -598,8 +608,8 @@ private fun DSIntroScreen(total: Int, onReady: () -> Unit) {
) {
StatusGlyph(
icon = CloserGlyphs.HeartOutline,
tint = CloserPalette.Romantic,
container = CloserPalette.Romantic.copy(alpha = 0.12f)
tint = desireTone.accent,
container = desireTone.accentContainer
)
Spacer(Modifier.height(20.dp))
Text(
@ -620,13 +630,18 @@ private fun DSIntroScreen(total: Int, onReady: () -> Unit) {
onClick = onReady,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.Romantic)
) { Text("I'm ready", color = Color.White) }
colors = ButtonDefaults.buttonColors(
containerColor = desireTone.accent,
contentColor = desireTone.onAccent
)
) { Text("I'm ready") }
}
}
@Composable
private fun DSWaitingScreen(partnerName: String, onBack: () -> Unit, onAbandon: () -> Unit = {}) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Column(
modifier = Modifier
.fillMaxSize()
@ -639,8 +654,8 @@ private fun DSWaitingScreen(partnerName: String, onBack: () -> Unit, onAbandon:
Spacer(Modifier.weight(1f))
StatusGlyph(
icon = CloserGlyphs.Heart,
tint = CloserPalette.Romantic,
container = CloserPalette.Romantic.copy(alpha = 0.12f)
tint = desireTone.accent,
container = desireTone.accentContainer
)
Text(
text = "Your answers are in!",
@ -708,6 +723,7 @@ private fun DSAnswerScreen(
) {
val config = question.answerConfig as? ChoiceAnswerConfigImpl
val options = config?.config?.options?.take(2) ?: return
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Column(
modifier = Modifier
@ -724,13 +740,13 @@ private fun DSAnswerScreen(
) {
Surface(
shape = RoundedCornerShape(999.dp),
color = CloserPalette.Romantic.copy(alpha = 0.12f)
color = desireTone.accentContainer
) {
Text(
text = "${index + 1} / $total",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium,
color = CloserPalette.Romantic,
color = desireTone.onAccentContainer,
fontWeight = FontWeight.SemiBold
)
}
@ -769,7 +785,7 @@ private fun DSAnswerScreen(
options.forEach { option ->
val isSelected = pendingSelection == option.id
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 {
isSelected -> selectedColor
pendingSelection != null -> MaterialTheme.colorScheme.surface.copy(alpha = 0.5f)
@ -791,7 +807,11 @@ private fun DSAnswerScreen(
Text(
text = option.text,
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
)
}
@ -811,6 +831,8 @@ private fun DSRevealScreen(
onOpenDaily: () -> Unit = {},
showNextBeat: Boolean = false
) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
LazyColumn(
modifier = Modifier
.fillMaxSize()
@ -827,8 +849,8 @@ private fun DSRevealScreen(
) {
StatusGlyph(
icon = if (matches.isEmpty()) CloserGlyphs.HeartOutline else CloserGlyphs.Heart,
tint = CloserPalette.Romantic,
container = CloserPalette.Romantic.copy(alpha = 0.12f),
tint = desireTone.accent,
container = desireTone.accentContainer,
size = 82.dp,
iconSize = 40.dp
)
@ -855,7 +877,7 @@ private fun DSRevealScreen(
Text(
text = "You both said yes to",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = CloserPalette.Romantic
color = desireTone.accent
)
}
@ -890,8 +912,11 @@ private fun DSRevealScreen(
onClick = onPlayAgain,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.Romantic)
) { Text("Play again", color = Color.White) }
colors = ButtonDefaults.buttonColors(
containerColor = desireTone.accent,
contentColor = desireTone.onAccent
)
) { Text("Play again") }
OutlinedButton(
onClick = onHome,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
@ -910,18 +935,20 @@ private fun DesireProgressPill(
progress: Float,
modifier: Modifier = Modifier
) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Box(
modifier = modifier
.height(8.dp)
.clip(RoundedCornerShape(999.dp))
.background(CloserPalette.Romantic.copy(alpha = 0.15f))
.background(desireTone.accentContainer)
) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(progress.coerceIn(0f, 1f))
.clip(RoundedCornerShape(999.dp))
.background(CloserPalette.Romantic)
.background(desireTone.accent)
)
}
}
@ -932,6 +959,8 @@ private fun DesireRevealMeter(
total: Int,
partnerName: String
) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(22.dp),
@ -960,8 +989,8 @@ private fun DesireRevealMeter(
)
StatusGlyph(
icon = CloserGlyphs.Sync,
tint = CloserPalette.Romantic,
container = CloserPalette.Romantic.copy(alpha = 0.12f),
tint = desireTone.accent,
container = desireTone.accentContainer,
size = 38.dp,
iconSize = 20.dp
)
@ -978,7 +1007,7 @@ private fun DesireRevealMeter(
Text(
text = "$matches shared, ${total - matches} kept private",
style = MaterialTheme.typography.labelMedium,
color = CloserPalette.Romantic,
color = desireTone.accent,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
@ -993,10 +1022,12 @@ private fun DesirePrivacyTile(
value: String,
modifier: Modifier = Modifier
) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Surface(
modifier = modifier,
shape = RoundedCornerShape(16.dp),
color = CloserPalette.Romantic.copy(alpha = 0.08f)
color = desireTone.accentContainer.copy(alpha = 0.55f)
) {
Column(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
@ -1013,7 +1044,7 @@ private fun DesirePrivacyTile(
Text(
text = value,
style = MaterialTheme.typography.labelMedium,
color = CloserPalette.Romantic,
color = desireTone.accent,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@ -1024,11 +1055,13 @@ private fun DesirePrivacyTile(
@Composable
private fun DesireMatchCard(match: DesireMatch) {
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(
containerColor = CloserPalette.Romantic.copy(alpha = 0.10f)
containerColor = desireTone.accentContainer.copy(alpha = 0.62f)
),
elevation = CardDefaults.cardElevation(0.dp)
) {
@ -1039,15 +1072,15 @@ private fun DesireMatchCard(match: DesireMatch) {
) {
StatusGlyph(
icon = CloserGlyphs.Heart,
tint = CloserPalette.Romantic,
container = CloserPalette.Romantic.copy(alpha = 0.12f),
tint = desireTone.accent,
container = desireTone.accentContainer,
size = 34.dp,
iconSize = 18.dp
)
Text(
text = match.question.text,
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
// (C-DS-001). onSurface adapts: dark text on light, light text on dark.
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 ────────────────────────────────────────────────────────────
sealed interface DSReplayPhase {

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
package app.closer.ui.home.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@ -15,12 +14,12 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import app.closer.R
import app.closer.ui.components.BrandIllustration
import app.closer.ui.components.CelebrationOverlay
import app.closer.ui.components.CloserActionButton
import app.closer.ui.settings.SettingsInk
@ -48,10 +47,11 @@ internal fun StreakMilestoneDialog(
modifier = Modifier.padding(horizontal = 28.dp, vertical = 30.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.illustration_streak_milestone),
BrandIllustration(
res = R.drawable.illustration_streak_milestone,
contentDescription = null,
modifier = Modifier.size(156.dp)
modifier = Modifier.size(156.dp),
tile = false
)
Spacer(Modifier.height(18.dp))
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.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.ResultGlyph
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.closerStatusColors
import app.closer.ui.theme.closerToneColors
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlin.math.abs
@ -585,6 +589,8 @@ private fun HWSetupScreen(
onStart: () -> Unit,
onBack: () -> Unit
) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Column(
modifier = Modifier
.fillMaxSize()
@ -617,16 +623,16 @@ private fun HWSetupScreen(
onClick = { onLengthSelected(len) },
modifier = Modifier.weight(1f),
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)
BorderStroke(1.dp, CloserPalette.PurpleDeep.copy(alpha = 0.4f))
BorderStroke(1.dp, howWellTone.accent.copy(alpha = 0.4f))
else null
) {
Text(
text = len.label,
modifier = Modifier.padding(vertical = 10.dp),
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
)
}
@ -637,7 +643,10 @@ private fun HWSetupScreen(
onClick = onStart,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
colors = ButtonDefaults.buttonColors(
containerColor = howWellTone.accent,
contentColor = howWellTone.onAccent
)
) {
Text("Create session")
}
@ -651,6 +660,8 @@ private fun PlayerIntroScreen(
total: Int,
onReady: () -> Unit
) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Column(
modifier = Modifier
.fillMaxSize()
@ -662,8 +673,8 @@ private fun PlayerIntroScreen(
) {
StatusGlyph(
icon = if (amSubject) CloserGlyphs.Person else CloserGlyphs.Psychology,
tint = CloserPalette.PurpleDeep,
container = CloserPalette.PurpleMist
tint = howWellTone.accent,
container = howWellTone.accentContainer
)
Spacer(Modifier.height(20.dp))
Text(
@ -691,13 +702,18 @@ private fun PlayerIntroScreen(
onClick = onReady,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
) { Text("I'm ready", color = Color.White) }
colors = ButtonDefaults.buttonColors(
containerColor = howWellTone.accent,
contentColor = howWellTone.onAccent
)
) { Text("I'm ready") }
}
}
@Composable
private fun HowWellWaitingScreen(amSubject: Boolean, partnerName: String, onBack: () -> Unit, onAbandon: () -> Unit = {}) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Column(
modifier = Modifier
.fillMaxSize()
@ -710,8 +726,8 @@ private fun HowWellWaitingScreen(amSubject: Boolean, partnerName: String, onBack
Spacer(Modifier.weight(1f))
StatusGlyph(
icon = CloserGlyphs.Sync,
tint = CloserPalette.PurpleDeep,
container = CloserPalette.PurpleMist
tint = howWellTone.accent,
container = howWellTone.accentContainer
)
Text(
text = "All done on your side!",
@ -786,6 +802,7 @@ private fun AnswerScreen(
onQuit: () -> Unit
) {
val hasSelection = selectedOptionId != null || selectedScale != null
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Column(
modifier = Modifier
@ -800,12 +817,12 @@ private fun AnswerScreen(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Surface(shape = RoundedCornerShape(999.dp), color = CloserPalette.PurpleMist) {
Surface(shape = RoundedCornerShape(999.dp), color = howWellTone.accentContainer) {
Text(
text = "${if (isGuesser) "Your guess" else "About you"} · ${index + 1} / $total",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium,
color = CloserPalette.PurpleDeep,
color = howWellTone.onAccentContainer,
fontWeight = FontWeight.SemiBold
)
}
@ -831,12 +848,12 @@ private fun AnswerScreen(
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
if (isGuesser) {
Surface(shape = RoundedCornerShape(999.dp), color = CloserPalette.PurpleMist) {
Surface(shape = RoundedCornerShape(999.dp), color = howWellTone.accentContainer) {
Text(
text = "How did $partnerName answer?",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 5.dp),
style = MaterialTheme.typography.labelSmall,
color = CloserPalette.PurpleDeep,
color = howWellTone.onAccentContainer,
fontWeight = FontWeight.SemiBold
)
}
@ -881,11 +898,13 @@ private fun AnswerScreen(
enabled = hasSelection,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
colors = ButtonDefaults.buttonColors(
containerColor = howWellTone.accent,
contentColor = howWellTone.onAccent
)
) {
Text(
text = if (index + 1 >= total) "Done →" else "Confirm →",
color = Color.White
text = if (index + 1 >= total) "Done →" else "Confirm →"
)
}
}
@ -904,6 +923,8 @@ private fun CompleteScreen(
onOpenDaily: () -> Unit = {},
showNextBeat: Boolean = false
) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
LazyColumn(
modifier = Modifier
.fillMaxSize()
@ -963,8 +984,11 @@ private fun CompleteScreen(
onClick = onSwapRoles,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
) { Text("Your turn — let $partnerName guess you", color = Color.White) }
colors = ButtonDefaults.buttonColors(
containerColor = howWellTone.accent,
contentColor = howWellTone.onAccent
)
) { Text("Your turn — let $partnerName guess you") }
OutlinedButton(
onClick = onPlayAgain,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
@ -975,8 +999,11 @@ private fun CompleteScreen(
onClick = onPlayAgain,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
) { Text("Play again", color = Color.White) }
colors = ButtonDefaults.buttonColors(
containerColor = howWellTone.accent,
contentColor = howWellTone.onAccent
)
) { Text("Play again") }
}
OutlinedButton(
onClick = onHome,
@ -996,18 +1023,20 @@ private fun HowWellProgressPill(
progress: Float,
modifier: Modifier = Modifier
) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Box(
modifier = modifier
.height(8.dp)
.clip(RoundedCornerShape(999.dp))
.background(CloserPalette.PurpleMist)
.background(howWellTone.accentContainer)
) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(progress.coerceIn(0f, 1f))
.clip(RoundedCornerShape(999.dp))
.background(CloserPalette.PurpleDeep)
.background(howWellTone.accent)
)
}
}
@ -1019,6 +1048,8 @@ private fun HowWellScoreRing(
modifier: Modifier = Modifier
) {
val progress = if (total == 0) 0f else score.toFloat() / total
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Box(
modifier = modifier.semantics(mergeDescendants = true) {
contentDescription = "You guessed $score of $total correctly"
@ -1028,14 +1059,14 @@ private fun HowWellScoreRing(
Canvas(modifier = Modifier.fillMaxSize()) {
val strokeWidth = 11.dp.toPx()
drawArc(
color = CloserPalette.PurpleMist,
color = howWellTone.accentContainer,
startAngle = -90f,
sweepAngle = 360f,
useCenter = false,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
drawArc(
color = CloserPalette.PurpleDeep,
color = howWellTone.accent,
startAngle = -90f,
sweepAngle = 360f * progress.coerceIn(0f, 1f),
useCenter = false,
@ -1046,7 +1077,7 @@ private fun HowWellScoreRing(
Text(
text = "$score",
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
color = CloserPalette.PurpleDeep
color = howWellTone.accent
)
Text(
text = "of $total",
@ -1060,14 +1091,12 @@ private fun HowWellScoreRing(
@Composable
private fun BreakdownRow(result: HowWellResult) {
// Match-row colors come as a container+content PAIR per theme: the old fixed pale-green
// container kept theme-driven onSurfaceVariant text, which is near-invisible in dark mode.
val dark = androidx.compose.foundation.isSystemInDarkTheme()
val matchColor = if (dark) Color(0xFF81C995) else Color(0xFF2E7D32)
val closeColor = if (dark) Color(0xFFF6C453) else Color(0xFFF57F17)
val missColor = if (dark) Color(0xFFE58A82) else Color(0xFFC62828)
val matchContainer = if (dark) Color(0xFF223A2A) else Color(0xFFE8F5E9)
val questionColor = if (result.isMatch && dark) Color(0xFFB9CBBE) else MaterialTheme.colorScheme.onSurfaceVariant
val statusColors = closerStatusColors()
val matchColor = statusColors.success
val closeColor = statusColors.warning
val missColor = statusColors.danger
val matchContainer = statusColors.successContainer
val questionColor = if (result.isMatch) statusColors.success else MaterialTheme.colorScheme.onSurfaceVariant
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp),
@ -1136,6 +1165,8 @@ private fun SingleChoiceInput(
selectedId: String?,
onSelect: (String) -> Unit
) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
options.forEach { option ->
val selected = selectedId == option.id
@ -1144,7 +1175,7 @@ private fun SingleChoiceInput(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp),
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)
) {
@ -1152,7 +1183,7 @@ private fun SingleChoiceInput(
text = option.text,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
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,
overflow = TextOverflow.Ellipsis
)
@ -1168,6 +1199,8 @@ private fun BinaryChoiceInput(
selectedId: String?,
onSelect: (String) -> Unit
) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
listOf(optionA to "A", optionB to "B").forEach { (option, label) ->
val selected = selectedId == option.id
@ -1176,7 +1209,7 @@ private fun BinaryChoiceInput(
modifier = Modifier.weight(1f).heightIn(min = 80.dp),
shape = RoundedCornerShape(14.dp),
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)
) {
@ -1189,12 +1222,12 @@ private fun BinaryChoiceInput(
text = label,
style = MaterialTheme.typography.labelSmall,
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 = option.text,
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,
maxLines = 2,
overflow = TextOverflow.Ellipsis
@ -1214,6 +1247,8 @@ private fun ScaleInput(
selected: Int?,
onSelect: (Int) -> Unit
) {
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
@ -1224,7 +1259,7 @@ private fun ScaleInput(
Surface(
onClick = { onSelect(value) },
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),
shadowElevation = if (isSelected) 6.dp else 2.dp
) {
@ -1232,7 +1267,7 @@ private fun ScaleInput(
Text(
text = "$value",
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"
}
@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 ────────────────────────────────────────────────────────────
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.repository.AuthRepository
import app.closer.domain.repository.CoupleRepository
import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerToneColors
import dagger.hilt.android.lifecycle.HiltViewModel
import java.text.SimpleDateFormat
import java.util.Date
@ -445,6 +447,8 @@ fun MemoryLaneScreen(
@Composable
private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) {
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
Column(
modifier = Modifier
.fillMaxSize()
@ -454,12 +458,12 @@ private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) {
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Surface(shape = RoundedCornerShape(50.dp), color = CloserPalette.Romantic.copy(alpha = 0.12f)) {
Surface(shape = RoundedCornerShape(50.dp), color = premiumTone.accentContainer) {
Icon(
CloserGlyphs.Lock,
contentDescription = null,
modifier = Modifier.padding(20.dp).size(32.dp),
tint = CloserPalette.Romantic
tint = premiumTone.accent
)
}
Spacer(Modifier.height(20.dp))
@ -467,8 +471,16 @@ private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) {
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)
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)) {
Text("Unlock premium", color = Color.White, style = MaterialTheme.typography.labelLarge)
Button(
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") }
}
@ -478,6 +490,8 @@ private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) {
@Composable
private fun MemoryLaneErrorScreen(message: String, onBack: () -> Unit, onRetry: () -> Unit) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
Column(
modifier = Modifier
.fillMaxSize()
@ -491,8 +505,15 @@ private fun MemoryLaneErrorScreen(message: String, onBack: () -> Unit, onRetry:
Spacer(Modifier.height(16.dp))
Text(message, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
Spacer(Modifier.height(24.dp))
Button(onClick = onRetry, shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)) {
Text("Try again", color = Color.White)
Button(
onClick = onRetry,
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = revealTone.accent,
contentColor = revealTone.onAccent
)
) {
Text("Try again")
}
TextButton(onClick = onBack) { Text("Go back") }
}
@ -507,6 +528,8 @@ private fun CapsuleListScreen(
onNew: () -> Unit,
onOpen: (TimeCapsule) -> Unit
) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
LazyColumn(
modifier = Modifier
.fillMaxSize()
@ -529,8 +552,8 @@ private fun CapsuleListScreen(
Text("Notes that open on a future date.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
IconButton(onClick = onNew) {
Surface(shape = RoundedCornerShape(12.dp), color = CloserPalette.PurpleDeep) {
Icon(CloserGlyphs.Add, contentDescription = "New capsule", tint = Color.White, modifier = Modifier.padding(6.dp).size(20.dp))
Surface(shape = RoundedCornerShape(12.dp), color = revealTone.accent) {
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("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))
Button(onClick = onNew, shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)) {
Text("Write your first capsule", color = Color.White)
Button(
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,
onSave: () -> Unit
) {
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
if (state.showDatePicker) {
val pickerState = rememberDatePickerState(
initialSelectedDateMillis = state.customUnlockMs ?: (System.currentTimeMillis() + UnlockPreset.ONE_YEAR.ms)
@ -774,10 +806,13 @@ private fun CapsuleCreateScreen(
enabled = !state.isSaving,
modifier = Modifier.fillMaxWidth().heightIn(min = 54.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)
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))

View File

@ -56,6 +56,7 @@ import app.closer.ui.auth.AuthPrimaryDeep
import app.closer.ui.components.BrandMessageRotator
import app.closer.ui.components.CloserHeartLoader
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerStatusColors
import kotlinx.coroutines.launch
private const val CTA_PAGE = 3
@ -388,6 +389,8 @@ private fun RevealCard(label: String, preview: String, revealed: Boolean, color:
@Composable
private fun GrowerVisual() {
val statusColors = closerStatusColors()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
@ -400,7 +403,7 @@ private fun GrowerVisual() {
GrowthPill("Desire Sync", CloserPalette.PinkMist, CloserPalette.Romantic)
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.SettingsSoft
import app.closer.ui.components.CloserGlyphs
import app.closer.ui.theme.closerStatusColors
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -206,9 +207,10 @@ private fun DeltaCard(baseline: Outcome, latest: Outcome) {
@Composable
private fun DeltaRow(label: String, delta: Int) {
val statusColors = closerStatusColors()
val color = when {
delta > 0 -> SettingsPrimaryDeep
delta < 0 -> Color(0xFFB00020)
delta < 0 -> statusColors.danger
else -> SettingsMuted
}
val sign = when {

View File

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

View File

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

View File

@ -1,6 +1,5 @@
package app.closer.ui.play
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -25,8 +24,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@ -37,6 +34,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import app.closer.R
import app.closer.core.navigation.AppRoute
import app.closer.ui.components.BrandIllustration
import app.closer.ui.components.CategoryGlyph
import app.closer.ui.components.CloserActionButton
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.UnpairedLockedCard
import app.closer.ui.components.CloserRadii
import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerPlayCardBrush
import app.closer.ui.components.CloserGlyphs
@ -802,18 +801,29 @@ private fun WheelGlyph(
contentAlignment = Alignment.Center,
modifier = Modifier.padding(5.dp)
) {
Image(
painter = painterResource(R.drawable.illustration_spin_wheel),
BrandIllustration(
res = R.drawable.illustration_spin_wheel,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize(),
tile = false
)
}
}
}
@Preview
@Composable
fun PlayHubScreenPreview() {
private fun PlayHubScreenPreviewBody() {
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,
colors = ButtonDefaults.buttonColors(
containerColor = SettingsDanger,
contentColor = Color.White
contentColor = MaterialTheme.colorScheme.onError
)
) {
Text("Delete", color = Color.White)
Text("Delete")
}
},
dismissButton = {
@ -249,13 +249,13 @@ fun DeleteAccountScreen(
enabled = !state.isReauthing,
colors = ButtonDefaults.buttonColors(
containerColor = SettingsDanger,
contentColor = Color.White
contentColor = MaterialTheme.colorScheme.onError
)
) {
if (state.isReauthing) {
CloserHeartLoader(size = 18.dp)
} else {
Text("Confirm", color = Color.White)
Text("Confirm")
}
}
}
@ -332,16 +332,16 @@ fun DeleteAccountScreen(
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
containerColor = SettingsDanger,
contentColor = Color.White
contentColor = MaterialTheme.colorScheme.onError
)
) {
if (state.isDeleting) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
CloserHeartLoader(size = 20.dp)
Text("Deleting…", color = Color.White)
Text("Deleting…")
}
} 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,
colors = ButtonDefaults.buttonColors(
containerColor = SettingsDanger,
contentColor = Color.White
contentColor = MaterialTheme.colorScheme.onError
)
) {
Text("Leave", color = Color.White)
Text("Leave")
}
},
dismissButton = {
@ -180,16 +180,16 @@ fun RelationshipSettingsScreen(
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
containerColor = SettingsDanger,
contentColor = Color.White
contentColor = MaterialTheme.colorScheme.onError
)
) {
if (state.isLeaving) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
CloserHeartLoader(size = 20.dp)
Text("Leaving…", color = Color.White)
Text("Leaving…")
}
} 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(
title: String? = null,
modifier: Modifier = Modifier,
accent: Color = Color(0xFFF7C8E4),
accent: Color? = null,
content: @Composable () -> Unit
) {
val sectionAccent = if (isCloserDarkTheme()) {
MaterialTheme.colorScheme.primaryContainer
} else {
accent
accent ?: MaterialTheme.colorScheme.secondaryContainer
}
Card(
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(
icon = CloserGlyphs.Heart,
label = "Together",
@ -520,7 +520,7 @@ fun SettingsScreen(
)
}
SettingsSection(title = "Your rhythm", accent = Color(0xFFD9B8FF)) {
SettingsSection(title = "Your rhythm", accent = MaterialTheme.colorScheme.primaryContainer) {
SettingsRow(
icon = CloserGlyphs.Bell,
label = "Notifications",
@ -536,7 +536,7 @@ fun SettingsScreen(
)
}
SettingsSection(title = "Premium", accent = Color(0xFFE7A2D1)) {
SettingsSection(title = "Premium", accent = MaterialTheme.colorScheme.tertiaryContainer) {
SettingsRow(
icon = ImageVector.vectorResource(R.drawable.glyph_couple_premium),
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(
icon = ImageVector.vectorResource(R.drawable.glyph_privacy_lock),
label = "Security",
@ -562,7 +562,7 @@ fun SettingsScreen(
)
}
SettingsSection(title = "Account", accent = Color(0xFFFFD9E8)) {
SettingsSection(title = "Account", accent = MaterialTheme.colorScheme.errorContainer) {
SettingsRow(
icon = ImageVector.vectorResource(R.drawable.glyph_delete_account),
label = "Delete account",
@ -679,14 +679,14 @@ private fun SettingsRow(
androidx.compose.foundation.layout.Box(
modifier = Modifier
.clip(RoundedCornerShape(999.dp))
.background(Color(0xFF8F67C5))
.background(MaterialTheme.colorScheme.primary)
.padding(horizontal = 8.dp, vertical = 2.dp),
contentAlignment = Alignment.Center
) {
Text(
text = if (badgeCount > 9) "9+" else badgeCount.toString(),
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.provider.Settings
import dagger.hilt.android.qualifiers.ApplicationContext
import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerToneColors
import app.closer.ui.theme.isCloserDarkTheme
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@ -933,7 +935,7 @@ private fun OptionCard(
) {
Surface(
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)
) {
Box(contentAlignment = Alignment.Center) {
@ -1065,6 +1067,7 @@ private fun ThisOrThatReveal(
showNextBeat: Boolean = false
) {
val ratio = if (total > 0) matched.toFloat() / total else 0f
val thisOrThatTone = closerToneColors(CloserExperienceTone.ThisOrThat)
val headline = when {
ratio >= 0.8f -> "Two peas in a pod 🫛"
ratio >= 0.5f -> "Lots in common 💛"
@ -1115,9 +1118,12 @@ private fun ThisOrThatReveal(
.fillMaxWidth()
.heightIn(min = 56.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))
OutlinedButton(

View File

@ -40,6 +40,7 @@ import app.closer.domain.model.QuestionCategory
import app.closer.ui.components.CategoryGlyph
import app.closer.ui.questions.displayCategoryName
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.components.CloserGlyphs
@ -316,9 +317,8 @@ private fun CategoryPill(label: String) {
}
}
@Preview
@Composable
fun CategoryPickerScreenPreview() {
private fun CategoryPickerScreenPreviewBody() {
CategoryPickerContent(
state = CategoryPickerUiState(
isLoading = false,
@ -340,3 +340,15 @@ fun CategoryPickerScreenPreview() {
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.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.Path
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.style.TextAlign
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.hilt.navigation.compose.hiltViewModel
import app.closer.R
import app.closer.ui.components.BrandIllustration
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.components.CloserGlyphs
@ -389,13 +388,13 @@ private fun WheelSpinner(
.scale(if (isSpinning) 1f else idlePulse),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.illustration_spin_wheel),
BrandIllustration(
res = R.drawable.illustration_spin_wheel,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.size(236.dp)
.rotate(wheelAngle)
.rotate(wheelAngle),
tile = false
)
Canvas(modifier = Modifier.size(236.dp)) {
@ -446,9 +445,8 @@ private fun WheelSpinner(
}
}
@Preview
@Composable
fun SpinWheelScreenPreview() {
private fun SpinWheelScreenPreviewBody() {
SpinWheelContent(
state = SpinWheelUiState(isLoading = false, categoryName = "Trust"),
onSpin = {},
@ -459,3 +457,15 @@ fun SpinWheelScreenPreview() {
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.domain.usecase.GameSessionManager
import app.closer.ui.components.StatusGlyph
import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.CloserTheme
import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerCardColor
import app.closer.ui.theme.closerToneColors
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
@ -266,6 +269,8 @@ private fun WheelWaitingContent(
onHome: () -> Unit,
onAbandon: () -> Unit = {}
) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Column(
modifier = Modifier
.fillMaxSize()
@ -279,8 +284,8 @@ private fun WheelWaitingContent(
StatusGlyph(
icon = CloserGlyphs.Check,
tint = CloserPalette.PurpleDeep,
container = CloserPalette.PurpleMist,
tint = wheelTone.accent,
container = wheelTone.accentContainer,
size = 82.dp,
iconSize = 40.dp
)
@ -330,6 +335,8 @@ private fun WheelRevealContent(
onOpenDaily: () -> Unit = {},
showNextBeat: Boolean = false
) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
LazyColumn(
modifier = Modifier
.fillMaxSize()
@ -346,8 +353,8 @@ private fun WheelRevealContent(
) {
StatusGlyph(
icon = CloserGlyphs.Check,
tint = CloserPalette.PurpleDeep,
container = CloserPalette.PurpleMist,
tint = wheelTone.accent,
container = wheelTone.accentContainer,
size = 72.dp,
iconSize = 34.dp
)
@ -380,9 +387,12 @@ private fun WheelRevealContent(
.fillMaxWidth()
.heightIn(min = 56.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))
OutlinedButton(
@ -404,6 +414,8 @@ private fun WheelRevealContent(
@Composable
private fun WheelRevealRow(item: WheelRevealItem, partnerName: String) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(18.dp),
@ -421,7 +433,7 @@ private fun WheelRevealRow(item: WheelRevealItem, partnerName: String) {
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold),
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)
}
}
@ -467,6 +479,8 @@ private fun WheelErrorContent(
onRetry: () -> Unit,
onHome: () -> Unit
) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Column(
modifier = Modifier
.fillMaxSize()
@ -494,9 +508,12 @@ private fun WheelErrorContent(
onClick = onRetry,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.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(
onClick = onHome,
@ -508,10 +525,9 @@ private fun WheelErrorContent(
}
}
@Preview
@Composable
fun WheelRevealPreview() {
Box(Modifier.background(Color(0xFFFFFBFE))) {
private fun WheelRevealPreviewBody() {
Box(Modifier.background(MaterialTheme.colorScheme.background)) {
WheelRevealContent(
categoryName = "Trust",
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.LoadingState
import app.closer.ui.questions.displayCategoryName
import app.closer.ui.theme.CloserExperienceTone
import app.closer.ui.theme.closerToneColors
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@ -183,6 +185,8 @@ fun GameHistoryScreen(
@Composable
private fun WheelSessionCard(session: QuestionSession, onClick: (() -> Unit)?) {
val replayable = onClick != null
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Card(
onClick = onClick ?: {},
enabled = replayable,
@ -214,7 +218,7 @@ private fun WheelSessionCard(session: QuestionSession, onClick: (() -> Unit)?) {
Text(
text = SimpleDateFormat("d MMM", Locale.getDefault()).format(Date(ts)),
style = MaterialTheme.typography.labelMedium,
color = Color(0xFFB98AF4)
color = wheelTone.accent
)
}
}
@ -234,6 +238,8 @@ private fun HistorySectionHeader(title: String) {
@Composable
private fun ChallengeHistoryCard(entry: CompletedChallengeEntry, onClick: (() -> Unit)? = null) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Card(
onClick = onClick ?: {},
enabled = onClick != null,
@ -265,7 +271,7 @@ private fun ChallengeHistoryCard(entry: CompletedChallengeEntry, onClick: (() ->
Text(
text = SimpleDateFormat("d MMM", java.util.Locale.getDefault()).format(Date(entry.startedAt)),
style = MaterialTheme.typography.labelMedium,
color = Color(0xFFB98AF4)
color = wheelTone.accent
)
}
}
@ -274,6 +280,8 @@ private fun ChallengeHistoryCard(entry: CompletedChallengeEntry, onClick: (() ->
@Composable
private fun CapsuleHistoryCard(capsule: TimeCapsule, onClick: (() -> Unit)? = null) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Card(
onClick = onClick ?: {},
enabled = onClick != null,
@ -307,7 +315,7 @@ private fun CapsuleHistoryCard(capsule: TimeCapsule, onClick: (() -> Unit)? = nu
Text(
text = SimpleDateFormat("d MMM", java.util.Locale.getDefault()).format(Date(capsule.unlockAt)),
style = MaterialTheme.typography.labelMedium,
color = Color(0xFFB98AF4)
color = wheelTone.accent
)
}
}
@ -340,6 +348,8 @@ private fun sessionReplayRoute(session: QuestionSession): String? = when (sessio
@Composable
private fun GameHistoryLockedCard(onUnlock: () -> Unit) {
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
@ -384,11 +394,11 @@ private fun GameHistoryLockedCard(onUnlock: () -> Unit) {
.heightIn(min = 52.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = MaterialTheme.colorScheme.onPrimary
containerColor = premiumTone.accent,
contentColor = premiumTone.onAccent
)
) {
Text("Unlock premium", color = MaterialTheme.colorScheme.onPrimary)
Text("Unlock premium")
}
}
}

View File

@ -1,8 +1,11 @@
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.closerCardColor
import app.closer.ui.theme.closerSoftPurpleColor
import app.closer.ui.theme.closerToneColors
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@ -48,7 +51,6 @@ import androidx.compose.ui.Alignment
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.semantics.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
@ -104,6 +106,8 @@ private fun WheelSessionContent(
onRetry: () -> Unit,
onAbandon: () -> Unit
) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Box(
modifier = Modifier
.fillMaxSize()
@ -169,7 +173,7 @@ private fun WheelSessionContent(
Text(
text = "${current + 1} / $total",
style = MaterialTheme.typography.labelLarge,
color = Color(0xFF9B8AA6),
color = wheelTone.muted,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@ -262,11 +266,13 @@ private fun WheelSessionContent(
.fillMaxWidth()
.heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF56306F))
colors = ButtonDefaults.buttonColors(
containerColor = wheelTone.accent,
contentColor = wheelTone.onAccent
)
) {
Text(
text = if (state.showCompletionPrompt || current + 1 >= total) "Finish" else "Next question",
color = Color.White
text = if (state.showCompletionPrompt || current + 1 >= total) "Finish" else "Next question"
)
}
Row(
@ -288,7 +294,7 @@ private fun WheelSessionContent(
.weight(1f)
.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
@ -299,7 +305,7 @@ private fun WheelSessionContent(
) {
Text(
"Quit game",
color = Color(0xFF9B8AA6).copy(alpha = 0.7f),
color = wheelTone.muted.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium
)
}
@ -318,6 +324,8 @@ private fun AnswerOptions(
onScaleChanged: (Int) -> Unit,
onWrittenTextChanged: (String) -> Unit
) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
when (question.type) {
"written" -> {
val config = question.answerConfig as? WrittenAnswerConfigImpl
@ -339,7 +347,7 @@ private fun AnswerOptions(
maxLines = 6,
shape = RoundedCornerShape(14.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF56306F),
focusedBorderColor = wheelTone.accent,
unfocusedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f),
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface
@ -359,11 +367,11 @@ private fun AnswerOptions(
shape = RoundedCornerShape(12.dp),
border = BorderStroke(
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)
),
colors = ButtonDefaults.outlinedButtonColors(
containerColor = if (isSelected) Color(0xFFF0EDF9)
containerColor = if (isSelected) wheelTone.accentContainer
else MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface
)
@ -376,7 +384,7 @@ private fun AnswerOptions(
selected = isSelected,
onClick = null,
colors = RadioButtonDefaults.colors(
selectedColor = Color(0xFF56306F)
selectedColor = wheelTone.accent
)
)
Text(
@ -403,11 +411,11 @@ private fun AnswerOptions(
shape = RoundedCornerShape(12.dp),
border = BorderStroke(
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)
),
colors = ButtonDefaults.outlinedButtonColors(
containerColor = if (isChecked) Color(0xFFF0EDF9)
containerColor = if (isChecked) wheelTone.accentContainer
else MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface
)
@ -420,7 +428,7 @@ private fun AnswerOptions(
checked = isChecked,
onCheckedChange = null,
colors = CheckboxDefaults.colors(
checkedColor = Color(0xFF56306F)
checkedColor = wheelTone.accent
)
)
Text(
@ -446,7 +454,7 @@ private fun AnswerOptions(
Text(
text = "$selectedScaleValue",
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),
color = Color(0xFF56306F)
color = wheelTone.accent
)
}
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,
modifier = Modifier.fillMaxWidth(),
colors = SliderDefaults.colors(
thumbColor = Color(0xFF56306F),
activeTrackColor = Color(0xFF56306F),
thumbColor = wheelTone.accent,
activeTrackColor = wheelTone.accent,
inactiveTrackColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f)
)
)
@ -495,9 +503,9 @@ private fun AnswerOptions(
.height(96.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (isSelected) Color(0xFF56306F)
containerColor = if (isSelected) wheelTone.accent
else MaterialTheme.colorScheme.surfaceVariant,
contentColor = if (isSelected) Color.White
contentColor = if (isSelected) wheelTone.onAccent
else MaterialTheme.colorScheme.onSurfaceVariant
),
elevation = ButtonDefaults.buttonElevation(
@ -550,6 +558,8 @@ private fun EmptySessionCard() {
@Composable
private fun SubmitErrorCard(message: String, onRetry: () -> Unit) {
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
@ -574,17 +584,19 @@ private fun SubmitErrorCard(message: String, onRetry: () -> Unit) {
onClick = onRetry,
modifier = Modifier.fillMaxWidth().heightIn(min = 52.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
fun WheelSessionScreenPreview() {
private fun WheelSessionScreenPreviewBody() {
WheelSessionContent(
state = WheelSessionUiState(
isLoading = false,
@ -605,3 +617,15 @@ fun WheelSessionScreenPreview() {
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:
# - 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
# 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
@ -18,6 +22,7 @@
#
# Usage:
# ./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)
#
set -euo pipefail
@ -25,10 +30,21 @@ set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
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"
SCAN_OUTPUT="${1:-}"
STRICT=false
if [[ "${1:-}" == "--strict" ]]; then
STRICT=true
SCAN_OUTPUT="${2:-}"
else
SCAN_OUTPUT="${1:-}"
fi
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
: > "$SCAN_OUTPUT"
@ -63,8 +79,35 @@ drop_previews() {
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 "Project: $PROJECT_ROOT"
if [[ "$STRICT" == true ]]; then
log "Mode: strict"
fi
log ""
# -----------------------------------------------------------------------------
@ -82,11 +125,12 @@ scan_after_container() {
grep -rnE "$pattern" "$UI_DIR" --include="*.kt" > "$tmp_containers" || true
while IFS= read -r container; do
local file=$(echo "$container" | cut -d: -f1)
local lineno=$(echo "$container" | cut -d: -f2)
if [[ ! -f "$file" ]]; then continue; fi
if in_preview "$file" "$lineno"; then continue; fi
local segment=$(sed -n "${lineno},$((lineno+7))p" "$file")
local file=$(echo "$container" | cut -d: -f1)
local lineno=$(echo "$container" | cut -d: -f2)
if [[ ! -f "$file" ]]; then continue; fi
if in_preview "$file" "$lineno"; then continue; fi
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)
if [[ -n "$match" ]]; then
log "${label} ${file}:${lineno}"
@ -107,6 +151,7 @@ grep -rnE 'Modifier\.background\(Color(\.|\()' "$UI_DIR" --include="*.kt" \
| grep -ivE 'Color\.Transparent' \
| grep -ivE 'Theme\.kt' \
| drop_previews \
| drop_allowed \
| while IFS= read -r line; do
log "🔴 CRITICAL $line"
done || true
@ -117,6 +162,7 @@ grep -rnE '(buttonColors|TextFieldDefaults\.colors|TabRowDefaults\.colors|Switch
| grep -iE 'Color(\.|\()' \
| grep -ivE 'Color\.Transparent' \
| drop_previews \
| drop_allowed \
| while IFS= read -r line; do
log "🟠 MAJOR $line"
done || true
@ -124,6 +170,7 @@ grep -rnE '(buttonColors|TextFieldDefaults\.colors|TabRowDefaults\.colors|Switch
grep -rnE '(Divider|HorizontalDivider)\s*\(' "$UI_DIR" --include="*.kt" \
| grep -iE 'color = Color(\.|\()' \
| drop_previews \
| drop_allowed \
| while IFS= read -r line; do
log "🟠 MAJOR $line"
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 -ivE 'Theme\.kt' \
| drop_previews \
| drop_allowed \
| while IFS= read -r line; do
log "🟡 REVIEW $line"
done || true
@ -141,6 +189,7 @@ log ""
log "## Tier 1E — Direct painterResource that bypasses BrandIllustration (MAJOR)"
grep -rnE 'painterResource\(R\.drawable\.(illustration_|pack_art_)' "$UI_DIR" --include="*.kt" \
| drop_previews \
| drop_allowed \
| while IFS= read -r line; do
log "🟠 MAJOR $line"
done || true
@ -150,6 +199,7 @@ log "## Tier 1F — Hardcoded border colors (REVIEW)"
grep -rnE 'Modifier\.border\([^)]*Color(\.|\()' "$UI_DIR" --include="*.kt" \
| grep -ivE 'Color\.Transparent' \
| drop_previews \
| drop_allowed \
| while IFS= read -r line; do
log "🟡 REVIEW $line"
done || true
@ -157,12 +207,38 @@ grep -rnE 'Modifier\.border\([^)]*Color(\.|\()' "$UI_DIR" --include="*.kt" \
log ""
log "## Tier 1G — Hardcoded Brush/gradient stops (REVIEW)"
grep -rnE 'Brush\.(linear|vertical|horizontal|radial)Gradient' "$UI_DIR" --include="*.kt" \
| grep -vE 'BrandIllustration\.kt' \
| drop_previews \
| drop_allowed \
| 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
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 "## Summary"
critical=$(grep -c "🔴 CRITICAL" "$SCAN_OUTPUT" || true)