From f7a732d68a2ce7447cae3c54dbfb623643ab9835 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Tue, 8 Sep 2026 00:32:27 +0200 Subject: [PATCH] refactor: move the 89 off-grid spacing values onto the M3 scale Phase 2, second step, of docs/material-design-conformance.md. 77 of the 89 literals that were off M3's spacing scale sat in spacing positions and now read `MaterialTheme.spacing.spaceNNN`; the remaining 12 are dimensions and are out of scope. One drifted corner moved onto the shape scale. **The mapping, and why each is the nearest stop rather than the nicest number.** 5.dp x10 -> space50 (4dp) padding and gaps in dense rows 15.dp x14 -> space200 (16dp) card and dialog padding, two gaps 30.dp x1 -> space400 (32dp) the spacer under LoadingDataIndicator's spinner 50.dp x52 -> space600 (48dp) the spacer above an empty or error message Nearest-stop throughout, so the largest move is 2dp and most are 1. `5.dp` is equidistant between `space50` and `space75`; it goes to 4dp because `spacedBy(4.dp)` is already the idiom elsewhere in the tree and a scale with two answers for the same input is not one. The 52 at 48dp are the same three lines copied into 16 files -- a `Spacer` pushing "Something went wrong" down the screen. Phase 5 retires them into a shared empty-state composable; migrating them first means that composable inherits a token rather than another literal. **One shape, and it is the argument for having a scale at all.** `RoundedCornerShape(30.dp)` in `TextNoteEventDetail` was the only hand-written corner off the M3 scale, at 30dp against `extraLarge`'s 28. Two units: invisible beside any single other card, and exactly the drift that happens when the value is a literal. It is now `MaterialTheme.shapes.extraLarge`, the first call site for the scale `Shape.kt` documented. **Rewritten by a script that reads call shapes, not values, and it is checked in.** `docs/scripts/m3-migrate-spacing.py` brace-matches three call shapes -- `padding(...)`/ `PaddingValues(...)`, `Arrangement.spacedBy(...)`, and a `.height()`/`.width()` whose enclosing call is `Spacer(` -- and rewrites only literals that fall inside one. A `.size(18.dp)` icon, a non-Spacer `.height()`, a `RoundedCornerShape` or a `BorderStroke` can never be caught, which a regex over `\\d+\\.dp` would have done to all of them. It inserts the two imports where they are missing and skips comment lines. Dry run by default. **The audit was measuring the wrong thing, and this is where that showed.** It split literals by value against a hardcoded `DIMENSION_EXEMPT` list -- and the split is not a property of the value. `16.dp` is a spacing stop *and* a plausible icon size. `50.dp` was a `Spacer` height in 52 places and a divider width in one, and no list of numbers separates those. `docs/scripts/m3-spacing-positions.py` replaces it with the same brace-matching parse the migration uses, so the audit and the migration agree by construction; the audit now reports **353 spacing literals** left and 76 dimensions out of scope, and the exemption table is gone. That reframes phase 2's acceptance criterion into something checkable: spacing positions to zero, dimensions untouched. The script exits 1 while any spacing literal remains. **What is left off-scale, and why none of it is a defect.** Twelve dimensions: avatar sizes at 35, 55, 70 and 75dp, icon sizes at 18 and 22dp, and a 50dp divider width. Avatar and icon sizing is a component-spec question rather than a spacing one -- M3 gives icons 18/20/ 24/40/48 and says nothing about avatars -- and the plan puts per-component specs after the adaptive phase. They are reported rather than exempted so the number stays visible. **Tests.** 942 pass, 594 jvm over 72 classes and 348 android over 44, unchanged -- this commit adds no assertions, and the ones it could add (`SpacingScaleTest`) landed with the scale. `:composeApp:compileDebugKotlinAndroid` builds, `m3-audit.sh --check` exits 0. Pixels move by at most 2dp, in 30 files. Co-Authored-By: Claude Opus 5 --- .../ui/composable/AddArtifactScreen.kt | 3 +- .../compose/ui/composable/AddChapterScreen.kt | 3 +- .../compose/ui/composable/AddDialectScreen.kt | 3 +- .../AddMemberToChatRoomConfirmationScreen.kt | 3 +- .../AddTranslationArtifactVersionScreen.kt | 3 +- .../ui/composable/ArtifactDetailScreen.kt | 3 +- .../ui/composable/ChapterDetailScreen.kt | 3 +- .../ui/composable/ChatRoomDetailScreen.kt | 5 +- .../ui/composable/ChatRoomMessagingScreen.kt | 3 +- .../ui/composable/DkgApprovalScaffold.kt | 7 +- .../compose/ui/composable/DkgRitualScreen.kt | 19 +-- .../ui/composable/FrostSigningScreen.kt | 11 +- .../ui/composable/ProposalListScreen.kt | 7 +- .../SearchMemberToAddToChatRoomScreen.kt | 5 +- .../ui/composable/SearchResultScreen.kt | 9 +- .../composable/SelectChatRoomMembersScreen.kt | 7 +- .../ui/composable/SelectChatRoomTypeScreen.kt | 17 +- .../ui/composable/TranslateChunkScreen.kt | 3 +- .../TranslationArtifactVersionDetailScreen.kt | 3 +- .../ui/composable/TranslationChapterScreen.kt | 3 +- .../ui/composable/WriteNewNoteScreen.kt | 5 +- .../widgets/LoadingDataIndicator.kt | 3 +- .../widgets/detail/TextNoteEventDetail.kt | 5 +- .../press/mantra/compose/ui/theme/Spacing.kt | 21 +++ .../press/mantra/compose/ui/theme/Theme.kt | 8 + .../ui/view/model/ChatMessageListViewModel.kt | 11 +- .../ui/view/model/ChatRoomListViewModel.kt | 7 +- .../ui/view/model/FeedListViewModel.kt | 10 +- .../ui/view/model/FollowersListViewModel.kt | 10 +- .../ui/view/model/FollowingListViewModel.kt | 10 +- .../ui/view/model/InReplyToViewModel.kt | 10 +- docs/scripts/m3-audit.sh | 56 ++----- docs/scripts/m3-migrate-spacing.py | 149 ++++++++++++++++++ docs/scripts/m3-spacing-positions.py | 94 +++++++++++ 34 files changed, 397 insertions(+), 122 deletions(-) create mode 100755 docs/scripts/m3-migrate-spacing.py create mode 100755 docs/scripts/m3-spacing-positions.py diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddArtifactScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddArtifactScreen.kt index 10dc902c..c1e2af21 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddArtifactScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddArtifactScreen.kt @@ -67,6 +67,7 @@ import press.mantra.compose.ui.composable.navigation.routes.FrostSigningRoute import press.mantra.compose.ui.composable.navigation.routes.ImplementationPendingRoute import press.mantra.compose.ui.view.model.AddArtifactViewModel import press.mantra.compose.ui.view.state.AddArtifactUIState +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable @@ -102,7 +103,7 @@ fun AddArtifactScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = addArtifactUIState.message, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddChapterScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddChapterScreen.kt index a4b2c60a..c3d69618 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddChapterScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddChapterScreen.kt @@ -52,6 +52,7 @@ import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.AddChapterViewModel import press.mantra.compose.ui.view.state.AddChapterUIState +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -87,7 +88,7 @@ fun AddChapterScreen( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) Text(text = addChapterUIState.message) } } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddDialectScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddDialectScreen.kt index 40b8cf69..2635433d 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddDialectScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddDialectScreen.kt @@ -54,6 +54,7 @@ import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.AddDialectViewModel import press.mantra.compose.ui.view.state.AddDialectUIState +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @Composable @@ -87,7 +88,7 @@ fun AddDialectScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = addDialectUIState.message, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt index 4ad91099..63d73e09 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt @@ -37,6 +37,7 @@ import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.AddMemberToChatRoomConfirmationViewModel import press.mantra.compose.ui.view.state.AddMemberToChatRoomConfirmationUIState import com.vitorpamplona.quartz.nip01Core.core.HexKey +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @Composable @@ -69,7 +70,7 @@ fun AddMemberToChatRoomConfirmationScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = addMemberToChatRoomConfirmationUIState.message, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddTranslationArtifactVersionScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddTranslationArtifactVersionScreen.kt index 0e3c5a1b..c9c0af32 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddTranslationArtifactVersionScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddTranslationArtifactVersionScreen.kt @@ -57,6 +57,7 @@ import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.AddTranslationArtifactVersionViewModel import press.mantra.compose.ui.view.state.AddTranslationArtifactVersionUIState +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable @@ -92,7 +93,7 @@ fun AddTranslationArtifactVersionScreen( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) Text(text = addTranslationUIState.message) } } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ArtifactDetailScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ArtifactDetailScreen.kt index 7c486a7e..17b480e2 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ArtifactDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ArtifactDetailScreen.kt @@ -48,6 +48,7 @@ import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.ArtifactDetailViewModel import press.mantra.compose.ui.view.state.ArtifactDetailUIState +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -78,7 +79,7 @@ fun ArtifactDetailScreen( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) Text(text = artifactDetailUIState.message) } } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChapterDetailScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChapterDetailScreen.kt index 54cfe1c0..5ae6b4c5 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChapterDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChapterDetailScreen.kt @@ -38,6 +38,7 @@ import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.ChapterDetailViewModel import press.mantra.compose.ui.view.state.ChapterDetailUIState +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -67,7 +68,7 @@ fun ChapterDetailScreen( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) Text(text = chapterDetailUIState.message) } } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomDetailScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomDetailScreen.kt index 9f69910f..d4704051 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomDetailScreen.kt @@ -68,6 +68,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import press.mantra.compose.ui.composable.navigation.routes.AddArtifactRoute import press.mantra.compose.ui.composable.navigation.routes.AddDialectRoute import press.mantra.compose.ui.composable.navigation.routes.ArtifactDetailRoute +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @Composable @@ -101,7 +102,7 @@ fun ChatRoomDetailScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = chatRoomDetailUIState.message, @@ -609,7 +610,7 @@ private fun ReindexMarmotGroupEventsButton( ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(5.dp) + verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space50) ) { TextButton( enabled = reindexState !is ChatRoomDetailViewModel.ReindexState.Running, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomMessagingScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomMessagingScreen.kt index 8bf6f8da..6f99968d 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomMessagingScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomMessagingScreen.kt @@ -56,6 +56,7 @@ import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.ChatRoomMessagingViewModel import press.mantra.compose.ui.view.state.ChatRoomMessagingUIState import com.vitorpamplona.quartz.nip01Core.core.HexKey +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @Composable @@ -88,7 +89,7 @@ fun ChatRoomMessagingScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = chatRoomDetailUIState.message, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/DkgApprovalScaffold.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/DkgApprovalScaffold.kt index c8933b8d..ea32e630 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/DkgApprovalScaffold.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/DkgApprovalScaffold.kt @@ -37,6 +37,7 @@ import press.mantra.compose.ui.view.state.DkgRitualUIState import com.vitorpamplona.quartz.nip01Core.core.HexKey import fr.acinq.phoenix.data.ActiveWallet import kotlinx.coroutines.flow.StateFlow +import press.mantra.compose.ui.theme.spacing /** * The frame every ChillDKG approval screen sits in: the view model, the states @@ -89,7 +90,7 @@ internal fun DkgApprovalScaffold( modifier = Modifier.fillMaxWidth().padding(padding).padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) CircularProgressIndicator() } @@ -97,7 +98,7 @@ internal fun DkgApprovalScaffold( modifier = Modifier.fillMaxWidth().padding(padding).padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) Text(text = state.message, textAlign = TextAlign.Center) } @@ -112,7 +113,7 @@ internal fun DkgApprovalScaffold( modifier = Modifier.fillMaxWidth().padding(padding).padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) CircularProgressIndicator() } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/DkgRitualScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/DkgRitualScreen.kt index ab1d81b6..493cff5f 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/DkgRitualScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/DkgRitualScreen.kt @@ -75,6 +75,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import fr.acinq.phoenix.data.ActiveWallet import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import press.mantra.compose.ui.theme.spacing /** * The shared-key ceremony: a ChillDKG ritual run across the group's NIP-17 @@ -113,7 +114,7 @@ fun DkgRitualScreen( modifier = Modifier.fillMaxWidth().padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) Text(text = dkgRitualUIState.message, textAlign = TextAlign.Center) } } @@ -201,7 +202,7 @@ fun DkgRitualScreen( }, actions = { Text( - modifier = Modifier.padding(start = 15.dp), + modifier = Modifier.padding(start = MaterialTheme.spacing.space200), text = "${dkgRitualViewModel.threshold.value} of $participantCount", style = MaterialTheme.typography.labelLarge ) @@ -328,7 +329,7 @@ private fun RitualProgress( ) ) { Row( - modifier = Modifier.fillMaxWidth().padding(15.dp), + modifier = Modifier.fillMaxWidth().padding(MaterialTheme.spacing.space200), horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -396,7 +397,7 @@ private fun RitualProgress( ) ) { Column( - modifier = Modifier.fillMaxWidth().padding(15.dp), + modifier = Modifier.fillMaxWidth().padding(MaterialTheme.spacing.space200), verticalArrangement = Arrangement.spacedBy(8.dp) ) { Row( @@ -501,7 +502,7 @@ private fun RitualRoster( ) { Card(modifier = Modifier.fillMaxWidth()) { Column( - modifier = Modifier.fillMaxWidth().padding(15.dp), + modifier = Modifier.fillMaxWidth().padding(MaterialTheme.spacing.space200), verticalArrangement = Arrangement.spacedBy(10.dp) ) { Text(text = "Members", style = MaterialTheme.typography.titleSmall) @@ -563,7 +564,7 @@ private fun RitualStep( ) { Card(modifier = Modifier.fillMaxWidth()) { Column( - modifier = Modifier.fillMaxWidth().padding(15.dp), + modifier = Modifier.fillMaxWidth().padding(MaterialTheme.spacing.space200), verticalArrangement = Arrangement.spacedBy(8.dp) ) { Row( @@ -602,8 +603,8 @@ private fun QuorumStepper( onThresholdChange: (Int) -> Unit, ) { Column( - modifier = Modifier.fillMaxWidth().padding(top = 5.dp), - verticalArrangement = Arrangement.spacedBy(5.dp) + modifier = Modifier.fillMaxWidth().padding(top = MaterialTheme.spacing.space50), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space50) ) { Text( text = "How many members will it take to sign?", @@ -612,7 +613,7 @@ private fun QuorumStepper( Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(15.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space200), verticalAlignment = Alignment.CenterVertically ) { FilledIconButton( diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/FrostSigningScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/FrostSigningScreen.kt index 8032eabb..8a075cac 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/FrostSigningScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/FrostSigningScreen.kt @@ -56,6 +56,7 @@ import press.mantra.compose.ui.composable.widgets.profile.ProfileAvatar import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.FrostSigningViewModel import press.mantra.compose.ui.view.state.FrostSigningUIState +import press.mantra.compose.ui.theme.spacing /** * One signing session, and the member's decision about it. @@ -123,7 +124,7 @@ fun FrostSigningScreen( modifier = Modifier.fillMaxWidth().padding(padding).padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) Text(text = state.message, textAlign = TextAlign.Center) } @@ -150,7 +151,7 @@ fun FrostSigningScreen( .fillMaxSize() .verticalScroll(scrollState) .padding(20.dp), - verticalArrangement = Arrangement.spacedBy(15.dp) + verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space200) ) { WhatIsBeingSigned(proposed, state.items.size) @@ -307,7 +308,7 @@ private fun Loading(padding: androidx.compose.foundation.layout.PaddingValues) { modifier = Modifier.fillMaxWidth().padding(padding).padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) CircularProgressIndicator() } } @@ -337,7 +338,7 @@ private fun WhatIsBeingSigned(events: List, expected: Int) { return } - Column(verticalArrangement = Arrangement.spacedBy(15.dp)) { + Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space200)) { if (events.size > 1) { Text( text = "${events.size} events, signed together", @@ -366,7 +367,7 @@ private fun WhatIsBeingSigned(events: List, expected: Int) { private fun OneThingBeingSigned(event: Event) { val summary = ProposedEvent.summarize(event) - Column(verticalArrangement = Arrangement.spacedBy(5.dp)) { + Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space50)) { Text(text = summary.label, style = MaterialTheme.typography.labelMedium) Text(text = summary.detail, style = MaterialTheme.typography.titleMedium) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ProposalListScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ProposalListScreen.kt index 5a2ee9a3..6736b880 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ProposalListScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ProposalListScreen.kt @@ -54,6 +54,7 @@ import press.mantra.compose.ui.composable.navigation.routes.Route import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.ProposalListViewModel import press.mantra.compose.ui.view.state.ProposalListUIState +import press.mantra.compose.ui.theme.spacing /** * Everything the group has asked its shared key to sign. @@ -119,7 +120,7 @@ fun ProposalListScreen( modifier = Modifier.fillMaxWidth().padding(padding).padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) CircularProgressIndicator() } @@ -127,7 +128,7 @@ fun ProposalListScreen( modifier = Modifier.fillMaxWidth().padding(padding).padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) Text(text = state.message, textAlign = TextAlign.Center) } @@ -149,7 +150,7 @@ fun ProposalListScreen( if (state.proposals.isEmpty()) { item { Text( - modifier = Modifier.fillMaxWidth().padding(top = 50.dp), + modifier = Modifier.fillMaxWidth().padding(top = MaterialTheme.spacing.space600), text = "This group has not been asked to sign anything yet.", textAlign = TextAlign.Center ) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SearchMemberToAddToChatRoomScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SearchMemberToAddToChatRoomScreen.kt index 0c9fc6d0..5657c8f8 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SearchMemberToAddToChatRoomScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SearchMemberToAddToChatRoomScreen.kt @@ -40,6 +40,7 @@ import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.SearchMemberToAddToChatRoomViewModel import press.mantra.compose.ui.view.state.SearchMemberToAddToChatRoomUIState import com.vitorpamplona.quartz.nip01Core.core.HexKey +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @Composable @@ -70,7 +71,7 @@ fun SearchMemberToAddToChatRoomScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = searchMemberToAddToChatRoomUIState.message, @@ -118,7 +119,7 @@ fun SearchMemberToAddToChatRoomScreen( ) } else { LazyColumn( - modifier = Modifier.fillMaxWidth().padding(5.dp), + modifier = Modifier.fillMaxWidth().padding(MaterialTheme.spacing.space50), verticalArrangement = Arrangement.spacedBy(10.dp), reverseLayout = true ) { diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SearchResultScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SearchResultScreen.kt index 159bf627..ab83836a 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SearchResultScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SearchResultScreen.kt @@ -54,6 +54,7 @@ import press.mantra.compose.ui.view.model.SearchResultType import press.mantra.compose.ui.view.model.SearchResultViewModel import press.mantra.compose.ui.view.state.SearchResultListUIState import kotlinx.coroutines.launch +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -210,7 +211,7 @@ fun SearchResultScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Something went wrong", @@ -225,7 +226,7 @@ fun SearchResultScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "No events were found", @@ -263,14 +264,14 @@ fun SearchResultScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Searching for ${searchViewModel.searchResultType.name.lowercase()} on \"${searchQuery.lowercase()}\"", textAlign = TextAlign.Center ) Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) LoadingIndicator() diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomMembersScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomMembersScreen.kt index 1d9d40c6..a11c07cf 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomMembersScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomMembersScreen.kt @@ -42,6 +42,7 @@ import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.SelectChatRoomMembersViewModel import press.mantra.compose.ui.view.state.SelectChatRoomMembersUIState import com.vitorpamplona.quartz.nip01Core.core.HexKey +import press.mantra.compose.ui.theme.spacing /** * Second step of group creation: pick who is in the group. @@ -77,7 +78,7 @@ fun SelectChatRoomMembersScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = selectChatRoomMembersUIState.message, @@ -126,7 +127,7 @@ fun SelectChatRoomMembersScreen( }, actions = { Text( - modifier = Modifier.padding(start = 15.dp), + modifier = Modifier.padding(start = MaterialTheme.spacing.space200), text = if (selectedCount > 0) { "$selectedCount selected" } else { @@ -175,7 +176,7 @@ fun SelectChatRoomMembersScreen( } } else { LazyColumn( - modifier = Modifier.weight(1f).fillMaxWidth().padding(5.dp), + modifier = Modifier.weight(1f).fillMaxWidth().padding(MaterialTheme.spacing.space50), verticalArrangement = Arrangement.spacedBy(10.dp) ) { items( diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomTypeScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomTypeScreen.kt index 3d840986..38057895 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomTypeScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomTypeScreen.kt @@ -55,6 +55,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import fr.acinq.phoenix.data.ActiveWallet import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import press.mantra.compose.ui.theme.spacing /** * Last step of group creation: convenient (one admin) or robust (everyone @@ -95,7 +96,7 @@ fun SelectChatRoomTypeScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = selectChatRoomTypeUIState.message, @@ -156,7 +157,7 @@ fun SelectChatRoomTypeScreen( }, actions = { Text( - modifier = Modifier.padding(start = 15.dp), + modifier = Modifier.padding(start = MaterialTheme.spacing.space200), text = when (selectChatRoomTypeUIState.members.size) { 0 -> "Just you for now" 1 -> "You and 1 other" @@ -185,7 +186,7 @@ fun SelectChatRoomTypeScreen( ) ) { Text( - modifier = Modifier.padding(15.dp), + modifier = Modifier.padding(MaterialTheme.spacing.space200), text = "$name was created, but ${membersNotAdded.joinToString { selectChatRoomTypeViewModel.displayNameFor(it) }} couldn't be added yet. Invite them again from the chat once they're on Torch.", style = MaterialTheme.typography.bodyMedium ) @@ -201,7 +202,7 @@ fun SelectChatRoomTypeScreen( ) ) { Text( - modifier = Modifier.padding(15.dp), + modifier = Modifier.padding(MaterialTheme.spacing.space200), text = "$name was created, but its shared key ceremony couldn't be started. Open the chat and start it from the group's details — until then the group has no key of its own.", style = MaterialTheme.typography.bodyMedium ) @@ -328,7 +329,7 @@ private fun ChatRoomTypeCard( } ) { Column( - modifier = Modifier.fillMaxWidth().padding(15.dp), + modifier = Modifier.fillMaxWidth().padding(MaterialTheme.spacing.space200), verticalArrangement = Arrangement.spacedBy(8.dp) ) { Row( @@ -394,8 +395,8 @@ private fun QuorumPicker( onQuorumChange: (Int) -> Unit, ) { Column( - modifier = Modifier.fillMaxWidth().padding(top = 5.dp), - verticalArrangement = Arrangement.spacedBy(5.dp) + modifier = Modifier.fillMaxWidth().padding(top = MaterialTheme.spacing.space50), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space50) ) { Text( text = "How many admins have to approve a change?", @@ -404,7 +405,7 @@ private fun QuorumPicker( Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(15.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space200), verticalAlignment = Alignment.CenterVertically ) { FilledIconButton( diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslateChunkScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslateChunkScreen.kt index b241f191..1e86e308 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslateChunkScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslateChunkScreen.kt @@ -52,6 +52,7 @@ import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.TranslateChunkViewModel import press.mantra.compose.ui.view.state.TranslateChunkUIState +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -89,7 +90,7 @@ fun TranslateChunkScreen( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) Text(text = translateChunkUIState.message) } } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationArtifactVersionDetailScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationArtifactVersionDetailScreen.kt index 2bb9427b..5a3cc1da 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationArtifactVersionDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationArtifactVersionDetailScreen.kt @@ -46,6 +46,7 @@ import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.TranslationArtifactVersionDetailViewModel import press.mantra.compose.ui.view.state.TranslationArtifactVersionDetailUIState +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -80,7 +81,7 @@ fun TranslationArtifactVersionDetailScreen( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) Text(text = translationDetailUIState.message) } } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationChapterScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationChapterScreen.kt index 8d7a5e18..5c4394df 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationChapterScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationChapterScreen.kt @@ -46,6 +46,7 @@ import press.mantra.compose.ui.theme.TorchTheme import press.mantra.compose.ui.view.model.TranslationChapterViewModel import press.mantra.compose.ui.view.state.ChunkTranslationPair import press.mantra.compose.ui.view.state.TranslationChapterUIState +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -76,7 +77,7 @@ fun TranslationChapterScreen( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.space600)) Text(text = translationChapterUIState.message) } } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/WriteNewNoteScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/WriteNewNoteScreen.kt index 6568ef55..099edac5 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/WriteNewNoteScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/WriteNewNoteScreen.kt @@ -43,6 +43,7 @@ import press.mantra.compose.extensions.toFormattedTimeAndDateString import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import kotlin.time.Clock +import press.mantra.compose.ui.theme.spacing @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalFoundationApi::class) @Composable @@ -110,7 +111,7 @@ fun WriteNewNoteScreen( if (writeNewNoteUIState.inReplyToNostrEvent != null) { Text( modifier = Modifier.padding( - start = 50.dp, + start = MaterialTheme.spacing.space600, bottom = 10.dp, ), @@ -233,7 +234,7 @@ fun WriteNewNoteScreen( if (writeNewNoteUIState.quotedNostrEvent?.nostrEvent?.kind == TextNoteEvent.KIND) { Card( modifier = Modifier.padding( - start = 50.dp, + start = MaterialTheme.spacing.space600, top = 10.dp ) ) { diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/LoadingDataIndicator.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/LoadingDataIndicator.kt index a4e062e3..1354d32d 100755 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/LoadingDataIndicator.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/LoadingDataIndicator.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import press.mantra.compose.ui.theme.spacing @Composable fun LoadingDataIndicator( @@ -37,7 +38,7 @@ fun LoadingDataIndicator( text?.let { Spacer( - modifier = Modifier.height(30.dp) + modifier = Modifier.height(MaterialTheme.spacing.space400) ) Text( diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/detail/TextNoteEventDetail.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/detail/TextNoteEventDetail.kt index 5aa93d77..3a62dfbe 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/detail/TextNoteEventDetail.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/detail/TextNoteEventDetail.kt @@ -107,7 +107,10 @@ fun TextNoteEventDetail( localNostrEvent.nostrEvent.id ) }, - shape = RoundedCornerShape(30.dp), + // The one hand-written corner that was off the shape scale, at 30dp + // against extraLarge's 28. Two units, invisible on its own and exactly + // the drift a scale exists to stop. + shape = MaterialTheme.shapes.extraLarge, color = MaterialTheme.colorScheme.surfaceVariant ) { Row( diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/theme/Spacing.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/theme/Spacing.kt index bf3a13af..ad1aa8c8 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/theme/Spacing.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/theme/Spacing.kt @@ -1,6 +1,9 @@ package press.mantra.compose.ui.theme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -88,3 +91,21 @@ data class Spacing( } val LocalSpacing = staticCompositionLocalOf { Spacing() } + +/** + * `MaterialTheme.spacing.containerPadding`, to match `MaterialTheme.colorScheme.primary`. + * + * The alternative is `LocalSpacing.current`, which has to be read into a local before it + * can be used and so cannot appear inline in a `Modifier` chain. That difference decides + * whether a call site reads + * + * Modifier.padding(MaterialTheme.spacing.containerPadding) + * + * or acquires a `val spacing = LocalSpacing.current` several lines above, in whichever + * composable happens to enclose it. Over 500 call sites the second shape is what stops + * people using the scale. + */ +val MaterialTheme.spacing: Spacing + @Composable + @ReadOnlyComposable + get() = LocalSpacing.current diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/theme/Theme.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/theme/Theme.kt index c6594078..cb695984 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/theme/Theme.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/theme/Theme.kt @@ -4,12 +4,14 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialExpressiveTheme +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MotionScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color @@ -414,6 +416,12 @@ val LocalExtendedColors = staticCompositionLocalOf { ExtendedColors(bluePill = bluePillLight, redPill = redPillLight) } +/** `MaterialTheme.extendedColors.redPill`, matching the [spacing] accessor. */ +val MaterialTheme.extendedColors: ExtendedColors + @Composable + @ReadOnlyComposable + get() = LocalExtendedColors.current + internal fun extendedColorsFor(darkTheme: Boolean): ExtendedColors = if (darkTheme) { ExtendedColors(bluePill = bluePillDark, redPill = redPillDark) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/ChatMessageListViewModel.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/ChatMessageListViewModel.kt index c8cc6793..08a46862 100755 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/ChatMessageListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/ChatMessageListViewModel.kt @@ -91,6 +91,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch +import press.mantra.compose.ui.theme.spacing class ChatMessageListViewModel( initialChatMessageListUIState: ChatMessageListUIState, @@ -393,7 +394,7 @@ class ChatMessageListViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Something went wrong", @@ -445,7 +446,7 @@ class ChatMessageListViewModel( val awaitingYou = proposalsAwaitingYou LazyColumn( - modifier = Modifier.fillMaxWidth().padding(5.dp), + modifier = Modifier.fillMaxWidth().padding(MaterialTheme.spacing.space50), verticalArrangement = Arrangement.spacedBy(10.dp), reverseLayout = true ) { @@ -744,14 +745,14 @@ class ChatMessageListViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Loading", textAlign = TextAlign.Center ) Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) LoadingIndicator() @@ -850,7 +851,7 @@ private fun ProposalsAwaitingYouNotice( Card( onClick = onClick, - modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = MaterialTheme.spacing.space50), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/ChatRoomListViewModel.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/ChatRoomListViewModel.kt index bf36213c..89bc9fcb 100755 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/ChatRoomListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/ChatRoomListViewModel.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.launch +import press.mantra.compose.ui.theme.spacing class ChatRoomListViewModel( initialChatRoomListUIState: ChatRoomListUIState, @@ -150,7 +151,7 @@ class ChatRoomListViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Something went wrong", @@ -232,14 +233,14 @@ class ChatRoomListViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Loading", textAlign = TextAlign.Center ) Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) LoadingIndicator() diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FeedListViewModel.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FeedListViewModel.kt index bc84b164..f71f4454 100755 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FeedListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FeedListViewModel.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -33,6 +34,7 @@ import kotlinx.coroutines.IO import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import press.mantra.compose.ui.theme.spacing class FeedListViewModel( initialFeedListUIState: FeedListUIState, @@ -116,7 +118,7 @@ class FeedListViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Something went wrong", @@ -130,7 +132,7 @@ class FeedListViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "No events were found", @@ -167,14 +169,14 @@ class FeedListViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Loading", textAlign = TextAlign.Center ) Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) LoadingIndicator() diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FollowersListViewModel.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FollowersListViewModel.kt index 82e3100d..560ab2a8 100755 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FollowersListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FollowersListViewModel.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -29,6 +30,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch +import press.mantra.compose.ui.theme.spacing class FollowersListViewModel( initialFollowingListUIState: press.mantra.compose.ui.view.state.FollowersListUIState, @@ -110,7 +112,7 @@ class FollowersListViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Something went wrong", @@ -124,7 +126,7 @@ class FollowersListViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "No events were found", @@ -155,14 +157,14 @@ class FollowersListViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Loading", textAlign = TextAlign.Center ) Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) LoadingIndicator() diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FollowingListViewModel.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FollowingListViewModel.kt index 51d5d92d..45e4d4a5 100755 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FollowingListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FollowingListViewModel.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -29,6 +30,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch +import press.mantra.compose.ui.theme.spacing class FollowingListViewModel( initialFollowingListUIState: press.mantra.compose.ui.view.state.FollowingListUIState, @@ -109,7 +111,7 @@ class FollowingListViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Something went wrong", @@ -123,7 +125,7 @@ class FollowingListViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "No events were found", @@ -154,14 +156,14 @@ class FollowingListViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Loading", textAlign = TextAlign.Center ) Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) LoadingIndicator() diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/InReplyToViewModel.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/InReplyToViewModel.kt index f03cd197..ddd2f9e4 100755 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/InReplyToViewModel.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/InReplyToViewModel.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -34,6 +35,7 @@ import kotlinx.coroutines.IO import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import kotlin.time.Instant +import press.mantra.compose.ui.theme.spacing class InReplyToViewModel( val nostrEvent: press.mantra.compose.database.model.NostrEvent, @@ -120,7 +122,7 @@ class InReplyToViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Something went wrong", @@ -134,7 +136,7 @@ class InReplyToViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "No events were found", @@ -171,14 +173,14 @@ class InReplyToViewModel( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) Text( text = "Loading", textAlign = TextAlign.Center ) Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(MaterialTheme.spacing.space600) ) LoadingIndicator() diff --git a/docs/scripts/m3-audit.sh b/docs/scripts/m3-audit.sh index 512f198b..96d2d727 100755 --- a/docs/scripts/m3-audit.sh +++ b/docs/scripts/m3-audit.sh @@ -25,23 +25,13 @@ THEME="$UI/theme" # Budgets. "-1" means not yet budgeted -- reported, but never fails --check. # --------------------------------------------------------------------------- BUDGET_HARDCODED_COLOR=9 # phase 3 drives to 0 outside theme/ -BUDGET_DP_LITERALS=-1 # phase 2 drives to ~0 outside theme/ -BUDGET_OFF_SCALE_DP=-1 # phase 2 drives to 0 +BUDGET_SPACING_LITERALS=353 # phase 2 drives to 0 BUDGET_BARE_CLICKABLE=33 # phase 3 drives to 0 BUDGET_NULL_DESCRIPTION=18 # phase 3 triages each one BUDGET_STRING_LITERALS=-1 # phase 4 drives to <10 BUDGET_TITLE_CASE=-1 # phase 4 drives to 0 BUDGET_UNSET_COLOR_ROLES=0 # phase 1: reached 2026-09-07 -# The M3 spacing scale: docs/material-design-conformance.md, "The numbers". -# space0..space900. Anything outside this set is off-scale. -ON_SCALE=(0 2 4 6 8 10 12 14 16 20 24 32 36 40 48 56 64 72) - -# Dimensions rather than spacing -- an avatar, an image height, a hairline -# border. These are exempt from the off-scale count; keep the list short and -# justify additions in the commit that makes them. -DIMENSION_EXEMPT=(1 80 128 180 200 500) - fail_count=0 hdr() { printf '\n\033[1m== %s\033[0m\n' "$1"; } @@ -136,40 +126,18 @@ report 'colours derived with .copy(alpha =)' "$alpha" -1 # --------------------------------------------------------------------------- hdr 'Spacing (phase 2)' -dp_all=$(grep -rhoE '\b[0-9]+\.dp' "$UI" --include=*.kt 2>/dev/null \ - | grep -v "^$THEME/" | wc -l | tr -d ' ') -dp_outside_theme=$(grep -rhoE '\b[0-9]+\.dp' \ - $(grep -rl '\.dp' "$UI" --include=*.kt 2>/dev/null | grep -v "^$THEME/") \ - 2>/dev/null | wc -l | tr -d ' ') -report '.dp literals outside theme/' "$dp_outside_theme" "$BUDGET_DP_LITERALS" +# Classified by call shape rather than by value, which is the only thing that says +# whether a given literal is spacing or a dimension: 16.dp is a spacing stop and also a +# plausible icon size, and 50.dp was a Spacer height in 53 places and a divider width in +# one. m3-spacing-positions.py does the parse; it exits 1 while any spacing literal is +# left, which is phase 2's acceptance criterion. +spacing_report=$(python3 docs/scripts/m3-spacing-positions.py) +spacing_left=$(echo "$spacing_report" | awk '/spacing positions/ {print $NF}') +dimensions=$(echo "$spacing_report" | grep 'dimension positions' | grep -oE '[0-9]+') +report 'dp literals in spacing positions' "$spacing_left" "$BUDGET_SPACING_LITERALS" +note "in dimension positions (out of scope): $dimensions" +note 'run docs/scripts/m3-spacing-positions.py --list to see them' -# Split the histogram into on-scale, exempt dimensions, and off-scale. -declare -A hist -while read -r n; do - hist[$n]=$(( ${hist[$n]:-0} + 1 )) -done < <(grep -rhoE '\b[0-9]+\.dp' \ - $(grep -rl '\.dp' "$UI" --include=*.kt 2>/dev/null | grep -v "^$THEME/") \ - 2>/dev/null | sed 's/\.dp//') - -on_scale_total=0; off_scale_total=0; exempt_total=0; off_scale_detail="" -for n in "${!hist[@]}"; do - c=${hist[$n]} - if printf '%s\n' "${ON_SCALE[@]}" | grep -qx "$n"; then - on_scale_total=$((on_scale_total + c)) - elif printf '%s\n' "${DIMENSION_EXEMPT[@]}" | grep -qx "$n"; then - exempt_total=$((exempt_total + c)) - else - off_scale_total=$((off_scale_total + c)) - off_scale_detail="$off_scale_detail ${n}dp:${c}" - fi -done -note "on the M3 scale: $on_scale_total" -note "exempt dimensions: $exempt_total" -report 'off the M3 spacing scale' "$off_scale_total" "$BUDGET_OFF_SCALE_DP" -[[ -n $off_scale_detail ]] && note "off-scale:$off_scale_detail" - -spacer_idiom=$(count 'height\(50\.dp\)') -note "Spacer(height(50.dp)) idiom: $spacer_idiom" # --------------------------------------------------------------------------- hdr 'Typography (phase 1)' diff --git a/docs/scripts/m3-migrate-spacing.py b/docs/scripts/m3-migrate-spacing.py new file mode 100755 index 00000000..5bce58d5 --- /dev/null +++ b/docs/scripts/m3-migrate-spacing.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Rewrite .dp literals that sit in spacing positions onto MaterialTheme.spacing. + +Checked in because phase 2 runs it twice and the breakpoint phase will want it again. +Usage: + + m3-migrate-spacing.py '{"5":"space50","15":"space200"}' # dry run + m3-migrate-spacing.py '{"5":"space50","15":"space200"}' --apply + +Only three call shapes are touched, and each is matched with the literal in place so a +dimension can never be caught by accident: + + padding(...) any of the overloads, including named start/end/top/bottom + Arrangement.spacedBy(N) horizontal or vertical + Spacer height/width a .height()/.width() whose enclosing call is Spacer( + +Everything else -- .size(), a non-Spacer .height(), RoundedCornerShape, BorderStroke -- +is a dimension and is left alone. +""" +import re, sys, io, os + +UI = 'composeApp/src/commonMain/kotlin/press/mantra/compose/ui' +MAPPING = {} # filled by the caller +DRY = '--apply' not in sys.argv + +def token(value): + return MAPPING.get(value) + +def spacer_spans(text): + """Character ranges covered by a Spacer( ... ) call, brace-matched.""" + spans = [] + for m in re.finditer(r'\bSpacer\s*\(', text): + depth, i = 0, m.end() - 1 + while i < len(text): + if text[i] == '(': + depth += 1 + elif text[i] == ')': + depth -= 1 + if depth == 0: + spans.append((m.start(), i)) + break + i += 1 + return spans + +def in_spans(pos, spans): + return any(a <= pos <= b for a, b in spans) + +def padding_spans(text): + spans = [] + for m in re.finditer(r'\.?\bpadding\s*\(|\bPaddingValues\s*\(', text): + depth, i = 0, m.end() - 1 + while i < len(text): + if text[i] == '(': + depth += 1 + elif text[i] == ')': + depth -= 1 + if depth == 0: + spans.append((m.start(), i)) + break + i += 1 + return spans + +def spacedby_spans(text): + spans = [] + for m in re.finditer(r'\bspacedBy\s*\(', text): + depth, i = 0, m.end() - 1 + while i < len(text): + if text[i] == '(': + depth += 1 + elif text[i] == ')': + depth -= 1 + if depth == 0: + spans.append((m.start(), i)) + break + i += 1 + return spans + +def process(path): + text = io.open(path, encoding='utf-8').read() + original = text + changed = [] + + for _ in range(60): # spans shift after each edit; recompute + pads = padding_spans(text) + gaps = spacedby_spans(text) + spacers = spacer_spans(text) + # a .height()/.width() literal counts only inside a Spacer( + hw = [(m.start(1), m.end(1), m.group(1)) + for m in re.finditer(r'\.(?:height|width)\s*\(\s*(\d+\.dp)\s*\)', text) + if in_spans(m.start(), spacers)] + + edit = None + for m in re.finditer(r'\b(\d+)\.dp\b', text): + lit = m.group(0) + tok = token(m.group(1)) + if tok is None: + continue + pos = m.start() + line_start = text.rfind('\n', 0, pos) + 1 + line = text[line_start:text.find('\n', pos)] + if re.match(r'\s*(//|\*|/\*)', line): # a comment + continue + if in_spans(pos, pads) or in_spans(pos, gaps) or \ + any(a <= pos < b for a, b, _ in hw): + edit = (m.start(), m.end(), tok, lit, line.strip()[:80]) + break + if edit is None: + break + a, b, tok, lit, ctx = edit + text = text[:a] + f'MaterialTheme.spacing.{tok}' + text[b:] + changed.append((lit, tok, ctx)) + + if text != original: + if 'import androidx.compose.material3.MaterialTheme' not in text: + # insert alphabetically among the material3 imports, else after the last import + lines = text.split('\n') + idx = max(i for i, l in enumerate(lines) if l.startswith('import ')) + for i, l in enumerate(lines): + if l.startswith('import androidx.compose.material3.') and l > 'import androidx.compose.material3.MaterialTheme': + idx = i - 1 + break + lines.insert(idx + 1, 'import androidx.compose.material3.MaterialTheme') + text = '\n'.join(lines) + if 'import press.mantra.compose.ui.theme.spacing' not in text and \ + not path.endswith('theme/Spacing.kt'): + lines = text.split('\n') + idx = max(i for i, l in enumerate(lines) if l.startswith('import ')) + lines.insert(idx + 1, 'import press.mantra.compose.ui.theme.spacing') + text = '\n'.join(lines) + if not DRY: + io.open(path, 'w', encoding='utf-8').write(text) + return changed + +if __name__ == '__main__': + import json + MAPPING.update(json.loads(sys.argv[1])) + total = 0 + for root, _, files in os.walk(UI): + for f in sorted(files): + if not f.endswith('.kt'): + continue + p = os.path.join(root, f) + ch = process(p) + if ch: + print(f'{p.replace(UI + "/", "")}') + for lit, tok, ctx in ch: + print(f' {lit:>7s} -> {tok:18s} {ctx}') + total += len(ch) + print(f'\n{total} literal(s) {"would be " if DRY else ""}rewritten') diff --git a/docs/scripts/m3-spacing-positions.py b/docs/scripts/m3-spacing-positions.py new file mode 100755 index 00000000..e52c58a4 --- /dev/null +++ b/docs/scripts/m3-spacing-positions.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Count .dp literals by whether they sit in a spacing position or a dimension one. + +m3-audit.sh used to split them by value, exempting a list of numbers that "looked like" +dimensions -- which is wrong twice over. 16.dp is a spacing stop *and* a plausible icon +size, and 50.dp was a Spacer height in 53 places and a divider width in one. Only the call +shape says which, so this reads the shape. + + spacing padding(...), PaddingValues(...), Arrangement.spacedBy(...), and a + .height()/.width() whose enclosing call is Spacer( + dimension .size(), a non-Spacer .height()/.width(), RoundedCornerShape(), + BorderStroke(), a `size =` argument + +Spacing positions are what phase 2 drives to zero; dimensions are out of its scope and are +reported so the number is visible rather than exempted. + +Usage: m3-spacing-positions.py [--list] +""" +import os, re, sys + +UI = 'composeApp/src/commonMain/kotlin/press/mantra/compose/ui' +THEME = os.path.join(UI, 'theme') + + +def _spans(text, opener): + """Brace-matched character ranges of every call matching `opener`.""" + spans = [] + for m in re.finditer(opener, text): + depth, i = 0, m.end() - 1 + while i < len(text): + if text[i] == '(': + depth += 1 + elif text[i] == ')': + depth -= 1 + if depth == 0: + spans.append((m.start(), i)) + break + i += 1 + return spans + + +def classify(path): + text = open(path, encoding='utf-8').read() + pads = _spans(text, r'\.?\bpadding\s*\(|\bPaddingValues\s*\(') + gaps = _spans(text, r'\bspacedBy\s*\(') + spacers = _spans(text, r'\bSpacer\s*\(') + + def inside(pos, spans): + return any(a <= pos <= b for a, b in spans) + + hw = [ + (m.start(1), m.end(1)) + for m in re.finditer(r'\.(?:height|width)\s*\(\s*(\d+\.dp)\s*\)', text) + if inside(m.start(), spacers) + ] + + spacing, dimension = [], [] + for m in re.finditer(r'\b(\d+)\.dp\b', text): + pos = m.start() + line_start = text.rfind('\n', 0, pos) + 1 + line = text[line_start:text.find('\n', pos)] + if re.match(r'\s*(//|\*|/\*)', line): + continue + entry = (path, text[:pos].count('\n') + 1, m.group(0), line.strip()[:90]) + if inside(pos, pads) or inside(pos, gaps) or any(a <= pos < b for a, b in hw): + spacing.append(entry) + else: + dimension.append(entry) + return spacing, dimension + + +def main(): + spacing, dimension = [], [] + for root, _, files in os.walk(UI): + if root.startswith(THEME): + continue + for f in sorted(files): + if f.endswith('.kt'): + s, d = classify(os.path.join(root, f)) + spacing += s + dimension += d + + print(f' {"dp literals in spacing positions":42s} {len(spacing):6d}') + print(f' {"dp literals in dimension positions":42s} {len(dimension):6d} (out of scope)') + + if '--list' in sys.argv: + for path, line, lit, ctx in spacing: + print(f' {path.replace(UI + "/", "")}:{line} {lit:>7s} {ctx}') + + return 1 if spacing else 0 + + +if __name__ == '__main__': + sys.exit(main())