Unify Android theme tokens
Add shared experience and status color tokens, migrate Android game/shared UI away from raw palettes, expand preview coverage, and strengthen the theme scanner with strict raw-color and design-system drift checks.
This commit is contained in:
parent
8a9bd41fe5
commit
fc5d709174
|
|
@ -42,9 +42,6 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -54,7 +51,7 @@ import app.closer.core.navigation.AppRoute
|
||||||
import app.closer.domain.model.LocalAnswer
|
import app.closer.domain.model.LocalAnswer
|
||||||
import app.closer.ui.components.CategoryGlyph
|
import app.closer.ui.components.CategoryGlyph
|
||||||
import app.closer.ui.components.EmptyState
|
import app.closer.ui.components.EmptyState
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.closerStatusColors
|
||||||
import app.closer.ui.questions.displayCategoryName
|
import app.closer.ui.questions.displayCategoryName
|
||||||
import app.closer.ui.components.CloserGlyphs
|
import app.closer.ui.components.CloserGlyphs
|
||||||
|
|
||||||
|
|
@ -94,6 +91,7 @@ private fun AnswerHistoryContent(
|
||||||
) {
|
) {
|
||||||
var pendingDelete by remember { mutableStateOf<LocalAnswer?>(null) }
|
var pendingDelete by remember { mutableStateOf<LocalAnswer?>(null) }
|
||||||
var selectedFilter by remember { mutableStateOf(AnswerHistoryFilter.ALL) }
|
var selectedFilter by remember { mutableStateOf(AnswerHistoryFilter.ALL) }
|
||||||
|
val statusColors = closerStatusColors()
|
||||||
val visibleAnswers = remember(state.answers, selectedFilter) {
|
val visibleAnswers = remember(state.answers, selectedFilter) {
|
||||||
state.answers.filter { answer ->
|
state.answers.filter { answer ->
|
||||||
when (selectedFilter) {
|
when (selectedFilter) {
|
||||||
|
|
@ -118,8 +116,8 @@ private fun AnswerHistoryContent(
|
||||||
pendingDelete = null
|
pendingDelete = null
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color(0xFF8D2D35),
|
containerColor = statusColors.danger,
|
||||||
contentColor = Color.White
|
contentColor = MaterialTheme.colorScheme.onError
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Remove")
|
Text("Remove")
|
||||||
|
|
@ -358,7 +356,7 @@ private fun AnswerHistoryCard(
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Remove",
|
text = "Remove",
|
||||||
color = Color(0xFF8D2D35)
|
color = closerStatusColors().danger
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -369,8 +367,9 @@ private fun AnswerHistoryCard(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HistoryStateBadge(isRevealed: Boolean) {
|
private fun HistoryStateBadge(isRevealed: Boolean) {
|
||||||
val tint = if (isRevealed) MaterialTheme.colorScheme.onPrimaryContainer else CloserPalette.Evergreen
|
val statusColors = closerStatusColors()
|
||||||
val container = if (isRevealed) closerSoftPurpleColor() else CloserPalette.Evergreen.copy(alpha = 0.10f)
|
val tint = if (isRevealed) MaterialTheme.colorScheme.onPrimaryContainer else statusColors.success
|
||||||
|
val container = if (isRevealed) closerSoftPurpleColor() else statusColors.successContainer
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(999.dp),
|
shape = RoundedCornerShape(999.dp),
|
||||||
color = container
|
color = container
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,8 @@ import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import app.closer.R
|
import app.closer.R
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
|
@ -66,6 +64,9 @@ import app.closer.domain.DailyModeResolver
|
||||||
import app.closer.ui.questions.displayCategoryName
|
import app.closer.ui.questions.displayCategoryName
|
||||||
import app.closer.ui.questions.displayQuestionType
|
import app.closer.ui.questions.displayQuestionType
|
||||||
import app.closer.ui.components.BrandMessageRotator
|
import app.closer.ui.components.BrandMessageRotator
|
||||||
|
import app.closer.ui.components.BrandIllustration
|
||||||
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnswerRevealScreen(
|
fun AnswerRevealScreen(
|
||||||
|
|
@ -265,6 +266,8 @@ private fun NoAnswerState(
|
||||||
onAnswerQuestion: () -> Unit,
|
onAnswerQuestion: () -> Unit,
|
||||||
onHome: () -> Unit
|
onHome: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
|
||||||
|
|
||||||
RevealMessageCard {
|
RevealMessageCard {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
RevealPill("No answer yet")
|
RevealPill("No answer yet")
|
||||||
|
|
@ -291,8 +294,8 @@ private fun NoAnswerState(
|
||||||
.heightIn(min = 48.dp),
|
.heightIn(min = 48.dp),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color(0xFFB98AF4),
|
containerColor = revealTone.accent,
|
||||||
contentColor = Color(0xFF24122F)
|
contentColor = revealTone.onAccent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Answer")
|
Text("Answer")
|
||||||
|
|
@ -319,6 +322,8 @@ private fun ReadyToRevealState(
|
||||||
onReveal: () -> Unit,
|
onReveal: () -> Unit,
|
||||||
onHistory: () -> Unit
|
onHistory: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
|
||||||
|
|
||||||
RevealMessageCard {
|
RevealMessageCard {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
RevealPill("Private answer saved")
|
RevealPill("Private answer saved")
|
||||||
|
|
@ -347,8 +352,8 @@ private fun ReadyToRevealState(
|
||||||
.heightIn(min = 48.dp),
|
.heightIn(min = 48.dp),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color(0xFFB98AF4),
|
containerColor = revealTone.accent,
|
||||||
contentColor = Color(0xFF24122F)
|
contentColor = revealTone.onAccent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Reveal answer")
|
Text("Reveal answer")
|
||||||
|
|
@ -432,15 +437,18 @@ private fun BothAnsweredSealedState(
|
||||||
onReveal: () -> Unit,
|
onReveal: () -> Unit,
|
||||||
onHistory: () -> Unit
|
onHistory: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
|
||||||
|
|
||||||
RevealMessageCard {
|
RevealMessageCard {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Image(
|
BrandIllustration(
|
||||||
painter = painterResource(R.drawable.illustration_reveal_celebration),
|
res = R.drawable.illustration_reveal_celebration,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(140.dp)
|
modifier = Modifier.size(140.dp),
|
||||||
|
tile = false
|
||||||
)
|
)
|
||||||
RevealPill("Both answers are in")
|
RevealPill("Both answers are in")
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -464,8 +472,8 @@ private fun BothAnsweredSealedState(
|
||||||
modifier = Modifier.weight(1f).heightIn(min = 48.dp),
|
modifier = Modifier.weight(1f).heightIn(min = 48.dp),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color(0xFFB98AF4),
|
containerColor = revealTone.accent,
|
||||||
contentColor = Color(0xFF24122F)
|
contentColor = revealTone.onAccent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Reveal answer")
|
Text("Reveal answer")
|
||||||
|
|
@ -504,6 +512,8 @@ private fun WaitingForPartnerState(
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
onHome: () -> Unit
|
onHome: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
|
||||||
|
|
||||||
RevealMessageCard {
|
RevealMessageCard {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
RevealPill("Almost there")
|
RevealPill("Almost there")
|
||||||
|
|
@ -528,8 +538,8 @@ private fun WaitingForPartnerState(
|
||||||
modifier = Modifier.weight(1f).heightIn(min = 48.dp),
|
modifier = Modifier.weight(1f).heightIn(min = 48.dp),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color(0xFFB98AF4),
|
containerColor = revealTone.accent,
|
||||||
contentColor = Color(0xFF24122F)
|
contentColor = revealTone.onAccent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Check again")
|
Text("Check again")
|
||||||
|
|
@ -595,6 +605,8 @@ private fun RevealedState(
|
||||||
loreSaved: Boolean = false,
|
loreSaved: Boolean = false,
|
||||||
wasSealed: Boolean = false
|
wasSealed: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
|
||||||
|
|
||||||
val matchLine = if (partnerAnswer != null) {
|
val matchLine = if (partnerAnswer != null) {
|
||||||
val ownIds = answer.selectedOptionIds.toSet()
|
val ownIds = answer.selectedOptionIds.toSet()
|
||||||
val partnerIds = partnerAnswer.selectedOptionIds.toSet()
|
val partnerIds = partnerAnswer.selectedOptionIds.toSet()
|
||||||
|
|
@ -616,7 +628,7 @@ private fun RevealedState(
|
||||||
Text(
|
Text(
|
||||||
text = matchLine,
|
text = matchLine,
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
color = Color(0xFF56306F),
|
color = revealTone.accent,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
|
@ -639,8 +651,8 @@ private fun RevealedState(
|
||||||
.heightIn(min = 48.dp),
|
.heightIn(min = 48.dp),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color(0xFFB98AF4),
|
containerColor = revealTone.accent,
|
||||||
contentColor = Color(0xFF24122F)
|
contentColor = revealTone.onAccent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Saved answers")
|
Text("Saved answers")
|
||||||
|
|
@ -663,7 +675,7 @@ private fun RevealedState(
|
||||||
Text(
|
Text(
|
||||||
"Save to Couple Lore",
|
"Save to Couple Lore",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = Color(0xFF56306F).copy(alpha = 0.75f)
|
color = revealTone.accent.copy(alpha = 0.75f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -684,6 +696,8 @@ private fun TinyActionCard(
|
||||||
mode: DailyModeResolver.DailyMode,
|
mode: DailyModeResolver.DailyMode,
|
||||||
onDoThis: () -> Unit
|
onDoThis: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(28.dp),
|
shape = RoundedCornerShape(28.dp),
|
||||||
|
|
@ -711,8 +725,8 @@ private fun TinyActionCard(
|
||||||
.heightIn(min = 40.dp),
|
.heightIn(min = 40.dp),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color(0xFFB98AF4),
|
containerColor = revealTone.accent,
|
||||||
contentColor = Color(0xFF24122F)
|
contentColor = revealTone.onAccent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Do this tonight", style = MaterialTheme.typography.labelMedium)
|
Text("Do this tonight", style = MaterialTheme.typography.labelMedium)
|
||||||
|
|
@ -769,6 +783,8 @@ private fun FollowUpChip(
|
||||||
label: String,
|
label: String,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -796,7 +812,7 @@ private fun FollowUpChip(
|
||||||
Text(
|
Text(
|
||||||
text = "→",
|
text = "→",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = Color(0xFFB98AF4),
|
color = revealTone.accent,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
@ -68,8 +69,11 @@ import app.closer.domain.model.ConnectionChallenge
|
||||||
import app.closer.domain.repository.AuthRepository
|
import app.closer.domain.repository.AuthRepository
|
||||||
import app.closer.domain.repository.CoupleRepository
|
import app.closer.domain.repository.CoupleRepository
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
|
import app.closer.ui.theme.CloserTheme
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
|
import app.closer.ui.theme.closerStatusColors
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -403,6 +407,9 @@ private fun ChallengePickCard(
|
||||||
onPick: (ConnectionChallenge) -> Unit,
|
onPick: (ConnectionChallenge) -> Unit,
|
||||||
onPaywall: () -> Unit
|
onPaywall: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val challengeTone = closerToneColors(CloserExperienceTone.Challenge)
|
||||||
|
val statusColors = closerStatusColors()
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
onClick = { if (challenge.isPremium && !hasPremium) onPaywall() else onPick(challenge) },
|
onClick = { if (challenge.isPremium && !hasPremium) onPaywall() else onPick(challenge) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|
@ -419,7 +426,7 @@ private fun ChallengePickCard(
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
color = CloserPalette.PurpleDeep.copy(alpha = 0.10f),
|
color = challengeTone.accentContainer,
|
||||||
modifier = Modifier.size(52.dp)
|
modifier = Modifier.size(52.dp)
|
||||||
) {
|
) {
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
|
@ -451,27 +458,27 @@ private fun ChallengePickCard(
|
||||||
if (challenge.isPremium && !hasPremium) {
|
if (challenge.isPremium && !hasPremium) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(999.dp),
|
shape = RoundedCornerShape(999.dp),
|
||||||
color = CloserPalette.Gold.copy(alpha = 0.15f)
|
color = statusColors.premiumContainer
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(3.dp)
|
horizontalArrangement = Arrangement.spacedBy(3.dp)
|
||||||
) {
|
) {
|
||||||
Icon(CloserGlyphs.Lock, contentDescription = null, tint = CloserPalette.Gold, modifier = Modifier.size(10.dp))
|
Icon(CloserGlyphs.Lock, contentDescription = null, tint = statusColors.premium, modifier = Modifier.size(10.dp))
|
||||||
Text("Premium", style = MaterialTheme.typography.labelSmall, color = CloserPalette.Gold, fontWeight = FontWeight.SemiBold)
|
Text("Premium", style = MaterialTheme.typography.labelSmall, color = statusColors.premium, fontWeight = FontWeight.SemiBold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(999.dp),
|
shape = RoundedCornerShape(999.dp),
|
||||||
color = CloserPalette.PurpleDeep.copy(alpha = 0.10f)
|
color = challengeTone.accentContainer
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "${challenge.durationDays} days",
|
text = "${challenge.durationDays} days",
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = CloserPalette.PurpleDeep,
|
color = challengeTone.onAccentContainer,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -487,7 +494,7 @@ private fun ChallengePickCard(
|
||||||
Text(
|
Text(
|
||||||
text = challenge.category,
|
text = challenge.category,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = CloserPalette.PurpleDeep.copy(alpha = 0.7f)
|
color = challengeTone.accent.copy(alpha = 0.7f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -509,6 +516,9 @@ private fun ChallengesActiveScreen(
|
||||||
onConfirmAbandon: () -> Unit,
|
onConfirmAbandon: () -> Unit,
|
||||||
onDismissAbandon: () -> Unit
|
onDismissAbandon: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val challengeTone = closerToneColors(CloserExperienceTone.Challenge)
|
||||||
|
val statusColors = closerStatusColors()
|
||||||
|
|
||||||
if (showAbandonConfirm) {
|
if (showAbandonConfirm) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissAbandon,
|
onDismissRequest = onDismissAbandon,
|
||||||
|
|
@ -585,13 +595,13 @@ private fun ChallengesActiveScreen(
|
||||||
if (progress.jointStreak > 0 && !isComplete) {
|
if (progress.jointStreak > 0 && !isComplete) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(999.dp),
|
shape = RoundedCornerShape(999.dp),
|
||||||
color = CloserPalette.PurpleDeep.copy(alpha = 0.12f)
|
color = challengeTone.accentContainer
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "🔥 ${progress.jointStreak}",
|
text = "🔥 ${progress.jointStreak}",
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = CloserPalette.PurpleDeep,
|
color = challengeTone.onAccentContainer,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -650,13 +660,13 @@ private fun ChallengesActiveScreen(
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(999.dp),
|
shape = RoundedCornerShape(999.dp),
|
||||||
color = CloserPalette.PurpleDeep.copy(alpha = 0.10f)
|
color = challengeTone.accentContainer
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Day $currentDay",
|
text = "Day $currentDay",
|
||||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = CloserPalette.PurpleDeep,
|
color = challengeTone.onAccentContainer,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -687,8 +697,8 @@ private fun ChallengesActiveScreen(
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
color = when {
|
color = when {
|
||||||
isBothDone -> CloserPalette.Evergreen.copy(alpha = 0.10f)
|
isBothDone -> statusColors.successContainer
|
||||||
isWaiting -> CloserPalette.PurpleDeep.copy(alpha = 0.07f)
|
isWaiting -> challengeTone.accentContainer.copy(alpha = 0.55f)
|
||||||
else -> MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)
|
else -> MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
@ -704,14 +714,14 @@ private fun ChallengesActiveScreen(
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
Icon(icon, contentDescription = null, tint = CloserPalette.Evergreen, modifier = Modifier.size(18.dp))
|
Icon(icon, contentDescription = null, tint = statusColors.success, modifier = Modifier.size(18.dp))
|
||||||
} else {
|
} else {
|
||||||
Text(if (isWaiting) "⏳" else "💬", style = MaterialTheme.typography.bodySmall)
|
Text(if (isWaiting) "⏳" else "💬", style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = stateCopy,
|
text = stateCopy,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = if (isBothDone) CloserPalette.Evergreen else MaterialTheme.colorScheme.onSurfaceVariant,
|
color = if (isBothDone) statusColors.success else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
fontWeight = if (isBothDone) FontWeight.Medium else FontWeight.Normal
|
fontWeight = if (isBothDone) FontWeight.Medium else FontWeight.Normal
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -729,14 +739,15 @@ private fun ChallengesActiveScreen(
|
||||||
.heightIn(min = 54.dp),
|
.heightIn(min = 54.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = CloserPalette.PurpleDeep,
|
containerColor = challengeTone.accent,
|
||||||
disabledContainerColor = CloserPalette.PurpleDeep.copy(alpha = 0.35f)
|
contentColor = challengeTone.onAccent,
|
||||||
|
disabledContainerColor = challengeTone.accent.copy(alpha = 0.35f),
|
||||||
|
disabledContentColor = challengeTone.onAccent.copy(alpha = 0.65f)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = ctaLabel,
|
text = ctaLabel,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge
|
||||||
color = Color.White
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -781,13 +792,13 @@ private fun ChallengesActiveScreen(
|
||||||
.heightIn(min = 54.dp),
|
.heightIn(min = 54.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = CloserPalette.PurpleDeep
|
containerColor = challengeTone.accent,
|
||||||
|
contentColor = challengeTone.onAccent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "View game history",
|
text = "View game history",
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge
|
||||||
color = Color.White
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -819,6 +830,8 @@ private fun DayTrackerStrip(
|
||||||
partnerCompletedDays: List<Int>,
|
partnerCompletedDays: List<Int>,
|
||||||
currentDay: Int
|
currentDay: Int
|
||||||
) {
|
) {
|
||||||
|
val challengeTone = closerToneColors(CloserExperienceTone.Challenge)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
|
@ -830,9 +843,9 @@ private fun DayTrackerStrip(
|
||||||
val isCurrent = day == currentDay.coerceAtMost(totalDays) && !myDone
|
val isCurrent = day == currentDay.coerceAtMost(totalDays) && !myDone
|
||||||
|
|
||||||
val bg = when {
|
val bg = when {
|
||||||
jointDone -> CloserPalette.PurpleDeep
|
jointDone -> challengeTone.accent
|
||||||
myDone -> CloserPalette.PurpleDeep.copy(alpha = 0.45f)
|
myDone -> challengeTone.accent.copy(alpha = 0.45f)
|
||||||
isCurrent -> CloserPalette.PurpleDeep.copy(alpha = 0.15f)
|
isCurrent -> challengeTone.accentContainer
|
||||||
else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -845,12 +858,14 @@ private fun DayTrackerStrip(
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
if (jointDone) {
|
if (jointDone) {
|
||||||
Icon(CloserGlyphs.Check, contentDescription = null, tint = Color.White, modifier = Modifier.size(14.dp))
|
Icon(CloserGlyphs.Check, contentDescription = null, tint = challengeTone.onAccent, modifier = Modifier.size(14.dp))
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = "$day",
|
text = "$day",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = if (myDone || isCurrent) Color.White else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
color = if (myDone) challengeTone.onAccent
|
||||||
|
else if (isCurrent) challengeTone.onAccentContainer
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -858,3 +873,31 @@ private fun DayTrackerStrip(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChallengesPreviewBody() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(closerBackgroundBrush())
|
||||||
|
) {
|
||||||
|
ChallengesPickScreen(
|
||||||
|
hasPremium = false,
|
||||||
|
onBack = {},
|
||||||
|
onPick = {},
|
||||||
|
onPaywall = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Challenges • light")
|
||||||
|
@Composable
|
||||||
|
private fun ChallengesPreviewLight() {
|
||||||
|
CloserTheme(darkTheme = false) { ChallengesPreviewBody() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Challenges • dark")
|
||||||
|
@Composable
|
||||||
|
private fun ChallengesPreviewDark() {
|
||||||
|
CloserTheme(darkTheme = true) { ChallengesPreviewBody() }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -56,7 +55,9 @@ import app.closer.domain.model.GameType
|
||||||
import app.closer.notifications.GamePromptController
|
import app.closer.notifications.GamePromptController
|
||||||
import app.closer.notifications.GamePromptKind
|
import app.closer.notifications.GamePromptKind
|
||||||
import app.closer.notifications.IncomingGamePrompt
|
import app.closer.notifications.IncomingGamePrompt
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
|
import app.closer.ui.theme.closerStatusColors
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
@ -179,6 +180,8 @@ private fun GamePromptCard(
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
val style = styleFor(prompt)
|
val style = styleFor(prompt)
|
||||||
|
val promptTone = closerToneColors(CloserExperienceTone.Wheel)
|
||||||
|
val statusColors = closerStatusColors()
|
||||||
var dragUp by remember(prompt.kind, prompt.gameSessionId) { mutableStateOf(0f) }
|
var dragUp by remember(prompt.kind, prompt.gameSessionId) { mutableStateOf(0f) }
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -189,10 +192,10 @@ private fun GamePromptCard(
|
||||||
.clip(RoundedCornerShape(20.dp))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(
|
.background(
|
||||||
Brush.linearGradient(
|
Brush.linearGradient(
|
||||||
listOf(CloserPalette.PurpleDeep, CloserPalette.PurpleRich)
|
listOf(promptTone.accent, promptTone.accentContainer)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.border(1.dp, Color.White.copy(alpha = 0.18f), RoundedCornerShape(20.dp))
|
.border(1.dp, promptTone.onAccent.copy(alpha = 0.18f), RoundedCornerShape(20.dp))
|
||||||
// Whole card taps act; flick up to dismiss (mirrors the chat bubble's drag-to-dismiss).
|
// Whole card taps act; flick up to dismiss (mirrors the chat bubble's drag-to-dismiss).
|
||||||
.clickable(onClick = onAction)
|
.clickable(onClick = onAction)
|
||||||
.pointerInput(prompt.gameSessionId) {
|
.pointerInput(prompt.gameSessionId) {
|
||||||
|
|
@ -214,8 +217,8 @@ private fun GamePromptCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(40.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color.White.copy(alpha = 0.18f))
|
.background(promptTone.onAccent.copy(alpha = 0.18f))
|
||||||
.border(1.5.dp, Color.White.copy(alpha = 0.5f), CircleShape),
|
.border(1.5.dp, promptTone.onAccent.copy(alpha = 0.5f), CircleShape),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
val avatar = prompt.avatarUrl
|
val avatar = prompt.avatarUrl
|
||||||
|
|
@ -230,7 +233,7 @@ private fun GamePromptCard(
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CloserGlyphs.Play,
|
imageVector = CloserGlyphs.Play,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color.White,
|
tint = promptTone.onAccent,
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(22.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -240,8 +243,8 @@ private fun GamePromptCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(12.dp)
|
.size(12.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color(0xFF3DD68C)) // live "here now" green
|
.background(statusColors.success)
|
||||||
.border(2.dp, CloserPalette.PurpleDeep, CircleShape)
|
.border(2.dp, promptTone.accent, CircleShape)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -250,20 +253,20 @@ private fun GamePromptCard(
|
||||||
Text(
|
Text(
|
||||||
text = style.line1,
|
text = style.line1,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = Color.White.copy(alpha = 0.85f)
|
color = promptTone.onAccent.copy(alpha = 0.85f)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = style.line2,
|
text = style.line2,
|
||||||
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold),
|
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold),
|
||||||
color = Color.White
|
color = promptTone.onAccent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = onAction,
|
onClick = onAction,
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color.White,
|
containerColor = promptTone.onAccent,
|
||||||
contentColor = CloserPalette.PurpleDeep
|
contentColor = promptTone.accent
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(14.dp)
|
shape = RoundedCornerShape(14.dp)
|
||||||
) {
|
) {
|
||||||
|
|
@ -279,7 +282,7 @@ private fun GamePromptCard(
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CloserGlyphs.Close,
|
imageVector = CloserGlyphs.Close,
|
||||||
contentDescription = "Dismiss",
|
contentDescription = "Dismiss",
|
||||||
tint = Color.White.copy(alpha = 0.8f),
|
tint = promptTone.onAccent.copy(alpha = 0.8f),
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,12 @@ private fun CloserMarkChipLoader(
|
||||||
size: Dp,
|
size: Dp,
|
||||||
reducedMotion: Boolean
|
reducedMotion: Boolean
|
||||||
) {
|
) {
|
||||||
|
val chipColors = listOf(
|
||||||
|
MaterialTheme.colorScheme.primary,
|
||||||
|
MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
MaterialTheme.colorScheme.secondary
|
||||||
|
)
|
||||||
|
val ringColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.10f)
|
||||||
val transition = rememberInfiniteTransition(label = "closerMarkLoader")
|
val transition = rememberInfiniteTransition(label = "closerMarkLoader")
|
||||||
val fill = if (reducedMotion) 1f else transition.animateFloat(
|
val fill = if (reducedMotion) 1f else transition.animateFloat(
|
||||||
initialValue = 0f,
|
initialValue = 0f,
|
||||||
|
|
@ -160,7 +166,7 @@ private fun CloserMarkChipLoader(
|
||||||
// Aubergine gradient chip (matches ic_launcher_background palette).
|
// Aubergine gradient chip (matches ic_launcher_background palette).
|
||||||
drawRoundRect(
|
drawRoundRect(
|
||||||
brush = Brush.linearGradient(
|
brush = Brush.linearGradient(
|
||||||
colors = listOf(Color(0xFF5A2F74), Color(0xFF3A1D4D), Color(0xFF24122F)),
|
colors = chipColors,
|
||||||
start = Offset(0f, 0f),
|
start = Offset(0f, 0f),
|
||||||
end = Offset(w, h)
|
end = Offset(w, h)
|
||||||
),
|
),
|
||||||
|
|
@ -190,7 +196,7 @@ private fun CloserMarkChipLoader(
|
||||||
}
|
}
|
||||||
// Faint ring so the dark chip separates on dark backgrounds.
|
// Faint ring so the dark chip separates on dark backgrounds.
|
||||||
drawRoundRect(
|
drawRoundRect(
|
||||||
color = Color.White.copy(alpha = 0.10f),
|
color = ringColor,
|
||||||
cornerRadius = corner,
|
cornerRadius = corner,
|
||||||
style = Stroke(width = 1.dp.toPx())
|
style = Stroke(width = 1.dp.toPx())
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -27,16 +27,16 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
private val Purple = Color(0xFF56306F)
|
import app.closer.ui.theme.closerToneColors
|
||||||
private val PurpleLight = Color(0xFFF0EDF9)
|
|
||||||
private val PinkPill = Color(0xFFB98AF4)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SpecialDatesSection(
|
fun SpecialDatesSection(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
compact: Boolean = false
|
compact: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
val dateTone = closerToneColors(CloserExperienceTone.Date)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(if (compact) 24.dp else 28.dp),
|
shape = RoundedCornerShape(if (compact) 24.dp else 28.dp),
|
||||||
|
|
@ -73,7 +73,7 @@ fun SpecialDatesSection(
|
||||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
DateBlock(day = "14", month = "Jul", tint = Purple, compact = compact)
|
DateBlock(day = "14", month = "Jul", tint = dateTone.accent, compact = compact)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
|
@ -102,7 +102,7 @@ fun SpecialDatesSection(
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CloserGlyphs.CardGiftcard,
|
imageVector = CloserGlyphs.CardGiftcard,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Purple,
|
tint = dateTone.accent,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -127,12 +127,14 @@ fun SpecialDatesSection(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BirthdayRow(name: String, day: String, month: String) {
|
private fun BirthdayRow(name: String, day: String, month: String) {
|
||||||
|
val dateTone = closerToneColors(CloserExperienceTone.Date)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
DateBlock(day = day, month = month, tint = Purple, compact = true)
|
DateBlock(day = day, month = month, tint = dateTone.accent, compact = true)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "$name's Birthday",
|
text = "$name's Birthday",
|
||||||
|
|
@ -144,7 +146,7 @@ private fun BirthdayRow(name: String, day: String, month: String) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CloserGlyphs.Cake,
|
imageVector = CloserGlyphs.Cake,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Purple.copy(alpha = 0.7f),
|
tint = dateTone.accent.copy(alpha = 0.7f),
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -184,15 +186,17 @@ private fun DateBlock(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TodayPill() {
|
private fun TodayPill() {
|
||||||
|
val dateTone = closerToneColors(CloserExperienceTone.Date)
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(999.dp),
|
shape = RoundedCornerShape(999.dp),
|
||||||
color = PinkPill
|
color = dateTone.accent
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Today",
|
text = "Today",
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = Color.White,
|
color = dateTone.onAccent,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -200,15 +204,17 @@ private fun TodayPill() {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DateCountPill() {
|
private fun DateCountPill() {
|
||||||
|
val dateTone = closerToneColors(CloserExperienceTone.Date)
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(999.dp),
|
shape = RoundedCornerShape(999.dp),
|
||||||
color = PurpleLight
|
color = dateTone.accentContainer
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "3 saved",
|
text = "3 saved",
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = Purple,
|
color = dateTone.onAccentContainer,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import app.closer.ui.components.CloserGlyphs
|
import app.closer.ui.components.CloserGlyphs
|
||||||
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DateBuilderScreen(
|
fun DateBuilderScreen(
|
||||||
|
|
@ -354,6 +356,8 @@ private fun BudgetField(
|
||||||
onBudgetChange: (Int) -> Unit,
|
onBudgetChange: (Int) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val dateTone = closerToneColors(CloserExperienceTone.Date)
|
||||||
|
|
||||||
var textValue by remember { mutableStateOf(budget.toString()) }
|
var textValue by remember { mutableStateOf(budget.toString()) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -386,7 +390,7 @@ private fun BudgetField(
|
||||||
Text(
|
Text(
|
||||||
text = "$budget",
|
text = "$budget",
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = Color(0xFF56306F),
|
color = dateTone.accent,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
|
|
@ -465,13 +469,15 @@ private fun SaveButton(
|
||||||
onSave: () -> Unit,
|
onSave: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val dateTone = closerToneColors(CloserExperienceTone.Date)
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = onSave,
|
onClick = onSave,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color(0xFFB98AF4),
|
containerColor = dateTone.accent,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
contentColor = dateTone.onAccent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Create Plan")
|
Text("Create Plan")
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,9 @@ import app.closer.ui.components.ErrorState
|
||||||
import app.closer.ui.components.LoadingState
|
import app.closer.ui.components.LoadingState
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import app.closer.ui.components.CloserGlyphs
|
import app.closer.ui.components.CloserGlyphs
|
||||||
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
|
import app.closer.ui.theme.closerStatusColors
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DateMatchScreen(
|
fun DateMatchScreen(
|
||||||
|
|
@ -493,6 +496,9 @@ private fun ActionButtons(
|
||||||
onMaybe: () -> Unit,
|
onMaybe: () -> Unit,
|
||||||
onLove: () -> Unit
|
onLove: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val dateTone = closerToneColors(CloserExperienceTone.Date)
|
||||||
|
val statusColors = closerStatusColors()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
|
@ -501,23 +507,23 @@ private fun ActionButtons(
|
||||||
CircularActionButton(
|
CircularActionButton(
|
||||||
icon = CloserGlyphs.Close,
|
icon = CloserGlyphs.Close,
|
||||||
contentDescription = "Skip",
|
contentDescription = "Skip",
|
||||||
color = MaterialTheme.colorScheme.errorContainer,
|
color = statusColors.dangerContainer,
|
||||||
iconColor = Color(0xFF8D2D35),
|
iconColor = statusColors.danger,
|
||||||
onClick = onSkip
|
onClick = onSkip
|
||||||
)
|
)
|
||||||
CircularActionButton(
|
CircularActionButton(
|
||||||
icon = CloserGlyphs.Star,
|
icon = CloserGlyphs.Star,
|
||||||
contentDescription = "Maybe",
|
contentDescription = "Maybe",
|
||||||
color = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.74f),
|
color = statusColors.warningContainer,
|
||||||
iconColor = Color(0xFF6B5D00),
|
iconColor = statusColors.warning,
|
||||||
onClick = onMaybe,
|
onClick = onMaybe,
|
||||||
size = 72.dp
|
size = 72.dp
|
||||||
)
|
)
|
||||||
CircularActionButton(
|
CircularActionButton(
|
||||||
icon = CloserGlyphs.Heart,
|
icon = CloserGlyphs.Heart,
|
||||||
contentDescription = "Love",
|
contentDescription = "Love",
|
||||||
color = closerSoftPinkColor(),
|
color = dateTone.accentContainer,
|
||||||
iconColor = Color(0xFF9B1B5A),
|
iconColor = dateTone.accent,
|
||||||
onClick = onLove
|
onClick = onLove
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -563,6 +569,8 @@ private fun MatchOverlay(
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onViewMatches: () -> Unit
|
onViewMatches: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val dateTone = closerToneColors(CloserExperienceTone.Date)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -619,8 +627,8 @@ private fun MatchOverlay(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color(0xFFF3E8FF),
|
containerColor = dateTone.accentContainer,
|
||||||
contentColor = Color(0xFF56306F)
|
contentColor = dateTone.onAccentContainer
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Keep swiping")
|
Text("Keep swiping")
|
||||||
|
|
@ -630,8 +638,8 @@ private fun MatchOverlay(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color(0xFFB98AF4),
|
containerColor = dateTone.accent,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
contentColor = dateTone.onAccent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("View matches")
|
Text("View matches")
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,9 @@ import app.closer.ui.components.EmptyState
|
||||||
import app.closer.ui.components.ErrorState
|
import app.closer.ui.components.ErrorState
|
||||||
import app.closer.ui.components.LoadingState
|
import app.closer.ui.components.LoadingState
|
||||||
import app.closer.ui.components.CloserGlyphs
|
import app.closer.ui.components.CloserGlyphs
|
||||||
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
|
import app.closer.ui.theme.closerStatusColors
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DateMatchesScreen(
|
fun DateMatchesScreen(
|
||||||
|
|
@ -87,6 +90,9 @@ private fun DateMatchesContent(
|
||||||
onMarkCompleted: (DateMatch) -> Unit = {},
|
onMarkCompleted: (DateMatch) -> Unit = {},
|
||||||
onMemories: () -> Unit = {}
|
onMemories: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val dateTone = closerToneColors(CloserExperienceTone.Date)
|
||||||
|
val statusColors = closerStatusColors()
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -171,7 +177,7 @@ private fun DateMatchesContent(
|
||||||
title = "Mutual love",
|
title = "Mutual love",
|
||||||
count = state.mutualMatches.size,
|
count = state.mutualMatches.size,
|
||||||
icon = CloserGlyphs.Heart,
|
icon = CloserGlyphs.Heart,
|
||||||
iconColor = Color(0xFF9B1B5A),
|
iconColor = dateTone.accent,
|
||||||
modifier = Modifier.padding(top = 8.dp)
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -186,7 +192,7 @@ private fun DateMatchesContent(
|
||||||
title = "Maybe together",
|
title = "Maybe together",
|
||||||
count = state.maybeMatches.size,
|
count = state.maybeMatches.size,
|
||||||
icon = CloserGlyphs.Star,
|
icon = CloserGlyphs.Star,
|
||||||
iconColor = Color(0xFF6B5D00),
|
iconColor = statusColors.warning,
|
||||||
modifier = Modifier.padding(top = 8.dp)
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -248,6 +254,8 @@ private fun SectionHeader(
|
||||||
@Composable
|
@Composable
|
||||||
private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) {
|
private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) {
|
||||||
val idea = match.dateIdea ?: return
|
val idea = match.dateIdea ?: return
|
||||||
|
val dateTone = closerToneColors(CloserExperienceTone.Date)
|
||||||
|
|
||||||
IdeaCard(
|
IdeaCard(
|
||||||
idea = idea,
|
idea = idea,
|
||||||
action = {
|
action = {
|
||||||
|
|
@ -256,7 +264,7 @@ private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) {
|
||||||
imageVector = CloserGlyphs.Heart,
|
imageVector = CloserGlyphs.Heart,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = Color(0xFF9B1B5A)
|
tint = dateTone.accent
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(6.dp))
|
Spacer(Modifier.width(6.dp))
|
||||||
Text("We did this", fontWeight = FontWeight.SemiBold)
|
Text("We did this", fontWeight = FontWeight.SemiBold)
|
||||||
|
|
@ -276,12 +284,12 @@ private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) {
|
||||||
imageVector = CloserGlyphs.Heart,
|
imageVector = CloserGlyphs.Heart,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(14.dp),
|
modifier = Modifier.size(14.dp),
|
||||||
tint = Color(0xFF9B1B5A)
|
tint = dateTone.accent
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Matched",
|
text = "Matched",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = Color(0xFF9B1B5A),
|
color = dateTone.accent,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -292,6 +300,8 @@ private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SuggestionCard(suggestion: DateMatchSuggestion) {
|
private fun SuggestionCard(suggestion: DateMatchSuggestion) {
|
||||||
|
val statusColors = closerStatusColors()
|
||||||
|
|
||||||
IdeaCard(
|
IdeaCard(
|
||||||
idea = suggestion.dateIdea,
|
idea = suggestion.dateIdea,
|
||||||
badge = {
|
badge = {
|
||||||
|
|
@ -308,12 +318,12 @@ private fun SuggestionCard(suggestion: DateMatchSuggestion) {
|
||||||
imageVector = CloserGlyphs.Star,
|
imageVector = CloserGlyphs.Star,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(14.dp),
|
modifier = Modifier.size(14.dp),
|
||||||
tint = Color(0xFF6B5D00)
|
tint = statusColors.warning
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = if (suggestion.partnerAction == SwipeAction.LOVE) "They love it" else "Maybe match",
|
text = if (suggestion.partnerAction == SwipeAction.LOVE) "They love it" else "Maybe match",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = Color(0xFF6B5D00),
|
color = statusColors.warning,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,8 @@ import app.closer.ui.components.BrandIllustration
|
||||||
import app.closer.ui.components.CloserCard
|
import app.closer.ui.components.CloserCard
|
||||||
import app.closer.ui.components.CloserHeartLoader
|
import app.closer.ui.components.CloserHeartLoader
|
||||||
import app.closer.ui.settings.SettingsSubpage
|
import app.closer.ui.settings.SettingsSubpage
|
||||||
import app.closer.ui.theme.CloserPalette
|
|
||||||
import app.closer.ui.theme.closerCardColor
|
import app.closer.ui.theme.closerCardColor
|
||||||
|
import app.closer.ui.theme.closerStatusColors
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
@ -216,10 +216,11 @@ private fun DateMemoryCard(row: DateMemoryRow, onClick: () -> Unit, onLongClick:
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReflectionChip(state: DateReflectionState) {
|
private fun ReflectionChip(state: DateReflectionState) {
|
||||||
|
val statusColors = closerStatusColors()
|
||||||
val (label, color) = when (state) {
|
val (label, color) = when (state) {
|
||||||
DateReflectionState.NONE -> "Reflect" to MaterialTheme.colorScheme.primary
|
DateReflectionState.NONE -> "Reflect" to MaterialTheme.colorScheme.primary
|
||||||
DateReflectionState.AWAITING_PARTNER -> "Waiting" to CloserPalette.Gold
|
DateReflectionState.AWAITING_PARTNER -> "Waiting" to statusColors.warning
|
||||||
DateReflectionState.BOTH_DONE -> "View" to CloserPalette.Evergreen
|
DateReflectionState.BOTH_DONE -> "View" to statusColors.success
|
||||||
}
|
}
|
||||||
Surface(shape = RoundedCornerShape(50), color = color.copy(alpha = 0.14f)) {
|
Surface(shape = RoundedCornerShape(50), color = color.copy(alpha = 0.14f)) {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
|
@ -69,8 +70,10 @@ import app.closer.ui.games.GameCopy
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import app.closer.ui.components.BrandMessageRotator
|
import app.closer.ui.components.BrandMessageRotator
|
||||||
import app.closer.ui.components.StatusGlyph
|
import app.closer.ui.components.StatusGlyph
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
|
import app.closer.ui.theme.CloserTheme
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -526,6 +529,8 @@ private fun DSSetupScreen(
|
||||||
onStart: () -> Unit,
|
onStart: () -> Unit,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -558,16 +563,16 @@ private fun DSSetupScreen(
|
||||||
onClick = { onLengthSelected(len) },
|
onClick = { onLengthSelected(len) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = if (len == selectedLength) CloserPalette.Romantic else Color.Transparent,
|
color = if (len == selectedLength) desireTone.accent else Color.Transparent,
|
||||||
border = if (len != selectedLength)
|
border = if (len != selectedLength)
|
||||||
BorderStroke(1.dp, CloserPalette.Romantic.copy(alpha = 0.4f))
|
BorderStroke(1.dp, desireTone.accent.copy(alpha = 0.4f))
|
||||||
else null
|
else null
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = len.label,
|
text = len.label,
|
||||||
modifier = Modifier.padding(vertical = 10.dp),
|
modifier = Modifier.padding(vertical = 10.dp),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = if (len == selectedLength) Color.White else CloserPalette.Romantic,
|
color = if (len == selectedLength) desireTone.onAccent else desireTone.accent,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -578,7 +583,10 @@ private fun DSSetupScreen(
|
||||||
onClick = onStart,
|
onClick = onStart,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.Romantic)
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = desireTone.accent,
|
||||||
|
contentColor = desireTone.onAccent
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text("Create session")
|
Text("Create session")
|
||||||
}
|
}
|
||||||
|
|
@ -587,6 +595,8 @@ private fun DSSetupScreen(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DSIntroScreen(total: Int, onReady: () -> Unit) {
|
private fun DSIntroScreen(total: Int, onReady: () -> Unit) {
|
||||||
|
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -598,8 +608,8 @@ private fun DSIntroScreen(total: Int, onReady: () -> Unit) {
|
||||||
) {
|
) {
|
||||||
StatusGlyph(
|
StatusGlyph(
|
||||||
icon = CloserGlyphs.HeartOutline,
|
icon = CloserGlyphs.HeartOutline,
|
||||||
tint = CloserPalette.Romantic,
|
tint = desireTone.accent,
|
||||||
container = CloserPalette.Romantic.copy(alpha = 0.12f)
|
container = desireTone.accentContainer
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -620,13 +630,18 @@ private fun DSIntroScreen(total: Int, onReady: () -> Unit) {
|
||||||
onClick = onReady,
|
onClick = onReady,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.Romantic)
|
colors = ButtonDefaults.buttonColors(
|
||||||
) { Text("I'm ready", color = Color.White) }
|
containerColor = desireTone.accent,
|
||||||
|
contentColor = desireTone.onAccent
|
||||||
|
)
|
||||||
|
) { Text("I'm ready") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DSWaitingScreen(partnerName: String, onBack: () -> Unit, onAbandon: () -> Unit = {}) {
|
private fun DSWaitingScreen(partnerName: String, onBack: () -> Unit, onAbandon: () -> Unit = {}) {
|
||||||
|
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -639,8 +654,8 @@ private fun DSWaitingScreen(partnerName: String, onBack: () -> Unit, onAbandon:
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
StatusGlyph(
|
StatusGlyph(
|
||||||
icon = CloserGlyphs.Heart,
|
icon = CloserGlyphs.Heart,
|
||||||
tint = CloserPalette.Romantic,
|
tint = desireTone.accent,
|
||||||
container = CloserPalette.Romantic.copy(alpha = 0.12f)
|
container = desireTone.accentContainer
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Your answers are in!",
|
text = "Your answers are in!",
|
||||||
|
|
@ -708,6 +723,7 @@ private fun DSAnswerScreen(
|
||||||
) {
|
) {
|
||||||
val config = question.answerConfig as? ChoiceAnswerConfigImpl
|
val config = question.answerConfig as? ChoiceAnswerConfigImpl
|
||||||
val options = config?.config?.options?.take(2) ?: return
|
val options = config?.config?.options?.take(2) ?: return
|
||||||
|
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -724,13 +740,13 @@ private fun DSAnswerScreen(
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(999.dp),
|
shape = RoundedCornerShape(999.dp),
|
||||||
color = CloserPalette.Romantic.copy(alpha = 0.12f)
|
color = desireTone.accentContainer
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "${index + 1} / $total",
|
text = "${index + 1} / $total",
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = CloserPalette.Romantic,
|
color = desireTone.onAccentContainer,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -769,7 +785,7 @@ private fun DSAnswerScreen(
|
||||||
options.forEach { option ->
|
options.forEach { option ->
|
||||||
val isSelected = pendingSelection == option.id
|
val isSelected = pendingSelection == option.id
|
||||||
val isPositive = option.id.lowercase() in POSITIVE_IDS
|
val isPositive = option.id.lowercase() in POSITIVE_IDS
|
||||||
val selectedColor = if (isPositive) CloserPalette.Romantic else Color(0xFF6B6B8A)
|
val selectedColor = if (isPositive) desireTone.accent else MaterialTheme.colorScheme.outline
|
||||||
val targetColor = when {
|
val targetColor = when {
|
||||||
isSelected -> selectedColor
|
isSelected -> selectedColor
|
||||||
pendingSelection != null -> MaterialTheme.colorScheme.surface.copy(alpha = 0.5f)
|
pendingSelection != null -> MaterialTheme.colorScheme.surface.copy(alpha = 0.5f)
|
||||||
|
|
@ -791,7 +807,11 @@ private fun DSAnswerScreen(
|
||||||
Text(
|
Text(
|
||||||
text = option.text,
|
text = option.text,
|
||||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
color = if (isSelected) Color.White else MaterialTheme.colorScheme.onSurface,
|
color = if (isSelected) {
|
||||||
|
if (isPositive) desireTone.onAccent else MaterialTheme.colorScheme.inverseOnSurface
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
},
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -811,6 +831,8 @@ private fun DSRevealScreen(
|
||||||
onOpenDaily: () -> Unit = {},
|
onOpenDaily: () -> Unit = {},
|
||||||
showNextBeat: Boolean = false
|
showNextBeat: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -827,8 +849,8 @@ private fun DSRevealScreen(
|
||||||
) {
|
) {
|
||||||
StatusGlyph(
|
StatusGlyph(
|
||||||
icon = if (matches.isEmpty()) CloserGlyphs.HeartOutline else CloserGlyphs.Heart,
|
icon = if (matches.isEmpty()) CloserGlyphs.HeartOutline else CloserGlyphs.Heart,
|
||||||
tint = CloserPalette.Romantic,
|
tint = desireTone.accent,
|
||||||
container = CloserPalette.Romantic.copy(alpha = 0.12f),
|
container = desireTone.accentContainer,
|
||||||
size = 82.dp,
|
size = 82.dp,
|
||||||
iconSize = 40.dp
|
iconSize = 40.dp
|
||||||
)
|
)
|
||||||
|
|
@ -855,7 +877,7 @@ private fun DSRevealScreen(
|
||||||
Text(
|
Text(
|
||||||
text = "You both said yes to",
|
text = "You both said yes to",
|
||||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
color = CloserPalette.Romantic
|
color = desireTone.accent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -890,8 +912,11 @@ private fun DSRevealScreen(
|
||||||
onClick = onPlayAgain,
|
onClick = onPlayAgain,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.Romantic)
|
colors = ButtonDefaults.buttonColors(
|
||||||
) { Text("Play again", color = Color.White) }
|
containerColor = desireTone.accent,
|
||||||
|
contentColor = desireTone.onAccent
|
||||||
|
)
|
||||||
|
) { Text("Play again") }
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = onHome,
|
onClick = onHome,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
||||||
|
|
@ -910,18 +935,20 @@ private fun DesireProgressPill(
|
||||||
progress: Float,
|
progress: Float,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.height(8.dp)
|
.height(8.dp)
|
||||||
.clip(RoundedCornerShape(999.dp))
|
.clip(RoundedCornerShape(999.dp))
|
||||||
.background(CloserPalette.Romantic.copy(alpha = 0.15f))
|
.background(desireTone.accentContainer)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.fillMaxWidth(progress.coerceIn(0f, 1f))
|
.fillMaxWidth(progress.coerceIn(0f, 1f))
|
||||||
.clip(RoundedCornerShape(999.dp))
|
.clip(RoundedCornerShape(999.dp))
|
||||||
.background(CloserPalette.Romantic)
|
.background(desireTone.accent)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -932,6 +959,8 @@ private fun DesireRevealMeter(
|
||||||
total: Int,
|
total: Int,
|
||||||
partnerName: String
|
partnerName: String
|
||||||
) {
|
) {
|
||||||
|
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(22.dp),
|
shape = RoundedCornerShape(22.dp),
|
||||||
|
|
@ -960,8 +989,8 @@ private fun DesireRevealMeter(
|
||||||
)
|
)
|
||||||
StatusGlyph(
|
StatusGlyph(
|
||||||
icon = CloserGlyphs.Sync,
|
icon = CloserGlyphs.Sync,
|
||||||
tint = CloserPalette.Romantic,
|
tint = desireTone.accent,
|
||||||
container = CloserPalette.Romantic.copy(alpha = 0.12f),
|
container = desireTone.accentContainer,
|
||||||
size = 38.dp,
|
size = 38.dp,
|
||||||
iconSize = 20.dp
|
iconSize = 20.dp
|
||||||
)
|
)
|
||||||
|
|
@ -978,7 +1007,7 @@ private fun DesireRevealMeter(
|
||||||
Text(
|
Text(
|
||||||
text = "$matches shared, ${total - matches} kept private",
|
text = "$matches shared, ${total - matches} kept private",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = CloserPalette.Romantic,
|
color = desireTone.accent,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
|
@ -993,10 +1022,12 @@ private fun DesirePrivacyTile(
|
||||||
value: String,
|
value: String,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
color = CloserPalette.Romantic.copy(alpha = 0.08f)
|
color = desireTone.accentContainer.copy(alpha = 0.55f)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
|
|
@ -1013,7 +1044,7 @@ private fun DesirePrivacyTile(
|
||||||
Text(
|
Text(
|
||||||
text = value,
|
text = value,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = CloserPalette.Romantic,
|
color = desireTone.accent,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
|
|
@ -1024,11 +1055,13 @@ private fun DesirePrivacyTile(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DesireMatchCard(match: DesireMatch) {
|
private fun DesireMatchCard(match: DesireMatch) {
|
||||||
|
val desireTone = closerToneColors(CloserExperienceTone.DesireSync)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = CloserPalette.Romantic.copy(alpha = 0.10f)
|
containerColor = desireTone.accentContainer.copy(alpha = 0.62f)
|
||||||
),
|
),
|
||||||
elevation = CardDefaults.cardElevation(0.dp)
|
elevation = CardDefaults.cardElevation(0.dp)
|
||||||
) {
|
) {
|
||||||
|
|
@ -1039,15 +1072,15 @@ private fun DesireMatchCard(match: DesireMatch) {
|
||||||
) {
|
) {
|
||||||
StatusGlyph(
|
StatusGlyph(
|
||||||
icon = CloserGlyphs.Heart,
|
icon = CloserGlyphs.Heart,
|
||||||
tint = CloserPalette.Romantic,
|
tint = desireTone.accent,
|
||||||
container = CloserPalette.Romantic.copy(alpha = 0.12f),
|
container = desireTone.accentContainer,
|
||||||
size = 34.dp,
|
size = 34.dp,
|
||||||
iconSize = 18.dp
|
iconSize = 18.dp
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = match.question.text,
|
text = match.question.text,
|
||||||
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium),
|
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium),
|
||||||
// Was a hardcoded dark plum (Color(0xFF3D1F2E)) — fine on the light card in
|
// Was a hardcoded dark plum — fine on the light card in
|
||||||
// light mode, but dim/low-contrast on the dark-tinted card in dark mode
|
// light mode, but dim/low-contrast on the dark-tinted card in dark mode
|
||||||
// (C-DS-001). onSurface adapts: dark text on light, light text on dark.
|
// (C-DS-001). onSurface adapts: dark text on light, light text on dark.
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
|
@ -1059,6 +1092,29 @@ private fun DesireMatchCard(match: DesireMatch) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DesireSyncPreviewBody() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(closerBackgroundBrush())
|
||||||
|
) {
|
||||||
|
DSIntroScreen(total = 5, onReady = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "DesireSync • light")
|
||||||
|
@Composable
|
||||||
|
private fun DesireSyncPreviewLight() {
|
||||||
|
CloserTheme(darkTheme = false) { DesireSyncPreviewBody() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "DesireSync • dark")
|
||||||
|
@Composable
|
||||||
|
private fun DesireSyncPreviewDark() {
|
||||||
|
CloserTheme(darkTheme = true) { DesireSyncPreviewBody() }
|
||||||
|
}
|
||||||
|
|
||||||
// ── History Replay ────────────────────────────────────────────────────────────
|
// ── History Replay ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
sealed interface DSReplayPhase {
|
sealed interface DSReplayPhase {
|
||||||
|
|
|
||||||
|
|
@ -282,7 +282,7 @@ private fun PartnerHomeContent(
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = CloserPalette.PurpleDeep,
|
containerColor = CloserPalette.PurpleDeep,
|
||||||
contentColor = Color.White
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("View today's question", fontWeight = FontWeight.SemiBold)
|
Text("View today's question", fontWeight = FontWeight.SemiBold)
|
||||||
|
|
|
||||||
|
|
@ -107,31 +107,31 @@ fun HomeActionTone.actionColors(): HomeActionColors {
|
||||||
soft = CloserPalette.PinkSoft,
|
soft = CloserPalette.PinkSoft,
|
||||||
accent = CloserPalette.PinkBright,
|
accent = CloserPalette.PinkBright,
|
||||||
deep = CloserPalette.PinkAccentDeep,
|
deep = CloserPalette.PinkAccentDeep,
|
||||||
onAccent = Color(0xFF24122F)
|
onAccent = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
HomeActionTone.Reflection -> HomeActionColors(
|
HomeActionTone.Reflection -> HomeActionColors(
|
||||||
soft = CloserPalette.PurpleGlow,
|
soft = CloserPalette.PurpleGlow,
|
||||||
accent = CloserPalette.PurpleRich,
|
accent = CloserPalette.PurpleRich,
|
||||||
deep = CloserPalette.PurpleDeep,
|
deep = CloserPalette.PurpleDeep,
|
||||||
onAccent = Color(0xFF24122F)
|
onAccent = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
)
|
)
|
||||||
HomeActionTone.Ritual -> HomeActionColors(
|
HomeActionTone.Ritual -> HomeActionColors(
|
||||||
soft = CloserPalette.PurpleSoft,
|
soft = CloserPalette.PurpleSoft,
|
||||||
accent = CloserPalette.PinkBright,
|
accent = CloserPalette.PinkBright,
|
||||||
deep = CloserPalette.PinkAccentDeep,
|
deep = CloserPalette.PinkAccentDeep,
|
||||||
onAccent = Color(0xFF24122F)
|
onAccent = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
HomeActionTone.Starter -> HomeActionColors(
|
HomeActionTone.Starter -> HomeActionColors(
|
||||||
soft = CloserPalette.PinkSoft,
|
soft = CloserPalette.PinkSoft,
|
||||||
accent = CloserPalette.PinkBright,
|
accent = CloserPalette.PinkBright,
|
||||||
deep = CloserPalette.PinkAccentDeep,
|
deep = CloserPalette.PinkAccentDeep,
|
||||||
onAccent = Color(0xFF24122F)
|
onAccent = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
HomeActionTone.Pack -> HomeActionColors(
|
HomeActionTone.Pack -> HomeActionColors(
|
||||||
soft = CloserPalette.PinkSoft,
|
soft = CloserPalette.PinkSoft,
|
||||||
accent = CloserPalette.PinkBright,
|
accent = CloserPalette.PinkBright,
|
||||||
deep = CloserPalette.PinkAccentDeep,
|
deep = CloserPalette.PinkAccentDeep,
|
||||||
onAccent = Color(0xFF24122F)
|
onAccent = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
HomeActionTone.Utility -> HomeActionColors(
|
HomeActionTone.Utility -> HomeActionColors(
|
||||||
soft = CloserPalette.PurpleSoft,
|
soft = CloserPalette.PurpleSoft,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
|
@ -125,7 +124,7 @@ private fun PartnerHeaderBubble(
|
||||||
) {
|
) {
|
||||||
// Brand gradient ring around the avatar — signals "your partner" + that it's tappable.
|
// Brand gradient ring around the avatar — signals "your partner" + that it's tappable.
|
||||||
val ringBrush = Brush.linearGradient(
|
val ringBrush = Brush.linearGradient(
|
||||||
listOf(CloserPalette.PurpleRich, CloserPalette.PinkBright)
|
listOf(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.secondary)
|
||||||
)
|
)
|
||||||
val label = when {
|
val label = when {
|
||||||
!isPaired -> "Invite your partner"
|
!isPaired -> "Invite your partner"
|
||||||
|
|
@ -327,12 +326,12 @@ private fun PartnerSheetAction(
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (enabled) 1f else 0.5f)
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (enabled) 1f else 0.5f)
|
||||||
)
|
)
|
||||||
if (trailing != null) {
|
if (trailing != null) {
|
||||||
Surface(shape = CircleShape, color = CloserPalette.PinkBright) {
|
Surface(shape = CircleShape, color = MaterialTheme.colorScheme.secondaryContainer) {
|
||||||
Text(
|
Text(
|
||||||
text = trailing,
|
text = trailing,
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp),
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = Color.White
|
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package app.closer.ui.home.components
|
package app.closer.ui.home.components
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
|
@ -15,12 +14,12 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import app.closer.R
|
import app.closer.R
|
||||||
|
import app.closer.ui.components.BrandIllustration
|
||||||
import app.closer.ui.components.CelebrationOverlay
|
import app.closer.ui.components.CelebrationOverlay
|
||||||
import app.closer.ui.components.CloserActionButton
|
import app.closer.ui.components.CloserActionButton
|
||||||
import app.closer.ui.settings.SettingsInk
|
import app.closer.ui.settings.SettingsInk
|
||||||
|
|
@ -48,10 +47,11 @@ internal fun StreakMilestoneDialog(
|
||||||
modifier = Modifier.padding(horizontal = 28.dp, vertical = 30.dp),
|
modifier = Modifier.padding(horizontal = 28.dp, vertical = 30.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Image(
|
BrandIllustration(
|
||||||
painter = painterResource(R.drawable.illustration_streak_milestone),
|
res = R.drawable.illustration_streak_milestone,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(156.dp)
|
modifier = Modifier.size(156.dp),
|
||||||
|
tile = false
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(18.dp))
|
Spacer(Modifier.height(18.dp))
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
|
@ -78,8 +79,11 @@ import app.closer.ui.games.GameCopy
|
||||||
import app.closer.ui.components.BrandMessageRotator
|
import app.closer.ui.components.BrandMessageRotator
|
||||||
import app.closer.ui.components.ResultGlyph
|
import app.closer.ui.components.ResultGlyph
|
||||||
import app.closer.ui.components.StatusGlyph
|
import app.closer.ui.components.StatusGlyph
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
|
import app.closer.ui.theme.CloserTheme
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
|
import app.closer.ui.theme.closerStatusColors
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
@ -585,6 +589,8 @@ private fun HWSetupScreen(
|
||||||
onStart: () -> Unit,
|
onStart: () -> Unit,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -617,16 +623,16 @@ private fun HWSetupScreen(
|
||||||
onClick = { onLengthSelected(len) },
|
onClick = { onLengthSelected(len) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = if (len == selectedLength) CloserPalette.PurpleDeep else Color.Transparent,
|
color = if (len == selectedLength) howWellTone.accent else Color.Transparent,
|
||||||
border = if (len != selectedLength)
|
border = if (len != selectedLength)
|
||||||
BorderStroke(1.dp, CloserPalette.PurpleDeep.copy(alpha = 0.4f))
|
BorderStroke(1.dp, howWellTone.accent.copy(alpha = 0.4f))
|
||||||
else null
|
else null
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = len.label,
|
text = len.label,
|
||||||
modifier = Modifier.padding(vertical = 10.dp),
|
modifier = Modifier.padding(vertical = 10.dp),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = if (len == selectedLength) Color.White else CloserPalette.PurpleDeep,
|
color = if (len == selectedLength) howWellTone.onAccent else howWellTone.accent,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -637,7 +643,10 @@ private fun HWSetupScreen(
|
||||||
onClick = onStart,
|
onClick = onStart,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = howWellTone.accent,
|
||||||
|
contentColor = howWellTone.onAccent
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text("Create session")
|
Text("Create session")
|
||||||
}
|
}
|
||||||
|
|
@ -651,6 +660,8 @@ private fun PlayerIntroScreen(
|
||||||
total: Int,
|
total: Int,
|
||||||
onReady: () -> Unit
|
onReady: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -662,8 +673,8 @@ private fun PlayerIntroScreen(
|
||||||
) {
|
) {
|
||||||
StatusGlyph(
|
StatusGlyph(
|
||||||
icon = if (amSubject) CloserGlyphs.Person else CloserGlyphs.Psychology,
|
icon = if (amSubject) CloserGlyphs.Person else CloserGlyphs.Psychology,
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = howWellTone.accent,
|
||||||
container = CloserPalette.PurpleMist
|
container = howWellTone.accentContainer
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -691,13 +702,18 @@ private fun PlayerIntroScreen(
|
||||||
onClick = onReady,
|
onClick = onReady,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
|
colors = ButtonDefaults.buttonColors(
|
||||||
) { Text("I'm ready", color = Color.White) }
|
containerColor = howWellTone.accent,
|
||||||
|
contentColor = howWellTone.onAccent
|
||||||
|
)
|
||||||
|
) { Text("I'm ready") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HowWellWaitingScreen(amSubject: Boolean, partnerName: String, onBack: () -> Unit, onAbandon: () -> Unit = {}) {
|
private fun HowWellWaitingScreen(amSubject: Boolean, partnerName: String, onBack: () -> Unit, onAbandon: () -> Unit = {}) {
|
||||||
|
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -710,8 +726,8 @@ private fun HowWellWaitingScreen(amSubject: Boolean, partnerName: String, onBack
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
StatusGlyph(
|
StatusGlyph(
|
||||||
icon = CloserGlyphs.Sync,
|
icon = CloserGlyphs.Sync,
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = howWellTone.accent,
|
||||||
container = CloserPalette.PurpleMist
|
container = howWellTone.accentContainer
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "All done on your side!",
|
text = "All done on your side!",
|
||||||
|
|
@ -786,6 +802,7 @@ private fun AnswerScreen(
|
||||||
onQuit: () -> Unit
|
onQuit: () -> Unit
|
||||||
) {
|
) {
|
||||||
val hasSelection = selectedOptionId != null || selectedScale != null
|
val hasSelection = selectedOptionId != null || selectedScale != null
|
||||||
|
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -800,12 +817,12 @@ private fun AnswerScreen(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Surface(shape = RoundedCornerShape(999.dp), color = CloserPalette.PurpleMist) {
|
Surface(shape = RoundedCornerShape(999.dp), color = howWellTone.accentContainer) {
|
||||||
Text(
|
Text(
|
||||||
text = "${if (isGuesser) "Your guess" else "About you"} · ${index + 1} / $total",
|
text = "${if (isGuesser) "Your guess" else "About you"} · ${index + 1} / $total",
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = CloserPalette.PurpleDeep,
|
color = howWellTone.onAccentContainer,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -831,12 +848,12 @@ private fun AnswerScreen(
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
if (isGuesser) {
|
if (isGuesser) {
|
||||||
Surface(shape = RoundedCornerShape(999.dp), color = CloserPalette.PurpleMist) {
|
Surface(shape = RoundedCornerShape(999.dp), color = howWellTone.accentContainer) {
|
||||||
Text(
|
Text(
|
||||||
text = "How did $partnerName answer?",
|
text = "How did $partnerName answer?",
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 5.dp),
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 5.dp),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = CloserPalette.PurpleDeep,
|
color = howWellTone.onAccentContainer,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -881,11 +898,13 @@ private fun AnswerScreen(
|
||||||
enabled = hasSelection,
|
enabled = hasSelection,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = howWellTone.accent,
|
||||||
|
contentColor = howWellTone.onAccent
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (index + 1 >= total) "Done →" else "Confirm →",
|
text = if (index + 1 >= total) "Done →" else "Confirm →"
|
||||||
color = Color.White
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -904,6 +923,8 @@ private fun CompleteScreen(
|
||||||
onOpenDaily: () -> Unit = {},
|
onOpenDaily: () -> Unit = {},
|
||||||
showNextBeat: Boolean = false
|
showNextBeat: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -963,8 +984,11 @@ private fun CompleteScreen(
|
||||||
onClick = onSwapRoles,
|
onClick = onSwapRoles,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
|
colors = ButtonDefaults.buttonColors(
|
||||||
) { Text("Your turn — let $partnerName guess you", color = Color.White) }
|
containerColor = howWellTone.accent,
|
||||||
|
contentColor = howWellTone.onAccent
|
||||||
|
)
|
||||||
|
) { Text("Your turn — let $partnerName guess you") }
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = onPlayAgain,
|
onClick = onPlayAgain,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
||||||
|
|
@ -975,8 +999,11 @@ private fun CompleteScreen(
|
||||||
onClick = onPlayAgain,
|
onClick = onPlayAgain,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
|
colors = ButtonDefaults.buttonColors(
|
||||||
) { Text("Play again", color = Color.White) }
|
containerColor = howWellTone.accent,
|
||||||
|
contentColor = howWellTone.onAccent
|
||||||
|
)
|
||||||
|
) { Text("Play again") }
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = onHome,
|
onClick = onHome,
|
||||||
|
|
@ -996,18 +1023,20 @@ private fun HowWellProgressPill(
|
||||||
progress: Float,
|
progress: Float,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.height(8.dp)
|
.height(8.dp)
|
||||||
.clip(RoundedCornerShape(999.dp))
|
.clip(RoundedCornerShape(999.dp))
|
||||||
.background(CloserPalette.PurpleMist)
|
.background(howWellTone.accentContainer)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.fillMaxWidth(progress.coerceIn(0f, 1f))
|
.fillMaxWidth(progress.coerceIn(0f, 1f))
|
||||||
.clip(RoundedCornerShape(999.dp))
|
.clip(RoundedCornerShape(999.dp))
|
||||||
.background(CloserPalette.PurpleDeep)
|
.background(howWellTone.accent)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1019,6 +1048,8 @@ private fun HowWellScoreRing(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val progress = if (total == 0) 0f else score.toFloat() / total
|
val progress = if (total == 0) 0f else score.toFloat() / total
|
||||||
|
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier.semantics(mergeDescendants = true) {
|
modifier = modifier.semantics(mergeDescendants = true) {
|
||||||
contentDescription = "You guessed $score of $total correctly"
|
contentDescription = "You guessed $score of $total correctly"
|
||||||
|
|
@ -1028,14 +1059,14 @@ private fun HowWellScoreRing(
|
||||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
val strokeWidth = 11.dp.toPx()
|
val strokeWidth = 11.dp.toPx()
|
||||||
drawArc(
|
drawArc(
|
||||||
color = CloserPalette.PurpleMist,
|
color = howWellTone.accentContainer,
|
||||||
startAngle = -90f,
|
startAngle = -90f,
|
||||||
sweepAngle = 360f,
|
sweepAngle = 360f,
|
||||||
useCenter = false,
|
useCenter = false,
|
||||||
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
|
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
|
||||||
)
|
)
|
||||||
drawArc(
|
drawArc(
|
||||||
color = CloserPalette.PurpleDeep,
|
color = howWellTone.accent,
|
||||||
startAngle = -90f,
|
startAngle = -90f,
|
||||||
sweepAngle = 360f * progress.coerceIn(0f, 1f),
|
sweepAngle = 360f * progress.coerceIn(0f, 1f),
|
||||||
useCenter = false,
|
useCenter = false,
|
||||||
|
|
@ -1046,7 +1077,7 @@ private fun HowWellScoreRing(
|
||||||
Text(
|
Text(
|
||||||
text = "$score",
|
text = "$score",
|
||||||
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
|
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
|
||||||
color = CloserPalette.PurpleDeep
|
color = howWellTone.accent
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "of $total",
|
text = "of $total",
|
||||||
|
|
@ -1060,14 +1091,12 @@ private fun HowWellScoreRing(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BreakdownRow(result: HowWellResult) {
|
private fun BreakdownRow(result: HowWellResult) {
|
||||||
// Match-row colors come as a container+content PAIR per theme: the old fixed pale-green
|
val statusColors = closerStatusColors()
|
||||||
// container kept theme-driven onSurfaceVariant text, which is near-invisible in dark mode.
|
val matchColor = statusColors.success
|
||||||
val dark = androidx.compose.foundation.isSystemInDarkTheme()
|
val closeColor = statusColors.warning
|
||||||
val matchColor = if (dark) Color(0xFF81C995) else Color(0xFF2E7D32)
|
val missColor = statusColors.danger
|
||||||
val closeColor = if (dark) Color(0xFFF6C453) else Color(0xFFF57F17)
|
val matchContainer = statusColors.successContainer
|
||||||
val missColor = if (dark) Color(0xFFE58A82) else Color(0xFFC62828)
|
val questionColor = if (result.isMatch) statusColors.success else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
val matchContainer = if (dark) Color(0xFF223A2A) else Color(0xFFE8F5E9)
|
|
||||||
val questionColor = if (result.isMatch && dark) Color(0xFFB9CBBE) else MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
|
@ -1136,6 +1165,8 @@ private fun SingleChoiceInput(
|
||||||
selectedId: String?,
|
selectedId: String?,
|
||||||
onSelect: (String) -> Unit
|
onSelect: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
|
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
options.forEach { option ->
|
options.forEach { option ->
|
||||||
val selected = selectedId == option.id
|
val selected = selectedId == option.id
|
||||||
|
|
@ -1144,7 +1175,7 @@ private fun SingleChoiceInput(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = if (selected) CloserPalette.PurpleDeep else MaterialTheme.colorScheme.surface
|
containerColor = if (selected) howWellTone.accent else MaterialTheme.colorScheme.surface
|
||||||
),
|
),
|
||||||
elevation = CardDefaults.cardElevation(if (selected) 6.dp else 2.dp)
|
elevation = CardDefaults.cardElevation(if (selected) 6.dp else 2.dp)
|
||||||
) {
|
) {
|
||||||
|
|
@ -1152,7 +1183,7 @@ private fun SingleChoiceInput(
|
||||||
text = option.text,
|
text = option.text,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||||
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium),
|
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium),
|
||||||
color = if (selected) Color.White else MaterialTheme.colorScheme.onSurface,
|
color = if (selected) howWellTone.onAccent else MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
|
@ -1168,6 +1199,8 @@ private fun BinaryChoiceInput(
|
||||||
selectedId: String?,
|
selectedId: String?,
|
||||||
onSelect: (String) -> Unit
|
onSelect: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
|
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
listOf(optionA to "A", optionB to "B").forEach { (option, label) ->
|
listOf(optionA to "A", optionB to "B").forEach { (option, label) ->
|
||||||
val selected = selectedId == option.id
|
val selected = selectedId == option.id
|
||||||
|
|
@ -1176,7 +1209,7 @@ private fun BinaryChoiceInput(
|
||||||
modifier = Modifier.weight(1f).heightIn(min = 80.dp),
|
modifier = Modifier.weight(1f).heightIn(min = 80.dp),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = if (selected) CloserPalette.PurpleDeep else MaterialTheme.colorScheme.surface
|
containerColor = if (selected) howWellTone.accent else MaterialTheme.colorScheme.surface
|
||||||
),
|
),
|
||||||
elevation = CardDefaults.cardElevation(if (selected) 6.dp else 2.dp)
|
elevation = CardDefaults.cardElevation(if (selected) 6.dp else 2.dp)
|
||||||
) {
|
) {
|
||||||
|
|
@ -1189,12 +1222,12 @@ private fun BinaryChoiceInput(
|
||||||
text = label,
|
text = label,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = if (selected) Color.White.copy(alpha = 0.7f) else MaterialTheme.colorScheme.onSurfaceVariant
|
color = if (selected) howWellTone.onAccent.copy(alpha = 0.7f) else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = option.text,
|
text = option.text,
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
color = if (selected) Color.White else MaterialTheme.colorScheme.onSurface,
|
color = if (selected) howWellTone.onAccent else MaterialTheme.colorScheme.onSurface,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
|
|
@ -1214,6 +1247,8 @@ private fun ScaleInput(
|
||||||
selected: Int?,
|
selected: Int?,
|
||||||
onSelect: (Int) -> Unit
|
onSelect: (Int) -> Unit
|
||||||
) {
|
) {
|
||||||
|
val howWellTone = closerToneColors(CloserExperienceTone.HowWell)
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|
@ -1224,7 +1259,7 @@ private fun ScaleInput(
|
||||||
Surface(
|
Surface(
|
||||||
onClick = { onSelect(value) },
|
onClick = { onSelect(value) },
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
color = if (isSelected) CloserPalette.PurpleDeep else MaterialTheme.colorScheme.surface,
|
color = if (isSelected) howWellTone.accent else MaterialTheme.colorScheme.surface,
|
||||||
modifier = Modifier.size(52.dp),
|
modifier = Modifier.size(52.dp),
|
||||||
shadowElevation = if (isSelected) 6.dp else 2.dp
|
shadowElevation = if (isSelected) 6.dp else 2.dp
|
||||||
) {
|
) {
|
||||||
|
|
@ -1232,7 +1267,7 @@ private fun ScaleInput(
|
||||||
Text(
|
Text(
|
||||||
text = "$value",
|
text = "$value",
|
||||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
color = if (isSelected) Color.White else MaterialTheme.colorScheme.onSurface
|
color = if (isSelected) howWellTone.onAccent else MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1255,6 +1290,34 @@ private fun scoreLabel(score: Int, total: Int): String = when {
|
||||||
else -> "Room to grow"
|
else -> "Room to grow"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HowWellPreviewBody() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(closerBackgroundBrush())
|
||||||
|
) {
|
||||||
|
PlayerIntroScreen(
|
||||||
|
amSubject = false,
|
||||||
|
partnerName = "Sam",
|
||||||
|
total = 5,
|
||||||
|
onReady = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "HowWell • light")
|
||||||
|
@Composable
|
||||||
|
private fun HowWellPreviewLight() {
|
||||||
|
CloserTheme(darkTheme = false) { HowWellPreviewBody() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "HowWell • dark")
|
||||||
|
@Composable
|
||||||
|
private fun HowWellPreviewDark() {
|
||||||
|
CloserTheme(darkTheme = true) { HowWellPreviewBody() }
|
||||||
|
}
|
||||||
|
|
||||||
// ── History Replay ────────────────────────────────────────────────────────────
|
// ── History Replay ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
sealed interface HWReplayPhase {
|
sealed interface HWReplayPhase {
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,10 @@ import app.closer.data.remote.FirestoreCapsuleDataSource
|
||||||
import app.closer.domain.model.TimeCapsule
|
import app.closer.domain.model.TimeCapsule
|
||||||
import app.closer.domain.repository.AuthRepository
|
import app.closer.domain.repository.AuthRepository
|
||||||
import app.closer.domain.repository.CoupleRepository
|
import app.closer.domain.repository.CoupleRepository
|
||||||
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserPalette
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
@ -445,6 +447,8 @@ fun MemoryLaneScreen(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) {
|
private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) {
|
||||||
|
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -454,12 +458,12 @@ private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Surface(shape = RoundedCornerShape(50.dp), color = CloserPalette.Romantic.copy(alpha = 0.12f)) {
|
Surface(shape = RoundedCornerShape(50.dp), color = premiumTone.accentContainer) {
|
||||||
Icon(
|
Icon(
|
||||||
CloserGlyphs.Lock,
|
CloserGlyphs.Lock,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.padding(20.dp).size(32.dp),
|
modifier = Modifier.padding(20.dp).size(32.dp),
|
||||||
tint = CloserPalette.Romantic
|
tint = premiumTone.accent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
|
|
@ -467,8 +471,16 @@ private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) {
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
Text("Write notes to your future selves — sealed until the date you choose.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
|
Text("Write notes to your future selves — sealed until the date you choose.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
|
||||||
Spacer(Modifier.height(28.dp))
|
Spacer(Modifier.height(28.dp))
|
||||||
Button(onClick = onUnlock, modifier = Modifier.fillMaxWidth().heightIn(min = 54.dp), shape = RoundedCornerShape(18.dp), colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.Romantic)) {
|
Button(
|
||||||
Text("Unlock premium", color = Color.White, style = MaterialTheme.typography.labelLarge)
|
onClick = onUnlock,
|
||||||
|
modifier = Modifier.fillMaxWidth().heightIn(min = 54.dp),
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = premiumTone.accent,
|
||||||
|
contentColor = premiumTone.onAccent
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Unlock premium", style = MaterialTheme.typography.labelLarge)
|
||||||
}
|
}
|
||||||
TextButton(onClick = onBack) { Text("Go back") }
|
TextButton(onClick = onBack) { Text("Go back") }
|
||||||
}
|
}
|
||||||
|
|
@ -478,6 +490,8 @@ private fun MemoryLaneLockedScreen(onBack: () -> Unit, onUnlock: () -> Unit) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MemoryLaneErrorScreen(message: String, onBack: () -> Unit, onRetry: () -> Unit) {
|
private fun MemoryLaneErrorScreen(message: String, onBack: () -> Unit, onRetry: () -> Unit) {
|
||||||
|
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -491,8 +505,15 @@ private fun MemoryLaneErrorScreen(message: String, onBack: () -> Unit, onRetry:
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Text(message, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
|
Text(message, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
Button(onClick = onRetry, shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)) {
|
Button(
|
||||||
Text("Try again", color = Color.White)
|
onClick = onRetry,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = revealTone.accent,
|
||||||
|
contentColor = revealTone.onAccent
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Try again")
|
||||||
}
|
}
|
||||||
TextButton(onClick = onBack) { Text("Go back") }
|
TextButton(onClick = onBack) { Text("Go back") }
|
||||||
}
|
}
|
||||||
|
|
@ -507,6 +528,8 @@ private fun CapsuleListScreen(
|
||||||
onNew: () -> Unit,
|
onNew: () -> Unit,
|
||||||
onOpen: (TimeCapsule) -> Unit
|
onOpen: (TimeCapsule) -> Unit
|
||||||
) {
|
) {
|
||||||
|
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -529,8 +552,8 @@ private fun CapsuleListScreen(
|
||||||
Text("Notes that open on a future date.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text("Notes that open on a future date.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onNew) {
|
IconButton(onClick = onNew) {
|
||||||
Surface(shape = RoundedCornerShape(12.dp), color = CloserPalette.PurpleDeep) {
|
Surface(shape = RoundedCornerShape(12.dp), color = revealTone.accent) {
|
||||||
Icon(CloserGlyphs.Add, contentDescription = "New capsule", tint = Color.White, modifier = Modifier.padding(6.dp).size(20.dp))
|
Icon(CloserGlyphs.Add, contentDescription = "New capsule", tint = revealTone.onAccent, modifier = Modifier.padding(6.dp).size(20.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -553,8 +576,15 @@ private fun CapsuleListScreen(
|
||||||
Text("No capsules yet", style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), color = MaterialTheme.colorScheme.onSurface)
|
Text("No capsules yet", style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), color = MaterialTheme.colorScheme.onSurface)
|
||||||
Text("Write a note to your future selves — it'll stay sealed until the date you choose.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
|
Text("Write a note to your future selves — it'll stay sealed until the date you choose.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Button(onClick = onNew, shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)) {
|
Button(
|
||||||
Text("Write your first capsule", color = Color.White)
|
onClick = onNew,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = revealTone.accent,
|
||||||
|
contentColor = revealTone.onAccent
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Write your first capsule")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -635,6 +665,8 @@ private fun CapsuleCreateScreen(
|
||||||
onHideDatePicker: () -> Unit,
|
onHideDatePicker: () -> Unit,
|
||||||
onSave: () -> Unit
|
onSave: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val revealTone = closerToneColors(CloserExperienceTone.Reveal)
|
||||||
|
|
||||||
if (state.showDatePicker) {
|
if (state.showDatePicker) {
|
||||||
val pickerState = rememberDatePickerState(
|
val pickerState = rememberDatePickerState(
|
||||||
initialSelectedDateMillis = state.customUnlockMs ?: (System.currentTimeMillis() + UnlockPreset.ONE_YEAR.ms)
|
initialSelectedDateMillis = state.customUnlockMs ?: (System.currentTimeMillis() + UnlockPreset.ONE_YEAR.ms)
|
||||||
|
|
@ -774,10 +806,13 @@ private fun CapsuleCreateScreen(
|
||||||
enabled = !state.isSaving,
|
enabled = !state.isSaving,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 54.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 54.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = revealTone.accent,
|
||||||
|
contentColor = revealTone.onAccent
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
if (state.isSaving) CloserHeartLoader(size = 22.dp)
|
if (state.isSaving) CloserHeartLoader(size = 22.dp)
|
||||||
else Text(if (isEditing) "Save changes" else "Seal the capsule 📦", color = Color.White, style = MaterialTheme.typography.labelLarge)
|
else Text(if (isEditing) "Save changes" else "Seal the capsule 📦", style = MaterialTheme.typography.labelLarge)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ import app.closer.ui.auth.AuthPrimaryDeep
|
||||||
import app.closer.ui.components.BrandMessageRotator
|
import app.closer.ui.components.BrandMessageRotator
|
||||||
import app.closer.ui.components.CloserHeartLoader
|
import app.closer.ui.components.CloserHeartLoader
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserPalette
|
||||||
|
import app.closer.ui.theme.closerStatusColors
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private const val CTA_PAGE = 3
|
private const val CTA_PAGE = 3
|
||||||
|
|
@ -388,6 +389,8 @@ private fun RevealCard(label: String, preview: String, revealed: Boolean, color:
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun GrowerVisual() {
|
private fun GrowerVisual() {
|
||||||
|
val statusColors = closerStatusColors()
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
|
@ -400,7 +403,7 @@ private fun GrowerVisual() {
|
||||||
GrowthPill("Desire Sync", CloserPalette.PinkMist, CloserPalette.Romantic)
|
GrowthPill("Desire Sync", CloserPalette.PinkMist, CloserPalette.Romantic)
|
||||||
GrowthPill("Memory Lane", CloserPalette.PurpleGlow, CloserPalette.PurpleDeep)
|
GrowthPill("Memory Lane", CloserPalette.PurpleGlow, CloserPalette.PurpleDeep)
|
||||||
}
|
}
|
||||||
GrowthPill("Question threads", Color(0xFFE8F5E9), Color(0xFF2E7D32))
|
GrowthPill("Question threads", statusColors.successContainer, statusColors.success)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import app.closer.ui.settings.SettingsMuted
|
||||||
import app.closer.ui.settings.SettingsPrimaryDeep
|
import app.closer.ui.settings.SettingsPrimaryDeep
|
||||||
import app.closer.ui.settings.SettingsSoft
|
import app.closer.ui.settings.SettingsSoft
|
||||||
import app.closer.ui.components.CloserGlyphs
|
import app.closer.ui.components.CloserGlyphs
|
||||||
|
import app.closer.ui.theme.closerStatusColors
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -206,9 +207,10 @@ private fun DeltaCard(baseline: Outcome, latest: Outcome) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DeltaRow(label: String, delta: Int) {
|
private fun DeltaRow(label: String, delta: Int) {
|
||||||
|
val statusColors = closerStatusColors()
|
||||||
val color = when {
|
val color = when {
|
||||||
delta > 0 -> SettingsPrimaryDeep
|
delta > 0 -> SettingsPrimaryDeep
|
||||||
delta < 0 -> Color(0xFFB00020)
|
delta < 0 -> statusColors.danger
|
||||||
else -> SettingsMuted
|
else -> SettingsMuted
|
||||||
}
|
}
|
||||||
val sign = when {
|
val sign = when {
|
||||||
|
|
|
||||||
|
|
@ -326,7 +326,7 @@ private fun PairAvatar(url: String, modifier: Modifier = Modifier) {
|
||||||
error = rememberVectorPainter(CloserGlyphs.Person),
|
error = rememberVectorPainter(CloserGlyphs.Person),
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.border(4.dp, Color.White, CircleShape)
|
.border(4.dp, MaterialTheme.colorScheme.surface, CircleShape)
|
||||||
.background(SettingsSoft)
|
.background(SettingsSoft)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -334,7 +334,7 @@ private fun PairAvatar(url: String, modifier: Modifier = Modifier) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.border(4.dp, Color.White, CircleShape)
|
.border(4.dp, MaterialTheme.colorScheme.surface, CircleShape)
|
||||||
.background(SettingsSoft),
|
.background(SettingsSoft),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import app.closer.ui.theme.closerSoftSurfaceColor
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -49,12 +48,8 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import app.closer.ui.components.BrandIllustration
|
import app.closer.ui.components.BrandIllustration
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -67,7 +62,8 @@ import app.closer.core.navigation.ExternalLinks
|
||||||
import app.closer.domain.repository.BillingState
|
import app.closer.domain.repository.BillingState
|
||||||
import app.closer.ui.components.ErrorState
|
import app.closer.ui.components.ErrorState
|
||||||
import app.closer.ui.components.LoadingState
|
import app.closer.ui.components.LoadingState
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
import com.revenuecat.purchases.Package
|
import com.revenuecat.purchases.Package
|
||||||
import app.closer.ui.brand.CloserCopy
|
import app.closer.ui.brand.CloserCopy
|
||||||
import app.closer.ui.components.CloserGlyphs
|
import app.closer.ui.components.CloserGlyphs
|
||||||
|
|
@ -184,6 +180,8 @@ private fun HeaderSection(
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
|
@ -215,7 +213,7 @@ private fun HeaderSection(
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CloserGlyphs.Close,
|
imageVector = CloserGlyphs.Close,
|
||||||
contentDescription = "Close",
|
contentDescription = "Close",
|
||||||
tint = Color(0xFF9B8AA6)
|
tint = premiumTone.muted
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -254,10 +252,12 @@ private fun BenefitsCard(modifier: Modifier = Modifier) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BenefitPill(label: String) {
|
private fun BenefitPill(label: String) {
|
||||||
|
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(999.dp))
|
.clip(RoundedCornerShape(999.dp))
|
||||||
.background(CloserPalette.PurpleMist)
|
.background(premiumTone.accentContainer)
|
||||||
.padding(horizontal = 10.dp, vertical = 7.dp),
|
.padding(horizontal = 10.dp, vertical = 7.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(7.dp),
|
horizontalArrangement = Arrangement.spacedBy(7.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
|
@ -265,16 +265,13 @@ private fun BenefitPill(label: String) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CloserGlyphs.Check,
|
imageVector = CloserGlyphs.Check,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = premiumTone.onAccentContainer,
|
||||||
modifier = Modifier.size(15.dp)
|
modifier = Modifier.size(15.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
// The pill background is always the light PurpleMist (not theme-adaptive), so the
|
color = premiumTone.onAccentContainer
|
||||||
// text needs a fixed dark brand color — onSurface is near-white in dark mode and
|
|
||||||
// rendered the labels invisible (C-PW-001).
|
|
||||||
color = CloserPalette.PurpleDeep
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -286,10 +283,12 @@ private fun PlanOptions(
|
||||||
onSelect: (Package) -> Unit,
|
onSelect: (Package) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(28.dp),
|
shape = RoundedCornerShape(28.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = CloserPalette.PurpleSoft),
|
colors = CardDefaults.cardColors(containerColor = premiumTone.accentContainer.copy(alpha = 0.56f)),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -306,7 +305,7 @@ private fun PlanOptions(
|
||||||
Text(
|
Text(
|
||||||
text = CloserCopy.Paywall.coupleShared,
|
text = CloserCopy.Paywall.coupleShared,
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium),
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium),
|
||||||
color = Color(0xFF56306F)
|
color = premiumTone.onAccentContainer
|
||||||
)
|
)
|
||||||
|
|
||||||
if (packages.isEmpty()) {
|
if (packages.isEmpty()) {
|
||||||
|
|
@ -336,13 +335,15 @@ private fun PlanRow(
|
||||||
onSelect: () -> Unit,
|
onSelect: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(18.dp))
|
.clip(RoundedCornerShape(18.dp))
|
||||||
.background(
|
.background(
|
||||||
if (isSelected) CloserPalette.PurpleRich.copy(alpha = 0.20f)
|
if (isSelected) premiumTone.accentContainer
|
||||||
else Color.White.copy(alpha = 0.64f)
|
else MaterialTheme.colorScheme.surface.copy(alpha = 0.64f)
|
||||||
)
|
)
|
||||||
.selectable(
|
.selectable(
|
||||||
selected = isSelected,
|
selected = isSelected,
|
||||||
|
|
@ -357,8 +358,8 @@ private fun PlanRow(
|
||||||
selected = isSelected,
|
selected = isSelected,
|
||||||
onClick = null,
|
onClick = null,
|
||||||
colors = RadioButtonDefaults.colors(
|
colors = RadioButtonDefaults.colors(
|
||||||
selectedColor = Color(0xFFB98AF4),
|
selectedColor = premiumTone.accent,
|
||||||
unselectedColor = Color(0xFF9B8AA6)
|
unselectedColor = premiumTone.muted
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -374,14 +375,14 @@ private fun PlanRow(
|
||||||
Text(
|
Text(
|
||||||
text = pricing.offer ?: pkg.product.description,
|
text = pricing.offer ?: pkg.product.description,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = if (pricing.offer != null) Color(0xFF56306F) else MaterialTheme.colorScheme.onSurfaceVariant
|
color = if (pricing.offer != null) premiumTone.accent else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = pricing.price,
|
text = pricing.price,
|
||||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
color = Color(0xFF56306F)
|
color = premiumTone.accent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -394,6 +395,8 @@ private fun ActionButtons(
|
||||||
onRestore: () -> Unit,
|
onRestore: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
|
@ -407,10 +410,10 @@ private fun ActionButtons(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = CloserPalette.PurpleDeep,
|
containerColor = premiumTone.accent,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
contentColor = premiumTone.onAccent,
|
||||||
disabledContainerColor = CloserPalette.PurpleRich.copy(alpha = 0.40f),
|
disabledContainerColor = premiumTone.accent.copy(alpha = 0.40f),
|
||||||
disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.54f)
|
disabledContentColor = premiumTone.onAccent.copy(alpha = 0.54f)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Continue", fontWeight = FontWeight.SemiBold)
|
Text("Continue", fontWeight = FontWeight.SemiBold)
|
||||||
|
|
@ -438,7 +441,7 @@ private fun ActionButtons(
|
||||||
TextButton(onClick = onRestore) {
|
TextButton(onClick = onRestore) {
|
||||||
Text(
|
Text(
|
||||||
text = "Restore",
|
text = "Restore",
|
||||||
color = CloserPalette.PurpleDeep,
|
color = premiumTone.accent,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -452,6 +455,8 @@ private fun LegalLinks(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
|
@ -461,10 +466,10 @@ private fun LegalLinks(
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally)
|
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally)
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = { ExternalLinks.openUrl(context, ExternalLinks.PRIVACY_POLICY) }) {
|
TextButton(onClick = { ExternalLinks.openUrl(context, ExternalLinks.PRIVACY_POLICY) }) {
|
||||||
Text("Privacy Policy", style = MaterialTheme.typography.labelSmall, color = Color(0xFF9B8AA6))
|
Text("Privacy Policy", style = MaterialTheme.typography.labelSmall, color = premiumTone.muted)
|
||||||
}
|
}
|
||||||
TextButton(onClick = { ExternalLinks.openUrl(context, ExternalLinks.TERMS_OF_SERVICE) }) {
|
TextButton(onClick = { ExternalLinks.openUrl(context, ExternalLinks.TERMS_OF_SERVICE) }) {
|
||||||
Text("Terms of Service", style = MaterialTheme.typography.labelSmall, color = Color(0xFF9B8AA6))
|
Text("Terms of Service", style = MaterialTheme.typography.labelSmall, color = premiumTone.muted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -472,7 +477,7 @@ private fun LegalLinks(
|
||||||
Text(
|
Text(
|
||||||
"Subscription terms apply",
|
"Subscription terms apply",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = Color(0xFF9B8AA6)
|
color = premiumTone.muted
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -480,6 +485,8 @@ private fun LegalLinks(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ThankYouOverlay(onDismiss: () -> Unit) {
|
private fun ThankYouOverlay(onDismiss: () -> Unit) {
|
||||||
|
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -498,7 +505,7 @@ private fun ThankYouOverlay(onDismiss: () -> Unit) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CloserGlyphs.Check,
|
imageVector = CloserGlyphs.Check,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color(0xFF56306F),
|
tint = premiumTone.accent,
|
||||||
modifier = Modifier.size(48.dp)
|
modifier = Modifier.size(48.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -516,8 +523,8 @@ private fun ThankYouOverlay(onDismiss: () -> Unit) {
|
||||||
onClick = onDismiss,
|
onClick = onDismiss,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color(0xFFB98AF4),
|
containerColor = premiumTone.accent,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
contentColor = premiumTone.onAccent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Continue")
|
Text("Continue")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package app.closer.ui.play
|
package app.closer.ui.play
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -25,8 +24,6 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
|
@ -37,6 +34,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import app.closer.R
|
import app.closer.R
|
||||||
import app.closer.core.navigation.AppRoute
|
import app.closer.core.navigation.AppRoute
|
||||||
|
import app.closer.ui.components.BrandIllustration
|
||||||
import app.closer.ui.components.CategoryGlyph
|
import app.closer.ui.components.CategoryGlyph
|
||||||
import app.closer.ui.components.CloserActionButton
|
import app.closer.ui.components.CloserActionButton
|
||||||
import app.closer.ui.components.CloserButtonStyle
|
import app.closer.ui.components.CloserButtonStyle
|
||||||
|
|
@ -45,6 +43,7 @@ import app.closer.ui.components.CloserElevations
|
||||||
import app.closer.ui.components.CloserPill
|
import app.closer.ui.components.CloserPill
|
||||||
import app.closer.ui.components.UnpairedLockedCard
|
import app.closer.ui.components.UnpairedLockedCard
|
||||||
import app.closer.ui.components.CloserRadii
|
import app.closer.ui.components.CloserRadii
|
||||||
|
import app.closer.ui.theme.CloserTheme
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
import app.closer.ui.theme.closerPlayCardBrush
|
import app.closer.ui.theme.closerPlayCardBrush
|
||||||
import app.closer.ui.components.CloserGlyphs
|
import app.closer.ui.components.CloserGlyphs
|
||||||
|
|
@ -802,18 +801,29 @@ private fun WheelGlyph(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
modifier = Modifier.padding(5.dp)
|
modifier = Modifier.padding(5.dp)
|
||||||
) {
|
) {
|
||||||
Image(
|
BrandIllustration(
|
||||||
painter = painterResource(R.drawable.illustration_spin_wheel),
|
res = R.drawable.illustration_spin_wheel,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Fit,
|
modifier = Modifier.fillMaxSize(),
|
||||||
modifier = Modifier.fillMaxSize()
|
tile = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlayHubScreenPreview() {
|
private fun PlayHubScreenPreviewBody() {
|
||||||
PlayHubContent(onNavigate = {}, hasPremium = true)
|
PlayHubContent(onNavigate = {}, hasPremium = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Play hub • light")
|
||||||
|
@Composable
|
||||||
|
fun PlayHubScreenPreviewLight() {
|
||||||
|
CloserTheme(darkTheme = false) { PlayHubScreenPreviewBody() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Play hub • dark")
|
||||||
|
@Composable
|
||||||
|
fun PlayHubScreenPreviewDark() {
|
||||||
|
CloserTheme(darkTheme = true) { PlayHubScreenPreviewBody() }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,10 +191,10 @@ fun DeleteAccountScreen(
|
||||||
enabled = state.canDelete,
|
enabled = state.canDelete,
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = SettingsDanger,
|
containerColor = SettingsDanger,
|
||||||
contentColor = Color.White
|
contentColor = MaterialTheme.colorScheme.onError
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Delete", color = Color.White)
|
Text("Delete")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
|
|
@ -249,13 +249,13 @@ fun DeleteAccountScreen(
|
||||||
enabled = !state.isReauthing,
|
enabled = !state.isReauthing,
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = SettingsDanger,
|
containerColor = SettingsDanger,
|
||||||
contentColor = Color.White
|
contentColor = MaterialTheme.colorScheme.onError
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (state.isReauthing) {
|
if (state.isReauthing) {
|
||||||
CloserHeartLoader(size = 18.dp)
|
CloserHeartLoader(size = 18.dp)
|
||||||
} else {
|
} else {
|
||||||
Text("Confirm", color = Color.White)
|
Text("Confirm")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -332,16 +332,16 @@ fun DeleteAccountScreen(
|
||||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp),
|
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = SettingsDanger,
|
containerColor = SettingsDanger,
|
||||||
contentColor = Color.White
|
contentColor = MaterialTheme.colorScheme.onError
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (state.isDeleting) {
|
if (state.isDeleting) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
CloserHeartLoader(size = 20.dp)
|
CloserHeartLoader(size = 20.dp)
|
||||||
Text("Deleting…", color = Color.White)
|
Text("Deleting…")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text("Delete my account", color = Color.White, fontWeight = FontWeight.SemiBold)
|
Text("Delete my account", fontWeight = FontWeight.SemiBold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,10 +114,10 @@ fun RelationshipSettingsScreen(
|
||||||
enabled = !state.isLeaving,
|
enabled = !state.isLeaving,
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = SettingsDanger,
|
containerColor = SettingsDanger,
|
||||||
contentColor = Color.White
|
contentColor = MaterialTheme.colorScheme.onError
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Leave", color = Color.White)
|
Text("Leave")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
|
|
@ -180,16 +180,16 @@ fun RelationshipSettingsScreen(
|
||||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp),
|
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = SettingsDanger,
|
containerColor = SettingsDanger,
|
||||||
contentColor = Color.White
|
contentColor = MaterialTheme.colorScheme.onError
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (state.isLeaving) {
|
if (state.isLeaving) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
CloserHeartLoader(size = 20.dp)
|
CloserHeartLoader(size = 20.dp)
|
||||||
Text("Leaving…", color = Color.White)
|
Text("Leaving…")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text("Leave this couple", color = Color.White, fontWeight = FontWeight.SemiBold)
|
Text("Leave this couple", fontWeight = FontWeight.SemiBold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,13 +112,13 @@ fun SettingsSubpage(
|
||||||
fun SettingsSection(
|
fun SettingsSection(
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
accent: Color = Color(0xFFF7C8E4),
|
accent: Color? = null,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val sectionAccent = if (isCloserDarkTheme()) {
|
val sectionAccent = if (isCloserDarkTheme()) {
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
} else {
|
} else {
|
||||||
accent
|
accent ?: MaterialTheme.colorScheme.secondaryContainer
|
||||||
}
|
}
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|
@ -480,7 +480,7 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSection(title = "For the two of you", accent = Color(0xFFF7C8E4)) {
|
SettingsSection(title = "For the two of you", accent = MaterialTheme.colorScheme.secondaryContainer) {
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
icon = CloserGlyphs.Heart,
|
icon = CloserGlyphs.Heart,
|
||||||
label = "Together",
|
label = "Together",
|
||||||
|
|
@ -520,7 +520,7 @@ fun SettingsScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSection(title = "Your rhythm", accent = Color(0xFFD9B8FF)) {
|
SettingsSection(title = "Your rhythm", accent = MaterialTheme.colorScheme.primaryContainer) {
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
icon = CloserGlyphs.Bell,
|
icon = CloserGlyphs.Bell,
|
||||||
label = "Notifications",
|
label = "Notifications",
|
||||||
|
|
@ -536,7 +536,7 @@ fun SettingsScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSection(title = "Premium", accent = Color(0xFFE7A2D1)) {
|
SettingsSection(title = "Premium", accent = MaterialTheme.colorScheme.tertiaryContainer) {
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_couple_premium),
|
icon = ImageVector.vectorResource(R.drawable.glyph_couple_premium),
|
||||||
label = "Subscription",
|
label = "Subscription",
|
||||||
|
|
@ -546,7 +546,7 @@ fun SettingsScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSection(title = "Privacy and safety", accent = Color(0xFFD9B8FF)) {
|
SettingsSection(title = "Privacy and safety", accent = MaterialTheme.colorScheme.primaryContainer) {
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_privacy_lock),
|
icon = ImageVector.vectorResource(R.drawable.glyph_privacy_lock),
|
||||||
label = "Security",
|
label = "Security",
|
||||||
|
|
@ -562,7 +562,7 @@ fun SettingsScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSection(title = "Account", accent = Color(0xFFFFD9E8)) {
|
SettingsSection(title = "Account", accent = MaterialTheme.colorScheme.errorContainer) {
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_delete_account),
|
icon = ImageVector.vectorResource(R.drawable.glyph_delete_account),
|
||||||
label = "Delete account",
|
label = "Delete account",
|
||||||
|
|
@ -679,14 +679,14 @@ private fun SettingsRow(
|
||||||
androidx.compose.foundation.layout.Box(
|
androidx.compose.foundation.layout.Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(999.dp))
|
.clip(RoundedCornerShape(999.dp))
|
||||||
.background(Color(0xFF8F67C5))
|
.background(MaterialTheme.colorScheme.primary)
|
||||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (badgeCount > 9) "9+" else badgeCount.toString(),
|
text = if (badgeCount > 9) "9+" else badgeCount.toString(),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = Color.White
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -87,9 +87,11 @@ import app.closer.ui.components.BrandMessageRotator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserPalette
|
||||||
import app.closer.ui.theme.CloserTheme
|
import app.closer.ui.theme.CloserTheme
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
import app.closer.ui.theme.isCloserDarkTheme
|
import app.closer.ui.theme.isCloserDarkTheme
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -933,7 +935,7 @@ private fun OptionCard(
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
color = if (isSelected) Color.White.copy(alpha = 0.22f) else accent.copy(alpha = 0.16f),
|
color = if (isSelected) onSelectedContainer.copy(alpha = 0.22f) else accent.copy(alpha = 0.16f),
|
||||||
modifier = Modifier.size(32.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
) {
|
) {
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
|
@ -1065,6 +1067,7 @@ private fun ThisOrThatReveal(
|
||||||
showNextBeat: Boolean = false
|
showNextBeat: Boolean = false
|
||||||
) {
|
) {
|
||||||
val ratio = if (total > 0) matched.toFloat() / total else 0f
|
val ratio = if (total > 0) matched.toFloat() / total else 0f
|
||||||
|
val thisOrThatTone = closerToneColors(CloserExperienceTone.ThisOrThat)
|
||||||
val headline = when {
|
val headline = when {
|
||||||
ratio >= 0.8f -> "Two peas in a pod 🫛"
|
ratio >= 0.8f -> "Two peas in a pod 🫛"
|
||||||
ratio >= 0.5f -> "Lots in common 💛"
|
ratio >= 0.5f -> "Lots in common 💛"
|
||||||
|
|
@ -1115,9 +1118,12 @@ private fun ThisOrThatReveal(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = 56.dp),
|
.heightIn(min = 56.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = thisOrThatTone.accent,
|
||||||
|
contentColor = thisOrThatTone.onAccent
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text("Play again", color = Color.White)
|
Text("Play again")
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import app.closer.domain.model.QuestionCategory
|
||||||
import app.closer.ui.components.CategoryGlyph
|
import app.closer.ui.components.CategoryGlyph
|
||||||
import app.closer.ui.questions.displayCategoryName
|
import app.closer.ui.questions.displayCategoryName
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserPalette
|
||||||
|
import app.closer.ui.theme.CloserTheme
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
import app.closer.ui.components.CloserGlyphs
|
import app.closer.ui.components.CloserGlyphs
|
||||||
|
|
||||||
|
|
@ -316,9 +317,8 @@ private fun CategoryPill(label: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryPickerScreenPreview() {
|
private fun CategoryPickerScreenPreviewBody() {
|
||||||
CategoryPickerContent(
|
CategoryPickerContent(
|
||||||
state = CategoryPickerUiState(
|
state = CategoryPickerUiState(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
|
@ -340,3 +340,15 @@ fun CategoryPickerScreenPreview() {
|
||||||
onRetry = {}
|
onRetry = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Wheel categories • light")
|
||||||
|
@Composable
|
||||||
|
fun CategoryPickerScreenPreviewLight() {
|
||||||
|
CloserTheme(darkTheme = false) { CategoryPickerScreenPreviewBody() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Wheel categories • dark")
|
||||||
|
@Composable
|
||||||
|
fun CategoryPickerScreenPreviewDark() {
|
||||||
|
CloserTheme(darkTheme = true) { CategoryPickerScreenPreviewBody() }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -51,8 +50,6 @@ import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
|
@ -60,7 +57,9 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import app.closer.R
|
import app.closer.R
|
||||||
|
import app.closer.ui.components.BrandIllustration
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserPalette
|
||||||
|
import app.closer.ui.theme.CloserTheme
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
import app.closer.ui.components.CloserGlyphs
|
import app.closer.ui.components.CloserGlyphs
|
||||||
|
|
||||||
|
|
@ -389,13 +388,13 @@ private fun WheelSpinner(
|
||||||
.scale(if (isSpinning) 1f else idlePulse),
|
.scale(if (isSpinning) 1f else idlePulse),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Image(
|
BrandIllustration(
|
||||||
painter = painterResource(R.drawable.illustration_spin_wheel),
|
res = R.drawable.illustration_spin_wheel,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(236.dp)
|
.size(236.dp)
|
||||||
.rotate(wheelAngle)
|
.rotate(wheelAngle),
|
||||||
|
tile = false
|
||||||
)
|
)
|
||||||
|
|
||||||
Canvas(modifier = Modifier.size(236.dp)) {
|
Canvas(modifier = Modifier.size(236.dp)) {
|
||||||
|
|
@ -446,9 +445,8 @@ private fun WheelSpinner(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SpinWheelScreenPreview() {
|
private fun SpinWheelScreenPreviewBody() {
|
||||||
SpinWheelContent(
|
SpinWheelContent(
|
||||||
state = SpinWheelUiState(isLoading = false, categoryName = "Trust"),
|
state = SpinWheelUiState(isLoading = false, categoryName = "Trust"),
|
||||||
onSpin = {},
|
onSpin = {},
|
||||||
|
|
@ -459,3 +457,15 @@ fun SpinWheelScreenPreview() {
|
||||||
onBack = {}
|
onBack = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Wheel spin • light")
|
||||||
|
@Composable
|
||||||
|
fun SpinWheelScreenPreviewLight() {
|
||||||
|
CloserTheme(darkTheme = false) { SpinWheelScreenPreviewBody() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Wheel spin • dark")
|
||||||
|
@Composable
|
||||||
|
fun SpinWheelScreenPreviewDark() {
|
||||||
|
CloserTheme(darkTheme = true) { SpinWheelScreenPreviewBody() }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,12 @@ import app.closer.data.remote.FirestoreWheelAnswerDataSource
|
||||||
import app.closer.data.remote.WheelRevealDoc
|
import app.closer.data.remote.WheelRevealDoc
|
||||||
import app.closer.domain.usecase.GameSessionManager
|
import app.closer.domain.usecase.GameSessionManager
|
||||||
import app.closer.ui.components.StatusGlyph
|
import app.closer.ui.components.StatusGlyph
|
||||||
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserPalette
|
||||||
|
import app.closer.ui.theme.CloserTheme
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
import app.closer.ui.theme.closerCardColor
|
import app.closer.ui.theme.closerCardColor
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -266,6 +269,8 @@ private fun WheelWaitingContent(
|
||||||
onHome: () -> Unit,
|
onHome: () -> Unit,
|
||||||
onAbandon: () -> Unit = {}
|
onAbandon: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -279,8 +284,8 @@ private fun WheelWaitingContent(
|
||||||
|
|
||||||
StatusGlyph(
|
StatusGlyph(
|
||||||
icon = CloserGlyphs.Check,
|
icon = CloserGlyphs.Check,
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = wheelTone.accent,
|
||||||
container = CloserPalette.PurpleMist,
|
container = wheelTone.accentContainer,
|
||||||
size = 82.dp,
|
size = 82.dp,
|
||||||
iconSize = 40.dp
|
iconSize = 40.dp
|
||||||
)
|
)
|
||||||
|
|
@ -330,6 +335,8 @@ private fun WheelRevealContent(
|
||||||
onOpenDaily: () -> Unit = {},
|
onOpenDaily: () -> Unit = {},
|
||||||
showNextBeat: Boolean = false
|
showNextBeat: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -346,8 +353,8 @@ private fun WheelRevealContent(
|
||||||
) {
|
) {
|
||||||
StatusGlyph(
|
StatusGlyph(
|
||||||
icon = CloserGlyphs.Check,
|
icon = CloserGlyphs.Check,
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = wheelTone.accent,
|
||||||
container = CloserPalette.PurpleMist,
|
container = wheelTone.accentContainer,
|
||||||
size = 72.dp,
|
size = 72.dp,
|
||||||
iconSize = 34.dp
|
iconSize = 34.dp
|
||||||
)
|
)
|
||||||
|
|
@ -380,9 +387,12 @@ private fun WheelRevealContent(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = 56.dp),
|
.heightIn(min = 56.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = wheelTone.accent,
|
||||||
|
contentColor = wheelTone.onAccent
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text("Spin again", color = Color.White)
|
Text("Spin again")
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
|
|
@ -404,6 +414,8 @@ private fun WheelRevealContent(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun WheelRevealRow(item: WheelRevealItem, partnerName: String) {
|
private fun WheelRevealRow(item: WheelRevealItem, partnerName: String) {
|
||||||
|
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
|
@ -421,7 +433,7 @@ private fun WheelRevealRow(item: WheelRevealItem, partnerName: String) {
|
||||||
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold),
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
AnswerBlock(label = "You", text = item.myDisplay, accent = CloserPalette.PurpleDeep)
|
AnswerBlock(label = "You", text = item.myDisplay, accent = wheelTone.accent)
|
||||||
AnswerBlock(label = partnerName, text = item.partnerDisplay, accent = CloserPalette.PinkAccentDeep)
|
AnswerBlock(label = partnerName, text = item.partnerDisplay, accent = CloserPalette.PinkAccentDeep)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -467,6 +479,8 @@ private fun WheelErrorContent(
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
onHome: () -> Unit
|
onHome: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -494,9 +508,12 @@ private fun WheelErrorContent(
|
||||||
onClick = onRetry,
|
onClick = onRetry,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = wheelTone.accent,
|
||||||
|
contentColor = wheelTone.onAccent
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text("Try again", color = Color.White)
|
Text("Try again")
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = onHome,
|
onClick = onHome,
|
||||||
|
|
@ -508,10 +525,9 @@ private fun WheelErrorContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WheelRevealPreview() {
|
private fun WheelRevealPreviewBody() {
|
||||||
Box(Modifier.background(Color(0xFFFFFBFE))) {
|
Box(Modifier.background(MaterialTheme.colorScheme.background)) {
|
||||||
WheelRevealContent(
|
WheelRevealContent(
|
||||||
categoryName = "Trust",
|
categoryName = "Trust",
|
||||||
partnerName = "Sam",
|
partnerName = "Sam",
|
||||||
|
|
@ -532,3 +548,15 @@ fun WheelRevealPreview() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Wheel reveal • light")
|
||||||
|
@Composable
|
||||||
|
fun WheelRevealPreviewLight() {
|
||||||
|
CloserTheme(darkTheme = false) { WheelRevealPreviewBody() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Wheel reveal • dark")
|
||||||
|
@Composable
|
||||||
|
fun WheelRevealPreviewDark() {
|
||||||
|
CloserTheme(darkTheme = true) { WheelRevealPreviewBody() }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@ import app.closer.ui.components.EmptyState
|
||||||
import app.closer.ui.components.ErrorState
|
import app.closer.ui.components.ErrorState
|
||||||
import app.closer.ui.components.LoadingState
|
import app.closer.ui.components.LoadingState
|
||||||
import app.closer.ui.questions.displayCategoryName
|
import app.closer.ui.questions.displayCategoryName
|
||||||
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
@ -183,6 +185,8 @@ fun GameHistoryScreen(
|
||||||
@Composable
|
@Composable
|
||||||
private fun WheelSessionCard(session: QuestionSession, onClick: (() -> Unit)?) {
|
private fun WheelSessionCard(session: QuestionSession, onClick: (() -> Unit)?) {
|
||||||
val replayable = onClick != null
|
val replayable = onClick != null
|
||||||
|
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
onClick = onClick ?: {},
|
onClick = onClick ?: {},
|
||||||
enabled = replayable,
|
enabled = replayable,
|
||||||
|
|
@ -214,7 +218,7 @@ private fun WheelSessionCard(session: QuestionSession, onClick: (() -> Unit)?) {
|
||||||
Text(
|
Text(
|
||||||
text = SimpleDateFormat("d MMM", Locale.getDefault()).format(Date(ts)),
|
text = SimpleDateFormat("d MMM", Locale.getDefault()).format(Date(ts)),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = Color(0xFFB98AF4)
|
color = wheelTone.accent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -234,6 +238,8 @@ private fun HistorySectionHeader(title: String) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChallengeHistoryCard(entry: CompletedChallengeEntry, onClick: (() -> Unit)? = null) {
|
private fun ChallengeHistoryCard(entry: CompletedChallengeEntry, onClick: (() -> Unit)? = null) {
|
||||||
|
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
onClick = onClick ?: {},
|
onClick = onClick ?: {},
|
||||||
enabled = onClick != null,
|
enabled = onClick != null,
|
||||||
|
|
@ -265,7 +271,7 @@ private fun ChallengeHistoryCard(entry: CompletedChallengeEntry, onClick: (() ->
|
||||||
Text(
|
Text(
|
||||||
text = SimpleDateFormat("d MMM", java.util.Locale.getDefault()).format(Date(entry.startedAt)),
|
text = SimpleDateFormat("d MMM", java.util.Locale.getDefault()).format(Date(entry.startedAt)),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = Color(0xFFB98AF4)
|
color = wheelTone.accent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -274,6 +280,8 @@ private fun ChallengeHistoryCard(entry: CompletedChallengeEntry, onClick: (() ->
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CapsuleHistoryCard(capsule: TimeCapsule, onClick: (() -> Unit)? = null) {
|
private fun CapsuleHistoryCard(capsule: TimeCapsule, onClick: (() -> Unit)? = null) {
|
||||||
|
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
onClick = onClick ?: {},
|
onClick = onClick ?: {},
|
||||||
enabled = onClick != null,
|
enabled = onClick != null,
|
||||||
|
|
@ -307,7 +315,7 @@ private fun CapsuleHistoryCard(capsule: TimeCapsule, onClick: (() -> Unit)? = nu
|
||||||
Text(
|
Text(
|
||||||
text = SimpleDateFormat("d MMM", java.util.Locale.getDefault()).format(Date(capsule.unlockAt)),
|
text = SimpleDateFormat("d MMM", java.util.Locale.getDefault()).format(Date(capsule.unlockAt)),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = Color(0xFFB98AF4)
|
color = wheelTone.accent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -340,6 +348,8 @@ private fun sessionReplayRoute(session: QuestionSession): String? = when (sessio
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun GameHistoryLockedCard(onUnlock: () -> Unit) {
|
private fun GameHistoryLockedCard(onUnlock: () -> Unit) {
|
||||||
|
val premiumTone = closerToneColors(CloserExperienceTone.Premium)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(24.dp),
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
|
@ -384,11 +394,11 @@ private fun GameHistoryLockedCard(onUnlock: () -> Unit) {
|
||||||
.heightIn(min = 52.dp),
|
.heightIn(min = 52.dp),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color(0xFFB98AF4),
|
containerColor = premiumTone.accent,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
contentColor = premiumTone.onAccent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Unlock premium", color = MaterialTheme.colorScheme.onPrimary)
|
Text("Unlock premium")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package app.closer.ui.wheel
|
package app.closer.ui.wheel
|
||||||
|
|
||||||
import app.closer.ui.theme.closerCardColor
|
import app.closer.ui.theme.CloserExperienceTone
|
||||||
|
import app.closer.ui.theme.CloserTheme
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
|
import app.closer.ui.theme.closerCardColor
|
||||||
import app.closer.ui.theme.closerSoftPurpleColor
|
import app.closer.ui.theme.closerSoftPurpleColor
|
||||||
|
import app.closer.ui.theme.closerToneColors
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -48,7 +51,6 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.semantics.LiveRegionMode
|
import androidx.compose.ui.semantics.LiveRegionMode
|
||||||
import androidx.compose.ui.semantics.liveRegion
|
import androidx.compose.ui.semantics.liveRegion
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
|
@ -104,6 +106,8 @@ private fun WheelSessionContent(
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
onAbandon: () -> Unit
|
onAbandon: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -169,7 +173,7 @@ private fun WheelSessionContent(
|
||||||
Text(
|
Text(
|
||||||
text = "${current + 1} / $total",
|
text = "${current + 1} / $total",
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = Color(0xFF9B8AA6),
|
color = wheelTone.muted,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
|
@ -262,11 +266,13 @@ private fun WheelSessionContent(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = 56.dp),
|
.heightIn(min = 56.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF56306F))
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = wheelTone.accent,
|
||||||
|
contentColor = wheelTone.onAccent
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (state.showCompletionPrompt || current + 1 >= total) "Finish" else "Next question",
|
text = if (state.showCompletionPrompt || current + 1 >= total) "Finish" else "Next question"
|
||||||
color = Color.White
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -288,7 +294,7 @@ private fun WheelSessionContent(
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.heightIn(min = 48.dp)
|
.heightIn(min = 48.dp)
|
||||||
) {
|
) {
|
||||||
Text("Finish now", color = Color(0xFF9B8AA6))
|
Text("Finish now", color = wheelTone.muted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Escape hatch: leave without finishing (force-completes the shared session so it
|
// Escape hatch: leave without finishing (force-completes the shared session so it
|
||||||
|
|
@ -299,7 +305,7 @@ private fun WheelSessionContent(
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Quit game",
|
"Quit game",
|
||||||
color = Color(0xFF9B8AA6).copy(alpha = 0.7f),
|
color = wheelTone.muted.copy(alpha = 0.7f),
|
||||||
style = MaterialTheme.typography.labelMedium
|
style = MaterialTheme.typography.labelMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -318,6 +324,8 @@ private fun AnswerOptions(
|
||||||
onScaleChanged: (Int) -> Unit,
|
onScaleChanged: (Int) -> Unit,
|
||||||
onWrittenTextChanged: (String) -> Unit
|
onWrittenTextChanged: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
|
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
|
||||||
|
|
||||||
when (question.type) {
|
when (question.type) {
|
||||||
"written" -> {
|
"written" -> {
|
||||||
val config = question.answerConfig as? WrittenAnswerConfigImpl
|
val config = question.answerConfig as? WrittenAnswerConfigImpl
|
||||||
|
|
@ -339,7 +347,7 @@ private fun AnswerOptions(
|
||||||
maxLines = 6,
|
maxLines = 6,
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
focusedBorderColor = Color(0xFF56306F),
|
focusedBorderColor = wheelTone.accent,
|
||||||
unfocusedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f),
|
unfocusedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f),
|
||||||
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface
|
unfocusedContainerColor = MaterialTheme.colorScheme.surface
|
||||||
|
|
@ -359,11 +367,11 @@ private fun AnswerOptions(
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
border = BorderStroke(
|
border = BorderStroke(
|
||||||
width = if (isSelected) 2.dp else 1.dp,
|
width = if (isSelected) 2.dp else 1.dp,
|
||||||
color = if (isSelected) Color(0xFF56306F)
|
color = if (isSelected) wheelTone.accent
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f)
|
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f)
|
||||||
),
|
),
|
||||||
colors = ButtonDefaults.outlinedButtonColors(
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
containerColor = if (isSelected) Color(0xFFF0EDF9)
|
containerColor = if (isSelected) wheelTone.accentContainer
|
||||||
else MaterialTheme.colorScheme.surface,
|
else MaterialTheme.colorScheme.surface,
|
||||||
contentColor = MaterialTheme.colorScheme.onSurface
|
contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
@ -376,7 +384,7 @@ private fun AnswerOptions(
|
||||||
selected = isSelected,
|
selected = isSelected,
|
||||||
onClick = null,
|
onClick = null,
|
||||||
colors = RadioButtonDefaults.colors(
|
colors = RadioButtonDefaults.colors(
|
||||||
selectedColor = Color(0xFF56306F)
|
selectedColor = wheelTone.accent
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -403,11 +411,11 @@ private fun AnswerOptions(
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
border = BorderStroke(
|
border = BorderStroke(
|
||||||
width = if (isChecked) 2.dp else 1.dp,
|
width = if (isChecked) 2.dp else 1.dp,
|
||||||
color = if (isChecked) Color(0xFF56306F)
|
color = if (isChecked) wheelTone.accent
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f)
|
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f)
|
||||||
),
|
),
|
||||||
colors = ButtonDefaults.outlinedButtonColors(
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
containerColor = if (isChecked) Color(0xFFF0EDF9)
|
containerColor = if (isChecked) wheelTone.accentContainer
|
||||||
else MaterialTheme.colorScheme.surface,
|
else MaterialTheme.colorScheme.surface,
|
||||||
contentColor = MaterialTheme.colorScheme.onSurface
|
contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
@ -420,7 +428,7 @@ private fun AnswerOptions(
|
||||||
checked = isChecked,
|
checked = isChecked,
|
||||||
onCheckedChange = null,
|
onCheckedChange = null,
|
||||||
colors = CheckboxDefaults.colors(
|
colors = CheckboxDefaults.colors(
|
||||||
checkedColor = Color(0xFF56306F)
|
checkedColor = wheelTone.accent
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -446,7 +454,7 @@ private fun AnswerOptions(
|
||||||
Text(
|
Text(
|
||||||
text = "$selectedScaleValue",
|
text = "$selectedScaleValue",
|
||||||
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),
|
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),
|
||||||
color = Color(0xFF56306F)
|
color = wheelTone.accent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
@ -457,8 +465,8 @@ private fun AnswerOptions(
|
||||||
steps = if (cfg.maxScale > cfg.minScale) (cfg.maxScale - cfg.minScale - 1).coerceAtLeast(0) else 0,
|
steps = if (cfg.maxScale > cfg.minScale) (cfg.maxScale - cfg.minScale - 1).coerceAtLeast(0) else 0,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = SliderDefaults.colors(
|
colors = SliderDefaults.colors(
|
||||||
thumbColor = Color(0xFF56306F),
|
thumbColor = wheelTone.accent,
|
||||||
activeTrackColor = Color(0xFF56306F),
|
activeTrackColor = wheelTone.accent,
|
||||||
inactiveTrackColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f)
|
inactiveTrackColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -495,9 +503,9 @@ private fun AnswerOptions(
|
||||||
.height(96.dp),
|
.height(96.dp),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = if (isSelected) Color(0xFF56306F)
|
containerColor = if (isSelected) wheelTone.accent
|
||||||
else MaterialTheme.colorScheme.surfaceVariant,
|
else MaterialTheme.colorScheme.surfaceVariant,
|
||||||
contentColor = if (isSelected) Color.White
|
contentColor = if (isSelected) wheelTone.onAccent
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
),
|
),
|
||||||
elevation = ButtonDefaults.buttonElevation(
|
elevation = ButtonDefaults.buttonElevation(
|
||||||
|
|
@ -550,6 +558,8 @@ private fun EmptySessionCard() {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SubmitErrorCard(message: String, onRetry: () -> Unit) {
|
private fun SubmitErrorCard(message: String, onRetry: () -> Unit) {
|
||||||
|
val wheelTone = closerToneColors(CloserExperienceTone.Wheel)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(24.dp),
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
|
@ -574,17 +584,19 @@ private fun SubmitErrorCard(message: String, onRetry: () -> Unit) {
|
||||||
onClick = onRetry,
|
onClick = onRetry,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 52.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 52.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF56306F))
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = wheelTone.accent,
|
||||||
|
contentColor = wheelTone.onAccent
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text("Retry", color = Color.White)
|
Text("Retry")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WheelSessionScreenPreview() {
|
private fun WheelSessionScreenPreviewBody() {
|
||||||
WheelSessionContent(
|
WheelSessionContent(
|
||||||
state = WheelSessionUiState(
|
state = WheelSessionUiState(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
|
@ -605,3 +617,15 @@ fun WheelSessionScreenPreview() {
|
||||||
onAbandon = {}
|
onAbandon = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Wheel session • light")
|
||||||
|
@Composable
|
||||||
|
fun WheelSessionScreenPreviewLight() {
|
||||||
|
CloserTheme(darkTheme = false) { WheelSessionScreenPreviewBody() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Wheel session • dark")
|
||||||
|
@Composable
|
||||||
|
fun WheelSessionScreenPreviewDark() {
|
||||||
|
CloserTheme(darkTheme = true) { WheelSessionScreenPreviewBody() }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@
|
||||||
#
|
#
|
||||||
# Exclusions:
|
# Exclusions:
|
||||||
# - Color.Transparent (intentional)
|
# - Color.Transparent (intentional)
|
||||||
# - Theme.kt (the theme definition itself)
|
# - ui/theme/* (the theme and token definitions themselves)
|
||||||
|
# - Google sign-in brand colors in AuthVisuals.kt
|
||||||
|
# - Debug-only ArtPreviewScreen.kt raw illustration previews
|
||||||
|
# - BrandIllustration.kt alpha masks
|
||||||
|
# - PackArtwork.kt, MessageBubbleOverlay.kt, and ChatComponents.kt media/scrim overlays
|
||||||
# - @Preview composables — a hardcoded color inside an `@Preview` function only affects
|
# - @Preview composables — a hardcoded color inside an `@Preview` function only affects
|
||||||
# Android Studio's design-time preview pane, never the shipped app, so it is NOT a theme
|
# Android Studio's design-time preview pane, never the shipped app, so it is NOT a theme
|
||||||
# defect. (Added 2026-06-28 after WheelCompleteScreen.kt `WheelRevealPreview` was filed as
|
# defect. (Added 2026-06-28 after WheelCompleteScreen.kt `WheelRevealPreview` was filed as
|
||||||
|
|
@ -18,6 +22,7 @@
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/theme-scan.sh > /tmp/claude-theme-scan-$(date +%Y%m%d).md
|
# ./scripts/theme-scan.sh > /tmp/claude-theme-scan-$(date +%Y%m%d).md
|
||||||
|
# ./scripts/theme-scan.sh --strict /tmp/closer-theme-scan-strict.md
|
||||||
# ./scripts/theme-scan.sh --json # (future improvement — machine-readable)
|
# ./scripts/theme-scan.sh --json # (future improvement — machine-readable)
|
||||||
#
|
#
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -25,10 +30,21 @@ set -euo pipefail
|
||||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
UI_DIR="$PROJECT_ROOT/app/src/main/java/app/closer/ui"
|
UI_DIR="$PROJECT_ROOT/app/src/main/java/app/closer/ui"
|
||||||
THEME_FILE="$PROJECT_ROOT/app/src/main/java/app/closer/ui/theme/Theme.kt"
|
THEME_FILE="$PROJECT_ROOT/app/src/main/java/app/closer/ui/theme/Theme.kt"
|
||||||
SCAN_OUTPUT="${1:-}"
|
STRICT=false
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--strict" ]]; then
|
||||||
|
STRICT=true
|
||||||
|
SCAN_OUTPUT="${2:-}"
|
||||||
|
else
|
||||||
|
SCAN_OUTPUT="${1:-}"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z "$SCAN_OUTPUT" ]]; then
|
if [[ -z "$SCAN_OUTPUT" ]]; then
|
||||||
|
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"
|
SCAN_OUTPUT="/tmp/claude-theme-scan-$(date +%Y%m%d).md"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
: > "$SCAN_OUTPUT"
|
: > "$SCAN_OUTPUT"
|
||||||
|
|
@ -63,8 +79,35 @@ drop_previews() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allowed_exception() {
|
||||||
|
local file="$1"
|
||||||
|
case "$file" in
|
||||||
|
"$UI_DIR"/theme/*) return 0 ;;
|
||||||
|
"$UI_DIR"/auth/AuthVisuals.kt) return 0 ;;
|
||||||
|
"$UI_DIR"/debug/ArtPreviewScreen.kt) return 0 ;;
|
||||||
|
"$UI_DIR"/components/BrandIllustration.kt) return 0 ;;
|
||||||
|
"$UI_DIR"/questions/PackArtwork.kt) return 0 ;;
|
||||||
|
"$UI_DIR"/components/MessageBubbleOverlay.kt) return 0 ;;
|
||||||
|
"$UI_DIR"/messages/components/ChatComponents.kt) return 0 ;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
drop_allowed() {
|
||||||
|
local line file lineno
|
||||||
|
while IFS= read -r line; do
|
||||||
|
file=$(echo "$line" | cut -d: -f1)
|
||||||
|
lineno=$(echo "$line" | cut -d: -f2)
|
||||||
|
allowed_exception "$file" "$lineno" && continue
|
||||||
|
echo "$line"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
log "# CloserApp Theme-Mismatch Scan — $(date)"
|
log "# CloserApp Theme-Mismatch Scan — $(date)"
|
||||||
log "Project: $PROJECT_ROOT"
|
log "Project: $PROJECT_ROOT"
|
||||||
|
if [[ "$STRICT" == true ]]; then
|
||||||
|
log "Mode: strict"
|
||||||
|
fi
|
||||||
log ""
|
log ""
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
@ -86,6 +129,7 @@ scan_after_container() {
|
||||||
local lineno=$(echo "$container" | cut -d: -f2)
|
local lineno=$(echo "$container" | cut -d: -f2)
|
||||||
if [[ ! -f "$file" ]]; then continue; fi
|
if [[ ! -f "$file" ]]; then continue; fi
|
||||||
if in_preview "$file" "$lineno"; then continue; fi
|
if in_preview "$file" "$lineno"; then continue; fi
|
||||||
|
if allowed_exception "$file" "$lineno"; then continue; fi
|
||||||
local segment=$(sed -n "${lineno},$((lineno+7))p" "$file")
|
local segment=$(sed -n "${lineno},$((lineno+7))p" "$file")
|
||||||
local match=$(echo "$segment" | grep -E '^\s*(color|containerColor|background)\s*=\s*Color(\.|\()' | grep -ivE "$exclude" | head -1 || true)
|
local match=$(echo "$segment" | grep -E '^\s*(color|containerColor|background)\s*=\s*Color(\.|\()' | grep -ivE "$exclude" | head -1 || true)
|
||||||
if [[ -n "$match" ]]; then
|
if [[ -n "$match" ]]; then
|
||||||
|
|
@ -107,6 +151,7 @@ grep -rnE 'Modifier\.background\(Color(\.|\()' "$UI_DIR" --include="*.kt" \
|
||||||
| grep -ivE 'Color\.Transparent' \
|
| grep -ivE 'Color\.Transparent' \
|
||||||
| grep -ivE 'Theme\.kt' \
|
| grep -ivE 'Theme\.kt' \
|
||||||
| drop_previews \
|
| drop_previews \
|
||||||
|
| drop_allowed \
|
||||||
| while IFS= read -r line; do
|
| while IFS= read -r line; do
|
||||||
log "🔴 CRITICAL $line"
|
log "🔴 CRITICAL $line"
|
||||||
done || true
|
done || true
|
||||||
|
|
@ -117,6 +162,7 @@ grep -rnE '(buttonColors|TextFieldDefaults\.colors|TabRowDefaults\.colors|Switch
|
||||||
| grep -iE 'Color(\.|\()' \
|
| grep -iE 'Color(\.|\()' \
|
||||||
| grep -ivE 'Color\.Transparent' \
|
| grep -ivE 'Color\.Transparent' \
|
||||||
| drop_previews \
|
| drop_previews \
|
||||||
|
| drop_allowed \
|
||||||
| while IFS= read -r line; do
|
| while IFS= read -r line; do
|
||||||
log "🟠 MAJOR $line"
|
log "🟠 MAJOR $line"
|
||||||
done || true
|
done || true
|
||||||
|
|
@ -124,6 +170,7 @@ grep -rnE '(buttonColors|TextFieldDefaults\.colors|TabRowDefaults\.colors|Switch
|
||||||
grep -rnE '(Divider|HorizontalDivider)\s*\(' "$UI_DIR" --include="*.kt" \
|
grep -rnE '(Divider|HorizontalDivider)\s*\(' "$UI_DIR" --include="*.kt" \
|
||||||
| grep -iE 'color = Color(\.|\()' \
|
| grep -iE 'color = Color(\.|\()' \
|
||||||
| drop_previews \
|
| drop_previews \
|
||||||
|
| drop_allowed \
|
||||||
| while IFS= read -r line; do
|
| while IFS= read -r line; do
|
||||||
log "🟠 MAJOR $line"
|
log "🟠 MAJOR $line"
|
||||||
done || true
|
done || true
|
||||||
|
|
@ -133,6 +180,7 @@ log "## Tier 1D — Text/Icon color hardcoded on themed surfaces (REVIEW)"
|
||||||
grep -rnE '^\s*color = Color\.(White|Black|(0x[0-9A-F]{8}))' "$UI_DIR" --include="*.kt" \
|
grep -rnE '^\s*color = Color\.(White|Black|(0x[0-9A-F]{8}))' "$UI_DIR" --include="*.kt" \
|
||||||
| grep -ivE 'Theme\.kt' \
|
| grep -ivE 'Theme\.kt' \
|
||||||
| drop_previews \
|
| drop_previews \
|
||||||
|
| drop_allowed \
|
||||||
| while IFS= read -r line; do
|
| while IFS= read -r line; do
|
||||||
log "🟡 REVIEW $line"
|
log "🟡 REVIEW $line"
|
||||||
done || true
|
done || true
|
||||||
|
|
@ -141,6 +189,7 @@ log ""
|
||||||
log "## Tier 1E — Direct painterResource that bypasses BrandIllustration (MAJOR)"
|
log "## Tier 1E — Direct painterResource that bypasses BrandIllustration (MAJOR)"
|
||||||
grep -rnE 'painterResource\(R\.drawable\.(illustration_|pack_art_)' "$UI_DIR" --include="*.kt" \
|
grep -rnE 'painterResource\(R\.drawable\.(illustration_|pack_art_)' "$UI_DIR" --include="*.kt" \
|
||||||
| drop_previews \
|
| drop_previews \
|
||||||
|
| drop_allowed \
|
||||||
| while IFS= read -r line; do
|
| while IFS= read -r line; do
|
||||||
log "🟠 MAJOR $line"
|
log "🟠 MAJOR $line"
|
||||||
done || true
|
done || true
|
||||||
|
|
@ -150,6 +199,7 @@ log "## Tier 1F — Hardcoded border colors (REVIEW)"
|
||||||
grep -rnE 'Modifier\.border\([^)]*Color(\.|\()' "$UI_DIR" --include="*.kt" \
|
grep -rnE 'Modifier\.border\([^)]*Color(\.|\()' "$UI_DIR" --include="*.kt" \
|
||||||
| grep -ivE 'Color\.Transparent' \
|
| grep -ivE 'Color\.Transparent' \
|
||||||
| drop_previews \
|
| drop_previews \
|
||||||
|
| drop_allowed \
|
||||||
| while IFS= read -r line; do
|
| while IFS= read -r line; do
|
||||||
log "🟡 REVIEW $line"
|
log "🟡 REVIEW $line"
|
||||||
done || true
|
done || true
|
||||||
|
|
@ -157,11 +207,37 @@ grep -rnE 'Modifier\.border\([^)]*Color(\.|\()' "$UI_DIR" --include="*.kt" \
|
||||||
log ""
|
log ""
|
||||||
log "## Tier 1G — Hardcoded Brush/gradient stops (REVIEW)"
|
log "## Tier 1G — Hardcoded Brush/gradient stops (REVIEW)"
|
||||||
grep -rnE 'Brush\.(linear|vertical|horizontal|radial)Gradient' "$UI_DIR" --include="*.kt" \
|
grep -rnE 'Brush\.(linear|vertical|horizontal|radial)Gradient' "$UI_DIR" --include="*.kt" \
|
||||||
| grep -vE 'BrandIllustration\.kt' \
|
|
||||||
| drop_previews \
|
| drop_previews \
|
||||||
|
| drop_allowed \
|
||||||
|
| while IFS= read -r line; do
|
||||||
|
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
|
| while IFS= read -r line; do
|
||||||
log "🟡 REVIEW $line"
|
log "🟡 REVIEW $line"
|
||||||
done || true
|
done || true
|
||||||
|
fi
|
||||||
|
|
||||||
log ""
|
log ""
|
||||||
log "## Summary"
|
log "## Summary"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue