refactor(home): extract primary/activation cards -> components/HomePrimaryCards.kt
Step 7/9. Verbatim move (activation + primary hero public, benefit-pill private). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d324efe182
commit
c13dbc5bfd
|
|
@ -96,6 +96,8 @@ import app.closer.ui.home.components.HomeGlyphIcon
|
||||||
import app.closer.ui.home.components.HomePill
|
import app.closer.ui.home.components.HomePill
|
||||||
import app.closer.ui.home.components.HomeStatusStrip
|
import app.closer.ui.home.components.HomeStatusStrip
|
||||||
import app.closer.ui.home.components.LoadingHomeCard
|
import app.closer.ui.home.components.LoadingHomeCard
|
||||||
|
import app.closer.ui.home.components.PartnerActivationCard
|
||||||
|
import app.closer.ui.home.components.PrimaryHomeActionCard
|
||||||
import app.closer.ui.home.components.WaitingForYouSection
|
import app.closer.ui.home.components.WaitingForYouSection
|
||||||
import app.closer.ui.home.components.actionColors
|
import app.closer.ui.home.components.actionColors
|
||||||
import app.closer.ui.home.components.homeActionGlyph
|
import app.closer.ui.home.components.homeActionGlyph
|
||||||
|
|
@ -731,358 +733,6 @@ private fun PartnerSheetAction(
|
||||||
private fun formatMonthYear(millis: Long): String =
|
private fun formatMonthYear(millis: Long): String =
|
||||||
java.text.SimpleDateFormat("MMM yyyy", java.util.Locale.getDefault()).format(java.util.Date(millis))
|
java.text.SimpleDateFormat("MMM yyyy", java.util.Locale.getDefault()).format(java.util.Date(millis))
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PartnerActivationCard(
|
|
||||||
onInvite: () -> Unit,
|
|
||||||
onAcceptInvite: () -> Unit
|
|
||||||
) {
|
|
||||||
val isDark = isCloserDarkTheme()
|
|
||||||
val inviteTone = HomeActionTone.Invite.actionColors()
|
|
||||||
CloserCard(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(CloserRadii.FeatureCard),
|
|
||||||
containerColor = closerCardColor(alpha = 0.94f),
|
|
||||||
elevation = CloserElevations.Feature
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(
|
|
||||||
Brush.linearGradient(
|
|
||||||
if (isDark) {
|
|
||||||
listOf(
|
|
||||||
MaterialTheme.colorScheme.surface,
|
|
||||||
MaterialTheme.colorScheme.surfaceVariant,
|
|
||||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.72f)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listOf(
|
|
||||||
MaterialTheme.colorScheme.surface,
|
|
||||||
CloserPalette.PurpleSoft,
|
|
||||||
CloserPalette.PinkMist.copy(alpha = 0.84f)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
start = Offset.Zero,
|
|
||||||
end = Offset.Infinite
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.padding(20.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(18.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
HomePill("1 of 2 connected")
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(CloserRadii.Pill),
|
|
||||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.76f)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
HomeGlyphIcon(
|
|
||||||
resId = R.drawable.glyph_lock,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = inviteTone.deep,
|
|
||||||
modifier = Modifier.size(14.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Private invite",
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = inviteTone.deep,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BrandIllustration(
|
|
||||||
res = R.drawable.illustration_partner_activation,
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(142.dp)
|
|
||||||
.clip(RoundedCornerShape(22.dp))
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = "A private space for two",
|
|
||||||
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Invite your partner to unlock shared reveals, games, streaks, and answers you can both respond to.",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
maxLines = 4,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
ActivationBenefitPill("Private reveals", Modifier.weight(1f))
|
|
||||||
ActivationBenefitPill("Shared streak", Modifier.weight(1f))
|
|
||||||
ActivationBenefitPill("Games for two", Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
|
||||||
CloserActionButton(
|
|
||||||
label = "Invite partner",
|
|
||||||
onClick = onInvite,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
containerColor = inviteTone.accent,
|
|
||||||
contentColor = inviteTone.onAccent
|
|
||||||
)
|
|
||||||
CloserActionButton(
|
|
||||||
label = "Enter code",
|
|
||||||
onClick = onAcceptInvite,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
style = CloserButtonStyle.Secondary,
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.7f),
|
|
||||||
contentColor = inviteTone.deep
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ActivationBenefitPill(
|
|
||||||
label: String,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val colors = HomeActionTone.Invite.actionColors()
|
|
||||||
Surface(
|
|
||||||
modifier = modifier,
|
|
||||||
shape = RoundedCornerShape(CloserRadii.Pill),
|
|
||||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.66f)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 9.dp, vertical = 7.dp),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = colors.deep,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PrimaryHomeActionCard(
|
|
||||||
action: HomeAction,
|
|
||||||
onAction: (HomeAction) -> Unit,
|
|
||||||
onReminder: () -> Unit,
|
|
||||||
onReveal: () -> Unit,
|
|
||||||
onFollowUp: () -> Unit,
|
|
||||||
dailyQuestionState: DailyQuestionState,
|
|
||||||
dailyQuestion: Question?
|
|
||||||
) {
|
|
||||||
val colors = action.tone.actionColors()
|
|
||||||
val isDark = isCloserDarkTheme()
|
|
||||||
// Daily-question art stays people-forward while keeping the two states distinct.
|
|
||||||
val artRes = if (action.target == HomeActionTarget.DailyQuestion) {
|
|
||||||
when (dailyQuestionState) {
|
|
||||||
DailyQuestionState.UNANSWERED -> R.drawable.illustration_tonight_partner_prompt
|
|
||||||
DailyQuestionState.BOTH_ANSWERED -> R.drawable.illustration_daily_reveal_ready
|
|
||||||
else -> homePrimaryArt(action.target)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
homePrimaryArt(action.target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For daily-question actions, route the CTA through the explicit state handlers
|
|
||||||
// so the same button label maps to the correct next step (answer, remind,
|
|
||||||
// reveal, or follow-up) even if the action target is still DailyQuestion.
|
|
||||||
val ctaClick = when (action.target) {
|
|
||||||
HomeActionTarget.DailyQuestion -> when (dailyQuestionState) {
|
|
||||||
DailyQuestionState.USER_ANSWERED_PARTNER_PENDING -> onReminder
|
|
||||||
DailyQuestionState.BOTH_ANSWERED -> onReveal
|
|
||||||
DailyQuestionState.REVEALED -> onFollowUp
|
|
||||||
else -> { { onAction(action) } }
|
|
||||||
}
|
|
||||||
else -> { { onAction(action) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
val titleOverride = when (action.target) {
|
|
||||||
HomeActionTarget.DailyQuestion -> when (dailyQuestionState) {
|
|
||||||
DailyQuestionState.UNANSWERED -> "Tonight's question is ready."
|
|
||||||
DailyQuestionState.USER_ANSWERED_PARTNER_PENDING -> "You showed up tonight. Waiting for your partner."
|
|
||||||
DailyQuestionState.PARTNER_ANSWERED_USER_PENDING -> "Your partner answered. Your turn."
|
|
||||||
DailyQuestionState.BOTH_ANSWERED -> "Your reveal is waiting"
|
|
||||||
DailyQuestionState.REVEALED -> "You opened a conversation tonight."
|
|
||||||
}
|
|
||||||
else -> action.title
|
|
||||||
}
|
|
||||||
|
|
||||||
val bodyOverride = when (action.target) {
|
|
||||||
HomeActionTarget.DailyQuestion -> when (dailyQuestionState) {
|
|
||||||
DailyQuestionState.UNANSWERED ->
|
|
||||||
dailyQuestion?.text ?: "Answer tonight's question privately, then choose when to share."
|
|
||||||
DailyQuestionState.USER_ANSWERED_PARTNER_PENDING ->
|
|
||||||
"Your answer is private until they answer too. No pressure — the reveal waits for both of you."
|
|
||||||
DailyQuestionState.PARTNER_ANSWERED_USER_PENDING ->
|
|
||||||
"Answer to unlock the reveal. Your response stays private until you are ready."
|
|
||||||
DailyQuestionState.BOTH_ANSWERED ->
|
|
||||||
"You both answered — open it together when you're ready."
|
|
||||||
DailyQuestionState.REVEALED ->
|
|
||||||
"You revealed an answer together. What comes next is up to both of you."
|
|
||||||
}
|
|
||||||
else -> action.body
|
|
||||||
}
|
|
||||||
|
|
||||||
val timeBudgetLabel = when (action.target) {
|
|
||||||
HomeActionTarget.DailyQuestion -> when (dailyQuestion?.type) {
|
|
||||||
"scale" -> "~1 min"
|
|
||||||
"single_choice" -> "~2 min"
|
|
||||||
"this_or_that" -> "~2 min"
|
|
||||||
else -> "~3 min"
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
CloserCard(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(CloserRadii.FeatureCard),
|
|
||||||
containerColor = closerCardColor(alpha = 0.92f),
|
|
||||||
elevation = CloserElevations.Feature
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(
|
|
||||||
Brush.linearGradient(
|
|
||||||
if (isDark) {
|
|
||||||
listOf(
|
|
||||||
MaterialTheme.colorScheme.surface,
|
|
||||||
MaterialTheme.colorScheme.surfaceVariant,
|
|
||||||
colors.soft.copy(alpha = 0.72f)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listOf(
|
|
||||||
MaterialTheme.colorScheme.surface,
|
|
||||||
colors.soft,
|
|
||||||
CloserPalette.PinkMist.copy(alpha = 0.72f)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
start = Offset.Zero,
|
|
||||||
end = Offset.Infinite
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.padding(20.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
HomePill(action.eyebrow)
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
||||||
timeBudgetLabel?.let { label ->
|
|
||||||
HomePill(label)
|
|
||||||
}
|
|
||||||
action.metric?.let { HomePill(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
artRes?.let { res ->
|
|
||||||
BrandIllustration(
|
|
||||||
res = res,
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(150.dp)
|
|
||||||
.clip(RoundedCornerShape(22.dp))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (artRes != null) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = titleOverride,
|
|
||||||
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
maxLines = 3,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = bodyOverride,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
maxLines = 4,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
|
||||||
verticalAlignment = Alignment.Top
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(CloserRadii.Tile),
|
|
||||||
color = colors.accent.copy(alpha = 0.16f),
|
|
||||||
modifier = Modifier.size(52.dp)
|
|
||||||
) {
|
|
||||||
Box(contentAlignment = Alignment.Center) {
|
|
||||||
HomeGlyphIcon(
|
|
||||||
resId = homeActionGlyph(action.target),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = colors.deep,
|
|
||||||
modifier = Modifier.size(26.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = titleOverride,
|
|
||||||
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
maxLines = 3,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = bodyOverride,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
maxLines = 4,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CloserActionButton(
|
|
||||||
label = action.cta,
|
|
||||||
onClick = ctaClick,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
containerColor = colors.accent,
|
|
||||||
contentColor = colors.onAccent
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,397 @@
|
||||||
|
package app.closer.ui.home.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.closer.R
|
||||||
|
import app.closer.domain.model.Question
|
||||||
|
import app.closer.ui.components.BrandIllustration
|
||||||
|
import app.closer.ui.components.CloserActionButton
|
||||||
|
import app.closer.ui.components.CloserButtonStyle
|
||||||
|
import app.closer.ui.components.CloserCard
|
||||||
|
import app.closer.ui.components.CloserElevations
|
||||||
|
import app.closer.ui.components.CloserRadii
|
||||||
|
import app.closer.ui.home.DailyQuestionState
|
||||||
|
import app.closer.ui.home.HomeAction
|
||||||
|
import app.closer.ui.home.HomeActionTarget
|
||||||
|
import app.closer.ui.home.HomeActionTone
|
||||||
|
import app.closer.ui.theme.CloserPalette
|
||||||
|
import app.closer.ui.theme.closerCardColor
|
||||||
|
import app.closer.ui.theme.isCloserDarkTheme
|
||||||
|
|
||||||
|
/** Home's hero cards — the unpaired partner-activation card and the primary daily-question/action
|
||||||
|
* card (with its per-state title/body/CTA overrides). Extracted from HomeScreen.kt. */
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PartnerActivationCard(
|
||||||
|
onInvite: () -> Unit,
|
||||||
|
onAcceptInvite: () -> Unit
|
||||||
|
) {
|
||||||
|
val isDark = isCloserDarkTheme()
|
||||||
|
val inviteTone = HomeActionTone.Invite.actionColors()
|
||||||
|
CloserCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(CloserRadii.FeatureCard),
|
||||||
|
containerColor = closerCardColor(alpha = 0.94f),
|
||||||
|
elevation = CloserElevations.Feature
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
Brush.linearGradient(
|
||||||
|
if (isDark) {
|
||||||
|
listOf(
|
||||||
|
MaterialTheme.colorScheme.surface,
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.72f)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
MaterialTheme.colorScheme.surface,
|
||||||
|
CloserPalette.PurpleSoft,
|
||||||
|
CloserPalette.PinkMist.copy(alpha = 0.84f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
start = Offset.Zero,
|
||||||
|
end = Offset.Infinite
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(18.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
HomePill("1 of 2 connected")
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(CloserRadii.Pill),
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.76f)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
HomeGlyphIcon(
|
||||||
|
resId = R.drawable.glyph_lock,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = inviteTone.deep,
|
||||||
|
modifier = Modifier.size(14.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Private invite",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = inviteTone.deep,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BrandIllustration(
|
||||||
|
res = R.drawable.illustration_partner_activation,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(142.dp)
|
||||||
|
.clip(RoundedCornerShape(22.dp))
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "A private space for two",
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Invite your partner to unlock shared reveals, games, streaks, and answers you can both respond to.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 4,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
ActivationBenefitPill("Private reveals", Modifier.weight(1f))
|
||||||
|
ActivationBenefitPill("Shared streak", Modifier.weight(1f))
|
||||||
|
ActivationBenefitPill("Games for two", Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
CloserActionButton(
|
||||||
|
label = "Invite partner",
|
||||||
|
onClick = onInvite,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
containerColor = inviteTone.accent,
|
||||||
|
contentColor = inviteTone.onAccent
|
||||||
|
)
|
||||||
|
CloserActionButton(
|
||||||
|
label = "Enter code",
|
||||||
|
onClick = onAcceptInvite,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
style = CloserButtonStyle.Secondary,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.7f),
|
||||||
|
contentColor = inviteTone.deep
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ActivationBenefitPill(
|
||||||
|
label: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val colors = HomeActionTone.Invite.actionColors()
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = RoundedCornerShape(CloserRadii.Pill),
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.66f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 9.dp, vertical = 7.dp),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = colors.deep,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PrimaryHomeActionCard(
|
||||||
|
action: HomeAction,
|
||||||
|
onAction: (HomeAction) -> Unit,
|
||||||
|
onReminder: () -> Unit,
|
||||||
|
onReveal: () -> Unit,
|
||||||
|
onFollowUp: () -> Unit,
|
||||||
|
dailyQuestionState: DailyQuestionState,
|
||||||
|
dailyQuestion: Question?
|
||||||
|
) {
|
||||||
|
val colors = action.tone.actionColors()
|
||||||
|
val isDark = isCloserDarkTheme()
|
||||||
|
// Daily-question art stays people-forward while keeping the two states distinct.
|
||||||
|
val artRes = if (action.target == HomeActionTarget.DailyQuestion) {
|
||||||
|
when (dailyQuestionState) {
|
||||||
|
DailyQuestionState.UNANSWERED -> R.drawable.illustration_tonight_partner_prompt
|
||||||
|
DailyQuestionState.BOTH_ANSWERED -> R.drawable.illustration_daily_reveal_ready
|
||||||
|
else -> homePrimaryArt(action.target)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
homePrimaryArt(action.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For daily-question actions, route the CTA through the explicit state handlers
|
||||||
|
// so the same button label maps to the correct next step (answer, remind,
|
||||||
|
// reveal, or follow-up) even if the action target is still DailyQuestion.
|
||||||
|
val ctaClick = when (action.target) {
|
||||||
|
HomeActionTarget.DailyQuestion -> when (dailyQuestionState) {
|
||||||
|
DailyQuestionState.USER_ANSWERED_PARTNER_PENDING -> onReminder
|
||||||
|
DailyQuestionState.BOTH_ANSWERED -> onReveal
|
||||||
|
DailyQuestionState.REVEALED -> onFollowUp
|
||||||
|
else -> { { onAction(action) } }
|
||||||
|
}
|
||||||
|
else -> { { onAction(action) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
val titleOverride = when (action.target) {
|
||||||
|
HomeActionTarget.DailyQuestion -> when (dailyQuestionState) {
|
||||||
|
DailyQuestionState.UNANSWERED -> "Tonight's question is ready."
|
||||||
|
DailyQuestionState.USER_ANSWERED_PARTNER_PENDING -> "You showed up tonight. Waiting for your partner."
|
||||||
|
DailyQuestionState.PARTNER_ANSWERED_USER_PENDING -> "Your partner answered. Your turn."
|
||||||
|
DailyQuestionState.BOTH_ANSWERED -> "Your reveal is waiting"
|
||||||
|
DailyQuestionState.REVEALED -> "You opened a conversation tonight."
|
||||||
|
}
|
||||||
|
else -> action.title
|
||||||
|
}
|
||||||
|
|
||||||
|
val bodyOverride = when (action.target) {
|
||||||
|
HomeActionTarget.DailyQuestion -> when (dailyQuestionState) {
|
||||||
|
DailyQuestionState.UNANSWERED ->
|
||||||
|
dailyQuestion?.text ?: "Answer tonight's question privately, then choose when to share."
|
||||||
|
DailyQuestionState.USER_ANSWERED_PARTNER_PENDING ->
|
||||||
|
"Your answer is private until they answer too. No pressure — the reveal waits for both of you."
|
||||||
|
DailyQuestionState.PARTNER_ANSWERED_USER_PENDING ->
|
||||||
|
"Answer to unlock the reveal. Your response stays private until you are ready."
|
||||||
|
DailyQuestionState.BOTH_ANSWERED ->
|
||||||
|
"You both answered — open it together when you're ready."
|
||||||
|
DailyQuestionState.REVEALED ->
|
||||||
|
"You revealed an answer together. What comes next is up to both of you."
|
||||||
|
}
|
||||||
|
else -> action.body
|
||||||
|
}
|
||||||
|
|
||||||
|
val timeBudgetLabel = when (action.target) {
|
||||||
|
HomeActionTarget.DailyQuestion -> when (dailyQuestion?.type) {
|
||||||
|
"scale" -> "~1 min"
|
||||||
|
"single_choice" -> "~2 min"
|
||||||
|
"this_or_that" -> "~2 min"
|
||||||
|
else -> "~3 min"
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
CloserCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(CloserRadii.FeatureCard),
|
||||||
|
containerColor = closerCardColor(alpha = 0.92f),
|
||||||
|
elevation = CloserElevations.Feature
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
Brush.linearGradient(
|
||||||
|
if (isDark) {
|
||||||
|
listOf(
|
||||||
|
MaterialTheme.colorScheme.surface,
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
colors.soft.copy(alpha = 0.72f)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
MaterialTheme.colorScheme.surface,
|
||||||
|
colors.soft,
|
||||||
|
CloserPalette.PinkMist.copy(alpha = 0.72f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
start = Offset.Zero,
|
||||||
|
end = Offset.Infinite
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
HomePill(action.eyebrow)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
timeBudgetLabel?.let { label ->
|
||||||
|
HomePill(label)
|
||||||
|
}
|
||||||
|
action.metric?.let { HomePill(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
artRes?.let { res ->
|
||||||
|
BrandIllustration(
|
||||||
|
res = res,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(150.dp)
|
||||||
|
.clip(RoundedCornerShape(22.dp))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artRes != null) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = titleOverride,
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = bodyOverride,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 4,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(CloserRadii.Tile),
|
||||||
|
color = colors.accent.copy(alpha = 0.16f),
|
||||||
|
modifier = Modifier.size(52.dp)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
HomeGlyphIcon(
|
||||||
|
resId = homeActionGlyph(action.target),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = colors.deep,
|
||||||
|
modifier = Modifier.size(26.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = titleOverride,
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = bodyOverride,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 4,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CloserActionButton(
|
||||||
|
label = action.cta,
|
||||||
|
onClick = ctaClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
containerColor = colors.accent,
|
||||||
|
contentColor = colors.onAccent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue