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 ab158c51b5..c5e8e92e07 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 @@ -28,13 +28,16 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row 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.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -48,205 +51,246 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.model.AccentColorType +import com.vitorpamplona.amethyst.model.ThemeType import kotlin.math.max import kotlin.math.min -// A living reference of every foreground/background pair the mobile theme uses, rendered with the -// real AmethystTheme colors (no approximation). Edit Theme.kt and the two columns below re-render. -// Left column is the dark theme, right column is the light theme, each with the current accent. -// -// Each pair references the actual ColorScheme roles the app consumes for that surface, so tweaking a -// role here previews everywhere it lands. `usage` names the concrete in-app surfaces. -private class Pair( +// Living reference + side-by-side "before / after" of the mobile theme, rendered with the real +// color schemes so editing Theme.kt re-renders both. Each row is one app surface (Save button, +// checked switch, settings tile, card, unchecked switch, …) shown four ways: +// Old·Dark | New·Dark | Old·Light | New·Light +// so the effect of the accent-normalization (+ the new filledAccent role) is directly comparable +// within each theme. "Old" reconstructs the pre-normalization scheme: purple primary + teal +// secondary with Material's default violet-tinted surfaces/containers and the deep default +// onPrimary — i.e. the theme before container-derivation, surface-neutralization and onPrimary=White. + +// ----- reconstructed pre-normalization scheme (default purple accent) ----- +private fun oldDarkColors(): ColorScheme = + darkColorScheme( + primary = Purple200, + secondary = Teal200, + tertiary = Teal200, + background = Color.Black, + surface = Color.Black, + surfaceDim = Color.Black, + surfaceVariant = Color(0xFF1D1A22), + ) + +private fun oldLightColors(): ColorScheme = + lightColorScheme( + primary = Purple500, + secondary = Teal200, + tertiary = Teal200, + surfaceContainerHighest = Color(0xFFECE6F0), + surfaceVariant = Color(0xFFFAF5FC), + ) + +// One app surface and how to resolve its fill/content from a scheme. `isOld` lets a surface pick a +// different role for the two eras — the Save button + switch used primary/onPrimary before and +// filledAccent/onFilledAccent now; everything else keeps its role and just shows the value drift. +private class StyleEntry( val name: String, - val bg: (ColorScheme) -> Color, - val fg: (ColorScheme) -> Color, val usage: String, + val bg: (ColorScheme, Boolean) -> Color, + val fg: (ColorScheme, Boolean) -> Color, val highlight: Boolean = false, ) -private val COLOR_PAIRS = +private val STYLE_ENTRIES = listOf( - Pair( - "filledAccent / onFilledAccent", - { it.filledAccent }, - { it.onFilledAccent }, - "Top-bar Save·Post·Send·Create button, standalone Save button, checked switch (track + thumb).", + StyleEntry( + "Save button · checked switch", + "Old: primary/onPrimary (washed out). New: the dedicated filledAccent fill.", + bg = { s, old -> if (old) s.primary else s.filledAccent }, + fg = { s, old -> if (old) s.onPrimary else s.onFilledAccent }, highlight = true, ), - Pair( - "primary / onPrimary", - { it.primary }, - { it.onPrimary }, - "Floating action buttons (New Note, New Image…), generic filled buttons, sliders, progress.", + StyleEntry( + "FAB · filled button", + "primary / onPrimary — unchanged; still the pastel pairing on dark.", + bg = { s, _ -> s.primary }, + fg = { s, _ -> s.onPrimary }, ), - Pair( - "primaryContainer / onPrimaryContainer", - { it.primaryContainer }, - { it.onPrimaryContainer }, - "Settings icon tiles, tonal cards (DVM, wallet, relay info), tonal chips.", + StyleEntry( + "Settings tile · tonal card", + "primaryContainer / onPrimaryContainer — old = Material violet, new = accent-derived.", + bg = { s, _ -> s.primaryContainer }, + fg = { s, _ -> s.onPrimaryContainer }, ), - Pair( - "secondaryContainer / onSecondaryContainer", - { it.secondaryContainer }, - { it.onSecondaryContainer }, - "Selected-reaction highlight, relay-group chips, chatroom header, reply-mode toggle.", + StyleEntry( + "Selected reaction · chip", + "secondaryContainer / onSecondaryContainer.", + bg = { s, _ -> s.secondaryContainer }, + fg = { s, _ -> s.onSecondaryContainer }, ), - Pair( - "secondary / onSecondary", - { it.secondary }, - { it.onSecondary }, - "Teal accent (purple theme only); tertiary mirrors it.", + StyleEntry( + "Teal accent", + "secondary / onSecondary (purple theme).", + bg = { s, _ -> s.secondary }, + fg = { s, _ -> s.onSecondary }, ), - Pair( - "tertiaryContainer / onTertiaryContainer", - { it.tertiaryContainer }, - { it.onTertiaryContainer }, - "Tertiary tonal surfaces; derived from the accent alongside the other containers.", + StyleEntry( + "App background · body text", + "surface / onSurface — old carried Material's violet tint, new is neutral grey.", + bg = { s, _ -> s.surface }, + fg = { s, _ -> s.onSurface }, ), - Pair( - "error / onError", - { it.error }, - { it.onError }, - "Destructive dialogs and error states. Material baseline — not themed to the accent.", + StyleEntry( + "Secondary text", + "surface / onSurfaceVariant.", + bg = { s, _ -> s.surface }, + fg = { s, _ -> s.onSurfaceVariant }, ), - Pair( - "surface / onSurface", - { it.surface }, - { it.onSurface }, - "App background and primary body text everywhere.", + StyleEntry( + "Card · sheet", + "surfaceContainer / onSurface — old = Material default, new = neutral ramp.", + bg = { s, _ -> s.surfaceContainer }, + fg = { s, _ -> s.onSurface }, ), - Pair( - "surface / onSurfaceVariant", - { it.surface }, - { it.onSurfaceVariant }, - "Secondary text, timestamps, subtitles and icons on the background.", + StyleEntry( + "Unchecked switch", + "surfaceContainerHighest / outline (off-track + thumb).", + bg = { s, _ -> s.surfaceContainerHighest }, + fg = { s, _ -> s.outline }, ), - Pair( - "surfaceContainer / onSurface", - { it.surfaceContainer }, - { it.onSurface }, - "Cards, bottom sheets and elevated rows, with body text on top.", - ), - Pair( - "surfaceVariant / onSurfaceVariant", - { it.surfaceVariant }, - { it.onSurfaceVariant }, - "Subtle filled areas — code blocks, inset panels, muted backgrounds.", - ), - Pair( - "surfaceContainerHighest / outline", - { it.surfaceContainerHighest }, - { it.outline }, - "The UNCHECKED switch (off-track fill + thumb) and other inactive controls.", + StyleEntry( + "Error", + "error / onError — Material baseline in both.", + bg = { s, _ -> s.error }, + fg = { s, _ -> s.onError }, ), ) private fun Color.hex(): String = "#%06X".format(0xFFFFFF and toArgb()) -// WCAG contrast ratio between two opaque colors. private fun contrastRatio( a: Color, b: Color, ): Float { val la = a.luminance() val lb = b.luminance() - val hi = max(la, lb) - val lo = min(la, lb) - return (hi + 0.05f) / (lo + 0.05f) + return (max(la, lb) + 0.05f) / (min(la, lb) + 0.05f) } -private fun contrastLabel(ratio: Float): String = - when { - ratio >= 4.5f -> "AA" - ratio >= 3f -> "AA-lg" - else -> "low" - } +private const val CELL_WIDTH = 116 +private const val LABEL_WIDTH = 196 @Composable -private fun ColorPairRow(pair: Pair) { - val scheme = MaterialTheme.colorScheme - val bg = pair.bg(scheme) - val fg = pair.fg(scheme) +private fun StyleCell( + entry: StyleEntry, + scheme: ColorScheme, + isOld: Boolean, +) { + val bg = entry.bg(scheme, isOld) + val fg = entry.fg(scheme, isOld) val ratio = contrastRatio(bg, fg) + val label = MaterialTheme.colorScheme.onSurfaceVariant - Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { + Column(modifier = Modifier.width(CELL_WIDTH.dp).padding(horizontal = 4.dp)) { Box( modifier = Modifier - .size(width = 92.dp, height = 58.dp) - .clip(RoundedCornerShape(10.dp)) + .fillMaxWidth() + .height(48.dp) + .clip(RoundedCornerShape(8.dp)) .background(bg) - .then( - if (pair.highlight) { - Modifier.border(2.dp, scheme.onBackground, RoundedCornerShape(10.dp)) - } else { - Modifier.border(1.dp, scheme.outlineVariant, RoundedCornerShape(10.dp)) - }, - ).padding(8.dp), + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(8.dp)) + .padding(6.dp), ) { - Text("Aa", color = fg, fontSize = 19.sp, fontWeight = FontWeight.Bold) + Text("Aa", color = fg, fontSize = 15.sp, fontWeight = FontWeight.Bold) Text( - "${"%.1f".format(ratio)}:1 ${contrastLabel(ratio)}", + "%.1f".format(ratio), color = fg, - fontSize = 8.5.sp, + fontSize = 8.sp, fontFamily = FontFamily.Monospace, modifier = Modifier.align(Alignment.BottomEnd), ) } + Text(bg.hex(), color = label, fontSize = 8.5.sp, fontFamily = FontFamily.Monospace) + Text(fg.hex(), color = label, fontSize = 8.5.sp, fontFamily = FontFamily.Monospace) + } +} - Spacer(Modifier.width(10.dp)) - - Column(modifier = Modifier.weight(1f)) { +@Composable +private fun StyleRow( + entry: StyleEntry, + oldDark: ColorScheme, + newDark: ColorScheme, + oldLight: ColorScheme, + newLight: ColorScheme, +) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp), + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.width(LABEL_WIDTH.dp).padding(end = 8.dp)) { Text( - pair.name, - color = scheme.onBackground, - fontSize = 12.5.sp, - fontWeight = if (pair.highlight) FontWeight.Bold else FontWeight.Medium, + entry.name, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 11.5.sp, + fontWeight = if (entry.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, + entry.usage, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 9.5.sp, + maxLines = 3, overflow = TextOverflow.Ellipsis, ) } + StyleCell(entry, oldDark, isOld = true) + StyleCell(entry, newDark, isOld = false) + StyleCell(entry, oldLight, isOld = true) + StyleCell(entry, newLight, isOld = false) } } @Composable -private fun ColorPairsList() { - Column( - modifier = Modifier.padding(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text( - "fg / bg pairs · $ACCENT_HINT", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 13.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), - ) - COLOR_PAIRS.forEach { ColorPairRow(it) } - } +private fun ColumnHeader(text: String) { + Text( + text, + modifier = Modifier.width(CELL_WIDTH.dp).padding(horizontal = 4.dp), + color = MaterialTheme.colorScheme.onSurface, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + ) } -private const val ACCENT_HINT = "accent = Settings → Accent Color (default Purple)" - -@Preview(name = "Theme color pairs · dark | light", widthDp = 820, showBackground = true) +@Preview(name = "Theme styleguide — old vs new", widthDp = 720, showBackground = true) @Composable -fun ThemeColorPairsPreview() { - ThemeComparisonRow { - ColorPairsList() +fun ThemeStyleguideOldVsNewPreview() { + val oldDark = oldDarkColors() + val newDark = darkColors(AccentColorType.PURPLE) + val oldLight = oldLightColors() + val newLight = lightColors(AccentColorType.PURPLE) + + // Neutral dark chrome for the table itself; each cell paints its own scheme color. + AmethystTheme(ThemeType.DARK) { + Surface(color = MaterialTheme.colorScheme.background) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + "Theme styleguide · before normalization → current", + color = MaterialTheme.colorScheme.onSurface, + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 6.dp), + ) + Row { + Spacer(Modifier.width(LABEL_WIDTH.dp)) + ColumnHeader("Old·Dark") + ColumnHeader("New·Dark") + ColumnHeader("Old·Light") + ColumnHeader("New·Light") + } + STYLE_ENTRIES.forEach { + StyleRow(it, oldDark, newDark, oldLight, newLight) + } + } + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index ead6d6e84d..42e8e71907 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -131,7 +131,7 @@ fun AccentColorType.previewColor(dark: Boolean): Color = accentPrimary(this, dar // Contrast colour (black or white) for content drawn on top of an accent swatch. fun contentColorOnAccent(color: Color): Color = onAccent(color) -private fun darkColors(accent: AccentColorType): ColorScheme { +internal fun darkColors(accent: AccentColorType): ColorScheme { val primary = accentPrimary(accent, dark = true) val secondary = accentSecondary(accent, dark = true) return darkColorScheme( @@ -173,7 +173,7 @@ private fun darkColors(accent: AccentColorType): ColorScheme { ) } -private fun lightColors(accent: AccentColorType): ColorScheme { +internal fun lightColors(accent: AccentColorType): ColorScheme { val primary = accentPrimary(accent, dark = false) val secondary = accentSecondary(accent, dark = false) return lightColorScheme(