diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/MainActivity.kt b/app/src/main/kotlin/io/flamenet/messenger/app/MainActivity.kt index d56bb03..07deed0 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/MainActivity.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/MainActivity.kt @@ -35,6 +35,10 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.heightIn class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -307,16 +311,63 @@ private fun ColumnScope.SignedIn( Text(phase.handle.ifBlank { "Signed in" }, style = MaterialTheme.typography.headlineMedium) Text("${phase.relay} · ${phase.deviceId.take(8)}", style = MaterialTheme.typography.bodySmall, + color = Palette.SecondaryText, modifier = Modifier.testTag("device-id")) // Above everything: an incoming call is the one thing that must be visible // whatever screen somebody is on. CallBanner(model, phase) - PresencePicker(model, phase.presence) - StoryStrip(model, phase) - BuddyList(model, phase) - GroupsSection(model, phase) + // Each section on its own card. ⚠️ The sections themselves are unchanged — + // same composables, same testTags, same order — because this is a skin over + // screens that work, and moving them would mean re-pointing the UI tests + // for a visual change. + Spacer(Modifier.height(Space.lg)) + Surface( + shape = RoundedCornerShape(Radius.medium), + color = MaterialTheme.colorScheme.surface, + shadowElevation = 2.dp, + modifier = Modifier.fillMaxWidth(), + ) { + Column(Modifier.padding(Space.lg)) { PresencePicker(model, phase.presence) } + } + + Spacer(Modifier.height(Space.md)) + Surface( + shape = RoundedCornerShape(Radius.medium), + color = MaterialTheme.colorScheme.surface, + shadowElevation = 2.dp, + modifier = Modifier.fillMaxWidth(), + ) { + Column(Modifier.padding(Space.lg)) { StoryStrip(model, phase) } + } + + Spacer(Modifier.height(Space.md)) + // ⚠️ **Bounded on purpose.** BuddyList's LazyColumn carries `weight(1f)`, + // which used to resolve against the whole signed-in Column. Inside a card + // with no height of its own it resolves against infinity instead, so the + // contacts card grew to fill the screen and pushed Groups and the tab bar + // off the bottom entirely. A max height keeps the list scrollable inside + // its own card and leaves the rest of the screen reachable. + Surface( + shape = RoundedCornerShape(Radius.medium), + color = MaterialTheme.colorScheme.surface, + shadowElevation = 2.dp, + modifier = Modifier.fillMaxWidth().heightIn(min = 96.dp, max = 340.dp), + ) { + Column(Modifier.padding(Space.lg)) { BuddyList(model, phase) } + } + + Spacer(Modifier.height(Space.md)) + Surface( + shape = RoundedCornerShape(Radius.medium), + color = MaterialTheme.colorScheme.surface, + shadowElevation = 2.dp, + modifier = Modifier.fillMaxWidth(), + ) { + Column(Modifier.padding(Space.lg)) { GroupsSection(model, phase) } + } + Spacer(Modifier.height(Space.lg)) } @Composable diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/Theme.kt b/app/src/main/kotlin/io/flamenet/messenger/app/Theme.kt index 963f267..36eaa06 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/Theme.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/Theme.kt @@ -1,20 +1,120 @@ package io.flamenet.messenger.app import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.Typography import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme 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.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp /** - * The Flamenet palette, matched to the iOS client's `Theme.swift` so the two - * apps look like the same product rather than two takes on it. + * The Flamenet design system. + * + * 🔴 **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 + * two were already matched — while iOS was indigo and this was ember, so the + * claim was simply false and nobody noticed because nothing checks it. + * **Change a value here and change it there in the same commit.** + * + * The look: light parchment ground, white cards with a soft shadow and a + * generous corner radius, one confident accent, quiet neutrals, and space + * between things. Screen structure and every `testTag` are deliberately + * untouched — this is a skin over working screens. */ -private val Ember = Color(0xFFE2571F) -private val EmberDim = Color(0xFFB33F12) -private val Ash = Color(0xFF16130F) -private val Parchment = Color(0xFFFBF7F2) +object Palette { + // Brand + val Ember = Color(0xFFE2571F) + val EmberDeep = Color(0xFFB33F12) + val EmberSoft = Color(0x1AE2571F) + + // Neutrals + val Ink = Color(0xFF16130F) + val Parchment = Color(0xFFFBF7F2) + val SurfaceAlt = Color(0xFFF4EFE9) + val IncomingBubble = Color(0xFFF1ECE5) + val AvatarFill = Color(0xFFF1E4DA) + val SecondaryText = Color(0xFF6B625A) + val Divider = Color(0x1416130F) + + // Semantic + val Success = Color(0xFF2E7D5B) + val Warning = Color(0xFFB8860B) + val Danger = Color(0xFFC0392B) + + // Dark-mode grounds + val InkSurface = Color(0xFF1F1B16) +} + +/** Corner radii, matching `Theme.Radius` on iOS. */ +object Radius { + val small = 10.dp + val medium = 16.dp + val large = 22.dp + val pill = 999.dp +} + +/** The 4dp spacing scale, matching `Theme.Space` on iOS. */ +object Space { + val xs = 4.dp + val sm = 8.dp + val md = 12.dp + val lg = 16.dp + val xl = 24.dp + val xxl = 32.dp +} + +private val FlamenetShapes = Shapes( + extraSmall = RoundedCornerShape(Radius.small), + small = RoundedCornerShape(Radius.small), + medium = RoundedCornerShape(Radius.medium), + large = RoundedCornerShape(Radius.large), + extraLarge = RoundedCornerShape(Radius.large), +) + +/** + * The type scale. + * + * ⚠️ Sizes only — no `fontFamily`. Bundling a font would need the licence to + * cover redistribution, and the platform default already reads well at these + * sizes. If a face is chosen later it goes here and in the iOS `Theme.Font`. + */ +private val FlamenetTypography = Typography().let { base -> + base.copy( + headlineLarge = base.headlineLarge.copy(fontSize = 32.sp, fontWeight = FontWeight.Bold), + headlineMedium = base.headlineMedium.copy(fontSize = 26.sp, fontWeight = FontWeight.Bold), + titleLarge = base.titleLarge.copy(fontSize = 22.sp, fontWeight = FontWeight.SemiBold), + titleMedium = base.titleMedium.copy(fontSize = 17.sp, fontWeight = FontWeight.SemiBold), + bodyLarge = base.bodyLarge.copy(fontSize = 16.sp), + bodyMedium = base.bodyMedium.copy(fontSize = 15.sp), + labelLarge = base.labelLarge.copy(fontSize = 15.sp, fontWeight = FontWeight.SemiBold), + labelSmall = base.labelSmall.copy(letterSpacing = 0.6.sp, fontWeight = FontWeight.SemiBold), + ) +} + +/** + * The card: raised surface, generous radius, one soft shadow. + * + * A modifier rather than a component so it can wrap the existing sections + * without restructuring them. The look depends on every card sharing one + * depth — resist giving any single card its own elevation. + */ +fun Modifier.flamenetCard( + padding: PaddingValues = PaddingValues(Space.lg), +): Modifier = this + .shadow(3.dp, RoundedCornerShape(Radius.medium), clip = false) + .clip(RoundedCornerShape(Radius.medium)) + .padding(padding) @Composable fun MessengerTheme(content: @Composable () -> Unit) { @@ -22,23 +122,35 @@ fun MessengerTheme(content: @Composable () -> Unit) { MaterialTheme( colorScheme = if (dark) { darkColorScheme( - primary = Ember, + primary = Palette.Ember, onPrimary = Color.White, - background = Ash, - surface = Color(0xFF1F1B16), - onBackground = Parchment, - onSurface = Parchment, + secondary = Palette.EmberDeep, + background = Palette.Ink, + surface = Palette.InkSurface, + surfaceVariant = Palette.InkSurface, + onBackground = Palette.Parchment, + onSurface = Palette.Parchment, + onSurfaceVariant = Palette.SecondaryText, + error = Palette.Danger, + outline = Palette.Divider, ) } else { lightColorScheme( - primary = EmberDim, + primary = Palette.EmberDeep, onPrimary = Color.White, - background = Parchment, + secondary = Palette.Ember, + background = Palette.Parchment, surface = Color.White, - onBackground = Ash, - onSurface = Ash, + surfaceVariant = Palette.SurfaceAlt, + onBackground = Palette.Ink, + onSurface = Palette.Ink, + onSurfaceVariant = Palette.SecondaryText, + error = Palette.Danger, + outline = Palette.Divider, ) }, + shapes = FlamenetShapes, + typography = FlamenetTypography, content = content, ) }