From fc5d70917417dd31fdfedf74b7b1153084b1ac18 Mon Sep 17 00:00:00 2001 From: null Date: Thu, 16 Jul 2026 02:31:37 -0500 Subject: [PATCH] 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. --- .../closer/ui/answers/AnswerHistoryScreen.kt | 17 +- .../closer/ui/answers/AnswerRevealScreen.kt | 56 ++++--- .../challenges/ConnectionChallengesScreen.kt | 99 ++++++++---- .../closer/ui/components/GamePromptBanner.kt | 31 ++-- .../app/closer/ui/components/LoadingState.kt | 10 +- .../ui/components/SpecialDatesSection.kt | 30 ++-- .../app/closer/ui/dates/DateBuilderScreen.kt | 12 +- .../app/closer/ui/dates/DateMatchScreen.kt | 28 ++-- .../app/closer/ui/dates/DateMatchesScreen.kt | 24 ++- .../app/closer/ui/dates/DateMemoriesScreen.kt | 7 +- .../closer/ui/desiresync/DesireSyncScreen.kt | 118 ++++++++++---- .../app/closer/ui/home/PartnerHomeScreen.kt | 2 +- .../ui/home/components/HomeActionStyle.kt | 10 +- .../ui/home/components/HomeHeaderSection.kt | 7 +- .../home/components/StreakMilestoneDialog.kt | 10 +- .../app/closer/ui/howwell/HowWellScreen.kt | 147 +++++++++++++----- .../closer/ui/memorylane/MemoryLaneScreen.kt | 59 +++++-- .../closer/ui/onboarding/OnboardingScreen.kt | 5 +- .../closer/ui/outcomes/YourProgressScreen.kt | 4 +- .../closer/ui/pairing/PairingSuccessScreen.kt | 4 +- .../app/closer/ui/paywall/PaywallScreen.kt | 71 +++++---- .../java/app/closer/ui/play/PlayHubScreen.kt | 28 ++-- .../closer/ui/settings/DeleteAccountScreen.kt | 14 +- .../ui/settings/RelationshipSettingsScreen.kt | 10 +- .../app/closer/ui/settings/SettingsScreen.kt | 18 +-- .../java/app/closer/ui/theme/CloserTokens.kt | 126 +++++++++++++++ .../closer/ui/thisorthat/ThisOrThatScreen.kt | 12 +- .../closer/ui/wheel/CategoryPickerScreen.kt | 16 +- .../app/closer/ui/wheel/SpinWheelScreen.kt | 28 ++-- .../closer/ui/wheel/WheelCompleteScreen.kt | 52 +++++-- .../app/closer/ui/wheel/WheelHistoryScreen.kt | 22 ++- .../app/closer/ui/wheel/WheelSessionScreen.kt | 72 ++++++--- scripts/theme-scan.sh | 96 ++++++++++-- 33 files changed, 905 insertions(+), 340 deletions(-) create mode 100644 app/src/main/java/app/closer/ui/theme/CloserTokens.kt diff --git a/app/src/main/java/app/closer/ui/answers/AnswerHistoryScreen.kt b/app/src/main/java/app/closer/ui/answers/AnswerHistoryScreen.kt index 91e1a334..e89caefb 100644 --- a/app/src/main/java/app/closer/ui/answers/AnswerHistoryScreen.kt +++ b/app/src/main/java/app/closer/ui/answers/AnswerHistoryScreen.kt @@ -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(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 diff --git a/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt b/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt index dd828c6a..8124eca0 100644 --- a/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt +++ b/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt @@ -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 ) } diff --git a/app/src/main/java/app/closer/ui/challenges/ConnectionChallengesScreen.kt b/app/src/main/java/app/closer/ui/challenges/ConnectionChallengesScreen.kt index 1211fa35..cd60159e 100644 --- a/app/src/main/java/app/closer/ui/challenges/ConnectionChallengesScreen.kt +++ b/app/src/main/java/app/closer/ui/challenges/ConnectionChallengesScreen.kt @@ -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, 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() } +} diff --git a/app/src/main/java/app/closer/ui/components/GamePromptBanner.kt b/app/src/main/java/app/closer/ui/components/GamePromptBanner.kt index 19a94cef..949cd15e 100644 --- a/app/src/main/java/app/closer/ui/components/GamePromptBanner.kt +++ b/app/src/main/java/app/closer/ui/components/GamePromptBanner.kt @@ -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) ) } diff --git a/app/src/main/java/app/closer/ui/components/LoadingState.kt b/app/src/main/java/app/closer/ui/components/LoadingState.kt index 3fc31548..821d6d78 100644 --- a/app/src/main/java/app/closer/ui/components/LoadingState.kt +++ b/app/src/main/java/app/closer/ui/components/LoadingState.kt @@ -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()) ) diff --git a/app/src/main/java/app/closer/ui/components/SpecialDatesSection.kt b/app/src/main/java/app/closer/ui/components/SpecialDatesSection.kt index 2ef28557..b91624ef 100644 --- a/app/src/main/java/app/closer/ui/components/SpecialDatesSection.kt +++ b/app/src/main/java/app/closer/ui/components/SpecialDatesSection.kt @@ -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 ) } diff --git a/app/src/main/java/app/closer/ui/dates/DateBuilderScreen.kt b/app/src/main/java/app/closer/ui/dates/DateBuilderScreen.kt index 08ff4c2d..d5523019 100644 --- a/app/src/main/java/app/closer/ui/dates/DateBuilderScreen.kt +++ b/app/src/main/java/app/closer/ui/dates/DateBuilderScreen.kt @@ -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") diff --git a/app/src/main/java/app/closer/ui/dates/DateMatchScreen.kt b/app/src/main/java/app/closer/ui/dates/DateMatchScreen.kt index 233c4e35..815788fc 100644 --- a/app/src/main/java/app/closer/ui/dates/DateMatchScreen.kt +++ b/app/src/main/java/app/closer/ui/dates/DateMatchScreen.kt @@ -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") diff --git a/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt b/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt index a8bb597f..2ad20c98 100644 --- a/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt +++ b/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt @@ -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 ) } diff --git a/app/src/main/java/app/closer/ui/dates/DateMemoriesScreen.kt b/app/src/main/java/app/closer/ui/dates/DateMemoriesScreen.kt index d1abbc4f..7bdee649 100644 --- a/app/src/main/java/app/closer/ui/dates/DateMemoriesScreen.kt +++ b/app/src/main/java/app/closer/ui/dates/DateMemoriesScreen.kt @@ -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( diff --git a/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt b/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt index 95afb438..cda4d6a0 100644 --- a/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt +++ b/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt @@ -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 { diff --git a/app/src/main/java/app/closer/ui/home/PartnerHomeScreen.kt b/app/src/main/java/app/closer/ui/home/PartnerHomeScreen.kt index 80b67bf1..b3e43e50 100644 --- a/app/src/main/java/app/closer/ui/home/PartnerHomeScreen.kt +++ b/app/src/main/java/app/closer/ui/home/PartnerHomeScreen.kt @@ -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) diff --git a/app/src/main/java/app/closer/ui/home/components/HomeActionStyle.kt b/app/src/main/java/app/closer/ui/home/components/HomeActionStyle.kt index 9889d30b..53f86f07 100644 --- a/app/src/main/java/app/closer/ui/home/components/HomeActionStyle.kt +++ b/app/src/main/java/app/closer/ui/home/components/HomeActionStyle.kt @@ -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, diff --git a/app/src/main/java/app/closer/ui/home/components/HomeHeaderSection.kt b/app/src/main/java/app/closer/ui/home/components/HomeHeaderSection.kt index 030546b3..2fc5d5d3 100644 --- a/app/src/main/java/app/closer/ui/home/components/HomeHeaderSection.kt +++ b/app/src/main/java/app/closer/ui/home/components/HomeHeaderSection.kt @@ -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 ) } } diff --git a/app/src/main/java/app/closer/ui/home/components/StreakMilestoneDialog.kt b/app/src/main/java/app/closer/ui/home/components/StreakMilestoneDialog.kt index 35aec239..d1231cbb 100644 --- a/app/src/main/java/app/closer/ui/home/components/StreakMilestoneDialog.kt +++ b/app/src/main/java/app/closer/ui/home/components/StreakMilestoneDialog.kt @@ -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( diff --git a/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt b/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt index 60206afa..891cdb8c 100644 --- a/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt +++ b/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt @@ -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 { diff --git a/app/src/main/java/app/closer/ui/memorylane/MemoryLaneScreen.kt b/app/src/main/java/app/closer/ui/memorylane/MemoryLaneScreen.kt index 84d1b56b..ab778279 100644 --- a/app/src/main/java/app/closer/ui/memorylane/MemoryLaneScreen.kt +++ b/app/src/main/java/app/closer/ui/memorylane/MemoryLaneScreen.kt @@ -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)) diff --git a/app/src/main/java/app/closer/ui/onboarding/OnboardingScreen.kt b/app/src/main/java/app/closer/ui/onboarding/OnboardingScreen.kt index 2d862e3f..88c846e3 100644 --- a/app/src/main/java/app/closer/ui/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/app/closer/ui/onboarding/OnboardingScreen.kt @@ -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) } } diff --git a/app/src/main/java/app/closer/ui/outcomes/YourProgressScreen.kt b/app/src/main/java/app/closer/ui/outcomes/YourProgressScreen.kt index affdfe80..f6333ae4 100644 --- a/app/src/main/java/app/closer/ui/outcomes/YourProgressScreen.kt +++ b/app/src/main/java/app/closer/ui/outcomes/YourProgressScreen.kt @@ -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 { diff --git a/app/src/main/java/app/closer/ui/pairing/PairingSuccessScreen.kt b/app/src/main/java/app/closer/ui/pairing/PairingSuccessScreen.kt index 94cdc996..c51f9d77 100644 --- a/app/src/main/java/app/closer/ui/pairing/PairingSuccessScreen.kt +++ b/app/src/main/java/app/closer/ui/pairing/PairingSuccessScreen.kt @@ -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 ) { diff --git a/app/src/main/java/app/closer/ui/paywall/PaywallScreen.kt b/app/src/main/java/app/closer/ui/paywall/PaywallScreen.kt index ddc89e3e..46a9ff54 100644 --- a/app/src/main/java/app/closer/ui/paywall/PaywallScreen.kt +++ b/app/src/main/java/app/closer/ui/paywall/PaywallScreen.kt @@ -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") diff --git a/app/src/main/java/app/closer/ui/play/PlayHubScreen.kt b/app/src/main/java/app/closer/ui/play/PlayHubScreen.kt index 0e801fc2..f1c406c8 100644 --- a/app/src/main/java/app/closer/ui/play/PlayHubScreen.kt +++ b/app/src/main/java/app/closer/ui/play/PlayHubScreen.kt @@ -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() } +} diff --git a/app/src/main/java/app/closer/ui/settings/DeleteAccountScreen.kt b/app/src/main/java/app/closer/ui/settings/DeleteAccountScreen.kt index eaf73626..39982dac 100644 --- a/app/src/main/java/app/closer/ui/settings/DeleteAccountScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/DeleteAccountScreen.kt @@ -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) } } } diff --git a/app/src/main/java/app/closer/ui/settings/RelationshipSettingsScreen.kt b/app/src/main/java/app/closer/ui/settings/RelationshipSettingsScreen.kt index bacf9fc8..eda3011c 100644 --- a/app/src/main/java/app/closer/ui/settings/RelationshipSettingsScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/RelationshipSettingsScreen.kt @@ -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) } } } diff --git a/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt b/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt index aeba32f2..fd6f6777 100644 --- a/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt @@ -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 ) } } diff --git a/app/src/main/java/app/closer/ui/theme/CloserTokens.kt b/app/src/main/java/app/closer/ui/theme/CloserTokens.kt new file mode 100644 index 00000000..bd804bcd --- /dev/null +++ b/app/src/main/java/app/closer/ui/theme/CloserTokens.kt @@ -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) + ) + } diff --git a/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt b/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt index aeb9e66b..0eebb2f1 100644 --- a/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt +++ b/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt @@ -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( diff --git a/app/src/main/java/app/closer/ui/wheel/CategoryPickerScreen.kt b/app/src/main/java/app/closer/ui/wheel/CategoryPickerScreen.kt index 6af5627f..644f23d1 100644 --- a/app/src/main/java/app/closer/ui/wheel/CategoryPickerScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/CategoryPickerScreen.kt @@ -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() } +} diff --git a/app/src/main/java/app/closer/ui/wheel/SpinWheelScreen.kt b/app/src/main/java/app/closer/ui/wheel/SpinWheelScreen.kt index f7d70157..1374f599 100644 --- a/app/src/main/java/app/closer/ui/wheel/SpinWheelScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/SpinWheelScreen.kt @@ -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() } +} diff --git a/app/src/main/java/app/closer/ui/wheel/WheelCompleteScreen.kt b/app/src/main/java/app/closer/ui/wheel/WheelCompleteScreen.kt index 431ec3e4..328f01ff 100644 --- a/app/src/main/java/app/closer/ui/wheel/WheelCompleteScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/WheelCompleteScreen.kt @@ -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() } +} diff --git a/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt b/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt index dfa7b989..be12a9fb 100644 --- a/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt @@ -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") } } } diff --git a/app/src/main/java/app/closer/ui/wheel/WheelSessionScreen.kt b/app/src/main/java/app/closer/ui/wheel/WheelSessionScreen.kt index 6677be4a..7e582219 100644 --- a/app/src/main/java/app/closer/ui/wheel/WheelSessionScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/WheelSessionScreen.kt @@ -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() } +} diff --git a/scripts/theme-scan.sh b/scripts/theme-scan.sh index 0263d2d5..8579c746 100755 --- a/scripts/theme-scan.sh +++ b/scripts/theme-scan.sh @@ -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)