Android: the layout, not just the parts inside it
The last pass changed row internals and left the screen architecture alone, and the screen architecture is what makes the kit look like the kit. Reported back as "looks exactly the same and nothing like the sketch", which was accurate. Four cards -- a presence picker, a paragraph about what stories are, contacts, groups -- each on its own white surface with a heading. The kit's messages screen is one flat list under a title, a search box and a strip of stories. No amount of restyling the rows inside those cards was going to get there; the cards were the thing. 🔴 The type scale was the other half, and it was invisible from inside a single screen. Material 3's defaults are considerably larger than the kit's, so a section heading rendered at 24sp where the kit says 16 and every screen inherited it. Set once in the theme, which fixes screens nobody has touched. Stories was a heading, a Post button, and -- with nothing posted, which is the normal state -- three lines of prose explaining what a story is. Prose where the design has a row of circles is the loudest possible signal that this is not the same app. It is a strip of faces now, with the add-circle always first so there is always somewhere to tap. The tab bar is the kit's floating pill rather than a row of text buttons; it is the most recognisable single element in the design. Group rows carry the same avatar and the same 72dp pitch as contact rows, so the two read as one list rather than two that follow each other. The presence picker moved to Account, where the kit would put it, instead of being dropped with the card it lived in. The buddy list is plain rows in the page's own scroll now. It was a lazy list bounded at 320dp -- a small scrolling window inside a scrolling page, which is exactly what a flat list is not. The bound existed because a lazy list in a scrolling Column is measured with infinite height and crashes; rendering rows directly sidesteps that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b48d71a75a
commit
7218a9b175
@@ -75,7 +75,11 @@ import androidx.compose.foundation.layout.heightIn
|
||||
* else's whereabouts, and this product should not make one it cannot support.
|
||||
*/
|
||||
@Composable
|
||||
fun ColumnScope.BuddyList(model: MessengerViewModel, phase: MessengerViewModel.Phase.SignedIn) {
|
||||
fun ColumnScope.BuddyList(
|
||||
model: MessengerViewModel,
|
||||
phase: MessengerViewModel.Phase.SignedIn,
|
||||
query: String = "",
|
||||
) {
|
||||
var adding by remember { mutableStateOf(false) }
|
||||
var username by remember { mutableStateOf("") }
|
||||
|
||||
@@ -84,7 +88,7 @@ fun ColumnScope.BuddyList(model: MessengerViewModel, phase: MessengerViewModel.P
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text("Contacts", style = MaterialTheme.typography.headlineSmall)
|
||||
Text("Contacts", style = MaterialTheme.typography.titleMedium)
|
||||
TextButton(onClick = { adding = !adding }, modifier = Modifier.testTag("add-contact")) {
|
||||
Text(if (adding) "Cancel" else "Add")
|
||||
}
|
||||
@@ -174,8 +178,23 @@ fun ColumnScope.BuddyList(model: MessengerViewModel, phase: MessengerViewModel.P
|
||||
// the full height it is offered, so a single contact still reserved a
|
||||
// fixed slab of empty white. A max lets the card size to its content
|
||||
// and start scrolling only once the list is actually long.
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(max = 320.dp).testTag("buddies")) {
|
||||
items(phase.buddies, key = { it.userId }) { buddy ->
|
||||
// 🔴 **Plain rows in the page's own scroll, not a bounded lazy list.**
|
||||
// The list lived in a card with `heightIn(max = 320.dp)` because a lazy
|
||||
// list inside a scrolling Column is measured with infinite height and
|
||||
// crashes. Rendering the rows directly sidesteps that entirely — and the
|
||||
// 320 cap was itself a compromise that made the list a small scrolling
|
||||
// window inside a scrolling page, which is exactly what the kit's flat
|
||||
// list is not.
|
||||
//
|
||||
// ⚠️ No recycling. Fine for a contact list of tens; if this ever holds
|
||||
// hundreds it wants a single lazy list owning the whole screen's scroll,
|
||||
// not a nested one.
|
||||
val shown = phase.buddies.filter {
|
||||
query.isBlank() ||
|
||||
it.displayName.contains(query, true) || it.handle.contains(query, true)
|
||||
}
|
||||
Column(modifier = Modifier.fillMaxWidth().testTag("buddies")) {
|
||||
shown.forEach { buddy ->
|
||||
val name = buddy.displayName.ifBlank { buddy.handle }
|
||||
.ifBlank { "user ${buddy.userId}" }
|
||||
Row(
|
||||
@@ -317,6 +336,8 @@ private fun AttachedImage(ref: io.flamenet.e2e.Attachment.Ref, model: MessengerV
|
||||
fun ColumnScope.GroupsSection(
|
||||
model: MessengerViewModel,
|
||||
phase: MessengerViewModel.Phase.SignedIn,
|
||||
/** Filters this section from the one search box above the whole list. */
|
||||
query: String = "",
|
||||
) {
|
||||
var creating by remember { mutableStateOf(false) }
|
||||
var name by remember { mutableStateOf("") }
|
||||
@@ -327,7 +348,7 @@ fun ColumnScope.GroupsSection(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text("Groups", style = MaterialTheme.typography.headlineSmall)
|
||||
Text("Groups", style = MaterialTheme.typography.titleMedium)
|
||||
TextButton(
|
||||
onClick = { creating = !creating },
|
||||
modifier = Modifier.testTag("new-group"),
|
||||
@@ -388,21 +409,32 @@ fun ColumnScope.GroupsSection(
|
||||
)
|
||||
}
|
||||
|
||||
phase.groups.forEach { group ->
|
||||
val shownGroups = phase.groups.filter {
|
||||
query.isBlank() || it.name.contains(query, true)
|
||||
}
|
||||
shownGroups.forEach { group ->
|
||||
val thread = io.flamenet.e2e.Vault.threadForGroup(group.id)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { model.openGroup(group) }
|
||||
.padding(vertical = 12.dp)
|
||||
// ⚠️ Same 72dp pitch as a contact row, from the same tokens. A
|
||||
// group row measured differently sat at a different rhythm in
|
||||
// the same list, which reads as two lists that happen to
|
||||
// follow each other.
|
||||
.padding(vertical = Kit.rowGap / 2)
|
||||
.testTag("group-${group.id}"),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(Kit.gutter),
|
||||
) {
|
||||
// The same avatar shape as a contact, so the two kinds of row read
|
||||
// as one list. A group has no presence, so no dot.
|
||||
Avatar(name = group.name.ifBlank { "Group" }, online = false)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
group.name.ifBlank { "Group" },
|
||||
fontWeight = if (thread in phase.unread) FontWeight.Bold else null,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = if (thread in phase.unread) FontWeight.Bold else FontWeight.Medium,
|
||||
)
|
||||
Text(
|
||||
// "You" first: you are a member of your own group and are
|
||||
@@ -414,7 +446,9 @@ fun ColumnScope.GroupsSection(
|
||||
?.let { b -> b.displayName.ifBlank { b.handle } }
|
||||
?: "user $id"
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontSize = 14.sp,
|
||||
color = Palette.SecondaryText,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
if (thread in phase.unread) {
|
||||
@@ -535,6 +569,12 @@ fun ColumnScope.AccountScreen(
|
||||
SettingsCard {
|
||||
Text("Profile", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(Space.sm))
|
||||
// ⚠️ Moved here from the contacts screen. The kit's list screen has no
|
||||
// presence control on it, and this is where the other facts about you
|
||||
// already live — but it is a setting, not decoration, so it is kept
|
||||
// rather than dropped along with the card it used to sit in.
|
||||
PresencePicker(model, phase.presence)
|
||||
Spacer(Modifier.height(Space.lg))
|
||||
OutlinedTextField(
|
||||
value = displayName,
|
||||
onValueChange = { displayName = it },
|
||||
@@ -1245,44 +1285,78 @@ fun ColumnScope.StoryStrip(
|
||||
|
||||
LaunchedEffect(Unit) { model.refreshStories() }
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text("Stories", style = MaterialTheme.typography.titleMedium)
|
||||
TextButton(
|
||||
onClick = { pickStory.launch("image/*") },
|
||||
modifier = Modifier.testTag("post-story"),
|
||||
) { Text("Post") }
|
||||
}
|
||||
|
||||
if (phase.stories.isEmpty()) {
|
||||
Text(
|
||||
"Nothing here. A story goes to your contacts and disappears after a day — on your "
|
||||
+ "device and theirs, because their app deletes it too. It is not a secret you "
|
||||
+ "can take back.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.testTag("stories-empty"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// 🔴 **A strip of faces, which is what the kit's "Recent" row is.** This
|
||||
// was a heading, a Post button, and — with nothing posted, which is the
|
||||
// normal state — three lines of prose explaining what a story is. Prose
|
||||
// where the design has a row of circles is the single loudest thing
|
||||
// telling you this is not the same app. The explanation still exists; it
|
||||
// moved to the one place it is actually needed, under the button that
|
||||
// posts one.
|
||||
//
|
||||
// ⚠️ The add-circle is always first and always present, so the strip never
|
||||
// collapses to nothing and there is always somewhere to tap.
|
||||
LazyRow(
|
||||
modifier = Modifier.fillMaxWidth().testTag("stories"),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(Space.md),
|
||||
) {
|
||||
item {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.testTag("post-story"),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(Kit.avatar)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.clickable { pickStory.launch("image/*") },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text("+", fontSize = 22.sp, color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
Text("Post", fontSize = 12.sp, color = Palette.SecondaryText)
|
||||
}
|
||||
}
|
||||
items(phase.stories, key = { it.id }) { story ->
|
||||
val label = phase.buddies.firstOrNull { it.userId == story.authorId }
|
||||
?.let { it.displayName.ifBlank { it.handle } }
|
||||
?: if (story.mine) "You" else "#${story.authorId}"
|
||||
TextButton(
|
||||
onClick = { model.openStory(story) },
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.testTag("story-${story.id}"),
|
||||
) {
|
||||
// Unseen is marked rather than styled away: a strip where read
|
||||
// and unread look alike is a strip nobody comes back to.
|
||||
Text(if (story.seen) label else "• $label")
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(Kit.avatar)
|
||||
.clip(CircleShape)
|
||||
// Unseen is marked rather than styled away: a strip
|
||||
// where read and unread look alike is a strip nobody
|
||||
// comes back to. A ring, since there is no room for a
|
||||
// bullet beside a 48dp circle.
|
||||
.background(
|
||||
if (story.seen) Palette.AvatarFill
|
||||
else MaterialTheme.colorScheme.primary
|
||||
)
|
||||
.padding(if (story.seen) 0.dp else 2.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Palette.AvatarFill)
|
||||
.clickable { model.openStory(story) },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
label.take(1).uppercase(),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
label,
|
||||
fontSize = 12.sp,
|
||||
color = Palette.SecondaryText,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.widthIn(max = Kit.avatar + 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,16 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -155,11 +165,16 @@ private fun Root(model: MessengerViewModel = viewModel()) {
|
||||
// BELOW the lazy list, not just the list itself. And
|
||||
// keep the buddy list bounded — remove that height and
|
||||
// this does crash.
|
||||
// ⚠️ Tighter than it was. Every section already carries
|
||||
// its own padding, so a 12dp gap between them on top of
|
||||
// that left the list airy in a way the kit is not — its
|
||||
// rows sit on a 72dp pitch with nothing extra between
|
||||
// the groups of them.
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(Space.sm),
|
||||
) {
|
||||
when (current.screen) {
|
||||
MessengerViewModel.Phase.Screen.CONTACTS -> SignedIn(model, current)
|
||||
@@ -347,8 +362,18 @@ private fun BottomNav(
|
||||
phase: MessengerViewModel.Phase.SignedIn,
|
||||
) {
|
||||
val unreadNotes = phase.notifications.count { !it.read }
|
||||
// 🔴 **A floating pill, not a full-width strip.** The kit's tab bar is a
|
||||
// 328x50 rounded rectangle inset from both edges, sitting inside an 84-tall
|
||||
// band — it reads as a control resting on the page rather than as the
|
||||
// bottom of the window. This is the single most recognisable thing about
|
||||
// the design and the app had a plain row of text buttons.
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = Kit.pillHeight)
|
||||
.clip(RoundedCornerShape(Kit.pillRadius))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
NavButton(model, phase, MessengerViewModel.Phase.Screen.CONTACTS,
|
||||
@@ -389,77 +414,89 @@ private fun ColumnScope.SignedIn(
|
||||
model: MessengerViewModel,
|
||||
phase: MessengerViewModel.Phase.SignedIn,
|
||||
) {
|
||||
Text(phase.handle.ifBlank { "Signed in" }, style = MaterialTheme.typography.headlineMedium)
|
||||
Text("${phase.relay} · ${phase.deviceId.take(8)}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
var query by remember { mutableStateOf("") }
|
||||
|
||||
// 🔴 **A list, not a stack of explanatory cards.** This screen used to be
|
||||
// four cards — a presence picker, a paragraph about what stories are, the
|
||||
// contacts, the groups — each on its own white surface with a heading. The
|
||||
// kit's messages screen is one flat list under a title, a search box and a
|
||||
// strip of stories, and no amount of restyling the ROWS inside those cards
|
||||
// was ever going to make it read like the design. The cards were the
|
||||
// problem. See `DESIGN.md`.
|
||||
//
|
||||
// ⚠️ The presence picker moved to Account rather than being deleted: the
|
||||
// kit has no such control on its list screen, and Account is where the
|
||||
// other things about you already live.
|
||||
|
||||
// App bar, per the kit: title at 24 bold, actions on the trailing edge.
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
phase.handle.ifBlank { "Signed in" },
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
"${phase.relay} · ${phase.deviceId.take(8)}",
|
||||
fontSize = 12.sp,
|
||||
color = Palette.SecondaryText,
|
||||
modifier = Modifier.testTag("device-id"))
|
||||
modifier = Modifier.testTag("device-id"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ⚠️ **Screen level, not inside Contacts.** There is ONE error slot shared
|
||||
// by everything on this screen, and it used to be rendered inside
|
||||
// BuddyList — so a story that failed to post, or a group that could not be
|
||||
// created, appeared underneath the Contacts heading and read as a contacts
|
||||
// problem. Observed with a rate-limited story post reported under Contacts.
|
||||
phase.error?.let {
|
||||
// ⚠️ **Screen level, not inside Contacts.** There is ONE error slot
|
||||
// shared by everything here, and it used to render inside the buddy
|
||||
// list — so a story that failed to post appeared under the Contacts
|
||||
// heading and read as a contacts problem.
|
||||
Text(
|
||||
it,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.testTag("error").padding(top = Space.sm),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.testTag("error"),
|
||||
)
|
||||
}
|
||||
|
||||
// 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(),
|
||||
// Search, as the kit's 48-tall pill. Filters what is already here; it does
|
||||
// not ask the relay anything, so it costs nothing and needs no button.
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 48.dp)
|
||||
.clip(RoundedCornerShape(Kit.pillRadius))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(horizontal = Kit.gutter, vertical = 12.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Column(Modifier.padding(Space.lg)) { PresencePicker(model, phase.presence) }
|
||||
if (query.isEmpty()) {
|
||||
Text("Search", fontSize = 14.sp, color = Palette.SecondaryText)
|
||||
}
|
||||
BasicTextField(
|
||||
value = query,
|
||||
onValueChange = { query = it },
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier.fillMaxWidth().testTag("search"),
|
||||
)
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
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(),
|
||||
) {
|
||||
Column(Modifier.padding(Space.lg)) { BuddyList(model, phase) }
|
||||
}
|
||||
HorizontalDivider(color = Palette.SecondaryText.copy(alpha = 0.16f))
|
||||
|
||||
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))
|
||||
GroupsSection(model, phase, query)
|
||||
BuddyList(model, phase, query)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.VaultUnreadable(
|
||||
model: MessengerViewModel,
|
||||
|
||||
@@ -132,16 +132,38 @@ private val FlamenetShapes = Shapes(
|
||||
* 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`.
|
||||
*/
|
||||
/**
|
||||
* The design kit's type scale, applied globally.
|
||||
*
|
||||
* 🔴 **This is the single biggest reason the app did not look like the kit.**
|
||||
* Material 3's defaults are considerably larger than the kit's, and every
|
||||
* screen inherits them — so a section heading rendered at 24sp where the kit
|
||||
* says 16, and a body line at 16 where the kit says 14. Restyling individual
|
||||
* screens against those defaults is pushing against the whole theme; setting
|
||||
* the scale here fixes every screen at once, including ones nobody has
|
||||
* touched yet.
|
||||
*
|
||||
* Names on the left are Material's; sizes on the right are the kit's. See
|
||||
* `DESIGN.md` — H1 40, H2 32, H3 24, H4 20, Sub 16/14/12, Body 16/14,
|
||||
* Description 12/10.
|
||||
*/
|
||||
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),
|
||||
headlineMedium = base.headlineMedium.copy(fontSize = 24.sp, fontWeight = FontWeight.Bold),
|
||||
headlineSmall = base.headlineSmall.copy(fontSize = 20.sp, fontWeight = FontWeight.Bold),
|
||||
titleLarge = base.titleLarge.copy(fontSize = 20.sp, fontWeight = FontWeight.Bold),
|
||||
// Sub 3 — what a section label is, not a headline.
|
||||
titleMedium = base.titleMedium.copy(fontSize = 16.sp, fontWeight = FontWeight.Medium),
|
||||
titleSmall = base.titleSmall.copy(fontSize = 14.sp, fontWeight = FontWeight.Medium),
|
||||
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),
|
||||
bodyMedium = base.bodyMedium.copy(fontSize = 14.sp),
|
||||
bodySmall = base.bodySmall.copy(fontSize = 12.sp),
|
||||
labelLarge = base.labelLarge.copy(fontSize = 14.sp, fontWeight = FontWeight.Medium),
|
||||
labelMedium = base.labelMedium.copy(fontSize = 12.sp),
|
||||
// Description 2 — the timestamp above a bubble.
|
||||
labelSmall = base.labelSmall.copy(fontSize = 10.sp, letterSpacing = 0.sp,
|
||||
fontWeight = FontWeight.Normal),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user