diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/ColorSchemePreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/ColorSchemePreview.kt index c5e8e92e07..5d094f3682 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/ColorSchemePreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/ColorSchemePreview.kt @@ -294,3 +294,187 @@ fun ThemeStyleguideOldVsNewPreview() { } } } + +// --------------------------------------------------------------------------- +// Role-oriented view: every fg/bg pair listed by its ColorScheme role name (not +// by the component using it), rendered for the CURRENT theme in dark | light. +// Answers "what is primaryContainer right now"; the table above answers "how did +// this surface change". Both read the live scheme, so Theme.kt edits re-render. +// --------------------------------------------------------------------------- + +private class RolePair( + val name: String, + val bg: (ColorScheme) -> Color, + val fg: (ColorScheme) -> Color, + val usage: String, + val highlight: Boolean = false, +) + +private val ROLE_PAIRS = + listOf( + RolePair( + "filledAccent / onFilledAccent", + { it.filledAccent }, + { it.onFilledAccent }, + "Top-bar Save·Post·Send·Create button, standalone Save button, checked switch.", + highlight = true, + ), + RolePair( + "primary / onPrimary", + { it.primary }, + { it.onPrimary }, + "Floating action buttons, generic filled buttons, sliders, progress.", + ), + RolePair( + "primaryContainer / onPrimaryContainer", + { it.primaryContainer }, + { it.onPrimaryContainer }, + "Settings icon tiles, tonal cards, tonal chips.", + ), + RolePair( + "secondaryContainer / onSecondaryContainer", + { it.secondaryContainer }, + { it.onSecondaryContainer }, + "Selected-reaction highlight, relay-group chips, chat header, reply toggle.", + ), + RolePair( + "secondary / onSecondary", + { it.secondary }, + { it.onSecondary }, + "Teal accent (purple theme); tertiary mirrors it.", + ), + RolePair( + "tertiaryContainer / onTertiaryContainer", + { it.tertiaryContainer }, + { it.onTertiaryContainer }, + "Tertiary tonal surfaces; derived from the accent.", + ), + RolePair( + "error / onError", + { it.error }, + { it.onError }, + "Destructive dialogs and error states. Material baseline.", + ), + RolePair( + "surface / onSurface", + { it.surface }, + { it.onSurface }, + "App background and primary body text.", + ), + RolePair( + "surface / onSurfaceVariant", + { it.surface }, + { it.onSurfaceVariant }, + "Secondary text, timestamps, subtitles, icons.", + ), + RolePair( + "surfaceContainer / onSurface", + { it.surfaceContainer }, + { it.onSurface }, + "Cards, bottom sheets, elevated rows.", + ), + RolePair( + "surfaceVariant / onSurfaceVariant", + { it.surfaceVariant }, + { it.onSurfaceVariant }, + "Subtle filled areas — code blocks, inset panels.", + ), + RolePair( + "surfaceContainerHighest / outline", + { it.surfaceContainerHighest }, + { it.outline }, + "Unchecked switch (off-track + thumb), inactive controls.", + ), + ) + +private fun contrastLabel(ratio: Float): String = + when { + ratio >= 4.5f -> "AA" + ratio >= 3f -> "AA-lg" + else -> "low" + } + +@Composable +private fun RolePairRow(pair: RolePair) { + val scheme = MaterialTheme.colorScheme + val bg = pair.bg(scheme) + val fg = pair.fg(scheme) + val ratio = contrastRatio(bg, fg) + + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = + Modifier + .width(92.dp) + .height(58.dp) + .clip(RoundedCornerShape(10.dp)) + .background(bg) + .border( + if (pair.highlight) 2.dp else 1.dp, + if (pair.highlight) scheme.onBackground else scheme.outlineVariant, + RoundedCornerShape(10.dp), + ).padding(8.dp), + ) { + Text("Aa", color = fg, fontSize = 19.sp, fontWeight = FontWeight.Bold) + Text( + "%.1f:1 %s".format(ratio, contrastLabel(ratio)), + color = fg, + fontSize = 8.5.sp, + fontFamily = FontFamily.Monospace, + modifier = Modifier.align(Alignment.BottomEnd), + ) + } + + Spacer(Modifier.width(10.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + pair.name, + color = scheme.onBackground, + fontSize = 12.5.sp, + fontWeight = if (pair.highlight) FontWeight.Bold else FontWeight.Medium, + ) + Text( + "${bg.hex()} / ${fg.hex()}", + color = scheme.onSurfaceVariant, + fontSize = 10.5.sp, + fontFamily = FontFamily.Monospace, + ) + Text( + pair.usage, + color = scheme.onSurfaceVariant, + fontSize = 10.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun RolePairList() { + Column( + modifier = Modifier.padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + "fg / bg pairs by role", + color = MaterialTheme.colorScheme.onBackground, + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), + ) + ROLE_PAIRS.forEach { RolePairRow(it) } + } +} + +@Preview(name = "Theme color pairs by role · dark | light", widthDp = 820, showBackground = true) +@Composable +fun ThemeColorPairsPreview() { + ThemeComparisonRow { + RolePairList() + } +}