feat: add answer history filter pills with reveal/private badges, DesireSync progress meter and reveal UI, HowWell ring progress and privacy tiles, Paywall feature scavenger hunt, category screen grid polish

This commit is contained in:
null 2026-06-18 03:26:12 -05:00
parent 15c1fbdda0
commit 606d724f12
5 changed files with 497 additions and 71 deletions

View File

@ -2,6 +2,7 @@ package app.closer.ui.answers
import app.closer.ui.theme.closerBackgroundBrush import app.closer.ui.theme.closerBackgroundBrush
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -19,11 +20,15 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -48,8 +53,13 @@ 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.questions.displayCategoryName import app.closer.ui.questions.displayCategoryName
private enum class AnswerHistoryFilter {
ALL, PRIVATE, REVEALED
}
@Composable @Composable
fun AnswerHistoryScreen( fun AnswerHistoryScreen(
onNavigate: (String) -> Unit = {}, onNavigate: (String) -> Unit = {},
@ -73,6 +83,16 @@ private fun AnswerHistoryContent(
onDelete: (String) -> Unit onDelete: (String) -> Unit
) { ) {
var pendingDelete by remember { mutableStateOf<LocalAnswer?>(null) } var pendingDelete by remember { mutableStateOf<LocalAnswer?>(null) }
var selectedFilter by remember { mutableStateOf(AnswerHistoryFilter.ALL) }
val visibleAnswers = remember(state.answers, selectedFilter) {
state.answers.filter { answer ->
when (selectedFilter) {
AnswerHistoryFilter.ALL -> true
AnswerHistoryFilter.PRIVATE -> !answer.isRevealed
AnswerHistoryFilter.REVEALED -> answer.isRevealed
}
}
}
pendingDelete?.let { answer -> pendingDelete?.let { answer ->
AlertDialog( AlertDialog(
@ -146,7 +166,25 @@ private fun AnswerHistoryContent(
) )
} }
} else { } else {
items(state.answers, key = { it.questionId }) { answer -> item {
HistoryFilterRow(
selected = selectedFilter,
privateCount = state.answers.count { !it.isRevealed },
revealedCount = state.answers.count { it.isRevealed },
onSelected = { selectedFilter = it }
)
}
if (visibleAnswers.isEmpty()) {
item {
EmptyState(
title = "Nothing in this view",
body = "Switch filters to see the rest of your saved answers."
)
}
}
items(visibleAnswers, key = { it.questionId }) { answer ->
AnswerHistoryCard( AnswerHistoryCard(
answer = answer, answer = answer,
onClick = { onAnswerSelected(answer) }, onClick = { onAnswerSelected(answer) },
@ -158,6 +196,69 @@ private fun AnswerHistoryContent(
} }
} }
@Composable
private fun HistoryFilterRow(
selected: AnswerHistoryFilter,
privateCount: Int,
revealedCount: Int,
onSelected: (AnswerHistoryFilter) -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
HistoryFilterPill(
label = "All",
count = privateCount + revealedCount,
selected = selected == AnswerHistoryFilter.ALL,
onClick = { onSelected(AnswerHistoryFilter.ALL) },
modifier = Modifier.weight(1f)
)
HistoryFilterPill(
label = "Private",
count = privateCount,
selected = selected == AnswerHistoryFilter.PRIVATE,
onClick = { onSelected(AnswerHistoryFilter.PRIVATE) },
modifier = Modifier.weight(1f)
)
HistoryFilterPill(
label = "Revealed",
count = revealedCount,
selected = selected == AnswerHistoryFilter.REVEALED,
onClick = { onSelected(AnswerHistoryFilter.REVEALED) },
modifier = Modifier.weight(1f)
)
}
}
@Composable
private fun HistoryFilterPill(
label: String,
count: Int,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier
.heightIn(min = 44.dp)
.clickable(onClick = onClick),
shape = RoundedCornerShape(999.dp),
color = if (selected) CloserPalette.PurpleMist else Color.White.copy(alpha = 0.74f),
shadowElevation = if (selected) 2.dp else 0.dp
) {
Text(
text = "$label $count",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
style = MaterialTheme.typography.labelMedium,
color = if (selected) CloserPalette.PurpleDeep else MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
@ -192,7 +293,7 @@ private fun AnswerHistoryCard(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
HistoryPill(if (answer.isRevealed) "Revealed" else "Private") HistoryStateBadge(isRevealed = answer.isRevealed)
HistoryPill(answer.category.displayCategoryName()) HistoryPill(answer.category.displayCategoryName())
} }
Text( Text(
@ -235,6 +336,35 @@ private fun AnswerHistoryCard(
} }
} }
@Composable
private fun HistoryStateBadge(isRevealed: Boolean) {
val tint = if (isRevealed) CloserPalette.PurpleDeep else CloserPalette.Evergreen
val container = if (isRevealed) CloserPalette.PurpleMist else CloserPalette.Evergreen.copy(alpha = 0.10f)
Surface(
shape = RoundedCornerShape(999.dp),
color = container
) {
Row(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = if (isRevealed) Icons.Filled.Visibility else Icons.Filled.Lock,
contentDescription = null,
tint = tint,
modifier = Modifier.size(14.dp)
)
Text(
text = if (isRevealed) "Revealed" else "Private",
style = MaterialTheme.typography.labelMedium,
color = tint,
fontWeight = FontWeight.SemiBold
)
}
}
}
@Composable @Composable
private fun HistoryPill(label: String) { private fun HistoryPill(label: String) {
Surface( Surface(

View File

@ -8,6 +8,7 @@ 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
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -21,6 +22,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.draw.clip
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.FavoriteBorder
@ -31,7 +33,6 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -499,11 +500,9 @@ private fun DSAnswerScreen(
} }
} }
LinearProgressIndicator( DesireProgressPill(
progress = { index.toFloat() / total }, progress = index.toFloat() / total,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
color = CloserPalette.Romantic,
trackColor = CloserPalette.Romantic.copy(alpha = 0.15f)
) )
Card( Card(
@ -605,6 +604,7 @@ private fun DSRevealScreen(
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
DesireRevealMeter(matches = matches.size, total = total)
} }
} }
@ -660,6 +660,116 @@ private fun DSRevealScreen(
} }
} }
@Composable
private fun DesireProgressPill(
progress: Float,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.height(8.dp)
.clip(RoundedCornerShape(999.dp))
.background(CloserPalette.Romantic.copy(alpha = 0.15f))
) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(progress.coerceIn(0f, 1f))
.clip(RoundedCornerShape(999.dp))
.background(CloserPalette.Romantic)
)
}
}
@Composable
private fun DesireRevealMeter(
matches: Int,
total: Int
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(22.dp),
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.86f)),
elevation = CardDefaults.cardElevation(0.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
DesirePrivacyTile(
label = "Partner A",
value = "$total private",
modifier = Modifier.weight(1f)
)
StatusGlyph(
icon = Icons.Filled.Sync,
tint = CloserPalette.Romantic,
container = CloserPalette.Romantic.copy(alpha = 0.12f),
size = 38.dp,
iconSize = 20.dp
)
DesirePrivacyTile(
label = "Partner B",
value = "$total private",
modifier = Modifier.weight(1f)
)
}
DesireProgressPill(
progress = matches.toFloat() / total.coerceAtLeast(1),
modifier = Modifier.fillMaxWidth()
)
Text(
text = "$matches shared, ${total - matches} kept private",
style = MaterialTheme.typography.labelMedium,
color = CloserPalette.Romantic,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Composable
private fun DesirePrivacyTile(
label: String,
value: String,
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(16.dp),
color = CloserPalette.Romantic.copy(alpha = 0.08f)
) {
Column(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
text = label,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = value,
style = MaterialTheme.typography.labelMedium,
color = CloserPalette.Romantic,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Composable @Composable
private fun DesireMatchCard(match: DesireMatch) { private fun DesireMatchCard(match: DesireMatch) {
Card( Card(

View File

@ -2,10 +2,12 @@ package app.closer.ui.howwell
import app.closer.ui.theme.closerCardColor import app.closer.ui.theme.closerCardColor
import android.util.Log import android.util.Log
import androidx.compose.foundation.Canvas
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
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -20,6 +22,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.draw.clip
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Psychology import androidx.compose.material.icons.filled.Psychology
@ -30,7 +33,6 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -43,6 +45,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment 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.StrokeCap
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
@ -502,11 +506,9 @@ private fun AnswerScreen(
} }
} }
LinearProgressIndicator( HowWellProgressPill(
progress = { index.toFloat() / total }, progress = index.toFloat() / total,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
color = CloserPalette.PurpleDeep,
trackColor = CloserPalette.PurpleMist
) )
Card( Card(
@ -656,6 +658,12 @@ private fun RevealScreen(
} }
} }
HowWellScoreStrip(
score = score,
answered = questionNumber,
total = total
)
Text( Text(
text = result.question.text, text = result.question.text,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
@ -755,18 +763,10 @@ private fun CompleteScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
StatusGlyph( HowWellScoreRing(
icon = Icons.Filled.Timeline, score = score,
tint = CloserPalette.PurpleDeep, total = total,
container = CloserPalette.PurpleMist, modifier = Modifier.size(118.dp)
size = 82.dp,
iconSize = 40.dp
)
Text(
text = "$score / $total",
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
color = CloserPalette.PurpleDeep,
textAlign = TextAlign.Center
) )
Text( Text(
text = scoreLabel(score, total), text = scoreLabel(score, total),
@ -808,6 +808,110 @@ private fun CompleteScreen(
} }
} }
@Composable
private fun HowWellProgressPill(
progress: Float,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.height(8.dp)
.clip(RoundedCornerShape(999.dp))
.background(CloserPalette.PurpleMist)
) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(progress.coerceIn(0f, 1f))
.clip(RoundedCornerShape(999.dp))
.background(CloserPalette.PurpleDeep)
)
}
}
@Composable
private fun HowWellScoreStrip(
score: Int,
answered: Int,
total: Int
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(18.dp),
color = CloserPalette.PurpleMist
) {
Row(
modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
HowWellProgressPill(
progress = score.toFloat() / answered.coerceAtLeast(1),
modifier = Modifier.weight(1f)
)
Text(
text = "$score / $answered read",
style = MaterialTheme.typography.labelMedium,
color = CloserPalette.PurpleDeep,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = "$total total",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Composable
private fun HowWellScoreRing(
score: Int,
total: Int,
modifier: Modifier = Modifier
) {
val progress = if (total == 0) 0f else score.toFloat() / total
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
Canvas(modifier = Modifier.fillMaxSize()) {
val strokeWidth = 11.dp.toPx()
drawArc(
color = CloserPalette.PurpleMist,
startAngle = -90f,
sweepAngle = 360f,
useCenter = false,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
drawArc(
color = CloserPalette.PurpleDeep,
startAngle = -90f,
sweepAngle = 360f * progress.coerceIn(0f, 1f),
useCenter = false,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "$score",
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
color = CloserPalette.PurpleDeep
)
Text(
text = "of $total",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold
)
}
}
}
@Composable @Composable
private fun BreakdownRow(result: HowWellResult) { private fun BreakdownRow(result: HowWellResult) {
val matchColor = Color(0xFF2E7D32) val matchColor = Color(0xFF2E7D32)

View File

@ -9,6 +9,8 @@ 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
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -25,6 +27,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
@ -60,6 +63,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.components.StatusGlyph
import app.closer.ui.theme.CloserPalette
import com.revenuecat.purchases.Package import com.revenuecat.purchases.Package
private val BENEFITS = listOf( private val BENEFITS = listOf(
@ -103,7 +108,6 @@ fun PaywallScreen(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
HeaderSection(onClose = { onNavigate("back") }) HeaderSection(onClose = { onNavigate("back") })
BenefitsCard()
when { when {
uiState.isLoading -> LoadingState( uiState.isLoading -> LoadingState(
@ -124,10 +128,12 @@ fun PaywallScreen(
) )
} }
BenefitsCard()
if (uiState.purchaseState is BillingState.Loading) { if (uiState.purchaseState is BillingState.Loading) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.size(28.dp), modifier = Modifier.size(28.dp),
color = Color(0xFFB98AF4) color = CloserPalette.PurpleRich
) )
} }
@ -163,9 +169,16 @@ private fun HeaderSection(
) { ) {
Row( Row(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.Top
) { ) {
StatusGlyph(
icon = Icons.Filled.Star,
tint = CloserPalette.PurpleDeep,
container = CloserPalette.PurpleGlow,
size = 58.dp,
iconSize = 28.dp
)
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = "Go deeper together", text = "Go deeper together",
@ -180,7 +193,7 @@ private fun HeaderSection(
) )
} }
TextButton(onClick = onClose) { TextButton(onClick = onClose, modifier = Modifier.size(48.dp)) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = "Close", contentDescription = "Close",
@ -190,44 +203,60 @@ private fun HeaderSection(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun BenefitsCard(modifier: Modifier = Modifier) { private fun BenefitsCard(modifier: Modifier = Modifier) {
Card( Card(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(28.dp), shape = RoundedCornerShape(22.dp),
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.88f)), colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.88f)),
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp) elevation = CardDefaults.cardElevation(defaultElevation = 3.dp)
) { ) {
Column( Column(
modifier = Modifier.padding(24.dp), modifier = Modifier.padding(18.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
Text( Text(
text = "What's included", text = "What's included",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
BENEFITS.forEach { benefit -> BENEFITS.forEach { benefit ->
BenefitPill(benefit)
}
}
}
}
}
@Composable
private fun BenefitPill(label: String) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier
.clip(RoundedCornerShape(999.dp))
.background(CloserPalette.PurpleMist)
.padding(horizontal = 10.dp, vertical = 7.dp),
horizontalArrangement = Arrangement.spacedBy(7.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
contentDescription = null, contentDescription = null,
tint = Color(0xFF56306F), tint = CloserPalette.PurpleDeep,
modifier = Modifier.size(18.dp) modifier = Modifier.size(15.dp)
) )
Text( Text(
text = benefit, text = label,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
} }
} }
}
}
}
@Composable @Composable
private fun PlanOptions( private fun PlanOptions(
@ -239,8 +268,8 @@ private fun PlanOptions(
Card( Card(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(28.dp), shape = RoundedCornerShape(28.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFF4E8FF)), colors = CardDefaults.cardColors(containerColor = CloserPalette.PurpleSoft),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) { ) {
Column( Column(
modifier = Modifier.padding(22.dp), modifier = Modifier.padding(22.dp),
@ -251,6 +280,11 @@ private fun PlanOptions(
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
Text(
text = "Plans are selected here. Restore and legal links stay below the purchase action.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (packages.isEmpty()) { if (packages.isEmpty()) {
Text( Text(
@ -284,7 +318,7 @@ private fun PlanRow(
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(18.dp)) .clip(RoundedCornerShape(18.dp))
.background( .background(
if (isSelected) Color(0xFFB98AF4).copy(alpha = 0.20f) if (isSelected) CloserPalette.PurpleRich.copy(alpha = 0.20f)
else Color.White.copy(alpha = 0.64f) else Color.White.copy(alpha = 0.64f)
) )
.selectable( .selectable(
@ -344,23 +378,44 @@ private fun ActionButtons(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4), containerColor = CloserPalette.PurpleDeep,
contentColor = MaterialTheme.colorScheme.onPrimary, contentColor = MaterialTheme.colorScheme.onPrimary,
disabledContainerColor = Color(0xFFB98AF4).copy(alpha = 0.40f), disabledContainerColor = CloserPalette.PurpleRich.copy(alpha = 0.40f),
disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.54f) disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.54f)
) )
) { ) {
Text("Continue", fontWeight = FontWeight.SemiBold) Text("Continue", fontWeight = FontWeight.SemiBold)
} }
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.54f)),
elevation = CardDefaults.cardElevation(0.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Already subscribed?",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
TextButton(onClick = onRestore) { TextButton(onClick = onRestore) {
Text( Text(
text = "Restore purchases", text = "Restore",
color = Color(0xFF9B8AA6) color = CloserPalette.PurpleDeep,
fontWeight = FontWeight.SemiBold
) )
} }
} }
} }
}
}
@Composable @Composable
private fun LegalLinks( private fun LegalLinks(

View File

@ -288,21 +288,48 @@ private fun FilterPill(
@Composable @Composable
private fun DepthHeader(depth: Int, count: Int) { private fun DepthHeader(depth: Int, count: Int) {
Row( Surface(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 6.dp), .padding(top = 6.dp),
shape = RoundedCornerShape(18.dp),
color = Color.White.copy(alpha = 0.68f),
shadowElevation = 0.dp
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 11.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = depthLabel(depth),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text( Text(
text = "Depth $depth", text = "Depth $depth",
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
CategoryPill("$count ${if (count == 1) "prompt" else "prompts"}")
} }
CategoryPill("$count ${if (count == 1) "prompt" else "prompts"}", emphasis = true)
}
}
}
private fun depthLabel(depth: Int): String = when (depth) {
1 -> "Light openers"
2 -> "Closer prompts"
3 -> "Deeper conversation"
else -> "Depth $depth"
} }
@Composable @Composable