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.HomeStatusStrip
|
||||
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.actionColors
|
||||
import app.closer.ui.home.components.homeActionGlyph
|
||||
|
|
@ -731,358 +733,6 @@ private fun PartnerSheetAction(
|
|||
private fun formatMonthYear(millis: Long): String =
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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