feat: add responsive spacing and padding system (Closes #3) #42

Merged
shahondin1624 merged 1 commits from feature/issue-3-responsive-spacing-and-padding-system into main 2026-03-13 12:57:45 +01:00
4 changed files with 110 additions and 5 deletions

View File

@@ -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)

View File

@@ -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
}
}
}

View File

@@ -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
)
}

View File

@@ -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")
}
}