From f9ea2936d9edd96e4eeed9e1f5c6e048dda0f52c Mon Sep 17 00:00:00 2001 From: shahondin1624 Date: Fri, 13 Mar 2026 12:57:27 +0100 Subject: [PATCH] feat: add responsive spacing and padding system (Closes #3) Add WindowSizeClass-aware spacing tokens (small/medium/large) to UiConstants and replace all hardcoded dp values in App.kt and AttributesPage.kt. Compact uses 8-12dp, Medium 12-16dp, Expanded 16-24dp. Co-Authored-By: Claude Opus 4.6 --- .../kotlin/org/shahondin1624/App.kt | 5 +- .../lib/components/UiConstants.kt | 36 +++++++++++ .../attributespage/AttributesPage.kt | 14 +++-- .../kotlin/org/shahondin1624/SpacingTest.kt | 60 +++++++++++++++++++ 4 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 sharedUI/src/commonTest/kotlin/org/shahondin1624/SpacingTest.kt diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt index 56a66f1..4990081 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt @@ -122,6 +122,9 @@ private fun AppContent( ) } ) { paddingValues -> + val windowSizeClass = LocalWindowSizeClass.current + val contentPadding = UiConstants.Spacing.large(windowSizeClass) + Box( modifier = Modifier .fillMaxSize() @@ -133,7 +136,7 @@ private fun AppContent( modifier = Modifier .widthIn(max = UiConstants.MAX_CONTENT_WIDTH) .fillMaxWidth() - .padding(16.dp), + .padding(contentPadding), horizontalAlignment = Alignment.CenterHorizontally ) { AttributesPage(character) diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/UiConstants.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/UiConstants.kt index 0ee8ef0..21c10e9 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/UiConstants.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/UiConstants.kt @@ -1,10 +1,46 @@ package org.shahondin1624.lib.components +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import org.shahondin1624.theme.LocalWindowSizeClass +import org.shahondin1624.theme.WindowSizeClass object UiConstants { val SMALL_PADDING = 6.dp /** Maximum width for the main content area to prevent uncomfortable stretching on ultra-wide displays. */ val MAX_CONTENT_WIDTH = 1200.dp + + /** + * Responsive spacing tokens that scale with [WindowSizeClass]. + * + * | Token | Compact | Medium | Expanded | + * |---------|---------|--------|----------| + * | small | 8 dp | 12 dp | 16 dp | + * | medium | 12 dp | 16 dp | 20 dp | + * | large | 12 dp | 16 dp | 24 dp | + */ + object Spacing { + /** Small spacing/gap (grid spacing, minor gaps). */ + fun small(sizeClass: WindowSizeClass): Dp = when (sizeClass) { + WindowSizeClass.Compact -> 8.dp + WindowSizeClass.Medium -> 12.dp + WindowSizeClass.Expanded -> 16.dp + } + + /** Medium spacing (content padding, section gaps). */ + fun medium(sizeClass: WindowSizeClass): Dp = when (sizeClass) { + WindowSizeClass.Compact -> 12.dp + WindowSizeClass.Medium -> 16.dp + WindowSizeClass.Expanded -> 20.dp + } + + /** Large spacing (outer page padding, major section dividers). */ + fun large(sizeClass: WindowSizeClass): Dp = when (sizeClass) { + WindowSizeClass.Compact -> 12.dp + WindowSizeClass.Medium -> 16.dp + WindowSizeClass.Expanded -> 24.dp + } + } } \ No newline at end of file diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/AttributesPage.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/AttributesPage.kt index f7a1b4f..dd9b8ad 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/AttributesPage.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/AttributesPage.kt @@ -15,19 +15,25 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import org.shahondin1624.lib.components.UiConstants import org.shahondin1624.model.charactermodel.ShadowrunCharacter +import org.shahondin1624.theme.LocalWindowSizeClass @Composable fun ColumnScope.AttributesPage(character: ShadowrunCharacter) { Box(modifier = Modifier.fillMaxWidth().weight(1f)) { val gridState = rememberLazyGridState() + val windowSizeClass = LocalWindowSizeClass.current + val gridSpacing = UiConstants.Spacing.small(windowSizeClass) + val gridPadding = UiConstants.Spacing.medium(windowSizeClass) + val dividerPadding = UiConstants.Spacing.medium(windowSizeClass) LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 75.dp), state = gridState, - modifier = Modifier.fillMaxSize().padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + modifier = Modifier.fillMaxSize().padding(gridPadding), + horizontalArrangement = Arrangement.spacedBy(gridSpacing), + verticalArrangement = Arrangement.spacedBy(gridSpacing) ) { items( items = character.attributes.getAllAttributes(), @@ -39,7 +45,7 @@ fun ColumnScope.AttributesPage(character: ShadowrunCharacter) { item(span = { GridItemSpan(maxLineSpan) }) { HorizontalDivider( - modifier = Modifier.padding(vertical = 16.dp), + modifier = Modifier.padding(vertical = dividerPadding), thickness = 2.dp ) } diff --git a/sharedUI/src/commonTest/kotlin/org/shahondin1624/SpacingTest.kt b/sharedUI/src/commonTest/kotlin/org/shahondin1624/SpacingTest.kt new file mode 100644 index 0000000..d50df59 --- /dev/null +++ b/sharedUI/src/commonTest/kotlin/org/shahondin1624/SpacingTest.kt @@ -0,0 +1,60 @@ +package org.shahondin1624 + +import androidx.compose.ui.unit.dp +import org.shahondin1624.lib.components.UiConstants +import org.shahondin1624.theme.WindowSizeClass +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SpacingTest { + + @Test + fun smallSpacingScalesWithSizeClass() { + val compact = UiConstants.Spacing.small(WindowSizeClass.Compact) + val medium = UiConstants.Spacing.small(WindowSizeClass.Medium) + val expanded = UiConstants.Spacing.small(WindowSizeClass.Expanded) + + assertEquals(8.dp, compact) + assertEquals(12.dp, medium) + assertEquals(16.dp, expanded) + assertTrue(compact < medium, "Compact small should be less than Medium small") + assertTrue(medium < expanded, "Medium small should be less than Expanded small") + } + + @Test + fun mediumSpacingScalesWithSizeClass() { + val compact = UiConstants.Spacing.medium(WindowSizeClass.Compact) + val medium = UiConstants.Spacing.medium(WindowSizeClass.Medium) + val expanded = UiConstants.Spacing.medium(WindowSizeClass.Expanded) + + assertEquals(12.dp, compact) + assertEquals(16.dp, medium) + assertEquals(20.dp, expanded) + assertTrue(compact < medium) + assertTrue(medium < expanded) + } + + @Test + fun largeSpacingScalesWithSizeClass() { + val compact = UiConstants.Spacing.large(WindowSizeClass.Compact) + val medium = UiConstants.Spacing.large(WindowSizeClass.Medium) + val expanded = UiConstants.Spacing.large(WindowSizeClass.Expanded) + + assertEquals(12.dp, compact) + assertEquals(16.dp, medium) + assertEquals(24.dp, expanded) + assertTrue(compact < medium) + assertTrue(medium < expanded) + } + + @Test + fun compactSpacingIsWithinRange() { + val small = UiConstants.Spacing.small(WindowSizeClass.Compact) + val med = UiConstants.Spacing.medium(WindowSizeClass.Compact) + val large = UiConstants.Spacing.large(WindowSizeClass.Compact) + assertTrue(small >= 8.dp && small <= 12.dp, "Compact small in 8-12dp range") + assertTrue(med >= 8.dp && med <= 12.dp, "Compact medium in 8-12dp range") + assertTrue(large >= 8.dp && large <= 12.dp, "Compact large in 8-12dp range") + } +} -- 2.49.1