mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
Merge pull request #3610 from vitorpamplona/claude/save-button-switcher-colors-7vnmm5
Fix onPrimary contrast and add color scheme preview tool
This commit is contained in:
+2
-1
@@ -38,6 +38,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
@@ -170,7 +171,7 @@ fun CreateNestSheet(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
|
||||
) {
|
||||
androidx.compose.material3.Switch(
|
||||
Switch(
|
||||
checked = state.scheduled,
|
||||
onCheckedChange = viewModel::onScheduledToggle,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.theme
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.graphics.toArgb
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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 kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
// Living reference of every foreground/background pair the mobile theme uses, listed by its
|
||||
// ColorScheme role name and rendered with the real AmethystTheme colors (dark | light). Each row
|
||||
// previews the foreground on its background, the live WCAG contrast, the two hexes, and the app
|
||||
// surfaces that consume the pair. Reads the live scheme, so editing Theme.kt re-renders it.
|
||||
|
||||
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(
|
||||
"primary / onPrimary",
|
||||
{ it.primary },
|
||||
{ it.onPrimary },
|
||||
"Top-bar Save button, FABs, filled buttons, checked switch, sliders, progress.",
|
||||
highlight = true,
|
||||
),
|
||||
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 Color.hex(): String = "#%06X".format(0xFFFFFF and toArgb())
|
||||
|
||||
private fun contrastRatio(
|
||||
a: Color,
|
||||
b: Color,
|
||||
): Float {
|
||||
val la = a.luminance()
|
||||
val lb = b.luminance()
|
||||
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"
|
||||
}
|
||||
|
||||
@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, heightDp = 980, showBackground = true)
|
||||
@Composable
|
||||
fun ThemeColorPairsPreview() {
|
||||
ThemeComparisonRow {
|
||||
RolePairList()
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,10 @@ private fun darkColors(accent: AccentColorType): ColorScheme {
|
||||
val secondary = accentSecondary(accent, dark = true)
|
||||
return darkColorScheme(
|
||||
primary = primary,
|
||||
onPrimary = Color.White,
|
||||
// Content on a primary fill (filled buttons, FAB glyphs). Picked by contrast against the
|
||||
// fill: on the dark theme `primary` is a light pastel, so this resolves to black (white on
|
||||
// it is only ~2.6:1). Left as white it washed out every filled button/FAB.
|
||||
onPrimary = onAccent(primary),
|
||||
primaryContainer = accentContainer(primary, dark = true),
|
||||
onPrimaryContainer = onAccentContainer(primary, dark = true),
|
||||
secondary = secondary,
|
||||
@@ -178,7 +181,9 @@ private fun lightColors(accent: AccentColorType): ColorScheme {
|
||||
val secondary = accentSecondary(accent, dark = false)
|
||||
return lightColorScheme(
|
||||
primary = primary,
|
||||
onPrimary = Color.White,
|
||||
// Contrast-picked content on the primary fill; the light theme's deep primary resolves to
|
||||
// white (unchanged), while keeping the rule identical across both schemes.
|
||||
onPrimary = onAccent(primary),
|
||||
primaryContainer = accentContainer(primary, dark = false),
|
||||
onPrimaryContainer = onAccentContainer(primary, dark = false),
|
||||
secondary = secondary,
|
||||
|
||||
Reference in New Issue
Block a user