fix(answers): update answer flow screens, navigation, and question input component

This commit is contained in:
null 2026-06-16 03:39:40 -05:00
parent 56f2d8c045
commit 0f73c656d8
6 changed files with 192 additions and 93 deletions

View File

@ -74,9 +74,20 @@ fun AppNavigation(
val shellTitle = currentRoute
?.takeIf { it in shellBackRoutes }
?.let(AppRoute::titleFor)
val navigateBackOrHome: () -> Unit = {
if (!navController.popBackStack()) {
navController.navigate(AppRoute.HOME) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
}
val navigateRoute: (String) -> Unit = { route ->
if (route == "back") {
navController.popBackStack()
navigateBackOrHome()
} else {
navController.navigate(route)
}
@ -94,7 +105,7 @@ fun AppNavigation(
)
},
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
IconButton(onClick = navigateBackOrHome) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
@ -200,7 +211,7 @@ fun AppNavigation(
previousQuestionId = it.arguments?.getString("prevId"),
nextQuestionId = it.arguments?.getString("nextId"),
onNavigate = navigateRoute,
onBack = { navController.popBackStack() }
onBack = navigateBackOrHome
)
}

View File

@ -13,15 +13,21 @@ import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
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.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
@ -58,12 +64,43 @@ private fun AnswerHistoryContent(
onDailyQuestion: () -> Unit,
onDelete: (String) -> Unit
) {
var pendingDelete by remember { mutableStateOf<LocalAnswer?>(null) }
pendingDelete?.let { answer ->
AlertDialog(
onDismissRequest = { pendingDelete = null },
title = { Text("Remove this answer?") },
text = {
Text("This removes the saved reflection from this device. The prompt itself will stay available.")
},
confirmButton = {
Button(
onClick = {
onDelete(answer.questionId)
pendingDelete = null
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF8D2D35),
contentColor = Color.White
)
) {
Text("Remove")
}
},
dismissButton = {
TextButton(onClick = { pendingDelete = null }) {
Text("Keep")
}
}
)
}
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.linearGradient(
listOf(Color(0xFFFFFBFA), Color(0xFFF3F7F1), Color(0xFFEAF0F4)),
listOf(Color(0xFFFFFBFE), Color(0xFFF8F1FF), Color(0xFFFFEEF7)),
start = Offset.Zero,
end = Offset.Infinite
)
@ -109,7 +146,7 @@ private fun AnswerHistoryContent(
AnswerHistoryCard(
answer = answer,
onClick = { onAnswerSelected(answer) },
onDelete = { onDelete(answer.questionId) }
onDelete = { pendingDelete = answer }
)
}
}
@ -128,7 +165,7 @@ private fun AnswerHistoryCard(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.86f)),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
) {
Column(
@ -154,12 +191,22 @@ private fun AnswerHistoryCard(
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
OutlinedButton(
onClick = onDelete,
Row(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp)
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Remove answer")
Text(
text = if (answer.isRevealed) "Opened" else "Private",
style = MaterialTheme.typography.labelMedium,
color = Color(0xFF6B4A86),
fontWeight = FontWeight.SemiBold
)
TextButton(onClick = onDelete) {
Text(
text = "Remove",
color = Color(0xFF8D2D35)
)
}
}
}
}

View File

@ -74,7 +74,7 @@ private fun AnswerRevealContent(
.fillMaxSize()
.background(
Brush.linearGradient(
listOf(Color(0xFFFFFBFA), Color(0xFFF3F7F1), Color(0xFFEAF0F4)),
listOf(Color(0xFFFFFBFE), Color(0xFFF8F1FF), Color(0xFFFFEEF7)),
start = Offset.Zero,
end = Offset.Infinite
)
@ -89,16 +89,7 @@ private fun AnswerRevealContent(
.padding(horizontal = 20.dp, vertical = 20.dp),
verticalArrangement = Arrangement.spacedBy(18.dp)
) {
Text(
text = "Reveal together",
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF27211F)
)
Text(
text = "Open a saved answer when you are ready to look at it together.",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF4E4642)
)
RevealHeader()
when {
state.isLoading -> RevealMessageCard {
@ -151,15 +142,15 @@ private fun NoAnswerState(
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
RevealPill("No answer yet")
Text(
text = question?.text ?: "Question $questionId is ready when you are.",
text = question?.text ?: "This prompt is ready when you are.",
style = MaterialTheme.typography.titleMedium,
color = Color(0xFF27211F),
color = Color(0xFF261D2E),
fontWeight = FontWeight.SemiBold
)
Text(
text = "Answer this prompt first, then come back when you are ready to reveal it.",
text = "Answer privately first. Reveal can wait until there is something worth opening together.",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF4E4642)
color = Color(0xFF5A5060)
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
@ -167,9 +158,9 @@ private fun NoAnswerState(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = Color(0xFF271236)
)
containerColor = Color(0xFFB98AF4),
contentColor = Color(0xFF24122F)
)
) {
Text("Answer")
}
@ -178,7 +169,7 @@ private fun NoAnswerState(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(16.dp)
) {
Text("Home")
Text("Not now")
}
}
}
@ -198,13 +189,14 @@ private fun ReadyToRevealState(
Text(
text = question?.text ?: answer.questionText,
style = MaterialTheme.typography.titleLarge,
color = Color(0xFF27211F),
color = Color(0xFF261D2E),
fontWeight = FontWeight.SemiBold
)
AnswerPreview(answer = answer, revealed = false)
Text(
text = "Your answer is private for now. Reveal it when the moment feels right.",
text = "No rush. Reveal this only when you want the conversation to open.",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF4E4642)
color = Color(0xFF5A5060)
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
@ -212,18 +204,18 @@ private fun ReadyToRevealState(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = Color(0xFF271236)
)
containerColor = Color(0xFFB98AF4),
contentColor = Color(0xFF24122F)
)
) {
Text("Reveal")
Text("Reveal answer")
}
OutlinedButton(
onClick = onHistory,
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(16.dp)
) {
Text("History")
Text("Saved answers")
}
}
}
@ -247,29 +239,21 @@ private fun RevealedState(
Text(
text = question?.text ?: answer.questionText,
style = MaterialTheme.typography.titleLarge,
color = Color(0xFF27211F),
color = Color(0xFF261D2E),
fontWeight = FontWeight.SemiBold
)
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
color = Color(0xFFF8F0FF)
) {
Text(
text = answer.revealSummary(),
modifier = Modifier.padding(18.dp),
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF3E3734)
)
}
AnswerPreview(answer = answer, revealed = true)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
onClick = onHistory,
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF81B29A))
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = Color(0xFF24122F)
)
) {
Text("History")
Text("Saved answers")
}
OutlinedButton(
onClick = onHome,
@ -288,7 +272,7 @@ private fun RevealMessageCard(content: @Composable () -> Unit) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(28.dp),
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.86f)),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp)
) {
Box(modifier = Modifier.padding(20.dp)) {
@ -297,6 +281,51 @@ private fun RevealMessageCard(content: @Composable () -> Unit) {
}
}
@Composable
private fun RevealHeader() {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(
text = "Reveal together",
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF261D2E)
)
Text(
text = "A saved answer can stay private, become a shared reflection, or simply wait for the right moment.",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF5A5060)
)
}
}
@Composable
private fun AnswerPreview(
answer: LocalAnswer,
revealed: Boolean
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
color = if (revealed) Color(0xFFF4E8FF) else Color(0xFFFFF8FC)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = if (revealed) "Opened answer" else "Private preview",
style = MaterialTheme.typography.labelMedium,
color = Color(0xFF56306F),
fontWeight = FontWeight.SemiBold
)
Text(
text = if (revealed) answer.revealSummary() else answer.privatePreview(),
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF3E3346)
)
}
}
}
@Composable
private fun RevealPill(label: String) {
Surface(
@ -323,6 +352,16 @@ fun LocalAnswer.revealSummary(): String {
}
}
private fun LocalAnswer.privatePreview(): String {
return when (answerType) {
"written" -> writtenText?.takeIf { it.isNotBlank() }?.let { "Your written answer is saved." }
?: "Your answer is saved."
"scale" -> "Your scale answer is saved."
"single_choice", "multi_choice", "this_or_that" -> "Your choice is saved."
else -> "Your answer is saved."
}
}
@Preview
@Composable
fun AnswerRevealScreenPreview() {

View File

@ -67,7 +67,7 @@ fun LocalQuestionContent(
modifier: Modifier = Modifier
) {
val background = Brush.linearGradient(
colors = listOf(Color(0xFFFFFBFA), Color(0xFFF3F7F1), Color(0xFFEAF0F4)),
colors = listOf(Color(0xFFFFFBFE), Color(0xFFF8F1FF), Color(0xFFFFEEF7)),
start = Offset.Zero,
end = Offset.Infinite
)
@ -135,7 +135,7 @@ fun LocalQuestionContent(
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFB98AF4),
contentColor = Color(0xFF271236)
contentColor = Color(0xFF24122F)
)
) {
Text(
@ -231,44 +231,46 @@ private fun SubmittedAnswerCard(
question: Question,
state: LocalQuestionUiState
) {
Card(
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFFFFFFF).copy(alpha = 0.86f)),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
shape = RoundedCornerShape(22.dp),
color = Color.White.copy(alpha = 0.78f),
shadowElevation = 0.dp
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
Row(
modifier = Modifier.padding(15.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(34.dp)
.clip(CircleShape)
.background(Color(0xFF81B29A).copy(alpha = 0.22f)),
contentAlignment = Alignment.Center
) {
Text(
text = "OK",
style = MaterialTheme.typography.labelSmall,
color = Color(0xFF2F5D45),
fontWeight = FontWeight.Bold
)
}
Box(
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(Color(0xFFB98AF4).copy(alpha = 0.18f)),
contentAlignment = Alignment.Center
) {
Text(
text = "Saved privately",
modifier = Modifier.padding(start = 10.dp),
style = MaterialTheme.typography.titleSmall,
color = Color(0xFF27211F),
fontWeight = FontWeight.SemiBold
text = "OK",
style = MaterialTheme.typography.labelSmall,
color = Color(0xFF56306F),
fontWeight = FontWeight.Bold
)
}
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(
text = "Private answer saved",
style = MaterialTheme.typography.titleSmall,
color = Color(0xFF261D2E),
fontWeight = FontWeight.SemiBold
)
Text(
text = answerSummary(question, state),
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF5A5060),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
Text(
text = answerSummary(question, state),
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF4E4642)
)
}
}
}

View File

@ -23,8 +23,8 @@ fun QuestionThreadScreen(
LocalQuestionContent(
state = state,
title = "Answer with care",
subtitle = "Take a moment for your own answer. When you are ready, move to the next prompt or revisit what you have saved.",
primaryRouteLabel = nextQuestionId?.let { "Next" } ?: "History",
subtitle = "Save your answer privately first. From there, choose whether to keep moving or revisit what you have opened.",
primaryRouteLabel = nextQuestionId?.let { "Next prompt" } ?: "Saved answers",
onPrimaryRoute = {
if (nextQuestionId != null) {
onNavigate(
@ -73,8 +73,8 @@ fun QuestionThreadScreenPreview() {
)
),
title = "Answer with care",
subtitle = "Take a moment for your own answer.",
primaryRouteLabel = "History",
subtitle = "Save your answer privately first.",
primaryRouteLabel = "Saved answers",
onPrimaryRoute = {},
onSecondaryRoute = {},
secondaryRouteLabel = "Back",

View File

@ -92,7 +92,7 @@ fun QuestionAnswerInput(
)
) {
Text(
text = if (isSubmitting) "Saving…" else "Submit answer",
text = if (isSubmitting) "Saving…" else "Save privately",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold
)