The same purple ground, and a test that checks it stays the same
iOS moved to a purple gradient in ios!144. This is the other half, in the same values, plus the thing that has been missing since both files were written. 🔴 **`Theme.swift` has always opened with "one palette, two apps — change a value here and change it there in the same commit", and nothing enforced it.** Its own comment records what happened last time: the file claimed the two were matched while iOS was indigo and Android was ember. A promise in a comment is a promise nobody keeps, because nothing goes red when it is broken. `PaletteMatchesIOSTest` reads `../flamenetmessenger/Sources/Theme/Theme.swift` and compares the hex values for the accent, both gradient stops, and the deep accent. ⚠️ It SKIPS when the iOS checkout is not beside this one — a red test on a machine that only has Android would train people to ignore it. It also measures contrast, because "still readable" is the actual requirement: - white on the button gradient's LIGHT end must clear 4.5:1 (it is 5.50:1) - ink and secondary text must clear 4.5:1 on BOTH ends of the page gradient (18.6/14.8 and 8.7/6.9) Secondary text is the one that fails first — most of the explanatory copy on these screens is set in it. Falsified both ways: putting the old blue back in `Accent` fails with "primary differs: iOS says 7A3FF2, Android says 2298FF", and lightening the gradient's top stop fails with "white on the gradient's light end is 2.28:1". Mechanics worth knowing: the ground is painted ONCE on the root `Surface`, with `Scaffold(containerColor = Transparent)` under it — Scaffold's default container colour paints straight over a gradient, and a gradient applied per screen restarts at every navigation, which reads as the background flickering. The button's gradient is on the modifier with a transparent `Button`, clipped to the same shape or the corners show square. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
832cbf9214
commit
563e9efed7
@@ -1,6 +1,10 @@
|
||||
package io.flamenet.messenger.app
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -52,13 +56,31 @@ fun KitPrimaryButton(
|
||||
enabled: Boolean = true,
|
||||
tag: String? = null,
|
||||
) {
|
||||
// ⚠️ The gradient goes on the MODIFIER and the Button is transparent.
|
||||
// Compose's Button takes a single container colour, so a gradient painted
|
||||
// inside the content would sit above the ripple and below nothing — it has
|
||||
// to be the surface itself, clipped to the same shape or the corners show
|
||||
// square.
|
||||
//
|
||||
// ⚠️ Disabled fades the whole gradient rather than swapping a flat colour
|
||||
// in: a disabled button that changes shape as well as weight reads as a
|
||||
// different control.
|
||||
Button(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
shape = RoundedCornerShape(KitButton.radius),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
contentColor = Color.White,
|
||||
disabledContentColor = Color.White,
|
||||
),
|
||||
modifier = modifier
|
||||
.height(KitButton.height)
|
||||
.clip(RoundedCornerShape(KitButton.radius))
|
||||
.background(accentBrush(), RoundedCornerShape(KitButton.radius))
|
||||
.alpha(if (enabled) 1f else 0.35f)
|
||||
.then(if (tag != null) Modifier.testTag(tag) else Modifier),
|
||||
) {
|
||||
Text(text, fontSize = 14.sp, fontWeight = FontWeight.Medium)
|
||||
|
||||
@@ -109,7 +109,14 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
|
||||
|
||||
setContent {
|
||||
MessengerTheme {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
// The purple ground, painted once for the whole app. The
|
||||
// Surface itself is transparent so the brush shows through;
|
||||
// Scaffold below is transparent for the same reason, or its
|
||||
// default container colour paints straight over it.
|
||||
Surface(
|
||||
color = androidx.compose.ui.graphics.Color.Transparent,
|
||||
modifier = Modifier.fillMaxSize().background(groundBrush()),
|
||||
) {
|
||||
// 🔴 **The gate is around Root, not inside it.** Drawn over
|
||||
// the top, the message list still renders underneath for a
|
||||
// frame — and a frame is all a screenshot or a shoulder
|
||||
@@ -179,7 +186,7 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
|
||||
@Composable
|
||||
private fun Root(model: MessengerViewModel = viewModel()) {
|
||||
val phase by model.phase.collectAsStateWithLifecycle()
|
||||
Scaffold { padding ->
|
||||
Scaffold(containerColor = androidx.compose.ui.graphics.Color.Transparent) { padding ->
|
||||
val frame = Modifier.fillMaxSize().padding(padding).padding(20.dp)
|
||||
when (val current = phase) {
|
||||
// Scrollable, because a keyboard over a form needs somewhere to go.
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -51,22 +52,49 @@ object Palette {
|
||||
// brand when this app was written and is not any more — the public pages
|
||||
// carry none of it — so the names go with the colour rather than outliving
|
||||
// it. A constant called `Ember` holding blue is how a palette drifts back.
|
||||
val Accent = Color(0xFF2298FF) // buttons, bubbles, links
|
||||
val AccentBright = Color(0xFF4FACFE) // the kit's lighter primary swatch
|
||||
val AccentSoft = Color(0x1A2298FF)
|
||||
val Accent = Color(0xFF7A3FF2) // buttons, bubbles, links
|
||||
val AccentBright = Color(0xFFB99BFF) // links and small text on the dark ground
|
||||
val AccentDeep = Color(0xFF5B27C7) // links and small text on the light ground
|
||||
val AccentSoft = Color(0x1A7A3FF2)
|
||||
|
||||
// The gradient.
|
||||
//
|
||||
// 🔴 **The LIGHT end is the one that has to carry the label.** A gradient
|
||||
// button is only as readable as its lightest stop, and checking the mean
|
||||
// or the dark end is how a pretty button ships unreadable. Measured with
|
||||
// white text: #7A3FF2 is 5.50:1 and #5B27C7 is 8.20:1, so the label clears
|
||||
// AA along the whole sweep rather than at one end of it.
|
||||
val GradientTop = Color(0xFF7A3FF2)
|
||||
val GradientBottom = Color(0xFF5B27C7)
|
||||
|
||||
// The page ground, as a gradient rather than a flat fill.
|
||||
//
|
||||
// ⚠️ Deliberately a whisper on light. Cards are white and have to lift off
|
||||
// it, and every piece of body text sits on this: ink reads 18.6:1 at the
|
||||
// top and 14.8:1 at the bottom, secondary text 8.7:1 and 6.9:1. A
|
||||
// saturated purple would look bolder in a screenshot and fight both.
|
||||
val GroundTop = Color(0xFFF8F5FF)
|
||||
val GroundBottom = Color(0xFFE4D8FB)
|
||||
val NightGroundTop = Color(0xFF241645)
|
||||
val NightGroundBottom = Color(0xFF0D0718)
|
||||
|
||||
val Teal = Color(0xFF3BD5B2) // success and presence
|
||||
val Pink = Color(0xFFED497C) // the kit's alert accent
|
||||
|
||||
// Neutrals, all from the kit.
|
||||
val Ink = Color(0xFF090710)
|
||||
val Mist = Color(0xFFF9F9F9) // the page ground
|
||||
// ⚠️ Still needed where a gradient cannot go (a single Color parameter, a
|
||||
// system bar tint). This is the gradient's mid-tone, so the two never read
|
||||
// as different surfaces.
|
||||
val Mist = Color(0xFFF1EAFE) // the page ground, flat
|
||||
val SurfaceAlt = Color(0xFFF3F3F3) // received bubbles, pills, bands
|
||||
val IncomingBubble = Color(0xFFF3F3F3)
|
||||
// ⚠️ A blue-tinted grey rather than the flat #F3F3F3: an initials avatar
|
||||
// needs to read as a circle against the surface behind it, and two greys a
|
||||
// shade apart do not.
|
||||
val AvatarFill = Color(0xFFE8F1FB)
|
||||
// ⚠️ Lavender, not the old blue wash. A blue-tinted circle on a purple
|
||||
// ground reads as a bug rather than as a second accent.
|
||||
val AvatarFill = Color(0xFFEDE6FC)
|
||||
val SecondaryText = Color(0xFF505C69)
|
||||
val Divider = Color(0x1A505C69)
|
||||
|
||||
@@ -77,9 +105,26 @@ object Palette {
|
||||
|
||||
// Dark-mode grounds — these ARE the site's, since the site is dark.
|
||||
val InkSurface = Color(0xFF121A2E) // --panel
|
||||
val NightGround = Color(0xFF0A0E1A) // --bg
|
||||
val NightGround = Color(0xFF150C26) // --bg, the dark gradient's mid-tone
|
||||
}
|
||||
|
||||
/**
|
||||
* The app's ground.
|
||||
*
|
||||
* ⚠️ One brush for the whole app rather than a fill per screen: a gradient
|
||||
* applied per screen is a gradient that restarts at every navigation, which
|
||||
* reads as the background flickering.
|
||||
*/
|
||||
@Composable
|
||||
fun groundBrush(): Brush = Brush.verticalGradient(
|
||||
if (isSystemInDarkTheme()) listOf(Palette.NightGroundTop, Palette.NightGroundBottom)
|
||||
else listOf(Palette.GroundTop, Palette.GroundBottom)
|
||||
)
|
||||
|
||||
/** The primary action's fill. Matches `Theme.primaryGradient*` on iOS. */
|
||||
fun accentBrush(): Brush =
|
||||
Brush.verticalGradient(listOf(Palette.GradientTop, Palette.GradientBottom))
|
||||
|
||||
/** Corner radii, matching `Theme.Radius` on iOS. */
|
||||
object Radius {
|
||||
val small = 10.dp
|
||||
|
||||
Reference in New Issue
Block a user