refactor: CloserPrimitives design system, PairPromptScreen, state component overhauls (batch v0.2.1)

- Add CloserPrimitives (CloserCard, CloserActionButton, CloserSpacing, CloserButtonStyle, SkeletonLine)
- Refactor EmptyState, ErrorState, LoadingState to use primitives with closer design tokens
- Add PairPromptScreen + nav route for post-signup partner invitation
- Update SignUpScreen onboarding copy to mention partner invite flow
- HomeScreen and PlayHubScreen layout/structure refinements
This commit is contained in:
null 2026-06-19 04:04:52 -05:00
parent 164e0e47ca
commit 79a63629f1
11 changed files with 644 additions and 299 deletions

View File

@ -42,6 +42,7 @@ import app.closer.ui.pairing.AcceptInviteScreen
import app.closer.ui.pairing.CreateInviteScreen
import app.closer.ui.pairing.EmailInviteScreen
import app.closer.ui.pairing.InviteConfirmScreen
import app.closer.ui.pairing.PairPromptScreen
import app.closer.ui.dates.DateMatchScreen
import app.closer.ui.dates.DateMatchesScreen
import app.closer.ui.dates.DateBuilderScreen
@ -161,6 +162,9 @@ fun AppNavigation(
composable(route = AppRoute.CREATE_PROFILE) {
CreateProfileScreen(onNavigate = navigateRoute)
}
composable(route = AppRoute.PAIR_PROMPT) {
PairPromptScreen(onNavigate = navigateRoute)
}
// Auth
composable(route = AppRoute.LOGIN) {

View File

@ -8,6 +8,7 @@ object AppRoute {
const val SIGN_UP = "sign_up"
const val FORGOT_PASSWORD = "forgot_password"
const val CREATE_PROFILE = "create_profile"
const val PAIR_PROMPT = "pair_prompt"
const val HOME = "home"
const val PARTNER_HOME = "partner_home"
const val PLAY = "play"
@ -62,6 +63,7 @@ object AppRoute {
val definitions = listOf(
Definition(ONBOARDING, "Onboarding", "onboarding"),
Definition(CREATE_PROFILE, "Create Profile", "onboarding"),
Definition(PAIR_PROMPT, "Invite Partner", "onboarding"),
Definition(LOGIN, "Login", "auth"),
Definition(SIGN_UP, "Sign Up", "auth"),
Definition(FORGOT_PASSWORD, "Forgot Password", "auth"),

View File

@ -110,7 +110,7 @@ fun SignUpScreen(
)
Spacer(Modifier.height(8.dp))
Text(
text = "You'll set your name after signing up.",
text = "Takes about a minute. You'll set your name and invite your partner right after.",
style = MaterialTheme.typography.bodyMedium,
color = AuthMuted,
textAlign = TextAlign.Center

View File

@ -0,0 +1,224 @@
package app.closer.ui.components
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerCardColor
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
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.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
object CloserSpacing {
val Xs = 4.dp
val Sm = 8.dp
val Md = 12.dp
val Lg = 16.dp
val Xl = 20.dp
val Xxl = 24.dp
val Xxxl = 32.dp
}
object CloserRadii {
val Button = 16.dp
val Card = 24.dp
val FeatureCard = 30.dp
val Pill = 999.dp
val Tile = 18.dp
}
object CloserElevations {
val Flat = 0.dp
val Card = 4.dp
val Feature = 10.dp
}
enum class CloserButtonStyle {
Primary,
Secondary,
Destructive
}
@Composable
fun CloserCard(
modifier: Modifier = Modifier,
containerColor: Color = closerCardColor(alpha = 0.9f),
contentColor: Color = MaterialTheme.colorScheme.onSurface,
shape: RoundedCornerShape = RoundedCornerShape(CloserRadii.Card),
elevation: Dp = CloserElevations.Card,
content: @Composable () -> Unit
) {
Card(
modifier = modifier,
shape = shape,
colors = CardDefaults.cardColors(
containerColor = containerColor,
contentColor = contentColor
),
elevation = CardDefaults.cardElevation(defaultElevation = elevation)
) {
content()
}
}
@Composable
fun CloserClickableCard(
onClick: () -> Unit,
modifier: Modifier = Modifier,
containerColor: Color = closerCardColor(alpha = 0.9f),
contentColor: Color = MaterialTheme.colorScheme.onSurface,
shape: RoundedCornerShape = RoundedCornerShape(CloserRadii.Card),
elevation: Dp = CloserElevations.Card,
content: @Composable () -> Unit
) {
Card(
onClick = onClick,
modifier = modifier,
shape = shape,
colors = CardDefaults.cardColors(
containerColor = containerColor,
contentColor = contentColor
),
elevation = CardDefaults.cardElevation(defaultElevation = elevation)
) {
content()
}
}
@Composable
fun CloserActionButton(
label: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
style: CloserButtonStyle = CloserButtonStyle.Primary,
enabled: Boolean = true,
containerColor: Color? = null,
contentColor: Color? = null
) {
val shape = RoundedCornerShape(CloserRadii.Button)
when (style) {
CloserButtonStyle.Primary -> Button(
onClick = onClick,
modifier = modifier,
enabled = enabled,
shape = shape,
colors = ButtonDefaults.buttonColors(
containerColor = containerColor ?: MaterialTheme.colorScheme.primary,
contentColor = contentColor ?: MaterialTheme.colorScheme.onPrimary
)
) {
Text(label)
}
CloserButtonStyle.Secondary -> OutlinedButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
shape = shape,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.34f)),
colors = ButtonDefaults.outlinedButtonColors(
containerColor = containerColor ?: MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.42f),
contentColor = contentColor ?: CloserPalette.PurpleDeep
)
) {
Text(label)
}
CloserButtonStyle.Destructive -> OutlinedButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
shape = shape,
border = BorderStroke(1.dp, CloserPalette.Danger.copy(alpha = 0.38f)),
colors = ButtonDefaults.outlinedButtonColors(
containerColor = containerColor ?: CloserPalette.PinkMist,
contentColor = contentColor ?: CloserPalette.Danger
)
) {
Text(label)
}
}
}
@Composable
fun CloserPill(
label: String,
modifier: Modifier = Modifier,
containerColor: Color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.55f),
contentColor: Color = CloserPalette.PurpleDeep
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(CloserRadii.Pill),
color = containerColor,
contentColor = contentColor
) {
Text(
text = label,
modifier = Modifier.padding(horizontal = CloserSpacing.Md, vertical = CloserSpacing.Xs),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.SemiBold
)
}
}
@Composable
fun CloserIconTile(
modifier: Modifier = Modifier,
containerColor: Color = MaterialTheme.colorScheme.primaryContainer,
contentColor: Color = MaterialTheme.colorScheme.primary,
content: @Composable RowScope.() -> Unit
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(CloserRadii.Tile),
color = containerColor,
contentColor = contentColor
) {
Row(
modifier = Modifier.padding(CloserSpacing.Md),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
content = content
)
}
}
@Composable
fun CloserSectionHeader(
title: String,
modifier: Modifier = Modifier,
action: (@Composable () -> Unit)? = null
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
)
Box {
action?.invoke()
}
}
}

View File

@ -5,18 +5,11 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun EmptyState(
@ -26,14 +19,13 @@ fun EmptyState(
onAction: (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
Card(
CloserCard(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(28.dp),
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.86f))
containerColor = closerCardColor(alpha = 0.86f)
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
modifier = Modifier.padding(CloserSpacing.Xl),
verticalArrangement = Arrangement.spacedBy(CloserSpacing.Md)
) {
Text(
text = title,
@ -47,17 +39,11 @@ fun EmptyState(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (actionLabel != null && onAction != null) {
Button(
CloserActionButton(
label = actionLabel,
onClick = onAction,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = MaterialTheme.colorScheme.onPrimary
modifier = Modifier.fillMaxWidth()
)
) {
Text(actionLabel)
}
}
}
}

View File

@ -1,20 +1,15 @@
package app.closer.ui.components
import app.closer.ui.theme.CloserPalette
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun ErrorState(
@ -24,20 +19,19 @@ fun ErrorState(
onRetry: (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
Card(
CloserCard(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(28.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFFFEEF7))
containerColor = CloserPalette.PinkMist
) {
Column(
modifier = Modifier.padding(22.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
modifier = Modifier.padding(CloserSpacing.Xxl),
verticalArrangement = Arrangement.spacedBy(CloserSpacing.Md)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = Color(0xFF8D2D35)
color = CloserPalette.Danger
)
Text(
text = message,
@ -45,13 +39,12 @@ fun ErrorState(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (onRetry != null) {
OutlinedButton(
CloserActionButton(
label = retryLabel,
onClick = onRetry,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp)
) {
Text(retryLabel)
}
style = CloserButtonStyle.Destructive
)
}
}
}

View File

@ -1,20 +1,28 @@
package app.closer.ui.components
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerCardColor
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
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.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.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@ -23,21 +31,32 @@ fun LoadingState(
message: String = "Loading…",
modifier: Modifier = Modifier
) {
Card(
val shimmer = closerSkeletonBrush()
CloserCard(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(28.dp),
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.8f))
containerColor = closerCardColor(alpha = 0.8f)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
.padding(CloserSpacing.Xxxl),
verticalArrangement = Arrangement.spacedBy(CloserSpacing.Md)
) {
CircularProgressIndicator(
modifier = Modifier.size(34.dp),
color = Color(0xFFB98AF4)
SkeletonLine(
brush = shimmer,
modifier = Modifier.fillMaxWidth(0.42f),
height = 18.dp
)
SkeletonLine(
brush = shimmer,
modifier = Modifier.fillMaxWidth(),
height = 13.dp
)
SkeletonLine(
brush = shimmer,
modifier = Modifier.fillMaxWidth(0.72f),
height = 13.dp
)
Text(
text = message,
@ -48,3 +67,41 @@ fun LoadingState(
}
}
}
@Composable
private fun closerSkeletonBrush(): Brush {
val transition = rememberInfiniteTransition(label = "closerSkeleton")
val shimmerOffset = transition.animateFloat(
initialValue = -320f,
targetValue = 640f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1200, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "closerSkeletonOffset"
)
return Brush.linearGradient(
colors = listOf(
CloserPalette.PurpleMist.copy(alpha = 0.55f),
CloserPalette.PurpleSoft.copy(alpha = 0.95f),
CloserPalette.PinkMist.copy(alpha = 0.72f)
),
start = Offset(shimmerOffset.value, 0f),
end = Offset(shimmerOffset.value + 280f, 0f)
)
}
@Composable
private fun SkeletonLine(
brush: Brush,
modifier: Modifier = Modifier,
height: androidx.compose.ui.unit.Dp
) {
Box(
modifier = modifier
.height(height)
.clip(RoundedCornerShape(CloserRadii.Pill))
.background(brush)
)
}

View File

@ -1,7 +1,22 @@
package app.closer.ui.home
import app.closer.ui.theme.closerCardColor
import app.closer.core.navigation.AppRoute
import app.closer.domain.model.Question
import app.closer.domain.model.QuestionCategory
import app.closer.ui.components.CategoryGlyph
import app.closer.ui.components.CloserActionButton
import app.closer.ui.components.CloserButtonStyle
import app.closer.ui.components.CloserCard
import app.closer.ui.components.CloserClickableCard
import app.closer.ui.components.CloserElevations
import app.closer.ui.components.CloserPill
import app.closer.ui.components.CloserRadii
import app.closer.ui.components.ErrorState
import app.closer.ui.components.LoadingState
import app.closer.ui.questions.displayCategoryName
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerCardColor
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -19,19 +34,15 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@ -41,16 +52,9 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import app.closer.core.navigation.AppRoute
import app.closer.domain.model.Question
import app.closer.domain.model.QuestionCategory
import app.closer.ui.components.CategoryGlyph
import app.closer.ui.questions.displayCategoryName
import androidx.hilt.navigation.compose.hiltViewModel
@Composable
fun HomeScreen(
@ -211,17 +215,17 @@ private fun PrimaryHomeActionCard(
) {
val colors = action.tone.actionColors()
Card(
CloserCard(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(32.dp),
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.92f)),
elevation = CardDefaults.cardElevation(defaultElevation = 18.dp)
shape = RoundedCornerShape(CloserRadii.FeatureCard),
containerColor = closerCardColor(alpha = 0.92f),
elevation = CloserElevations.Feature
) {
Column(
modifier = Modifier
.background(
Brush.linearGradient(
listOf(Color.White, colors.soft, Color(0xFFFFF7FB)),
listOf(MaterialTheme.colorScheme.surface, colors.soft, CloserPalette.PinkMist.copy(alpha = 0.72f)),
start = Offset.Zero,
end = Offset.Infinite
)
@ -243,7 +247,7 @@ private fun PrimaryHomeActionCard(
verticalAlignment = Alignment.Top
) {
Surface(
shape = RoundedCornerShape(20.dp),
shape = RoundedCornerShape(CloserRadii.Tile),
color = colors.accent.copy(alpha = 0.16f),
modifier = Modifier.size(52.dp)
) {
@ -280,17 +284,13 @@ private fun PrimaryHomeActionCard(
HomePulseStrip(stats = stats, streakCount = streakCount)
Button(
CloserActionButton(
label = action.cta,
onClick = { onAction(action) },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(
containerColor = colors.accent,
contentColor = colors.onAccent
)
) {
Text(action.cta)
}
containerColor = colors.accent,
contentColor = colors.onAccent
)
}
}
}
@ -330,8 +330,8 @@ private fun PulseMetric(
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(18.dp),
color = Color.White.copy(alpha = 0.66f)
shape = RoundedCornerShape(CloserRadii.Tile),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.66f)
) {
Column(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
@ -340,7 +340,7 @@ private fun PulseMetric(
Text(
text = value,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF56336F),
color = CloserPalette.PurpleDeep,
maxLines = 1
)
Text(
@ -365,7 +365,7 @@ private fun ActionFeedSection(
Text(
text = "After that",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF2C2233)
color = MaterialTheme.colorScheme.onSurface
)
actions.forEach { action ->
SecondaryHomeActionCard(action = action, onAction = onAction)
@ -380,12 +380,12 @@ private fun SecondaryHomeActionCard(
) {
val colors = action.tone.actionColors()
Card(
CloserClickableCard(
onClick = { onAction(action) },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
shape = RoundedCornerShape(CloserRadii.Card),
containerColor = MaterialTheme.colorScheme.surface,
elevation = CloserElevations.Card
) {
Row(
modifier = Modifier
@ -397,7 +397,7 @@ private fun SecondaryHomeActionCard(
Box(
modifier = Modifier
.size(40.dp)
.background(colors.soft, RoundedCornerShape(15.dp)),
.background(colors.soft, RoundedCornerShape(CloserRadii.Button)),
contentAlignment = Alignment.Center
) {
Icon(
@ -435,7 +435,7 @@ private fun SecondaryHomeActionCard(
)
}
Surface(
shape = RoundedCornerShape(14.dp),
shape = RoundedCornerShape(CloserRadii.Button),
color = colors.soft
) {
Icon(
@ -453,10 +453,11 @@ private fun SecondaryHomeActionCard(
@Composable
private fun MomentCueCard() {
Surface(
CloserCard(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
color = Color(0xFFFFF8FC)
shape = RoundedCornerShape(CloserRadii.Card),
containerColor = CloserPalette.PinkMist.copy(alpha = 0.7f),
elevation = CloserElevations.Flat
) {
Column(
modifier = Modifier.padding(17.dp),
@ -489,42 +490,43 @@ private data class HomeActionColors(
val onAccent: Color = Color(0xFF24122F)
)
@Composable
private fun HomeActionTone.actionColors(): HomeActionColors =
when (this) {
HomeActionTone.Invite -> HomeActionColors(
soft = Color(0xFFF4E8FF),
accent = Color(0xFFB98AF4),
deep = Color(0xFF56306F)
soft = CloserPalette.PurpleSoft,
accent = MaterialTheme.colorScheme.primary,
deep = CloserPalette.PurpleDeep
)
HomeActionTone.Daily -> HomeActionColors(
soft = Color(0xFFFFE9F4),
accent = Color(0xFFE7A2D1),
deep = Color(0xFF6D2B55)
soft = CloserPalette.PinkSoft,
accent = CloserPalette.PinkBright,
deep = CloserPalette.PinkAccentDeep
)
HomeActionTone.Reflection -> HomeActionColors(
soft = Color(0xFFF6E8FF),
accent = Color(0xFFC89AF2),
deep = Color(0xFF56306F)
soft = CloserPalette.PurpleGlow,
accent = CloserPalette.PurpleRich,
deep = CloserPalette.PurpleDeep
)
HomeActionTone.Ritual -> HomeActionColors(
soft = Color(0xFFF4E8FF),
accent = Color(0xFFE7A2D1),
deep = Color(0xFF6D2B55)
soft = CloserPalette.PurpleSoft,
accent = CloserPalette.PinkBright,
deep = CloserPalette.PinkAccentDeep
)
HomeActionTone.Starter -> HomeActionColors(
soft = Color(0xFFFFE8F4),
accent = Color(0xFFE7A2D1),
deep = Color(0xFF6D2B55)
soft = CloserPalette.PinkSoft,
accent = CloserPalette.PinkBright,
deep = CloserPalette.PinkAccentDeep
)
HomeActionTone.Pack -> HomeActionColors(
soft = Color(0xFFFFE8F4),
accent = Color(0xFFE7A2D1),
deep = Color(0xFF6D2B55)
soft = CloserPalette.PinkSoft,
accent = CloserPalette.PinkBright,
deep = CloserPalette.PinkAccentDeep
)
HomeActionTone.Utility -> HomeActionColors(
soft = Color(0xFFF4E8FF),
accent = Color(0xFFB98AF4),
deep = Color(0xFF56306F)
soft = CloserPalette.PurpleSoft,
accent = MaterialTheme.colorScheme.primary,
deep = CloserPalette.PurpleDeep
)
}
@ -546,15 +548,14 @@ private fun CategoryPreviewGrid(
Text(
text = "More doorways",
style = MaterialTheme.typography.titleMedium,
color = Color(0xFF2C2233),
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold
)
OutlinedButton(
CloserActionButton(
label = "All packs",
onClick = onPacks,
shape = RoundedCornerShape(14.dp)
) {
Text("All packs")
}
style = CloserButtonStyle.Secondary
)
}
featuredCategories.chunked(2).forEach { rowItems ->
@ -580,12 +581,12 @@ private fun CategoryMiniCard(
modifier: Modifier,
onClick: () -> Unit
) {
Card(
CloserClickableCard(
onClick = onClick,
modifier = modifier,
shape = RoundedCornerShape(22.dp),
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.82f)),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
containerColor = closerCardColor(alpha = 0.82f),
shape = RoundedCornerShape(CloserRadii.Card),
elevation = CloserElevations.Card
) {
Column(
modifier = Modifier.padding(15.dp),
@ -618,24 +619,7 @@ private fun CategoryMiniCard(
@Composable
private fun LoadingHomeCard() {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(26.dp),
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.84f))
) {
Row(
modifier = Modifier.padding(22.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp)
) {
CircularProgressIndicator(color = Color(0xFFB98AF4))
Text(
text = "Opening your dashboard",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
LoadingState(message = "Opening your dashboard")
}
@Composable
@ -643,55 +627,21 @@ private fun ErrorHomeCard(
message: String,
onRefresh: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(26.dp),
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.84f))
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Home paused",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold
)
Text(
text = message,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Button(
onClick = onRefresh,
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = MaterialTheme.colorScheme.onPrimary
)
) {
Text("Retry")
}
}
}
ErrorState(
title = "Home paused",
message = message,
retryLabel = "Retry",
onRetry = onRefresh
)
}
@Composable
private fun HomePill(label: String) {
Surface(
shape = RoundedCornerShape(999.dp),
color = Color(0xFFFFF8FC)
) {
Text(
text = label,
modifier = Modifier.padding(horizontal = 11.dp, vertical = 7.dp),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
CloserPill(
label = label,
containerColor = CloserPalette.PinkMist.copy(alpha = 0.72f),
contentColor = MaterialTheme.colorScheme.onSurface
)
}
@Preview

View File

@ -93,7 +93,7 @@ fun CreateProfileScreen(
val context = LocalContext.current
LaunchedEffect(state.success) {
if (state.success) onNavigate(AppRoute.HOME)
if (state.success) onNavigate(AppRoute.PAIR_PROMPT)
}
LaunchedEffect(state.error) {
state.error?.let { snackbar.showSnackbar(it); viewModel.dismissError() }
@ -274,7 +274,7 @@ private fun SexStep(
)
Spacer(Modifier.height(12.dp))
Text(
text = "Some questions in Desire Sync and other features are tailored differently based on your sex. This helps us show you relevant questions.",
text = "Some questions — especially in Desire Sync — are tailored based on your sex. This just helps us show you the right ones.",
style = MaterialTheme.typography.bodyMedium,
color = AuthMuted,
textAlign = TextAlign.Center

View File

@ -0,0 +1,165 @@
package app.closer.ui.pairing
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import app.closer.core.navigation.AppRoute
import app.closer.ui.auth.AuthBackgroundBrush
import app.closer.ui.auth.AuthInk
import app.closer.ui.auth.AuthMuted
import app.closer.ui.auth.AuthOnPrimary
import app.closer.ui.auth.AuthPrimary
import app.closer.ui.auth.AuthPrimaryDeep
import app.closer.ui.theme.CloserPalette
private val WHY_ITEMS = listOf(
"Daily questions reveal together — both answer privately, then see each other's response at the same time.",
"Couples games: This or That, How Well Do You Know Me, Desire Sync, and more.",
"A shared timeline of answers, moments, and milestones you build together over time."
)
@Composable
fun PairPromptScreen(
onNavigate: (String) -> Unit = {}
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(AuthBackgroundBrush)
.safeDrawingPadding()
.navigationBarsPadding()
.verticalScroll(rememberScrollState())
.padding(horizontal = 28.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.height(56.dp))
Surface(
shape = RoundedCornerShape(20.dp),
color = AuthPrimary.copy(alpha = 0.12f)
) {
Text(
text = "You're in",
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
style = MaterialTheme.typography.labelLarge,
color = AuthPrimaryDeep,
fontWeight = FontWeight.SemiBold
)
}
Spacer(Modifier.height(20.dp))
Text(
text = "Now bring\nyour person in.",
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
color = AuthInk,
textAlign = TextAlign.Center
)
Spacer(Modifier.height(16.dp))
Text(
text = "Closer is built for two. Connect your partner to unlock everything worth unlocking:",
style = MaterialTheme.typography.bodyLarge,
color = AuthMuted,
textAlign = TextAlign.Center
)
Spacer(Modifier.height(28.dp))
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
WHY_ITEMS.forEach { item ->
WhyRow(text = item)
}
}
Spacer(Modifier.height(40.dp))
Button(
onClick = { onNavigate(AppRoute.CREATE_INVITE) },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = AuthPrimary,
contentColor = AuthOnPrimary
)
) {
Text("Invite my partner", style = MaterialTheme.typography.labelLarge)
}
Spacer(Modifier.height(12.dp))
TextButton(
onClick = { onNavigate(AppRoute.HOME) },
modifier = Modifier.fillMaxWidth()
) {
Text(
"I'll do this later",
style = MaterialTheme.typography.bodyMedium,
color = AuthMuted
)
}
Spacer(Modifier.height(32.dp))
}
}
@Composable
private fun WhyRow(text: String) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Top
) {
Surface(
shape = RoundedCornerShape(8.dp),
color = CloserPalette.PurpleDeep,
modifier = Modifier.size(22.dp)
) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
tint = AuthOnPrimary,
modifier = Modifier
.padding(4.dp)
.size(14.dp)
)
}
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
color = AuthInk.copy(alpha = 0.82f)
)
}
}

View File

@ -22,10 +22,6 @@ import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@ -41,6 +37,12 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import app.closer.core.navigation.AppRoute
import app.closer.ui.components.CategoryGlyph
import app.closer.ui.components.CloserActionButton
import app.closer.ui.components.CloserButtonStyle
import app.closer.ui.components.CloserClickableCard
import app.closer.ui.components.CloserElevations
import app.closer.ui.components.CloserPill
import app.closer.ui.components.CloserRadii
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerBrandGlyphBrush
@ -183,14 +185,14 @@ private fun PlayHubContent(
private fun ThisOrThatCard(
onClick: () -> Unit
) {
Card(
CloserClickableCard(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 132.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
shape = RoundedCornerShape(CloserRadii.Card),
containerColor = MaterialTheme.colorScheme.surface,
elevation = CloserElevations.Card
) {
Row(
modifier = Modifier
@ -200,7 +202,7 @@ private fun ThisOrThatCard(
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = RoundedCornerShape(18.dp),
shape = RoundedCornerShape(CloserRadii.Tile),
color = CloserPalette.PinkMist,
modifier = Modifier.size(52.dp)
) {
@ -228,18 +230,11 @@ private fun ThisOrThatCard(
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Surface(
shape = RoundedCornerShape(999.dp),
color = CloserPalette.PinkMist
) {
Text(
text = "10 prompts",
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
color = CloserPalette.PinkAccentDeep,
fontWeight = FontWeight.SemiBold
)
}
CloserPill(
label = "10 prompts",
containerColor = CloserPalette.PinkMist,
contentColor = CloserPalette.PinkAccentDeep
)
}
Text(
text = "Rapid-fire A or B choices. See where you and your partner land.",
@ -263,12 +258,12 @@ private fun ThisOrThatCard(
private fun DesireSyncCard(
onClick: () -> Unit
) {
Card(
CloserClickableCard(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
shape = RoundedCornerShape(CloserRadii.Card),
containerColor = MaterialTheme.colorScheme.surface,
elevation = CloserElevations.Card
) {
Row(
modifier = Modifier
@ -300,7 +295,7 @@ private fun DesireSyncCard(
overflow = TextOverflow.Ellipsis
)
Surface(
shape = RoundedCornerShape(999.dp),
shape = RoundedCornerShape(CloserRadii.Pill),
color = CloserPalette.Romantic.copy(alpha = 0.12f)
) {
Row(
@ -345,12 +340,12 @@ private fun DesireSyncCard(
private fun HowWellCard(
onClick: () -> Unit
) {
Card(
CloserClickableCard(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
shape = RoundedCornerShape(CloserRadii.Card),
containerColor = MaterialTheme.colorScheme.surface,
elevation = CloserElevations.Card
) {
Row(
modifier = Modifier
@ -382,18 +377,11 @@ private fun HowWellCard(
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Surface(
shape = RoundedCornerShape(999.dp),
color = CloserPalette.Romantic.copy(alpha = 0.12f)
) {
Text(
text = "10 rounds",
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
color = CloserPalette.Romantic,
fontWeight = FontWeight.SemiBold
)
}
CloserPill(
label = "10 rounds",
containerColor = CloserPalette.Romantic.copy(alpha = 0.12f),
contentColor = CloserPalette.Romantic
)
}
Text(
text = "One answers, the other predicts. Find out how well you really know each other.",
@ -417,14 +405,14 @@ private fun HowWellCard(
private fun ConnectionChallengesCard(
onClick: () -> Unit
) {
Card(
CloserClickableCard(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 132.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
shape = RoundedCornerShape(CloserRadii.Card),
containerColor = MaterialTheme.colorScheme.surface,
elevation = CloserElevations.Card
) {
Row(
modifier = Modifier
@ -434,7 +422,7 @@ private fun ConnectionChallengesCard(
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = RoundedCornerShape(18.dp),
shape = RoundedCornerShape(CloserRadii.Tile),
color = CloserPalette.PurpleDeep.copy(alpha = 0.12f),
modifier = Modifier.size(52.dp)
) {
@ -461,18 +449,11 @@ private fun ConnectionChallengesCard(
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Surface(
shape = RoundedCornerShape(999.dp),
color = CloserPalette.PurpleDeep.copy(alpha = 0.10f)
) {
Text(
text = "7 days",
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
color = CloserPalette.PurpleDeep,
fontWeight = FontWeight.SemiBold
)
}
CloserPill(
label = "7 days",
containerColor = CloserPalette.PurpleDeep.copy(alpha = 0.10f),
contentColor = CloserPalette.PurpleDeep
)
}
Text(
text = "Pick a series and build a small habit together, one day at a time.",
@ -496,14 +477,14 @@ private fun ConnectionChallengesCard(
private fun MemoryLaneCard(
onClick: () -> Unit
) {
Card(
CloserClickableCard(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 132.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
shape = RoundedCornerShape(CloserRadii.Card),
containerColor = MaterialTheme.colorScheme.surface,
elevation = CloserElevations.Card
) {
Row(
modifier = Modifier
@ -513,7 +494,7 @@ private fun MemoryLaneCard(
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = RoundedCornerShape(18.dp),
shape = RoundedCornerShape(CloserRadii.Tile),
color = CloserPalette.Romantic.copy(alpha = 0.12f),
modifier = Modifier.size(52.dp)
) {
@ -555,12 +536,12 @@ private fun MemoryLaneCard(
private fun FeaturedPlayCard(
onClick: () -> Unit
) {
Card(
CloserClickableCard(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(30.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)),
elevation = CardDefaults.cardElevation(defaultElevation = 12.dp)
shape = RoundedCornerShape(CloserRadii.FeatureCard),
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f),
elevation = CloserElevations.Feature
) {
Column(
modifier = Modifier
@ -573,30 +554,16 @@ private fun FeaturedPlayCard(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = RoundedCornerShape(999.dp),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.72f)
) {
Text(
text = "Wheel",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 7.dp),
style = MaterialTheme.typography.labelMedium,
color = CloserPalette.PurpleDeep,
fontWeight = FontWeight.SemiBold
)
}
Surface(
shape = RoundedCornerShape(999.dp),
color = MaterialTheme.colorScheme.secondaryContainer
) {
Text(
text = "10 prompts",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 7.dp),
style = MaterialTheme.typography.labelMedium,
color = CloserPalette.PinkAccentDeep,
fontWeight = FontWeight.SemiBold
)
}
CloserPill(
label = "Wheel",
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.72f),
contentColor = CloserPalette.PurpleDeep
)
CloserPill(
label = "10 prompts",
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = CloserPalette.PinkAccentDeep
)
}
Row(
@ -628,19 +595,16 @@ private fun FeaturedPlayCard(
}
}
Button(
CloserActionButton(
label = "Choose category",
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 54.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(
containerColor = CloserPalette.PurpleDeep,
contentColor = MaterialTheme.colorScheme.surface
)
) {
Text("Choose category")
}
style = CloserButtonStyle.Primary,
containerColor = CloserPalette.PurpleDeep,
contentColor = MaterialTheme.colorScheme.surface
)
}
}
}
@ -654,12 +618,12 @@ private fun CompactPlayCard(
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
Card(
CloserClickableCard(
onClick = onClick,
modifier = modifier.heightIn(min = 154.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
shape = RoundedCornerShape(CloserRadii.Card),
containerColor = MaterialTheme.colorScheme.surface,
elevation = CloserElevations.Card
) {
Column(
modifier = Modifier
@ -668,7 +632,7 @@ private fun CompactPlayCard(
verticalArrangement = Arrangement.SpaceBetween
) {
Surface(
shape = RoundedCornerShape(18.dp),
shape = RoundedCornerShape(CloserRadii.Tile),
color = tint.copy(alpha = 0.14f),
modifier = Modifier.size(46.dp)
) {