From 2b02a1341feeab9dd50ec04836406adca422faab Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 22:52:35 +0000 Subject: [PATCH] feat: modernize road event cards with a map-hero + floating pill Redesign the kind 1315/1316 cards from a text-title-over-small-map layout into a map-hero card: the OSM preview becomes a full-bleed hero with a floating, color-coded category pill overlaid on it (auto-contrast text), and the comment/status sits in a clean padded block below. Shared RoadEventCard + CategoryPill chrome backs both the report and the confirmation, with a graceful no-location fallback. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_017tYbcy4UGWxqQbcycyL7Yd --- .../amethyst/ui/note/types/RoadEvent.kt | 181 +++++++++++------- 1 file changed, 111 insertions(+), 70 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RoadEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RoadEvent.kt index 1be9c18fd0..b9f111aaff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RoadEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RoadEvent.kt @@ -20,19 +20,24 @@ */ package com.vitorpamplona.amethyst.ui.note.types +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R @@ -111,10 +116,10 @@ private fun RoadEventType.labelRes(): Int = /** * Self-contained card for a Roadstr road event report (kind 1315). * - * Shows the category (emoji + localized label), the optional free-text comment, - * and a map pinned at the event's coordinates. Identical in the feed and the - * opened thread view, so it takes no `makeItShort` flag — mirroring - * [RenderBirdex]. + * A map hero pinned at the event's coordinates, with a floating category pill + * (colored per [RoadEventType.color]) and the optional free-text comment below. + * Identical in the feed and the opened thread view, so it takes no `makeItShort` + * flag — mirroring [RenderBirdex]. */ @Composable fun RenderRoadEventReport(baseNote: Note) { @@ -125,43 +130,25 @@ fun RenderRoadEventReport(baseNote: Note) { val point = remember(noteEvent) { noteEvent.roadEventPoint() } val freshness = remember(noteEvent) { noteEvent.freshnessAlpha() } - Column(MaterialTheme.colorScheme.replyModifier.padding(10.dp)) { - val title = - if (type != null) { - "${type.emoji()} ${stringResource(type.labelRes())}" - } else { - stringResource(R.string.road_event_unknown) - } + val color = type?.color() ?: MaterialTheme.colorScheme.outline + val emoji = type?.emoji() ?: "📍" + val label = if (type != null) stringResource(type.labelRes()) else stringResource(R.string.road_event_unknown) - Text(text = title, style = MaterialTheme.typography.titleMedium) - - if (comment.isNotEmpty()) { - Spacer(Modifier.height(6.dp)) - Text( - text = comment, - style = MaterialTheme.typography.bodyMedium, - maxLines = 4, - overflow = TextOverflow.Ellipsis, - ) - } - - if (point != null) { - Spacer(Modifier.height(8.dp)) - RoadEventMap( - point = point, - pinColor = type?.color(), - pinEmoji = type?.emoji(), - pinAlpha = freshness, - ) - } - } + RoadEventCard( + chipColor = color, + chipEmoji = emoji, + chipLabel = label, + point = point, + pinAlpha = freshness, + comment = comment, + ) } /** * Self-contained card for a Roadstr confirmation/denial (kind 1316). * - * Shows whether the referenced report was confirmed (still there) or denied - * (no longer there), plus a one-line description of the action. + * Same hero/pill layout as a report, with a green ✅ "Still there" or red ❌ + * "No longer there" pill and a one-line description of the action. */ @Composable fun RenderRoadEventConfirmation(baseNote: Note) { @@ -172,50 +159,104 @@ fun RenderRoadEventConfirmation(baseNote: Note) { val denied = status == RoadEventStatus.NO_LONGER_THERE val emoji = if (denied) "❌" else "✅" - val pinColor = if (denied) Color(0xFFF44336) else Color(0xFF4CAF50) + val color = if (denied) Color(0xFFF44336) else Color(0xFF4CAF50) - Column(MaterialTheme.colorScheme.replyModifier.padding(10.dp)) { - val titleRes = if (denied) R.string.road_event_denied else R.string.road_event_confirmed - - Text(text = "$emoji ${stringResource(titleRes)}", style = MaterialTheme.typography.titleMedium) - - Spacer(Modifier.height(4.dp)) - Text( - text = - stringResource( - if (denied) R.string.road_event_denies_report else R.string.road_event_confirms_report, - ), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.placeholderText, - ) + RoadEventCard( + chipColor = color, + chipEmoji = emoji, + chipLabel = stringResource(if (denied) R.string.road_event_denied else R.string.road_event_confirmed), + point = point, + subtitle = stringResource(if (denied) R.string.road_event_denies_report else R.string.road_event_confirms_report), + ) +} +/** + * Shared road event card chrome: a full-bleed [LocationPreviewMap] hero with a + * floating [CategoryPill] overlaid on its top-start, and an optional + * [subtitle] + [comment] block below. When [point] is null the pill stands on + * its own so the card still reads. + */ +@Composable +private fun RoadEventCard( + chipColor: Color, + chipEmoji: String, + chipLabel: String, + point: Pair?, + pinAlpha: Float = 1f, + comment: String = "", + subtitle: String? = null, +) { + Column(MaterialTheme.colorScheme.replyModifier) { if (point != null) { - Spacer(Modifier.height(8.dp)) - RoadEventMap( - point = point, - pinColor = pinColor, - pinEmoji = emoji, + Box(Modifier.fillMaxWidth()) { + LocationPreviewMap( + latitude = point.first, + longitude = point.second, + pinColor = chipColor, + pinEmoji = chipEmoji, + pinAlpha = pinAlpha, + ) + CategoryPill( + color = chipColor, + emoji = chipEmoji, + label = chipLabel, + modifier = Modifier.align(Alignment.TopStart).padding(10.dp), + ) + } + } else { + CategoryPill( + color = chipColor, + emoji = chipEmoji, + label = chipLabel, + modifier = Modifier.padding(12.dp), ) } + + if (subtitle != null || comment.isNotEmpty()) { + Column(Modifier.padding(horizontal = 12.dp, vertical = 10.dp)) { + if (subtitle != null) { + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.placeholderText, + ) + } + if (comment.isNotEmpty()) { + if (subtitle != null) Spacer(Modifier.height(4.dp)) + Text( + text = comment, + style = MaterialTheme.typography.bodyMedium, + maxLines = 4, + overflow = TextOverflow.Ellipsis, + ) + } + } + } } } -/** A rounded OpenStreetMap preview with a colored emoji pin at the road event's [point]. */ +/** A floating rounded pill: [emoji] + [label] on a [color] background, text auto-contrasted. */ @Composable -private fun RoadEventMap( - point: Pair, - pinColor: Color? = null, - pinEmoji: String? = null, - pinAlpha: Float = 1f, +private fun CategoryPill( + color: Color, + emoji: String, + label: String, + modifier: Modifier = Modifier, ) { - LocationPreviewMap( - latitude = point.first, - longitude = point.second, - modifier = Modifier.clip(RoundedCornerShape(8.dp)), - pinColor = pinColor, - pinEmoji = pinEmoji, - pinAlpha = pinAlpha, - ) + Surface( + modifier = modifier, + color = color, + contentColor = if (color.luminance() > 0.55f) Color.Black else Color.White, + shape = RoundedCornerShape(50), + shadowElevation = 3.dp, + ) { + Text( + text = "$emoji $label", + modifier = Modifier.padding(horizontal = 12.dp, vertical = 5.dp), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold, + ) + } } /**