From 745dc987394a87f40a8d95d927bc3daab0883b27 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 8 Aug 2026 10:16:10 -0300 Subject: [PATCH 01/13] feat: add dark mode and a full M3 colour ramp on Android MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The theme filled in only 10 of ~30 M3 colour roles, so anything resolving error, tertiary, scrim, inverseSurface or surfaceContainer* fell back to baseline Material purple. There was also no dark scheme at all. Replace the flat `object WornColors` façade with complete light and dark ColorSchemes and drive all ~290 call sites off MaterialTheme directly, so the M3 role in play is visible at the point of use instead of hidden behind an alias. The handful of brand tokens M3 has no role for (gradient stops, muted text/icon greys, category dots) hang off MaterialTheme as `wornExtras`. Light keeps every existing hue unchanged. Dark uses a warm #211D18 base rather than a neutral near-black, which would read cold against the sage, with the ramp spaced widely enough that page, card and bar actually separate. Also corrects outline/outlineVariant, which were assigned the wrong way round — M3 defines outline as the stronger of the pair. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/main/kotlin/com/github/worn/App.kt | 4 +- .../worn/ui/components/AddItemFields.kt | 80 ++++--- .../worn/ui/components/AiLockedSheet.kt | 19 +- .../worn/ui/components/CategoryFilterChips.kt | 3 +- .../github/worn/ui/components/ClothingCard.kt | 24 +- .../worn/ui/components/ClothingPhoto.kt | 5 +- .../worn/ui/components/CropEditorDialog.kt | 19 +- .../worn/ui/components/CropPhotoButton.kt | 6 +- .../ui/components/DeleteConfirmationDialog.kt | 6 +- .../worn/ui/components/EmptyStateView.kt | 10 +- .../worn/ui/components/ErrorContentView.kt | 14 +- .../github/worn/ui/components/OutfitCard.kt | 32 +-- .../github/worn/ui/components/PropertyRow.kt | 11 +- .../worn/ui/components/SelectionHeader.kt | 8 +- .../worn/ui/components/SelectionIndicator.kt | 6 +- .../worn/ui/components/SheetDragHandle.kt | 9 +- .../worn/ui/components/WornBottomBar.kt | 18 +- .../com/github/worn/ui/components/WornChip.kt | 8 +- .../worn/ui/components/WornGradientButton.kt | 31 ++- .../com/github/worn/ui/screen/AddItemSheet.kt | 10 +- .../worn/ui/screen/CreateOutfitSheet.kt | 32 +-- .../com/github/worn/ui/screen/GapsScreen.kt | 84 +++---- .../github/worn/ui/screen/ItemDetailSheet.kt | 42 ++-- .../worn/ui/screen/OutfitDetailSheet.kt | 28 +-- .../github/worn/ui/screen/OutfitsScreen.kt | 28 ++- .../github/worn/ui/screen/SettingsScreen.kt | 139 ++++++----- .../com/github/worn/ui/screen/TryItScreen.kt | 91 ++++---- .../github/worn/ui/screen/WardrobeScreen.kt | 40 ++-- .../com/github/worn/ui/theme/SheetPreview.kt | 8 +- .../github/worn/ui/theme/WornColorScheme.kt | 216 ++++++++++++++++++ .../com/github/worn/ui/theme/WornTheme.kt | 75 ++---- .../com/github/worn/ui/util/CameraCapture.kt | 2 +- .../worn/ui/util/RememberDecodedImage.kt | 2 +- 33 files changed, 682 insertions(+), 428 deletions(-) create mode 100644 composeApp/src/main/kotlin/com/github/worn/ui/theme/WornColorScheme.kt diff --git a/composeApp/src/main/kotlin/com/github/worn/App.kt b/composeApp/src/main/kotlin/com/github/worn/App.kt index 8e742a1..40c3a9e 100644 --- a/composeApp/src/main/kotlin/com/github/worn/App.kt +++ b/composeApp/src/main/kotlin/com/github/worn/App.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -24,7 +25,6 @@ import com.github.worn.ui.screen.OutfitsScreen import com.github.worn.ui.screen.SettingsScreen import com.github.worn.ui.screen.TryItScreen import com.github.worn.ui.screen.WardrobeScreen -import com.github.worn.ui.theme.WornColors import com.github.worn.ui.theme.WornTheme import com.github.worn.ui.util.SharedPhoto import com.github.worn.ui.util.ShortcutCommand @@ -75,7 +75,7 @@ fun App( Box( modifier = Modifier .fillMaxSize() - .background(WornColors.BgPage) + .background(MaterialTheme.colorScheme.surface) .semantics { testTagsAsResourceId = true }, ) { HorizontalPager( diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt index 0b26e16..07feff8 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt @@ -2,6 +2,7 @@ package com.github.worn.ui.components +import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically @@ -23,7 +24,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.annotation.DrawableRes import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.outlined.CameraAlt @@ -34,6 +34,7 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.SwitchDefaults @@ -65,7 +66,7 @@ import com.github.worn.domain.model.Material import com.github.worn.domain.model.Season import com.github.worn.domain.model.Subcategory import com.github.worn.domain.model.subcategoriesFor -import com.github.worn.ui.theme.WornColors +import com.github.worn.ui.theme.wornExtras val addItemColorPalette = listOf( "White" to Color(0xFFFFFFFF), @@ -95,7 +96,7 @@ fun PhotoUploadZone( onClick = onClick, shape = RoundedCornerShape(16.dp), color = Color.Transparent, - border = BorderStroke(1.5.dp, WornColors.BorderStrong), + border = BorderStroke(1.5.dp, MaterialTheme.colorScheme.outline), modifier = modifier.fillMaxWidth().height(140.dp), ) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { @@ -115,13 +116,13 @@ fun PhotoUploadZone( Icon( imageVector = Icons.Outlined.CameraAlt, contentDescription = null, - tint = WornColors.IconMuted, + tint = MaterialTheme.wornExtras.iconMuted, modifier = Modifier.size(32.dp), ) Spacer(Modifier.height(8.dp)) Text( text = stringResource(R.string.add_item_photo_hint), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, fontWeight = FontWeight.Medium, ) @@ -133,9 +134,12 @@ fun PhotoUploadZone( modifier = Modifier .fillMaxSize() .clip(RoundedCornerShape(16.dp)) - .background(WornColors.BgElevated.copy(alpha = 0.6f)), + .background(MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.6f)), ) { - CircularProgressIndicator(color = WornColors.AccentGreen, modifier = Modifier.size(28.dp)) + CircularProgressIndicator( + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(28.dp), + ) } } } @@ -155,7 +159,7 @@ fun RemoveBackgroundToggle( ) { Text( text = stringResource(R.string.add_item_remove_background), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 15.sp, fontWeight = FontWeight.Medium, modifier = Modifier.weight(1f), @@ -166,7 +170,7 @@ fun RemoveBackgroundToggle( enabled = enabled, colors = SwitchDefaults.colors( checkedThumbColor = Color.White, - checkedTrackColor = WornColors.AccentGreen, + checkedTrackColor = MaterialTheme.colorScheme.primary, ), ) } @@ -177,7 +181,7 @@ fun AiBadge(onClick: () -> Unit = {}, modifier: Modifier = Modifier) { Surface( onClick = onClick, shape = RoundedCornerShape(28.dp), - color = WornColors.AccentIndigo, + color = MaterialTheme.colorScheme.secondary, modifier = modifier, ) { Row(modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)) { @@ -200,18 +204,20 @@ fun ItemNameField(value: String, onValueChange: (String) -> Unit, modifier: Modi placeholder = { Text( stringResource(R.string.add_item_name_hint), - color = WornColors.IconMuted, + color = MaterialTheme.wornExtras.iconMuted, fontSize = 15.sp, ) }, colors = TextFieldDefaults.colors( - focusedContainerColor = WornColors.BgCard, - unfocusedContainerColor = WornColors.BgCard, + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, ), shape = RoundedCornerShape(12.dp), - modifier = modifier.fillMaxWidth().border(1.dp, WornColors.BorderSubtle, RoundedCornerShape(12.dp)), + modifier = modifier + .fillMaxWidth() + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(12.dp)), ) } @@ -220,8 +226,8 @@ fun CategoryDropdown(selected: Category?, onSelected: (Category) -> Unit, modifi var expanded by remember { mutableStateOf(false) } Surface( shape = RoundedCornerShape(12.dp), - color = WornColors.BgCard, - border = BorderStroke(1.dp, WornColors.BorderSubtle), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = modifier.fillMaxWidth(), ) { Column { @@ -237,14 +243,18 @@ fun CategoryDropdown(selected: Category?, onSelected: (Category) -> Unit, modifi Icon( painter = painterResource(selected.iconRes()), contentDescription = null, - tint = WornColors.TextSecondary, + tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(20.dp), ) Spacer(Modifier.size(12.dp)) } Text( text = selected?.displayName() ?: stringResource(R.string.label_category), - color = if (selected != null) WornColors.TextPrimary else WornColors.IconMuted, + color = if (selected != null) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.wornExtras.iconMuted + }, fontSize = 15.sp, modifier = Modifier.weight(1f), ) @@ -252,7 +262,7 @@ fun CategoryDropdown(selected: Category?, onSelected: (Category) -> Unit, modifi imageVector = if (expanded) Icons.Outlined.KeyboardArrowUp else Icons.Outlined.KeyboardArrowDown, contentDescription = null, - tint = WornColors.IconMuted, + tint = MaterialTheme.wornExtras.iconMuted, modifier = Modifier.size(18.dp), ) } @@ -274,7 +284,7 @@ fun CategoryDropdown(selected: Category?, onSelected: (Category) -> Unit, modifi @Composable private fun CategoryOptionList(onSelected: (Category) -> Unit) { Column { - HorizontalDivider(color = WornColors.BorderSubtle) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) Category.entries.forEach { category -> Surface( onClick = { onSelected(category) }, @@ -287,20 +297,20 @@ private fun CategoryOptionList(onSelected: (Category) -> Unit) { Icon( painter = painterResource(category.iconRes()), contentDescription = null, - tint = WornColors.TextSecondary, + tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(20.dp), ) Spacer(Modifier.size(12.dp)) Text( text = category.displayName(), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, fontWeight = FontWeight.Medium, ) } } if (category != Category.entries.last()) { - HorizontalDivider(color = WornColors.BorderSubtle.copy(alpha = 0.5f)) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) } } } @@ -312,7 +322,7 @@ fun ColorSection(selectedColors: Set, onToggle: (String) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Text( stringResource(R.string.label_color), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, ) @@ -326,7 +336,7 @@ fun ColorSection(selectedColors: Set, onToggle: (String) -> Unit) { onClick = { onToggle(name) }, shape = CircleShape, color = color, - border = if (isSelected) BorderStroke(2.dp, WornColors.AccentGreen) else null, + border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null, modifier = Modifier.size(28.dp), ) { if (isSelected) { @@ -355,7 +365,7 @@ fun SeasonSection(selectedSeasons: Set, onToggle: (Season) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Text( stringResource(R.string.label_season), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, ) @@ -415,8 +425,8 @@ fun SubcategoryDropdown(category: Category, selected: Subcategory?, onSelected: var expanded by remember { mutableStateOf(false) } Surface( shape = RoundedCornerShape(12.dp), - color = WornColors.BgCard, - border = BorderStroke(1.dp, WornColors.BorderSubtle), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = Modifier.fillMaxWidth(), ) { Column { @@ -452,7 +462,7 @@ private fun DropdownHeader(text: String, hasSelection: Boolean, expanded: Boolea ) { Text( text = text, - color = if (hasSelection) WornColors.TextPrimary else WornColors.IconMuted, + color = if (hasSelection) MaterialTheme.colorScheme.onSurface else MaterialTheme.wornExtras.iconMuted, fontSize = 15.sp, modifier = Modifier.weight(1f), ) @@ -460,7 +470,7 @@ private fun DropdownHeader(text: String, hasSelection: Boolean, expanded: Boolea imageVector = if (expanded) Icons.Outlined.KeyboardArrowUp else Icons.Outlined.KeyboardArrowDown, contentDescription = null, - tint = WornColors.IconMuted, + tint = MaterialTheme.wornExtras.iconMuted, modifier = Modifier.size(18.dp), ) } @@ -469,12 +479,12 @@ private fun DropdownHeader(text: String, hasSelection: Boolean, expanded: Boolea @Composable private fun SubcategoryOptionList(options: List, onSelected: (Subcategory) -> Unit) { Column { - HorizontalDivider(color = WornColors.BorderSubtle) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) options.forEach { subcategory -> Surface(onClick = { onSelected(subcategory) }, color = Color.Transparent) { Text( text = subcategory.displayName(), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, fontWeight = FontWeight.Medium, modifier = Modifier @@ -483,7 +493,7 @@ private fun SubcategoryOptionList(options: List, onSelected: (Subca ) } if (subcategory != options.last()) { - HorizontalDivider(color = WornColors.BorderSubtle.copy(alpha = 0.5f)) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) } } } @@ -495,7 +505,7 @@ fun FitSection(selected: Fit?, onSelected: (Fit?) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Text( stringResource(R.string.label_fit), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, ) @@ -517,7 +527,7 @@ fun MaterialSection(selected: Material?, onSelected: (Material?) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Text( stringResource(R.string.label_material), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, ) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/AiLockedSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/AiLockedSheet.kt index 97c51b4..8ec2fe0 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/AiLockedSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/AiLockedSheet.kt @@ -1,11 +1,10 @@ package com.github.worn.ui.components -import androidx.compose.foundation.background import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -19,6 +18,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -28,19 +28,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.SheetPreview -import com.github.worn.ui.theme.WornColors - -private val IndigoAccent = WornColors.AccentIndigo @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -50,7 +47,7 @@ fun AiLockedSheet(onDismiss: () -> Unit, onGoToSettings: () -> Unit = {}) { ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - containerColor = WornColors.BgElevated, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), dragHandle = { SheetDragHandle() }, ) { @@ -76,7 +73,7 @@ internal fun AiLockedContent( contentAlignment = Alignment.Center, modifier = Modifier .size(44.dp) - .background(IndigoAccent, RoundedCornerShape(12.dp)), + .background(MaterialTheme.colorScheme.secondary, RoundedCornerShape(12.dp)), ) { Icon( Icons.Outlined.SmartToy, @@ -88,14 +85,14 @@ internal fun AiLockedContent( Text( text = stringResource(R.string.ai_locked_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 22.sp, fontWeight = FontWeight.Medium, ) Text( text = stringResource(R.string.ai_locked_description), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, textAlign = TextAlign.Center, lineHeight = 21.sp, diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/CategoryFilterChips.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/CategoryFilterChips.kt index c2cc6cf..8d6cf14 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/CategoryFilterChips.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/CategoryFilterChips.kt @@ -12,13 +12,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R import com.github.worn.domain.model.Category -import com.github.worn.ui.theme.WornColors @Composable fun CategoryFilterChips( diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt index f238329..8dd0a73 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt @@ -18,9 +18,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -32,7 +34,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.domain.model.Category import com.github.worn.domain.model.ClothingItem -import com.github.worn.ui.theme.WornColors +import com.github.worn.ui.theme.wornExtras private val photoShape = RoundedCornerShape(16.dp) @@ -74,8 +76,8 @@ private fun PhotoArea( Box(modifier = Modifier.fillMaxWidth().height(height)) { Surface( shape = photoShape, - color = WornColors.BgCard, - border = BorderStroke(1.dp, WornColors.BorderSubtle), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 4.dp, modifier = Modifier.fillMaxSize(), ) { @@ -105,7 +107,7 @@ private fun ItemInfo(item: ClothingItem) { ) { Text( text = item.name, - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, fontWeight = FontWeight.Medium, // AI-generated names can run long; left unbounded they push the category row down and @@ -125,17 +127,19 @@ private fun ItemInfo(item: ClothingItem) { ) Text( text = item.category.displayLabel(), - color = WornColors.TextMuted, + color = MaterialTheme.wornExtras.textMuted, fontSize = 12.sp, ) } } } +@Composable +@ReadOnlyComposable internal fun Category.dotColor(): Color = when (this) { - Category.TOP -> WornColors.CategoryDotTop - Category.BOTTOM -> WornColors.CategoryDotBottom - Category.OUTERWEAR -> WornColors.CategoryDotOuterwear - Category.SHOES -> WornColors.CategoryDotShoes - Category.ACCESSORY -> WornColors.CategoryDotAccessory + Category.TOP -> MaterialTheme.wornExtras.categoryDotTop + Category.BOTTOM -> MaterialTheme.wornExtras.categoryDotBottom + Category.OUTERWEAR -> MaterialTheme.wornExtras.categoryDotOuterwear + Category.SHOES -> MaterialTheme.wornExtras.categoryDotShoes + Category.ACCESSORY -> MaterialTheme.wornExtras.categoryDotAccessory } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingPhoto.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingPhoto.kt index 82d5e8d..2b79d69 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingPhoto.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingPhoto.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Checkroom import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -16,7 +17,7 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.Dp import coil3.compose.AsyncImage -import com.github.worn.ui.theme.WornColors +import com.github.worn.ui.theme.wornExtras import java.io.File /** @@ -34,7 +35,7 @@ fun ClothingPhoto( shape: Shape, placeholderIconSize: Dp, modifier: Modifier = Modifier, - placeholderTint: Color = WornColors.IconMuted, + placeholderTint: Color = MaterialTheme.wornExtras.iconMuted, ) { val photoFile = remember(photoPath) { photoPath.takeIf { it.isNotEmpty() }?.let(::File) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/CropEditorDialog.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/CropEditorDialog.kt index 2f28166..10eadc6 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/CropEditorDialog.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/CropEditorDialog.kt @@ -2,6 +2,7 @@ package com.github.worn.ui.components import android.graphics.Bitmap import android.widget.Toast +import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectDragGestures @@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -28,14 +30,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.ClipOp import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.clipRect -import androidx.compose.ui.graphics.ClipOp +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext @@ -48,12 +51,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.compose.foundation.Canvas -import androidx.compose.ui.input.pointer.pointerInput import com.github.worn.R import com.github.worn.ui.exposeTestTagsAsResourceId -import com.github.worn.ui.theme.WornColors import com.github.worn.ui.theme.WornTheme +import com.github.worn.ui.theme.wornExtras import com.github.worn.ui.util.cropToJpeg import com.github.worn.ui.util.decodeForEditing import com.github.worn.util.image.CropCorner @@ -203,7 +204,7 @@ private fun CropCanvas( Canvas(modifier = Modifier.fillMaxSize()) { drawCropOverlay(current) } } if (isProcessing) { - CircularProgressIndicator(color = WornColors.AccentGreen, modifier = Modifier.size(36.dp)) + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary, modifier = Modifier.size(36.dp)) } } } @@ -283,7 +284,7 @@ private fun CropEditorTopBar(canApply: Boolean, onCancel: () -> Unit, onApply: ( ) { Text( text = stringResource(R.string.crop_apply), - color = if (canApply) WornColors.AccentGreen else WornColors.TextMuted, + color = if (canApply) MaterialTheme.colorScheme.primary else MaterialTheme.wornExtras.textMuted, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, ) @@ -367,9 +368,11 @@ private const val GUIDE_WIDTH_PX = 1.5f private const val HANDLE_WIDTH_PX = 8f private const val HANDLE_ARM_PX = 56f +// Preview-only stand-in for a real photo; the exact fill is irrelevant, so it stays a literal +// rather than a theme lookup that would force this helper to become @Composable. private fun previewBitmap(): ImageBitmap = Bitmap.createBitmap(PREVIEW_WIDTH, PREVIEW_HEIGHT, Bitmap.Config.ARGB_8888) - .apply { eraseColor(WornColors.AccentGreen.toArgb()) } + .apply { eraseColor(android.graphics.Color.rgb(0x7A, 0x94, 0x68)) } .asImageBitmap() private const val PREVIEW_WIDTH = 600 diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/CropPhotoButton.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/CropPhotoButton.kt index c37eca8..799345e 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/CropPhotoButton.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/CropPhotoButton.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Crop import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -18,7 +19,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R -import com.github.worn.ui.theme.WornColors /** * Opens the crop editor for the photo shown above it. Rendered under a photo preview zone rather @@ -35,13 +35,13 @@ fun CropPhotoButton( Icon( imageVector = Icons.Outlined.Crop, contentDescription = null, - tint = WornColors.TextSecondary, + tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(18.dp), ) Spacer(Modifier.width(8.dp)) Text( text = stringResource(R.string.crop_photo_button), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 15.sp, fontWeight = FontWeight.Medium, ) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/DeleteConfirmationDialog.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/DeleteConfirmationDialog.kt index 403745a..be3c14a 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/DeleteConfirmationDialog.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/DeleteConfirmationDialog.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -15,7 +16,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R -import com.github.worn.ui.theme.WornColors import com.github.worn.ui.theme.WornTheme @Composable @@ -38,7 +38,7 @@ fun DeleteConfirmationDialog( text = { Text( message, - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 15.sp, lineHeight = 22.sp, ) @@ -47,7 +47,7 @@ fun DeleteConfirmationDialog( Button( onClick = onConfirm, enabled = !isDeleting, - colors = ButtonDefaults.buttonColors(containerColor = WornColors.DeleteRed), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), shape = RoundedCornerShape(24.dp), modifier = Modifier.testTag("delete_dialog_confirm"), ) { diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/EmptyStateView.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/EmptyStateView.kt index d162d83..6be6dcf 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/EmptyStateView.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/EmptyStateView.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -20,7 +21,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.github.worn.ui.theme.WornColors import com.github.worn.ui.theme.WornTheme @Composable @@ -39,8 +39,8 @@ fun EmptyStateView( Box( modifier = Modifier.size(130.dp) .shadow(15.dp, CircleShape) - .background(WornColors.BgCard, CircleShape) - .border(1.dp, WornColors.BorderSubtle, CircleShape), + .background(MaterialTheme.colorScheme.surfaceContainerLowest, CircleShape) + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape), contentAlignment = Alignment.Center, ) { icon() @@ -48,7 +48,7 @@ fun EmptyStateView( Spacer(Modifier.height(24.dp)) Text( title, - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.5).sp, @@ -56,7 +56,7 @@ fun EmptyStateView( Spacer(Modifier.height(24.dp)) Text( description, - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 15.sp, lineHeight = 22.sp, textAlign = TextAlign.Center, diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt index bac8661..0a41ac8 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -27,7 +28,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R -import com.github.worn.ui.theme.WornColors import com.github.worn.ui.theme.WornTheme @Composable @@ -35,7 +35,7 @@ fun ErrorContentView( message: String, onRetry: () -> Unit, modifier: Modifier = Modifier, - retryButtonColor: Color = WornColors.AccentGreen, + retryButtonColor: Color = MaterialTheme.colorScheme.primary, ) { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -46,19 +46,19 @@ fun ErrorContentView( modifier = Modifier .size(72.dp) .clip(CircleShape) - .background(WornColors.DeleteRed.copy(alpha = 0.1f)), + .background(MaterialTheme.colorScheme.error.copy(alpha = 0.1f)), ) { Icon( imageVector = Icons.Outlined.ErrorOutline, contentDescription = null, - tint = WornColors.DeleteRed, + tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(32.dp), ) } Spacer(Modifier.height(24.dp)) Text( text = message, - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, lineHeight = 20.sp, textAlign = TextAlign.Center, @@ -67,7 +67,7 @@ fun ErrorContentView( Surface( onClick = onRetry, shape = RoundedCornerShape(16.dp), - color = WornColors.BgCard, + color = MaterialTheme.colorScheme.surfaceContainerLowest, ) { Text( text = stringResource(R.string.common_retry), @@ -100,7 +100,7 @@ private fun ErrorContentViewTabletPreview() { message = "Something went wrong. Please try again.", onRetry = {}, modifier = Modifier.padding(vertical = 60.dp), - retryButtonColor = WornColors.AccentIndigo, + retryButtonColor = MaterialTheme.colorScheme.secondary, ) } } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt index d3a5a7e..a8f60b2 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt @@ -18,9 +18,11 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight import androidx.compose.material.icons.filled.Check import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -33,7 +35,7 @@ import androidx.compose.ui.unit.sp import com.github.worn.R import com.github.worn.domain.model.Category import com.github.worn.domain.model.Outfit -import com.github.worn.ui.theme.WornColors +import com.github.worn.ui.theme.wornExtras import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -43,11 +45,13 @@ private val cardShape = RoundedCornerShape(20.dp) private val thumbnailShape = RoundedCornerShape(10.dp) private val badgeShape = RoundedCornerShape(8.dp) -private val badgeColors = listOf( - WornColors.AccentIndigo, - WornColors.AccentCoral, - WornColors.AccentGreen, -) +private val badgeColors: List + @Composable @ReadOnlyComposable + get() = listOf( + MaterialTheme.colorScheme.secondary, + MaterialTheme.colorScheme.tertiary, + MaterialTheme.colorScheme.primary, + ) @OptIn(ExperimentalFoundationApi::class) @Composable @@ -62,10 +66,10 @@ fun OutfitCard( ) { Surface( shape = cardShape, - color = WornColors.BgCard, + color = MaterialTheme.colorScheme.surfaceContainerLowest, border = BorderStroke( 1.dp, - if (isSelected) WornColors.AccentGreen else WornColors.BorderSubtle, + if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant, ), shadowElevation = 4.dp, modifier = modifier @@ -112,14 +116,14 @@ private fun ItemThumbnailRow( private fun ItemThumbnail(category: Category?) { Surface( shape = thumbnailShape, - color = WornColors.BgElevated, + color = MaterialTheme.colorScheme.surfaceContainerHigh, modifier = Modifier.size(40.dp), ) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { Icon( painter = painterResource(id = (category ?: Category.TOP).iconRes()), contentDescription = null, - tint = WornColors.IconMuted, + tint = MaterialTheme.wornExtras.iconMuted, modifier = Modifier.size(20.dp), ) } @@ -132,7 +136,7 @@ private fun ItemCountBadge(outfit: Outfit) { Surface(shape = badgeShape, color = badgeColor) { Text( text = stringResource(R.string.outfit_detail_items_count, outfit.itemIds.size), - color = WornColors.TextOnColor, + color = MaterialTheme.colorScheme.onPrimary, fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), @@ -150,7 +154,7 @@ private fun BottomRow(outfit: Outfit) { Column(verticalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.weight(1f)) { Text( text = outfit.name, - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, // Auto-generated names concatenate every item, so they can outgrow the card. @@ -159,14 +163,14 @@ private fun BottomRow(outfit: Outfit) { ) Text( text = formatDate(outfit.createdAt), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, ) } Icon( imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, contentDescription = null, - tint = WornColors.IconMuted, + tint = MaterialTheme.wornExtras.iconMuted, modifier = Modifier.size(20.dp), ) } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/PropertyRow.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/PropertyRow.kt index 5405d0a..18319e0 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/PropertyRow.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/PropertyRow.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -14,7 +15,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.github.worn.ui.theme.WornColors import com.github.worn.ui.theme.WornTheme @Composable @@ -29,8 +29,13 @@ fun PropertyRow( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Text(label, color = WornColors.TextSecondary, fontSize = fontSize, fontWeight = FontWeight.Medium) - Text(value, color = WornColors.TextPrimary, fontSize = fontSize, fontWeight = FontWeight.Medium) + Text( + label, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = fontSize, + fontWeight = FontWeight.Medium, + ) + Text(value, color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize, fontWeight = FontWeight.Medium) } } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionHeader.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionHeader.kt index 3807ac5..a882189 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionHeader.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionHeader.kt @@ -14,6 +14,7 @@ import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -26,7 +27,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R -import com.github.worn.ui.theme.WornColors import com.github.worn.ui.theme.WornTheme @Composable @@ -45,14 +45,14 @@ fun SelectionHeader( ) { Text( text = pluralStringResource(R.plurals.selected_count, count, count), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 28.sp, fontWeight = FontWeight.Medium, letterSpacing = (-0.8).sp, ) Button( onClick = onDelete, - colors = ButtonDefaults.buttonColors(containerColor = WornColors.DeleteRed), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), shape = RoundedCornerShape(22.dp), ) { Icon(Icons.Outlined.Delete, contentDescription = null, tint = Color.White) @@ -68,7 +68,7 @@ fun SelectionHeader( Spacer(modifier = Modifier.height(8.dp)) Text( text = stringResource(R.string.common_cancel), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 15.sp, fontWeight = FontWeight.Medium, modifier = Modifier.clickable(onClick = onCancel), diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionIndicator.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionIndicator.kt index 7455398..34fa321 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionIndicator.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionIndicator.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -19,7 +20,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.github.worn.ui.theme.WornColors import com.github.worn.ui.theme.WornTheme @Composable @@ -31,8 +31,8 @@ fun SelectionIndicator( ) { Surface( shape = RoundedCornerShape(size / 2), - color = if (isSelected) WornColors.AccentGreen else WornColors.BgCard, - border = if (isSelected) null else BorderStroke(1.5.dp, WornColors.BorderSubtle), + color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerLowest, + border = if (isSelected) null else BorderStroke(1.5.dp, MaterialTheme.colorScheme.outlineVariant), modifier = modifier.size(size), ) { if (isSelected) { diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/SheetDragHandle.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/SheetDragHandle.kt index cdf47bb..1fe966a 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/SheetDragHandle.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/SheetDragHandle.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -14,13 +15,13 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.github.worn.ui.theme.WornColors import com.github.worn.ui.theme.WornTheme +import com.github.worn.ui.theme.wornExtras @Composable fun SheetDragHandle( modifier: Modifier = Modifier, - color: Color = WornColors.IconMuted, + color: Color = MaterialTheme.wornExtras.iconMuted, ) { Box( contentAlignment = Alignment.Center, @@ -40,7 +41,7 @@ fun SheetDragHandle( @Composable private fun SheetDragHandlePhonePreview() { WornTheme { - Box(modifier = Modifier.background(WornColors.BgElevated)) { + Box(modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainerHigh)) { SheetDragHandle() } } @@ -50,7 +51,7 @@ private fun SheetDragHandlePhonePreview() { @Composable private fun SheetDragHandleTabletPreview() { WornTheme { - Box(modifier = Modifier.background(WornColors.BgElevated)) { + Box(modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainerHigh)) { SheetDragHandle() } } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt index c9a5af6..54b8148 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt @@ -22,6 +22,7 @@ import androidx.compose.material.icons.outlined.Layers import androidx.compose.material.icons.outlined.QrCodeScanner import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,16 +31,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag -import androidx.compose.ui.semantics.Role import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R -import com.github.worn.ui.theme.WornColors enum class Tab( @StringRes val labelRes: Int, @@ -74,8 +74,8 @@ fun WornBottomBar( ) { Surface( shape = RoundedCornerShape(36.dp), - color = WornColors.BgElevated, - border = BorderStroke(1.dp, WornColors.BorderSubtle), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = Modifier .then( if (isCompact) Modifier.fillMaxWidth() @@ -110,7 +110,7 @@ private fun TabItem( ) { Surface( shape = RoundedCornerShape(26.dp), - color = if (isActive) WornColors.AccentGreen else WornColors.BgElevated, + color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerHigh, // No ripple: it repaints the bar for ~1s after each tap, which reads as a slow page // switch. The active tab's fill already signals selection. modifier = modifier @@ -127,7 +127,7 @@ private fun TabItem( verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight(), ) { - val tint = if (isActive) WornColors.TextOnColor else WornColors.TextSecondary + val tint = if (isActive) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant val label = stringResource(tab.labelRes) if (tab.iconRes != null) { Icon( @@ -147,7 +147,11 @@ private fun TabItem( Text( text = label, style = TextStyle( - color = if (isActive) WornColors.TextOnColor else WornColors.TextSecondary, + color = if (isActive) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, fontSize = 10.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 0.5.sp, diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt index 77dab60..b73c56e 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -14,7 +15,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.github.worn.ui.theme.WornColors import com.github.worn.ui.theme.WornTheme private val chipShape = RoundedCornerShape(20.dp) @@ -29,13 +29,13 @@ fun WornChip( Surface( onClick = onClick, shape = chipShape, - color = if (isActive) WornColors.AccentGreen else WornColors.BgCard, - border = if (isActive) null else BorderStroke(1.dp, WornColors.BorderSubtle), + color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerLowest, + border = if (isActive) null else BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = modifier, ) { Text( text = label, - color = if (isActive) WornColors.TextOnColor else WornColors.TextSecondary, + color = if (isActive) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp, fontWeight = FontWeight.Medium, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornGradientButton.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornGradientButton.kt index f30043d..b650b74 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornGradientButton.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornGradientButton.kt @@ -12,8 +12,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -24,15 +26,32 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.github.worn.ui.theme.WornColors +import com.github.worn.ui.theme.LocalWornExtras import com.github.worn.ui.theme.WornTheme +import com.github.worn.ui.theme.wornExtras +/** + * Gradient stops, resolved per theme. + * + * These are `@Composable` getters rather than top-level `val`s: captured in a `val` the stops + * would freeze to whichever theme was active at class-init time and never follow dark mode. + */ object WornGradients { - val Save = listOf(WornColors.SaveGradientStart, WornColors.SaveGradientEnd) - val Green = listOf(WornColors.AccentGreen, WornColors.AccentGreenDark) - val GreenCta = listOf(WornColors.AccentGreen, WornColors.AccentGreenEnd) - val Indigo = listOf(WornColors.AccentIndigo, Color(0xFF556070)) - val Disabled = listOf(WornColors.TextMuted, WornColors.IconMuted) + val Save: List + @Composable @ReadOnlyComposable + get() = LocalWornExtras.current.let { listOf(it.saveGradientStart, it.saveGradientEnd) } + val Green: List + @Composable @ReadOnlyComposable + get() = LocalWornExtras.current.let { listOf(it.greenCtaStart, it.accentGreenDark) } + val GreenCta: List + @Composable @ReadOnlyComposable + get() = LocalWornExtras.current.let { listOf(it.greenCtaStart, it.greenCtaEnd) } + val Indigo: List + @Composable @ReadOnlyComposable + get() = LocalWornExtras.current.let { listOf(it.indigoGradientStart, it.indigoGradientEnd) } + val Disabled: List + @Composable @ReadOnlyComposable + get() = listOf(MaterialTheme.wornExtras.textMuted, MaterialTheme.wornExtras.iconMuted) } @Composable diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/AddItemSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/AddItemSheet.kt index 037dbb0..292242d 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/AddItemSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/AddItemSheet.kt @@ -25,6 +25,7 @@ import androidx.compose.material.icons.outlined.PhotoLibrary import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -55,9 +56,7 @@ import com.github.worn.domain.model.Fit import com.github.worn.domain.model.Material import com.github.worn.domain.model.Season import com.github.worn.domain.model.Subcategory -import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.components.AiBadge -import com.github.worn.ui.components.SheetDragHandle import com.github.worn.ui.components.AiLockedSheet import com.github.worn.ui.components.CategoryDropdown import com.github.worn.ui.components.ColorSection @@ -70,13 +69,14 @@ import com.github.worn.ui.components.PhotoUploadZone import com.github.worn.ui.components.RemoveBackgroundToggle import com.github.worn.ui.components.SaveButton import com.github.worn.ui.components.SeasonSection +import com.github.worn.ui.components.SheetDragHandle import com.github.worn.ui.components.SubcategoryDropdown +import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.SheetPreview import com.github.worn.ui.util.decodePreviewImage import com.github.worn.ui.util.readImageBytes import com.github.worn.ui.util.rememberCameraCapture import com.github.worn.ui.util.rememberDecodedImage -import com.github.worn.ui.theme.WornColors import kotlinx.coroutines.launch import org.koin.compose.koinInject @@ -99,7 +99,7 @@ fun AddItemSheet( ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - containerColor = WornColors.BgElevated, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), dragHandle = { SheetDragHandle() }, ) { @@ -355,7 +355,7 @@ private fun AddItemFormContent( ) { Text( text = stringResource(if (isEditing) R.string.add_item_title_edit else R.string.add_item_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.5).sp, diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt index 1074c4a..a7a56f8 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -56,15 +57,15 @@ import com.github.worn.R import com.github.worn.domain.model.Category import com.github.worn.domain.model.ClothingItem import com.github.worn.domain.model.Outfit -import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.components.CategoryFilterChips +import com.github.worn.ui.components.ClothingPhoto import com.github.worn.ui.components.SelectionIndicator import com.github.worn.ui.components.SheetDragHandle import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients +import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.SheetPreview -import com.github.worn.ui.components.ClothingPhoto -import com.github.worn.ui.theme.WornColors +import com.github.worn.ui.theme.wornExtras @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -84,7 +85,7 @@ fun CreateOutfitSheet( ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - containerColor = WornColors.BgElevated, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), dragHandle = { SheetDragHandle() }, ) { @@ -128,7 +129,7 @@ internal fun CreateOutfitForm( ) { Text( text = stringResource(if (isEditing) R.string.create_outfit_title_edit else R.string.create_outfit_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.5).sp, @@ -163,20 +164,20 @@ private fun OutfitNameField(name: String, onNameChange: (String) -> Unit, modifi placeholder = { Text( stringResource(R.string.create_outfit_name_hint), - color = WornColors.IconMuted, + color = MaterialTheme.wornExtras.iconMuted, fontSize = 15.sp, ) }, colors = TextFieldDefaults.colors( - focusedContainerColor = WornColors.BgCard, - unfocusedContainerColor = WornColors.BgCard, + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, ), shape = RoundedCornerShape(12.dp), modifier = modifier .fillMaxWidth() - .border(1.dp, WornColors.BorderSubtle, RoundedCornerShape(12.dp)), + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(12.dp)), ) } @@ -189,14 +190,14 @@ private fun SelectItemsHeader(selectedCount: Int) { ) { Text( text = stringResource(R.string.create_outfit_select_items), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, ) if (selectedCount > 0) { Text( text = pluralStringResource(R.plurals.selected_count, selectedCount, selectedCount), - color = WornColors.AccentGreen, + color = MaterialTheme.colorScheme.primary, fontSize = 13.sp, fontWeight = FontWeight.Medium, ) @@ -246,7 +247,7 @@ private fun SelectableItemCell( .clip(cellShape) .border( width = if (isSelected) 2.dp else 1.dp, - color = if (isSelected) WornColors.AccentGreen else WornColors.BorderSubtle, + color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant, shape = cellShape, ) .clickable(onClick = onClick), @@ -258,7 +259,12 @@ private fun SelectableItemCell( @Composable private fun ItemThumbnail(item: ClothingItem) { - Surface(shape = cellShape, color = WornColors.BgCard, shadowElevation = 4.dp, modifier = Modifier.fillMaxSize()) { + Surface( + shape = cellShape, + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 4.dp, + modifier = Modifier.fillMaxSize(), + ) { ClothingPhoto( photoPath = item.photoPath, contentDescription = item.name, diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt index a72d99a..80f0ee3 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt @@ -3,6 +3,7 @@ package com.github.worn.ui.screen import android.widget.Toast +import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -28,25 +29,27 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.annotation.StringRes import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -54,9 +57,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.window.core.layout.WindowWidthSizeClass import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.window.core.layout.WindowWidthSizeClass import com.github.worn.R import com.github.worn.domain.model.Category import com.github.worn.domain.model.GapRecommendation @@ -65,18 +67,18 @@ import com.github.worn.presentation.viewmodel.GapsEffect import com.github.worn.presentation.viewmodel.GapsIntent import com.github.worn.presentation.viewmodel.GapsState import com.github.worn.presentation.viewmodel.GapsViewModel -import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.components.AiLockedSheet import com.github.worn.ui.components.ErrorContentView import com.github.worn.ui.components.SheetDragHandle -import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.Tab +import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.displayLabel import com.github.worn.ui.components.displayName import com.github.worn.ui.components.iconRes -import com.github.worn.ui.theme.WornColors +import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme +import com.github.worn.ui.theme.wornExtras import org.koin.compose.viewmodel.koinViewModel @Composable @@ -176,7 +178,7 @@ private fun GapsScaffold( Scaffold( modifier = Modifier.testTag("gaps_screen"), - containerColor = WornColors.BgPage, + containerColor = MaterialTheme.colorScheme.surface, ) { paddingValues -> LazyColumn( modifier = Modifier @@ -188,14 +190,14 @@ private fun GapsScaffold( Spacer(Modifier.height(24.dp)) Text( text = stringResource(R.string.gaps_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 28.sp, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.5).sp, ) Text( text = stringResource(R.string.gaps_subtitle), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, ) Spacer(Modifier.height(20.dp)) @@ -231,7 +233,7 @@ private fun LoadingContent() { contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth().padding(vertical = 80.dp), ) { - CircularProgressIndicator(color = WornColors.AccentGreen) + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) } } @@ -246,26 +248,26 @@ private fun CompleteContent() { modifier = Modifier .size(72.dp) .clip(CircleShape) - .background(WornColors.BgElevated), + .background(MaterialTheme.colorScheme.surfaceContainerHigh), ) { Icon( imageVector = Icons.Default.Check, contentDescription = null, - tint = WornColors.AccentGreen, + tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(32.dp), ) } Spacer(Modifier.height(24.dp)) Text( text = stringResource(R.string.gaps_complete_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, ) Spacer(Modifier.height(8.dp)) Text( text = stringResource(R.string.gaps_complete_description), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, lineHeight = 20.sp, ) @@ -302,7 +304,7 @@ private fun LazyListScope.gapsContent( @Composable private fun GapsBanner(isAiMode: Boolean, onClick: () -> Unit) { - val bgColor = if (isAiMode) WornColors.AccentGreen else WornColors.AccentGreenDark + val bgColor = if (isAiMode) MaterialTheme.colorScheme.primary else MaterialTheme.wornExtras.accentGreenDark Surface( onClick = onClick, shape = RoundedCornerShape(16.dp), @@ -345,7 +347,7 @@ private fun GapsBanner(isAiMode: Boolean, onClick: () -> Unit) { private fun SectionLabel(text: String) { Text( text = text.uppercase(), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, fontWeight = FontWeight.Medium, letterSpacing = 0.5.sp, @@ -361,7 +363,7 @@ private fun GapCard( Surface( onClick = onClick, shape = RoundedCornerShape(12.dp), - color = WornColors.BgCard, + color = MaterialTheme.colorScheme.surfaceContainerLowest, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -372,7 +374,7 @@ private fun GapCard( Column(modifier = Modifier.weight(1f)) { Text( text = recommendation.itemName, - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 15.sp, fontWeight = FontWeight.Medium, ) @@ -382,14 +384,14 @@ private fun GapCard( } else { stringResource(R.string.gaps_pairing_common) }, - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, ) } Icon( imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, contentDescription = null, - tint = WornColors.IconMuted, + tint = MaterialTheme.wornExtras.iconMuted, modifier = Modifier.size(18.dp), ) } @@ -415,12 +417,14 @@ private fun CategoryIcon(category: Category) { } } +@Composable +@ReadOnlyComposable private fun Category.dotColor(): Color = when (this) { - Category.TOP -> WornColors.CategoryDotTop - Category.BOTTOM -> WornColors.CategoryDotBottom - Category.OUTERWEAR -> WornColors.CategoryDotOuterwear - Category.SHOES -> WornColors.CategoryDotShoes - Category.ACCESSORY -> WornColors.CategoryDotAccessory + Category.TOP -> MaterialTheme.wornExtras.categoryDotTop + Category.BOTTOM -> MaterialTheme.wornExtras.categoryDotBottom + Category.OUTERWEAR -> MaterialTheme.wornExtras.categoryDotOuterwear + Category.SHOES -> MaterialTheme.wornExtras.categoryDotShoes + Category.ACCESSORY -> MaterialTheme.wornExtras.categoryDotAccessory } // region Detail Sheet @@ -438,9 +442,9 @@ private fun GapDetailSheet( ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - containerColor = WornColors.BgElevated, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), - dragHandle = { SheetDragHandle(color = WornColors.BorderStrong) }, + dragHandle = { SheetDragHandle(color = MaterialTheme.colorScheme.outline) }, ) { GapDetailContent( recommendation = recommendation, @@ -482,19 +486,19 @@ private fun DetailHeader(recommendation: GapRecommendation) { .fillMaxWidth() .height(140.dp) .clip(RoundedCornerShape(16.dp)) - .background(WornColors.BgCard), + .background(MaterialTheme.colorScheme.surfaceContainerLowest), ) { Icon( painter = painterResource(recommendation.mappedCategory.iconRes()), contentDescription = null, - tint = WornColors.IconMuted, + tint = MaterialTheme.wornExtras.iconMuted, modifier = Modifier.size(48.dp), ) } Spacer(Modifier.height(16.dp)) Text( text = recommendation.itemName, - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 22.sp, fontWeight = FontWeight.SemiBold, ) @@ -509,7 +513,7 @@ private fun DetailHeader(recommendation: GapRecommendation) { Spacer(Modifier.width(6.dp)) Text( text = recommendation.mappedCategory.displayLabel(), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, ) } @@ -519,7 +523,7 @@ private fun DetailHeader(recommendation: GapRecommendation) { private fun DetailPairingInfo(recommendation: GapRecommendation, isAiMode: Boolean) { Surface( shape = RoundedCornerShape(8.dp), - color = WornColors.BgCard, + color = MaterialTheme.colorScheme.surfaceContainerLowest, modifier = Modifier.fillMaxWidth(), ) { Row( @@ -529,7 +533,7 @@ private fun DetailPairingInfo(recommendation: GapRecommendation, isAiMode: Boole Icon( imageVector = Icons.Outlined.AutoAwesome, contentDescription = null, - tint = WornColors.AccentGreen, + tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp), ) Spacer(Modifier.width(8.dp)) @@ -539,7 +543,7 @@ private fun DetailPairingInfo(recommendation: GapRecommendation, isAiMode: Boole } else { stringResource(R.string.gaps_pairing_common) }, - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp, ) } @@ -581,8 +585,8 @@ private fun DetailRow(label: String, value: String) { modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, ) { - Text(label, color = WornColors.TextSecondary, fontSize = 14.sp) - Text(value, color = WornColors.TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.Medium) + Text(label, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) + Text(value, color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, fontWeight = FontWeight.Medium) } } @@ -597,7 +601,7 @@ private fun DetailActions(onAddToWardrobe: () -> Unit, onDismiss: () -> Unit) { Surface( onClick = onDismiss, shape = RoundedCornerShape(16.dp), - color = WornColors.BgCard, + color = MaterialTheme.colorScheme.surfaceContainerLowest, modifier = Modifier.fillMaxWidth().testTag("gap_dismiss"), ) { Box( @@ -606,7 +610,7 @@ private fun DetailActions(onAddToWardrobe: () -> Unit, onDismiss: () -> Unit) { ) { Text( stringResource(R.string.gaps_dismiss), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 15.sp, fontWeight = FontWeight.Medium, ) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt index 527a2b9..a80567a 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -39,30 +40,29 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.github.worn.ui.exposeTestTagsAsResourceId +import com.github.worn.R import com.github.worn.domain.model.Category import com.github.worn.domain.model.ClothingItem import com.github.worn.domain.model.Fit import com.github.worn.domain.model.Material import com.github.worn.domain.model.Season import com.github.worn.domain.model.Subcategory -import androidx.compose.ui.res.stringResource -import com.github.worn.R +import com.github.worn.ui.components.ClothingPhoto import com.github.worn.ui.components.PropertyRow import com.github.worn.ui.components.SheetDragHandle import com.github.worn.ui.components.addItemColorPalette import com.github.worn.ui.components.displayLabel import com.github.worn.ui.components.displayName import com.github.worn.ui.components.dotColor +import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.SheetPreview -import com.github.worn.ui.components.ClothingPhoto -import com.github.worn.ui.theme.WornColors @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -79,9 +79,9 @@ fun ItemDetailSheet( ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - containerColor = WornColors.BgElevated, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), - dragHandle = { SheetDragHandle(color = WornColors.BorderStrong) }, + dragHandle = { SheetDragHandle(color = MaterialTheme.colorScheme.outline) }, ) { ItemDetailContent( item = item, @@ -158,8 +158,8 @@ private data class ItemDetailDimens(val isCompact: Boolean) { private fun ItemPhoto(item: ClothingItem, dims: ItemDetailDimens) { Surface( shape = RoundedCornerShape(dims.photoRadius), - color = WornColors.BgCard, - border = BorderStroke(1.dp, WornColors.BorderSubtle), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 8.dp, modifier = Modifier.fillMaxWidth().height(dims.photoHeight), ) { @@ -177,7 +177,7 @@ private fun ItemNameGroup(item: ClothingItem, nameSize: TextUnit) { Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { Text( text = item.name, - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = nameSize, fontWeight = FontWeight.SemiBold, ) @@ -193,7 +193,7 @@ private fun ItemNameGroup(item: ClothingItem, nameSize: TextUnit) { ) Text( text = item.category.displayLabel(), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, ) } @@ -206,7 +206,7 @@ private fun HorizontalDivider() { modifier = Modifier .fillMaxWidth() .height(1.dp) - .background(WornColors.BorderSubtle), + .background(MaterialTheme.colorScheme.outlineVariant), ) } @@ -249,7 +249,7 @@ private fun ColorPropertyRow(item: ClothingItem, fontSize: TextUnit) { ) { Text( stringResource(R.string.label_color), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = fontSize, fontWeight = FontWeight.Medium, ) @@ -257,12 +257,12 @@ private fun ColorPropertyRow(item: ClothingItem, fontSize: TextUnit) { Surface( shape = CircleShape, color = colorForName(item.colors.first()), - border = BorderStroke(1.dp, WornColors.BorderSubtle), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = Modifier.size(14.dp), ) {} Text( text = item.colors.joinToString(", ") { it.replaceFirstChar(Char::uppercase) }, - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize, fontWeight = FontWeight.Medium, ) @@ -286,13 +286,13 @@ internal fun DetailActionButtons( Surface( onClick = onEdit, shape = RoundedCornerShape(24.dp), - color = WornColors.BgCard, - border = BorderStroke(1.dp, WornColors.BorderSubtle), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = Modifier.fillMaxWidth().height(buttonHeight).testTag(editTestTag), ) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { Text( - editLabel, color = WornColors.TextPrimary, + editLabel, color = MaterialTheme.colorScheme.onSurface, fontSize = buttonFontSize, fontWeight = FontWeight.SemiBold, ) } @@ -300,7 +300,7 @@ internal fun DetailActionButtons( Surface( onClick = onDelete, shape = RoundedCornerShape(24.dp), - color = WornColors.DeleteRed, + color = MaterialTheme.colorScheme.error, modifier = Modifier.fillMaxWidth().height(buttonHeight).testTag(deleteTestTag), ) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { @@ -324,13 +324,13 @@ private fun DeleteItemDialog(itemName: String, onConfirm: () -> Unit, onDismiss: text = { Text( stringResource(R.string.item_detail_delete_dialog_message, itemName), - color = WornColors.TextSecondary, fontSize = 15.sp, lineHeight = 22.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 15.sp, lineHeight = 22.sp, ) }, confirmButton = { Button( onClick = onConfirm, - colors = ButtonDefaults.buttonColors(containerColor = WornColors.DeleteRed), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), shape = RoundedCornerShape(24.dp), ) { Text(stringResource(R.string.common_delete), fontWeight = FontWeight.SemiBold) } }, diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt index d0933cb..2f36ca2 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -36,27 +37,26 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.res.stringResource import com.github.worn.R import com.github.worn.domain.model.Category import com.github.worn.domain.model.ClothingItem import com.github.worn.domain.model.Outfit import com.github.worn.domain.model.Season -import com.github.worn.ui.exposeTestTagsAsResourceId +import com.github.worn.ui.components.ClothingPhoto import com.github.worn.ui.components.PropertyRow import com.github.worn.ui.components.SheetDragHandle +import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.SheetPreview -import com.github.worn.ui.components.ClothingPhoto -import com.github.worn.ui.theme.WornColors @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -73,9 +73,9 @@ fun OutfitDetailSheet( ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - containerColor = WornColors.BgElevated, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), - dragHandle = { SheetDragHandle(color = WornColors.BorderStrong) }, + dragHandle = { SheetDragHandle(color = MaterialTheme.colorScheme.outline) }, ) { OutfitDetailContent( outfit = outfit, @@ -116,7 +116,7 @@ internal fun OutfitDetailContent( if (!isCompact) { Box( modifier = Modifier.fillMaxWidth().padding(horizontal = contentPadding) - .height(1.dp).background(WornColors.BorderSubtle), + .height(1.dp).background(MaterialTheme.colorScheme.outlineVariant), ) } OutfitProperties(outfit = outfit, items = outfitItems, isCompact = isCompact, padding = contentPadding) @@ -147,7 +147,7 @@ internal fun OutfitDetailContent( private fun OutfitTitle(name: String, nameSize: TextUnit, padding: Dp) { Text( text = name, - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = nameSize, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(horizontal = padding), @@ -206,13 +206,13 @@ private fun DeleteOutfitDialog(outfitName: String, onConfirm: () -> Unit, onDism text = { Text( stringResource(R.string.outfit_detail_delete_dialog_message, outfitName), - color = WornColors.TextSecondary, fontSize = 15.sp, lineHeight = 22.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 15.sp, lineHeight = 22.sp, ) }, confirmButton = { Button( onClick = onConfirm, - colors = ButtonDefaults.buttonColors(containerColor = WornColors.DeleteRed), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), shape = RoundedCornerShape(24.dp), ) { Text(stringResource(R.string.common_delete), fontWeight = FontWeight.SemiBold) } }, @@ -232,8 +232,8 @@ private fun OutfitItemCard( ) { Surface( shape = RoundedCornerShape(cornerRadius), - color = WornColors.BgCard, - border = BorderStroke(1.dp, WornColors.BorderSubtle), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 8.dp, modifier = Modifier.size(size), ) { @@ -246,7 +246,7 @@ private fun OutfitItemCard( } Text( text = item.name, - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 13.sp, fontWeight = FontWeight.Medium, ) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt index 703a75b..c19c298 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt @@ -6,8 +6,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -28,6 +28,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -60,13 +61,12 @@ import com.github.worn.presentation.viewmodel.OutfitIntent import com.github.worn.presentation.viewmodel.OutfitState import com.github.worn.presentation.viewmodel.OutfitViewModel import com.github.worn.ui.components.DeleteConfirmationDialog -import com.github.worn.ui.components.SelectionHeader import com.github.worn.ui.components.EmptyStateView import com.github.worn.ui.components.OutfitCard +import com.github.worn.ui.components.SelectionHeader +import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients -import com.github.worn.ui.components.Tab -import com.github.worn.ui.theme.WornColors import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme import org.koin.compose.viewmodel.koinViewModel @@ -179,7 +179,7 @@ private fun OutfitsScaffold( Scaffold( modifier = Modifier.testTag("outfits_screen"), - containerColor = WornColors.BgPage, + containerColor = MaterialTheme.colorScheme.surface, ) { paddingValues -> val isEmpty = !state.isLoading && state.outfits.isEmpty() @@ -202,7 +202,7 @@ private fun OutfitsScaffold( EmptyState(onCreateClick = onCreateClick) } else if (state.isLoading && state.outfits.isEmpty()) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - CircularProgressIndicator(color = WornColors.AccentGreen) + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) } } else { Spacer(modifier = Modifier.height(sectionGap)) @@ -234,7 +234,7 @@ private fun OutfitsHeader(outfitCount: Int, onCreateClick: () -> Unit = {}) { ) { Text( text = stringResource(R.string.outfits_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = if (outfitCount == 0) 22.sp else 28.sp, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.5).sp, @@ -242,7 +242,7 @@ private fun OutfitsHeader(outfitCount: Int, onCreateClick: () -> Unit = {}) { if (outfitCount > 0) { Button( onClick = onCreateClick, - colors = ButtonDefaults.buttonColors(containerColor = WornColors.AccentGreen), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), shape = RoundedCornerShape(20.dp), modifier = Modifier.testTag("outfits_create_button"), ) { @@ -256,7 +256,7 @@ private fun OutfitsHeader(outfitCount: Int, onCreateClick: () -> Unit = {}) { Spacer(modifier = Modifier.height(8.dp)) Text( text = pluralStringResource(R.plurals.saved_combinations, outfitCount, outfitCount), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, fontWeight = FontWeight.Medium, ) @@ -296,7 +296,6 @@ private fun OutfitsContent( } private val CtaShape = RoundedCornerShape(28.dp) -private val CtaGradient = Brush.verticalGradient(listOf(WornColors.AccentGreen, WornColors.AccentGreenEnd)) @Composable private fun EmptyState(onCreateClick: () -> Unit = {}) { @@ -306,7 +305,7 @@ private fun EmptyState(onCreateClick: () -> Unit = {}) { imageVector = Icons.Outlined.Layers, contentDescription = null, modifier = Modifier.size(52.dp), - tint = WornColors.TextSecondary, + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) }, title = stringResource(R.string.outfits_empty_title), @@ -323,7 +322,12 @@ private fun EmptyState(onCreateClick: () -> Unit = {}) { fixedHeight = null, contentPadding = PaddingValues(horizontal = 36.dp, vertical = 16.dp), icon = { - Icon(Icons.Default.Add, contentDescription = null, Modifier.size(18.dp), WornColors.BgPage) + Icon( + Icons.Default.Add, + contentDescription = null, + Modifier.size(18.dp), + MaterialTheme.colorScheme.surface, + ) }, ) }, diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt index 3309357..60b976f 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt @@ -2,6 +2,7 @@ package com.github.worn.ui.screen +import androidx.annotation.StringRes import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -32,6 +33,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -40,6 +42,7 @@ import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -49,25 +52,23 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation -import androidx.annotation.StringRes import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.window.core.layout.WindowWidthSizeClass import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.window.core.layout.WindowWidthSizeClass import com.github.worn.R import com.github.worn.domain.model.AgeRange import com.github.worn.domain.model.BodyType @@ -82,14 +83,14 @@ import com.github.worn.presentation.viewmodel.SettingsEffect import com.github.worn.presentation.viewmodel.SettingsIntent import com.github.worn.presentation.viewmodel.SettingsState import com.github.worn.presentation.viewmodel.SettingsViewModel -import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.components.SheetDragHandle +import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornChip import com.github.worn.ui.components.WornGradientButton -import com.github.worn.ui.components.Tab -import com.github.worn.ui.theme.WornColors +import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme +import com.github.worn.ui.theme.wornExtras import org.koin.compose.viewmodel.koinViewModel @Suppress("UnusedParameter") @@ -175,7 +176,7 @@ private fun SettingsScaffold( Scaffold( modifier = Modifier.testTag("settings_screen"), - containerColor = WornColors.BgPage, + containerColor = MaterialTheme.colorScheme.surface, ) { paddingValues -> Column( modifier = Modifier @@ -187,7 +188,7 @@ private fun SettingsScaffold( Spacer(Modifier.height(24.dp)) Text( text = stringResource(R.string.settings_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 28.sp, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.5).sp, @@ -197,7 +198,7 @@ private fun SettingsScaffold( SectionLabel(stringResource(R.string.settings_section_profile)) Spacer(Modifier.height(10.dp)) SettingsCard( - icon = { SettingsIcon(color = WornColors.AccentGreen, icon = Icons.Outlined.Person) }, + icon = { SettingsIcon(color = MaterialTheme.colorScheme.primary, icon = Icons.Outlined.Person) }, title = stringResource(R.string.settings_your_profile), subtitle = state.userProfile.summaryText(), onClick = onProfileClick, @@ -209,7 +210,7 @@ private fun SettingsScaffold( Spacer(Modifier.height(10.dp)) // Listed before the key card: it is free and private, so it should be the first option. SettingsToggleCard( - icon = { SettingsIcon(color = WornColors.AccentGreen, icon = Icons.Outlined.PhoneAndroid) }, + icon = { SettingsIcon(color = MaterialTheme.colorScheme.primary, icon = Icons.Outlined.PhoneAndroid) }, title = stringResource(R.string.settings_on_device_ai_title), subtitle = stringResource(state.onDeviceAiAvailability.subtitleRes()), checked = state.onDeviceAiEnabled, @@ -219,7 +220,7 @@ private fun SettingsScaffold( ) Spacer(Modifier.height(10.dp)) SettingsCard( - icon = { SettingsIcon(color = WornColors.AccentIndigo, icon = Icons.Outlined.AutoAwesome) }, + icon = { SettingsIcon(color = MaterialTheme.colorScheme.secondary, icon = Icons.Outlined.AutoAwesome) }, title = stringResource(R.string.settings_api_key_title), subtitle = stringResource( if (state.hasApiKey) R.string.settings_api_key_connected else R.string.settings_api_key_required, @@ -229,7 +230,7 @@ private fun SettingsScaffold( ) Spacer(Modifier.height(10.dp)) SettingsCard( - icon = { SettingsIcon(color = WornColors.AccentIndigo, icon = Icons.Outlined.Checkroom) }, + icon = { SettingsIcon(color = MaterialTheme.colorScheme.secondary, icon = Icons.Outlined.Checkroom) }, title = stringResource(R.string.settings_youcam_title), subtitle = stringResource( if (state.hasYouCamKey) R.string.settings_youcam_connected else R.string.settings_youcam_required, @@ -255,7 +256,7 @@ private fun SettingsScaffold( private fun SectionLabel(text: String) { Text( text = text, - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, fontWeight = FontWeight.Medium, letterSpacing = 0.5.sp, @@ -299,7 +300,7 @@ private fun SettingsToggleCard( onClick = { onCheckedChange(!checked) }, enabled = enabled, shape = RoundedCornerShape(16.dp), - color = WornColors.BgCard, + color = MaterialTheme.colorScheme.surfaceContainerLowest, modifier = modifier, ) { Row( @@ -309,8 +310,13 @@ private fun SettingsToggleCard( icon() Spacer(Modifier.width(14.dp)) Column(modifier = Modifier.weight(1f)) { - Text(title, color = WornColors.TextPrimary, fontSize = 16.sp, fontWeight = FontWeight.Medium) - Text(subtitle, color = WornColors.TextSecondary, fontSize = 13.sp) + Text( + title, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + ) + Text(subtitle, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp) } Spacer(Modifier.width(8.dp)) Switch( @@ -319,7 +325,7 @@ private fun SettingsToggleCard( enabled = enabled, colors = SwitchDefaults.colors( checkedThumbColor = Color.White, - checkedTrackColor = WornColors.AccentGreen, + checkedTrackColor = MaterialTheme.colorScheme.primary, ), ) } @@ -352,7 +358,7 @@ private fun SettingsCard( Surface( onClick = onClick, shape = RoundedCornerShape(16.dp), - color = WornColors.BgCard, + color = MaterialTheme.colorScheme.surfaceContainerLowest, modifier = modifier, ) { Row( @@ -362,13 +368,18 @@ private fun SettingsCard( icon() Spacer(Modifier.width(14.dp)) Column(modifier = Modifier.weight(1f)) { - Text(title, color = WornColors.TextPrimary, fontSize = 16.sp, fontWeight = FontWeight.Medium) - Text(subtitle, color = WornColors.TextSecondary, fontSize = 13.sp) + Text( + title, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + ) + Text(subtitle, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp) } Icon( imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, contentDescription = null, - tint = WornColors.IconMuted, + tint = MaterialTheme.wornExtras.iconMuted, modifier = Modifier.size(20.dp), ) } @@ -387,7 +398,7 @@ private fun AboutCard() { Surface( shape = RoundedCornerShape(16.dp), - color = WornColors.BgCard, + color = MaterialTheme.colorScheme.surfaceContainerLowest, ) { Column { Row( @@ -396,15 +407,15 @@ private fun AboutCard() { ) { Text( stringResource(R.string.settings_version), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 15.sp, modifier = Modifier.weight(1f), ) - Text(versionName ?: "1.0", color = WornColors.TextSecondary, fontSize = 15.sp) + Text(versionName ?: "1.0", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 15.sp) } - HorizontalDivider(color = WornColors.BorderSubtle.copy(alpha = 0.5f)) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) AboutLinkRow(stringResource(R.string.settings_suggestions_bugs)) { uriHandler.openUri(FEEDBACK_URL) } - HorizontalDivider(color = WornColors.BorderSubtle.copy(alpha = 0.5f)) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) AboutLinkRow(stringResource(R.string.settings_licenses)) { uriHandler.openUri(LICENSE_URL) } } } @@ -417,11 +428,11 @@ private fun AboutLinkRow(label: String, onClick: () -> Unit) { verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(16.dp), ) { - Text(label, color = WornColors.TextPrimary, fontSize = 15.sp, modifier = Modifier.weight(1f)) + Text(label, color = MaterialTheme.colorScheme.onSurface, fontSize = 15.sp, modifier = Modifier.weight(1f)) Icon( imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, contentDescription = null, - tint = WornColors.IconMuted, + tint = MaterialTheme.wornExtras.iconMuted, modifier = Modifier.size(20.dp), ) } @@ -436,19 +447,19 @@ private fun DonationCard() { Surface( shape = RoundedCornerShape(16.dp), - color = WornColors.BgCard, + color = MaterialTheme.colorScheme.surfaceContainerLowest, ) { Column(modifier = Modifier.padding(16.dp)) { Text( stringResource(R.string.settings_donate_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 15.sp, fontWeight = FontWeight.Medium, ) Spacer(Modifier.height(4.dp)) Text( stringResource(R.string.settings_donate_subtitle), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp, ) Spacer(Modifier.height(12.dp)) @@ -458,8 +469,8 @@ private fun DonationCard() { showCopied = true }, shape = RoundedCornerShape(12.dp), - color = WornColors.BgElevated, - border = BorderStroke(1.dp, WornColors.BorderSubtle), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -467,7 +478,7 @@ private fun DonationCard() { ) { Text( DONATION_LN_ADDRESS, - color = WornColors.AccentGreen, + color = MaterialTheme.colorScheme.primary, fontSize = 13.sp, fontWeight = FontWeight.Medium, modifier = Modifier.weight(1f), @@ -475,7 +486,7 @@ private fun DonationCard() { Spacer(Modifier.width(8.dp)) Text( if (showCopied) copiedText else stringResource(R.string.settings_donate_copy), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, fontWeight = FontWeight.Medium, ) @@ -504,7 +515,7 @@ private fun ProfileSheet( ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - containerColor = WornColors.BgElevated, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), dragHandle = { SheetDragHandle() }, ) { @@ -538,13 +549,13 @@ private fun ProfileSheetContent(state: SettingsState, onIntent: (SettingsIntent) ) { Text( text = stringResource(R.string.settings_your_profile), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, ) Text( text = stringResource(R.string.settings_profile_help), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, ) ChipGroup( @@ -601,7 +612,7 @@ private fun ApiKeySheet( ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - containerColor = WornColors.BgElevated, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), dragHandle = { SheetDragHandle() }, ) { @@ -653,7 +664,7 @@ private fun ApiKeySheetContent( ) { Text( text = stringResource(R.string.settings_remove_key), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, fontWeight = FontWeight.Medium, modifier = Modifier.padding(vertical = 8.dp), @@ -668,18 +679,18 @@ private fun ApiKeySheetContent( private fun ApiKeySheetHeader() { Text( text = stringResource(R.string.settings_connect_claude), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, ) Text( text = stringResource(R.string.settings_api_description), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, ) Text( text = stringResource(R.string.settings_api_get_key), - color = WornColors.AccentGreen, + color = MaterialTheme.colorScheme.primary, fontSize = 13.sp, fontWeight = FontWeight.Medium, ) @@ -715,14 +726,14 @@ private fun ApiKeyTextField( contentDescription = stringResource( if (passwordVisible) R.string.settings_api_hide else R.string.settings_api_show, ), - tint = WornColors.IconMuted, + tint = MaterialTheme.wornExtras.iconMuted, ) } }, colors = TextFieldDefaults.colors( - focusedContainerColor = WornColors.BgCard, - unfocusedContainerColor = WornColors.BgCard, - disabledContainerColor = WornColors.BgCard, + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, disabledIndicatorColor = Color.Transparent, @@ -751,7 +762,7 @@ private fun YouCamCredentialsSheet( ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - containerColor = WornColors.BgElevated, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), dragHandle = { SheetDragHandle() }, ) { @@ -788,18 +799,18 @@ private fun YouCamCredentialsSheetContent( ) { Text( text = stringResource(R.string.settings_youcam_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, ) Text( text = stringResource(R.string.settings_youcam_description), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, ) Text( text = stringResource(R.string.settings_youcam_get_key), - color = WornColors.AccentGreen, + color = MaterialTheme.colorScheme.primary, fontSize = 13.sp, fontWeight = FontWeight.Medium, ) @@ -832,7 +843,7 @@ private fun YouCamCredentialsSheetContent( if (errorMessage != null && !verifying) { Text( text = errorMessage, - color = WornColors.DeleteRed, + color = MaterialTheme.colorScheme.error, fontSize = 13.sp, modifier = Modifier.testTag("youcam_error"), ) @@ -846,7 +857,7 @@ private fun YouCamCredentialsSheetContent( ) { Text( text = stringResource(R.string.settings_youcam_remove), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, fontWeight = FontWeight.Medium, modifier = Modifier.padding(vertical = 8.dp), @@ -859,7 +870,7 @@ private fun YouCamCredentialsSheetContent( @Composable private fun FieldLabel(text: String) { - Text(text, color = WornColors.TextPrimary, fontSize = 13.sp, fontWeight = FontWeight.SemiBold) + Text(text, color = MaterialTheme.colorScheme.onSurface, fontSize = 13.sp, fontWeight = FontWeight.SemiBold) } // endregion @@ -885,7 +896,7 @@ private fun ChipGroup( onSelected: (T?) -> Unit, ) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text(title, color = WornColors.TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.SemiBold) + Text(title, color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, fontWeight = FontWeight.SemiBold) FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), @@ -911,9 +922,13 @@ private fun MultiChipGroup( ) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Row { - Text(title, color = WornColors.TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.SemiBold) + Text(title, color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, fontWeight = FontWeight.SemiBold) Spacer(Modifier.width(6.dp)) - Text(stringResource(R.string.settings_multi_select), color = WornColors.TextMuted, fontSize = 12.sp) + Text( + stringResource(R.string.settings_multi_select), + color = MaterialTheme.wornExtras.textMuted, + fontSize = 12.sp, + ) } FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt index 951a5a1..a407cf4 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt @@ -46,6 +46,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -80,8 +81,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.window.core.layout.WindowWidthSizeClass import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.window.core.layout.WindowWidthSizeClass import com.github.worn.R import com.github.worn.domain.model.ClothingItem import com.github.worn.domain.model.GarmentCategory @@ -91,20 +92,20 @@ import com.github.worn.presentation.viewmodel.TryItEffect import com.github.worn.presentation.viewmodel.TryItIntent import com.github.worn.presentation.viewmodel.TryItState import com.github.worn.presentation.viewmodel.TryItViewModel +import com.github.worn.ui.components.ClothingPhoto import com.github.worn.ui.components.CropEditorDialog import com.github.worn.ui.components.CropPhotoButton import com.github.worn.ui.components.ErrorContentView import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients -import com.github.worn.ui.components.ClothingPhoto +import com.github.worn.ui.theme.WornDimens +import com.github.worn.ui.theme.WornTheme +import com.github.worn.ui.theme.wornExtras import com.github.worn.ui.util.SharedPhoto import com.github.worn.ui.util.readImageBytes import com.github.worn.ui.util.rememberCameraCapture import com.github.worn.ui.util.rememberDecodedImage -import com.github.worn.ui.theme.WornColors -import com.github.worn.ui.theme.WornDimens -import com.github.worn.ui.theme.WornTheme import kotlinx.coroutines.launch import org.koin.compose.viewmodel.koinViewModel @@ -452,7 +453,7 @@ private fun TryItScaffold( Scaffold( modifier = Modifier.testTag("try_it_screen"), - containerColor = WornColors.BgPage, + containerColor = MaterialTheme.colorScheme.surface, snackbarHost = { SnackbarHost(snackbarHostState) }, ) { paddingValues -> if (!state.hasApiKey && !state.hasYouCamKey) { @@ -510,8 +511,8 @@ private fun AiEmptyContent( ) { Surface( shape = CircleShape, - color = WornColors.BgCard, - border = BorderStroke(1.dp, WornColors.BorderSubtle), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 8.dp, modifier = Modifier.size(circleSize), ) { @@ -519,7 +520,7 @@ private fun AiEmptyContent( Icon( imageVector = Icons.Outlined.SmartToy, contentDescription = null, - tint = WornColors.AccentIndigo, + tint = MaterialTheme.colorScheme.secondary, modifier = Modifier.size(iconSize), ) } @@ -527,7 +528,7 @@ private fun AiEmptyContent( Spacer(Modifier.height(24.dp)) Text( text = stringResource(R.string.tryit_locked_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = titleSize, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center, @@ -535,7 +536,7 @@ private fun AiEmptyContent( Spacer(Modifier.height(12.dp)) Text( text = stringResource(R.string.tryit_locked_description), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = if (isCompact) 15.sp else 16.sp, lineHeight = if (isCompact) 22.sp else 24.sp, textAlign = TextAlign.Center, @@ -660,7 +661,7 @@ private fun TryItPhoneContent( message = errorMsg, onRetry = onAnalyze, modifier = Modifier.padding(vertical = 40.dp), - retryButtonColor = WornColors.AccentIndigo, + retryButtonColor = MaterialTheme.colorScheme.secondary, ) } } @@ -730,7 +731,7 @@ private fun TryItTabletContent( message = errorMsg, onRetry = onAnalyze, modifier = Modifier.padding(vertical = 40.dp), - retryButtonColor = WornColors.AccentIndigo, + retryButtonColor = MaterialTheme.colorScheme.secondary, ) } } @@ -776,7 +777,7 @@ private fun TryItTabletContent( private fun TryItTitle(fontSize: androidx.compose.ui.unit.TextUnit) { Text( text = stringResource(R.string.tryit_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.8).sp, @@ -792,8 +793,8 @@ private fun UploadZone( Surface( onClick = onClick, shape = RoundedCornerShape(20.dp), - color = WornColors.BgCard, - border = BorderStroke(1.5.dp, WornColors.BorderStrong), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + border = BorderStroke(1.5.dp, MaterialTheme.colorScheme.outline), shadowElevation = 1.dp, modifier = Modifier.fillMaxWidth().height(height).testTag("try_it_upload_zone"), ) { @@ -813,13 +814,13 @@ private fun UploadZone( Icon( imageVector = Icons.Outlined.CameraAlt, contentDescription = null, - tint = WornColors.IconMuted, + tint = MaterialTheme.wornExtras.iconMuted, modifier = Modifier.size(44.dp), ) Spacer(Modifier.height(12.dp)) Text( text = stringResource(R.string.tryit_upload_hint), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center, @@ -857,7 +858,7 @@ private fun LoadingIndicator() { contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth().padding(vertical = 40.dp), ) { - CircularProgressIndicator(color = WornColors.AccentIndigo) + CircularProgressIndicator(color = MaterialTheme.colorScheme.secondary) } } @@ -885,7 +886,7 @@ private fun PairsSection( Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( text = stringResource(R.string.tryit_pairs_with), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.2).sp, @@ -909,8 +910,8 @@ private fun ItemThumbnail(item: ClothingItem, size: Dp, onClick: () -> Unit) { Surface( onClick = onClick, shape = RoundedCornerShape(16.dp), - color = WornColors.BgCard, - border = BorderStroke(1.dp, WornColors.BorderSubtle), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 2.dp, modifier = Modifier.size(size), ) { @@ -919,7 +920,7 @@ private fun ItemThumbnail(item: ClothingItem, size: Dp, onClick: () -> Unit) { contentDescription = item.name, shape = RoundedCornerShape(16.dp), placeholderIconSize = 28.dp, - placeholderTint = WornColors.TextSecondary, + placeholderTint = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -931,8 +932,8 @@ private fun CombinationsCard(count: Int, isCompact: Boolean) { Surface( shape = RoundedCornerShape(20.dp), - color = WornColors.BgCard, - border = BorderStroke(1.dp, WornColors.BorderSubtle), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 2.dp, modifier = Modifier.fillMaxWidth(), ) { @@ -944,14 +945,14 @@ private fun CombinationsCard(count: Int, isCompact: Boolean) { ) { Text( text = stringResource(R.string.tryit_combinations_unlocked), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 0.5.sp, ) Text( text = count.toString(), - color = WornColors.AccentGreen, + color = MaterialTheme.colorScheme.primary, fontSize = valueSize, fontWeight = FontWeight.Bold, letterSpacing = (-1.2).sp, @@ -969,7 +970,7 @@ private fun GapsFilledSection(gaps: List, isCompact: Boolean) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( text = stringResource(R.string.tryit_gaps_filled), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.2).sp, @@ -984,11 +985,11 @@ private fun GapsFilledSection(gaps: List, isCompact: Boolean) { modifier = Modifier .size(8.dp) .clip(CircleShape) - .background(WornColors.AccentGreen), + .background(MaterialTheme.colorScheme.primary), ) Text( text = gap, - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize, ) } @@ -1000,7 +1001,7 @@ private fun GapsFilledSection(gaps: List, isCompact: Boolean) { private fun DecisionBanner(worthAdding: Boolean, isCompact: Boolean) { val bannerHeight = if (isCompact) 56.dp else 60.dp val gradient = if (worthAdding) { - Brush.verticalGradient(listOf(WornColors.AccentGreen, WornColors.AccentGreenDark)) + Brush.verticalGradient(listOf(MaterialTheme.colorScheme.primary, MaterialTheme.wornExtras.accentGreenDark)) } else { Brush.verticalGradient(listOf(Color(0xFF8B7D7D), Color(0xFF6B5E5E))) } @@ -1055,7 +1056,7 @@ private fun TryOnSection( ) { Text( text = stringResource(R.string.tryit_your_photo_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.2).sp, @@ -1073,7 +1074,7 @@ private fun TryOnSection( } Text( text = stringResource(R.string.tryit_tryon_category_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.2).sp, @@ -1093,7 +1094,7 @@ private fun TryOnSection( message = errorMsg, onRetry = onGenerateTryOn, modifier = Modifier.padding(vertical = 24.dp), - retryButtonColor = WornColors.AccentIndigo, + retryButtonColor = MaterialTheme.colorScheme.secondary, ) } } @@ -1102,7 +1103,7 @@ private fun TryOnSection( } Text( text = stringResource(R.string.tryit_tryon_cost_note), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, ) } @@ -1114,8 +1115,8 @@ private fun PersonPhotoZone(personImage: ByteArray?, height: Dp, onClick: () -> Surface( onClick = onClick, shape = RoundedCornerShape(20.dp), - color = WornColors.BgCard, - border = BorderStroke(1.5.dp, WornColors.BorderStrong), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + border = BorderStroke(1.5.dp, MaterialTheme.colorScheme.outline), shadowElevation = 1.dp, modifier = Modifier.fillMaxWidth().height(height).testTag("try_on_person_zone"), ) { @@ -1135,13 +1136,13 @@ private fun PersonPhotoZone(personImage: ByteArray?, height: Dp, onClick: () -> Icon( imageVector = Icons.Outlined.CameraAlt, contentDescription = null, - tint = WornColors.IconMuted, + tint = MaterialTheme.wornExtras.iconMuted, modifier = Modifier.size(44.dp), ) Spacer(Modifier.height(12.dp)) Text( text = stringResource(R.string.tryit_your_photo_hint), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center, @@ -1167,7 +1168,7 @@ private fun GarmentCategorySelector( onClick = { onSelect(category) }, label = { Text(stringResource(categoryLabelRes(category))) }, colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = WornColors.AccentIndigo, + selectedContainerColor = MaterialTheme.colorScheme.secondary, selectedLabelColor = Color.White, ), ) @@ -1211,10 +1212,10 @@ private fun TryOnLoadingIndicator() { verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth().padding(vertical = 32.dp), ) { - CircularProgressIndicator(color = WornColors.AccentIndigo) + CircularProgressIndicator(color = MaterialTheme.colorScheme.secondary) Text( text = stringResource(R.string.tryit_tryon_generating), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, ) } @@ -1226,15 +1227,15 @@ private fun TryOnResultView(imageBytes: ByteArray, height: Dp) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( text = stringResource(R.string.tryit_tryon_result_title), - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.2).sp, ) Surface( shape = RoundedCornerShape(20.dp), - color = WornColors.BgCard, - border = BorderStroke(1.dp, WornColors.BorderSubtle), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 2.dp, modifier = Modifier.fillMaxWidth().height(height).testTag("try_on_result"), ) { diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt index 21b5a95..29d4b66 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt @@ -6,9 +6,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -29,9 +29,11 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -43,19 +45,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.window.core.layout.WindowWidthSizeClass import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.window.core.layout.WindowWidthSizeClass import com.github.worn.R import com.github.worn.domain.model.Category import com.github.worn.domain.model.ClothingItem @@ -64,14 +65,13 @@ import com.github.worn.presentation.viewmodel.WardrobeIntent import com.github.worn.presentation.viewmodel.WardrobeState import com.github.worn.presentation.viewmodel.WardrobeViewModel import com.github.worn.ui.components.CategoryFilterChips +import com.github.worn.ui.components.ClothingCard import com.github.worn.ui.components.DeleteConfirmationDialog -import com.github.worn.ui.components.SelectionHeader import com.github.worn.ui.components.EmptyStateView +import com.github.worn.ui.components.SelectionHeader +import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients -import com.github.worn.ui.components.ClothingCard -import com.github.worn.ui.components.Tab -import com.github.worn.ui.theme.WornColors import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme import com.github.worn.ui.util.ShortcutCommand @@ -206,7 +206,7 @@ private fun WardrobeScaffold( Scaffold( modifier = Modifier.testTag("wardrobe_screen"), - containerColor = WornColors.BgPage, + containerColor = MaterialTheme.colorScheme.surface, floatingActionButton = { val isWardrobeEmpty = !state.isLoading && state.totalItemCount == 0 if (!isSelectionMode && !isWardrobeEmpty) { @@ -278,7 +278,7 @@ private fun WardrobeHeader(itemCount: Int) { } else { stringResource(R.string.wardrobe_title) }, - color = WornColors.TextPrimary, + color = MaterialTheme.colorScheme.onSurface, fontSize = if (itemCount == 0) 22.sp else 28.sp, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.5).sp, @@ -287,7 +287,7 @@ private fun WardrobeHeader(itemCount: Int) { Spacer(modifier = Modifier.height(8.dp)) Text( text = stringResource(R.string.wardrobe_subtitle, itemCount), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, fontWeight = FontWeight.Medium, ) @@ -308,7 +308,7 @@ private fun WardrobeContent( if (state.isLoading && state.items.isEmpty()) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - CircularProgressIndicator(color = WornColors.AccentGreen) + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) } } else { LazyVerticalGrid( @@ -338,7 +338,6 @@ private fun WardrobeContent( } private val CtaShape = RoundedCornerShape(28.dp) -private val CtaGradient = Brush.verticalGradient(listOf(WornColors.AccentGreen, WornColors.AccentGreenEnd)) @Composable private fun CategoryEmptyState() { @@ -351,12 +350,12 @@ private fun CategoryEmptyState() { painter = painterResource(id = R.drawable.ic_shirt), contentDescription = null, modifier = Modifier.size(48.dp), - tint = WornColors.TextSecondary.copy(alpha = 0.5f), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), ) Spacer(Modifier.height(16.dp)) Text( stringResource(R.string.wardrobe_category_empty), - color = WornColors.TextSecondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 16.sp, fontWeight = FontWeight.Medium, ) @@ -371,7 +370,7 @@ private fun EmptyState(onAddItemClick: () -> Unit) { painter = painterResource(id = R.drawable.ic_shirt), contentDescription = null, modifier = Modifier.size(52.dp), - tint = WornColors.TextSecondary, + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) }, title = stringResource(R.string.wardrobe_empty_title), @@ -388,7 +387,12 @@ private fun EmptyState(onAddItemClick: () -> Unit) { fixedHeight = null, contentPadding = PaddingValues(horizontal = 36.dp, vertical = 16.dp), icon = { - Icon(Icons.Default.Add, contentDescription = null, Modifier.size(18.dp), WornColors.BgPage) + Icon( + Icons.Default.Add, + contentDescription = null, + Modifier.size(18.dp), + MaterialTheme.colorScheme.surface, + ) }, ) }, @@ -399,8 +403,8 @@ private fun EmptyState(onAddItemClick: () -> Unit) { private fun AddItemFab(onClick: () -> Unit, modifier: Modifier = Modifier) { ExtendedFloatingActionButton( onClick = onClick, - containerColor = WornColors.AccentGreen, - contentColor = WornColors.TextOnColor, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, shape = RoundedCornerShape(30.dp), modifier = modifier, ) { diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/theme/SheetPreview.kt b/composeApp/src/main/kotlin/com/github/worn/ui/theme/SheetPreview.kt index db6200a..ca22bda 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/theme/SheetPreview.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/theme/SheetPreview.kt @@ -8,11 +8,13 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import com.github.worn.ui.theme.wornExtras @Composable fun SheetPreview(content: @Composable () -> Unit) { @@ -21,20 +23,20 @@ fun SheetPreview(content: @Composable () -> Unit) { contentAlignment = Alignment.BottomCenter, modifier = Modifier .fillMaxSize() - .background(WornColors.BgPage.copy(alpha = 0.5f)), + .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.5f)), ) { Column( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)) - .background(WornColors.BgElevated), + .background(MaterialTheme.colorScheme.surfaceContainerHigh), ) { Spacer(modifier = Modifier.height(12.dp)) Box( modifier = Modifier .align(Alignment.CenterHorizontally) .clip(RoundedCornerShape(2.dp)) - .background(WornColors.IconMuted) + .background(MaterialTheme.wornExtras.iconMuted) .height(4.dp) .fillMaxWidth(0.1f), ) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornColorScheme.kt b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornColorScheme.kt new file mode 100644 index 0000000..3b852e1 --- /dev/null +++ b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornColorScheme.kt @@ -0,0 +1,216 @@ +package com.github.worn.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color + +/** + * Worn's palette in two layers. + * + * The M3 [androidx.compose.material3.ColorScheme] below is the real source of truth: every + * Material component (sheets, dialogs, ripples, snackbars) reads it, so filling in *all* the roles + * — not just the handful the app names — is what keeps stray baseline-purple out of the UI. + * + * [WornExtras] carries the few brand tokens M3 has no role for: gradient stops, the muted + * text/icon greys and the category dots. It hangs off [MaterialTheme] as [wornExtras], so call + * sites read `MaterialTheme.wornExtras.iconMuted` right next to + * `MaterialTheme.colorScheme.primary` and every colour comes from one place. + */ + +// --------------------------------------------------------------------------------------------- +// Light — the established warm beige + sage brand, unchanged in hue. +// --------------------------------------------------------------------------------------------- + +internal val WornLightColorScheme = lightColorScheme( + primary = Color(0xFF7A9468), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFDCE6D2), + onPrimaryContainer = Color(0xFF2A3A20), + inversePrimary = Color(0xFFA8C295), + + secondary = Color(0xFF6B7B8E), + onSecondary = Color(0xFFFFFFFF), + secondaryContainer = Color(0xFFDCE3EA), + onSecondaryContainer = Color(0xFF22303E), + + tertiary = Color(0xFFA87560), + onTertiary = Color(0xFFFFFFFF), + tertiaryContainer = Color(0xFFF2DED4), + onTertiaryContainer = Color(0xFF3D2318), + + error = Color(0xFFC45B4A), + onError = Color(0xFFFFFFFF), + errorContainer = Color(0xFFF7DAD4), + onErrorContainer = Color(0xFF43110A), + + background = Color(0xFFF5F0EB), + onBackground = Color(0xFF2C2924), + surface = Color(0xFFF5F0EB), + onSurface = Color(0xFF2C2924), + surfaceVariant = Color(0xFFEDE8E1), + onSurfaceVariant = Color(0xFF7D776F), + surfaceTint = Color(0xFF7A9468), + + surfaceDim = Color(0xFFE4DDD3), + surfaceBright = Color(0xFFFFFBF7), + surfaceContainerLowest = Color(0xFFFFFFFF), + // ModalBottomSheet reads surfaceContainerLow for its default container; setting it here is + // what lets the sheets stop passing containerColor by hand. + surfaceContainerLow = Color(0xFFFAF6F1), + surfaceContainer = Color(0xFFF2EDE6), + surfaceContainerHigh = Color(0xFFEDE8E1), + surfaceContainerHighest = Color(0xFFE7E0D7), + + // M3 defines outline as the stronger of the pair; the previous scheme had these two swapped. + outline = Color(0xFFC8C0B5), + outlineVariant = Color(0xFFE0D9D0), + + scrim = Color(0xFF000000), + inverseSurface = Color(0xFF322F2A), + inverseOnSurface = Color(0xFFF5F0EB), +) + +internal val WornLightExtras = WornExtras( + textMuted = Color(0xFFB5AFA8), + iconMuted = Color(0xFFA09A92), + accentGreenDark = Color(0xFF5C6E50), + saveGradientStart = Color(0xFF8FA47D), + saveGradientEnd = Color(0xFF6B7F5E), + greenCtaStart = Color(0xFF7A9468), + greenCtaEnd = Color(0xFF6B8A58), + indigoGradientStart = Color(0xFF6B7B8E), + indigoGradientEnd = Color(0xFF556070), + categoryDotTop = Color(0xFF444444), + categoryDotBottom = Color(0xFF2B4570), + categoryDotDress = Color(0xFFA87560), + categoryDotOuterwear = Color(0xFF7A9468), + categoryDotShoes = Color(0xFF8B6914), + categoryDotAccessory = Color(0xFFB59D6E), +) + +// --------------------------------------------------------------------------------------------- +// Dark — the same hues on a warm dark axis. Two things this deliberately avoids: +// +// * Near-black. A #121212-style base reads cold next to the sage and drops the brand's warmth +// entirely, so the page sits at #211D18 — a brown-grey that still reads as "Worn". +// * A compressed ramp. The steps are spaced widely enough to be visible: page #211D18 against +// cards #1A1713 is a step you can actually see, where a 2% difference just looks like one +// flat black sheet no matter how correct the token names are. +// --------------------------------------------------------------------------------------------- + +internal val WornDarkColorScheme = darkColorScheme( + primary = Color(0xFFA8C295), + onPrimary = Color(0xFF1B2913), + primaryContainer = Color(0xFF3D5230), + onPrimaryContainer = Color(0xFFC4DEB0), + inversePrimary = Color(0xFF7A9468), + + secondary = Color(0xFFA9BACE), + onSecondary = Color(0xFF1B2733), + secondaryContainer = Color(0xFF3A4756), + onSecondaryContainer = Color(0xFFC7D6E6), + + tertiary = Color(0xFFD8A88F), + onTertiary = Color(0xFF2C1509), + tertiaryContainer = Color(0xFF59392A), + onTertiaryContainer = Color(0xFFF2DED4), + + error = Color(0xFFF2B8AC), + onError = Color(0xFF4E1509), + errorContainer = Color(0xFF7A2A1E), + onErrorContainer = Color(0xFFFFDAD3), + + background = Color(0xFF211D18), + onBackground = Color(0xFFEFE8DD), + surface = Color(0xFF211D18), + onSurface = Color(0xFFEFE8DD), + surfaceVariant = Color(0xFF363029), + onSurfaceVariant = Color(0xFFD2C9BC), + surfaceTint = Color(0xFFA8C295), + + surfaceDim = Color(0xFF211D18), + surfaceBright = Color(0xFF4A443C), + surfaceContainerLowest = Color(0xFF1A1713), + surfaceContainerLow = Color(0xFF262119), + surfaceContainer = Color(0xFF2B261F), + surfaceContainerHigh = Color(0xFF363029), + surfaceContainerHighest = Color(0xFF423B32), + + outline = Color(0xFFA0988B), + outlineVariant = Color(0xFF554E45), + + scrim = Color(0xFF000000), + inverseSurface = Color(0xFFEDE6DC), + inverseOnSurface = Color(0xFF322F2A), +) + +internal val WornDarkExtras = WornExtras( + textMuted = Color(0xFF8A8378), + iconMuted = Color(0xFF9B9488), + accentGreenDark = Color(0xFF4E6641), + saveGradientStart = Color(0xFF6E8A5C), + saveGradientEnd = Color(0xFF546B45), + greenCtaStart = Color(0xFF6E8A5C), + greenCtaEnd = Color(0xFF5C7A4B), + indigoGradientStart = Color(0xFF5E6E80), + indigoGradientEnd = Color(0xFF4A5462), + // These do double duty: 8dp dots on the page surface, and 36dp tile fills behind *white* + // icons in Gaps. So they can only be lifted far enough to be visible on #14120F, not so far + // that white stops reading on top — which rules out simply using the light-scheme tints. + // Only the two darkest needed moving; the rest already clear both bars unchanged. + categoryDotTop = Color(0xFF6E6862), + categoryDotBottom = Color(0xFF42618F), + categoryDotDress = Color(0xFFA87560), + categoryDotOuterwear = Color(0xFF7A9468), + categoryDotShoes = Color(0xFF8B6914), + categoryDotAccessory = Color(0xFFB59D6E), +) + +/** + * Brand tokens with no equivalent M3 role. + * + * The gradient stops are deliberately *not* derived from `primary` / `secondary`. Those roles + * invert between light and dark (sage goes from #7A9468 to a much lighter #A8C295), and the + * gradient buttons always draw white label text — deriving them would silently drop the label to + * roughly 1.8:1 in dark mode. These stay saturated enough for white in both themes. + */ +@Immutable +data class WornExtras( + val textMuted: Color, + val iconMuted: Color, + /** Banner and gradient-end fills that always carry white text — dark in *both* themes. */ + val accentGreenDark: Color, + val saveGradientStart: Color, + val saveGradientEnd: Color, + val greenCtaStart: Color, + val greenCtaEnd: Color, + val indigoGradientStart: Color, + val indigoGradientEnd: Color, + val categoryDotTop: Color, + val categoryDotBottom: Color, + val categoryDotDress: Color, + val categoryDotOuterwear: Color, + val categoryDotShoes: Color, + val categoryDotAccessory: Color, +) + +internal val LocalWornExtras = staticCompositionLocalOf { WornLightExtras } + +/** + * Extension point for the brand tokens that have no M3 role. + * + * This is the standard Compose way to widen a design system: hang the extra tokens off + * [MaterialTheme] so a call site reads `MaterialTheme.wornExtras.iconMuted` right beside + * `MaterialTheme.colorScheme.primary`. Everything colour-related then comes from one object, and + * the M3 role in play is visible at the point of use instead of hidden behind an alias. + * + * Reading it outside a composition is a compile error by design — a top-level `val` capturing a + * colour would freeze it to whichever theme was active at class-init time. + */ +val MaterialTheme.wornExtras: WornExtras + @Composable @ReadOnlyComposable get() = LocalWornExtras.current diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt index 1fd8a6c..49ce2e4 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt @@ -1,72 +1,27 @@ package com.github.worn.ui.theme +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.unit.dp -import androidx.compose.ui.graphics.Color object WornDimens { val BottomBarClearance = 95.dp } -object WornColors { - // Backgrounds - val BgPage = Color(0xFFF5F0EB) - val BgCard = Color(0xFFFFFFFF) - val BgElevated = Color(0xFFEDE8E1) - - // Borders - val BorderSubtle = Color(0xFFE0D9D0) - val BorderStrong = Color(0xFFC8C0B5) - - // Accents - val AccentGreen = Color(0xFF7A9468) - val AccentGreenEnd = Color(0xFF6B8A58) - val AccentGreenDark = Color(0xFF5C6E50) - val AccentIndigo = Color(0xFF6B7B8E) - val AccentCoral = Color(0xFFA87560) - val DeleteRed = Color(0xFFC45B4A) - - // Gradients - val SaveGradientStart = Color(0xFF8FA47D) - val SaveGradientEnd = Color(0xFF6B7F5E) - - // Text - val TextPrimary = Color(0xFF2C2924) - val TextSecondary = Color(0xFF7D776F) - val TextMuted = Color(0xFFB5AFA8) - val TextOnColor = Color(0xFFFFFFFF) - - // Icons - val IconMuted = Color(0xFFA09A92) - - // Category dots - val CategoryDotTop = Color(0xFF444444) - val CategoryDotBottom = Color(0xFF2B4570) - val CategoryDotDress = Color(0xFFA87560) - val CategoryDotOuterwear = Color(0xFF7A9468) - val CategoryDotShoes = Color(0xFF8B6914) - val CategoryDotAccessory = Color(0xFFB59D6E) -} - -private val WornLightColorScheme = lightColorScheme( - primary = WornColors.AccentGreen, - onPrimary = WornColors.TextOnColor, - background = WornColors.BgPage, - onBackground = WornColors.TextPrimary, - surface = WornColors.BgCard, - onSurface = WornColors.TextPrimary, - surfaceVariant = WornColors.BgElevated, - onSurfaceVariant = WornColors.TextSecondary, - outline = WornColors.BorderSubtle, - outlineVariant = WornColors.BorderStrong, -) - @Composable -fun WornTheme(content: @Composable () -> Unit) { - MaterialTheme( - colorScheme = WornLightColorScheme, - content = content, - ) +fun WornTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val colorScheme = if (darkTheme) WornDarkColorScheme else WornLightColorScheme + val extras = if (darkTheme) WornDarkExtras else WornLightExtras + + CompositionLocalProvider(LocalWornExtras provides extras) { + MaterialTheme( + colorScheme = colorScheme, + content = content, + ) + } } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/util/CameraCapture.kt b/composeApp/src/main/kotlin/com/github/worn/ui/util/CameraCapture.kt index 0013816..0aae6af 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/util/CameraCapture.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/util/CameraCapture.kt @@ -9,10 +9,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalContext import androidx.core.content.FileProvider +import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.io.File /** * Launches the camera and hands back the captured photo at full resolution. diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/util/RememberDecodedImage.kt b/composeApp/src/main/kotlin/com/github/worn/ui/util/RememberDecodedImage.kt index d89dae2..5722888 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/util/RememberDecodedImage.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/util/RememberDecodedImage.kt @@ -5,9 +5,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap +import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.io.File /** * Decodes [bytes] into a preview-sized [ImageBitmap] off the main thread. From 06480c05090745803614a802a0fd8126c609b1b8 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 8 Aug 2026 10:17:58 -0300 Subject: [PATCH 02/13] feat: add dark mode to iOS with the same palette as Android Mirrors WornColorScheme.kt token for token. Each colour is now built from a light/dark pair via UIColor(dynamicProvider:), so SwiftUI resolves it against the trait collection and every existing WornColors call site follows the system appearance with no colorScheme checks anywhere. Gradient stops move onto dedicated tokens rather than the accent roles, which invert between appearances while these buttons always draw white labels. Info.plist deliberately still does not pin UIUserInterfaceStyle: the app follows the system, which also stops the photo picker and share sheet from rendering dark against a permanently light app. Co-Authored-By: Claude Opus 5 (1M context) --- iosApp/iosApp/Components/EmptyStateView.swift | 2 +- .../Components/WornGradientButton.swift | 21 ++-- iosApp/iosApp/Theme/WornColors.swift | 96 +++++++++++++------ 3 files changed, 76 insertions(+), 43 deletions(-) diff --git a/iosApp/iosApp/Components/EmptyStateView.swift b/iosApp/iosApp/Components/EmptyStateView.swift index 86cdd00..b340076 100644 --- a/iosApp/iosApp/Components/EmptyStateView.swift +++ b/iosApp/iosApp/Components/EmptyStateView.swift @@ -24,7 +24,7 @@ struct EmptyStateView: View { ZStack { Circle() - .fill(Color.white) + .fill(WornColors.bgCard) .frame(width: 130, height: 130) .shadow(color: WornColors.accentIndigo.opacity(0.08), radius: 15, x: 0, y: 0) .overlay( diff --git a/iosApp/iosApp/Components/WornGradientButton.swift b/iosApp/iosApp/Components/WornGradientButton.swift index 6dfed2a..c138383 100644 --- a/iosApp/iosApp/Components/WornGradientButton.swift +++ b/iosApp/iosApp/Components/WornGradientButton.swift @@ -1,10 +1,15 @@ import SwiftUI +/// Gradient stops for the filled CTAs. +/// +/// These read from the dedicated `*Gradient*` tokens rather than the accent roles: the accents +/// invert between appearances while these buttons always draw white label text, so pairing them +/// with `accentGreen` would leave the label at roughly 1.8:1 in dark. See `WornColors`. enum WornGradients { static let save = [WornColors.saveGradientStart, WornColors.saveGradientEnd] - static let green = [WornColors.accentGreen, WornColors.accentGreenDark] - static let greenCta = [WornColors.accentGreen, WornColors.accentGreenEnd] - static let indigo = [WornColors.accentIndigo, Color(hex: 0x556070)] + static let green = [WornColors.greenCtaStart, WornColors.accentGreenDark] + static let greenCta = [WornColors.greenCtaStart, WornColors.greenCtaEnd] + static let indigo = [WornColors.indigoGradientStart, WornColors.indigoGradientEnd] static let disabled = [WornColors.textMuted, WornColors.iconMuted] } @@ -59,16 +64,6 @@ struct WornGradientButton: View { } } -private extension Color { - init(hex: UInt) { - self.init( - red: Double((hex >> 16) & 0xFF) / 255.0, - green: Double((hex >> 8) & 0xFF) / 255.0, - blue: Double(hex & 0xFF) / 255.0 - ) - } -} - #Preview("iPhone") { VStack(spacing: 16) { WornGradientButton(text: "Save to Wardrobe", action: {}) diff --git a/iosApp/iosApp/Theme/WornColors.swift b/iosApp/iosApp/Theme/WornColors.swift index 007ac66..c2b08bc 100644 --- a/iosApp/iosApp/Theme/WornColors.swift +++ b/iosApp/iosApp/Theme/WornColors.swift @@ -1,40 +1,64 @@ import SwiftUI import Shared +/// Worn's palette, mirroring `WornColorScheme.kt` on Android value for value. +/// +/// Every token is built from a light/dark pair through `UIColor(dynamicProvider:)`, so SwiftUI +/// resolves it against the current trait collection and the whole app follows the system +/// appearance with no `@Environment(\.colorScheme)` checks at any call site. +/// +/// Light keeps the established warm beige + sage brand unchanged. Dark sits on a warm #211D18 +/// base rather than a neutral near-black — the latter reads cold against the sage and drops the +/// brand's warmth — with the ramp spaced widely enough that page, card and bar actually separate. enum WornColors { // Backgrounds - static let bgPage = Color(hex: "F5F0EB") - static let bgCard = Color.white - static let bgElevated = Color(hex: "EDE8E1") + static let bgPage = Color(light: "F5F0EB", dark: "211D18") + static let bgCard = Color(light: "FFFFFF", dark: "1A1713") + static let bgElevated = Color(light: "EDE8E1", dark: "363029") // Borders - static let borderSubtle = Color(hex: "E0D9D0") - static let borderStrong = Color(hex: "C8C0B5") + static let borderSubtle = Color(light: "E0D9D0", dark: "554E45") + static let borderStrong = Color(light: "C8C0B5", dark: "A0988B") // Accents - static let accentGreen = Color(hex: "7A9468") - static let accentGreenEnd = Color(hex: "6B8A58") - static let accentGreenDark = Color(hex: "5C6E50") - static let accentIndigo = Color(hex: "6B7B8E") - static let accentCoral = Color(hex: "A87560") - static let deleteRed = Color(hex: "C45B4A") + static let accentGreen = Color(light: "7A9468", dark: "A8C295") + static let accentIndigo = Color(light: "6B7B8E", dark: "A9BACE") + static let accentCoral = Color(light: "A87560", dark: "D8A88F") + static let deleteRed = Color(light: "C45B4A", dark: "F2B8AC") - // Gradients - static let saveGradientStart = Color(hex: "8FA47D") - static let saveGradientEnd = Color(hex: "6B7F5E") + /// Banner and gradient-end fills that always carry white text, so dark in *both* appearances. + static let accentGreenDark = Color(light: "5C6E50", dark: "4E6641") + + // Gradient stops. + // + // Deliberately not derived from `accentGreen` / `accentIndigo`: those invert between + // appearances (sage goes from #7A9468 to a much lighter #A8C295) and the gradient buttons + // always draw white label text, so deriving them would silently drop the label to about + // 1.8:1 in dark. These stay saturated enough for white either way. + static let saveGradientStart = Color(light: "8FA47D", dark: "6E8A5C") + static let saveGradientEnd = Color(light: "6B7F5E", dark: "546B45") + static let greenCtaStart = Color(light: "7A9468", dark: "6E8A5C") + static let greenCtaEnd = Color(light: "6B8A58", dark: "5C7A4B") + static let indigoGradientStart = Color(light: "6B7B8E", dark: "5E6E80") + static let indigoGradientEnd = Color(light: "556070", dark: "4A5462") // Text - static let textPrimary = Color(hex: "2C2924") - static let textSecondary = Color(hex: "7D776F") - static let textMuted = Color(hex: "B5AFA8") + static let textPrimary = Color(light: "2C2924", dark: "EFE8DD") + static let textSecondary = Color(light: "7D776F", dark: "D2C9BC") + static let textMuted = Color(light: "B5AFA8", dark: "8A8378") + /// Label colour on top of a filled accent — white in both appearances by design. static let textOnColor = Color.white // Icons - static let iconMuted = Color(hex: "A09A92") + static let iconMuted = Color(light: "A09A92", dark: "9B9488") - // Category dots - static let categoryDotTop = Color(hex: "444444") - static let categoryDotBottom = Color(hex: "2B4570") + // Category dots. + // + // These do double duty: small dots on the page surface, and 36pt tile fills behind *white* + // icons in Gaps. So they can only be lifted far enough to stay visible on #211D18, not so far + // that white stops reading on top. Only the two darkest needed moving for dark. + static let categoryDotTop = Color(light: "444444", dark: "6E6862") + static let categoryDotBottom = Color(light: "2B4570", dark: "42618F") static let categoryDotDress = Color(hex: "A87560") static let categoryDotOuterwear = Color(hex: "7A9468") static let categoryDotShoes = Color(hex: "8B6914") @@ -48,14 +72,14 @@ extension Outfit: @retroactive Identifiable {} extension Color { init(hex: String) { - let scanner = Scanner(string: hex) - var rgb: UInt64 = 0 - scanner.scanHexInt64(&rgb) - self.init( - red: Double((rgb >> 16) & 0xFF) / 255, - green: Double((rgb >> 8) & 0xFF) / 255, - blue: Double(rgb & 0xFF) / 255 - ) + self.init(uiColor: UIColor(hex: hex)) + } + + /// Builds a colour that resolves per appearance, so it tracks the system theme automatically. + init(light: String, dark: String) { + self.init(uiColor: UIColor { traits in + traits.userInterfaceStyle == .dark ? UIColor(hex: dark) : UIColor(hex: light) + }) } var isBright: Bool { @@ -65,3 +89,17 @@ extension Color { return brightness > 0.5 } } + +extension UIColor { + convenience init(hex: String) { + let scanner = Scanner(string: hex) + var rgb: UInt64 = 0 + scanner.scanHexInt64(&rgb) + self.init( + red: CGFloat((rgb >> 16) & 0xFF) / 255, + green: CGFloat((rgb >> 8) & 0xFF) / 255, + blue: CGFloat(rgb & 0xFF) / 255, + alpha: 1 + ) + } +} From 567e524886858950d6676e9474f4c17264dacab5 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 8 Aug 2026 10:21:17 -0300 Subject: [PATCH 03/13] fix: raise card surfaces above the page in dark mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cards were mapped to surfaceContainerLowest, which is the darkest step of the M3 ramp, so in dark they sank behind the page and read as black holes. The app's design is white cards standing forward on a beige page; dark has to preserve that relationship rather than invert it. surfaceContainer is the role that means "elevated container" and sits above surface in dark, so cards move there — #2B261F against a #211D18 page. Light is unchanged: surfaceContainer is #FFFFFF, exactly what the cards already were. Co-Authored-By: Claude Opus 5 (1M context) --- .../worn/ui/components/AddItemFields.kt | 8 +- .../github/worn/ui/components/ClothingCard.kt | 2 +- .../worn/ui/components/EmptyStateView.kt | 2 +- .../worn/ui/components/ErrorContentView.kt | 2 +- .../github/worn/ui/components/OutfitCard.kt | 2 +- .../worn/ui/components/SelectionIndicator.kt | 2 +- .../com/github/worn/ui/components/WornChip.kt | 2 +- .../worn/ui/screen/CreateOutfitSheet.kt | 6 +- .../com/github/worn/ui/screen/GapsScreen.kt | 8 +- .../github/worn/ui/screen/ItemDetailSheet.kt | 4 +- .../worn/ui/screen/OutfitDetailSheet.kt | 2 +- .../github/worn/ui/screen/SettingsScreen.kt | 14 +-- .../com/github/worn/ui/screen/TryItScreen.kt | 12 +-- .../github/worn/ui/theme/WornColorScheme.kt | 15 ++- .../com/github/worn/ui/theme/WornTheme.kt | 1 + .../github/worn/ui/theme/WornTypography.kt | 99 +++++++++++++++++++ iosApp/iosApp/Theme/WornColors.swift | 2 +- 17 files changed, 144 insertions(+), 39 deletions(-) create mode 100644 composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTypography.kt diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt index 07feff8..7c1dafd 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt @@ -209,8 +209,8 @@ fun ItemNameField(value: String, onValueChange: (String) -> Unit, modifier: Modi ) }, colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, ), @@ -226,7 +226,7 @@ fun CategoryDropdown(selected: Category?, onSelected: (Category) -> Unit, modifi var expanded by remember { mutableStateOf(false) } Surface( shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = modifier.fillMaxWidth(), ) { @@ -425,7 +425,7 @@ fun SubcategoryDropdown(category: Category, selected: Subcategory?, onSelected: var expanded by remember { mutableStateOf(false) } Surface( shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = Modifier.fillMaxWidth(), ) { diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt index 8dd0a73..05033a6 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt @@ -76,7 +76,7 @@ private fun PhotoArea( Box(modifier = Modifier.fillMaxWidth().height(height)) { Surface( shape = photoShape, - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 4.dp, modifier = Modifier.fillMaxSize(), diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/EmptyStateView.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/EmptyStateView.kt index 6be6dcf..8426042 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/EmptyStateView.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/EmptyStateView.kt @@ -39,7 +39,7 @@ fun EmptyStateView( Box( modifier = Modifier.size(130.dp) .shadow(15.dp, CircleShape) - .background(MaterialTheme.colorScheme.surfaceContainerLowest, CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainer, CircleShape) .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape), contentAlignment = Alignment.Center, ) { diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt index 0a41ac8..3af74a0 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt @@ -67,7 +67,7 @@ fun ErrorContentView( Surface( onClick = onRetry, shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, ) { Text( text = stringResource(R.string.common_retry), diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt index a8f60b2..0fe57c8 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt @@ -66,7 +66,7 @@ fun OutfitCard( ) { Surface( shape = cardShape, - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke( 1.dp, if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant, diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionIndicator.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionIndicator.kt index 34fa321..6b9b513 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionIndicator.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionIndicator.kt @@ -31,7 +31,7 @@ fun SelectionIndicator( ) { Surface( shape = RoundedCornerShape(size / 2), - color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerLowest, + color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainer, border = if (isSelected) null else BorderStroke(1.5.dp, MaterialTheme.colorScheme.outlineVariant), modifier = modifier.size(size), ) { diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt index b73c56e..e48f0a8 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt @@ -29,7 +29,7 @@ fun WornChip( Surface( onClick = onClick, shape = chipShape, - color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerLowest, + color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainer, border = if (isActive) null else BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = modifier, ) { diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt index a7a56f8..4fdfe3f 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt @@ -169,8 +169,8 @@ private fun OutfitNameField(name: String, onNameChange: (String) -> Unit, modifi ) }, colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, ), @@ -261,7 +261,7 @@ private fun SelectableItemCell( private fun ItemThumbnail(item: ClothingItem) { Surface( shape = cellShape, - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, shadowElevation = 4.dp, modifier = Modifier.fillMaxSize(), ) { diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt index 80f0ee3..fae7220 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt @@ -363,7 +363,7 @@ private fun GapCard( Surface( onClick = onClick, shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -486,7 +486,7 @@ private fun DetailHeader(recommendation: GapRecommendation) { .fillMaxWidth() .height(140.dp) .clip(RoundedCornerShape(16.dp)) - .background(MaterialTheme.colorScheme.surfaceContainerLowest), + .background(MaterialTheme.colorScheme.surfaceContainer), ) { Icon( painter = painterResource(recommendation.mappedCategory.iconRes()), @@ -523,7 +523,7 @@ private fun DetailHeader(recommendation: GapRecommendation) { private fun DetailPairingInfo(recommendation: GapRecommendation, isAiMode: Boolean) { Surface( shape = RoundedCornerShape(8.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, modifier = Modifier.fillMaxWidth(), ) { Row( @@ -601,7 +601,7 @@ private fun DetailActions(onAddToWardrobe: () -> Unit, onDismiss: () -> Unit) { Surface( onClick = onDismiss, shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, modifier = Modifier.fillMaxWidth().testTag("gap_dismiss"), ) { Box( diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt index a80567a..244c0ea 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt @@ -158,7 +158,7 @@ private data class ItemDetailDimens(val isCompact: Boolean) { private fun ItemPhoto(item: ClothingItem, dims: ItemDetailDimens) { Surface( shape = RoundedCornerShape(dims.photoRadius), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 8.dp, modifier = Modifier.fillMaxWidth().height(dims.photoHeight), @@ -286,7 +286,7 @@ internal fun DetailActionButtons( Surface( onClick = onEdit, shape = RoundedCornerShape(24.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = Modifier.fillMaxWidth().height(buttonHeight).testTag(editTestTag), ) { diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt index 2f36ca2..4d1daf2 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt @@ -232,7 +232,7 @@ private fun OutfitItemCard( ) { Surface( shape = RoundedCornerShape(cornerRadius), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 8.dp, modifier = Modifier.size(size), diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt index 60b976f..cb1eaf8 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt @@ -300,7 +300,7 @@ private fun SettingsToggleCard( onClick = { onCheckedChange(!checked) }, enabled = enabled, shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, modifier = modifier, ) { Row( @@ -358,7 +358,7 @@ private fun SettingsCard( Surface( onClick = onClick, shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, modifier = modifier, ) { Row( @@ -398,7 +398,7 @@ private fun AboutCard() { Surface( shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, ) { Column { Row( @@ -447,7 +447,7 @@ private fun DonationCard() { Surface( shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, ) { Column(modifier = Modifier.padding(16.dp)) { Text( @@ -731,9 +731,9 @@ private fun ApiKeyTextField( } }, colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainer, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, disabledIndicatorColor = Color.Transparent, diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt index a407cf4..2997f2e 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt @@ -511,7 +511,7 @@ private fun AiEmptyContent( ) { Surface( shape = CircleShape, - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 8.dp, modifier = Modifier.size(circleSize), @@ -793,7 +793,7 @@ private fun UploadZone( Surface( onClick = onClick, shape = RoundedCornerShape(20.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.5.dp, MaterialTheme.colorScheme.outline), shadowElevation = 1.dp, modifier = Modifier.fillMaxWidth().height(height).testTag("try_it_upload_zone"), @@ -910,7 +910,7 @@ private fun ItemThumbnail(item: ClothingItem, size: Dp, onClick: () -> Unit) { Surface( onClick = onClick, shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 2.dp, modifier = Modifier.size(size), @@ -932,7 +932,7 @@ private fun CombinationsCard(count: Int, isCompact: Boolean) { Surface( shape = RoundedCornerShape(20.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 2.dp, modifier = Modifier.fillMaxWidth(), @@ -1115,7 +1115,7 @@ private fun PersonPhotoZone(personImage: ByteArray?, height: Dp, onClick: () -> Surface( onClick = onClick, shape = RoundedCornerShape(20.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.5.dp, MaterialTheme.colorScheme.outline), shadowElevation = 1.dp, modifier = Modifier.fillMaxWidth().height(height).testTag("try_on_person_zone"), @@ -1234,7 +1234,7 @@ private fun TryOnResultView(imageBytes: ByteArray, height: Dp) { ) Surface( shape = RoundedCornerShape(20.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, + color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 2.dp, modifier = Modifier.fillMaxWidth().height(height).testTag("try_on_result"), diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornColorScheme.kt b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornColorScheme.kt index 3b852e1..d61bdb9 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornColorScheme.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornColorScheme.kt @@ -59,10 +59,11 @@ internal val WornLightColorScheme = lightColorScheme( surfaceDim = Color(0xFFE4DDD3), surfaceBright = Color(0xFFFFFBF7), surfaceContainerLowest = Color(0xFFFFFFFF), - // ModalBottomSheet reads surfaceContainerLow for its default container; setting it here is - // what lets the sheets stop passing containerColor by hand. - surfaceContainerLow = Color(0xFFFAF6F1), - surfaceContainer = Color(0xFFF2EDE6), + surfaceContainerLow = Color(0xFFFFFFFF), + // Cards and tiles. White here, one step *above* the beige page — see the dark scheme for why + // this role rather than surfaceContainerLowest. + surfaceContainer = Color(0xFFFFFFFF), + // Chrome that sits behind content: the bottom bar and the filter chips. surfaceContainerHigh = Color(0xFFEDE8E1), surfaceContainerHighest = Color(0xFFE7E0D7), @@ -135,8 +136,12 @@ internal val WornDarkColorScheme = darkColorScheme( surfaceDim = Color(0xFF211D18), surfaceBright = Color(0xFF4A443C), - surfaceContainerLowest = Color(0xFF1A1713), + surfaceContainerLowest = Color(0xFF17140F), surfaceContainerLow = Color(0xFF262119), + // Cards and tiles — one step *above* the page, so they still read as standing forward the way + // the white-on-beige light scheme does. Mapping them to surfaceContainerLowest instead (the + // darkest step, which is what "card = white = brightest" naively translates to) sinks them + // into the background and reads as black holes. surfaceContainer = Color(0xFF2B261F), surfaceContainerHigh = Color(0xFF363029), surfaceContainerHighest = Color(0xFF423B32), diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt index 49ce2e4..05f9492 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt @@ -21,6 +21,7 @@ fun WornTheme( CompositionLocalProvider(LocalWornExtras provides extras) { MaterialTheme( colorScheme = colorScheme, + typography = WornTypography, content = content, ) } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTypography.kt b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTypography.kt new file mode 100644 index 0000000..4574518 --- /dev/null +++ b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTypography.kt @@ -0,0 +1,99 @@ +package com.github.worn.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +/** + * Worn's type scale. + * + * The sizes are the ones the app was already using at ~145 scattered `fontSize =` call sites, so + * adopting this changes no pixels — it just gives each one a name, a matching line height and a + * single place to change. The M3 role names are kept (rather than invented ones) so Material + * components that reach for `typography` on their own land on the right style too. + * + * `letterSpacing` on [Typography.headlineMedium] is the -0.5sp the screen titles already carried; + * [Typography.labelSmall] keeps the +0.5sp of the uppercase tab labels. + */ +internal val WornTypography = Typography( + // Large stat numbers. + displaySmall = TextStyle( + fontSize = 42.sp, + lineHeight = 50.sp, + fontWeight = FontWeight.Normal, + ), + + headlineLarge = TextStyle( + fontSize = 32.sp, + lineHeight = 40.sp, + fontWeight = FontWeight.SemiBold, + ), + // Screen titles: "Worn", "Your outfits", "What's missing", "Settings". + headlineMedium = TextStyle( + fontSize = 28.sp, + lineHeight = 36.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = (-0.5).sp, + ), + headlineSmall = TextStyle( + fontSize = 24.sp, + lineHeight = 32.sp, + fontWeight = FontWeight.SemiBold, + ), + + // Empty-state and sheet titles. + titleLarge = TextStyle( + fontSize = 22.sp, + lineHeight = 28.sp, + fontWeight = FontWeight.SemiBold, + ), + // Section headers. + titleMedium = TextStyle( + fontSize = 18.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.SemiBold, + ), + // Row titles and button labels. + titleSmall = TextStyle( + fontSize = 16.sp, + lineHeight = 22.sp, + fontWeight = FontWeight.SemiBold, + ), + + bodyLarge = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.Normal, + ), + bodyMedium = TextStyle( + fontSize = 15.sp, + lineHeight = 22.sp, + fontWeight = FontWeight.Normal, + ), + // Card names and descriptions — the most common style in the app. + bodySmall = TextStyle( + fontSize = 14.sp, + lineHeight = 20.sp, + fontWeight = FontWeight.Normal, + ), + + labelLarge = TextStyle( + fontSize = 13.sp, + lineHeight = 18.sp, + fontWeight = FontWeight.Medium, + ), + // Category labels and captions. + labelMedium = TextStyle( + fontSize = 12.sp, + lineHeight = 16.sp, + fontWeight = FontWeight.Medium, + ), + // Bottom-bar tab labels. + labelSmall = TextStyle( + fontSize = 10.sp, + lineHeight = 14.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 0.5.sp, + ), +) diff --git a/iosApp/iosApp/Theme/WornColors.swift b/iosApp/iosApp/Theme/WornColors.swift index c2b08bc..2ea70af 100644 --- a/iosApp/iosApp/Theme/WornColors.swift +++ b/iosApp/iosApp/Theme/WornColors.swift @@ -13,7 +13,7 @@ import Shared enum WornColors { // Backgrounds static let bgPage = Color(light: "F5F0EB", dark: "211D18") - static let bgCard = Color(light: "FFFFFF", dark: "1A1713") + static let bgCard = Color(light: "FFFFFF", dark: "2B261F") static let bgElevated = Color(light: "EDE8E1", dark: "363029") // Borders From 3fe7b6533a15d4fafaaffa348e35e44f72ab8a63 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 8 Aug 2026 10:32:36 -0300 Subject: [PATCH 04/13] feat: add a shared type scale and restore Dynamic Type on iOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android had 145 hardcoded `fontSize =` sites and zero uses of MaterialTheme.typography; iOS had 167 `.font(.system(size:))` and zero semantic text styles. The iOS side is the serious one: fixed point sizes do not respond to the system text-size setting, so Dynamic Type was entirely non-functional. Android gains a Typography built from the sizes already in use, so the scale names what was there rather than redesigning it. iOS maps onto the built-in text styles, which scale automatically — the existing point sizes already sat within a point or two of them, so the visual change is small and the accessibility gain is not. Where a size was threaded through a composable as a raw TextUnit/CGFloat (PropertyRow, ItemDetailSheet, OutfitDetailSheet, TryItTitle) the parameter now carries a TextStyle/Font, so responsive variants pick from the scale instead of from loose literals. Two follow-on details: - The tab bar is capped at accessibility1. It is a fixed-height row of five equal slots and cannot grow the way flowing text can; the rest of the app is uncapped. - SF Symbol sizes are deliberately left as fixed glyph sizes. Previews move to PhonePreview/TabletPreview multipreview annotations, which carry a dark variant each, so dark mode is now visible in the IDE — that is where a missed colour role shows up. detekt.yml lists the aliases so the TooManyFunctions exemption keeps applying. Note: explicit line heights slightly tighten vertical rhythm versus the previous font-metric defaults. Co-Authored-By: Claude Opus 5 (1M context) --- .../worn/ui/components/AddItemFields.kt | 31 +++-- .../worn/ui/components/AiLockedSheet.kt | 12 +- .../github/worn/ui/components/ClothingCard.kt | 4 +- .../worn/ui/components/CropEditorDialog.kt | 22 ++-- .../worn/ui/components/CropPhotoButton.kt | 2 +- .../ui/components/DeleteConfirmationDialog.kt | 13 +- .../worn/ui/components/EmptyStateView.kt | 17 +-- .../worn/ui/components/ErrorContentView.kt | 12 +- .../github/worn/ui/components/OutfitCard.kt | 7 +- .../github/worn/ui/components/PropertyRow.kt | 30 +++-- .../worn/ui/components/SelectionHeader.kt | 14 ++- .../worn/ui/components/SelectionIndicator.kt | 8 +- .../worn/ui/components/SheetDragHandle.kt | 8 +- .../worn/ui/components/WornBottomBar.kt | 17 ++- .../com/github/worn/ui/components/WornChip.kt | 11 +- .../worn/ui/components/WornGradientButton.kt | 12 +- .../com/github/worn/ui/screen/AddItemSheet.kt | 18 +-- .../worn/ui/screen/CreateOutfitSheet.kt | 19 ++- .../com/github/worn/ui/screen/GapsScreen.kt | 52 ++++---- .../github/worn/ui/screen/ItemDetailSheet.kt | 112 ++++++++++++------ .../worn/ui/screen/OutfitDetailSheet.kt | 49 +++++--- .../github/worn/ui/screen/OutfitsScreen.kt | 30 +++-- .../github/worn/ui/screen/SettingsScreen.kt | 107 ++++++++++------- .../com/github/worn/ui/screen/TryItScreen.kt | 89 +++++++------- .../github/worn/ui/screen/WardrobeScreen.kt | 36 +++--- .../com/github/worn/ui/theme/WornPreviews.kt | 27 +++++ detekt.yml | 6 +- iosApp/iosApp/Components/AiLockedSheet.swift | 4 +- iosApp/iosApp/Components/ClothingCard.swift | 4 +- iosApp/iosApp/Components/CropEditorView.swift | 6 +- .../iosApp/Components/CropPhotoButton.swift | 2 +- iosApp/iosApp/Components/EmptyStateView.swift | 4 +- .../iosApp/Components/ErrorContentView.swift | 4 +- iosApp/iosApp/Components/PropertyRow.swift | 6 +- .../iosApp/Components/SelectionHeader.swift | 6 +- iosApp/iosApp/Components/WornBottomBar.swift | 6 +- iosApp/iosApp/Components/WornChip.swift | 2 +- .../Components/WornGradientButton.swift | 2 +- iosApp/iosApp/Screens/AddItemSheet.swift | 31 +++-- iosApp/iosApp/Screens/CreateOutfitSheet.swift | 20 +++- iosApp/iosApp/Screens/GapsScreen.swift | 54 ++++++--- iosApp/iosApp/Screens/ItemDetailSheet.swift | 34 ++++-- iosApp/iosApp/Screens/OutfitDetailSheet.swift | 26 ++-- iosApp/iosApp/Screens/OutfitsScreen.swift | 36 +++++- iosApp/iosApp/Screens/SettingsScreen.swift | 80 ++++++++----- iosApp/iosApp/Screens/TryItScreen.swift | 47 ++++---- iosApp/iosApp/Screens/WardrobeScreen.swift | 42 ++++++- 47 files changed, 745 insertions(+), 436 deletions(-) create mode 100644 composeApp/src/main/kotlin/com/github/worn/ui/theme/WornPreviews.kt diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt index 7c1dafd..0fbcdcf 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt @@ -123,7 +123,7 @@ fun PhotoUploadZone( Text( text = stringResource(R.string.add_item_photo_hint), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, ) } @@ -160,7 +160,7 @@ fun RemoveBackgroundToggle( Text( text = stringResource(R.string.add_item_remove_background), color = MaterialTheme.colorScheme.onSurface, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, modifier = Modifier.weight(1f), ) @@ -185,11 +185,16 @@ fun AiBadge(onClick: () -> Unit = {}, modifier: Modifier = Modifier) { modifier = modifier, ) { Row(modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)) { - Text("✦ ", color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.SemiBold) + Text( + "✦ ", + color = Color.White, + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + ) Text( stringResource(R.string.add_item_ai_badge), color = Color.White, - fontSize = 12.sp, + style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, ) } @@ -205,7 +210,7 @@ fun ItemNameField(value: String, onValueChange: (String) -> Unit, modifier: Modi Text( stringResource(R.string.add_item_name_hint), color = MaterialTheme.wornExtras.iconMuted, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, ) }, colors = TextFieldDefaults.colors( @@ -255,7 +260,7 @@ fun CategoryDropdown(selected: Category?, onSelected: (Category) -> Unit, modifi } else { MaterialTheme.wornExtras.iconMuted }, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f), ) Icon( @@ -304,7 +309,7 @@ private fun CategoryOptionList(onSelected: (Category) -> Unit) { Text( text = category.displayName(), color = MaterialTheme.colorScheme.onSurface, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, ) } @@ -323,7 +328,7 @@ fun ColorSection(selectedColors: Set, onToggle: (String) -> Unit) { Text( stringResource(R.string.label_color), color = MaterialTheme.colorScheme.onSurface, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.SemiBold, ) FlowRow( @@ -366,7 +371,7 @@ fun SeasonSection(selectedSeasons: Set, onToggle: (Season) -> Unit) { Text( stringResource(R.string.label_season), color = MaterialTheme.colorScheme.onSurface, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.SemiBold, ) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { @@ -463,7 +468,7 @@ private fun DropdownHeader(text: String, hasSelection: Boolean, expanded: Boolea Text( text = text, color = if (hasSelection) MaterialTheme.colorScheme.onSurface else MaterialTheme.wornExtras.iconMuted, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f), ) Icon( @@ -485,7 +490,7 @@ private fun SubcategoryOptionList(options: List, onSelected: (Subca Text( text = subcategory.displayName(), color = MaterialTheme.colorScheme.onSurface, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, modifier = Modifier .fillMaxWidth() @@ -506,7 +511,7 @@ fun FitSection(selected: Fit?, onSelected: (Fit?) -> Unit) { Text( stringResource(R.string.label_fit), color = MaterialTheme.colorScheme.onSurface, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.SemiBold, ) FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { @@ -528,7 +533,7 @@ fun MaterialSection(selected: Material?, onSelected: (Material?) -> Unit) { Text( stringResource(R.string.label_material), color = MaterialTheme.colorScheme.onSurface, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.SemiBold, ) FlowRow( diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/AiLockedSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/AiLockedSheet.kt index 8ec2fe0..833db6d 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/AiLockedSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/AiLockedSheet.kt @@ -32,12 +32,13 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R import com.github.worn.ui.exposeTestTagsAsResourceId +import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.SheetPreview +import com.github.worn.ui.theme.TabletPreview @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -86,14 +87,14 @@ internal fun AiLockedContent( Text( text = stringResource(R.string.ai_locked_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = 22.sp, + style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Medium, ) Text( text = stringResource(R.string.ai_locked_description), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Center, lineHeight = 21.sp, modifier = Modifier.widthIn(max = 280.dp), @@ -119,14 +120,15 @@ private fun SettingsCta(onClick: () -> Unit) { ) } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun AiLockedPhonePreview() { SheetPreview { AiLockedContent() } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun AiLockedTabletPreview() { SheetPreview { AiLockedContent() } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt index 05033a6..a19c60c 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt @@ -108,7 +108,7 @@ private fun ItemInfo(item: ClothingItem) { Text( text = item.name, color = MaterialTheme.colorScheme.onSurface, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, // AI-generated names can run long; left unbounded they push the category row down and // misalign the cards next to them in the grid row. @@ -128,7 +128,7 @@ private fun ItemInfo(item: ClothingItem) { Text( text = item.category.displayLabel(), color = MaterialTheme.wornExtras.textMuted, - fontSize = 12.sp, + style = MaterialTheme.typography.labelMedium, ) } } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/CropEditorDialog.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/CropEditorDialog.kt index 10eadc6..e091b55 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/CropEditorDialog.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/CropEditorDialog.kt @@ -46,13 +46,14 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.github.worn.R import com.github.worn.ui.exposeTestTagsAsResourceId +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme import com.github.worn.ui.theme.wornExtras import com.github.worn.ui.util.cropToJpeg @@ -269,13 +270,16 @@ private fun CropEditorTopBar(canApply: Boolean, onCancel: () -> Unit, onApply: ( modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), ) { TextButton(onClick = onCancel, modifier = Modifier.testTag("crop_editor_cancel")) { - Text(stringResource(R.string.common_cancel), color = Color.White, fontSize = 16.sp) + Text( + stringResource(R.string.common_cancel), + color = Color.White, + style = MaterialTheme.typography.titleSmall, + ) } Text( text = stringResource(R.string.crop_title), color = Color.White, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleSmall, ) TextButton( onClick = onApply, @@ -285,8 +289,7 @@ private fun CropEditorTopBar(canApply: Boolean, onCancel: () -> Unit, onApply: ( Text( text = stringResource(R.string.crop_apply), color = if (canApply) MaterialTheme.colorScheme.primary else MaterialTheme.wornExtras.textMuted, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleSmall, ) } } @@ -299,7 +302,7 @@ private fun CropEditorBottomBar(enabled: Boolean, onReset: () -> Unit) { modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), ) { TextButton(onClick = onReset, enabled = enabled, modifier = Modifier.testTag("crop_editor_reset")) { - Text(stringResource(R.string.crop_reset), color = Color.White, fontSize = 15.sp) + Text(stringResource(R.string.crop_reset), color = Color.White, style = MaterialTheme.typography.bodyMedium) } } } @@ -378,14 +381,15 @@ private fun previewBitmap(): ImageBitmap = private const val PREVIEW_WIDTH = 600 private const val PREVIEW_HEIGHT = 800 -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun CropEditorContentPhonePreview() { WornTheme { CropEditorContent(bitmap = previewBitmap()) } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun CropEditorContentTabletPreview() { WornTheme { CropEditorContent(bitmap = previewBitmap()) } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/CropPhotoButton.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/CropPhotoButton.kt index 799345e..41e45be 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/CropPhotoButton.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/CropPhotoButton.kt @@ -42,7 +42,7 @@ fun CropPhotoButton( Text( text = stringResource(R.string.crop_photo_button), color = MaterialTheme.colorScheme.onSurface, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, ) } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/DeleteConfirmationDialog.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/DeleteConfirmationDialog.kt index be3c14a..b15d8e8 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/DeleteConfirmationDialog.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/DeleteConfirmationDialog.kt @@ -12,10 +12,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme @Composable @@ -32,14 +33,14 @@ fun DeleteConfirmationDialog( Text( title, fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, + style = MaterialTheme.typography.titleLarge, ) }, text = { Text( message, color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, lineHeight = 22.sp, ) }, @@ -70,7 +71,7 @@ fun DeleteConfirmationDialog( ) } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun DeleteConfirmationDialogPhonePreview() { WornTheme { @@ -84,7 +85,7 @@ private fun DeleteConfirmationDialogPhonePreview() { } } -@Preview(showSystemUi = true, device = "spec:width=800dp,height=1280dp,dpi=240") +@TabletPreview @Composable private fun DeleteConfirmationDialogTabletPreview() { WornTheme { @@ -97,3 +98,5 @@ private fun DeleteConfirmationDialogTabletPreview() { ) } } + + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/EmptyStateView.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/EmptyStateView.kt index 8426042..282e601 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/EmptyStateView.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/EmptyStateView.kt @@ -18,9 +18,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme @Composable @@ -49,15 +50,14 @@ fun EmptyStateView( Text( title, color = MaterialTheme.colorScheme.onSurface, - fontSize = 24.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.headlineSmall, letterSpacing = (-0.5).sp, ) Spacer(Modifier.height(24.dp)) Text( description, color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, lineHeight = 22.sp, textAlign = TextAlign.Center, ) @@ -68,13 +68,13 @@ fun EmptyStateView( } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun EmptyStatePhonePreview() { WornTheme { EmptyStateView( icon = { - Text("👕", fontSize = 42.sp) + Text("👕", style = MaterialTheme.typography.displaySmall) }, title = "Your wardrobe is empty", description = "Add your first item to get started", @@ -82,16 +82,17 @@ private fun EmptyStatePhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun EmptyStateTabletPreview() { WornTheme { EmptyStateView( icon = { - Text("👕", fontSize = 42.sp) + Text("👕", style = MaterialTheme.typography.displaySmall) }, title = "Your wardrobe is empty", description = "Add your first item to get started", ) } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt index 3af74a0..525fae3 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt @@ -24,10 +24,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme @Composable @@ -59,7 +60,7 @@ fun ErrorContentView( Text( text = message, color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, lineHeight = 20.sp, textAlign = TextAlign.Center, ) @@ -72,7 +73,7 @@ fun ErrorContentView( Text( text = stringResource(R.string.common_retry), color = retryButtonColor, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp), ) @@ -80,7 +81,7 @@ fun ErrorContentView( } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun ErrorContentViewPhonePreview() { WornTheme { @@ -92,7 +93,7 @@ private fun ErrorContentViewPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun ErrorContentViewTabletPreview() { WornTheme { @@ -104,3 +105,4 @@ private fun ErrorContentViewTabletPreview() { ) } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt index 0fe57c8..9a36d1b 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt @@ -137,7 +137,7 @@ private fun ItemCountBadge(outfit: Outfit) { Text( text = stringResource(R.string.outfit_detail_items_count, outfit.itemIds.size), color = MaterialTheme.colorScheme.onPrimary, - fontSize = 11.sp, + style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), ) @@ -155,8 +155,7 @@ private fun BottomRow(outfit: Outfit) { Text( text = outfit.name, color = MaterialTheme.colorScheme.onSurface, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleSmall, // Auto-generated names concatenate every item, so they can outgrow the card. maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -164,7 +163,7 @@ private fun BottomRow(outfit: Outfit) { Text( text = formatDate(outfit.createdAt), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, + style = MaterialTheme.typography.labelMedium, ) } Icon( diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/PropertyRow.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/PropertyRow.kt index 18319e0..295978e 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/PropertyRow.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/PropertyRow.kt @@ -10,18 +10,18 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme @Composable fun PropertyRow( label: String, value: String, - fontSize: TextUnit, + textStyle: TextStyle, modifier: Modifier = Modifier, ) { Row( @@ -32,31 +32,37 @@ fun PropertyRow( Text( label, color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = fontSize, + style = textStyle, + fontWeight = FontWeight.Medium, + ) + Text( + value, + color = MaterialTheme.colorScheme.onSurface, + style = textStyle, fontWeight = FontWeight.Medium, ) - Text(value, color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize, fontWeight = FontWeight.Medium) } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun PropertyRowPhonePreview() { WornTheme { Column(modifier = Modifier.padding(16.dp)) { - PropertyRow(label = "Season", value = "Summer", fontSize = 15.sp) - PropertyRow(label = "Fit", value = "Regular", fontSize = 15.sp) + PropertyRow(label = "Season", value = "Summer", textStyle = MaterialTheme.typography.bodyMedium) + PropertyRow(label = "Fit", value = "Regular", textStyle = MaterialTheme.typography.bodyMedium) } } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun PropertyRowTabletPreview() { WornTheme { Column(modifier = Modifier.padding(16.dp)) { - PropertyRow(label = "Season", value = "Summer", fontSize = 15.sp) - PropertyRow(label = "Fit", value = "Regular", fontSize = 15.sp) + PropertyRow(label = "Season", value = "Summer", textStyle = MaterialTheme.typography.bodyMedium) + PropertyRow(label = "Fit", value = "Regular", textStyle = MaterialTheme.typography.bodyMedium) } } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionHeader.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionHeader.kt index a882189..57947aa 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionHeader.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionHeader.kt @@ -23,10 +23,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme @Composable @@ -46,7 +47,7 @@ fun SelectionHeader( Text( text = pluralStringResource(R.plurals.selected_count, count, count), color = MaterialTheme.colorScheme.onSurface, - fontSize = 28.sp, + style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Medium, letterSpacing = (-0.8).sp, ) @@ -61,7 +62,7 @@ fun SelectionHeader( stringResource(R.string.common_delete), color = Color.White, fontWeight = FontWeight.SemiBold, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, ) } } @@ -69,14 +70,14 @@ fun SelectionHeader( Text( text = stringResource(R.string.common_cancel), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, modifier = Modifier.clickable(onClick = onCancel), ) } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun SelectionHeaderPhonePreview() { WornTheme { @@ -84,10 +85,11 @@ private fun SelectionHeaderPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun SelectionHeaderTabletPreview() { WornTheme { SelectionHeader(count = 5, onCancel = {}, onDelete = {}) } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionIndicator.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionIndicator.kt index 6b9b513..97f6a0b 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionIndicator.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionIndicator.kt @@ -17,9 +17,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme @Composable @@ -48,7 +49,7 @@ fun SelectionIndicator( } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun SelectionIndicatorPhonePreview() { WornTheme { @@ -65,7 +66,7 @@ private fun SelectionIndicatorPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun SelectionIndicatorTabletPreview() { WornTheme { @@ -77,3 +78,4 @@ private fun SelectionIndicatorTabletPreview() { } } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/SheetDragHandle.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/SheetDragHandle.kt index 1fe966a..a911416 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/SheetDragHandle.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/SheetDragHandle.kt @@ -13,8 +13,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme import com.github.worn.ui.theme.wornExtras @@ -37,7 +38,7 @@ fun SheetDragHandle( } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun SheetDragHandlePhonePreview() { WornTheme { @@ -47,7 +48,7 @@ private fun SheetDragHandlePhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun SheetDragHandleTabletPreview() { WornTheme { @@ -56,3 +57,4 @@ private fun SheetDragHandleTabletPreview() { } } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt index 54b8148..71fa7fa 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt @@ -146,16 +146,13 @@ private fun TabItem( } Text( text = label, - style = TextStyle( - color = if (isActive) { - MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - fontSize = 10.sp, - fontWeight = FontWeight.SemiBold, - letterSpacing = 0.5.sp, - ), + // labelSmall already carries the 10sp/SemiBold/+0.5sp tracking these labels used. + style = MaterialTheme.typography.labelSmall, + color = if (isActive) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt index e48f0a8..2889ecc 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt @@ -12,9 +12,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme private val chipShape = RoundedCornerShape(20.dp) @@ -36,14 +37,13 @@ fun WornChip( Text( text = label, color = if (isActive) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.labelLarge, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), ) } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun WornChipPhonePreview() { WornTheme { @@ -55,7 +55,7 @@ private fun WornChipPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun WornChipTabletPreview() { WornTheme { @@ -66,3 +66,4 @@ private fun WornChipTabletPreview() { } } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornGradientButton.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornGradientButton.kt index b650b74..3a0e828 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornGradientButton.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornGradientButton.kt @@ -22,11 +22,12 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.ui.theme.LocalWornExtras +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme import com.github.worn.ui.theme.wornExtras @@ -102,16 +103,16 @@ fun WornGradientButton( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { icon() - Text(text, color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) + Text(text, color = Color.White, style = MaterialTheme.typography.titleSmall) } } else { - Text(text, color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) + Text(text, color = Color.White, style = MaterialTheme.typography.titleSmall) } } } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun WornGradientButtonPhonePreview() { WornTheme { @@ -121,7 +122,7 @@ private fun WornGradientButtonPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun WornGradientButtonTabletPreview() { WornTheme { @@ -130,3 +131,4 @@ private fun WornGradientButtonTabletPreview() { } } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/AddItemSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/AddItemSheet.kt index 292242d..8960c65 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/AddItemSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/AddItemSheet.kt @@ -45,7 +45,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R @@ -72,7 +71,9 @@ import com.github.worn.ui.components.SeasonSection import com.github.worn.ui.components.SheetDragHandle import com.github.worn.ui.components.SubcategoryDropdown import com.github.worn.ui.exposeTestTagsAsResourceId +import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.SheetPreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.util.decodePreviewImage import com.github.worn.ui.util.readImageBytes import com.github.worn.ui.util.rememberCameraCapture @@ -356,8 +357,7 @@ private fun AddItemFormContent( Text( text = stringResource(if (isEditing) R.string.add_item_title_edit else R.string.add_item_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = 24.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.headlineSmall, letterSpacing = (-0.5).sp, ) PhotoUploadZone( @@ -440,7 +440,7 @@ private fun PhotoSourceDialog( ) { Icon(Icons.Outlined.CameraAlt, contentDescription = null, modifier = Modifier.size(24.dp)) Spacer(Modifier.width(12.dp)) - Text(stringResource(R.string.add_item_take_photo), fontSize = 16.sp) + Text(stringResource(R.string.add_item_take_photo), style = MaterialTheme.typography.titleSmall) } } TextButton( @@ -454,7 +454,10 @@ private fun PhotoSourceDialog( ) { Icon(Icons.Outlined.PhotoLibrary, contentDescription = null, modifier = Modifier.size(24.dp)) Spacer(Modifier.width(12.dp)) - Text(stringResource(R.string.add_item_choose_gallery), fontSize = 16.sp) + Text( + stringResource(R.string.add_item_choose_gallery), + style = MaterialTheme.typography.titleSmall, + ) } } } @@ -470,14 +473,15 @@ private inline fun toggleInSet(item: T, current: Set, update: (Set) -> update(if (item in current) current - item else current + item) } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun AddItemFormPhonePreview() { SheetPreview { AddItemForm() } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun AddItemFormTabletPreview() { SheetPreview { AddItemForm() } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt index 4fdfe3f..cea2460 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R @@ -64,7 +63,9 @@ import com.github.worn.ui.components.SheetDragHandle import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients import com.github.worn.ui.exposeTestTagsAsResourceId +import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.SheetPreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.wornExtras @OptIn(ExperimentalMaterial3Api::class) @@ -130,8 +131,7 @@ internal fun CreateOutfitForm( Text( text = stringResource(if (isEditing) R.string.create_outfit_title_edit else R.string.create_outfit_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = 24.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.headlineSmall, letterSpacing = (-0.5).sp, ) OutfitNameField( @@ -165,7 +165,7 @@ private fun OutfitNameField(name: String, onNameChange: (String) -> Unit, modifi Text( stringResource(R.string.create_outfit_name_hint), color = MaterialTheme.wornExtras.iconMuted, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, ) }, colors = TextFieldDefaults.colors( @@ -191,15 +191,13 @@ private fun SelectItemsHeader(selectedCount: Int) { Text( text = stringResource(R.string.create_outfit_select_items), color = MaterialTheme.colorScheme.onSurface, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleSmall, ) if (selectedCount > 0) { Text( text = pluralStringResource(R.plurals.selected_count, selectedCount, selectedCount), color = MaterialTheme.colorScheme.primary, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.labelLarge, ) } } @@ -305,7 +303,7 @@ private val previewItems = listOf( ClothingItem("6", "Chinos", Category.BOTTOM, listOf("khaki"), photoPath = "", createdAt = 0), ) -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun CreateOutfitFormPhonePreview() { SheetPreview { @@ -316,7 +314,7 @@ private fun CreateOutfitFormPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun CreateOutfitFormTabletPreview() { SheetPreview { @@ -326,3 +324,4 @@ private fun CreateOutfitFormTabletPreview() { ) } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt index fae7220..94aa548 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt @@ -54,7 +54,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -76,6 +75,8 @@ import com.github.worn.ui.components.displayLabel import com.github.worn.ui.components.displayName import com.github.worn.ui.components.iconRes import com.github.worn.ui.exposeTestTagsAsResourceId +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme import com.github.worn.ui.theme.wornExtras @@ -191,14 +192,13 @@ private fun GapsScaffold( Text( text = stringResource(R.string.gaps_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = 28.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.headlineMedium, letterSpacing = (-0.5).sp, ) Text( text = stringResource(R.string.gaps_subtitle), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, ) Spacer(Modifier.height(20.dp)) } @@ -261,14 +261,13 @@ private fun CompleteContent() { Text( text = stringResource(R.string.gaps_complete_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = 18.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleMedium, ) Spacer(Modifier.height(8.dp)) Text( text = stringResource(R.string.gaps_complete_description), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, lineHeight = 20.sp, ) } @@ -323,13 +322,12 @@ private fun GapsBanner(isAiMode: Boolean, onClick: () -> Unit) { Text( text = stringResource(titleRes), color = Color.White, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleSmall, ) Text( text = stringResource(subtitleRes), color = Color.White.copy(alpha = 0.8f), - fontSize = 13.sp, + style = MaterialTheme.typography.labelLarge, ) } Spacer(Modifier.width(12.dp)) @@ -348,8 +346,7 @@ private fun SectionLabel(text: String) { Text( text = text.uppercase(), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.labelMedium, letterSpacing = 0.5.sp, ) } @@ -375,7 +372,7 @@ private fun GapCard( Text( text = recommendation.itemName, color = MaterialTheme.colorScheme.onSurface, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, ) Text( @@ -385,7 +382,7 @@ private fun GapCard( stringResource(R.string.gaps_pairing_common) }, color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, + style = MaterialTheme.typography.labelMedium, ) } Icon( @@ -499,8 +496,7 @@ private fun DetailHeader(recommendation: GapRecommendation) { Text( text = recommendation.itemName, color = MaterialTheme.colorScheme.onSurface, - fontSize = 22.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleLarge, ) Spacer(Modifier.height(4.dp)) Row(verticalAlignment = Alignment.CenterVertically) { @@ -514,7 +510,7 @@ private fun DetailHeader(recommendation: GapRecommendation) { Text( text = recommendation.mappedCategory.displayLabel(), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, ) } } @@ -544,7 +540,7 @@ private fun DetailPairingInfo(recommendation: GapRecommendation, isAiMode: Boole stringResource(R.string.gaps_pairing_common) }, color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 13.sp, + style = MaterialTheme.typography.labelLarge, ) } } @@ -585,8 +581,13 @@ private fun DetailRow(label: String, value: String) { modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, ) { - Text(label, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) - Text(value, color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, fontWeight = FontWeight.Medium) + Text(label, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall) + Text( + value, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Medium, + ) } } @@ -611,7 +612,7 @@ private fun DetailActions(onAddToWardrobe: () -> Unit, onDismiss: () -> Unit) { Text( stringResource(R.string.gaps_dismiss), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, ) } @@ -642,7 +643,7 @@ private fun GapRecommendation.toPreFilledItem() = createdAt = 0L, ) -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun GapsScreenPhonePreview() { WornTheme { @@ -656,7 +657,7 @@ private fun GapsScreenPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun GapsScreenTabletPreview() { WornTheme { @@ -671,7 +672,7 @@ private fun GapsScreenTabletPreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun GapsScreenCompletePreview() { WornTheme { @@ -679,7 +680,7 @@ private fun GapsScreenCompletePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun GapsScreenErrorPreview() { WornTheme { @@ -692,3 +693,4 @@ private fun GapsScreenErrorPreview() { ) } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt index 244c0ea..f8eee61 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt @@ -41,10 +41,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R @@ -62,7 +61,9 @@ import com.github.worn.ui.components.displayLabel import com.github.worn.ui.components.displayName import com.github.worn.ui.components.dotColor import com.github.worn.ui.exposeTestTagsAsResourceId +import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.SheetPreview +import com.github.worn.ui.theme.TabletPreview @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -101,7 +102,7 @@ internal fun ItemDetailContent( onDelete: (String) -> Unit = {}, showActions: Boolean = true, ) { - val dims = ItemDetailDimens(isCompact) + val dims = itemDetailDimens(isCompact) var showDeleteDialog by remember { mutableStateOf(false) } Column( @@ -115,15 +116,15 @@ internal fun ItemDetailContent( verticalArrangement = Arrangement.spacedBy(dims.sectionGap), ) { ItemPhoto(item = item, dims = dims) - ItemNameGroup(item = item, nameSize = dims.nameSize) + ItemNameGroup(item = item, nameStyle = dims.nameStyle) HorizontalDivider() - ItemProperties(item = item, fontSize = dims.propFontSize, gap = dims.propGap) + ItemProperties(item = item, textStyle = dims.propStyle, gap = dims.propGap) if (showActions) { DetailActionButtons( editLabel = stringResource(R.string.item_detail_edit), deleteLabel = stringResource(R.string.item_detail_delete), buttonHeight = dims.buttonHeight, - buttonFontSize = dims.buttonFontSize, + buttonStyle = dims.buttonStyle, onEdit = { onEdit(item) }, onDelete = { showDeleteDialog = true }, editTestTag = "item_detail_edit", @@ -141,18 +142,48 @@ internal fun ItemDetailContent( } } -private data class ItemDetailDimens(val isCompact: Boolean) { - val contentPadding: Dp = if (isCompact) 24.dp else 32.dp - val sectionGap: Dp = if (isCompact) 20.dp else 24.dp - val photoHeight: Dp = if (isCompact) 280.dp else 360.dp - val photoRadius: Dp = if (isCompact) 20.dp else 24.dp - val nameSize: TextUnit = if (isCompact) 22.sp else 26.sp - val propFontSize: TextUnit = if (isCompact) 14.sp else 15.sp - val propGap: Dp = if (isCompact) 14.dp else 16.dp - val buttonHeight: Dp = if (isCompact) 48.dp else 52.dp - val buttonFontSize: TextUnit = if (isCompact) 15.sp else 16.sp - val placeholderIconSize: Dp = if (isCompact) 64.dp else 80.dp -} +private data class ItemDetailDimens( + val contentPadding: Dp, + val sectionGap: Dp, + val photoHeight: Dp, + val photoRadius: Dp, + val nameStyle: TextStyle, + val propStyle: TextStyle, + val propGap: Dp, + val buttonHeight: Dp, + val buttonStyle: TextStyle, + val placeholderIconSize: Dp, +) + +/** + * Composable rather than a plain constructor so the text styles come from the shared type scale + * instead of loose `sp` literals; only the spacing still varies by raw dimension. + */ +@Composable +private fun itemDetailDimens(isCompact: Boolean): ItemDetailDimens = ItemDetailDimens( + contentPadding = if (isCompact) 24.dp else 32.dp, + sectionGap = if (isCompact) 20.dp else 24.dp, + photoHeight = if (isCompact) 280.dp else 360.dp, + photoRadius = if (isCompact) 20.dp else 24.dp, + nameStyle = if (isCompact) { + MaterialTheme.typography.titleLarge + } else { + MaterialTheme.typography.headlineSmall + }, + propStyle = if (isCompact) { + MaterialTheme.typography.bodySmall + } else { + MaterialTheme.typography.bodyMedium + }, + propGap = if (isCompact) 14.dp else 16.dp, + buttonHeight = if (isCompact) 48.dp else 52.dp, + buttonStyle = if (isCompact) { + MaterialTheme.typography.bodyMedium + } else { + MaterialTheme.typography.titleSmall + }, + placeholderIconSize = if (isCompact) 64.dp else 80.dp, +) @Composable private fun ItemPhoto(item: ClothingItem, dims: ItemDetailDimens) { @@ -173,12 +204,12 @@ private fun ItemPhoto(item: ClothingItem, dims: ItemDetailDimens) { } @Composable -private fun ItemNameGroup(item: ClothingItem, nameSize: TextUnit) { +private fun ItemNameGroup(item: ClothingItem, nameStyle: TextStyle) { Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { Text( text = item.name, color = MaterialTheme.colorScheme.onSurface, - fontSize = nameSize, + style = nameStyle, fontWeight = FontWeight.SemiBold, ) Row( @@ -194,7 +225,7 @@ private fun ItemNameGroup(item: ClothingItem, nameSize: TextUnit) { Text( text = item.category.displayLabel(), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, ) } } @@ -211,10 +242,10 @@ private fun HorizontalDivider() { } @Composable -private fun ItemProperties(item: ClothingItem, fontSize: TextUnit, gap: Dp) { +private fun ItemProperties(item: ClothingItem, textStyle: TextStyle, gap: Dp) { Column(verticalArrangement = Arrangement.spacedBy(gap)) { if (item.colors.isNotEmpty()) { - ColorPropertyRow(item = item, fontSize = fontSize) + ColorPropertyRow(item = item, textStyle = textStyle) } if (item.seasons.isNotEmpty()) { val seasonText = if (item.seasons.size == Season.entries.size) { @@ -222,26 +253,30 @@ private fun ItemProperties(item: ClothingItem, fontSize: TextUnit, gap: Dp) { } else { item.seasons.map { it.displayName() }.joinToString(", ") } - PropertyRow(label = stringResource(R.string.label_season), value = seasonText, fontSize = fontSize) + PropertyRow(label = stringResource(R.string.label_season), value = seasonText, textStyle = textStyle) } item.fit?.let { - PropertyRow(label = stringResource(R.string.label_fit), value = it.displayName(), fontSize = fontSize) + PropertyRow(label = stringResource(R.string.label_fit), value = it.displayName(), textStyle = textStyle) } item.subcategory?.let { PropertyRow( label = stringResource(R.string.label_subcategory), value = it.displayName(), - fontSize = fontSize, + textStyle = textStyle, ) } item.material?.let { - PropertyRow(label = stringResource(R.string.label_material), value = it.displayName(), fontSize = fontSize) + PropertyRow( + label = stringResource(R.string.label_material), + value = it.displayName(), + textStyle = textStyle, + ) } } } @Composable -private fun ColorPropertyRow(item: ClothingItem, fontSize: TextUnit) { +private fun ColorPropertyRow(item: ClothingItem, textStyle: TextStyle) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -250,7 +285,7 @@ private fun ColorPropertyRow(item: ClothingItem, fontSize: TextUnit) { Text( stringResource(R.string.label_color), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = fontSize, + style = textStyle, fontWeight = FontWeight.Medium, ) Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { @@ -263,7 +298,7 @@ private fun ColorPropertyRow(item: ClothingItem, fontSize: TextUnit) { Text( text = item.colors.joinToString(", ") { it.replaceFirstChar(Char::uppercase) }, color = MaterialTheme.colorScheme.onSurface, - fontSize = fontSize, + style = textStyle, fontWeight = FontWeight.Medium, ) } @@ -276,7 +311,7 @@ internal fun DetailActionButtons( editLabel: String, deleteLabel: String, buttonHeight: Dp, - buttonFontSize: TextUnit, + buttonStyle: TextStyle, onEdit: () -> Unit, onDelete: () -> Unit, editTestTag: String, @@ -293,7 +328,7 @@ internal fun DetailActionButtons( Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { Text( editLabel, color = MaterialTheme.colorScheme.onSurface, - fontSize = buttonFontSize, fontWeight = FontWeight.SemiBold, + style = buttonStyle, fontWeight = FontWeight.SemiBold, ) } } @@ -304,7 +339,7 @@ internal fun DetailActionButtons( modifier = Modifier.fillMaxWidth().height(buttonHeight).testTag(deleteTestTag), ) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - Text(deleteLabel, color = Color.White, fontSize = buttonFontSize, fontWeight = FontWeight.SemiBold) + Text(deleteLabel, color = Color.White, style = buttonStyle, fontWeight = FontWeight.SemiBold) } } } @@ -318,13 +353,15 @@ private fun DeleteItemDialog(itemName: String, onConfirm: () -> Unit, onDismiss: Text( stringResource(R.string.item_detail_delete_dialog_title), fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, + style = MaterialTheme.typography.titleLarge, ) }, text = { Text( stringResource(R.string.item_detail_delete_dialog_message, itemName), - color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 15.sp, lineHeight = 22.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + lineHeight = 22.sp, ) }, confirmButton = { @@ -350,14 +387,15 @@ private val previewItem = ClothingItem( photoPath = "", createdAt = 0, ) -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun ItemDetailSheetPhonePreview() { SheetPreview { ItemDetailContent(item = previewItem, isCompact = true) } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun ItemDetailSheetTabletPreview() { SheetPreview { ItemDetailContent(item = previewItem, isCompact = false) } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt index 4d1daf2..7e36857 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt @@ -41,10 +41,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.worn.R @@ -56,7 +55,9 @@ import com.github.worn.ui.components.ClothingPhoto import com.github.worn.ui.components.PropertyRow import com.github.worn.ui.components.SheetDragHandle import com.github.worn.ui.exposeTestTagsAsResourceId +import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.SheetPreview +import com.github.worn.ui.theme.TabletPreview @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -111,7 +112,15 @@ internal fun OutfitDetailContent( .padding(bottom = 36.dp), verticalArrangement = Arrangement.spacedBy(sectionGap), ) { - OutfitTitle(name = outfit.name, nameSize = if (isCompact) 22.sp else 26.sp, padding = contentPadding) + OutfitTitle( + name = outfit.name, + nameStyle = if (isCompact) { + MaterialTheme.typography.titleLarge + } else { + MaterialTheme.typography.headlineSmall + }, + padding = contentPadding, + ) OutfitItemsPreview(items = outfitItems, isCompact = isCompact, contentPadding = contentPadding) if (!isCompact) { Box( @@ -125,7 +134,11 @@ internal fun OutfitDetailContent( editLabel = stringResource(R.string.outfit_detail_edit), deleteLabel = stringResource(R.string.outfit_detail_delete), buttonHeight = if (isCompact) 48.dp else 52.dp, - buttonFontSize = if (isCompact) 15.sp else 16.sp, + buttonStyle = if (isCompact) { + MaterialTheme.typography.bodyMedium + } else { + MaterialTheme.typography.titleSmall + }, onEdit = { onEdit(outfit) }, onDelete = { showDeleteDialog = true }, editTestTag = "outfit_detail_edit", @@ -144,11 +157,11 @@ internal fun OutfitDetailContent( } @Composable -private fun OutfitTitle(name: String, nameSize: TextUnit, padding: Dp) { +private fun OutfitTitle(name: String, nameStyle: TextStyle, padding: Dp) { Text( text = name, color = MaterialTheme.colorScheme.onSurface, - fontSize = nameSize, + style = nameStyle, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(horizontal = padding), ) @@ -172,7 +185,11 @@ private fun OutfitItemsPreview(items: List, isCompact: Boolean, co @Composable private fun OutfitProperties(outfit: Outfit, items: List, isCompact: Boolean, padding: Dp) { - val propFontSize = if (isCompact) 14.sp else 15.sp + val propStyle = if (isCompact) { + MaterialTheme.typography.bodySmall + } else { + MaterialTheme.typography.bodyMedium + } val propGap = if (isCompact) 14.dp else 16.dp Column( @@ -182,12 +199,12 @@ private fun OutfitProperties(outfit: Outfit, items: List, isCompac PropertyRow( label = stringResource(R.string.label_items), value = stringResource(R.string.outfit_detail_items_count, outfit.itemIds.size), - fontSize = propFontSize, + textStyle = propStyle, ) PropertyRow( label = stringResource(R.string.label_season), value = deriveSeasonText(items), - fontSize = propFontSize, + textStyle = propStyle, ) } } @@ -200,13 +217,15 @@ private fun DeleteOutfitDialog(outfitName: String, onConfirm: () -> Unit, onDism Text( stringResource(R.string.outfit_detail_delete_dialog_title), fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, + style = MaterialTheme.typography.titleLarge, ) }, text = { Text( stringResource(R.string.outfit_detail_delete_dialog_message, outfitName), - color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 15.sp, lineHeight = 22.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + lineHeight = 22.sp, ) }, confirmButton = { @@ -247,8 +266,7 @@ private fun OutfitItemCard( Text( text = item.name, color = MaterialTheme.colorScheme.onSurface, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.labelLarge, ) } } @@ -289,7 +307,7 @@ private val previewOutfit = Outfit( createdAt = 1_710_460_800_000, ) -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun OutfitDetailSheetPhonePreview() { SheetPreview { @@ -301,7 +319,7 @@ private fun OutfitDetailSheetPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun OutfitDetailSheetTabletPreview() { SheetPreview { @@ -312,3 +330,4 @@ private fun OutfitDetailSheetTabletPreview() { ) } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt index c19c298..5a2daf1 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt @@ -49,7 +49,6 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -67,6 +66,8 @@ import com.github.worn.ui.components.SelectionHeader import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme import org.koin.compose.viewmodel.koinViewModel @@ -235,9 +236,11 @@ private fun OutfitsHeader(outfitCount: Int, onCreateClick: () -> Unit = {}) { Text( text = stringResource(R.string.outfits_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = if (outfitCount == 0) 22.sp else 28.sp, - fontWeight = FontWeight.SemiBold, - letterSpacing = (-0.5).sp, + style = if (outfitCount == 0) { + MaterialTheme.typography.titleLarge + } else { + MaterialTheme.typography.headlineMedium + }, ) if (outfitCount > 0) { Button( @@ -248,7 +251,11 @@ private fun OutfitsHeader(outfitCount: Int, onCreateClick: () -> Unit = {}) { ) { Icon(Icons.Default.Add, contentDescription = null, Modifier.size(16.dp)) Spacer(Modifier.width(4.dp)) - Text(stringResource(R.string.outfits_button_create), fontWeight = FontWeight.SemiBold, fontSize = 14.sp) + Text( + stringResource(R.string.outfits_button_create), + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.SemiBold, + ) } } } @@ -257,7 +264,7 @@ private fun OutfitsHeader(outfitCount: Int, onCreateClick: () -> Unit = {}) { Text( text = pluralStringResource(R.plurals.saved_combinations, outfitCount, outfitCount), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, ) } @@ -341,7 +348,7 @@ private val previewOutfits = listOf( Outfit("3", "Evening Out", listOf("i1", "i2", "i3", "i4", "i5"), 1_709_856_000_000), ) -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun OutfitsPhonePreview() { WornTheme { @@ -352,7 +359,7 @@ private fun OutfitsPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun OutfitsSelectionPreview() { WornTheme { @@ -363,7 +370,7 @@ private fun OutfitsSelectionPreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun OutfitsEmptyPhonePreview() { WornTheme { @@ -374,7 +381,7 @@ private fun OutfitsEmptyPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun OutfitsTabletPreview() { WornTheme { @@ -385,7 +392,7 @@ private fun OutfitsTabletPreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun OutfitsEmptyTabletPreview() { WornTheme { @@ -395,3 +402,4 @@ private fun OutfitsEmptyTabletPreview() { ) } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt index cb1eaf8..d30b42d 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt @@ -64,7 +64,6 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -88,6 +87,8 @@ import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornChip import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.exposeTestTagsAsResourceId +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme import com.github.worn.ui.theme.wornExtras @@ -189,8 +190,7 @@ private fun SettingsScaffold( Text( text = stringResource(R.string.settings_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = 28.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.headlineMedium, letterSpacing = (-0.5).sp, ) @@ -257,8 +257,7 @@ private fun SectionLabel(text: String) { Text( text = text, color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.labelMedium, letterSpacing = 0.5.sp, ) } @@ -313,10 +312,14 @@ private fun SettingsToggleCard( Text( title, color = MaterialTheme.colorScheme.onSurface, - fontSize = 16.sp, + style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Medium, ) - Text(subtitle, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp) + Text( + subtitle, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + ) } Spacer(Modifier.width(8.dp)) Switch( @@ -371,10 +374,14 @@ private fun SettingsCard( Text( title, color = MaterialTheme.colorScheme.onSurface, - fontSize = 16.sp, + style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Medium, ) - Text(subtitle, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp) + Text( + subtitle, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + ) } Icon( imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, @@ -408,10 +415,14 @@ private fun AboutCard() { Text( stringResource(R.string.settings_version), color = MaterialTheme.colorScheme.onSurface, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f), ) - Text(versionName ?: "1.0", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 15.sp) + Text( + versionName ?: "1.0", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) } HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) AboutLinkRow(stringResource(R.string.settings_suggestions_bugs)) { uriHandler.openUri(FEEDBACK_URL) } @@ -428,7 +439,12 @@ private fun AboutLinkRow(label: String, onClick: () -> Unit) { verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(16.dp), ) { - Text(label, color = MaterialTheme.colorScheme.onSurface, fontSize = 15.sp, modifier = Modifier.weight(1f)) + Text( + label, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f), + ) Icon( imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, contentDescription = null, @@ -453,14 +469,14 @@ private fun DonationCard() { Text( stringResource(R.string.settings_donate_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = 15.sp, + style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, ) Spacer(Modifier.height(4.dp)) Text( stringResource(R.string.settings_donate_subtitle), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 13.sp, + style = MaterialTheme.typography.labelLarge, ) Spacer(Modifier.height(12.dp)) Surface( @@ -479,16 +495,14 @@ private fun DonationCard() { Text( DONATION_LN_ADDRESS, color = MaterialTheme.colorScheme.primary, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.labelLarge, modifier = Modifier.weight(1f), ) Spacer(Modifier.width(8.dp)) Text( if (showCopied) copiedText else stringResource(R.string.settings_donate_copy), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.labelMedium, ) } } @@ -550,13 +564,12 @@ private fun ProfileSheetContent(state: SettingsState, onIntent: (SettingsIntent) Text( text = stringResource(R.string.settings_your_profile), color = MaterialTheme.colorScheme.onSurface, - fontSize = 24.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.headlineSmall, ) Text( text = stringResource(R.string.settings_profile_help), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, ) ChipGroup( title = stringResource(R.string.label_body_type), @@ -665,7 +678,7 @@ private fun ApiKeySheetContent( Text( text = stringResource(R.string.settings_remove_key), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, modifier = Modifier.padding(vertical = 8.dp), ) @@ -680,19 +693,17 @@ private fun ApiKeySheetHeader() { Text( text = stringResource(R.string.settings_connect_claude), color = MaterialTheme.colorScheme.onSurface, - fontSize = 24.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.headlineSmall, ) Text( text = stringResource(R.string.settings_api_description), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, ) Text( text = stringResource(R.string.settings_api_get_key), color = MaterialTheme.colorScheme.primary, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.labelLarge, ) } @@ -800,19 +811,17 @@ private fun YouCamCredentialsSheetContent( Text( text = stringResource(R.string.settings_youcam_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = 24.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.headlineSmall, ) Text( text = stringResource(R.string.settings_youcam_description), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, ) Text( text = stringResource(R.string.settings_youcam_get_key), color = MaterialTheme.colorScheme.primary, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.labelLarge, ) FieldLabel(stringResource(R.string.settings_youcam_client_id_hint)) ApiKeyTextField( @@ -844,7 +853,7 @@ private fun YouCamCredentialsSheetContent( Text( text = errorMessage, color = MaterialTheme.colorScheme.error, - fontSize = 13.sp, + style = MaterialTheme.typography.labelLarge, modifier = Modifier.testTag("youcam_error"), ) } @@ -858,7 +867,7 @@ private fun YouCamCredentialsSheetContent( Text( text = stringResource(R.string.settings_youcam_remove), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, modifier = Modifier.padding(vertical = 8.dp), ) @@ -870,7 +879,12 @@ private fun YouCamCredentialsSheetContent( @Composable private fun FieldLabel(text: String) { - Text(text, color = MaterialTheme.colorScheme.onSurface, fontSize = 13.sp, fontWeight = FontWeight.SemiBold) + Text( + text, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold, + ) } // endregion @@ -896,7 +910,12 @@ private fun ChipGroup( onSelected: (T?) -> Unit, ) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text(title, color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, fontWeight = FontWeight.SemiBold) + Text( + title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.SemiBold, + ) FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), @@ -922,12 +941,17 @@ private fun MultiChipGroup( ) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Row { - Text(title, color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, fontWeight = FontWeight.SemiBold) + Text( + title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.SemiBold, + ) Spacer(Modifier.width(6.dp)) Text( stringResource(R.string.settings_multi_select), color = MaterialTheme.wornExtras.textMuted, - fontSize = 12.sp, + style = MaterialTheme.typography.labelMedium, ) } FlowRow( @@ -1011,7 +1035,7 @@ private const val DONATION_LN_ADDRESS = "jvsena42@blink.sv" private const val FEEDBACK_URL = "https://github.com/jvsena42/worn/issues/new" private const val LICENSE_URL = "https://github.com/jvsena42/worn/blob/main/LICENSE" -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun SettingsScreenPhonePreview() { WornTheme { @@ -1024,7 +1048,7 @@ private fun SettingsScreenPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun SettingsScreenTabletPreview() { WornTheme { @@ -1039,10 +1063,11 @@ private fun SettingsScreenTabletPreview() { } /** The default [SettingsState] already reports on-device AI as unavailable. */ -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun SettingsScreenOnDeviceAiUnavailablePreview() { WornTheme { SettingsScaffold(state = SettingsState()) } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt index 2997f2e..f0a0dcc 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt @@ -77,7 +77,6 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -99,6 +98,8 @@ import com.github.worn.ui.components.ErrorContentView import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme import com.github.worn.ui.theme.wornExtras @@ -302,7 +303,7 @@ private fun PhotoSourceDialog( ) { Icon(Icons.Outlined.CameraAlt, contentDescription = null, modifier = Modifier.size(24.dp)) Spacer(Modifier.width(12.dp)) - Text(stringResource(R.string.add_item_take_photo), fontSize = 16.sp) + Text(stringResource(R.string.add_item_take_photo), style = MaterialTheme.typography.titleSmall) } } TextButton(onClick = onGallery, modifier = Modifier.fillMaxWidth()) { @@ -317,7 +318,10 @@ private fun PhotoSourceDialog( modifier = Modifier.size(24.dp), ) Spacer(Modifier.width(12.dp)) - Text(stringResource(R.string.add_item_choose_gallery), fontSize = 16.sp) + Text( + stringResource(R.string.add_item_choose_gallery), + style = MaterialTheme.typography.titleSmall, + ) } } } @@ -424,7 +428,7 @@ private fun FeatureChoiceRow( ) { Icon(icon, contentDescription = null, modifier = Modifier.size(24.dp)) Spacer(Modifier.width(12.dp)) - Text(label, fontSize = 16.sp) + Text(label, style = MaterialTheme.typography.titleSmall) } } } @@ -501,7 +505,11 @@ private fun AiEmptyContent( ) { val circleSize = if (isCompact) 130.dp else 150.dp val iconSize = if (isCompact) 52.dp else 60.dp - val titleSize = if (isCompact) 24.sp else 26.sp + val titleStyle = if (isCompact) { + MaterialTheme.typography.headlineSmall + } else { + MaterialTheme.typography.headlineLarge + } val descWidth = if (isCompact) 280.dp else 380.dp Column( @@ -529,7 +537,7 @@ private fun AiEmptyContent( Text( text = stringResource(R.string.tryit_locked_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = titleSize, + style = titleStyle, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center, ) @@ -537,7 +545,11 @@ private fun AiEmptyContent( Text( text = stringResource(R.string.tryit_locked_description), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = if (isCompact) 15.sp else 16.sp, + style = if (isCompact) { + MaterialTheme.typography.bodyMedium + } else { + MaterialTheme.typography.bodyLarge + }, lineHeight = if (isCompact) 22.sp else 24.sp, textAlign = TextAlign.Center, modifier = Modifier.widthIn(max = descWidth), @@ -643,7 +655,7 @@ private fun TryItPhoneContent( verticalArrangement = Arrangement.spacedBy(20.dp), ) { Spacer(Modifier.height(4.dp)) - TryItTitle(fontSize = 28.sp) + TryItTitle(style = MaterialTheme.typography.headlineMedium) UploadZone(photoBitmap = photoBitmap, height = 200.dp, onClick = onPhotoClick) if (hasPhoto) { CropPhotoButton(onClick = onCropClick, modifier = Modifier.testTag("try_it_crop_button")) @@ -704,7 +716,7 @@ private fun TryItTabletContent( ) { Column(modifier = modifier.verticalScroll(scrollState)) { Spacer(Modifier.height(4.dp)) - TryItTitle(fontSize = 32.sp) + TryItTitle(style = MaterialTheme.typography.headlineLarge) Spacer(Modifier.height(28.dp)) Row( horizontalArrangement = Arrangement.spacedBy(32.dp), @@ -774,11 +786,11 @@ private fun TryItTabletContent( } @Composable -private fun TryItTitle(fontSize: androidx.compose.ui.unit.TextUnit) { +private fun TryItTitle(style: androidx.compose.ui.text.TextStyle) { Text( text = stringResource(R.string.tryit_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = fontSize, + style = style, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.8).sp, ) @@ -821,8 +833,7 @@ private fun UploadZone( Text( text = stringResource(R.string.tryit_upload_hint), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.labelLarge, textAlign = TextAlign.Center, ) } @@ -887,8 +898,7 @@ private fun PairsSection( Text( text = stringResource(R.string.tryit_pairs_with), color = MaterialTheme.colorScheme.onSurface, - fontSize = 18.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleMedium, letterSpacing = (-0.2).sp, ) LazyRow( @@ -928,7 +938,7 @@ private fun ItemThumbnail(item: ClothingItem, size: Dp, onClick: () -> Unit) { @Composable private fun CombinationsCard(count: Int, isCompact: Boolean) { val cardHeight = if (isCompact) 90.dp else 110.dp - val valueSize = if (isCompact) 40.sp else 44.sp + val valueStyle = MaterialTheme.typography.displaySmall Surface( shape = RoundedCornerShape(20.dp), @@ -946,14 +956,14 @@ private fun CombinationsCard(count: Int, isCompact: Boolean) { Text( text = stringResource(R.string.tryit_combinations_unlocked), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, + style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, letterSpacing = 0.5.sp, ) Text( text = count.toString(), color = MaterialTheme.colorScheme.primary, - fontSize = valueSize, + style = valueStyle, fontWeight = FontWeight.Bold, letterSpacing = (-1.2).sp, ) @@ -965,14 +975,17 @@ private fun CombinationsCard(count: Int, isCompact: Boolean) { private fun GapsFilledSection(gaps: List, isCompact: Boolean) { if (gaps.isEmpty()) return - val fontSize = if (isCompact) 14.sp else 15.sp + val bodyStyle = if (isCompact) { + MaterialTheme.typography.bodySmall + } else { + MaterialTheme.typography.bodyMedium + } Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( text = stringResource(R.string.tryit_gaps_filled), color = MaterialTheme.colorScheme.onSurface, - fontSize = 18.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleMedium, letterSpacing = (-0.2).sp, ) gaps.forEach { gap -> @@ -990,7 +1003,7 @@ private fun GapsFilledSection(gaps: List, isCompact: Boolean) { Text( text = gap, color = MaterialTheme.colorScheme.onSurface, - fontSize = fontSize, + style = bodyStyle, ) } } @@ -1029,8 +1042,7 @@ private fun DecisionBanner(worthAdding: Boolean, isCompact: Boolean) { Text( text = text, color = Color.White, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleSmall, ) } } @@ -1057,8 +1069,7 @@ private fun TryOnSection( Text( text = stringResource(R.string.tryit_your_photo_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = 18.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleMedium, letterSpacing = (-0.2).sp, ) PersonPhotoZone( @@ -1075,8 +1086,7 @@ private fun TryOnSection( Text( text = stringResource(R.string.tryit_tryon_category_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = 18.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleMedium, letterSpacing = (-0.2).sp, ) GarmentCategorySelector(selected = state.selectedCategory, onSelect = onSelectCategory) @@ -1104,7 +1114,7 @@ private fun TryOnSection( Text( text = stringResource(R.string.tryit_tryon_cost_note), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, + style = MaterialTheme.typography.labelMedium, ) } } @@ -1143,8 +1153,7 @@ private fun PersonPhotoZone(personImage: ByteArray?, height: Dp, onClick: () -> Text( text = stringResource(R.string.tryit_your_photo_hint), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.labelLarge, textAlign = TextAlign.Center, ) } @@ -1216,7 +1225,7 @@ private fun TryOnLoadingIndicator() { Text( text = stringResource(R.string.tryit_tryon_generating), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, ) } } @@ -1228,8 +1237,7 @@ private fun TryOnResultView(imageBytes: ByteArray, height: Dp) { Text( text = stringResource(R.string.tryit_tryon_result_title), color = MaterialTheme.colorScheme.onSurface, - fontSize = 18.sp, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleMedium, letterSpacing = (-0.2).sp, ) Surface( @@ -1277,7 +1285,7 @@ private val previewResult = TryItResult( worthAdding = true, ) -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun TryItResultsPhonePreview() { WornTheme { @@ -1290,7 +1298,7 @@ private fun TryItResultsPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun TryItEmptyPhonePreview() { WornTheme { @@ -1303,7 +1311,7 @@ private fun TryItEmptyPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun TryItResultsTabletPreview() { WornTheme { @@ -1316,7 +1324,7 @@ private fun TryItResultsTabletPreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun TryItShareChooserPhonePreview() { WornTheme { @@ -1330,7 +1338,7 @@ private fun TryItShareChooserPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun TryItShareChooserTabletPreview() { WornTheme { @@ -1344,7 +1352,7 @@ private fun TryItShareChooserTabletPreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun TryItTryOnPhonePreview() { WornTheme { @@ -1361,3 +1369,4 @@ private fun TryItTryOnPhonePreview() { } // endregion + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt index 29d4b66..5452c18 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt @@ -51,7 +51,6 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -72,6 +71,8 @@ import com.github.worn.ui.components.SelectionHeader import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients +import com.github.worn.ui.theme.PhonePreview +import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme import com.github.worn.ui.util.ShortcutCommand @@ -279,16 +280,18 @@ private fun WardrobeHeader(itemCount: Int) { stringResource(R.string.wardrobe_title) }, color = MaterialTheme.colorScheme.onSurface, - fontSize = if (itemCount == 0) 22.sp else 28.sp, - fontWeight = FontWeight.SemiBold, - letterSpacing = (-0.5).sp, + style = if (itemCount == 0) { + MaterialTheme.typography.titleLarge + } else { + MaterialTheme.typography.headlineMedium + }, ) if (itemCount > 0) { Spacer(modifier = Modifier.height(8.dp)) Text( text = stringResource(R.string.wardrobe_subtitle, itemCount), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, + style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium, ) } @@ -356,7 +359,7 @@ private fun CategoryEmptyState() { Text( stringResource(R.string.wardrobe_category_empty), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 16.sp, + style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Medium, ) } @@ -410,7 +413,11 @@ private fun AddItemFab(onClick: () -> Unit, modifier: Modifier = Modifier) { ) { Icon(Icons.Default.Add, contentDescription = null) Spacer(Modifier.width(8.dp)) - Text(text = stringResource(R.string.wardrobe_fab_add), fontWeight = FontWeight.SemiBold, fontSize = 15.sp) + Text( + text = stringResource(R.string.wardrobe_fab_add), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + ) } } @@ -424,7 +431,7 @@ private val previewItems = listOf( ClothingItem("6", "Chinos", Category.BOTTOM, listOf("khaki"), photoPath = "", createdAt = 0), ) -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun WardrobeScreenPhonePreview() { WornTheme { @@ -436,7 +443,7 @@ private fun WardrobeScreenPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun WardrobeSelectModePreview() { WornTheme { @@ -452,7 +459,7 @@ private fun WardrobeSelectModePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun WardrobeEmptyPhonePreview() { WornTheme { @@ -464,7 +471,7 @@ private fun WardrobeEmptyPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun WardrobeScreenTabletPreview() { WornTheme { @@ -476,7 +483,7 @@ private fun WardrobeScreenTabletPreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun WardrobeEmptyTabletPreview() { WornTheme { @@ -488,7 +495,7 @@ private fun WardrobeEmptyTabletPreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_8") +@PhonePreview @Composable private fun WardrobeEmptyCategoryPhonePreview() { WornTheme { @@ -503,7 +510,7 @@ private fun WardrobeEmptyCategoryPhonePreview() { } } -@Preview(showSystemUi = true, device = "id:pixel_tablet") +@TabletPreview @Composable private fun WardrobeEmptyCategoryTabletPreview() { WornTheme { @@ -517,3 +524,4 @@ private fun WardrobeEmptyCategoryTabletPreview() { ) } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornPreviews.kt b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornPreviews.kt new file mode 100644 index 0000000..ce2f781 --- /dev/null +++ b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornPreviews.kt @@ -0,0 +1,27 @@ +package com.github.worn.ui.theme + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.ui.tooling.preview.Preview + +/** + * Multipreview annotations for the two form factors the project targets. + * + * Each carries its light and dark variant, so a composable annotated with [PhonePreview] renders + * both without the file having to repeat the annotation. Compose resolves a "multipreview" by + * expanding every `@Preview` on the annotation class onto the annotated function. + * + * Prefer these over a bare `@Preview`: they are the only thing keeping dark mode covered in the + * IDE, and dark is exactly where a missed colour role shows up. + */ +@Preview(name = "Phone", showSystemUi = true, device = "id:pixel_8") +@Preview(name = "Phone · Dark", showSystemUi = true, device = "id:pixel_8", uiMode = UI_MODE_NIGHT_YES) +annotation class PhonePreview + +@Preview(name = "Tablet", showSystemUi = true, device = "id:pixel_tablet") +@Preview( + name = "Tablet · Dark", + showSystemUi = true, + device = "id:pixel_tablet", + uiMode = UI_MODE_NIGHT_YES, +) +annotation class TabletPreview diff --git a/detekt.yml b/detekt.yml index b3f02b6..0529d27 100644 --- a/detekt.yml +++ b/detekt.yml @@ -8,8 +8,10 @@ naming: complexity: TooManyFunctions: - # Preview composables inflate count but are not real logic - ignoreAnnotatedFunctions: ['Preview'] + # Preview composables inflate count but are not real logic. PhonePreview/TabletPreview are the + # project's multipreview annotations (see ui/theme/WornPreviews.kt) — detekt matches on the + # annotation name written at the call site, so the aliases have to be listed too. + ignoreAnnotatedFunctions: ['Preview', 'PhonePreview', 'TabletPreview'] LongParameterList: # Composable functions pass many state + callback params with defaults ignoreDefaultParameters: true diff --git a/iosApp/iosApp/Components/AiLockedSheet.swift b/iosApp/iosApp/Components/AiLockedSheet.swift index 82f1bcf..a6b2c78 100644 --- a/iosApp/iosApp/Components/AiLockedSheet.swift +++ b/iosApp/iosApp/Components/AiLockedSheet.swift @@ -26,11 +26,11 @@ struct AiLockedSheet: View { } Text(String(localized: "ai_locked_title")) - .font(.system(size: 22, weight: .medium)) + .font(.title2.weight(.medium)) .foregroundColor(WornColors.textPrimary) Text(String(localized: "ai_locked_description")) - .font(.system(size: 14)) + .font(.subheadline) .foregroundColor(WornColors.textSecondary) .multilineTextAlignment(.center) .lineSpacing(7) diff --git a/iosApp/iosApp/Components/ClothingCard.swift b/iosApp/iosApp/Components/ClothingCard.swift index 02d2a29..f4dd77d 100644 --- a/iosApp/iosApp/Components/ClothingCard.swift +++ b/iosApp/iosApp/Components/ClothingCard.swift @@ -48,7 +48,7 @@ struct ClothingCard: View { // AI-generated names can run long; left unbounded they push the category row down and // misalign the cards next to them in the grid row. Text(item.name) - .font(.system(size: 14, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textPrimary) .lineLimit(2) .truncationMode(.tail) @@ -58,7 +58,7 @@ struct ClothingCard: View { .fill(dotColor(for: item.category)) .frame(width: 8, height: 8) Text(displayLabel(for: item.category)) - .font(.system(size: 12)) + .font(.caption) .foregroundColor(WornColors.textMuted) } } diff --git a/iosApp/iosApp/Components/CropEditorView.swift b/iosApp/iosApp/Components/CropEditorView.swift index 8cbb492..3f0848c 100644 --- a/iosApp/iosApp/Components/CropEditorView.swift +++ b/iosApp/iosApp/Components/CropEditorView.swift @@ -48,11 +48,11 @@ struct CropEditorView: View { .accessibilityIdentifier("crop_editor_cancel") Spacer() Text(String(localized: "crop_title")) - .font(.system(size: 16, weight: .semibold)) + .font(.callout.weight(.semibold)) .foregroundColor(.white) Spacer() Button(String(localized: "crop_apply"), action: applyCrop) - .font(.system(size: 16, weight: .semibold)) + .font(.callout.weight(.semibold)) .foregroundColor(canApply ? WornColors.accentGreen : WornColors.textMuted) .disabled(!canApply) .accessibilityIdentifier("crop_editor_apply") @@ -63,7 +63,7 @@ struct CropEditorView: View { private var bottomBar: some View { Button(String(localized: "crop_reset")) { selection = bounds } - .font(.system(size: 15)) + .font(.subheadline) .foregroundColor(.white) .disabled(bounds == nil || isProcessing) .padding(.vertical, 12) diff --git a/iosApp/iosApp/Components/CropPhotoButton.swift b/iosApp/iosApp/Components/CropPhotoButton.swift index 3498ccb..30a1b20 100644 --- a/iosApp/iosApp/Components/CropPhotoButton.swift +++ b/iosApp/iosApp/Components/CropPhotoButton.swift @@ -14,7 +14,7 @@ struct CropPhotoButton: View { .font(.system(size: 15)) .foregroundColor(WornColors.textSecondary) Text(String(localized: "crop_photo_button")) - .font(.system(size: 15, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textPrimary) } } diff --git a/iosApp/iosApp/Components/EmptyStateView.swift b/iosApp/iosApp/Components/EmptyStateView.swift index b340076..3ee4fe0 100644 --- a/iosApp/iosApp/Components/EmptyStateView.swift +++ b/iosApp/iosApp/Components/EmptyStateView.swift @@ -36,12 +36,12 @@ struct EmptyStateView: View { } Text(title) - .font(.system(size: 24, weight: .semibold)) + .font(.title2.weight(.semibold)) .tracking(-0.5) .foregroundColor(WornColors.textPrimary) Text(description) - .font(.system(size: 15)) + .font(.subheadline) .lineSpacing(4) .multilineTextAlignment(.center) .foregroundColor(WornColors.textSecondary) diff --git a/iosApp/iosApp/Components/ErrorContentView.swift b/iosApp/iosApp/Components/ErrorContentView.swift index c86d33b..07f774b 100644 --- a/iosApp/iosApp/Components/ErrorContentView.swift +++ b/iosApp/iosApp/Components/ErrorContentView.swift @@ -18,14 +18,14 @@ struct ErrorContentView: View { .frame(maxWidth: .infinity) Text(message) - .font(.system(size: 14)) + .font(.subheadline) .foregroundColor(WornColors.textSecondary) .multilineTextAlignment(.center) .padding(.top, 24) Button(action: onRetry) { Text(String(localized: "common_retry")) - .font(.system(size: 15, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(retryButtonColor) .padding(.horizontal, 24) .padding(.vertical, 12) diff --git a/iosApp/iosApp/Components/PropertyRow.swift b/iosApp/iosApp/Components/PropertyRow.swift index e568030..8606a40 100644 --- a/iosApp/iosApp/Components/PropertyRow.swift +++ b/iosApp/iosApp/Components/PropertyRow.swift @@ -3,16 +3,16 @@ import SwiftUI struct PropertyRow: View { let label: String let value: String - var fontSize: CGFloat = 15 + var textFont: Font = .subheadline var body: some View { HStack { Text(label) - .font(.system(size: fontSize, weight: .medium)) + .font(textFont.weight(.medium)) .foregroundColor(WornColors.textSecondary) Spacer() Text(value) - .font(.system(size: fontSize, weight: .medium)) + .font(textFont.weight(.medium)) .foregroundColor(WornColors.textPrimary) } } diff --git a/iosApp/iosApp/Components/SelectionHeader.swift b/iosApp/iosApp/Components/SelectionHeader.swift index e521864..2d12657 100644 --- a/iosApp/iosApp/Components/SelectionHeader.swift +++ b/iosApp/iosApp/Components/SelectionHeader.swift @@ -9,7 +9,7 @@ struct SelectionHeader: View { VStack(alignment: .leading, spacing: 8) { HStack { Text(String(format: String(localized: "selected_count"), count)) - .font(.system(size: 28, weight: .medium)) + .font(.title.weight(.medium)) .tracking(-0.8) .foregroundColor(WornColors.textPrimary) Spacer() @@ -20,7 +20,7 @@ struct SelectionHeader: View { Image(systemName: "trash") .font(.system(size: 15)) Text(String(localized: "common_delete")) - .font(.system(size: 15, weight: .semibold)) + .font(.subheadline.weight(.semibold)) } .foregroundColor(.white) .padding(.horizontal, 20) @@ -30,7 +30,7 @@ struct SelectionHeader: View { } } Button(String(localized: "common_cancel")) { onCancel() } - .font(.system(size: 15, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textSecondary) } } diff --git a/iosApp/iosApp/Components/WornBottomBar.swift b/iosApp/iosApp/Components/WornBottomBar.swift index bc1d487..a8b4f51 100644 --- a/iosApp/iosApp/Components/WornBottomBar.swift +++ b/iosApp/iosApp/Components/WornBottomBar.swift @@ -105,7 +105,7 @@ private struct TabItem: View { Image(systemName: tab.icon) .font(.system(size: 18)) Text(tab.label) - .font(.system(size: 10, weight: .semibold)) + .font(.caption2.weight(.semibold)) .tracking(0.5) // Labels wrap on a 5-tab compact bar, which clips the descender. Shrink // instead, down far enough for the longest translation to fit its slot, and @@ -115,6 +115,10 @@ private struct TabItem: View { .truncationMode(.tail) .padding(.horizontal, 2) } + // The bar is a fixed-height row of five equal slots, so it cannot grow with the + // largest accessibility sizes the way flowing text can. Cap the scaling here and let + // minimumScaleFactor absorb the rest; the rest of the app stays uncapped. + .dynamicTypeSize(...DynamicTypeSize.accessibility1) .foregroundColor(isActive ? WornColors.textOnColor : WornColors.textSecondary) .frame(maxWidth: .infinity, maxHeight: .infinity) .background( diff --git a/iosApp/iosApp/Components/WornChip.swift b/iosApp/iosApp/Components/WornChip.swift index fc92e1a..0f77858 100644 --- a/iosApp/iosApp/Components/WornChip.swift +++ b/iosApp/iosApp/Components/WornChip.swift @@ -8,7 +8,7 @@ struct WornChip: View { var body: some View { Button(action: onTap) { Text(label) - .font(.system(size: 13, weight: .medium)) + .font(.footnote.weight(.medium)) .foregroundColor(isActive ? WornColors.textOnColor : WornColors.textSecondary) .padding(.horizontal, 16) .padding(.vertical, 8) diff --git a/iosApp/iosApp/Components/WornGradientButton.swift b/iosApp/iosApp/Components/WornGradientButton.swift index c138383..0f82346 100644 --- a/iosApp/iosApp/Components/WornGradientButton.swift +++ b/iosApp/iosApp/Components/WornGradientButton.swift @@ -59,7 +59,7 @@ struct WornGradientButton: View { private var buttonText: some View { Text(text) - .font(.system(size: 16, weight: .semibold)) + .font(.callout.weight(.semibold)) .foregroundColor(.white) } } diff --git a/iosApp/iosApp/Screens/AddItemSheet.swift b/iosApp/iosApp/Screens/AddItemSheet.swift index 718d302..b6c6f33 100644 --- a/iosApp/iosApp/Screens/AddItemSheet.swift +++ b/iosApp/iosApp/Screens/AddItemSheet.swift @@ -177,7 +177,7 @@ struct AddItemSheet: View { .font(.system(size: 32)) .foregroundColor(WornColors.iconMuted) Text(String(localized: "add_item_photo_hint")) - .font(.system(size: 14, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textSecondary) } } @@ -211,7 +211,7 @@ struct AddItemSheet: View { set: { onRemoveBackgroundChange($0) } )) { Text(String(localized: "add_item_remove_background")) - .font(.system(size: 15, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textPrimary) } .tint(WornColors.accentGreen) @@ -248,10 +248,10 @@ struct AddItemSheet: View { } label: { HStack(spacing: 6) { Text("✦") - .font(.system(size: 12, weight: .semibold)) + .font(.caption.weight(.semibold)) .foregroundColor(.white) Text(String(localized: "add_item_ai_badge")) - .font(.system(size: 12, weight: .semibold)) + .font(.caption.weight(.semibold)) .foregroundColor(.white) } .padding(.horizontal, 12) @@ -269,7 +269,7 @@ struct AddItemSheet: View { private var nameField: some View { TextField(String(localized: "add_item_name_hint"), text: $name) - .font(.system(size: 15)) + .font(.subheadline) .padding(16) .background(WornColors.bgCard) .clipShape(RoundedRectangle(cornerRadius: 12)) @@ -293,7 +293,7 @@ struct AddItemSheet: View { .frame(width: 20, height: 20) } Text(selectedCategory.map { displayName(for: $0) } ?? String(localized: "label_category")) - .font(.system(size: 15)) + .font(.subheadline) .foregroundColor(selectedCategory != nil ? WornColors.textPrimary : WornColors.iconMuted) Spacer() Image(systemName: categoryExpanded ? "chevron.up" : "chevron.down") @@ -323,7 +323,7 @@ struct AddItemSheet: View { .foregroundColor(WornColors.textSecondary) .frame(width: 20, height: 20) Text(label) - .font(.system(size: 14, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textPrimary) Spacer() } @@ -350,7 +350,7 @@ struct AddItemSheet: View { private var colorSection: some View { VStack(alignment: .leading, spacing: 10) { Text(String(localized: "label_color")) - .font(.system(size: 14, weight: .semibold)) + .font(.subheadline.weight(.semibold)) .foregroundColor(WornColors.textPrimary) LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: 7), spacing: 12) { @@ -387,7 +387,7 @@ struct AddItemSheet: View { private var seasonSection: some View { VStack(alignment: .leading, spacing: 10) { Text(String(localized: "label_season")) - .font(.system(size: 14, weight: .semibold)) + .font(.subheadline.weight(.semibold)) .foregroundColor(WornColors.textPrimary) HStack(spacing: 8) { @@ -478,7 +478,7 @@ struct AddItemSheet: View { Text(selectedSubcategory.map { localizedSubcategoryName($0) } ?? String(localized: "label_subcategory")) - .font(.system(size: 15)) + .font(.subheadline) .foregroundColor(selectedSubcategory != nil ? WornColors.textPrimary : WornColors.iconMuted) Spacer() Image(systemName: subcategoryExpanded ? "chevron.up" : "chevron.down") @@ -498,7 +498,7 @@ struct AddItemSheet: View { withAnimation { subcategoryExpanded = false } } label: { Text(label) - .font(.system(size: 14, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textPrimary) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 16) @@ -532,7 +532,7 @@ struct AddItemSheet: View { private var fitSection: some View { VStack(alignment: .leading, spacing: 10) { Text(String(localized: "label_fit")) - .font(.system(size: 14, weight: .semibold)) + .font(.subheadline.weight(.semibold)) .foregroundColor(WornColors.textPrimary) HStack(spacing: 8) { @@ -560,7 +560,7 @@ struct AddItemSheet: View { private var materialSection: some View { VStack(alignment: .leading, spacing: 10) { Text(String(localized: "label_material")) - .font(.system(size: 14, weight: .semibold)) + .font(.subheadline.weight(.semibold)) .foregroundColor(WornColors.textPrimary) LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 4), spacing: 8) { @@ -579,6 +579,11 @@ struct AddItemSheet: View { AddItemSheet(isSaving: false, isAiAvailable: false, onSave: { _, _, _, _, _, _, _, _ in }, onDismiss: {}) } +#Preview("iPhone · Dark") { + AddItemSheet(isSaving: false, isAiAvailable: false, onSave: { _, _, _, _, _, _, _, _ in }, onDismiss: {}) + .preferredColorScheme(.dark) +} + #Preview("iPad Portrait", traits: .portrait) { AddItemSheet(isSaving: false, isAiAvailable: false, onSave: { _, _, _, _, _, _, _, _ in }, onDismiss: {}) } diff --git a/iosApp/iosApp/Screens/CreateOutfitSheet.swift b/iosApp/iosApp/Screens/CreateOutfitSheet.swift index 2d572ac..4ad246d 100644 --- a/iosApp/iosApp/Screens/CreateOutfitSheet.swift +++ b/iosApp/iosApp/Screens/CreateOutfitSheet.swift @@ -60,7 +60,7 @@ struct CreateOutfitSheet: View { private var nameField: some View { TextField(String(localized: "create_outfit_name_hint"), text: $name) - .font(.system(size: 15)) + .font(.subheadline) .padding(16) .background(WornColors.bgCard) .clipShape(RoundedRectangle(cornerRadius: 12)) @@ -74,12 +74,12 @@ struct CreateOutfitSheet: View { private var selectItemsHeader: some View { HStack { Text(String(localized: "create_outfit_select_items")) - .font(.system(size: 16, weight: .semibold)) + .font(.callout.weight(.semibold)) .foregroundColor(WornColors.textPrimary) Spacer() if !selectedItemIds.isEmpty { Text(String(format: String(localized: "selected_count"), selectedItemIds.count)) - .font(.system(size: 13, weight: .medium)) + .font(.footnote.weight(.medium)) .foregroundColor(WornColors.accentGreen) } } @@ -178,6 +178,20 @@ private let previewItems: [ClothingItem] = [ ) } +#Preview("iPhone · Dark") { + CreateOutfitSheet( + clothingItems: previewItems, + selectedItemIds: Set(["1", "2"]), + activeCategory: nil, + isSaving: false, + onCategorySelected: { _ in }, + onToggleItem: { _ in }, + onSave: { _ in }, + onDismiss: {} + ) + .preferredColorScheme(.dark) +} + #Preview("iPad Portrait", traits: .portrait) { CreateOutfitSheet( clothingItems: previewItems, diff --git a/iosApp/iosApp/Screens/GapsScreen.swift b/iosApp/iosApp/Screens/GapsScreen.swift index 5b9da21..8f3b9f1 100644 --- a/iosApp/iosApp/Screens/GapsScreen.swift +++ b/iosApp/iosApp/Screens/GapsScreen.swift @@ -96,11 +96,11 @@ struct GapsContent: View { ScrollView { VStack(alignment: .leading, spacing: 0) { Text(String(localized: "gaps_title")) - .font(.system(size: 28, weight: .semibold)) + .font(.title.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .padding(.top, 24) Text(String(localized: "gaps_subtitle")) - .font(.system(size: 14)) + .font(.subheadline) .foregroundColor(WornColors.textSecondary) .padding(.top, 4) .padding(.bottom, 20) @@ -155,13 +155,13 @@ struct GapsContent: View { .frame(maxWidth: .infinity) Text(String(localized: "gaps_complete_title")) - .font(.system(size: 18, weight: .semibold)) + .font(.headline.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .padding(.top, 24) .multilineTextAlignment(.center) Text(String(localized: "gaps_complete_description")) - .font(.system(size: 14)) + .font(.subheadline) .foregroundColor(WornColors.textSecondary) .multilineTextAlignment(.center) .padding(.top, 8) @@ -204,12 +204,12 @@ struct GapsContent: View { HStack { VStack(alignment: .leading, spacing: 2) { Text(state.isAiMode ? String(localized: "gaps_banner_ai_title") : String(localized: "gaps_banner_common_title")) - .font(.system(size: 16, weight: .semibold)) + .font(.callout.weight(.semibold)) .foregroundColor(.white) Text(state.isAiMode ? String(localized: "gaps_banner_ai_subtitle") : String(localized: "gaps_banner_common_subtitle")) - .font(.system(size: 13)) + .font(.footnote) .foregroundColor(.white.opacity(0.8)) } Spacer() @@ -227,7 +227,7 @@ struct GapsContent: View { private func sectionLabel(_ text: String) -> some View { Text(text.uppercased()) - .font(.system(size: 12, weight: .medium)) + .font(.caption.weight(.medium)) .foregroundColor(WornColors.textSecondary) .tracking(0.5) } @@ -240,12 +240,12 @@ struct GapsContent: View { categoryIcon(for: recommendation.mappedCategory) VStack(alignment: .leading, spacing: 2) { Text(recommendation.itemName) - .font(.system(size: 15, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textPrimary) Text(state.isAiMode ? String(format: String(localized: "gaps_pairing_ai"), recommendation.pairingCount) : String(localized: "gaps_pairing_common")) - .font(.system(size: 12)) + .font(.caption) .foregroundColor(WornColors.textSecondary) } Spacer() @@ -332,7 +332,7 @@ private struct GapDetailSheet: View { .frame(maxWidth: .infinity) Text(recommendation.itemName) - .font(.system(size: 22, weight: .semibold)) + .font(.title2.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .padding(.top, 16) @@ -341,7 +341,7 @@ private struct GapDetailSheet: View { .fill(dotColor(for: recommendation.mappedCategory)) .frame(width: 8, height: 8) Text(displayLabel(for: recommendation.mappedCategory)) - .font(.system(size: 14)) + .font(.subheadline) .foregroundColor(WornColors.textSecondary) } .padding(.top, 4) @@ -356,7 +356,7 @@ private struct GapDetailSheet: View { Text(isAiMode ? String(format: String(localized: "gaps_pairing_ai"), recommendation.pairingCount) : String(localized: "gaps_pairing_common")) - .font(.system(size: 13)) + .font(.footnote) .foregroundColor(WornColors.textSecondary) } .padding(12) @@ -391,11 +391,11 @@ private struct GapDetailSheet: View { private func detailRow(_ label: String, _ value: String) -> some View { HStack { Text(label) - .font(.system(size: 14)) + .font(.subheadline) .foregroundColor(WornColors.textSecondary) Spacer() Text(value) - .font(.system(size: 14, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textPrimary) } .padding(.vertical, 8) @@ -405,7 +405,7 @@ private struct GapDetailSheet: View { VStack(spacing: 8) { Button(action: onAddToWardrobe) { Text(String(localized: "gaps_add_to_wardrobe")) - .font(.system(size: 16, weight: .semibold)) + .font(.callout.weight(.semibold)) .foregroundColor(.white) .frame(maxWidth: .infinity) .frame(height: 52) @@ -421,7 +421,7 @@ private struct GapDetailSheet: View { Button(action: onDismiss) { Text(String(localized: "gaps_dismiss")) - .font(.system(size: 15, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textSecondary) .frame(maxWidth: .infinity) .frame(height: 48) @@ -514,6 +514,17 @@ private let previewGaps: [GapRecommendation] = [ ) } +#Preview("iPhone · Dark") { + GapsContent( + state: GapsState( + recommendations: previewGaps, isLoading: false, isSaving: false, + isAiAvailable: true, isAiMode: true, error: nil + ), + isCompact: true + ) + .preferredColorScheme(.dark) +} + #Preview("iPhone - Complete") { GapsContent( state: GapsState( @@ -524,6 +535,17 @@ private let previewGaps: [GapRecommendation] = [ ) } +#Preview("iPhone - Complete · Dark") { + GapsContent( + state: GapsState( + recommendations: [], isLoading: false, isSaving: false, + isAiAvailable: false, isAiMode: false, error: nil + ), + isCompact: true + ) + .preferredColorScheme(.dark) +} + #Preview("iPad Portrait", traits: .portrait) { GapsContent( state: GapsState( diff --git a/iosApp/iosApp/Screens/ItemDetailSheet.swift b/iosApp/iosApp/Screens/ItemDetailSheet.swift index c28256e..63a388c 100644 --- a/iosApp/iosApp/Screens/ItemDetailSheet.swift +++ b/iosApp/iosApp/Screens/ItemDetailSheet.swift @@ -12,11 +12,11 @@ struct ItemDetailSheet: View { private var photoHeight: CGFloat { isCompact ? 280 : 360 } private var photoRadius: CGFloat { isCompact ? 20 : 24 } - private var nameSize: CGFloat { isCompact ? 22 : 26 } - private var propFontSize: CGFloat { isCompact ? 14 : 15 } + private var nameFont: Font { isCompact ? .title2 : .title } + private var propFont: Font { isCompact ? .subheadline : .callout } private var propGap: CGFloat { isCompact ? 14 : 16 } private var buttonHeight: CGFloat { isCompact ? 48 : 52 } - private var buttonFontSize: CGFloat { isCompact ? 15 : 16 } + private var buttonFont: Font { isCompact ? .subheadline : .callout } private var contentPadding: CGFloat { isCompact ? 24 : 32 } private var sectionGap: CGFloat { isCompact ? 20 : 24 } private var placeholderIconSize: CGFloat { isCompact ? 64 : 80 } @@ -68,7 +68,7 @@ struct ItemDetailSheet: View { private var nameGroup: some View { VStack(alignment: .leading, spacing: 6) { Text(item.name) - .font(.system(size: nameSize, weight: .semibold)) + .font(nameFont.weight(.semibold)) .foregroundColor(WornColors.textPrimary) HStack(spacing: 8) { @@ -76,7 +76,7 @@ struct ItemDetailSheet: View { .fill(dotColor(for: item.category)) .frame(width: 10, height: 10) Text(displayLabel(for: item.category)) - .font(.system(size: 14)) + .font(.subheadline) .foregroundColor(WornColors.textSecondary) } } @@ -94,7 +94,7 @@ struct ItemDetailSheet: View { if !item.colors.isEmpty { HStack { Text(String(localized: "label_color")) - .font(.system(size: propFontSize, weight: .medium)) + .font(propFont.weight(.medium)) .foregroundColor(WornColors.textSecondary) Spacer() HStack(spacing: 8) { @@ -105,7 +105,7 @@ struct ItemDetailSheet: View { Circle().stroke(WornColors.borderSubtle, lineWidth: 1) ) Text(item.colors.map { $0.capitalized }.joined(separator: ", ")) - .font(.system(size: propFontSize, weight: .medium)) + .font(propFont.weight(.medium)) .foregroundColor(WornColors.textPrimary) } } @@ -115,19 +115,19 @@ struct ItemDetailSheet: View { let seasonText = item.seasons.count == Season.entries.count ? String(localized: "common_all_seasons") : item.seasons.map { seasonDisplayName($0) }.joined(separator: ", ") - PropertyRow(label: String(localized: "label_season"), value: seasonText, fontSize: propFontSize) + PropertyRow(label: String(localized: "label_season"), value: seasonText, textFont: propFont) } if let fit = item.fit { - PropertyRow(label: String(localized: "label_fit"), value: fitDisplayName(fit), fontSize: propFontSize) + PropertyRow(label: String(localized: "label_fit"), value: fitDisplayName(fit), textFont: propFont) } if let subcategory = item.subcategory { - PropertyRow(label: String(localized: "label_subcategory"), value: subcategoryDisplayName(subcategory), fontSize: propFontSize) + PropertyRow(label: String(localized: "label_subcategory"), value: subcategoryDisplayName(subcategory), textFont: propFont) } if let material = item.material { - PropertyRow(label: String(localized: "label_material"), value: materialDisplayName(material), fontSize: propFontSize) + PropertyRow(label: String(localized: "label_material"), value: materialDisplayName(material), textFont: propFont) } } } @@ -137,7 +137,7 @@ struct ItemDetailSheet: View { VStack(spacing: 12) { Button { onEdit(item) } label: { Text(String(localized: "item_detail_edit")) - .font(.system(size: buttonFontSize, weight: .semibold)) + .font(buttonFont.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .frame(maxWidth: .infinity) .frame(height: buttonHeight) @@ -152,7 +152,7 @@ struct ItemDetailSheet: View { Button { showDeleteAlert = true } label: { Text(String(localized: "item_detail_delete")) - .font(.system(size: buttonFontSize, weight: .semibold)) + .font(buttonFont.weight(.semibold)) .foregroundColor(.white) .frame(maxWidth: .infinity) .frame(height: buttonHeight) @@ -245,6 +245,14 @@ private let previewItem = ClothingItem( ) } +#Preview("iPhone · Dark") { + ItemDetailSheet( + item: previewItem, isCompact: true, + onEdit: { _ in }, onDelete: { _ in } + ) + .preferredColorScheme(.dark) +} + #Preview("iPad Portrait", traits: .portrait) { ItemDetailSheet( item: previewItem, isCompact: false, diff --git a/iosApp/iosApp/Screens/OutfitDetailSheet.swift b/iosApp/iosApp/Screens/OutfitDetailSheet.swift index 5762f4e..79cf1bb 100644 --- a/iosApp/iosApp/Screens/OutfitDetailSheet.swift +++ b/iosApp/iosApp/Screens/OutfitDetailSheet.swift @@ -12,14 +12,14 @@ struct OutfitDetailSheet: View { private var contentPadding: CGFloat { isCompact ? 24 : 32 } private var sectionGap: CGFloat { isCompact ? 20 : 24 } - private var nameSize: CGFloat { isCompact ? 22 : 26 } + private var nameFont: Font { isCompact ? .title2 : .title } private var cardSize: CGFloat { isCompact ? 200 : 300 } private var cardRadius: CGFloat { isCompact ? 18 : 20 } private var cardGap: CGFloat { isCompact ? 12 : 16 } - private var propFontSize: CGFloat { isCompact ? 14 : 15 } + private var propFont: Font { isCompact ? .subheadline : .callout } private var propGap: CGFloat { isCompact ? 14 : 16 } private var buttonHeight: CGFloat { isCompact ? 48 : 52 } - private var buttonFontSize: CGFloat { isCompact ? 15 : 16 } + private var buttonFont: Font { isCompact ? .subheadline : .callout } private var outfitItems: [ClothingItem] { outfit.itemIds.compactMap { id in @@ -32,7 +32,7 @@ struct OutfitDetailSheet: View { VStack(spacing: sectionGap) { // Title Text(outfit.name) - .font(.system(size: nameSize, weight: .semibold)) + .font(nameFont.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, contentPadding) @@ -57,8 +57,8 @@ struct OutfitDetailSheet: View { // Properties VStack(spacing: propGap) { - PropertyRow(label: String(localized: "label_items"), value: String(format: String(localized: "outfit_detail_items_count"), outfit.itemIds.count), fontSize: propFontSize) - PropertyRow(label: String(localized: "label_season"), value: deriveSeasonText(), fontSize: propFontSize) + PropertyRow(label: String(localized: "label_items"), value: String(format: String(localized: "outfit_detail_items_count"), outfit.itemIds.count), textFont: propFont) + PropertyRow(label: String(localized: "label_season"), value: deriveSeasonText(), textFont: propFont) } .padding(.horizontal, contentPadding) @@ -66,7 +66,7 @@ struct OutfitDetailSheet: View { VStack(spacing: 12) { Button { onEdit(outfit) } label: { Text(String(localized: "outfit_detail_edit")) - .font(.system(size: buttonFontSize, weight: .semibold)) + .font(buttonFont.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .frame(maxWidth: .infinity) .frame(height: buttonHeight) @@ -81,7 +81,7 @@ struct OutfitDetailSheet: View { Button { showDeleteAlert = true } label: { Text(String(localized: "outfit_detail_delete")) - .font(.system(size: buttonFontSize, weight: .semibold)) + .font(buttonFont.weight(.semibold)) .foregroundColor(.white) .frame(maxWidth: .infinity) .frame(height: buttonHeight) @@ -117,7 +117,7 @@ struct OutfitDetailSheet: View { .shadow(color: .black.opacity(0.25), radius: 8, x: 0, y: 4) Text(item.name) - .font(.system(size: 13, weight: .medium)) + .font(.footnote.weight(.medium)) .foregroundColor(WornColors.textPrimary) } } @@ -164,6 +164,14 @@ private let previewOutfit = Outfit(id: "1", name: "Weekend Casual", itemIds: ["i ) } +#Preview("iPhone · Dark") { + OutfitDetailSheet( + outfit: previewOutfit, clothingItems: previewItems, + isCompact: true, onEdit: { _ in }, onDelete: { _ in } + ) + .preferredColorScheme(.dark) +} + #Preview("iPad Portrait", traits: .portrait) { OutfitDetailSheet( outfit: previewOutfit, clothingItems: previewItems, diff --git a/iosApp/iosApp/Screens/OutfitsScreen.swift b/iosApp/iosApp/Screens/OutfitsScreen.swift index abdafe5..c2cd50d 100644 --- a/iosApp/iosApp/Screens/OutfitsScreen.swift +++ b/iosApp/iosApp/Screens/OutfitsScreen.swift @@ -144,7 +144,7 @@ struct OutfitsContent: View { VStack(alignment: .leading, spacing: 8) { HStack { Text(String(localized: "outfits_title")) - .font(.system(size: state.outfits.isEmpty ? 22 : 28, weight: .semibold)) + .font((state.outfits.isEmpty ? Font.title2 : Font.title).weight(.semibold)) .tracking(-0.5) .foregroundColor(WornColors.textPrimary) Spacer() @@ -154,7 +154,7 @@ struct OutfitsContent: View { Image(systemName: "plus") .font(.system(size: 12, weight: .semibold)) Text(String(localized: "outfits_button_create")) - .font(.system(size: 14, weight: .semibold)) + .font(.subheadline.weight(.semibold)) } .foregroundColor(.white) .padding(.horizontal, 16) @@ -168,7 +168,7 @@ struct OutfitsContent: View { if !state.outfits.isEmpty { Text(String(format: String(localized: "saved_combinations"), state.outfits.count)) - .font(.system(size: 14, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textSecondary) } } @@ -311,7 +311,7 @@ private struct OutfitCardView: View { private var itemCountBadge: some View { Text(String(format: String(localized: "outfit_detail_items_count"), outfit.itemIds.count)) - .font(.system(size: 11, weight: .semibold)) + .font(.caption2.weight(.semibold)) .foregroundColor(.white) .padding(.horizontal, 10) .padding(.vertical, 4) @@ -324,12 +324,12 @@ private struct OutfitCardView: View { VStack(alignment: .leading, spacing: 2) { // Auto-generated names concatenate every item, so they can outgrow the card. Text(outfit.name) - .font(.system(size: 16, weight: .semibold)) + .font(.callout.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .lineLimit(1) .truncationMode(.tail) Text(formatDate(outfit.createdAt)) - .font(.system(size: 12)) + .font(.caption) .foregroundColor(WornColors.textSecondary) } Spacer() @@ -361,6 +361,14 @@ private let previewOutfits: [Outfit] = [ ) } +#Preview("iPhone · Dark") { + OutfitsContent( + state: OutfitState(outfits: previewOutfits, isLoading: false, isDeleting: false, selectedIds: Set(), error: nil, itemCategories: [:], allClothingItems: [], clothingItems: [], selectedItemIds: Set(), activeItemCategory: nil, isSaving: false, isLoadingItems: false), + isCompact: true + ) + .preferredColorScheme(.dark) +} + #Preview("iPhone - Selection") { OutfitsContent( state: OutfitState(outfits: previewOutfits, isLoading: false, isDeleting: false, selectedIds: Set(["1", "3"]), error: nil, itemCategories: [:], allClothingItems: [], clothingItems: [], selectedItemIds: Set(), activeItemCategory: nil, isSaving: false, isLoadingItems: false), @@ -368,6 +376,14 @@ private let previewOutfits: [Outfit] = [ ) } +#Preview("iPhone - Selection · Dark") { + OutfitsContent( + state: OutfitState(outfits: previewOutfits, isLoading: false, isDeleting: false, selectedIds: Set(["1", "3"]), error: nil, itemCategories: [:], allClothingItems: [], clothingItems: [], selectedItemIds: Set(), activeItemCategory: nil, isSaving: false, isLoadingItems: false), + isCompact: true + ) + .preferredColorScheme(.dark) +} + #Preview("iPhone - Empty") { OutfitsContent( state: OutfitState(outfits: [], isLoading: false, isDeleting: false, selectedIds: Set(), error: nil, itemCategories: [:], allClothingItems: [], clothingItems: [], selectedItemIds: Set(), activeItemCategory: nil, isSaving: false, isLoadingItems: false), @@ -375,6 +391,14 @@ private let previewOutfits: [Outfit] = [ ) } +#Preview("iPhone - Empty · Dark") { + OutfitsContent( + state: OutfitState(outfits: [], isLoading: false, isDeleting: false, selectedIds: Set(), error: nil, itemCategories: [:], allClothingItems: [], clothingItems: [], selectedItemIds: Set(), activeItemCategory: nil, isSaving: false, isLoadingItems: false), + isCompact: true + ) + .preferredColorScheme(.dark) +} + #Preview("iPad Portrait", traits: .portrait) { OutfitsContent( state: OutfitState(outfits: previewOutfits, isLoading: false, isDeleting: false, selectedIds: Set(), error: nil, itemCategories: [:], allClothingItems: [], clothingItems: [], selectedItemIds: Set(), activeItemCategory: nil, isSaving: false, isLoadingItems: false), diff --git a/iosApp/iosApp/Screens/SettingsScreen.swift b/iosApp/iosApp/Screens/SettingsScreen.swift index 012cc59..6183564 100644 --- a/iosApp/iosApp/Screens/SettingsScreen.swift +++ b/iosApp/iosApp/Screens/SettingsScreen.swift @@ -67,7 +67,7 @@ struct SettingsContent: View { ScrollView { VStack(alignment: .leading, spacing: 0) { Text(String(localized: "settings_title")) - .font(.system(size: 28, weight: .semibold)) + .font(.title.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .padding(.top, 24) .padding(.bottom, 28) @@ -170,7 +170,7 @@ struct SettingsContent: View { private func sectionLabel(_ text: String) -> some View { Text(text) - .font(.system(size: 12, weight: .medium)) + .font(.caption.weight(.medium)) .foregroundColor(WornColors.textSecondary) .tracking(0.5) } @@ -188,10 +188,10 @@ struct SettingsContent: View { ) VStack(alignment: .leading, spacing: 2) { Text(title) - .font(.system(size: 16, weight: .medium)) + .font(.callout.weight(.medium)) .foregroundColor(WornColors.textPrimary) Text(subtitle) - .font(.system(size: 13)) + .font(.footnote) .foregroundColor(WornColors.textSecondary) } Spacer() @@ -227,10 +227,10 @@ struct SettingsContent: View { ) VStack(alignment: .leading, spacing: 2) { Text(title) - .font(.system(size: 16, weight: .medium)) + .font(.callout.weight(.medium)) .foregroundColor(WornColors.textPrimary) Text(subtitle) - .font(.system(size: 13)) + .font(.footnote) .foregroundColor(WornColors.textSecondary) } Spacer() @@ -252,11 +252,11 @@ struct SettingsContent: View { VStack(spacing: 0) { HStack { Text(String(localized: "settings_version")) - .font(.system(size: 15)) + .font(.subheadline) .foregroundColor(WornColors.textPrimary) Spacer() Text(appVersion) - .font(.system(size: 15)) + .font(.subheadline) .foregroundColor(WornColors.textSecondary) } .padding(16) @@ -270,7 +270,7 @@ struct SettingsContent: View { } label: { HStack { Text(String(localized: "settings_suggestions_bugs")) - .font(.system(size: 15)) + .font(.subheadline) .foregroundColor(WornColors.textPrimary) Spacer() Image(systemName: "chevron.right") @@ -290,7 +290,7 @@ struct SettingsContent: View { } label: { HStack { Text(String(localized: "settings_licenses")) - .font(.system(size: 15)) + .font(.subheadline) .foregroundColor(WornColors.textPrimary) Spacer() Image(systemName: "chevron.right") @@ -310,11 +310,11 @@ struct SettingsContent: View { private var donationCard: some View { VStack(alignment: .leading, spacing: 4) { Text(String(localized: "settings_donate_title")) - .font(.system(size: 15, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textPrimary) Text(String(localized: "settings_donate_subtitle")) - .font(.system(size: 13)) + .font(.footnote) .foregroundColor(WornColors.textSecondary) Button { @@ -323,11 +323,11 @@ struct SettingsContent: View { } label: { HStack { Text(donationLNAddress) - .font(.system(size: 13, weight: .medium)) + .font(.footnote.weight(.medium)) .foregroundColor(WornColors.accentGreen) Spacer() Text(showCopied ? String(localized: "settings_donate_copied") : String(localized: "settings_donate_copy")) - .font(.system(size: 12, weight: .medium)) + .font(.caption.weight(.medium)) .foregroundColor(WornColors.textSecondary) } .padding(12) @@ -362,10 +362,10 @@ private struct ProfileSheet: View { ScrollView { VStack(alignment: .leading, spacing: 20) { Text(String(localized: "settings_your_profile")) - .font(.system(size: 24, weight: .semibold)) + .font(.title2.weight(.semibold)) .foregroundColor(WornColors.textPrimary) Text(String(localized: "settings_profile_help")) - .font(.system(size: 14)) + .font(.subheadline) .foregroundColor(WornColors.textSecondary) chipGroup(title: String(localized: "label_body_type"), options: bodyTypeOptions, @@ -435,13 +435,13 @@ private struct ApiKeySheet: View { var body: some View { VStack(alignment: .leading, spacing: 16) { Text(String(localized: "settings_connect_claude")) - .font(.system(size: 24, weight: .semibold)) + .font(.title2.weight(.semibold)) .foregroundColor(WornColors.textPrimary) Text(String(localized: "settings_api_description")) - .font(.system(size: 14)) + .font(.subheadline) .foregroundColor(WornColors.textSecondary) Text(String(localized: "settings_api_get_key")) - .font(.system(size: 13, weight: .medium)) + .font(.footnote.weight(.medium)) .foregroundColor(WornColors.accentGreen) HStack { @@ -453,7 +453,7 @@ private struct ApiKeySheet: View { } } .disabled(hasApiKey) - .font(.system(size: 15)) + .font(.subheadline) Button { passwordVisible.toggle() } label: { Image(systemName: passwordVisible ? "eye" : "eye.slash") @@ -484,7 +484,7 @@ private struct ApiKeySheet: View { dismiss() } label: { Text(String(localized: "settings_remove_key")) - .font(.system(size: 14, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textSecondary) } .accessibilityIdentifier("api_key_remove_button") @@ -517,13 +517,13 @@ private struct YouCamCredentialsSheet: View { ScrollView { VStack(alignment: .leading, spacing: 14) { Text(String(localized: "settings_youcam_title")) - .font(.system(size: 24, weight: .semibold)) + .font(.title2.weight(.semibold)) .foregroundColor(WornColors.textPrimary) Text(String(localized: "settings_youcam_description")) - .font(.system(size: 14)) + .font(.subheadline) .foregroundColor(WornColors.textSecondary) Text(String(localized: "settings_youcam_get_key")) - .font(.system(size: 13, weight: .medium)) + .font(.footnote.weight(.medium)) .foregroundColor(WornColors.accentGreen) fieldLabel(String(localized: "settings_youcam_client_id_hint")) @@ -543,7 +543,7 @@ private struct YouCamCredentialsSheet: View { if let errorMessage, !verifying { Text(errorMessage) - .font(.system(size: 13)) + .font(.footnote) .foregroundColor(WornColors.deleteRed) .accessibilityIdentifier("youcam_error") } @@ -556,7 +556,7 @@ private struct YouCamCredentialsSheet: View { dismiss() } label: { Text(String(localized: "settings_youcam_remove")) - .font(.system(size: 14, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textSecondary) } .accessibilityIdentifier("youcam_remove_button") @@ -573,7 +573,7 @@ private struct YouCamCredentialsSheet: View { private func fieldLabel(_ text: String) -> some View { Text(text) - .font(.system(size: 13, weight: .semibold)) + .font(.footnote.weight(.semibold)) .foregroundColor(WornColors.textPrimary) } @@ -587,7 +587,7 @@ private struct YouCamCredentialsSheet: View { } } .disabled(!enabled) - .font(.system(size: 15)) + .font(.subheadline) Button { visible.wrappedValue.toggle() } label: { Image(systemName: visible.wrappedValue ? "eye" : "eye.slash") @@ -611,7 +611,7 @@ private func chipGroup( ) -> some View { VStack(alignment: .leading, spacing: 10) { Text(title) - .font(.system(size: 14, weight: .semibold)) + .font(.subheadline.weight(.semibold)) .foregroundColor(WornColors.textPrimary) FlowLayout(spacing: 8) { ForEach(Array(options.enumerated()), id: \.offset) { _, item in @@ -631,10 +631,10 @@ private func multiChipGroup( VStack(alignment: .leading, spacing: 10) { HStack(spacing: 6) { Text(title) - .font(.system(size: 14, weight: .semibold)) + .font(.subheadline.weight(.semibold)) .foregroundColor(WornColors.textPrimary) Text(String(localized: "settings_multi_select")) - .font(.system(size: 12)) + .font(.caption) .foregroundColor(WornColors.textMuted) } FlowLayout(spacing: 8) { @@ -759,6 +759,14 @@ private func previewSettingsState( ) } +#Preview("iPhone · Dark") { + SettingsContent( + state: previewSettingsState(availability: OnDeviceAiAvailabilityAvailable()), + isCompact: true + ) + .preferredColorScheme(.dark) +} + #Preview("iPhone - AI unavailable") { SettingsContent( state: previewSettingsState( @@ -768,6 +776,16 @@ private func previewSettingsState( ) } +#Preview("iPhone - AI unavailable · Dark") { + SettingsContent( + state: previewSettingsState( + availability: OnDeviceAiAvailabilityUnavailable(reason: .unsupportedDevice) + ), + isCompact: true + ) + .preferredColorScheme(.dark) +} + #Preview("iPad Portrait", traits: .portrait) { SettingsContent( state: previewSettingsState(availability: OnDeviceAiAvailabilityAvailable()), diff --git a/iosApp/iosApp/Screens/TryItScreen.swift b/iosApp/iosApp/Screens/TryItScreen.swift index f3ec315..d196b83 100644 --- a/iosApp/iosApp/Screens/TryItScreen.swift +++ b/iosApp/iosApp/Screens/TryItScreen.swift @@ -154,12 +154,12 @@ struct TryItScreen: View { } Text(String(localized: "tryit_locked_title")) - .font(.system(size: isCompact ? 24 : 26, weight: .medium)) + .font((isCompact ? Font.title2 : Font.title).weight(.medium)) .foregroundColor(WornColors.textPrimary) .multilineTextAlignment(.center) Text(String(localized: "tryit_locked_description")) - .font(.system(size: isCompact ? 15 : 16)) + .font(isCompact ? .subheadline : .callout) .foregroundColor(WornColors.textSecondary) .multilineTextAlignment(.center) .lineSpacing(4) @@ -205,7 +205,7 @@ struct TryItScreen: View { private var phoneContent: some View { VStack(alignment: .leading, spacing: 20) { - tryItTitle(fontSize: 28) + tryItTitle(font: .title) .id(Self.analysisAnchor) uploadZone(height: 200) if photoData != nil { garmentCropButton } @@ -241,7 +241,7 @@ struct TryItScreen: View { private var tabletContent: some View { VStack(alignment: .leading, spacing: 28) { - tryItTitle(fontSize: 32) + tryItTitle(font: .largeTitle) .id(Self.analysisAnchor) HStack(alignment: .top, spacing: 32) { @@ -293,9 +293,9 @@ struct TryItScreen: View { // MARK: - Components - private func tryItTitle(fontSize: CGFloat) -> some View { + private func tryItTitle(font: Font) -> some View { Text(String(localized: "tryit_title")) - .font(.system(size: fontSize, weight: .semibold)) + .font(font.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .tracking(-0.8) } @@ -320,7 +320,7 @@ struct TryItScreen: View { .font(.system(size: 44)) .foregroundColor(WornColors.iconMuted) Text(String(localized: "tryit_upload_hint")) - .font(.system(size: 13, weight: .medium)) + .font(.footnote.weight(.medium)) .foregroundColor(WornColors.textSecondary) .multilineTextAlignment(.center) } @@ -400,7 +400,7 @@ struct TryItScreen: View { if !matchingItems.isEmpty { VStack(alignment: .leading, spacing: 12) { Text(String(localized: "tryit_pairs_with")) - .font(.system(size: 18, weight: .semibold)) + .font(.headline.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .tracking(-0.2) @@ -438,15 +438,15 @@ struct TryItScreen: View { private func combinationsCard(count: Int, isCompact: Bool) -> some View { let cardHeight: CGFloat = isCompact ? 90 : 110 - let valueSize: CGFloat = isCompact ? 40 : 44 + let valueFont: Font = .largeTitle return VStack(alignment: .leading, spacing: 4) { Text(String(localized: "tryit_combinations_unlocked")) - .font(.system(size: 12, weight: .semibold)) + .font(.caption.weight(.semibold)) .foregroundColor(WornColors.textSecondary) .tracking(0.5) Text("\(count)") - .font(.system(size: valueSize, weight: .bold)) + .font(valueFont.weight(.bold)) .foregroundColor(WornColors.accentGreen) .tracking(-1.2) } @@ -468,7 +468,7 @@ struct TryItScreen: View { if !gaps.isEmpty { VStack(alignment: .leading, spacing: 12) { Text(String(localized: "tryit_gaps_filled")) - .font(.system(size: 18, weight: .semibold)) + .font(.headline.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .tracking(-0.2) @@ -478,7 +478,7 @@ struct TryItScreen: View { .fill(WornColors.accentGreen) .frame(width: 8, height: 8) Text(gap) - .font(.system(size: isCompact ? 14 : 15)) + .font(isCompact ? .subheadline : .callout) .foregroundColor(WornColors.textPrimary) } } @@ -500,7 +500,7 @@ struct TryItScreen: View { .font(.system(size: 22)) .foregroundColor(.white) Text(text) - .font(.system(size: 16, weight: .semibold)) + .font(.callout.weight(.semibold)) .foregroundColor(.white) } .frame(maxWidth: .infinity) @@ -542,7 +542,7 @@ struct TryItScreen: View { private func tryOnSection(isCompact: Bool) -> some View { VStack(alignment: .leading, spacing: 16) { Text(String(localized: "tryit_your_photo_title")) - .font(.system(size: 18, weight: .semibold)) + .font(.headline.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .tracking(-0.2) @@ -554,7 +554,7 @@ struct TryItScreen: View { } Text(String(localized: "tryit_tryon_category_title")) - .font(.system(size: 18, weight: .semibold)) + .font(.headline.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .tracking(-0.2) @@ -584,7 +584,7 @@ struct TryItScreen: View { } Text(String(localized: "tryit_tryon_cost_note")) - .font(.system(size: 12)) + .font(.caption) .foregroundColor(WornColors.textSecondary) } .frame(maxWidth: .infinity, alignment: .leading) @@ -645,7 +645,7 @@ struct TryItScreen: View { .font(.system(size: 44)) .foregroundColor(WornColors.iconMuted) Text(String(localized: "tryit_your_photo_hint")) - .font(.system(size: 13, weight: .medium)) + .font(.footnote.weight(.medium)) .foregroundColor(WornColors.textSecondary) .multilineTextAlignment(.center) } @@ -672,7 +672,7 @@ struct TryItScreen: View { let selected = viewModel.state.selectedCategory == category Button { viewModel.selectCategory(category) } label: { Text(label) - .font(.system(size: 14, weight: .medium)) + .font(.subheadline.weight(.medium)) .padding(.horizontal, 14) .padding(.vertical, 8) .background(selected ? WornColors.accentIndigo : WornColors.bgCard) @@ -711,7 +711,7 @@ struct TryItScreen: View { VStack(spacing: 12) { ProgressView().tint(WornColors.accentIndigo) Text(String(localized: "tryit_tryon_generating")) - .font(.system(size: 14)) + .font(.subheadline) .foregroundColor(WornColors.textSecondary) } .frame(maxWidth: .infinity) @@ -721,7 +721,7 @@ struct TryItScreen: View { private func tryOnResultView(imageData: Data, height: CGFloat) -> some View { VStack(alignment: .leading, spacing: 12) { Text(String(localized: "tryit_tryon_result_title")) - .font(.system(size: 18, weight: .semibold)) + .font(.headline.weight(.semibold)) .foregroundColor(WornColors.textPrimary) .tracking(-0.2) @@ -757,6 +757,11 @@ struct TryItScreen: View { TryItScreen(onTabSelected: { _ in }) } +#Preview("iPhone · Dark") { + TryItScreen(onTabSelected: { _ in }) + .preferredColorScheme(.dark) +} + #Preview("iPad Portrait", traits: .portrait) { TryItScreen(onTabSelected: { _ in }) } diff --git a/iosApp/iosApp/Screens/WardrobeScreen.swift b/iosApp/iosApp/Screens/WardrobeScreen.swift index 6277487..45ac7f9 100644 --- a/iosApp/iosApp/Screens/WardrobeScreen.swift +++ b/iosApp/iosApp/Screens/WardrobeScreen.swift @@ -157,17 +157,17 @@ struct WardrobeContent: View { VStack(alignment: .leading, spacing: 8) { if state.totalItemCount == 0 { Text(String(localized: "wardrobe_title_empty")) - .font(.system(size: 22, weight: .semibold)) + .font(.title2.weight(.semibold)) .tracking(-0.5) .foregroundColor(WornColors.textPrimary) } else { Text(String(localized: "wardrobe_title")) - .font(.system(size: 28, weight: .semibold)) + .font(.title.weight(.semibold)) .tracking(-0.8) .foregroundColor(WornColors.textPrimary) Text(String(format: String(localized: "wardrobe_subtitle"), state.totalItemCount)) - .font(.system(size: 14, weight: .medium)) + .font(.subheadline.weight(.medium)) .foregroundColor(WornColors.textSecondary) } } @@ -222,7 +222,7 @@ struct WardrobeContent: View { .foregroundColor(WornColors.textSecondary.opacity(0.5)) Text(String(localized: "wardrobe_category_empty")) - .font(.system(size: 16, weight: .medium)) + .font(.callout.weight(.medium)) .foregroundColor(WornColors.textSecondary) Spacer() @@ -267,7 +267,7 @@ struct WardrobeContent: View { Image(systemName: "plus") .font(.system(size: 15, weight: .semibold)) Text(String(localized: "wardrobe_fab_add")) - .font(.system(size: 15, weight: .semibold)) + .font(.subheadline.weight(.semibold)) } .foregroundColor(WornColors.textOnColor) .padding(.horizontal, 20) @@ -296,6 +296,14 @@ private let previewItems: [ClothingItem] = [ ) } +#Preview("iPhone · Dark") { + WardrobeContent( + state: WardrobeState(items: previewItems, isLoading: false, isSaving: false, isDeleting: false, selectedIds: Set(), activeCategory: nil, isAiAvailable: false, error: nil, totalItemCount: Int32(previewItems.count)), + isCompact: true + ) + .preferredColorScheme(.dark) +} + #Preview("iPhone - Selection") { WardrobeContent( state: WardrobeState(items: previewItems, isLoading: false, isSaving: false, isDeleting: false, selectedIds: Set(["1", "3"]), activeCategory: nil, isAiAvailable: false, error: nil, totalItemCount: Int32(previewItems.count)), @@ -303,6 +311,14 @@ private let previewItems: [ClothingItem] = [ ) } +#Preview("iPhone - Selection · Dark") { + WardrobeContent( + state: WardrobeState(items: previewItems, isLoading: false, isSaving: false, isDeleting: false, selectedIds: Set(["1", "3"]), activeCategory: nil, isAiAvailable: false, error: nil, totalItemCount: Int32(previewItems.count)), + isCompact: true + ) + .preferredColorScheme(.dark) +} + #Preview("iPhone - Empty") { WardrobeContent( state: WardrobeState(items: [], isLoading: false, isSaving: false, isDeleting: false, selectedIds: Set(), activeCategory: nil, isAiAvailable: false, error: nil, totalItemCount: 0), @@ -310,6 +326,14 @@ private let previewItems: [ClothingItem] = [ ) } +#Preview("iPhone - Empty · Dark") { + WardrobeContent( + state: WardrobeState(items: [], isLoading: false, isSaving: false, isDeleting: false, selectedIds: Set(), activeCategory: nil, isAiAvailable: false, error: nil, totalItemCount: 0), + isCompact: true + ) + .preferredColorScheme(.dark) +} + #Preview("iPad Portrait", traits: .portrait) { WardrobeContent( state: WardrobeState(items: previewItems, isLoading: false, isSaving: false, isDeleting: false, selectedIds: Set(), activeCategory: nil, isAiAvailable: false, error: nil, totalItemCount: Int32(previewItems.count)), @@ -331,6 +355,14 @@ private let previewItems: [ClothingItem] = [ ) } +#Preview("iPhone - Empty Category · Dark") { + WardrobeContent( + state: WardrobeState(items: [], isLoading: false, isSaving: false, isDeleting: false, selectedIds: Set(), activeCategory: .top, isAiAvailable: false, error: nil, totalItemCount: Int32(previewItems.count)), + isCompact: true + ) + .preferredColorScheme(.dark) +} + #Preview("iPad Portrait - Empty Category", traits: .portrait) { WardrobeContent( state: WardrobeState(items: [], isLoading: false, isSaving: false, isDeleting: false, selectedIds: Set(), activeCategory: .top, isAiAvailable: false, error: nil, totalItemCount: Int32(previewItems.count)), From 2e9a0f13cca4b5e0fc821dab9fed94cedbe98634 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 8 Aug 2026 10:36:40 -0300 Subject: [PATCH 05/13] feat: add a shared corner-radius scale on both platforms Radii were 12 different literals spread across ~60 call sites, four of which (10/22/26/30dp) differ from a neighbour by 2dp and read as the same curve. Collapse them onto the eight M3 shape slots, wired into the theme so Material components pick the right corner on their own, and mirror the same scale on iOS as WornShape. Bottom sheets get a named `MaterialTheme.sheetShape` since M3 has no slot for a partly-rounded shape and ten copies of the literal had accumulated. The largeIncreased/extraLargeIncreased/extraExtraLarge slots are Expressive APIs, so the files referencing them carry the opt-in. Co-Authored-By: Claude Opus 5 (1M context) --- .../worn/ui/components/AddItemFields.kt | 18 ++++---- .../worn/ui/components/AiLockedSheet.kt | 6 ++- .../github/worn/ui/components/ClothingCard.kt | 4 +- .../ui/components/DeleteConfirmationDialog.kt | 2 +- .../worn/ui/components/ErrorContentView.kt | 2 +- .../github/worn/ui/components/OutfitCard.kt | 12 +++-- .../worn/ui/components/SelectionHeader.kt | 2 +- .../worn/ui/components/WornBottomBar.kt | 6 ++- .../com/github/worn/ui/components/WornChip.kt | 7 ++- .../worn/ui/components/WornGradientButton.kt | 2 +- .../com/github/worn/ui/screen/AddItemSheet.kt | 4 +- .../worn/ui/screen/CreateOutfitSheet.kt | 14 ++++-- .../com/github/worn/ui/screen/GapsScreen.kt | 16 ++++--- .../github/worn/ui/screen/ItemDetailSheet.kt | 10 ++-- .../worn/ui/screen/OutfitDetailSheet.kt | 6 ++- .../github/worn/ui/screen/OutfitsScreen.kt | 10 +++- .../github/worn/ui/screen/SettingsScreen.kt | 22 +++++---- .../com/github/worn/ui/screen/TryItScreen.kt | 28 +++++------ .../github/worn/ui/screen/WardrobeScreen.kt | 10 +++- .../com/github/worn/ui/theme/SheetPreview.kt | 4 +- .../com/github/worn/ui/theme/WornShapes.kt | 46 +++++++++++++++++++ .../com/github/worn/ui/theme/WornTheme.kt | 1 + iosApp/iosApp/Components/AiLockedSheet.swift | 2 +- iosApp/iosApp/Components/ClothingCard.swift | 4 +- .../iosApp/Components/ErrorContentView.swift | 2 +- iosApp/iosApp/Components/WornBottomBar.swift | 6 +-- .../Components/WornGradientButton.swift | 2 +- iosApp/iosApp/Screens/AddItemSheet.swift | 16 +++---- iosApp/iosApp/Screens/CreateOutfitSheet.swift | 8 ++-- iosApp/iosApp/Screens/GapsScreen.swift | 14 +++--- iosApp/iosApp/Screens/ItemDetailSheet.swift | 6 +-- iosApp/iosApp/Screens/OutfitDetailSheet.swift | 6 +-- iosApp/iosApp/Screens/OutfitsScreen.swift | 10 ++-- iosApp/iosApp/Screens/SettingsScreen.swift | 24 +++++----- iosApp/iosApp/Screens/TryItScreen.swift | 28 +++++------ iosApp/iosApp/Screens/WardrobeScreen.swift | 2 +- iosApp/iosApp/Theme/WornShapes.swift | 23 ++++++++++ 37 files changed, 253 insertions(+), 132 deletions(-) create mode 100644 composeApp/src/main/kotlin/com/github/worn/ui/theme/WornShapes.kt create mode 100644 iosApp/iosApp/Theme/WornShapes.swift diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt index 0fbcdcf..c40c3b6 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt @@ -1,3 +1,5 @@ +@file:OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) + @file:Suppress("TooManyFunctions") package com.github.worn.ui.components @@ -94,7 +96,7 @@ fun PhotoUploadZone( ) { Surface( onClick = onClick, - shape = RoundedCornerShape(16.dp), + shape = MaterialTheme.shapes.large, color = Color.Transparent, border = BorderStroke(1.5.dp, MaterialTheme.colorScheme.outline), modifier = modifier.fillMaxWidth().height(140.dp), @@ -105,7 +107,7 @@ fun PhotoUploadZone( bitmap = bitmap, contentDescription = "Selected photo", contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(16.dp)), + modifier = Modifier.fillMaxSize().clip(MaterialTheme.shapes.large), ) } else { Column( @@ -133,7 +135,7 @@ fun PhotoUploadZone( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxSize() - .clip(RoundedCornerShape(16.dp)) + .clip(MaterialTheme.shapes.large) .background(MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.6f)), ) { CircularProgressIndicator( @@ -180,7 +182,7 @@ fun RemoveBackgroundToggle( fun AiBadge(onClick: () -> Unit = {}, modifier: Modifier = Modifier) { Surface( onClick = onClick, - shape = RoundedCornerShape(28.dp), + shape = MaterialTheme.shapes.extraLargeIncreased, color = MaterialTheme.colorScheme.secondary, modifier = modifier, ) { @@ -219,10 +221,10 @@ fun ItemNameField(value: String, onValueChange: (String) -> Unit, modifier: Modi focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, ), - shape = RoundedCornerShape(12.dp), + shape = MaterialTheme.shapes.medium, modifier = modifier .fillMaxWidth() - .border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(12.dp)), + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, MaterialTheme.shapes.medium), ) } @@ -230,7 +232,7 @@ fun ItemNameField(value: String, onValueChange: (String) -> Unit, modifier: Modi fun CategoryDropdown(selected: Category?, onSelected: (Category) -> Unit, modifier: Modifier = Modifier) { var expanded by remember { mutableStateOf(false) } Surface( - shape = RoundedCornerShape(12.dp), + shape = MaterialTheme.shapes.medium, color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = modifier.fillMaxWidth(), @@ -429,7 +431,7 @@ fun SubcategoryDropdown(category: Category, selected: Subcategory?, onSelected: val options = subcategoriesFor(category) var expanded by remember { mutableStateOf(false) } Surface( - shape = RoundedCornerShape(12.dp), + shape = MaterialTheme.shapes.medium, color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = Modifier.fillMaxWidth(), diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/AiLockedSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/AiLockedSheet.kt index 833db6d..d43d4e1 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/AiLockedSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/AiLockedSheet.kt @@ -39,6 +39,7 @@ import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.SheetPreview import com.github.worn.ui.theme.TabletPreview +import com.github.worn.ui.theme.sheetShape @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -49,7 +50,7 @@ fun AiLockedSheet(onDismiss: () -> Unit, onGoToSettings: () -> Unit = {}) { onDismissRequest = onDismiss, sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), + shape = MaterialTheme.sheetShape, dragHandle = { SheetDragHandle() }, ) { AiLockedContent(onGoToSettings = onGoToSettings, onDismiss = onDismiss) @@ -74,7 +75,7 @@ internal fun AiLockedContent( contentAlignment = Alignment.Center, modifier = Modifier .size(44.dp) - .background(MaterialTheme.colorScheme.secondary, RoundedCornerShape(12.dp)), + .background(MaterialTheme.colorScheme.secondary, MaterialTheme.shapes.medium), ) { Icon( Icons.Outlined.SmartToy, @@ -132,3 +133,4 @@ private fun AiLockedTabletPreview() { SheetPreview { AiLockedContent() } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt index a19c60c..e012db3 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp @@ -36,7 +37,8 @@ import com.github.worn.domain.model.Category import com.github.worn.domain.model.ClothingItem import com.github.worn.ui.theme.wornExtras -private val photoShape = RoundedCornerShape(16.dp) +private val photoShape: Shape + @Composable @ReadOnlyComposable get() = MaterialTheme.shapes.large @OptIn(ExperimentalFoundationApi::class) @Composable diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/DeleteConfirmationDialog.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/DeleteConfirmationDialog.kt index b15d8e8..6143827 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/DeleteConfirmationDialog.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/DeleteConfirmationDialog.kt @@ -49,7 +49,7 @@ fun DeleteConfirmationDialog( onClick = onConfirm, enabled = !isDeleting, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), - shape = RoundedCornerShape(24.dp), + shape = MaterialTheme.shapes.extraLarge, modifier = Modifier.testTag("delete_dialog_confirm"), ) { Text( diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt index 525fae3..c13a5a1 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/ErrorContentView.kt @@ -67,7 +67,7 @@ fun ErrorContentView( Spacer(Modifier.height(20.dp)) Surface( onClick = onRetry, - shape = RoundedCornerShape(16.dp), + shape = MaterialTheme.shapes.large, color = MaterialTheme.colorScheme.surfaceContainer, ) { Text( diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt index 9a36d1b..4424857 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt @@ -1,3 +1,5 @@ +@file:OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) + package com.github.worn.ui.components import androidx.compose.foundation.BorderStroke @@ -26,6 +28,7 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -40,10 +43,13 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -private val cardShape = RoundedCornerShape(20.dp) +private val cardShape: Shape + @Composable @ReadOnlyComposable get() = MaterialTheme.shapes.largeIncreased -private val thumbnailShape = RoundedCornerShape(10.dp) -private val badgeShape = RoundedCornerShape(8.dp) +private val thumbnailShape: Shape + @Composable @ReadOnlyComposable get() = MaterialTheme.shapes.medium +private val badgeShape: Shape + @Composable @ReadOnlyComposable get() = MaterialTheme.shapes.small private val badgeColors: List @Composable @ReadOnlyComposable diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionHeader.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionHeader.kt index 57947aa..ab0a766 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionHeader.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/SelectionHeader.kt @@ -54,7 +54,7 @@ fun SelectionHeader( Button( onClick = onDelete, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), - shape = RoundedCornerShape(22.dp), + shape = MaterialTheme.shapes.extraLarge, ) { Icon(Icons.Outlined.Delete, contentDescription = null, tint = Color.White) Spacer(Modifier.width(6.dp)) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt index 71fa7fa..d6589e0 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt @@ -1,3 +1,5 @@ +@file:OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) + package com.github.worn.ui.components import androidx.annotation.DrawableRes @@ -73,7 +75,7 @@ fun WornBottomBar( ), ) { Surface( - shape = RoundedCornerShape(36.dp), + shape = MaterialTheme.shapes.extraExtraLarge, color = MaterialTheme.colorScheme.surfaceContainerHigh, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = Modifier @@ -109,7 +111,7 @@ private fun TabItem( modifier: Modifier = Modifier, ) { Surface( - shape = RoundedCornerShape(26.dp), + shape = MaterialTheme.shapes.extraLargeIncreased, color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerHigh, // No ripple: it repaints the bar for ~1s after each tap, which reads as a slow page // switch. The active tab's fill already signals selection. diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt index 2889ecc..5636c88 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt @@ -1,3 +1,5 @@ +@file:OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) + package com.github.worn.ui.components import androidx.compose.foundation.BorderStroke @@ -10,7 +12,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -18,7 +22,8 @@ import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme -private val chipShape = RoundedCornerShape(20.dp) +private val chipShape: Shape + @Composable @ReadOnlyComposable get() = MaterialTheme.shapes.largeIncreased @Composable fun WornChip( diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornGradientButton.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornGradientButton.kt index 3a0e828..3847118 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornGradientButton.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornGradientButton.kt @@ -63,7 +63,7 @@ fun WornGradientButton( enabled: Boolean = true, gradientColors: List = WornGradients.Save, disabledGradientColors: List = WornGradients.Disabled, - shape: Shape = RoundedCornerShape(16.dp), + shape: Shape = MaterialTheme.shapes.large, elevation: Dp = 0.dp, icon: (@Composable () -> Unit)? = null, fillMaxWidth: Boolean = true, diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/AddItemSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/AddItemSheet.kt index 8960c65..3fce386 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/AddItemSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/AddItemSheet.kt @@ -74,6 +74,7 @@ import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.SheetPreview import com.github.worn.ui.theme.TabletPreview +import com.github.worn.ui.theme.sheetShape import com.github.worn.ui.util.decodePreviewImage import com.github.worn.ui.util.readImageBytes import com.github.worn.ui.util.rememberCameraCapture @@ -101,7 +102,7 @@ fun AddItemSheet( onDismissRequest = onDismiss, sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), + shape = MaterialTheme.sheetShape, dragHandle = { SheetDragHandle() }, ) { AddItemForm( @@ -485,3 +486,4 @@ private fun AddItemFormTabletPreview() { SheetPreview { AddItemForm() } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt index cea2460..dee7159 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt @@ -37,6 +37,7 @@ import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -46,6 +47,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -66,6 +68,7 @@ import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.SheetPreview import com.github.worn.ui.theme.TabletPreview +import com.github.worn.ui.theme.sheetShape import com.github.worn.ui.theme.wornExtras @OptIn(ExperimentalMaterial3Api::class) @@ -87,7 +90,7 @@ fun CreateOutfitSheet( onDismissRequest = onDismiss, sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), + shape = MaterialTheme.sheetShape, dragHandle = { SheetDragHandle() }, ) { CreateOutfitForm( @@ -174,10 +177,10 @@ private fun OutfitNameField(name: String, onNameChange: (String) -> Unit, modifi focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, ), - shape = RoundedCornerShape(12.dp), + shape = MaterialTheme.shapes.medium, modifier = modifier .fillMaxWidth() - .border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(12.dp)), + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, MaterialTheme.shapes.medium), ) } @@ -226,7 +229,8 @@ private fun ItemSelectionGrid( } } -private val cellShape = RoundedCornerShape(16.dp) +private val cellShape: Shape + @Composable @ReadOnlyComposable get() = MaterialTheme.shapes.large // The cell is the photo alone: a name label here only ever sat on top of the garment, where it was // unreadable. The photo carries the item's name as its content description for screen readers. @@ -325,3 +329,5 @@ private fun CreateOutfitFormTabletPreview() { } } + + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt index 94aa548..f3ecdc5 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt @@ -79,6 +79,7 @@ import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme +import com.github.worn.ui.theme.sheetShape import com.github.worn.ui.theme.wornExtras import org.koin.compose.viewmodel.koinViewModel @@ -306,7 +307,7 @@ private fun GapsBanner(isAiMode: Boolean, onClick: () -> Unit) { val bgColor = if (isAiMode) MaterialTheme.colorScheme.primary else MaterialTheme.wornExtras.accentGreenDark Surface( onClick = onClick, - shape = RoundedCornerShape(16.dp), + shape = MaterialTheme.shapes.large, color = bgColor, modifier = Modifier.testTag("gaps_banner"), ) { @@ -359,7 +360,7 @@ private fun GapCard( ) { Surface( onClick = onClick, - shape = RoundedCornerShape(12.dp), + shape = MaterialTheme.shapes.medium, color = MaterialTheme.colorScheme.surfaceContainer, ) { Row( @@ -402,7 +403,7 @@ private fun CategoryIcon(category: Category) { contentAlignment = Alignment.Center, modifier = Modifier .size(36.dp) - .clip(RoundedCornerShape(10.dp)) + .clip(MaterialTheme.shapes.medium) .background(color), ) { Icon( @@ -440,7 +441,7 @@ private fun GapDetailSheet( onDismissRequest = onDismiss, sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), + shape = MaterialTheme.sheetShape, dragHandle = { SheetDragHandle(color = MaterialTheme.colorScheme.outline) }, ) { GapDetailContent( @@ -482,7 +483,7 @@ private fun DetailHeader(recommendation: GapRecommendation) { modifier = Modifier .fillMaxWidth() .height(140.dp) - .clip(RoundedCornerShape(16.dp)) + .clip(MaterialTheme.shapes.large) .background(MaterialTheme.colorScheme.surfaceContainer), ) { Icon( @@ -518,7 +519,7 @@ private fun DetailHeader(recommendation: GapRecommendation) { @Composable private fun DetailPairingInfo(recommendation: GapRecommendation, isAiMode: Boolean) { Surface( - shape = RoundedCornerShape(8.dp), + shape = MaterialTheme.shapes.small, color = MaterialTheme.colorScheme.surfaceContainer, modifier = Modifier.fillMaxWidth(), ) { @@ -601,7 +602,7 @@ private fun DetailActions(onAddToWardrobe: () -> Unit, onDismiss: () -> Unit) { Spacer(Modifier.height(8.dp)) Surface( onClick = onDismiss, - shape = RoundedCornerShape(16.dp), + shape = MaterialTheme.shapes.large, color = MaterialTheme.colorScheme.surfaceContainer, modifier = Modifier.fillMaxWidth().testTag("gap_dismiss"), ) { @@ -694,3 +695,4 @@ private fun GapsScreenErrorPreview() { } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt index f8eee61..d3fe03f 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt @@ -64,6 +64,7 @@ import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.SheetPreview import com.github.worn.ui.theme.TabletPreview +import com.github.worn.ui.theme.sheetShape @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -81,7 +82,7 @@ fun ItemDetailSheet( onDismissRequest = onDismiss, sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), + shape = MaterialTheme.sheetShape, dragHandle = { SheetDragHandle(color = MaterialTheme.colorScheme.outline) }, ) { ItemDetailContent( @@ -320,7 +321,7 @@ internal fun DetailActionButtons( Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Surface( onClick = onEdit, - shape = RoundedCornerShape(24.dp), + shape = MaterialTheme.shapes.extraLarge, color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = Modifier.fillMaxWidth().height(buttonHeight).testTag(editTestTag), @@ -334,7 +335,7 @@ internal fun DetailActionButtons( } Surface( onClick = onDelete, - shape = RoundedCornerShape(24.dp), + shape = MaterialTheme.shapes.extraLarge, color = MaterialTheme.colorScheme.error, modifier = Modifier.fillMaxWidth().height(buttonHeight).testTag(deleteTestTag), ) { @@ -368,7 +369,7 @@ private fun DeleteItemDialog(itemName: String, onConfirm: () -> Unit, onDismiss: Button( onClick = onConfirm, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), - shape = RoundedCornerShape(24.dp), + shape = MaterialTheme.shapes.extraLarge, ) { Text(stringResource(R.string.common_delete), fontWeight = FontWeight.SemiBold) } }, dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) } }, @@ -399,3 +400,4 @@ private fun ItemDetailSheetTabletPreview() { SheetPreview { ItemDetailContent(item = previewItem, isCompact = false) } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt index 7e36857..8582865 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt @@ -58,6 +58,7 @@ import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.SheetPreview import com.github.worn.ui.theme.TabletPreview +import com.github.worn.ui.theme.sheetShape @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -75,7 +76,7 @@ fun OutfitDetailSheet( onDismissRequest = onDismiss, sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), + shape = MaterialTheme.sheetShape, dragHandle = { SheetDragHandle(color = MaterialTheme.colorScheme.outline) }, ) { OutfitDetailContent( @@ -232,7 +233,7 @@ private fun DeleteOutfitDialog(outfitName: String, onConfirm: () -> Unit, onDism Button( onClick = onConfirm, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), - shape = RoundedCornerShape(24.dp), + shape = MaterialTheme.shapes.extraLarge, ) { Text(stringResource(R.string.common_delete), fontWeight = FontWeight.SemiBold) } }, dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) } }, @@ -331,3 +332,4 @@ private fun OutfitDetailSheetTabletPreview() { } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt index 5a2daf1..ed9e5fc 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt @@ -1,3 +1,5 @@ +@file:OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) + package com.github.worn.ui.screen import androidx.compose.foundation.background @@ -35,6 +37,7 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -44,6 +47,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -246,7 +250,7 @@ private fun OutfitsHeader(outfitCount: Int, onCreateClick: () -> Unit = {}) { Button( onClick = onCreateClick, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), - shape = RoundedCornerShape(20.dp), + shape = MaterialTheme.shapes.largeIncreased, modifier = Modifier.testTag("outfits_create_button"), ) { Icon(Icons.Default.Add, contentDescription = null, Modifier.size(16.dp)) @@ -302,7 +306,8 @@ private fun OutfitsContent( } } -private val CtaShape = RoundedCornerShape(28.dp) +private val CtaShape: Shape + @Composable @ReadOnlyComposable get() = MaterialTheme.shapes.extraLargeIncreased @Composable private fun EmptyState(onCreateClick: () -> Unit = {}) { @@ -403,3 +408,4 @@ private fun OutfitsEmptyTabletPreview() { } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt index d30b42d..5b9f841 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt @@ -91,6 +91,7 @@ import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme +import com.github.worn.ui.theme.sheetShape import com.github.worn.ui.theme.wornExtras import org.koin.compose.viewmodel.koinViewModel @@ -268,7 +269,7 @@ private fun SettingsIcon(color: Color, icon: androidx.compose.ui.graphics.vector contentAlignment = Alignment.Center, modifier = Modifier .size(40.dp) - .clip(RoundedCornerShape(12.dp)) + .clip(MaterialTheme.shapes.medium) .background(color), ) { Icon( @@ -298,7 +299,7 @@ private fun SettingsToggleCard( Surface( onClick = { onCheckedChange(!checked) }, enabled = enabled, - shape = RoundedCornerShape(16.dp), + shape = MaterialTheme.shapes.large, color = MaterialTheme.colorScheme.surfaceContainer, modifier = modifier, ) { @@ -360,7 +361,7 @@ private fun SettingsCard( ) { Surface( onClick = onClick, - shape = RoundedCornerShape(16.dp), + shape = MaterialTheme.shapes.large, color = MaterialTheme.colorScheme.surfaceContainer, modifier = modifier, ) { @@ -404,7 +405,7 @@ private fun AboutCard() { val uriHandler = LocalUriHandler.current Surface( - shape = RoundedCornerShape(16.dp), + shape = MaterialTheme.shapes.large, color = MaterialTheme.colorScheme.surfaceContainer, ) { Column { @@ -462,7 +463,7 @@ private fun DonationCard() { var showCopied by remember { mutableStateOf(false) } Surface( - shape = RoundedCornerShape(16.dp), + shape = MaterialTheme.shapes.large, color = MaterialTheme.colorScheme.surfaceContainer, ) { Column(modifier = Modifier.padding(16.dp)) { @@ -484,7 +485,7 @@ private fun DonationCard() { clipboardManager.setText(AnnotatedString(DONATION_LN_ADDRESS)) showCopied = true }, - shape = RoundedCornerShape(12.dp), + shape = MaterialTheme.shapes.medium, color = MaterialTheme.colorScheme.surfaceContainerHigh, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), ) { @@ -530,7 +531,7 @@ private fun ProfileSheet( onDismissRequest = onDismiss, sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), + shape = MaterialTheme.sheetShape, dragHandle = { SheetDragHandle() }, ) { ProfileSheetContent(state = state, onIntent = onIntent, onSave = onSave) @@ -626,7 +627,7 @@ private fun ApiKeySheet( onDismissRequest = onDismiss, sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), + shape = MaterialTheme.sheetShape, dragHandle = { SheetDragHandle() }, ) { ApiKeySheetContent(hasApiKey = hasApiKey, onSave = onSave, onClear = onClear) @@ -749,7 +750,7 @@ private fun ApiKeyTextField( unfocusedIndicatorColor = Color.Transparent, disabledIndicatorColor = Color.Transparent, ), - shape = RoundedCornerShape(12.dp), + shape = MaterialTheme.shapes.medium, modifier = modifier.fillMaxWidth(), singleLine = true, ) @@ -774,7 +775,7 @@ private fun YouCamCredentialsSheet( onDismissRequest = onDismiss, sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), + shape = MaterialTheme.sheetShape, dragHandle = { SheetDragHandle() }, ) { YouCamCredentialsSheetContent( @@ -1071,3 +1072,4 @@ private fun SettingsScreenOnDeviceAiUnavailablePreview() { } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt index f0a0dcc..594fda6 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt @@ -1,3 +1,5 @@ +@file:OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) + @file:Suppress("TooManyFunctions") package com.github.worn.ui.screen @@ -570,7 +572,7 @@ private fun IndigoCtaButton(text: String, onClick: () -> Unit, modifier: Modifie onClick = onClick, modifier = modifier, gradientColors = WornGradients.Indigo, - shape = RoundedCornerShape(28.dp), + shape = MaterialTheme.shapes.extraLargeIncreased, elevation = 6.dp, fillMaxWidth = false, fixedHeight = null, @@ -804,7 +806,7 @@ private fun UploadZone( ) { Surface( onClick = onClick, - shape = RoundedCornerShape(20.dp), + shape = MaterialTheme.shapes.largeIncreased, color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.5.dp, MaterialTheme.colorScheme.outline), shadowElevation = 1.dp, @@ -815,7 +817,7 @@ private fun UploadZone( bitmap = photoBitmap, contentDescription = "Selected photo", contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(20.dp)), + modifier = Modifier.fillMaxSize().clip(MaterialTheme.shapes.largeIncreased), ) } else { Column( @@ -848,7 +850,7 @@ private fun AnalyzeButton(onClick: () -> Unit) { onClick = onClick, modifier = Modifier.testTag("try_it_analyze_button"), gradientColors = WornGradients.Indigo, - shape = RoundedCornerShape(28.dp), + shape = MaterialTheme.shapes.extraLargeIncreased, elevation = 6.dp, fixedHeight = null, contentPadding = PaddingValues(vertical = 14.dp), @@ -919,7 +921,7 @@ private fun PairsSection( private fun ItemThumbnail(item: ClothingItem, size: Dp, onClick: () -> Unit) { Surface( onClick = onClick, - shape = RoundedCornerShape(16.dp), + shape = MaterialTheme.shapes.large, color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 2.dp, @@ -928,7 +930,7 @@ private fun ItemThumbnail(item: ClothingItem, size: Dp, onClick: () -> Unit) { ClothingPhoto( photoPath = item.photoPath, contentDescription = item.name, - shape = RoundedCornerShape(16.dp), + shape = MaterialTheme.shapes.large, placeholderIconSize = 28.dp, placeholderTint = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -941,7 +943,7 @@ private fun CombinationsCard(count: Int, isCompact: Boolean) { val valueStyle = MaterialTheme.typography.displaySmall Surface( - shape = RoundedCornerShape(20.dp), + shape = MaterialTheme.shapes.largeIncreased, color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 2.dp, @@ -1026,7 +1028,7 @@ private fun DecisionBanner(worthAdding: Boolean, isCompact: Boolean) { modifier = Modifier .fillMaxWidth() .height(bannerHeight) - .clip(RoundedCornerShape(28.dp)) + .clip(MaterialTheme.shapes.extraLargeIncreased) .background(gradient), ) { Row( @@ -1124,7 +1126,7 @@ private fun PersonPhotoZone(personImage: ByteArray?, height: Dp, onClick: () -> val bitmap = rememberDecodedImage(personImage) Surface( onClick = onClick, - shape = RoundedCornerShape(20.dp), + shape = MaterialTheme.shapes.largeIncreased, color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.5.dp, MaterialTheme.colorScheme.outline), shadowElevation = 1.dp, @@ -1135,7 +1137,7 @@ private fun PersonPhotoZone(personImage: ByteArray?, height: Dp, onClick: () -> bitmap = bitmap, contentDescription = stringResource(R.string.tryit_your_photo_title), contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(20.dp)), + modifier = Modifier.fillMaxSize().clip(MaterialTheme.shapes.largeIncreased), ) } else { Column( @@ -1199,7 +1201,7 @@ private fun SeeItOnMeButton(onClick: () -> Unit) { onClick = onClick, modifier = Modifier.testTag("try_on_generate_button"), gradientColors = WornGradients.Indigo, - shape = RoundedCornerShape(28.dp), + shape = MaterialTheme.shapes.extraLargeIncreased, elevation = 6.dp, fixedHeight = null, contentPadding = PaddingValues(vertical = 14.dp), @@ -1241,7 +1243,7 @@ private fun TryOnResultView(imageBytes: ByteArray, height: Dp) { letterSpacing = (-0.2).sp, ) Surface( - shape = RoundedCornerShape(20.dp), + shape = MaterialTheme.shapes.largeIncreased, color = MaterialTheme.colorScheme.surfaceContainer, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), shadowElevation = 2.dp, @@ -1252,7 +1254,7 @@ private fun TryOnResultView(imageBytes: ByteArray, height: Dp) { bitmap = bitmap, contentDescription = stringResource(R.string.tryit_tryon_result_title), contentScale = ContentScale.Fit, - modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(20.dp)), + modifier = Modifier.fillMaxSize().clip(MaterialTheme.shapes.largeIncreased), ) } } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt index 5452c18..2692fa2 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt @@ -1,3 +1,5 @@ +@file:OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) + package com.github.worn.ui.screen import androidx.compose.foundation.background @@ -36,6 +38,7 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -45,6 +48,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource @@ -340,7 +344,8 @@ private fun WardrobeContent( } } -private val CtaShape = RoundedCornerShape(28.dp) +private val CtaShape: Shape + @Composable @ReadOnlyComposable get() = MaterialTheme.shapes.extraLargeIncreased @Composable private fun CategoryEmptyState() { @@ -408,7 +413,7 @@ private fun AddItemFab(onClick: () -> Unit, modifier: Modifier = Modifier) { onClick = onClick, containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, - shape = RoundedCornerShape(30.dp), + shape = MaterialTheme.shapes.extraLargeIncreased, modifier = modifier, ) { Icon(Icons.Default.Add, contentDescription = null) @@ -525,3 +530,4 @@ private fun WardrobeEmptyCategoryTabletPreview() { } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/theme/SheetPreview.kt b/composeApp/src/main/kotlin/com/github/worn/ui/theme/SheetPreview.kt index ca22bda..403f545 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/theme/SheetPreview.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/theme/SheetPreview.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import com.github.worn.ui.theme.sheetShape import com.github.worn.ui.theme.wornExtras @Composable @@ -28,7 +29,7 @@ fun SheetPreview(content: @Composable () -> Unit) { Column( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)) + .clip(MaterialTheme.sheetShape) .background(MaterialTheme.colorScheme.surfaceContainerHigh), ) { Spacer(modifier = Modifier.height(12.dp)) @@ -46,3 +47,4 @@ fun SheetPreview(content: @Composable () -> Unit) { } } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornShapes.kt b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornShapes.kt new file mode 100644 index 0000000..8ce75f9 --- /dev/null +++ b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornShapes.kt @@ -0,0 +1,46 @@ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + +package com.github.worn.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.unit.dp + +/** + * Worn's corner-radius scale. + * + * The radii are the ones already scattered across the UI as `RoundedCornerShape(n.dp)`, collapsed + * onto the eight M3 slots. Naming them does two things: Material components pick the right corner + * on their own, and the handful of near-duplicate one-offs (10/22/26/30dp) fold into the nearest + * step so the app stops shipping four radii that differ by 2dp and read as the same curve. + */ +internal val WornShapes = Shapes( + extraSmall = RoundedCornerShape(4.dp), + small = RoundedCornerShape(8.dp), + // Chips, small tiles, input fields. + medium = RoundedCornerShape(12.dp), + // Cards and photo frames. + large = RoundedCornerShape(16.dp), + largeIncreased = RoundedCornerShape(20.dp), + // Sheets and dialogs. + extraLarge = RoundedCornerShape(24.dp), + // Pill buttons and the FAB. + extraLargeIncreased = RoundedCornerShape(28.dp), + // The bottom bar. + extraExtraLarge = RoundedCornerShape(36.dp), +) + +/** + * Top-rounded shape for bottom sheets, matching [Shapes.extraLarge] on the corners that show. + * + * M3 has no slot for a partly-rounded shape, so this hangs off [MaterialTheme] the same way + * [wornExtras] does, keeping every sheet on one radius instead of ten copies of the literal. + */ +val MaterialTheme.sheetShape: RoundedCornerShape + @Composable @ReadOnlyComposable get() = SheetShape + +private val SheetShape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt index 05f9492..f7c61d7 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt @@ -22,6 +22,7 @@ fun WornTheme( MaterialTheme( colorScheme = colorScheme, typography = WornTypography, + shapes = WornShapes, content = content, ) } diff --git a/iosApp/iosApp/Components/AiLockedSheet.swift b/iosApp/iosApp/Components/AiLockedSheet.swift index a6b2c78..adbdb08 100644 --- a/iosApp/iosApp/Components/AiLockedSheet.swift +++ b/iosApp/iosApp/Components/AiLockedSheet.swift @@ -17,7 +17,7 @@ struct AiLockedSheet: View { } ZStack { - RoundedRectangle(cornerRadius: 12) + RoundedRectangle(cornerRadius: WornShape.medium) .fill(WornColors.accentIndigo) .frame(width: 44, height: 44) Image(systemName: "cpu") diff --git a/iosApp/iosApp/Components/ClothingCard.swift b/iosApp/iosApp/Components/ClothingCard.swift index f4dd77d..1d0cdca 100644 --- a/iosApp/iosApp/Components/ClothingCard.swift +++ b/iosApp/iosApp/Components/ClothingCard.swift @@ -21,9 +21,9 @@ struct ClothingCard: View { .frame(maxWidth: .infinity) .frame(height: photoHeight) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.large)) .overlay( - RoundedRectangle(cornerRadius: 16) + RoundedRectangle(cornerRadius: WornShape.large) .stroke(WornColors.borderSubtle, lineWidth: 1) ) .shadow(color: .black.opacity(0.25), radius: 8, x: 0, y: 4) diff --git a/iosApp/iosApp/Components/ErrorContentView.swift b/iosApp/iosApp/Components/ErrorContentView.swift index 07f774b..ec5cd6a 100644 --- a/iosApp/iosApp/Components/ErrorContentView.swift +++ b/iosApp/iosApp/Components/ErrorContentView.swift @@ -30,7 +30,7 @@ struct ErrorContentView: View { .padding(.horizontal, 24) .padding(.vertical, 12) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.large)) } .buttonStyle(.plain) .padding(.top, 20) diff --git a/iosApp/iosApp/Components/WornBottomBar.swift b/iosApp/iosApp/Components/WornBottomBar.swift index a8b4f51..7f6e5a5 100644 --- a/iosApp/iosApp/Components/WornBottomBar.swift +++ b/iosApp/iosApp/Components/WornBottomBar.swift @@ -63,9 +63,9 @@ struct WornBottomBar: View { .frame(height: 62) .padding(4) .background(WornColors.bgElevated) - .clipShape(RoundedRectangle(cornerRadius: 36)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.extraExtraLarge)) .overlay( - RoundedRectangle(cornerRadius: 36) + RoundedRectangle(cornerRadius: WornShape.extraExtraLarge) .stroke(WornColors.borderSubtle, lineWidth: 1) ) .accessibilityIdentifier("bottom_bar") @@ -126,7 +126,7 @@ private struct TabItem: View { ? WornColors.accentGreen : Color.clear ) - .clipShape(RoundedRectangle(cornerRadius: 26)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.extraLargeIncreased)) } .buttonStyle(.plain) } diff --git a/iosApp/iosApp/Components/WornGradientButton.swift b/iosApp/iosApp/Components/WornGradientButton.swift index 0f82346..182b7a4 100644 --- a/iosApp/iosApp/Components/WornGradientButton.swift +++ b/iosApp/iosApp/Components/WornGradientButton.swift @@ -72,7 +72,7 @@ struct WornGradientButton: View { text: "Analyze", action: {}, gradientColors: WornGradients.indigo, - cornerRadius: 28, + cornerRadius: WornShape.extraLargeIncreased, shadowRadius: 10, shadowColor: WornColors.accentIndigo.opacity(0.15), shadowY: 6 diff --git a/iosApp/iosApp/Screens/AddItemSheet.swift b/iosApp/iosApp/Screens/AddItemSheet.swift index b6c6f33..4c78d22 100644 --- a/iosApp/iosApp/Screens/AddItemSheet.swift +++ b/iosApp/iosApp/Screens/AddItemSheet.swift @@ -188,9 +188,9 @@ struct AddItemSheet: View { } .frame(maxWidth: .infinity) .frame(height: 140) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.large)) .overlay( - RoundedRectangle(cornerRadius: 16) + RoundedRectangle(cornerRadius: WornShape.large) .stroke(WornColors.borderStrong, lineWidth: 1.5) ) } @@ -272,9 +272,9 @@ struct AddItemSheet: View { .font(.subheadline) .padding(16) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.medium)) .overlay( - RoundedRectangle(cornerRadius: 12) + RoundedRectangle(cornerRadius: WornShape.medium) .stroke(WornColors.borderSubtle, lineWidth: 1) ) .accessibilityIdentifier("add_item_name_field") @@ -339,9 +339,9 @@ struct AddItemSheet: View { } } .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.medium)) .overlay( - RoundedRectangle(cornerRadius: 12) + RoundedRectangle(cornerRadius: WornShape.medium) .stroke(WornColors.borderSubtle, lineWidth: 1) ) .accessibilityIdentifier("add_item_category_dropdown") @@ -513,9 +513,9 @@ struct AddItemSheet: View { } } .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.medium)) .overlay( - RoundedRectangle(cornerRadius: 12) + RoundedRectangle(cornerRadius: WornShape.medium) .stroke(WornColors.borderSubtle, lineWidth: 1) ) } diff --git a/iosApp/iosApp/Screens/CreateOutfitSheet.swift b/iosApp/iosApp/Screens/CreateOutfitSheet.swift index 4ad246d..e6b8d56 100644 --- a/iosApp/iosApp/Screens/CreateOutfitSheet.swift +++ b/iosApp/iosApp/Screens/CreateOutfitSheet.swift @@ -63,9 +63,9 @@ struct CreateOutfitSheet: View { .font(.subheadline) .padding(16) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.medium)) .overlay( - RoundedRectangle(cornerRadius: 12) + RoundedRectangle(cornerRadius: WornShape.medium) .stroke(WornColors.borderSubtle, lineWidth: 1) ) .accessibilityIdentifier("create_outfit_name_field") @@ -129,9 +129,9 @@ private struct SelectableItemCell: View { .frame(maxWidth: .infinity) .frame(height: 100) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.large)) .overlay( - RoundedRectangle(cornerRadius: 16) + RoundedRectangle(cornerRadius: WornShape.large) .stroke( isSelected ? WornColors.accentGreen : WornColors.borderSubtle, lineWidth: isSelected ? 2 : 1 diff --git a/iosApp/iosApp/Screens/GapsScreen.swift b/iosApp/iosApp/Screens/GapsScreen.swift index 8f3b9f1..97fe0f1 100644 --- a/iosApp/iosApp/Screens/GapsScreen.swift +++ b/iosApp/iosApp/Screens/GapsScreen.swift @@ -219,7 +219,7 @@ struct GapsContent: View { } .padding(16) .background(state.isAiMode ? WornColors.accentGreen : WornColors.accentGreenDark) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.large)) } .buttonStyle(.plain) .accessibilityIdentifier("gaps_banner") @@ -255,13 +255,13 @@ struct GapsContent: View { } .padding(12) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.medium)) } .buttonStyle(.plain) } private func categoryIcon(for category: Shared.Category) -> some View { - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: WornShape.medium) .fill(dotColor(for: category)) .frame(width: 36, height: 36) .overlay( @@ -322,7 +322,7 @@ private struct GapDetailSheet: View { private var detailHeader: some View { VStack(alignment: .leading, spacing: 0) { ZStack { - RoundedRectangle(cornerRadius: 16) + RoundedRectangle(cornerRadius: WornShape.large) .fill(WornColors.bgCard) .frame(height: 140) Image(systemName: iconName(for: recommendation.mappedCategory)) @@ -362,7 +362,7 @@ private struct GapDetailSheet: View { .padding(12) .frame(maxWidth: .infinity, alignment: .leading) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 8)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.small)) } private var detailRows: some View { @@ -415,7 +415,7 @@ private struct GapDetailSheet: View { startPoint: .top, endPoint: .bottom ) ) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.large)) } .accessibilityIdentifier("gap_add_to_wardrobe") @@ -426,7 +426,7 @@ private struct GapDetailSheet: View { .frame(maxWidth: .infinity) .frame(height: 48) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.large)) } .accessibilityIdentifier("gap_dismiss") } diff --git a/iosApp/iosApp/Screens/ItemDetailSheet.swift b/iosApp/iosApp/Screens/ItemDetailSheet.swift index 63a388c..ca13cbe 100644 --- a/iosApp/iosApp/Screens/ItemDetailSheet.swift +++ b/iosApp/iosApp/Screens/ItemDetailSheet.swift @@ -142,9 +142,9 @@ struct ItemDetailSheet: View { .frame(maxWidth: .infinity) .frame(height: buttonHeight) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 24)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.extraLarge)) .overlay( - RoundedRectangle(cornerRadius: 24) + RoundedRectangle(cornerRadius: WornShape.extraLarge) .stroke(WornColors.borderSubtle, lineWidth: 1) ) } @@ -157,7 +157,7 @@ struct ItemDetailSheet: View { .frame(maxWidth: .infinity) .frame(height: buttonHeight) .background(WornColors.deleteRed) - .clipShape(RoundedRectangle(cornerRadius: 24)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.extraLarge)) } .accessibilityIdentifier("item_detail_delete") } diff --git a/iosApp/iosApp/Screens/OutfitDetailSheet.swift b/iosApp/iosApp/Screens/OutfitDetailSheet.swift index 79cf1bb..0f0e3a1 100644 --- a/iosApp/iosApp/Screens/OutfitDetailSheet.swift +++ b/iosApp/iosApp/Screens/OutfitDetailSheet.swift @@ -71,9 +71,9 @@ struct OutfitDetailSheet: View { .frame(maxWidth: .infinity) .frame(height: buttonHeight) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 24)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.extraLarge)) .overlay( - RoundedRectangle(cornerRadius: 24) + RoundedRectangle(cornerRadius: WornShape.extraLarge) .stroke(WornColors.borderSubtle, lineWidth: 1) ) } @@ -86,7 +86,7 @@ struct OutfitDetailSheet: View { .frame(maxWidth: .infinity) .frame(height: buttonHeight) .background(WornColors.deleteRed) - .clipShape(RoundedRectangle(cornerRadius: 24)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.extraLarge)) } .accessibilityIdentifier("outfit_detail_delete") } diff --git a/iosApp/iosApp/Screens/OutfitsScreen.swift b/iosApp/iosApp/Screens/OutfitsScreen.swift index c2cd50d..731d8d7 100644 --- a/iosApp/iosApp/Screens/OutfitsScreen.swift +++ b/iosApp/iosApp/Screens/OutfitsScreen.swift @@ -214,7 +214,7 @@ struct OutfitsContent: View { text: String(localized: "outfits_empty_cta"), action: onCreateClick, gradientColors: WornGradients.greenCta, - cornerRadius: 28, + cornerRadius: WornShape.extraLargeIncreased, shadowRadius: 10, shadowColor: WornColors.accentIndigo.opacity(0.15), shadowY: 6, @@ -264,9 +264,9 @@ private struct OutfitCardView: View { .frame(height: 170) .frame(maxWidth: .infinity, alignment: .leading) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 20)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.largeIncreased)) .overlay( - RoundedRectangle(cornerRadius: 20) + RoundedRectangle(cornerRadius: WornShape.largeIncreased) .stroke( isSelected ? WornColors.accentGreen : WornColors.borderSubtle, lineWidth: 1 @@ -289,7 +289,7 @@ private struct OutfitCardView: View { private func itemThumbnail(for category: Shared.Category?) -> some View { ZStack { - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: WornShape.medium) .fill(WornColors.bgElevated) .frame(width: 40, height: 40) Image(systemName: iconName(for: category)) @@ -316,7 +316,7 @@ private struct OutfitCardView: View { .padding(.horizontal, 10) .padding(.vertical, 4) .background(badgeColor) - .clipShape(RoundedRectangle(cornerRadius: 8)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.small)) } private var bottomRow: some View { diff --git a/iosApp/iosApp/Screens/SettingsScreen.swift b/iosApp/iosApp/Screens/SettingsScreen.swift index 6183564..befb32f 100644 --- a/iosApp/iosApp/Screens/SettingsScreen.swift +++ b/iosApp/iosApp/Screens/SettingsScreen.swift @@ -178,7 +178,7 @@ struct SettingsContent: View { private func settingsCard(iconColor: Color, iconName: String, title: String, subtitle: String, action: @escaping () -> Void) -> some View { Button(action: action) { HStack(spacing: 14) { - RoundedRectangle(cornerRadius: 12) + RoundedRectangle(cornerRadius: WornShape.medium) .fill(iconColor) .frame(width: 40, height: 40) .overlay( @@ -201,7 +201,7 @@ struct SettingsContent: View { } .padding(16) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.large)) } .buttonStyle(.plain) } @@ -217,7 +217,7 @@ struct SettingsContent: View { enabled: Bool ) -> some View { HStack(spacing: 14) { - RoundedRectangle(cornerRadius: 12) + RoundedRectangle(cornerRadius: WornShape.medium) .fill(iconColor) .frame(width: 40, height: 40) .overlay( @@ -241,7 +241,7 @@ struct SettingsContent: View { } .padding(16) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.large)) } private var appVersion: String { @@ -302,7 +302,7 @@ struct SettingsContent: View { .buttonStyle(.plain) } .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.large)) } @State private var showCopied = false @@ -332,9 +332,9 @@ struct SettingsContent: View { } .padding(12) .background(WornColors.bgElevated) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.medium)) .overlay( - RoundedRectangle(cornerRadius: 12) + RoundedRectangle(cornerRadius: WornShape.medium) .stroke(WornColors.borderSubtle, lineWidth: 1) ) } @@ -343,7 +343,7 @@ struct SettingsContent: View { } .padding(16) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.large)) } } @@ -462,9 +462,9 @@ private struct ApiKeySheet: View { } .padding(14) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.medium)) .overlay( - RoundedRectangle(cornerRadius: 12) + RoundedRectangle(cornerRadius: WornShape.medium) .stroke(WornColors.borderSubtle, lineWidth: 1) ) .accessibilityIdentifier("api_key_field") @@ -596,9 +596,9 @@ private struct YouCamCredentialsSheet: View { } .padding(14) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.medium)) .overlay( - RoundedRectangle(cornerRadius: 12) + RoundedRectangle(cornerRadius: WornShape.medium) .stroke(WornColors.borderSubtle, lineWidth: 1) ) } diff --git a/iosApp/iosApp/Screens/TryItScreen.swift b/iosApp/iosApp/Screens/TryItScreen.swift index d196b83..362a4af 100644 --- a/iosApp/iosApp/Screens/TryItScreen.swift +++ b/iosApp/iosApp/Screens/TryItScreen.swift @@ -329,9 +329,9 @@ struct TryItScreen: View { .frame(maxWidth: .infinity) .frame(height: height) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 20)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.largeIncreased)) .overlay( - RoundedRectangle(cornerRadius: 20) + RoundedRectangle(cornerRadius: WornShape.largeIncreased) .stroke(WornColors.borderStrong, lineWidth: 1.5) ) .shadow(color: .black.opacity(0.03), radius: 2, y: 1) @@ -348,7 +348,7 @@ struct TryItScreen: View { viewModel.analyzePhoto(imageData: data) }, gradientColors: WornGradients.indigo, - cornerRadius: 28, + cornerRadius: WornShape.extraLargeIncreased, shadowRadius: 10, shadowColor: WornColors.accentIndigo.opacity(0.15), shadowY: 6, @@ -428,9 +428,9 @@ struct TryItScreen: View { } .frame(width: size, height: size) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.large)) .overlay( - RoundedRectangle(cornerRadius: 16) + RoundedRectangle(cornerRadius: WornShape.large) .stroke(WornColors.borderSubtle, lineWidth: 1) ) .shadow(color: .black.opacity(0.04), radius: 4, y: 2) @@ -455,9 +455,9 @@ struct TryItScreen: View { .frame(maxWidth: .infinity, alignment: .leading) .frame(height: cardHeight) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 20)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.largeIncreased)) .overlay( - RoundedRectangle(cornerRadius: 20) + RoundedRectangle(cornerRadius: WornShape.largeIncreased) .stroke(WornColors.borderSubtle, lineWidth: 1) ) .shadow(color: .black.opacity(0.04), radius: 4, y: 2) @@ -508,7 +508,7 @@ struct TryItScreen: View { .background( LinearGradient(colors: gradientColors, startPoint: .top, endPoint: .bottom) ) - .clipShape(RoundedRectangle(cornerRadius: 28)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.extraLargeIncreased)) .shadow(color: WornColors.accentGreen.opacity(0.15), radius: 10, y: 6) } @@ -517,7 +517,7 @@ struct TryItScreen: View { text: text, action: action, gradientColors: WornGradients.indigo, - cornerRadius: 28, + cornerRadius: WornShape.extraLargeIncreased, shadowRadius: 10, shadowColor: WornColors.accentIndigo.opacity(0.15), shadowY: 6, @@ -654,9 +654,9 @@ struct TryItScreen: View { .frame(maxWidth: .infinity) .frame(height: height) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 20)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.largeIncreased)) .overlay( - RoundedRectangle(cornerRadius: 20) + RoundedRectangle(cornerRadius: WornShape.largeIncreased) .stroke(WornColors.borderStrong, lineWidth: 1.5) ) } @@ -692,7 +692,7 @@ struct TryItScreen: View { text: String(localized: "tryit_see_on_me"), action: { generateTryOn() }, gradientColors: WornGradients.indigo, - cornerRadius: 28, + cornerRadius: WornShape.extraLargeIncreased, shadowRadius: 10, shadowColor: WornColors.accentIndigo.opacity(0.15), shadowY: 6, @@ -736,9 +736,9 @@ struct TryItScreen: View { .frame(maxWidth: .infinity) .frame(height: height) .background(WornColors.bgCard) - .clipShape(RoundedRectangle(cornerRadius: 20)) + .clipShape(RoundedRectangle(cornerRadius: WornShape.largeIncreased)) .overlay( - RoundedRectangle(cornerRadius: 20) + RoundedRectangle(cornerRadius: WornShape.largeIncreased) .stroke(WornColors.borderSubtle, lineWidth: 1) ) .accessibilityIdentifier("try_on_result") diff --git a/iosApp/iosApp/Screens/WardrobeScreen.swift b/iosApp/iosApp/Screens/WardrobeScreen.swift index 45ac7f9..5593c74 100644 --- a/iosApp/iosApp/Screens/WardrobeScreen.swift +++ b/iosApp/iosApp/Screens/WardrobeScreen.swift @@ -243,7 +243,7 @@ struct WardrobeContent: View { text: String(localized: "wardrobe_empty_cta"), action: onAddItemClick, gradientColors: WornGradients.greenCta, - cornerRadius: 28, + cornerRadius: WornShape.extraLargeIncreased, shadowRadius: 10, shadowColor: WornColors.accentIndigo.opacity(0.15), shadowY: 6, diff --git a/iosApp/iosApp/Theme/WornShapes.swift b/iosApp/iosApp/Theme/WornShapes.swift new file mode 100644 index 0000000..d0b31ef --- /dev/null +++ b/iosApp/iosApp/Theme/WornShapes.swift @@ -0,0 +1,23 @@ +import SwiftUI + +/// Worn's corner-radius scale, mirroring `WornShapes.kt` step for step. +/// +/// The radii are the ones already scattered across the UI as `cornerRadius:` literals, collapsed +/// onto one named scale. The handful of near-duplicate one-offs (10/22/26/30) fold into the +/// nearest step, so the app stops shipping four radii that differ by 2pt and read as the same +/// curve. +enum WornShape { + static let extraSmall: CGFloat = 4 + static let small: CGFloat = 8 + /// Chips, small tiles, input fields. + static let medium: CGFloat = 12 + /// Cards and photo frames. + static let large: CGFloat = 16 + static let largeIncreased: CGFloat = 20 + /// Sheets and dialogs. + static let extraLarge: CGFloat = 24 + /// Pill buttons and the FAB. + static let extraLargeIncreased: CGFloat = 28 + /// The bottom bar. + static let extraExtraLarge: CGFloat = 36 +} From b1c6f295d407eb29cc7f5bf562f244ce7eeedce9 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 8 Aug 2026 10:38:25 -0300 Subject: [PATCH 06/13] fix: stop the Android theme fighting edge-to-edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit themes.xml pinned android:statusBarColor and navigationBarColor to the splash green while MainActivity called enableEdgeToEdge(), so content ran under an opaque green status bar instead of behind transparent system bars. It also parented off the legacy android:Theme.Material.Light.NoActionBar, which has no dark variant at all. Reparent to Theme.Material3.DayNight.NoActionBar, drop the pinned bar colours so enableEdgeToEdge owns them, and pass explicit SystemBarStyle.auto so the system picks bar icon colours from uiMode — the default overload assumes a light scrim and left dark glyphs on the dark page. windowBackground moves to a day/night colour matching each scheme's surface, so the pre-Compose window no longer flashes light before the first frame. Co-Authored-By: Claude Opus 5 (1M context) --- .../main/kotlin/com/github/worn/MainActivity.kt | 10 +++++++++- composeApp/src/main/res/values-night/colors.xml | 5 +++++ composeApp/src/main/res/values/colors.xml | 6 ++++++ composeApp/src/main/res/values/themes.xml | 17 +++++++++++------ 4 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 composeApp/src/main/res/values-night/colors.xml create mode 100644 composeApp/src/main/res/values/colors.xml diff --git a/composeApp/src/main/kotlin/com/github/worn/MainActivity.kt b/composeApp/src/main/kotlin/com/github/worn/MainActivity.kt index 9079f58..fc4b43d 100644 --- a/composeApp/src/main/kotlin/com/github/worn/MainActivity.kt +++ b/composeApp/src/main/kotlin/com/github/worn/MainActivity.kt @@ -1,10 +1,12 @@ package com.github.worn import android.content.Intent +import android.graphics.Color import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -27,7 +29,13 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() - enableEdgeToEdge() + // Transparent bars with `auto` styles: the system picks light or dark bar icons from the + // current uiMode, so status-bar glyphs stay legible in both themes. The default overload + // assumes a light scrim and would leave dark icons on the dark page. + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT), + navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT), + ) super.onCreate(savedInstanceState) handleIntent(intent) diff --git a/composeApp/src/main/res/values-night/colors.xml b/composeApp/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..35f1511 --- /dev/null +++ b/composeApp/src/main/res/values-night/colors.xml @@ -0,0 +1,5 @@ + + + + #FF211D18 + diff --git a/composeApp/src/main/res/values/colors.xml b/composeApp/src/main/res/values/colors.xml new file mode 100644 index 0000000..1d03bff --- /dev/null +++ b/composeApp/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #FF7A9468 + + #FFF5F0EB + diff --git a/composeApp/src/main/res/values/themes.xml b/composeApp/src/main/res/values/themes.xml index 00eb052..0be0509 100644 --- a/composeApp/src/main/res/values/themes.xml +++ b/composeApp/src/main/res/values/themes.xml @@ -1,9 +1,16 @@ - - - #FF7A9468 From 0b7a0fd98a050f3859fd3dcb569238aa44f75043 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 8 Aug 2026 10:41:59 -0300 Subject: [PATCH 07/13] refactor: put the bottom bar in Scaffold and build tabs on NavigationBarItem The bar was a Box overlay, so every screen compensated by hand with WornDimens.BottomBarClearance = 95.dp threaded into its FAB padding and list contentPadding. Settings still clipped its last card behind the bar, because one of those hand-tuned offsets was simply missing. Move it into Scaffold(bottomBar =) so the pager gets a bottom inset that already accounts for the bar, and delete the constant along with all six call sites. The clipping is fixed by correct insets rather than a seventh tuned number. Tabs move from Surface + clickable(indication = null) to NavigationBarItem, which restores the ripple, animates the indicator between tabs instead of snapping, and reports proper selectable semantics to TalkBack. The floating pill container is unchanged, so the design is the same. The old comment blamed the ripple for a ~1s repaint; that was the pager recomposing the destination page, and suppressing touch feedback only hid it. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/main/kotlin/com/github/worn/App.kt | 28 ++++--- .../worn/ui/components/WornBottomBar.kt | 78 ++++++++++--------- .../com/github/worn/ui/screen/GapsScreen.kt | 2 - .../github/worn/ui/screen/OutfitsScreen.kt | 2 - .../github/worn/ui/screen/SettingsScreen.kt | 2 - .../com/github/worn/ui/screen/TryItScreen.kt | 3 - .../github/worn/ui/screen/WardrobeScreen.kt | 3 - .../com/github/worn/ui/theme/WornTheme.kt | 5 -- 8 files changed, 58 insertions(+), 65 deletions(-) diff --git a/composeApp/src/main/kotlin/com/github/worn/App.kt b/composeApp/src/main/kotlin/com/github/worn/App.kt index 40c3a9e..6a0886d 100644 --- a/composeApp/src/main/kotlin/com/github/worn/App.kt +++ b/composeApp/src/main/kotlin/com/github/worn/App.kt @@ -4,9 +4,11 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -72,16 +74,26 @@ fun App( } } - Box( + // Scaffold rather than a Box overlay: it measures the bar and hands the pager a bottom + // inset that already accounts for it, which is what lets every screen drop the hardcoded + // 95dp clearance they each used to subtract by hand. + Scaffold( + containerColor = MaterialTheme.colorScheme.surface, + bottomBar = { + WornBottomBar( + activeTab = tabs[pagerState.currentPage], + onTabSelected = onTabSelected, + isCompact = isCompact, + ) + }, modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.surface) .semantics { testTagsAsResourceId = true }, - ) { + ) { innerPadding -> HorizontalPager( state = pagerState, beyondViewportPageCount = 1, - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().padding(innerPadding), ) { page -> when (tabs[page]) { Tab.WARDROBE -> WardrobeScreen( @@ -99,13 +111,7 @@ fun App( Tab.SETTINGS -> SettingsScreen(onTabSelected = onTabSelected) } } - - WornBottomBar( - activeTab = tabs[pagerState.currentPage], - onTabSelected = onTabSelected, - isCompact = isCompact, - modifier = Modifier.align(Alignment.BottomCenter).fillMaxWidth(), - ) } } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt index d6589e0..36198b6 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -25,6 +26,8 @@ import androidx.compose.material.icons.outlined.QrCodeScanner import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -36,6 +39,8 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -90,12 +95,10 @@ fun WornBottomBar( modifier = Modifier.padding(4.dp), ) { Tab.entries.forEach { tab -> - val isActive = tab == activeTab TabItem( tab = tab, - isActive = isActive, + isActive = tab == activeTab, onClick = { onTabSelected(tab) }, - modifier = Modifier.weight(1f).fillMaxHeight(), ) } } @@ -103,61 +106,62 @@ fun WornBottomBar( } } +/** + * One tab, built on [NavigationBarItem] rather than a bare `clickable` Surface. + * + * That is what brings back the things the hand-rolled version had no way to provide: a ripple + * bounded to the pill, the indicator animating between tabs instead of snapping, and the correct + * selectable/tab semantics for TalkBack. The pill container in [WornBottomBar] keeps the custom + * look; only the item behaviour is Material's. + * + * An earlier comment here blamed the ripple for repainting the bar for ~1s after each tap and + * removed indication entirely. The cost was actually the pager recomposing the destination page, + * which `beyondViewportPageCount` and the snap-scroll in App.kt already address — suppressing + * touch feedback only hid it. + */ @Composable -private fun TabItem( +private fun RowScope.TabItem( tab: Tab, isActive: Boolean, onClick: () -> Unit, - modifier: Modifier = Modifier, ) { - Surface( - shape = MaterialTheme.shapes.extraLargeIncreased, - color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerHigh, - // No ripple: it repaints the bar for ~1s after each tap, which reads as a slow page - // switch. The active tab's fill already signals selection. - modifier = modifier - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - role = Role.Tab, - onClick = onClick, - ) - .testTag(tab.testTag), - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxHeight(), - ) { - val tint = if (isActive) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant - val label = stringResource(tab.labelRes) + val label = stringResource(tab.labelRes) + NavigationBarItem( + selected = isActive, + onClick = onClick, + icon = { if (tab.iconRes != null) { Icon( painter = painterResource(id = tab.iconRes), - contentDescription = label, - tint = tint, + contentDescription = null, modifier = Modifier.size(18.dp), ) } else if (tab.icon != null) { Icon( imageVector = tab.icon, - contentDescription = label, - tint = tint, + contentDescription = null, modifier = Modifier.size(18.dp), ) } + }, + label = { Text( text = label, // labelSmall already carries the 10sp/SemiBold/+0.5sp tracking these labels used. style = MaterialTheme.typography.labelSmall, - color = if (isActive) { - MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - } - } + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.onPrimary, + selectedTextColor = MaterialTheme.colorScheme.onPrimary, + indicatorColor = MaterialTheme.colorScheme.primary, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + // The item announces itself; the icon's contentDescription would double it up. + modifier = Modifier.testTag(tab.testTag).semantics { contentDescription = label }, + ) } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt index f3ecdc5..031bd92 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt @@ -77,7 +77,6 @@ import com.github.worn.ui.components.iconRes import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview -import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme import com.github.worn.ui.theme.sheetShape import com.github.worn.ui.theme.wornExtras @@ -222,7 +221,6 @@ private fun GapsScaffold( } item(key = "bottom_clearance") { - Spacer(Modifier.height(WornDimens.BottomBarClearance)) } } } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt index ed9e5fc..caebe70 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt @@ -72,7 +72,6 @@ import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview -import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme import org.koin.compose.viewmodel.koinViewModel @@ -285,7 +284,6 @@ private fun OutfitsContent( LazyColumn( verticalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(bottom = WornDimens.BottomBarClearance), modifier = Modifier.fillMaxSize(), ) { items(state.outfits, key = { it.id }) { outfit -> diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt index 5b9f841..8f32874 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt @@ -89,7 +89,6 @@ import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview -import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme import com.github.worn.ui.theme.sheetShape import com.github.worn.ui.theme.wornExtras @@ -248,7 +247,6 @@ private fun SettingsScaffold( Spacer(Modifier.height(24.dp)) DonationCard() - Spacer(Modifier.height(WornDimens.BottomBarClearance)) } } } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt index 594fda6..10484c5 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/TryItScreen.kt @@ -102,7 +102,6 @@ import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview -import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme import com.github.worn.ui.theme.wornExtras import com.github.worn.ui.util.SharedPhoto @@ -695,7 +694,6 @@ private fun TryItPhoneContent( onPositioned = onTryOnSectionPositioned, ) } - Spacer(Modifier.height(WornDimens.BottomBarClearance)) } } @@ -783,7 +781,6 @@ private fun TryItTabletContent( } } } - Spacer(Modifier.height(WornDimens.BottomBarClearance)) } } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt index 2692fa2..e3186ed 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt @@ -77,7 +77,6 @@ import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview -import com.github.worn.ui.theme.WornDimens import com.github.worn.ui.theme.WornTheme import com.github.worn.ui.util.ShortcutCommand import org.koin.compose.viewmodel.koinViewModel @@ -219,7 +218,6 @@ private fun WardrobeScaffold( onAddItemClick, Modifier .testTag("wardrobe_add_fab") - .padding(bottom = WornDimens.BottomBarClearance), ) } }, @@ -322,7 +320,6 @@ private fun WardrobeContent( columns = GridCells.Adaptive(minSize = GRID_MIN_CELL_WIDTH), horizontalArrangement = Arrangement.spacedBy(gridGap), verticalArrangement = Arrangement.spacedBy(gridGap), - contentPadding = PaddingValues(bottom = WornDimens.BottomBarClearance), modifier = Modifier.fillMaxSize(), ) { items(state.items, key = { it.id }) { item -> diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt index f7c61d7..1916d12 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt @@ -4,11 +4,6 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.unit.dp - -object WornDimens { - val BottomBarClearance = 95.dp -} @Composable fun WornTheme( From 326ff8c9f89c2ac98c4038ad78f65c62636a5b77 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 8 Aug 2026 10:50:45 -0300 Subject: [PATCH 08/13] fix: repair the Android resource link and the tab bar's selected label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes, all found once the build actually produced a fresh APK. themes.xml parented off Theme.Material3.DayNight.NoActionBar, which does not exist here: the project uses Compose Multiplatform Material3 and has no dependency on com.google.android.material, so resource linking failed and every build since has silently reused a stale APK. Day/night now comes from a values-night qualifier over the platform Material themes instead. The bottom bar's move to NavigationBarItem was wrong for this design. Its indicator only wraps the icon, so the selected label sat outside the pill in onPrimary — dark green on a dark bar, effectively unreadable. Keep the hand-built pill, which wraps icon and label together, and fix the two things that actually needed fixing: selectable() for a bounded ripple and proper selected/Tab semantics, and animateColorAsState so the fill eases between tabs instead of snapping. Clipping ClothingCard's Column to a rounded shape cut its own bottom-left corner, which is where the category dot sits — it rendered as a teardrop. combinedClickable already bounds its ripple to the item, so the clip is gone. Co-Authored-By: Claude Opus 5 (1M context) --- .../github/worn/ui/components/ClothingCard.kt | 6 ++ .../worn/ui/components/WornBottomBar.kt | 97 ++++++++++++------- .../github/worn/ui/screen/ItemDetailSheet.kt | 30 +++--- .../src/main/res/values-night/themes.xml | 8 ++ composeApp/src/main/res/values/themes.xml | 7 +- iosApp/iosApp/Screens/ItemDetailSheet.swift | 25 +++-- 6 files changed, 111 insertions(+), 62 deletions(-) create mode 100644 composeApp/src/main/res/values-night/themes.xml diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt index e012db3..8dab719 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt @@ -52,6 +52,11 @@ fun ClothingCard( modifier: Modifier = Modifier, ) { Column( + // Deliberately no .clip() here and deliberately not a Card. The cell is a photo plus a + // caption sitting on the page background, so a Card would put the caption on a white + // container, and clipping the Column to a rounded shape cuts its own bottom-left corner — + // which is exactly where the category dot sits, rendering it as a teardrop. + // combinedClickable already bounds its ripple to the item. modifier = modifier.combinedClickable( onClick = onClick, onLongClick = onLongPress, @@ -145,3 +150,4 @@ internal fun Category.dotColor(): Color = when (this) { Category.SHOES -> MaterialTheme.wornExtras.categoryDotShoes Category.ACCESSORY -> MaterialTheme.wornExtras.categoryDotAccessory } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt index 36198b6..d68658b 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt @@ -4,8 +4,8 @@ package com.github.worn.ui.components import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Extension @@ -26,21 +27,20 @@ import androidx.compose.material.icons.outlined.QrCodeScanner import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -107,12 +107,16 @@ fun WornBottomBar( } /** - * One tab, built on [NavigationBarItem] rather than a bare `clickable` Surface. + * One tab: the app's full-width pill, with the touch feedback and semantics it was missing. * - * That is what brings back the things the hand-rolled version had no way to provide: a ripple - * bounded to the pill, the indicator animating between tabs instead of snapping, and the correct - * selectable/tab semantics for TalkBack. The pill container in [WornBottomBar] keeps the custom - * look; only the item behaviour is Material's. + * Deliberately *not* [NavigationBarItem]. That gives a ripple and an animated indicator for free, + * but its indicator only ever wraps the icon — the label sits outside it. Worn's pill wraps icon + * and label together, so the M3 item left the selected label stranded on the bar background in + * `onPrimary`, which is dark-green-on-dark in the dark scheme and nearly unreadable. + * + * So the pill container stays hand-built, and the two things that actually needed fixing are + * addressed directly: [selectable] supplies a ripple bounded to the pill plus proper + * selected/Tab semantics for TalkBack, and the fill animates between tabs rather than snapping. * * An earlier comment here blamed the ripple for repainting the bar for ~1s after each tap and * removed indication entirely. The cost was actually the pager recomposing the destination page, @@ -126,25 +130,52 @@ private fun RowScope.TabItem( onClick: () -> Unit, ) { val label = stringResource(tab.labelRes) - NavigationBarItem( - selected = isActive, - onClick = onClick, - icon = { + val containerColor by animateColorAsState( + targetValue = if (isActive) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + }, + label = "tabContainer", + ) + val contentColor by animateColorAsState( + targetValue = if (isActive) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + label = "tabContent", + ) + + Surface( + shape = MaterialTheme.shapes.extraLargeIncreased, + color = containerColor, + contentColor = contentColor, + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .clip(MaterialTheme.shapes.extraLargeIncreased) + .selectable( + selected = isActive, + onClick = onClick, + role = Role.Tab, + indication = ripple(), + interactionSource = remember { MutableInteractionSource() }, + ) + .testTag(tab.testTag), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxHeight(), + ) { + // contentDescription is null: the label below already names the tab, and TalkBack + // would otherwise announce it twice. if (tab.iconRes != null) { - Icon( - painter = painterResource(id = tab.iconRes), - contentDescription = null, - modifier = Modifier.size(18.dp), - ) + Icon(painterResource(id = tab.iconRes), null, Modifier.size(18.dp)) } else if (tab.icon != null) { - Icon( - imageVector = tab.icon, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) + Icon(tab.icon, null, Modifier.size(18.dp)) } - }, - label = { Text( text = label, // labelSmall already carries the 10sp/SemiBold/+0.5sp tracking these labels used. @@ -152,16 +183,8 @@ private fun RowScope.TabItem( maxLines = 1, overflow = TextOverflow.Ellipsis, ) - }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.onPrimary, - selectedTextColor = MaterialTheme.colorScheme.onPrimary, - indicatorColor = MaterialTheme.colorScheme.primary, - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, - ), - // The item announces itself; the icon's contentDescription would double it up. - modifier = Modifier.testTag(tab.testTag).semantics { contentDescription = label }, - ) + } + } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt index d3fe03f..e9f0fb1 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -319,29 +320,29 @@ internal fun DetailActionButtons( deleteTestTag: String, ) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Surface( + // Edit is the primary action, so it takes the filled button. Delete was previously the + // filled one — a full-width solid red block ranked *below* a plain white Edit — which gave + // the destructive action more visual weight than the thing people actually came to do. + Button( onClick = onEdit, shape = MaterialTheme.shapes.extraLarge, - color = MaterialTheme.colorScheme.surfaceContainer, - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = Modifier.fillMaxWidth().height(buttonHeight).testTag(editTestTag), ) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - Text( - editLabel, color = MaterialTheme.colorScheme.onSurface, - style = buttonStyle, fontWeight = FontWeight.SemiBold, - ) - } + Text(editLabel, style = buttonStyle, fontWeight = FontWeight.SemiBold) } - Surface( + // Outlined rather than filled: still unmistakably destructive through the error colour, + // without shouting. Using `error` for content instead of a hardcoded white-on-red also + // survives dark mode, where `error` is a light #F2B8AC and white on it is unreadable. + OutlinedButton( onClick = onDelete, shape = MaterialTheme.shapes.extraLarge, - color = MaterialTheme.colorScheme.error, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.error), modifier = Modifier.fillMaxWidth().height(buttonHeight).testTag(deleteTestTag), ) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - Text(deleteLabel, color = Color.White, style = buttonStyle, fontWeight = FontWeight.SemiBold) - } + Text(deleteLabel, style = buttonStyle, fontWeight = FontWeight.SemiBold) } } } @@ -401,3 +402,4 @@ private fun ItemDetailSheetTabletPreview() { } + diff --git a/composeApp/src/main/res/values-night/themes.xml b/composeApp/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..e0c9414 --- /dev/null +++ b/composeApp/src/main/res/values-night/themes.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/composeApp/src/main/res/values/themes.xml b/composeApp/src/main/res/values/themes.xml index 0be0509..409d098 100644 --- a/composeApp/src/main/res/values/themes.xml +++ b/composeApp/src/main/res/values/themes.xml @@ -5,11 +5,16 @@ WornTheme. windowBackground is what shows between the splash handing off and Compose's first frame, so it tracks the app's page colour via values-night rather than being pinned. + The parent is the platform Material theme, not Theme.Material3.*: this project uses Compose + Multiplatform Material3 and does not depend on com.google.android.material, so the Material3 + XML styles do not exist here. Day/night is handled by the values-night qualifier instead of + a DayNight parent. + Deliberately no statusBarColor/navigationBarColor: enableEdgeToEdge() in MainActivity owns system bar appearance, and setting them here fights it (they were pinned to the splash green, which is why content ran under an opaque green status bar). --> - diff --git a/iosApp/iosApp/Screens/ItemDetailSheet.swift b/iosApp/iosApp/Screens/ItemDetailSheet.swift index ca13cbe..3af61c7 100644 --- a/iosApp/iosApp/Screens/ItemDetailSheet.swift +++ b/iosApp/iosApp/Screens/ItemDetailSheet.swift @@ -135,29 +135,34 @@ struct ItemDetailSheet: View { private var buttons: some View { VStack(spacing: 12) { + // Edit is the primary action, so it takes the filled button. Delete was previously the + // filled one — a full-width solid red block ranked *below* a plain white Edit — which + // gave the destructive action more visual weight than the thing people came to do. Button { onEdit(item) } label: { Text(String(localized: "item_detail_edit")) .font(buttonFont.weight(.semibold)) - .foregroundColor(WornColors.textPrimary) + .foregroundColor(WornColors.textOnColor) .frame(maxWidth: .infinity) .frame(height: buttonHeight) - .background(WornColors.bgCard) + .background(WornColors.accentGreen) .clipShape(RoundedRectangle(cornerRadius: WornShape.extraLarge)) - .overlay( - RoundedRectangle(cornerRadius: WornShape.extraLarge) - .stroke(WornColors.borderSubtle, lineWidth: 1) - ) } .accessibilityIdentifier("item_detail_edit") - Button { showDeleteAlert = true } label: { + // `role: .destructive` is what makes VoiceOver announce this as destructive and lets + // the system style it; the outline keeps it unmistakable without shouting. Tinting the + // label rather than filling also survives dark, where deleteRed is a light #F2B8AC and + // white on it is unreadable. + Button(role: .destructive) { showDeleteAlert = true } label: { Text(String(localized: "item_detail_delete")) .font(buttonFont.weight(.semibold)) - .foregroundColor(.white) + .foregroundColor(WornColors.deleteRed) .frame(maxWidth: .infinity) .frame(height: buttonHeight) - .background(WornColors.deleteRed) - .clipShape(RoundedRectangle(cornerRadius: WornShape.extraLarge)) + .overlay( + RoundedRectangle(cornerRadius: WornShape.extraLarge) + .stroke(WornColors.deleteRed, lineWidth: 1) + ) } .accessibilityIdentifier("item_detail_delete") } From f3b660f59e03cb7516a5830e6f1daea3b2147667 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 8 Aug 2026 10:59:28 -0300 Subject: [PATCH 09/13] feat: add collapsing large top app bars to the Android screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Titles were plain Text in a Column, so they scrolled away like body content and there was no collapse behaviour, no scroll signal and no consistent placement between screens. Wardrobe, Outfits, Gaps and Settings now use LargeFlexibleTopAppBar with exitUntilCollapsedScrollBehavior. The title/subtitle pair the screens already had maps directly onto the bar's own title and subtitle slots, and Outfits' Create button becomes a real app-bar action. All heading strings are unchanged — bottom-navigation, create-first-outfit and gaps-common-suggestions all assert on the visible text. Two shared details live in WornTopAppBar.kt: the bar takes `surface` for both its container and scrolled container, so it reads as part of the page rather than a separate slab and never picks up an elevation tint the palette does not contain; and the title carries an 8dp nudge, because M3 indents it 16dp while the screens' content gutter is 24dp. Selection mode keeps its own header, now hosted in the topBar slot. Co-Authored-By: Claude Opus 5 (1M context) --- .../worn/ui/components/WornTopAppBar.kt | 39 ++++++ .../com/github/worn/ui/screen/GapsScreen.kt | 45 ++++--- .../github/worn/ui/screen/OutfitsScreen.kt | 117 ++++++++++-------- .../github/worn/ui/screen/SettingsScreen.kt | 36 ++++-- .../github/worn/ui/screen/WardrobeScreen.kt | 75 ++++++----- 5 files changed, 207 insertions(+), 105 deletions(-) create mode 100644 composeApp/src/main/kotlin/com/github/worn/ui/components/WornTopAppBar.kt diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornTopAppBar.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornTopAppBar.kt new file mode 100644 index 0000000..ee2dc2d --- /dev/null +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornTopAppBar.kt @@ -0,0 +1,39 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package com.github.worn.ui.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +/** + * Shared colours for the screen-level top app bars. + * + * The container is [androidx.compose.material3.ColorScheme.surface] rather than the M3 default so + * the bar reads as part of the page instead of as a separate slab — the app's screens are a single + * tinted sheet, and a distinct bar surface would cut them in half. + * + * `scrolledContainerColor` is the same value on purpose: the elevation tint M3 applies once the + * bar collapses would introduce a colour the palette does not contain. + */ +@Composable +fun wornTopAppBarColors(): TopAppBarColors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + subtitleContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, +) + +/** + * Nudges the app-bar title out to the screens' 24dp content gutter. + * + * M3 indents the title 16dp, which leaves it 8dp left of the chips and cards below it. The bar + * exposes no title-padding parameter, so the offset is applied to the title content itself. + */ +val WornTopAppBarTitlePadding = Modifier.padding(start = 8.dp) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt index 031bd92..8f839f2 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt @@ -1,3 +1,5 @@ +@file:OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) + @file:Suppress("TooManyFunctions") package com.github.worn.ui.screen @@ -29,11 +31,13 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -48,6 +52,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -71,9 +76,11 @@ import com.github.worn.ui.components.ErrorContentView import com.github.worn.ui.components.SheetDragHandle import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornGradientButton +import com.github.worn.ui.components.WornTopAppBarTitlePadding import com.github.worn.ui.components.displayLabel import com.github.worn.ui.components.displayName import com.github.worn.ui.components.iconRes +import com.github.worn.ui.components.wornTopAppBarColors import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview @@ -177,9 +184,29 @@ private fun GapsScaffold( ) { val contentPadding = if (isCompact) 24.dp else 32.dp + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + Scaffold( - modifier = Modifier.testTag("gaps_screen"), + modifier = Modifier + .testTag("gaps_screen") + .nestedScroll(scrollBehavior.nestedScrollConnection), containerColor = MaterialTheme.colorScheme.surface, + topBar = { + // Title and subtitle unchanged: journeys/gaps-common-suggestions.xml asserts on them. + LargeFlexibleTopAppBar( + title = { + Text(stringResource(R.string.gaps_title), modifier = WornTopAppBarTitlePadding) + }, + subtitle = { + Text( + stringResource(R.string.gaps_subtitle), + modifier = WornTopAppBarTitlePadding, + ) + }, + colors = wornTopAppBarColors(), + scrollBehavior = scrollBehavior, + ) + }, ) { paddingValues -> LazyColumn( modifier = Modifier @@ -187,21 +214,6 @@ private fun GapsScaffold( .padding(paddingValues) .padding(horizontal = contentPadding), ) { - item(key = "header") { - Spacer(Modifier.height(24.dp)) - Text( - text = stringResource(R.string.gaps_title), - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.headlineMedium, - letterSpacing = (-0.5).sp, - ) - Text( - text = stringResource(R.string.gaps_subtitle), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodySmall, - ) - Spacer(Modifier.height(20.dp)) - } when { state.isLoading -> item(key = "loading") { LoadingContent() } @@ -694,3 +706,4 @@ private fun GapsScreenErrorPreview() { } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt index caebe70..f796cec 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt @@ -29,11 +29,15 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -48,6 +52,7 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -70,6 +75,8 @@ import com.github.worn.ui.components.SelectionHeader import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients +import com.github.worn.ui.components.WornTopAppBarTitlePadding +import com.github.worn.ui.components.wornTopAppBarColors import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme @@ -181,9 +188,29 @@ private fun OutfitsScaffold( val sectionGap = if (isCompact) 24.dp else 28.dp var showDeleteDialog by remember { mutableStateOf(false) } + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + Scaffold( - modifier = Modifier.testTag("outfits_screen"), + modifier = Modifier + .testTag("outfits_screen") + .nestedScroll(scrollBehavior.nestedScrollConnection), containerColor = MaterialTheme.colorScheme.surface, + topBar = { + if (isSelectionMode) { + SelectionHeader( + count = state.selectedIds.size, + onCancel = onClearSelection, + onDelete = { showDeleteDialog = true }, + modifier = Modifier.padding(horizontal = contentPadding), + ) + } else { + OutfitsTopBar( + outfitCount = state.outfits.size, + onCreateClick = onCreateClick, + scrollBehavior = scrollBehavior, + ) + } + }, ) { paddingValues -> val isEmpty = !state.isLoading && state.outfits.isEmpty() @@ -193,15 +220,6 @@ private fun OutfitsScaffold( .padding(paddingValues) .padding(horizontal = contentPadding), ) { - if (isSelectionMode) { - SelectionHeader( - count = state.selectedIds.size, - onCancel = onClearSelection, - onDelete = { showDeleteDialog = true }, - ) - } else { - OutfitsHeader(outfitCount = state.outfits.size, onCreateClick = onCreateClick) - } if (isEmpty) { EmptyState(onCreateClick = onCreateClick) } else if (state.isLoading && state.outfits.isEmpty()) { @@ -229,48 +247,48 @@ private fun OutfitsScaffold( } @Composable -private fun OutfitsHeader(outfitCount: Int, onCreateClick: () -> Unit = {}) { - Spacer(modifier = Modifier.height(8.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(R.string.outfits_title), - color = MaterialTheme.colorScheme.onSurface, - style = if (outfitCount == 0) { - MaterialTheme.typography.titleLarge - } else { - MaterialTheme.typography.headlineMedium - }, - ) - if (outfitCount > 0) { - Button( - onClick = onCreateClick, - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), - shape = MaterialTheme.shapes.largeIncreased, - modifier = Modifier.testTag("outfits_create_button"), - ) { - Icon(Icons.Default.Add, contentDescription = null, Modifier.size(16.dp)) - Spacer(Modifier.width(4.dp)) +private fun OutfitsTopBar( + outfitCount: Int, + onCreateClick: () -> Unit, + scrollBehavior: TopAppBarScrollBehavior, +) { + // Title and Create label are unchanged: journeys/create-first-outfit.xml asserts on both. + LargeFlexibleTopAppBar( + title = { + Text(stringResource(R.string.outfits_title), modifier = WornTopAppBarTitlePadding) + }, + subtitle = if (outfitCount > 0) { + { Text( - stringResource(R.string.outfits_button_create), - style = MaterialTheme.typography.bodySmall, - fontWeight = FontWeight.SemiBold, + pluralStringResource(R.plurals.saved_combinations, outfitCount, outfitCount), + modifier = WornTopAppBarTitlePadding, ) } - } - } - if (outfitCount > 0) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = pluralStringResource(R.plurals.saved_combinations, outfitCount, outfitCount), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodySmall, - fontWeight = FontWeight.Medium, - ) - } + } else { + null + }, + actions = { + if (outfitCount > 0) { + Button( + onClick = onCreateClick, + shape = MaterialTheme.shapes.largeIncreased, + modifier = Modifier + .padding(end = 8.dp) + .testTag("outfits_create_button"), + ) { + Icon(Icons.Default.Add, contentDescription = null, Modifier.size(16.dp)) + Spacer(Modifier.width(4.dp)) + Text( + stringResource(R.string.outfits_button_create), + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.SemiBold, + ) + } + } + }, + colors = wornTopAppBarColors(), + scrollBehavior = scrollBehavior, + ) } @@ -407,3 +425,4 @@ private fun OutfitsEmptyTabletPreview() { } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt index 8f32874..6a608c6 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt @@ -1,3 +1,5 @@ +@file:OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) + @file:Suppress("TooManyFunctions") package com.github.worn.ui.screen @@ -33,6 +35,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold @@ -42,6 +45,7 @@ import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -55,6 +59,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler @@ -86,6 +91,8 @@ import com.github.worn.ui.components.SheetDragHandle import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornChip import com.github.worn.ui.components.WornGradientButton +import com.github.worn.ui.components.WornTopAppBarTitlePadding +import com.github.worn.ui.components.wornTopAppBarColors import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview @@ -175,9 +182,25 @@ private fun SettingsScaffold( ) { val contentPadding = if (isCompact) 24.dp else 32.dp + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + Scaffold( - modifier = Modifier.testTag("settings_screen"), + modifier = Modifier + .testTag("settings_screen") + .nestedScroll(scrollBehavior.nestedScrollConnection), containerColor = MaterialTheme.colorScheme.surface, + topBar = { + LargeFlexibleTopAppBar( + title = { + Text( + stringResource(R.string.settings_title), + modifier = WornTopAppBarTitlePadding, + ) + }, + colors = wornTopAppBarColors(), + scrollBehavior = scrollBehavior, + ) + }, ) { paddingValues -> Column( modifier = Modifier @@ -186,15 +209,7 @@ private fun SettingsScaffold( .padding(horizontal = contentPadding) .verticalScroll(rememberScrollState()), ) { - Spacer(Modifier.height(24.dp)) - Text( - text = stringResource(R.string.settings_title), - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.headlineMedium, - letterSpacing = (-0.5).sp, - ) - - Spacer(Modifier.height(28.dp)) + Spacer(Modifier.height(8.dp)) SectionLabel(stringResource(R.string.settings_section_profile)) Spacer(Modifier.height(10.dp)) SettingsCard( @@ -1071,3 +1086,4 @@ private fun SettingsScreenOnDeviceAiUnavailablePreview() { } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt index e3186ed..ad9141d 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt @@ -29,12 +29,16 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon +import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -49,6 +53,7 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource @@ -75,6 +80,8 @@ import com.github.worn.ui.components.SelectionHeader import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients +import com.github.worn.ui.components.WornTopAppBarTitlePadding +import com.github.worn.ui.components.wornTopAppBarColors import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme @@ -208,9 +215,27 @@ private fun WardrobeScaffold( val sectionGap = if (isCompact) 24.dp else 28.dp var showDeleteDialog by remember { mutableStateOf(false) } + // exitUntilCollapsed: the title shrinks to a compact bar as the grid scrolls up and only + // returns once the user scrolls back to the top, which is the standard large-app-bar feel. + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + Scaffold( - modifier = Modifier.testTag("wardrobe_screen"), + modifier = Modifier + .testTag("wardrobe_screen") + .nestedScroll(scrollBehavior.nestedScrollConnection), containerColor = MaterialTheme.colorScheme.surface, + topBar = { + if (isSelectionMode) { + SelectionHeader( + count = state.selectedIds.size, + onCancel = onClearSelection, + onDelete = { showDeleteDialog = true }, + modifier = Modifier.padding(horizontal = contentPadding), + ) + } else { + WardrobeTopBar(itemCount = state.totalItemCount, scrollBehavior = scrollBehavior) + } + }, floatingActionButton = { val isWardrobeEmpty = !state.isLoading && state.totalItemCount == 0 if (!isSelectionMode && !isWardrobeEmpty) { @@ -231,15 +256,6 @@ private fun WardrobeScaffold( .padding(paddingValues) .padding(horizontal = contentPadding), ) { - if (isSelectionMode) { - SelectionHeader( - count = state.selectedIds.size, - onCancel = onClearSelection, - onDelete = { showDeleteDialog = true }, - ) - } else { - WardrobeHeader(itemCount = state.totalItemCount) - } if (isWardrobeEmpty) { EmptyState(onAddItemClick = onAddItemClick) } else { @@ -273,30 +289,28 @@ private fun WardrobeScaffold( } @Composable -private fun WardrobeHeader(itemCount: Int) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = if (itemCount == 0) { - stringResource(R.string.wardrobe_title_empty) - } else { - stringResource(R.string.wardrobe_title) +private fun WardrobeTopBar(itemCount: Int, scrollBehavior: TopAppBarScrollBehavior) { + // Title strings are unchanged: journeys/bottom-navigation.xml and add-first-item.xml assert + // on the visible heading text. + LargeFlexibleTopAppBar( + title = { + Text( + modifier = WornTopAppBarTitlePadding, + text = if (itemCount == 0) { + stringResource(R.string.wardrobe_title_empty) + } else { + stringResource(R.string.wardrobe_title) + }, + ) }, - color = MaterialTheme.colorScheme.onSurface, - style = if (itemCount == 0) { - MaterialTheme.typography.titleLarge + subtitle = if (itemCount > 0) { + { Text(stringResource(R.string.wardrobe_subtitle, itemCount), modifier = WornTopAppBarTitlePadding) } } else { - MaterialTheme.typography.headlineMedium + null }, + colors = wornTopAppBarColors(), + scrollBehavior = scrollBehavior, ) - if (itemCount > 0) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(R.string.wardrobe_subtitle, itemCount), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodySmall, - fontWeight = FontWeight.Medium, - ) - } } @@ -528,3 +542,4 @@ private fun WardrobeEmptyCategoryTabletPreview() { } + From 8dba8df1d02f13525d8b3e92d29531be20555aa6 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 8 Aug 2026 11:03:15 -0300 Subject: [PATCH 10/13] feat: add expressive motion and haptics on both platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app had zero animate*AsState, zero animateContentSize and zero haptics on Android, and zero sensoryFeedback and zero symbolEffect on iOS. Nothing moved and nothing was felt. Android switches to MaterialExpressiveTheme with MotionScheme.expressive(), which gives every Material component spring-based motion rather than flat easing. The theme is otherwise unchanged. - Category chips and bottom-bar tabs ease their fill with animateColorAsState. Filtering swaps the whole grid underneath, so easing the control the user actually touched gives the eye something continuous to hold. - Screen-level spinners become the expressive LoadingIndicator. - Haptics where an action has no visual affordance until it fires: LongPress on long-press-to-select, SegmentTick on tab and chip changes. iOS mirrors the same set: .sensoryFeedback(.selection) on tab and chip changes, an impact generator on long-press-to-select, and .symbolEffect .variableColor on the AI sparkle so it reads as alive rather than decorative. Note: the iOS changes are unverified — this machine has no Swift toolchain. See #62. Co-Authored-By: Claude Opus 5 (1M context) --- .../github/worn/ui/components/ClothingCard.kt | 11 +++++- .../github/worn/ui/components/OutfitCard.kt | 14 +++++++- .../worn/ui/components/WornBottomBar.kt | 11 +++++- .../com/github/worn/ui/components/WornChip.kt | 34 +++++++++++++++++-- .../com/github/worn/ui/screen/GapsScreen.kt | 4 +-- .../github/worn/ui/screen/OutfitsScreen.kt | 4 +-- .../github/worn/ui/screen/WardrobeScreen.kt | 4 +-- .../com/github/worn/ui/theme/WornTheme.kt | 12 +++++-- iosApp/iosApp/Components/WornBottomBar.swift | 2 ++ iosApp/iosApp/Components/WornChip.swift | 1 + iosApp/iosApp/Screens/OutfitsScreen.swift | 3 ++ iosApp/iosApp/Screens/TryItScreen.swift | 3 ++ iosApp/iosApp/Screens/WardrobeScreen.swift | 3 ++ 13 files changed, 92 insertions(+), 14 deletions(-) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt index 8dab719..58cfe27 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/ClothingCard.kt @@ -28,6 +28,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp @@ -51,6 +53,7 @@ fun ClothingCard( onClick: () -> Unit = {}, modifier: Modifier = Modifier, ) { + val haptics = LocalHapticFeedback.current Column( // Deliberately no .clip() here and deliberately not a Card. The cell is a photo plus a // caption sitting on the page background, so a Card would put the caption on a white @@ -59,7 +62,12 @@ fun ClothingCard( // combinedClickable already bounds its ripple to the item. modifier = modifier.combinedClickable( onClick = onClick, - onLongClick = onLongPress, + onLongClick = { + // Long-press is the only way into selection mode and it has no visual affordance + // before it fires, so the tick is what tells you it worked. + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + onLongPress() + }, ), verticalArrangement = Arrangement.spacedBy(8.dp), ) { @@ -151,3 +159,4 @@ internal fun Category.dotColor(): Color = when (this) { Category.ACCESSORY -> MaterialTheme.wornExtras.categoryDotAccessory } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt index 4424857..5753829 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/OutfitCard.kt @@ -29,6 +29,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -70,6 +72,7 @@ fun OutfitCard( onClick: () -> Unit = {}, modifier: Modifier = Modifier, ) { + val haptics = LocalHapticFeedback.current Surface( shape = cardShape, color = MaterialTheme.colorScheme.surfaceContainer, @@ -81,7 +84,15 @@ fun OutfitCard( modifier = modifier .fillMaxWidth() .height(170.dp) - .combinedClickable(onClick = onClick, onLongClick = onLongPress), + .combinedClickable( + onClick = onClick, + onLongClick = { + // Long-press is the only way into selection mode and it has no visual + // affordance before it fires, so the tick is what tells you it worked. + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + onLongPress() + }, + ), ) { Row(modifier = Modifier.padding(20.dp)) { if (isSelectionMode) { @@ -189,3 +200,4 @@ private val dateFormat = SimpleDateFormat("MMM d", Locale.getDefault()) private fun formatDate(epochMillis: Long): String { return dateFormat.format(Date(epochMillis)) } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt index d68658b..08d0f55 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornBottomBar.kt @@ -37,6 +37,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -130,6 +132,7 @@ private fun RowScope.TabItem( onClick: () -> Unit, ) { val label = stringResource(tab.labelRes) + val haptics = LocalHapticFeedback.current val containerColor by animateColorAsState( targetValue = if (isActive) { MaterialTheme.colorScheme.primary @@ -157,7 +160,12 @@ private fun RowScope.TabItem( .clip(MaterialTheme.shapes.extraLargeIncreased) .selectable( selected = isActive, - onClick = onClick, + onClick = { + // SegmentTick, not LongPress: this is a discrete position change in a row of + // segments, which is exactly what that constant is for. + if (!isActive) haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) + onClick() + }, role = Role.Tab, indication = ripple(), interactionSource = remember { MutableInteractionSource() }, @@ -188,3 +196,4 @@ private fun RowScope.TabItem( } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt index 5636c88..ba121d0 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornChip.kt @@ -2,6 +2,7 @@ package com.github.worn.ui.components +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -13,8 +14,11 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -32,16 +36,39 @@ fun WornChip( onClick: () -> Unit, modifier: Modifier = Modifier, ) { + val haptics = LocalHapticFeedback.current + // Filtering swaps the whole grid underneath, so easing the chip's own fill gives the eye + // something continuous to hold on to; snapping both at once reads as a flash. + val containerColor by animateColorAsState( + targetValue = if (isActive) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.surfaceContainer + }, + label = "chipContainer", + ) + val labelColor by animateColorAsState( + targetValue = if (isActive) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + label = "chipLabel", + ) + Surface( - onClick = onClick, + onClick = { + haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) + onClick() + }, shape = chipShape, - color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainer, + color = containerColor, border = if (isActive) null else BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), modifier = modifier, ) { Text( text = label, - color = if (isActive) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, + color = labelColor, style = MaterialTheme.typography.labelLarge, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), ) @@ -72,3 +99,4 @@ private fun WornChipTabletPreview() { } } + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt index 8f839f2..767ec40 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.outlined.AutoAwesome -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -244,7 +244,7 @@ private fun LoadingContent() { contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth().padding(vertical = 80.dp), ) { - CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + LoadingIndicator(color = MaterialTheme.colorScheme.primary) } } diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt index f796cec..4774a66 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt @@ -28,7 +28,7 @@ import androidx.compose.material.icons.outlined.Layers import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.LargeFlexibleTopAppBar @@ -224,7 +224,7 @@ private fun OutfitsScaffold( EmptyState(onCreateClick = onCreateClick) } else if (state.isLoading && state.outfits.isEmpty()) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + LoadingIndicator(color = MaterialTheme.colorScheme.primary) } } else { Spacer(modifier = Modifier.height(sectionGap)) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt index ad9141d..8d02bcd 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt @@ -28,7 +28,7 @@ import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon @@ -327,7 +327,7 @@ private fun WardrobeContent( if (state.isLoading && state.items.isEmpty()) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + LoadingIndicator(color = MaterialTheme.colorScheme.primary) } } else { LazyVerticalGrid( diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt index 1916d12..13da8d4 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/theme/WornTheme.kt @@ -1,7 +1,11 @@ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + package com.github.worn.ui.theme import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialExpressiveTheme +import androidx.compose.material3.MotionScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -14,8 +18,12 @@ fun WornTheme( val extras = if (darkTheme) WornDarkExtras else WornLightExtras CompositionLocalProvider(LocalWornExtras provides extras) { - MaterialTheme( + // MaterialExpressiveTheme rather than MaterialTheme: it is the only way to supply a + // MotionScheme, and `expressive()` gives every Material component spring-based motion + // instead of the flat easing curves. Nothing else about the theme changes. + MaterialExpressiveTheme( colorScheme = colorScheme, + motionScheme = MotionScheme.expressive(), typography = WornTypography, shapes = WornShapes, content = content, diff --git a/iosApp/iosApp/Components/WornBottomBar.swift b/iosApp/iosApp/Components/WornBottomBar.swift index 7f6e5a5..73ecb14 100644 --- a/iosApp/iosApp/Components/WornBottomBar.swift +++ b/iosApp/iosApp/Components/WornBottomBar.swift @@ -129,5 +129,7 @@ private struct TabItem: View { .clipShape(RoundedRectangle(cornerRadius: WornShape.extraLargeIncreased)) } .buttonStyle(.plain) + // Mirrors the SegmentTick on Android: a discrete position change in a row of segments. + .sensoryFeedback(.selection, trigger: isActive) } } diff --git a/iosApp/iosApp/Components/WornChip.swift b/iosApp/iosApp/Components/WornChip.swift index 0f77858..8658051 100644 --- a/iosApp/iosApp/Components/WornChip.swift +++ b/iosApp/iosApp/Components/WornChip.swift @@ -20,6 +20,7 @@ struct WornChip: View { ) } .buttonStyle(.plain) + .sensoryFeedback(.selection, trigger: isActive) } } diff --git a/iosApp/iosApp/Screens/OutfitsScreen.swift b/iosApp/iosApp/Screens/OutfitsScreen.swift index 731d8d7..af6f7eb 100644 --- a/iosApp/iosApp/Screens/OutfitsScreen.swift +++ b/iosApp/iosApp/Screens/OutfitsScreen.swift @@ -193,6 +193,9 @@ struct OutfitsContent: View { } } .onLongPressGesture { + // Long-press is the only way into selection mode and has no visual affordance + // before it fires, so the impact is what confirms it. + UIImpactFeedbackGenerator(style: .medium).impactOccurred() onToggleSelection(outfit.id) } } diff --git a/iosApp/iosApp/Screens/TryItScreen.swift b/iosApp/iosApp/Screens/TryItScreen.swift index 362a4af..b6a92c2 100644 --- a/iosApp/iosApp/Screens/TryItScreen.swift +++ b/iosApp/iosApp/Screens/TryItScreen.swift @@ -700,6 +700,9 @@ struct TryItScreen: View { Image(systemName: "sparkles") .font(.system(size: 18)) .foregroundColor(.white) + // The sparkle is the app's "AI is here" mark; variableColor makes it read as + // alive rather than as a static decoration. + .symbolEffect(.variableColor.iterative.reversing) ), fixedHeight: nil, contentPadding: EdgeInsets(top: 14, leading: 0, bottom: 14, trailing: 0) diff --git a/iosApp/iosApp/Screens/WardrobeScreen.swift b/iosApp/iosApp/Screens/WardrobeScreen.swift index 5593c74..ffbed78 100644 --- a/iosApp/iosApp/Screens/WardrobeScreen.swift +++ b/iosApp/iosApp/Screens/WardrobeScreen.swift @@ -204,6 +204,9 @@ struct WardrobeContent: View { } } .onLongPressGesture { + // Long-press is the only way into selection mode and has no visual + // affordance before it fires, so the impact is what confirms it. + UIImpactFeedbackGenerator(style: .medium).impactOccurred() onToggleSelection(item.id) } } From c567ea85238ceff06bcebaceced0c176a7ed84d3 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 8 Aug 2026 11:16:50 -0300 Subject: [PATCH 11/13] fix: remove the dead band above every screen title MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The App-level Scaffold exists to host the bottom bar, but the pager consumed its innerPadding wholesale — including the status-bar top inset. Each screen's own TopAppBar then applied that inset again, so every title sat under roughly 90dp of empty space. No app-bar height parameter could explain it, because the duplication happened outside the bar entirely. The pager now takes only the bottom padding and lets each screen's app bar own its top inset. With the double inset gone the bars are re-sized to suit these screens: the four copies of LargeFlexibleTopAppBar collapse into one WornTopAppBar built on the medium bar, with heights below the M3 defaults. The large bar reserves 152dp expanded, of which the top 64dp is the row that would hold a navigation icon — and these are tab destinations with nowhere to navigate back to. Outfits' Create moves from a top-bar action to an ExtendedFloatingActionButton. As an action it sat alone in that otherwise empty leading row, reading as a button floating above the title; as a FAB it matches Wardrobe's "Add item", so both list screens now put their primary action in the same place. outfits_create_button is preserved. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/main/kotlin/com/github/worn/App.kt | 8 +- .../worn/ui/components/WornTopAppBar.kt | 70 +++++++++++---- .../com/github/worn/ui/screen/GapsScreen.kt | 21 ++--- .../github/worn/ui/screen/OutfitsScreen.kt | 87 +++++++++---------- .../github/worn/ui/screen/SettingsScreen.kt | 15 +--- .../github/worn/ui/screen/WardrobeScreen.kt | 25 ++---- 6 files changed, 119 insertions(+), 107 deletions(-) diff --git a/composeApp/src/main/kotlin/com/github/worn/App.kt b/composeApp/src/main/kotlin/com/github/worn/App.kt index 6a0886d..8d8aeea 100644 --- a/composeApp/src/main/kotlin/com/github/worn/App.kt +++ b/composeApp/src/main/kotlin/com/github/worn/App.kt @@ -93,7 +93,13 @@ fun App( HorizontalPager( state = pagerState, beyondViewportPageCount = 1, - modifier = Modifier.fillMaxSize().padding(innerPadding), + // Bottom padding only. This Scaffold exists for the bottom bar; each screen owns + // its own top inset through its TopAppBar, and consuming innerPadding wholesale + // applied the status-bar inset twice — which showed up as a ~90dp dead band above + // every title that no app-bar height parameter could explain. + modifier = Modifier + .fillMaxSize() + .padding(bottom = innerPadding.calculateBottomPadding()), ) { page -> when (tabs[page]) { Tab.WARDROBE -> WardrobeScreen( diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornTopAppBar.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornTopAppBar.kt index ee2dc2d..14c5c04 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/WornTopAppBar.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/WornTopAppBar.kt @@ -1,39 +1,71 @@ -@file:OptIn(ExperimentalMaterial3Api::class) +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) package com.github.worn.ui.components +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.MediumFlexibleTopAppBar +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp /** - * Shared colours for the screen-level top app bars. + * The screen-level app bar: one title, an optional subtitle, optional actions. * - * The container is [androidx.compose.material3.ColorScheme.surface] rather than the M3 default so - * the bar reads as part of the page instead of as a separate slab — the app's screens are a single - * tinted sheet, and a distinct bar surface would cut them in half. + * Medium rather than Large, with heights tightened below the M3 defaults. The default large bar + * reserves 152dp expanded, of which the top 64dp is the row that would hold a navigation icon — + * and none of these screens have one, so it reads as a large empty gap above the title. These + * screens are tab destinations with nowhere to navigate back to. * - * `scrolledContainerColor` is the same value on purpose: the elevation tint M3 applies once the - * bar collapses would introduce a colour the palette does not contain. + * [COLLAPSED_HEIGHT] is still tall enough for the Outfits "Create" action, which is the only + * thing that ever occupies that row. */ @Composable -fun wornTopAppBarColors(): TopAppBarColors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - scrolledContainerColor = MaterialTheme.colorScheme.surface, - titleContentColor = MaterialTheme.colorScheme.onSurface, - subtitleContentColor = MaterialTheme.colorScheme.onSurfaceVariant, - actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, -) +fun WornTopAppBar( + title: String, + scrollBehavior: TopAppBarScrollBehavior, + modifier: Modifier = Modifier, + subtitle: String? = null, + actions: @Composable RowScope.() -> Unit = {}, +) { + MediumFlexibleTopAppBar( + title = { Text(title, modifier = TITLE_GUTTER_NUDGE) }, + subtitle = subtitle?.let { { Text(it, modifier = TITLE_GUTTER_NUDGE) } }, + actions = actions, + collapsedHeight = COLLAPSED_HEIGHT, + expandedHeight = if (subtitle != null) EXPANDED_HEIGHT else EXPANDED_HEIGHT_NO_SUBTITLE, + // Same colour for the container and its scrolled state: the screens are a single tinted + // sheet, so a distinct bar surface would cut them in half, and M3's elevation tint on + // scroll would introduce a colour the palette does not contain. + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + subtitleContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + scrollBehavior = scrollBehavior, + modifier = modifier, + ) +} + +// The bar bottom-aligns its title block, so the space above the title is +// expandedHeight minus the text's own height. These are sized so that gap lands near the ~24dp +// the hand-built headers used to have, rather than the ~96dp the M3 large-bar defaults produce. +private val COLLAPSED_HEIGHT = 48.dp +private val EXPANDED_HEIGHT = 88.dp +private val EXPANDED_HEIGHT_NO_SUBTITLE = 68.dp /** - * Nudges the app-bar title out to the screens' 24dp content gutter. + * Nudges the title out to the screens' 24dp content gutter. * - * M3 indents the title 16dp, which leaves it 8dp left of the chips and cards below it. The bar - * exposes no title-padding parameter, so the offset is applied to the title content itself. + * M3 indents it 16dp, which leaves it 8dp left of the chips and cards below. The bar exposes no + * title-padding parameter, so the offset goes on the title content itself. */ -val WornTopAppBarTitlePadding = Modifier.padding(start = 8.dp) +private val TITLE_GUTTER_NUDGE = Modifier.padding(start = 8.dp) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt index 767ec40..19dc7c0 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt @@ -27,11 +27,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.outlined.AutoAwesome -import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.LargeFlexibleTopAppBar +import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold @@ -76,11 +75,10 @@ import com.github.worn.ui.components.ErrorContentView import com.github.worn.ui.components.SheetDragHandle import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornGradientButton -import com.github.worn.ui.components.WornTopAppBarTitlePadding +import com.github.worn.ui.components.WornTopAppBar import com.github.worn.ui.components.displayLabel import com.github.worn.ui.components.displayName import com.github.worn.ui.components.iconRes -import com.github.worn.ui.components.wornTopAppBarColors import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview @@ -193,17 +191,9 @@ private fun GapsScaffold( containerColor = MaterialTheme.colorScheme.surface, topBar = { // Title and subtitle unchanged: journeys/gaps-common-suggestions.xml asserts on them. - LargeFlexibleTopAppBar( - title = { - Text(stringResource(R.string.gaps_title), modifier = WornTopAppBarTitlePadding) - }, - subtitle = { - Text( - stringResource(R.string.gaps_subtitle), - modifier = WornTopAppBarTitlePadding, - ) - }, - colors = wornTopAppBarColors(), + WornTopAppBar( + title = stringResource(R.string.gaps_title), + subtitle = stringResource(R.string.gaps_subtitle), scrollBehavior = scrollBehavior, ) }, @@ -707,3 +697,4 @@ private fun GapsScreenErrorPreview() { + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt index 4774a66..aea0bb7 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt @@ -28,10 +28,10 @@ import androidx.compose.material.icons.outlined.Layers import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon -import androidx.compose.material3.LargeFlexibleTopAppBar +import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -75,8 +75,7 @@ import com.github.worn.ui.components.SelectionHeader import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients -import com.github.worn.ui.components.WornTopAppBarTitlePadding -import com.github.worn.ui.components.wornTopAppBarColors +import com.github.worn.ui.components.WornTopAppBar import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme @@ -195,6 +194,11 @@ private fun OutfitsScaffold( .testTag("outfits_screen") .nestedScroll(scrollBehavior.nestedScrollConnection), containerColor = MaterialTheme.colorScheme.surface, + floatingActionButton = { + if (!isSelectionMode && state.outfits.isNotEmpty()) { + CreateOutfitFab(onCreateClick, Modifier.testTag("outfits_create_button")) + } + }, topBar = { if (isSelectionMode) { SelectionHeader( @@ -204,11 +208,7 @@ private fun OutfitsScaffold( modifier = Modifier.padding(horizontal = contentPadding), ) } else { - OutfitsTopBar( - outfitCount = state.outfits.size, - onCreateClick = onCreateClick, - scrollBehavior = scrollBehavior, - ) + OutfitsTopBar(outfitCount = state.outfits.size, scrollBehavior = scrollBehavior) } }, ) { paddingValues -> @@ -247,50 +247,45 @@ private fun OutfitsScaffold( } @Composable -private fun OutfitsTopBar( - outfitCount: Int, - onCreateClick: () -> Unit, - scrollBehavior: TopAppBarScrollBehavior, -) { - // Title and Create label are unchanged: journeys/create-first-outfit.xml asserts on both. - LargeFlexibleTopAppBar( - title = { - Text(stringResource(R.string.outfits_title), modifier = WornTopAppBarTitlePadding) - }, +private fun OutfitsTopBar(outfitCount: Int, scrollBehavior: TopAppBarScrollBehavior) { + // Title unchanged: journeys/create-first-outfit.xml asserts on it. + WornTopAppBar( + title = stringResource(R.string.outfits_title), subtitle = if (outfitCount > 0) { - { - Text( - pluralStringResource(R.plurals.saved_combinations, outfitCount, outfitCount), - modifier = WornTopAppBarTitlePadding, - ) - } + pluralStringResource(R.plurals.saved_combinations, outfitCount, outfitCount) } else { null }, - actions = { - if (outfitCount > 0) { - Button( - onClick = onCreateClick, - shape = MaterialTheme.shapes.largeIncreased, - modifier = Modifier - .padding(end = 8.dp) - .testTag("outfits_create_button"), - ) { - Icon(Icons.Default.Add, contentDescription = null, Modifier.size(16.dp)) - Spacer(Modifier.width(4.dp)) - Text( - stringResource(R.string.outfits_button_create), - style = MaterialTheme.typography.bodySmall, - fontWeight = FontWeight.SemiBold, - ) - } - } - }, - colors = wornTopAppBarColors(), scrollBehavior = scrollBehavior, ) } +/** + * Create as a FAB rather than a top-bar action. + * + * As an action it sat alone in the app bar's otherwise empty leading row, reading as a button + * floating above the title. A FAB also matches Wardrobe's "Add item", so the two list screens + * now offer their primary action in the same place. + */ +@Composable +private fun CreateOutfitFab(onClick: () -> Unit, modifier: Modifier = Modifier) { + ExtendedFloatingActionButton( + onClick = onClick, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + shape = MaterialTheme.shapes.extraLargeIncreased, + modifier = modifier, + ) { + Icon(Icons.Default.Add, contentDescription = null) + Spacer(Modifier.width(8.dp)) + Text( + text = stringResource(R.string.outfits_button_create), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + ) + } +} + @Composable private fun OutfitsContent( @@ -426,3 +421,5 @@ private fun OutfitsEmptyTabletPreview() { + + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt index 6a608c6..8461ef6 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt @@ -35,7 +35,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold @@ -91,8 +90,7 @@ import com.github.worn.ui.components.SheetDragHandle import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornChip import com.github.worn.ui.components.WornGradientButton -import com.github.worn.ui.components.WornTopAppBarTitlePadding -import com.github.worn.ui.components.wornTopAppBarColors +import com.github.worn.ui.components.WornTopAppBar import com.github.worn.ui.exposeTestTagsAsResourceId import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview @@ -190,14 +188,8 @@ private fun SettingsScaffold( .nestedScroll(scrollBehavior.nestedScrollConnection), containerColor = MaterialTheme.colorScheme.surface, topBar = { - LargeFlexibleTopAppBar( - title = { - Text( - stringResource(R.string.settings_title), - modifier = WornTopAppBarTitlePadding, - ) - }, - colors = wornTopAppBarColors(), + WornTopAppBar( + title = stringResource(R.string.settings_title), scrollBehavior = scrollBehavior, ) }, @@ -1087,3 +1079,4 @@ private fun SettingsScreenOnDeviceAiUnavailablePreview() { + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt index 8d02bcd..c01723a 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt @@ -28,11 +28,10 @@ import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon -import androidx.compose.material3.LargeFlexibleTopAppBar +import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -80,8 +79,7 @@ import com.github.worn.ui.components.SelectionHeader import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornGradientButton import com.github.worn.ui.components.WornGradients -import com.github.worn.ui.components.WornTopAppBarTitlePadding -import com.github.worn.ui.components.wornTopAppBarColors +import com.github.worn.ui.components.WornTopAppBar import com.github.worn.ui.theme.PhonePreview import com.github.worn.ui.theme.TabletPreview import com.github.worn.ui.theme.WornTheme @@ -292,23 +290,17 @@ private fun WardrobeScaffold( private fun WardrobeTopBar(itemCount: Int, scrollBehavior: TopAppBarScrollBehavior) { // Title strings are unchanged: journeys/bottom-navigation.xml and add-first-item.xml assert // on the visible heading text. - LargeFlexibleTopAppBar( - title = { - Text( - modifier = WornTopAppBarTitlePadding, - text = if (itemCount == 0) { - stringResource(R.string.wardrobe_title_empty) - } else { - stringResource(R.string.wardrobe_title) - }, - ) + WornTopAppBar( + title = if (itemCount == 0) { + stringResource(R.string.wardrobe_title_empty) + } else { + stringResource(R.string.wardrobe_title) }, subtitle = if (itemCount > 0) { - { Text(stringResource(R.string.wardrobe_subtitle, itemCount), modifier = WornTopAppBarTitlePadding) } + stringResource(R.string.wardrobe_subtitle, itemCount) } else { null }, - colors = wornTopAppBarColors(), scrollBehavior = scrollBehavior, ) } @@ -543,3 +535,4 @@ private fun WardrobeEmptyCategoryTabletPreview() { + From c9a4402417331d1310bd131119e61da49dbcf356 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 8 Aug 2026 11:21:25 -0300 Subject: [PATCH 12/13] fix: restore the gap between the header and the first card on Gaps and Settings Moving these titles into the app bar took their trailing spacing with them: the Gaps header item carried a 20dp Spacer before the Common Suggestions banner, and Settings had 28dp between its title and the first section label. Both ended up flush against the bar. Gaps gets the gap back as LazyColumn contentPadding rather than a Spacer item, so it scrolls away with the content instead of occupying a list slot. 20dp matches the .padding(.bottom, 20) the iOS Gaps header already uses, so the two platforms line up again. Co-Authored-By: Claude Opus 5 (1M context) --- .../main/kotlin/com/github/worn/ui/screen/GapsScreen.kt | 9 +++++++++ .../kotlin/com/github/worn/ui/screen/SettingsScreen.kt | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt index 19dc7c0..8311272 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/GapsScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -87,6 +88,9 @@ import com.github.worn.ui.theme.sheetShape import com.github.worn.ui.theme.wornExtras import org.koin.compose.viewmodel.koinViewModel +/** Gap between the app-bar header and the first card, matched to the iOS Gaps header. */ +private val HEADER_CONTENT_GAP = 20.dp + @Composable fun GapsScreen(onTabSelected: (Tab) -> Unit) { val viewModel: GapsViewModel = koinViewModel() @@ -203,6 +207,10 @@ private fun GapsScaffold( .fillMaxSize() .padding(paddingValues) .padding(horizontal = contentPadding), + // Restores the gap the old inline header carried as a trailing Spacer before it moved + // into the app bar. contentPadding rather than a Spacer item so it scrolls away with + // the content and matches the 20pt the iOS Gaps header uses. + contentPadding = PaddingValues(top = HEADER_CONTENT_GAP), ) { when { @@ -698,3 +706,4 @@ private fun GapsScreenErrorPreview() { + diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt index 8461ef6..6342270 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/screen/SettingsScreen.kt @@ -201,7 +201,7 @@ private fun SettingsScaffold( .padding(horizontal = contentPadding) .verticalScroll(rememberScrollState()), ) { - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(20.dp)) SectionLabel(stringResource(R.string.settings_section_profile)) Spacer(Modifier.height(10.dp)) SettingsCard( From af2d0e8082205f2e4b9570a120c5610c256cb9f3 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 8 Aug 2026 11:32:30 -0300 Subject: [PATCH 13/13] feat: replace the shoes icon with a sneaker silhouette MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Category.SHOES used Lucide's "footprints", which reads as walking or tracking rather than as an item of clothing — the odd one out next to a shirt, a wind glyph and a pair of glasses. Its six thin disconnected strokes also dissolved at the 18dp the Gaps category tiles render at. ic_sneaker is a single closed side-profile silhouette plus a sole seam, which survives that size, in the same 2dp round-cap stroke style as the other category icons. This also closes a parity gap: iOS already used the SF Symbol "shoe", so the two platforms showed different things for the same category. Verified in both places the icon renders — the Gaps category tiles and the Add Item category picker. Co-Authored-By: Claude Opus 5 (1M context) --- .../worn/ui/components/AddItemFields.kt | 2 +- .../src/main/res/drawable/ic_footprints.xml | 48 ------------------- .../src/main/res/drawable/ic_sneaker.xml | 33 +++++++++++++ 3 files changed, 34 insertions(+), 49 deletions(-) delete mode 100644 composeApp/src/main/res/drawable/ic_footprints.xml create mode 100644 composeApp/src/main/res/drawable/ic_sneaker.xml diff --git a/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt b/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt index c40c3b6..4937847 100644 --- a/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt +++ b/composeApp/src/main/kotlin/com/github/worn/ui/components/AddItemFields.kt @@ -413,7 +413,7 @@ internal fun Category.iconRes(): Int = when (this) { Category.TOP -> R.drawable.ic_shirt Category.BOTTOM -> R.drawable.ic_rectangle_horizontal Category.OUTERWEAR -> R.drawable.ic_wind - Category.SHOES -> R.drawable.ic_footprints + Category.SHOES -> R.drawable.ic_sneaker Category.ACCESSORY -> R.drawable.ic_glasses } diff --git a/composeApp/src/main/res/drawable/ic_footprints.xml b/composeApp/src/main/res/drawable/ic_footprints.xml deleted file mode 100644 index 48f6c9a..0000000 --- a/composeApp/src/main/res/drawable/ic_footprints.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - diff --git a/composeApp/src/main/res/drawable/ic_sneaker.xml b/composeApp/src/main/res/drawable/ic_sneaker.xml new file mode 100644 index 0000000..7836cbf --- /dev/null +++ b/composeApp/src/main/res/drawable/ic_sneaker.xml @@ -0,0 +1,33 @@ + + + + + + +