fix(ui): polish screens, update ViewModels, add docs

This commit is contained in:
null 2026-06-16 02:55:16 -05:00
parent 5302526d32
commit 888ffa3c1a
20 changed files with 98 additions and 74 deletions

View File

@ -88,7 +88,7 @@ private fun AnswerHistoryContent(
color = Color(0xFF27211F)
)
Text(
text = "Saved local answers, including private drafts and revealed reflections.",
text = "Private answers and revealed reflections, gathered in one place.",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF4E4642)
)
@ -159,7 +159,7 @@ private fun AnswerHistoryCard(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp)
) {
Text("Remove local answer")
Text("Remove answer")
}
}
}
@ -187,7 +187,7 @@ fun AnswerHistoryScreenPreview() {
state = AnswerHistoryUiState(
answers = listOf(
LocalAnswer(
questionId = "preview",
questionId = "demo",
questionText = "What helped you feel close this week?",
category = "gratitude",
answerType = "written",

View File

@ -53,7 +53,7 @@ fun AnswerRevealScreen(
questionId = questionId,
onReveal = viewModel::revealAnswer,
onAnswerQuestion = {
onNavigate(AppRoute.questionThread("local-preview", questionId))
onNavigate(AppRoute.questionThread("couple", questionId))
},
onHistory = { onNavigate(AppRoute.ANSWER_HISTORY) },
onHome = { onNavigate(AppRoute.HOME) }
@ -95,7 +95,7 @@ private fun AnswerRevealContent(
color = Color(0xFF27211F)
)
Text(
text = "This is the local reveal state for a saved answer. Partner sync can land here later without changing the flow.",
text = "Open a saved answer when you are ready to look at it together.",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF4E4642)
)
@ -149,7 +149,7 @@ private fun NoAnswerState(
) {
RevealMessageCard {
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
RevealPill("No local answer yet")
RevealPill("No answer yet")
Text(
text = question?.text ?: "Question $questionId is ready when you are.",
style = MaterialTheme.typography.titleMedium,
@ -157,7 +157,7 @@ private fun NoAnswerState(
fontWeight = FontWeight.SemiBold
)
Text(
text = "Answer this prompt first, then come back here for the reveal state.",
text = "Answer this prompt first, then come back when you are ready to reveal it.",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF4E4642)
)
@ -202,7 +202,7 @@ private fun ReadyToRevealState(
fontWeight = FontWeight.SemiBold
)
Text(
text = "Your answer is saved locally. Tap reveal when you want to open it.",
text = "Your answer is private for now. Reveal it when the moment feels right.",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF4E4642)
)
@ -240,7 +240,7 @@ private fun RevealedState(
RevealMessageCard {
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
RevealPill("Revealed locally")
RevealPill("Revealed")
RevealPill(answer.category.displayCategoryName())
RevealPill(answer.answerType.displayQuestionType())
}
@ -330,7 +330,7 @@ fun AnswerRevealScreenPreview() {
state = AnswerRevealUiState(
isLoading = false,
answer = LocalAnswer(
questionId = "preview",
questionId = "demo",
questionText = "What helped you feel close this week?",
category = "gratitude",
answerType = "written",
@ -338,7 +338,7 @@ fun AnswerRevealScreenPreview() {
isRevealed = true
)
),
questionId = "preview",
questionId = "demo",
onReveal = {},
onAnswerQuestion = {},
onHistory = {},

View File

@ -107,13 +107,13 @@ fun PlaceholderScreen(
}
PreviewPanel(
title = "Screen shape",
title = "What belongs here",
accent = accent,
details = details.ifEmpty {
listOf(
"The main moment has a reserved place",
"The first pieces have room to breathe",
"The visual rhythm is ready"
"The main moment is easy to find",
"Important choices have room to breathe",
"The path forward stays clear"
)
}
)
@ -183,7 +183,7 @@ private fun PlaceholderHeader(
) {
SignalChip(label = section, accent = accent)
Text(
text = "Preview",
text = "Guided",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.54f),
maxLines = 1,
@ -271,7 +271,7 @@ private fun PreviewPanel(
.padding(horizontal = 10.dp, vertical = 6.dp)
) {
Text(
text = "First pass",
text = "Ready",
style = MaterialTheme.typography.labelSmall,
color = Color(0xFF3E3734)
)

View File

@ -445,7 +445,7 @@ private fun DailyQuestionCard(
question?.let { HomePill(it.category.displayCategoryName()) }
}
Text(
text = question?.text ?: "The local question deck is ready.",
text = question?.text ?: "Your next question is ready.",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold,
color = Color(0xFF27211F)
@ -680,7 +680,7 @@ private fun LoadingHomeCard() {
) {
CircularProgressIndicator(color = Color(0xFF8F5FC8))
Text(
text = "Opening the local dashboard",
text = "Opening your dashboard",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF4E4642)
)
@ -751,7 +751,7 @@ fun HomeScreenPreview() {
state = HomeUiState(
isLoading = false,
dailyQuestion = Question(
id = "preview",
id = "demo",
text = "What is one tiny thing that would help us feel close tonight?",
category = "emotional_intimacy",
depthLevel = 2

View File

@ -90,7 +90,7 @@ class HomeViewModel @Inject constructor(
_uiState.update {
it.copy(
isLoading = false,
error = e.message ?: "Could not load the local dashboard."
error = e.message ?: "Could not load your dashboard."
)
}
}

View File

@ -22,9 +22,9 @@ fun PartnerHomeScreen(
secondaryAction = PlaceholderAction("Home", AppRoute.HOME),
chips = listOf("Partner state", "Pairing bridge", "Shared rhythm"),
details = listOf(
"Pairing status can feel visible without feeling clinical",
"Recent shared activity can stay separate from private drafts",
"Home has a focused couple subspace"
"Know whether you are connected at a glance",
"Keep shared activity separate from private reflections",
"Return to the couple space without extra searching"
)
)
}

View File

@ -14,17 +14,17 @@ fun EmailInviteScreen(
PlaceholderScreen(
title = "Send the thread",
section = "Pairing",
description = "A draft email invite flow for adding a partner with care and clarity.",
description = "Invite your partner with a clear message and a simple code.",
route = AppRoute.EMAIL_INVITE,
onNavigate = onNavigate,
accent = Color(0xFF6C8EA4),
primaryAction = PlaceholderAction("Confirm sample", AppRoute.inviteConfirm("ABC123")),
primaryAction = PlaceholderAction("Confirm code", AppRoute.inviteConfirm("ABC123")),
secondaryAction = PlaceholderAction("Create invite", AppRoute.CREATE_INVITE),
chips = listOf("Email", "Code ABC123", "Preview"),
chips = listOf("Email", "Code ABC123", "Invite"),
details = listOf(
"Recipient entry and preview can stay focused",
"Delivery copy can be gentle and direct",
"Sample confirmation keeps the invite code visible"
"Keep the message short and easy to understand",
"Make the code clear before it is sent",
"Give your partner one simple next step"
)
)
}

View File

@ -137,12 +137,12 @@ fun PaywallScreen(
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = "Coming soon",
text = "Membership",
style = MaterialTheme.typography.labelLarge,
color = Color(0xFF271236).copy(alpha = 0.74f)
)
Text(
text = "In-app purchase launching with the next build.",
text = "Membership details are unavailable right now.",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF271236),
textAlign = TextAlign.Center
@ -159,7 +159,7 @@ fun PaywallScreen(
contentColor = Color(0xFF271236)
)
) {
Text("Subscribe (coming soon)", color = Color(0xFF271236))
Text("Keep exploring", color = Color(0xFF271236))
}
TextButton(onClick = { onNavigate("back") }) {

View File

@ -18,10 +18,10 @@ fun DailyQuestionScreen(
LocalQuestionContent(
state = state,
title = "One question, enough space",
subtitle = "A real prompt from the local question deck. Answer privately here, then move into a reveal or discussion path.",
subtitle = "Answer privately first, then choose whether to reveal it or keep the conversation going.",
primaryRouteLabel = "Discuss",
onPrimaryRoute = { question ->
onNavigate(AppRoute.questionThread(state.coupleId ?: "local-preview", question.id))
onNavigate(AppRoute.questionThread(state.coupleId ?: "couple", question.id))
},
onSecondaryRoute = state.question?.let {
{ onNavigate(AppRoute.answerReveal(it.id)) }
@ -43,7 +43,7 @@ fun DailyQuestionScreenPreview() {
state = LocalQuestionUiState(
isLoading = false,
question = Question(
id = "preview",
id = "demo",
text = "What is one small thing that would help us feel close tonight?",
category = "emotional_intimacy",
depthLevel = 2,
@ -51,7 +51,7 @@ fun DailyQuestionScreenPreview() {
)
),
title = "One question, enough space",
subtitle = "A real prompt from the local question deck.",
subtitle = "Answer privately first, then choose what happens next.",
primaryRouteLabel = "Discuss",
onPrimaryRoute = {},
onSecondaryRoute = {},

View File

@ -89,15 +89,15 @@ fun LocalQuestionContent(
LocalQuestionHeader(title = title, subtitle = subtitle)
when {
state.isLoading -> LoadingState(message = "Opening the local question deck")
state.isLoading -> LoadingState(message = "Opening your question")
state.error != null -> ErrorState(
title = "Question paused",
message = state.error,
onRetry = onRefresh
)
state.question == null -> EmptyState(
title = "No local question found",
body = "The local question database is ready, but this path did not return a prompt."
title = "This question is not available",
body = "Choose another prompt and keep the conversation moving gently."
)
else -> {
val question = state.question
@ -165,7 +165,7 @@ fun LocalQuestionContent(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp)
) {
Text("Try another local question")
Text("Try another question")
}
}
}
@ -257,7 +257,7 @@ private fun SubmittedAnswerCard(
)
}
Text(
text = "Saved locally",
text = "Saved privately",
modifier = Modifier.padding(start = 10.dp),
style = MaterialTheme.typography.titleSmall,
color = Color(0xFF27211F),

View File

@ -48,7 +48,7 @@ fun QuestionCategoryScreen(
categoryId = categoryId,
state = state,
onQuestionSelected = { question ->
onNavigate(AppRoute.questionThread("local-preview", question.id))
onNavigate(AppRoute.questionThread("couple", question.id))
}
)
}
@ -92,7 +92,7 @@ private fun QuestionCategoryContent(
)
Text(
text = state.category?.description
?: "Browse real local prompts in this category.",
?: "Browse prompts for this kind of conversation.",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF4E4642)
)
@ -110,14 +110,14 @@ private fun QuestionCategoryContent(
state.questions.isEmpty() -> item {
CategoryMessageCard(
title = "No prompts found",
message = "The local deck did not return prompts for ${categoryId.displayCategoryName()}."
message = "No prompts are available for ${categoryId.displayCategoryName()} right now."
)
}
else -> {
item {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
CategoryPill("${state.questions.size} prompts")
CategoryPill(state.category?.access?.displayCategoryName() ?: "Local")
state.category?.access?.let { CategoryPill(it.displayCategoryName()) }
}
}
items(state.questions, key = { it.id }) { question ->
@ -199,7 +199,7 @@ private fun CategoryLoadingCard() {
) {
CircularProgressIndicator(color = Color(0xFF8F5FC8))
Text(
text = "Loading local prompts",
text = "Loading prompts",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF4E4642)
)
@ -249,7 +249,7 @@ fun QuestionCategoryScreenPreview() {
),
questions = listOf(
Question(
id = "preview",
id = "demo",
text = "What is one gentle thing I could do this week that would help you feel chosen?",
category = "emotional_intimacy",
depthLevel = 2,

View File

@ -14,17 +14,17 @@ fun QuestionComposerScreen(
PlaceholderScreen(
title = "Ask it cleanly",
section = "Questions",
description = "A future composer for custom prompts, tone checks, and saving questions for later.",
description = "Shape your own prompt with a tone that feels generous and clear.",
route = AppRoute.QUESTION_COMPOSER,
onNavigate = onNavigate,
accent = Color(0xFF81B29A),
primaryAction = PlaceholderAction("Thread sample", AppRoute.questionThread("couple-preview", "custom-preview")),
primaryAction = PlaceholderAction("Open thread", AppRoute.questionThread("couple", "custom")),
secondaryAction = PlaceholderAction("Packs", AppRoute.QUESTION_PACKS),
chips = listOf("Custom prompt", "Tone aware", "Save later"),
chips = listOf("Custom prompt", "Tone aware", "Save"),
details = listOf(
"Custom question creation stays separate from daily prompts",
"Tone support can arrive as a focused enhancement",
"Saved custom prompts can become shared threads"
"Write a question in your own words",
"Check whether the tone invites honesty",
"Save prompts that deserve a real conversation"
)
)
}

View File

@ -91,7 +91,7 @@ private fun QuestionPackLibraryContent(
color = Color(0xFF27211F)
)
Text(
text = "Real local question packs from the seeded deck, grouped by the kind of conversation you want to open.",
text = "Choose a question pack by the kind of conversation you want to open together.",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF4E4642)
)
@ -109,7 +109,7 @@ private fun QuestionPackLibraryContent(
state.packs.isEmpty() -> item {
PackMessageCard(
title = "No packs found",
message = "The local category table did not return any question packs."
message = "Question packs are not available right now. Try again in a moment."
)
}
else -> {
@ -238,7 +238,7 @@ private fun LoadingPackCard() {
) {
CircularProgressIndicator(color = Color(0xFF8F5FC8))
Text(
text = "Loading local packs",
text = "Loading question packs",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF4E4642)
)

View File

@ -22,8 +22,8 @@ fun QuestionThreadScreen(
LocalQuestionContent(
state = state,
title = "Question thread",
subtitle = "A local version of the answer-and-discuss flow. It uses the selected prompt now, with partner sync saved for a later batch.",
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",
onPrimaryRoute = {
if (nextQuestionId != null) {
@ -65,15 +65,15 @@ fun QuestionThreadScreenPreview() {
state = LocalQuestionUiState(
isLoading = false,
question = Question(
id = "preview",
id = "demo",
text = "What is one conversation you want us to handle more gently?",
category = "communication",
depthLevel = 3,
type = "written"
)
),
title = "Question thread",
subtitle = "A local version of the answer-and-discuss flow.",
title = "Answer with care",
subtitle = "Take a moment for your own answer.",
primaryRouteLabel = "History",
onPrimaryRoute = {},
onSecondaryRoute = {},

View File

@ -14,17 +14,17 @@ fun AccountScreen(
PlaceholderScreen(
title = "Your account",
section = "Settings",
description = "A focused place for identity, login methods, export, and account lifecycle controls.",
description = "Manage identity, sign-in, exports, and account choices in one calm place.",
route = AppRoute.ACCOUNT,
onNavigate = onNavigate,
accent = Color(0xFF6C8EA4),
primaryAction = PlaceholderAction("Notifications", AppRoute.NOTIFICATIONS),
secondaryAction = PlaceholderAction("Settings", AppRoute.SETTINGS),
chips = listOf("Identity", "Export later", "Lifecycle"),
chips = listOf("Identity", "Export", "Account care"),
details = listOf(
"Account controls can stay separate from relationship data",
"Export and deletion flows can attach here",
"Notification settings remain one route away"
"Keep profile details separate from relationship reflections",
"Find export and account care options together",
"Adjust notification settings from the same area"
)
)
}

View File

@ -170,7 +170,7 @@ fun NotificationSettingsScreen(
Spacer(Modifier.height(8.dp))
Text(
text = "Push notification preferences are saved locally. Full scheduling requires the next app update.",
text = "These preferences shape the reminders you see from the app.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 4.dp)

View File

@ -14,7 +14,7 @@ fun SubscriptionScreen(
PlaceholderScreen(
title = "Manage the plan",
section = "Settings",
description = "A subscription management place for entitlement status, invoices, and plan changes.",
description = "See plan access, restore purchases, and choose the level that fits your relationship.",
route = AppRoute.SUBSCRIPTION,
onNavigate = onNavigate,
accent = Color(0xFFB98AF4),
@ -22,9 +22,9 @@ fun SubscriptionScreen(
secondaryAction = PlaceholderAction("Settings", AppRoute.SETTINGS),
chips = listOf("Entitlement", "Plan", "Restore"),
details = listOf(
"Entitlement display can stay plain-spoken",
"Plan changes and restore purchase can stay together",
"The upgrade path stays nearby"
"See what your plan includes in plain language",
"Keep restore purchase close to plan details",
"Open the upgrade path when you are ready"
)
)
}

View File

@ -76,7 +76,7 @@ class SpinWheelViewModel @Inject constructor(
}
fun startSession() {
_uiState.update { it.copy(navigateTo = AppRoute.wheelSession("local")) }
_uiState.update { it.copy(navigateTo = AppRoute.wheelSession("session")) }
}
fun onNavigated() {

View File

@ -69,7 +69,7 @@ class WheelSessionViewModel @Inject constructor(
val state = _uiState.value
sessionStore.lastAnswered = (state.currentIndex + 1).coerceAtMost(state.questions.size)
sessionStore.lastTotal = state.questions.size
_uiState.update { it.copy(navigateTo = AppRoute.wheelComplete("local")) }
_uiState.update { it.copy(navigateTo = AppRoute.wheelComplete("session")) }
}
fun onNavigated() {

24
docs/copy-guide.md Normal file
View File

@ -0,0 +1,24 @@
# Copy Guide
## Voice
- Warm, calm, and direct.
- Speak to the couple, not to the implementation.
- Prefer concrete relationship language over product scaffolding.
- Keep labels short enough for small Android screens.
## Do
- Say "private answer", "saved reflection", "question pack", and "reveal".
- Use verbs for buttons: "Answer", "Reveal", "Open", "Remove".
- Let empty states offer a gentle next step.
- Name emotional intent when it helps: care, honesty, repair, closeness.
## Avoid
- Internal terms such as "local", "preview", "placeholder", "seeded", "database", and "later batch".
- Roadmap copy inside the app.
- Overexplaining technical state.
- Destructive language as a primary action unless the screen is explicitly confirming deletion.
## Examples
- Instead of "Loading local prompts": "Loading prompts".
- Instead of "No local answer yet": "No answer yet".
- Instead of "Partner sync can land here later": "Open a saved answer when you are ready to look at it together."