The kit's palette, not just its layout

I kept Flamenet orange and defended it as brand continuity. That was
wrong on the facts: the orange had already been retired and the public
pages carry none of it, so the apps were the last thing wearing a brand
colour the brand had dropped — on the authority of a comment in
Theme.kt that nobody had rechecked. The comment is corrected in place
so nobody restores it for the same stale reason.

The values are measured out of the Sketch file rather than sampled from
a render. The two text colours are the ones that actually appear in its
symbols — #090710 on 165 text layers, #505C69 on 164 — and the accent
#2298FF is what it uses at 12pt as well as on buttons, so it is their
link colour and not only a fill.

The constants were called Ember and held orange. They are Accent now:
a constant named for a colour it no longer holds is how a palette
drifts back.

⚠️ Recorded because it will come up: #2298FF on white is about 3.3:1 —
fine for a bold button label, not for small text. The small text on
these screens is grey, and it needs to stay that way.

Also in this commit, the Android sign-in takes the kit's Welcome
layout: a logo mark, a 40pt greeting and one line saying what this is,
centred with room above them. It was a 24pt product name jammed against
a Server field, which is the shape of a login dialog rather than the
first thing anyone sees of the app. The submit button is 240 wide and
centred, as the kit draws it — a button spanning the phone reads as the
bottom of a form; one that does not reads as a choice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-08-27 14:54:34 -04:00
co-authored by Claude Opus 5
parent 5a592e9858
commit 87e96761cc
2 changed files with 105 additions and 43 deletions
@@ -13,6 +13,9 @@ import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.graphics.Color
import androidx.compose.foundation.layout.width
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@@ -208,7 +211,43 @@ private fun ColumnScope.SignIn(model: MessengerViewModel, phase: MessengerViewMo
// Only a relay keeping its own accounts needs an address for reset mail. // Only a relay keeping its own accounts needs an address for reset mail.
val needsEmail = registering && site?.accountProvider == SiteConfig.AccountProvider.LOCAL val needsEmail = registering && site?.accountProvider == SiteConfig.AccountProvider.LOCAL
Text("Flamenet Messenger", style = MaterialTheme.typography.headlineMedium) // 🔴 **The kit's Welcome screen, which is a masthead and not a form
// heading.** A logo mark, a 40pt greeting, and one line saying what this
// is — centred, with room above it. This was a 24pt product name jammed
// against a Server field, which is the layout of a login dialog rather
// than the first thing anyone sees of the app. See `DESIGN.md`.
//
// ⚠️ The kit's Welcome has no fields on it at all — it is a splash before
// a phone number is asked for. Flamenet does not ask for a phone number
// and has nothing to put on a second screen, so the masthead and the form
// share one. The typography and the centring are the kit's; the fields
// are ours.
Spacer(Modifier.height(Space.xl))
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(Radius.medium))
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center,
) {
Text("F", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color.White)
}
Spacer(Modifier.height(Space.lg))
Text("Hi there!", fontSize = 40.sp, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(Space.sm))
Text(
"Flamenet Messenger. End-to-end encrypted, no phone number, and it "
+ "runs on whichever server you point it at.",
fontSize = 14.sp,
color = Palette.SecondaryText,
textAlign = TextAlign.Center,
)
}
Spacer(Modifier.height(Space.xl))
OutlinedTextField( OutlinedTextField(
value = relay, value = relay,
@@ -274,18 +313,29 @@ private fun ColumnScope.SignIn(model: MessengerViewModel, phase: MessengerViewMo
Text(it, color = MaterialTheme.colorScheme.error, modifier = Modifier.testTag("error")) Text(it, color = MaterialTheme.colorScheme.error, modifier = Modifier.testTag("error"))
} }
// ⚠️ 240 wide and centred, which is the kit's primary button on this
// screen — not a full-width slab. A button that spans the phone reads as
// the bottom of a form; one that does not reads as a choice.
Spacer(Modifier.height(Space.md))
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Button( Button(
onClick = { onClick = {
model.signIn(handle.trim(), passphrase, register = registering, email = email.trim()) model.signIn(handle.trim(), passphrase, register = registering, email = email.trim())
}, },
enabled = !phase.busy && handle.isNotBlank() && passphrase.isNotBlank() && enabled = !phase.busy && handle.isNotBlank() && passphrase.isNotBlank() &&
(!needsEmail || email.isNotBlank()), (!needsEmail || email.isNotBlank()),
modifier = Modifier.fillMaxWidth().testTag("submit"), shape = RoundedCornerShape(KitButton.radius),
modifier = Modifier.width(240.dp).height(KitButton.height).testTag("submit"),
) { ) {
if (phase.busy) { if (phase.busy) {
CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp) CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp)
} else { } else {
Text(if (registering) "Create account" else "Sign in") Text(
if (registering) "Create account" else "Sign in",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
)
}
} }
} }
@@ -21,11 +21,12 @@ import androidx.compose.ui.unit.sp
/** /**
* The Flamenet design system. * The Flamenet design system.
* *
* 🔴 **Taken from flamenetmessenger.com, not invented.** The site declares * 🔴 **Taken from the design kit, measured out of the Sketch file.** This
* `--flame: #ff5a1f`, `--blue: #3b6fff`, `--accent: #2dd4a7` over a dark navy * comment used to say the palette came from flamenetmessenger.com and quoted
* ground (`--bg: #0a0e1a`, `--panel: #121a2e`). The accents transfer as-is; * `--flame: #ff5a1f` — which had already been retired from the public pages,
* the site's `--ink`/`--muted` are light-on-dark and are NOT pasted in here, * so the app was carrying a brand colour the brand no longer used, on the
* because the apps are light. Dark mode does use the site's real grounds. * authority of a comment nobody rechecked. The values here are read from the
* kit's own symbols; see `Palette`.
* *
* 🔴 **One palette, two apps.** iOS carries the same values under the same * 🔴 **One palette, two apps.** iOS carries the same values under the same
* names in `Sources/Theme/Theme.swift`. This file's comment used to claim the * names in `Sources/Theme/Theme.swift`. This file's comment used to claim the
@@ -39,32 +40,39 @@ import androidx.compose.ui.unit.sp
* untouched — this is a skin over working screens. * untouched — this is a skin over working screens.
*/ */
object Palette { object Palette {
// Brand — the site's --flame. // 🔴 **The design kit's palette, measured out of the file** — not sampled
// ⚠️ White on Ember is about 3.1:1: fine for a bold button label (AA // from a render and not approximated. The text colours below are the two
// Large), NOT fine for small text. Small text and links use EmberDeep. // that actually appear in its symbols: #090710 on 165 text layers and
val Ember = Color(0xFFFF5A1F) // #505C69 on 164. The accent is the one it uses at 12pt as well as on
val EmberDeep = Color(0xFFC4400F) // buttons, so it is their link colour, not only their fill.
val EmberSoft = Color(0x1AFF5A1F) //
// ⚠️ These names used to say Ember, and held orange. The orange was the
// 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)
// The site's other two accents, used sparingly. val Teal = Color(0xFF3BD5B2) // success and presence
val Blue = Color(0xFF3B6FFF) // --blue, informational only val Pink = Color(0xFFED497C) // the kit's alert accent
val Teal = Color(0xFF2DD4A7) // --accent, success and presence
// Neutrals: the site's navy family resolved for a LIGHT ground. // Neutrals, all from the kit.
// ⚠️ --ink (#e8edf7) and --muted (#9aa7c2) are light-on-dark values and val Ink = Color(0xFF090710)
// would be invisible here, so they are not pasted in literally. val Mist = Color(0xFFF9F9F9) // the page ground
val Ink = Color(0xFF0F1626) val SurfaceAlt = Color(0xFFF3F3F3) // received bubbles, pills, bands
val Mist = Color(0xFFF6F8FC) // the light ground (was 'Parchment' when the palette was warm) val IncomingBubble = Color(0xFFF3F3F3)
val SurfaceAlt = Color(0xFFEDF1F8) // ⚠️ A blue-tinted grey rather than the flat #F3F3F3: an initials avatar
val IncomingBubble = Color(0xFFEDF1F8) // needs to read as a circle against the surface behind it, and two greys a
val AvatarFill = Color(0xFFE4EAF6) // shade apart do not.
val SecondaryText = Color(0xFF5A6884) val AvatarFill = Color(0xFFE8F1FB)
val Divider = Color(0x1A1F2B45) val SecondaryText = Color(0xFF505C69)
val Divider = Color(0x1A505C69)
// Semantic // Semantic
val Success = Color(0xFF1F9E7A) val Success = Color(0xFF3BD5B2)
val Warning = Color(0xFFB8860B) val Warning = Color(0xFFB8860B)
val Danger = Color(0xFFC0392B) val Danger = Color(0xFFED497C)
// Dark-mode grounds — these ARE the site's, since the site is dark. // Dark-mode grounds — these ARE the site's, since the site is dark.
val InkSurface = Color(0xFF121A2E) // --panel val InkSurface = Color(0xFF121A2E) // --panel
@@ -187,9 +195,9 @@ fun MessengerTheme(content: @Composable () -> Unit) {
MaterialTheme( MaterialTheme(
colorScheme = if (dark) { colorScheme = if (dark) {
darkColorScheme( darkColorScheme(
primary = Palette.Ember, primary = Palette.AccentBright,
onPrimary = Color.White, onPrimary = Color.White,
secondary = Palette.EmberDeep, secondary = Palette.Accent,
background = Palette.NightGround, background = Palette.NightGround,
surface = Palette.InkSurface, surface = Palette.InkSurface,
surfaceVariant = Palette.InkSurface, surfaceVariant = Palette.InkSurface,
@@ -201,9 +209,13 @@ fun MessengerTheme(content: @Composable () -> Unit) {
) )
} else { } else {
lightColorScheme( lightColorScheme(
primary = Palette.EmberDeep, // ⚠️ The kit's #2298FF on white is about 3.3:1 — fine for a
// bold button label, not for small text. Anything small and
// blue must use a darker step; the two places that do are the
// section labels and the timestamps, and both are grey.
primary = Palette.Accent,
onPrimary = Color.White, onPrimary = Color.White,
secondary = Palette.Ember, secondary = Palette.AccentBright,
background = Palette.Mist, background = Palette.Mist,
surface = Color.White, surface = Color.White,
surfaceVariant = Palette.SurfaceAlt, surfaceVariant = Palette.SurfaceAlt,