From 94360a5438cb9958cd9a872477db7cdc7035d316 Mon Sep 17 00:00:00 2001 From: null Date: Sat, 11 Jul 2026 19:43:22 -0500 Subject: [PATCH] refactor(home): extract header + partner sheet -> components/HomeHeaderSection.kt Step 8/9. Verbatim move (HomeHeader + PartnerQuickActionsSheet public, rest private; @OptIn(ExperimentalMaterial3Api) + .semantics a11y + full state param preserved). Co-Authored-By: Claude Fable 5 --- .../java/app/closer/ui/home/HomeScreen.kt | 308 +-------------- .../ui/home/components/HomeHeaderSection.kt | 354 ++++++++++++++++++ 2 files changed, 356 insertions(+), 306 deletions(-) create mode 100644 app/src/main/java/app/closer/ui/home/components/HomeHeaderSection.kt diff --git a/app/src/main/java/app/closer/ui/home/HomeScreen.kt b/app/src/main/java/app/closer/ui/home/HomeScreen.kt index 3290cd47..99e2de18 100644 --- a/app/src/main/java/app/closer/ui/home/HomeScreen.kt +++ b/app/src/main/java/app/closer/ui/home/HomeScreen.kt @@ -92,10 +92,12 @@ import app.closer.ui.home.components.CategoryPreviewGrid import app.closer.ui.home.components.EmptyHomeContent import app.closer.ui.home.components.ErrorHomeCard import app.closer.ui.home.components.HomeActionColors +import app.closer.ui.home.components.HomeHeader 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.PartnerQuickActionsSheet import app.closer.ui.home.components.PartnerActivationCard import app.closer.ui.home.components.PrimaryHomeActionCard import app.closer.ui.home.components.WaitingForYouSection @@ -426,312 +428,6 @@ private fun HomeContent( } } -@Composable -private fun HomeHeader( - partnerName: String?, - partnerPhotoUrl: String?, - streakCount: Int, - isPaired: Boolean, - unreadActivityCount: Int = 0, - onTogether: () -> Unit = {} -) { - Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "For tonight", - style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold), - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f) - ) - if (streakCount > 0) { - HomePill("$streakCount nights") - } - // Partner/together entry point with an unread badge. - Box { - PartnerHeaderBubble( - partnerName = partnerName, - partnerPhotoUrl = partnerPhotoUrl, - isPaired = isPaired, - onClick = onTogether - ) - if (unreadActivityCount > 0) { - // Surface-ringed dot so it stays legible over the avatar photo or the gradient ring. - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .size(14.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surface), - contentAlignment = Alignment.Center - ) { - Box( - modifier = Modifier - .size(10.dp) - .clip(CircleShape) - .background(CloserPalette.PinkBright) - ) - } - } - } - } - Text( - text = if (!isPaired) - "Set up your shared space, then keep exploring at your own pace." - else if (partnerName != null) - "Connected with $partnerName. Here's what matters tonight." - else - "Open the app, see what matters, and take one small step toward closeness.", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - } -} - -@Composable -private fun PartnerHeaderBubble( - partnerName: String?, - partnerPhotoUrl: String?, - isPaired: Boolean, - onClick: () -> Unit -) { - // Brand gradient ring around the avatar โ€” signals "your partner" + that it's tappable. - val ringBrush = Brush.linearGradient( - listOf(CloserPalette.PurpleRich, CloserPalette.PinkBright) - ) - val label = when { - !isPaired -> "Invite your partner" - !partnerName.isNullOrBlank() -> "Open $partnerName's space" - else -> "Open your shared space" - } - - Box( - modifier = Modifier - .size(52.dp) - .clip(CircleShape) - .background(ringBrush) - .clickable(onClick = onClick) - .semantics { contentDescription = label }, - contentAlignment = Alignment.Center - ) { - Box( - modifier = Modifier - .size(46.dp) - .clip(CircleShape) - .background(CloserPalette.PurpleSoft), - contentAlignment = Alignment.Center - ) { - when { - !isPaired -> HomeGlyphIcon( - resId = R.drawable.glyph_couple, - contentDescription = null, - tint = CloserPalette.PurpleRich, - modifier = Modifier.size(24.dp) - ) - !partnerPhotoUrl.isNullOrBlank() -> SubcomposeAsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(partnerPhotoUrl) - .crossfade(true) - .build(), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize().clip(CircleShape), - // While loading or on failure, keep the partner's initials centered in view - // (never a blank/grey circle). - loading = { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - PartnerInitials(partnerName = partnerName) - } - }, - error = { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - PartnerInitials(partnerName = partnerName) - } - } - ) - else -> PartnerInitials(partnerName = partnerName) - } - } - } -} - -@Composable -private fun PartnerInitials(partnerName: String?) { - Text( - text = partnerInitials(partnerName), - style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), - color = CloserPalette.PurpleRich, - maxLines = 1 - ) -} - -private fun partnerInitials(name: String?): String { - val clean = name?.trim().orEmpty() - if (clean.isBlank()) return "2" - val words = clean.split(Regex("\\s+")).filter { it.isNotBlank() } - val initials = if (words.size >= 2) { - "${words[0].first()}${words[1].first()}" - } else { - clean.take(2) - } - return initials.uppercase() -} - -/** - * Warm quick-actions sheet shown when the partner avatar is tapped: a relationship glance + one-tap - * actions. Replaces the old dead-end into the read-only "Together" feed. Only shown when paired. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun PartnerQuickActionsSheet( - state: HomeUiState, - onDismiss: () -> Unit, - onNavigate: (String) -> Unit, - onThinkingOfYou: () -> Unit -) { - val sheetState = rememberModalBottomSheetState() - // A locked/blank name (E2EE key missing on this device) must never surface ciphertext/placeholder. - val name = state.partnerName - ?.takeIf { it.isNotBlank() && it != FieldEncryptor.LOCKED_PLACEHOLDER } - ?: "Your partner" - val glance = remember(state.streakCount, state.togetherSince) { - buildList { - if (state.streakCount > 0) { - add("๐Ÿ’œ ${state.streakCount} ${if (state.streakCount == 1) "night" else "nights"}") - } - if (state.togetherSince > 0L) add("together since ${formatMonthYear(state.togetherSince)}") - }.joinToString(" ยท ") - } - // Living "today" status โ€” reflects where the couple is in the daily ritual. Hidden when there's no - // question assigned (no misleading "still open"). - val todayStatus = state.dailyQuestion?.let { - when (state.dailyQuestionState) { - DailyQuestionState.BOTH_ANSWERED, DailyQuestionState.REVEALED -> "You've both answered today ๐Ÿ’œ" - DailyQuestionState.PARTNER_ANSWERED_USER_PENDING -> "They answered โ€” your turn" - DailyQuestionState.USER_ANSWERED_PARTNER_PENDING -> "Waiting on their answer" - DailyQuestionState.UNANSWERED -> "Tonight's question is still open" - } - } - val openPartnerPage = { onNavigate(AppRoute.PARTNER_HOME); onDismiss() } - - ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(bottom = 28.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .clickable(onClick = openPartnerPage) - .padding(vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(14.dp), - verticalAlignment = Alignment.CenterVertically - ) { - PartnerHeaderBubble( - partnerName = name, - partnerPhotoUrl = state.partnerPhotoUrl, - isPaired = true, - onClick = openPartnerPage - ) - Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text( - text = name, - style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold), - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - if (glance.isNotBlank()) { - Text( - text = glance, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - todayStatus?.let { - Text( - text = it, - style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium), - color = MaterialTheme.colorScheme.primary, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - } - - Divider(modifier = Modifier.padding(vertical = 4.dp), thickness = 0.5.dp) - - PartnerSheetAction(R.drawable.glyph_heart, "Thinking of you", enabled = !state.isSendingNudge, onClick = onThinkingOfYou) - PartnerSheetAction(R.drawable.glyph_chat, "Message") { onNavigate(AppRoute.MESSAGES); onDismiss() } - PartnerSheetAction( - R.drawable.glyph_paired_cards, - "Together", - trailing = state.unreadActivityCount.takeIf { it > 0 }?.let { if (it > 9) "9+" else "$it" } - ) { onNavigate(AppRoute.ACTIVITY); onDismiss() } - PartnerSheetAction(R.drawable.glyph_memory_capsule, "Our memories") { onNavigate(AppRoute.MEMORY_LANE); onDismiss() } - PartnerSheetAction(R.drawable.glyph_settings, "Your relationship") { onNavigate(AppRoute.RELATIONSHIP_SETTINGS); onDismiss() } - } - } -} - -@Composable -private fun PartnerSheetAction( - @DrawableRes iconRes: Int, - label: String, - enabled: Boolean = true, - trailing: String? = null, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(14.dp)) - .clickable(enabled = enabled, onClick = onClick) - .padding(horizontal = 8.dp, vertical = 16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - HomeGlyphIcon( - resId = iconRes, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(22.dp) - ) - Text( - text = label, - modifier = Modifier.weight(1f), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (enabled) 1f else 0.5f) - ) - if (trailing != null) { - Surface(shape = CircleShape, color = CloserPalette.PinkBright) { - Text( - text = trailing, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp), - style = MaterialTheme.typography.labelSmall, - color = Color.White - ) - } - } - } -} - -private fun formatMonthYear(millis: Long): String = - java.text.SimpleDateFormat("MMM yyyy", java.util.Locale.getDefault()).format(java.util.Date(millis)) diff --git a/app/src/main/java/app/closer/ui/home/components/HomeHeaderSection.kt b/app/src/main/java/app/closer/ui/home/components/HomeHeaderSection.kt new file mode 100644 index 00000000..19e61401 --- /dev/null +++ b/app/src/main/java/app/closer/ui/home/components/HomeHeaderSection.kt @@ -0,0 +1,354 @@ +package app.closer.ui.home.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import app.closer.R +import app.closer.core.navigation.AppRoute +import app.closer.crypto.FieldEncryptor +import app.closer.ui.home.DailyQuestionState +import app.closer.ui.home.HomeUiState +import app.closer.ui.theme.CloserPalette +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest + +/** Home's header row (title + streak + partner presence bubble) and the partner quick-actions bottom + * sheet. Extracted from HomeScreen.kt. */ + +@Composable +fun HomeHeader( + partnerName: String?, + partnerPhotoUrl: String?, + streakCount: Int, + isPaired: Boolean, + unreadActivityCount: Int = 0, + onTogether: () -> Unit = {} +) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "For tonight", + style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + if (streakCount > 0) { + HomePill("$streakCount nights") + } + // Partner/together entry point with an unread badge. + Box { + PartnerHeaderBubble( + partnerName = partnerName, + partnerPhotoUrl = partnerPhotoUrl, + isPaired = isPaired, + onClick = onTogether + ) + if (unreadActivityCount > 0) { + // Surface-ringed dot so it stays legible over the avatar photo or the gradient ring. + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(14.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surface), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(10.dp) + .clip(CircleShape) + .background(CloserPalette.PinkBright) + ) + } + } + } + } + Text( + text = if (!isPaired) + "Set up your shared space, then keep exploring at your own pace." + else if (partnerName != null) + "Connected with $partnerName. Here's what matters tonight." + else + "Open the app, see what matters, and take one small step toward closeness.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Composable +private fun PartnerHeaderBubble( + partnerName: String?, + partnerPhotoUrl: String?, + isPaired: Boolean, + onClick: () -> Unit +) { + // Brand gradient ring around the avatar โ€” signals "your partner" + that it's tappable. + val ringBrush = Brush.linearGradient( + listOf(CloserPalette.PurpleRich, CloserPalette.PinkBright) + ) + val label = when { + !isPaired -> "Invite your partner" + !partnerName.isNullOrBlank() -> "Open $partnerName's space" + else -> "Open your shared space" + } + + Box( + modifier = Modifier + .size(52.dp) + .clip(CircleShape) + .background(ringBrush) + .clickable(onClick = onClick) + .semantics { contentDescription = label }, + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(46.dp) + .clip(CircleShape) + .background(CloserPalette.PurpleSoft), + contentAlignment = Alignment.Center + ) { + when { + !isPaired -> HomeGlyphIcon( + resId = R.drawable.glyph_couple, + contentDescription = null, + tint = CloserPalette.PurpleRich, + modifier = Modifier.size(24.dp) + ) + !partnerPhotoUrl.isNullOrBlank() -> SubcomposeAsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(partnerPhotoUrl) + .crossfade(true) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize().clip(CircleShape), + // While loading or on failure, keep the partner's initials centered in view + // (never a blank/grey circle). + loading = { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + PartnerInitials(partnerName = partnerName) + } + }, + error = { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + PartnerInitials(partnerName = partnerName) + } + } + ) + else -> PartnerInitials(partnerName = partnerName) + } + } + } +} + +@Composable +private fun PartnerInitials(partnerName: String?) { + Text( + text = partnerInitials(partnerName), + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), + color = CloserPalette.PurpleRich, + maxLines = 1 + ) +} + +private fun partnerInitials(name: String?): String { + val clean = name?.trim().orEmpty() + if (clean.isBlank()) return "2" + val words = clean.split(Regex("\\s+")).filter { it.isNotBlank() } + val initials = if (words.size >= 2) { + "${words[0].first()}${words[1].first()}" + } else { + clean.take(2) + } + return initials.uppercase() +} + +/** + * Warm quick-actions sheet shown when the partner avatar is tapped: a relationship glance + one-tap + * actions. Replaces the old dead-end into the read-only "Together" feed. Only shown when paired. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PartnerQuickActionsSheet( + state: HomeUiState, + onDismiss: () -> Unit, + onNavigate: (String) -> Unit, + onThinkingOfYou: () -> Unit +) { + val sheetState = rememberModalBottomSheetState() + // A locked/blank name (E2EE key missing on this device) must never surface ciphertext/placeholder. + val name = state.partnerName + ?.takeIf { it.isNotBlank() && it != FieldEncryptor.LOCKED_PLACEHOLDER } + ?: "Your partner" + val glance = remember(state.streakCount, state.togetherSince) { + buildList { + if (state.streakCount > 0) { + add("๐Ÿ’œ ${state.streakCount} ${if (state.streakCount == 1) "night" else "nights"}") + } + if (state.togetherSince > 0L) add("together since ${formatMonthYear(state.togetherSince)}") + }.joinToString(" ยท ") + } + // Living "today" status โ€” reflects where the couple is in the daily ritual. Hidden when there's no + // question assigned (no misleading "still open"). + val todayStatus = state.dailyQuestion?.let { + when (state.dailyQuestionState) { + DailyQuestionState.BOTH_ANSWERED, DailyQuestionState.REVEALED -> "You've both answered today ๐Ÿ’œ" + DailyQuestionState.PARTNER_ANSWERED_USER_PENDING -> "They answered โ€” your turn" + DailyQuestionState.USER_ANSWERED_PARTNER_PENDING -> "Waiting on their answer" + DailyQuestionState.UNANSWERED -> "Tonight's question is still open" + } + } + val openPartnerPage = { onNavigate(AppRoute.PARTNER_HOME); onDismiss() } + + ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 28.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .clickable(onClick = openPartnerPage) + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + PartnerHeaderBubble( + partnerName = name, + partnerPhotoUrl = state.partnerPhotoUrl, + isPaired = true, + onClick = openPartnerPage + ) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = name, + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (glance.isNotBlank()) { + Text( + text = glance, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + todayStatus?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium), + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + + Divider(modifier = Modifier.padding(vertical = 4.dp), thickness = 0.5.dp) + + PartnerSheetAction(R.drawable.glyph_heart, "Thinking of you", enabled = !state.isSendingNudge, onClick = onThinkingOfYou) + PartnerSheetAction(R.drawable.glyph_chat, "Message") { onNavigate(AppRoute.MESSAGES); onDismiss() } + PartnerSheetAction( + R.drawable.glyph_paired_cards, + "Together", + trailing = state.unreadActivityCount.takeIf { it > 0 }?.let { if (it > 9) "9+" else "$it" } + ) { onNavigate(AppRoute.ACTIVITY); onDismiss() } + PartnerSheetAction(R.drawable.glyph_memory_capsule, "Our memories") { onNavigate(AppRoute.MEMORY_LANE); onDismiss() } + PartnerSheetAction(R.drawable.glyph_settings, "Your relationship") { onNavigate(AppRoute.RELATIONSHIP_SETTINGS); onDismiss() } + } + } +} + +@Composable +private fun PartnerSheetAction( + @DrawableRes iconRes: Int, + label: String, + enabled: Boolean = true, + trailing: String? = null, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .clickable(enabled = enabled, onClick = onClick) + .padding(horizontal = 8.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + HomeGlyphIcon( + resId = iconRes, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(22.dp) + ) + Text( + text = label, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (enabled) 1f else 0.5f) + ) + if (trailing != null) { + Surface(shape = CircleShape, color = CloserPalette.PinkBright) { + Text( + text = trailing, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp), + style = MaterialTheme.typography.labelSmall, + color = Color.White + ) + } + } + } +} + +private fun formatMonthYear(millis: Long): String = + java.text.SimpleDateFormat("MMM yyyy", java.util.Locale.getDefault()).format(java.util.Date(millis))