White text boxes — matches ios!144
An input is the one surface a person types into and reads back character by character, and a dark field on a dark ground gave it the least contrast on the screen at the moment it mattered most. Typed text reads 18.3:1 on white; the placeholder 4.6:1, because a placeholder is still text. ⚠️ **Its own colour, not `SurfaceAlt`.** That fill is shared with incoming bubbles and the tab pill, which stay dark — a white message bubble on this ground would read as somebody else's app. 🔴 **Every colour a field draws has to be named, not just the container.** Material derives label, placeholder, cursor and outline from the colour scheme, which is the DARK scheme here: setting only `containerColor` leaves a light-on-dark placeholder on a white box, and it simply disappears. iOS had the same failure in a different dialect — SwiftUI drew the placeholder and caret for a dark background until the field was given `.environment(\.colorScheme, .light)`. Two frameworks, one mistake: a white island in a dark app has to be told it is an island. One `fieldColors()` in `Theme.kt`, applied to all 34 `OutlinedTextField` call sites. Mechanical and wide rather than a new wrapper composable, because a wrapper would have to be adopted one screen at a time and the ones that were missed would be exactly the ones nobody looks at. ⚠️ The message composer is a `BasicTextField` and stays dark. It is a pill in the chat, not a form field, and turning it white would put a white bar across the bottom of every conversation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b824e626e7
commit
8fa66cd603
@@ -447,6 +447,7 @@ fun ColumnScope.GroupsSection(
|
||||
|
||||
if (creating) {
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("Group name") },
|
||||
@@ -795,6 +796,7 @@ fun ColumnScope.AccountScreen(
|
||||
// same duplication the contact rows had.
|
||||
SettingsRow(label = "Display name") {}
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = displayName,
|
||||
onValueChange = { displayName = it },
|
||||
placeholder = { Text("How others see you", fontSize = 14.sp) },
|
||||
@@ -816,6 +818,7 @@ fun ColumnScope.AccountScreen(
|
||||
description = "Changing it signs this device out — that is what changing it means.",
|
||||
) {}
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
placeholder = { Text("At least 8 characters", fontSize = 14.sp) },
|
||||
@@ -2923,6 +2926,7 @@ private fun ColumnScope.GroupMembers(
|
||||
|
||||
if (renaming) {
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = draftName,
|
||||
onValueChange = { draftName = it },
|
||||
label = { Text("Group name") },
|
||||
@@ -3165,6 +3169,7 @@ fun ColumnScope.AddContactForm(
|
||||
fontSize = 12.sp, color = Palette.SecondaryText)
|
||||
Spacer(Modifier.height(Space.xs))
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = gatePin, onValueChange = { gatePin = it; gateWrong = false },
|
||||
placeholder = { Text("Parent PIN", fontSize = 14.sp) },
|
||||
singleLine = true,
|
||||
@@ -3200,6 +3205,7 @@ fun ColumnScope.AddContactForm(
|
||||
val card = remember(username) { ContactCard.decode(username) }
|
||||
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = username,
|
||||
onValueChange = {
|
||||
username = it
|
||||
@@ -3828,6 +3834,7 @@ fun ColumnScope.MailboxesSection(
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = host,
|
||||
onValueChange = { host = it },
|
||||
placeholder = { Text("relay.example.com", fontSize = 14.sp) },
|
||||
@@ -3836,6 +3843,7 @@ fun ColumnScope.MailboxesSection(
|
||||
)
|
||||
Spacer(Modifier.height(Space.xs))
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = passphrase,
|
||||
onValueChange = { passphrase = it },
|
||||
placeholder = { Text("Your passphrase", fontSize = 14.sp) },
|
||||
@@ -4031,6 +4039,7 @@ private fun EditMessageSheet(
|
||||
text = {
|
||||
Column {
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = text,
|
||||
onValueChange = { text = it },
|
||||
modifier = Modifier.fillMaxWidth().testTag("edit-field"),
|
||||
@@ -4169,6 +4178,7 @@ internal fun ReportSheet(
|
||||
|
||||
Spacer(Modifier.height(Space.sm))
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = note,
|
||||
onValueChange = { note = it },
|
||||
label = { Text("Anything you want to add (optional)") },
|
||||
|
||||
@@ -145,6 +145,7 @@ private fun ColumnScope.FilesSetUp(
|
||||
Spacer(Modifier.height(Space.md))
|
||||
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = passphrase, onValueChange = { passphrase = it },
|
||||
label = { Text("Your Flamenet passphrase") },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
@@ -153,6 +154,7 @@ private fun ColumnScope.FilesSetUp(
|
||||
)
|
||||
Spacer(Modifier.height(Space.sm))
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = again, onValueChange = { again = it },
|
||||
label = { Text("Type it again") },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
@@ -187,6 +189,7 @@ private fun ColumnScope.FilesUnlock(
|
||||
fontSize = 14.sp, color = Palette.SecondaryText)
|
||||
Spacer(Modifier.height(Space.md))
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = passphrase, onValueChange = { passphrase = it },
|
||||
label = { Text("Passphrase") },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
@@ -452,6 +455,7 @@ private fun ColumnScope.AddFileSheet(
|
||||
Text("Add a file", fontWeight = FontWeight.Medium, fontSize = 16.sp)
|
||||
Spacer(Modifier.height(Space.sm))
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = name, onValueChange = { name = it },
|
||||
label = { Text("Name") }, singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth().testTag("files-add-name"),
|
||||
@@ -510,6 +514,7 @@ private fun ColumnScope.FolderNameSheet(
|
||||
Text("New folder", fontWeight = FontWeight.Medium, fontSize = 16.sp)
|
||||
Spacer(Modifier.height(Space.sm))
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = name, onValueChange = { name = it },
|
||||
label = { Text("Name") }, singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth().testTag("files-folder-name"),
|
||||
|
||||
@@ -187,6 +187,7 @@ private fun PassphraseDialog(
|
||||
Text(explainer, fontSize = 12.sp, color = Palette.SecondaryText)
|
||||
Spacer(Modifier.height(Space.sm))
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = phrase,
|
||||
onValueChange = { phrase = it },
|
||||
placeholder = { Text("Passphrase", fontSize = 14.sp) },
|
||||
@@ -197,6 +198,7 @@ private fun PassphraseDialog(
|
||||
if (requireRepeat) {
|
||||
Spacer(Modifier.height(Space.xs))
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = repeat,
|
||||
onValueChange = { repeat = it },
|
||||
placeholder = { Text("Repeat passphrase", fontSize = 14.sp) },
|
||||
|
||||
@@ -395,6 +395,7 @@ private fun ColumnScope.SignIn(model: MessengerViewModel, phase: MessengerViewMo
|
||||
Spacer(Modifier.height(Space.xl))
|
||||
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = relay,
|
||||
onValueChange = model::setRelay,
|
||||
label = { Text("Server") },
|
||||
@@ -415,6 +416,7 @@ private fun ColumnScope.SignIn(model: MessengerViewModel, phase: MessengerViewMo
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = handle,
|
||||
onValueChange = { handle = it },
|
||||
label = { Text("Handle") },
|
||||
@@ -429,6 +431,7 @@ private fun ColumnScope.SignIn(model: MessengerViewModel, phase: MessengerViewMo
|
||||
// would be collecting something nobody needs.
|
||||
if (needsEmail) {
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = { Text("Email") },
|
||||
@@ -444,6 +447,7 @@ private fun ColumnScope.SignIn(model: MessengerViewModel, phase: MessengerViewMo
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = passphrase,
|
||||
onValueChange = { passphrase = it },
|
||||
label = { Text("Passphrase") },
|
||||
@@ -459,6 +463,7 @@ private fun ColumnScope.SignIn(model: MessengerViewModel, phase: MessengerViewMo
|
||||
// this device to refuse an under-13 sign-up, then forgotten. Nothing
|
||||
// about age is sent to any server.
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = birthYear,
|
||||
onValueChange = { birthYear = it.filter(Char::isDigit).take(4) },
|
||||
label = { Text("Year of birth") },
|
||||
@@ -569,6 +574,7 @@ private fun ColumnScope.ResetPassword(model: MessengerViewModel, handle: String)
|
||||
) { Text("Send a code") }
|
||||
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = code,
|
||||
onValueChange = { code = it },
|
||||
label = { Text("Code from the email") },
|
||||
@@ -576,6 +582,7 @@ private fun ColumnScope.ResetPassword(model: MessengerViewModel, handle: String)
|
||||
modifier = Modifier.fillMaxWidth().testTag("reset-code"),
|
||||
)
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = fresh,
|
||||
onValueChange = { fresh = it },
|
||||
label = { Text("New password") },
|
||||
|
||||
@@ -124,6 +124,7 @@ fun ColumnScope.MessagesScreen(
|
||||
Spacer(Modifier.height(Space.sm))
|
||||
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = query.value,
|
||||
onValueChange = { query.value = it },
|
||||
placeholder = { Text("Search conversations") },
|
||||
|
||||
@@ -148,6 +148,7 @@ fun ColumnScope.NetworkSection(
|
||||
Spacer(Modifier.height(Space.md))
|
||||
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = draft,
|
||||
onValueChange = { draft = it },
|
||||
placeholder = { Text("obfs4 198.51.100.7:4444 …", fontSize = 13.sp) },
|
||||
|
||||
@@ -56,6 +56,7 @@ fun ParentalControlsSection() {
|
||||
)
|
||||
Spacer(Modifier.height(Space.sm))
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = newPin, onValueChange = { newPin = it },
|
||||
placeholder = { Text("Parent PIN (at least 4 digits)", fontSize = 14.sp) },
|
||||
singleLine = true,
|
||||
@@ -64,6 +65,7 @@ fun ParentalControlsSection() {
|
||||
)
|
||||
Spacer(Modifier.height(Space.xs))
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = repeatPin, onValueChange = { repeatPin = it },
|
||||
placeholder = { Text("Repeat PIN", fontSize = 14.sp) },
|
||||
singleLine = true,
|
||||
@@ -86,6 +88,7 @@ fun ParentalControlsSection() {
|
||||
fontSize = 12.sp, color = Palette.SecondaryText)
|
||||
Spacer(Modifier.height(Space.sm))
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = pinEntry, onValueChange = { pinEntry = it; wrong = false },
|
||||
placeholder = { Text("Parent PIN", fontSize = 14.sp) },
|
||||
singleLine = true,
|
||||
|
||||
@@ -357,6 +357,7 @@ private fun StoryActions(
|
||||
+ "saw this story can read it.", fontSize = 13.sp)
|
||||
Spacer(Modifier.height(Space.sm))
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = draft, onValueChange = { draft = it },
|
||||
placeholder = { Text("Message") },
|
||||
modifier = Modifier.fillMaxWidth().testTag("story-reply-field"),
|
||||
@@ -453,6 +454,7 @@ fun StoryComposerDialog(
|
||||
}
|
||||
}
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = caption.value,
|
||||
onValueChange = { caption.value = it },
|
||||
placeholder = { Text("Add a caption…") },
|
||||
@@ -464,6 +466,7 @@ fun StoryComposerDialog(
|
||||
// §6.13 — attach a live stream. The picked picture becomes its
|
||||
// poster: what people see before deciding whether to connect.
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = streamUrl.value,
|
||||
onValueChange = { streamUrl.value = it },
|
||||
label = { Text("Live stream link (optional)", fontSize = 12.sp) },
|
||||
@@ -706,6 +709,7 @@ private fun TextStoryComposer(onDismiss: () -> Unit, onPost: (String) -> Unit) {
|
||||
title = { Text("New story") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = text,
|
||||
onValueChange = { text = it },
|
||||
placeholder = { Text("Say something") },
|
||||
|
||||
@@ -14,6 +14,7 @@ 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.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -154,6 +155,50 @@ fun groundBrush(): Brush =
|
||||
fun accentBrush(): Brush =
|
||||
Brush.verticalGradient(listOf(Palette.GradientTop, Palette.GradientBottom))
|
||||
|
||||
/**
|
||||
* Text fields, and only text fields.
|
||||
*
|
||||
* 🔴 **White, on a dark app, deliberately.** An input is the one surface a
|
||||
* person types into and reads back character by character, and a dark field on
|
||||
* a dark ground gives it the least contrast on the screen at the moment it
|
||||
* matters most. Typed text reads 18.3:1 on white; the placeholder 4.6:1,
|
||||
* because a placeholder is still text. Matches `Theme.fieldFill` on iOS.
|
||||
*
|
||||
* ⚠️ **Not `SurfaceAlt`.** That fill is shared with incoming bubbles and the
|
||||
* tab pill, which stay dark — a white message bubble on this ground would read
|
||||
* as somebody else's app.
|
||||
*
|
||||
* ⚠️ Every colour a field draws has to be named, not just the container.
|
||||
* Material derives label, placeholder, cursor and outline from the colour
|
||||
* scheme, which is the DARK scheme here — leaving any of them unset puts a
|
||||
* light-on-dark colour on a white box, and the placeholder simply disappears.
|
||||
*/
|
||||
object FieldPalette {
|
||||
val fill = Color(0xFFFFFFFF)
|
||||
val ink = Color(0xFF0E1230)
|
||||
val hint = Color(0xFF6B7590)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun fieldColors() = OutlinedTextFieldDefaults.colors(
|
||||
focusedContainerColor = FieldPalette.fill,
|
||||
unfocusedContainerColor = FieldPalette.fill,
|
||||
disabledContainerColor = FieldPalette.fill,
|
||||
errorContainerColor = FieldPalette.fill,
|
||||
focusedTextColor = FieldPalette.ink,
|
||||
unfocusedTextColor = FieldPalette.ink,
|
||||
disabledTextColor = FieldPalette.hint,
|
||||
errorTextColor = FieldPalette.ink,
|
||||
focusedLabelColor = FieldPalette.hint,
|
||||
unfocusedLabelColor = FieldPalette.hint,
|
||||
disabledLabelColor = FieldPalette.hint,
|
||||
focusedPlaceholderColor = FieldPalette.hint,
|
||||
unfocusedPlaceholderColor = FieldPalette.hint,
|
||||
cursorColor = Palette.Accent,
|
||||
focusedBorderColor = Palette.Accent,
|
||||
unfocusedBorderColor = FieldPalette.hint,
|
||||
)
|
||||
|
||||
/** Corner radii, matching `Theme.Radius` on iOS. */
|
||||
object Radius {
|
||||
val small = 10.dp
|
||||
|
||||
@@ -129,6 +129,7 @@ private fun ColumnScope.NoWallet(model: MessengerViewModel) {
|
||||
Text("Already have one?", fontWeight = FontWeight.Medium, fontSize = 16.sp)
|
||||
Spacer(Modifier.height(Space.xs))
|
||||
OutlinedTextField(
|
||||
colors = fieldColors(),
|
||||
value = phrase,
|
||||
onValueChange = { phrase = it },
|
||||
placeholder = { Text("Your 12 or 24 word recovery phrase", fontSize = 13.sp) },
|
||||
|
||||
Reference in New Issue
Block a user