056159abcf3a

feat: Add design system primitives (v2.3.60)

time
2026-07-08 19:39 UTC · 3 months ago
machine
koh2
parent
e6295790f7af
7 files app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/components/PeachEmptyOrEndState.kt+50 −0app/src/main/java/com/peachpasswords/ui/components/PeachExpandableCard.kt+66 −0app/src/main/java/com/peachpasswords/ui/components/PeachField.kt+90 −0app/src/main/java/com/peachpasswords/ui/components/PeachSegmentedControl.kt+67 −0app/src/main/java/com/peachpasswords/ui/components/PeachTopBar.kt+64 −0app/src/main/java/com/peachpasswords/ui/theme/PeachSpacing.kt+13 −0
app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616         applicationId "com.peachpasswords"1717         minSdk 261818         targetSdk 3519-        versionCode 23590020-        versionName "2.3.59"19+        versionCode 23600020+        versionName "2.3.60"2121 2222         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323         vectorDrawables {

view file

app/src/main/java/com/peachpasswords/ui/components/PeachEmptyOrEndState.kt+50 −0
@@ -0,0 +1,50 @@1+package com.peachpasswords.ui.components2+3+import androidx.compose.foundation.layout.Arrangement4+import androidx.compose.foundation.layout.Column5+import androidx.compose.foundation.layout.fillMaxWidth6+import androidx.compose.foundation.layout.padding7+import androidx.compose.material3.Text8+import androidx.compose.runtime.Composable9+import androidx.compose.ui.Alignment10+import androidx.compose.ui.Modifier11+import androidx.compose.ui.text.font.FontStyle12+import androidx.compose.ui.unit.dp13+import androidx.compose.ui.unit.sp14+import com.peachpasswords.ui.theme.PeachSpacing15+import com.peachpasswords.ui.theme.PeachType16+import com.peachpasswords.ui.theme.peachColors17+18+@Composable19+fun PeachEmptyOrEndState(20+    message: String,21+    modifier: Modifier = Modifier,22+    detail: String? = null,23+    action: (@Composable () -> Unit)? = null,24+) {25+    Column(26+        modifier = modifier27+            .fillMaxWidth()28+            .padding(vertical = PeachSpacing.xxl, horizontal = PeachSpacing.xl),29+        horizontalAlignment = Alignment.CenterHorizontally,30+        verticalArrangement = Arrangement.spacedBy(PeachSpacing.sm),31+    ) {32+        PeachMark(size = 34.dp, alpha = 0.52f)33+        Text(34+            text = message,35+            color = peachColors.T1,36+            style = PeachType.Display.let {37+                androidx.compose.ui.text.TextStyle(38+                    fontFamily = it,39+                    fontStyle = FontStyle.Italic,40+                    fontSize = 20.sp,41+                    lineHeight = 22.sp,42+                )43+            },44+        )45+        if (detail != null) {46+            Text(detail, color = peachColors.T2, style = PeachType.hint)47+        }48+        if (action != null) action()49+    }50+}

view file

app/src/main/java/com/peachpasswords/ui/components/PeachExpandableCard.kt+66 −0
@@ -0,0 +1,66 @@1+package com.peachpasswords.ui.components2+3+import androidx.compose.animation.core.Spring4+import androidx.compose.animation.core.animateDpAsState5+import androidx.compose.animation.core.spring6+import androidx.compose.foundation.gestures.Orientation7+import androidx.compose.foundation.gestures.scrollable8+import androidx.compose.foundation.gestures.rememberScrollableState9+import androidx.compose.foundation.layout.Column10+import androidx.compose.foundation.layout.ColumnScope11+import androidx.compose.foundation.layout.fillMaxWidth12+import androidx.compose.foundation.layout.heightIn13+import androidx.compose.foundation.layout.padding14+import androidx.compose.foundation.shape.RoundedCornerShape15+import androidx.compose.material3.Surface16+import androidx.compose.runtime.Composable17+import androidx.compose.runtime.getValue18+import androidx.compose.runtime.mutableFloatStateOf19+import androidx.compose.runtime.remember20+import androidx.compose.runtime.setValue21+import androidx.compose.ui.Modifier22+import androidx.compose.ui.unit.Dp23+import androidx.compose.ui.unit.dp24+import com.peachpasswords.ui.theme.LocalReduceMotion25+import com.peachpasswords.ui.theme.PeachSpacing26+import com.peachpasswords.ui.theme.peachColors27+28+@Composable29+fun PeachExpandableCard(30+    modifier: Modifier = Modifier,31+    collapsedHeight: Dp = 168.dp,32+    expandedHeight: Dp = 360.dp,33+    content: @Composable ColumnScope.(progress: Float) -> Unit,34+) {35+    val reduceMotion = LocalReduceMotion.current36+    var progress by remember { mutableFloatStateOf(0f) }37+    val animatedHeight by animateDpAsState(38+        targetValue = collapsedHeight + (expandedHeight - collapsedHeight) * progress,39+        animationSpec = if (reduceMotion) {40+            spring(stiffness = Spring.StiffnessHigh, dampingRatio = Spring.DampingRatioNoBouncy)41+        } else {42+            spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)43+        },44+        label = "peach_expandable_card_height",45+    )46+    val scrollState = rememberScrollableState { delta ->47+        val old = progress48+        progress = (progress + delta / 900f).coerceIn(0f, 1f)49+        if (old == progress) 0f else delta50+    }51+52+    Surface(53+        modifier = modifier54+            .fillMaxWidth()55+            .heightIn(min = collapsedHeight, max = animatedHeight)56+            .scrollable(scrollState, orientation = Orientation.Vertical),57+        color = peachColors.SurfaceElevated,58+        contentColor = peachColors.T0,59+        shape = RoundedCornerShape(18.dp),60+        tonalElevation = 1.dp,61+    ) {62+        Column(Modifier.padding(PeachSpacing.lg)) {63+            content(progress)64+        }65+    }66+}

view file

app/src/main/java/com/peachpasswords/ui/components/PeachField.kt+90 −0
@@ -0,0 +1,90 @@1+package com.peachpasswords.ui.components2+3+import androidx.compose.foundation.interaction.MutableInteractionSource4+import androidx.compose.foundation.interaction.collectIsFocusedAsState5+import androidx.compose.foundation.layout.Column6+import androidx.compose.foundation.layout.Row7+import androidx.compose.foundation.layout.Spacer8+import androidx.compose.foundation.layout.fillMaxWidth9+import androidx.compose.foundation.layout.padding10+import androidx.compose.foundation.layout.width11+import androidx.compose.foundation.text.BasicTextField12+import androidx.compose.foundation.text.KeyboardOptions13+import androidx.compose.material3.Text14+import androidx.compose.runtime.Composable15+import androidx.compose.runtime.getValue16+import androidx.compose.runtime.remember17+import androidx.compose.ui.Alignment18+import androidx.compose.ui.Modifier19+import androidx.compose.ui.draw.drawBehind20+import androidx.compose.ui.geometry.Offset21+import androidx.compose.ui.geometry.Size22+import androidx.compose.ui.graphics.SolidColor23+import androidx.compose.ui.text.TextStyle24+import androidx.compose.ui.text.input.KeyboardType25+import androidx.compose.ui.text.input.VisualTransformation26+import androidx.compose.ui.unit.dp27+import androidx.compose.ui.unit.em28+import androidx.compose.ui.unit.sp29+import com.peachpasswords.ui.theme.LocalPeachAccent30+import com.peachpasswords.ui.theme.PeachSpacing31+import com.peachpasswords.ui.theme.PeachType32+import com.peachpasswords.ui.theme.peachColors33+34+@Composable35+fun PeachField(36+    label: String,37+    value: String,38+    onValueChange: (String) -> Unit,39+    modifier: Modifier = Modifier,40+    singleLine: Boolean = true,41+    mono: Boolean = true,42+    keyboardType: KeyboardType = KeyboardType.Text,43+    visualTransformation: VisualTransformation = VisualTransformation.None,44+    trailing: (@Composable () -> Unit)? = null,45+) {46+    val accent = LocalPeachAccent.current47+    val interaction = remember { MutableInteractionSource() }48+    val focused by interaction.collectIsFocusedAsState()49+    val hairline = peachColors.T350+51+    Column(modifier.fillMaxWidth()) {52+        Text(53+            label.uppercase(),54+            color = peachColors.T2,55+            fontFamily = PeachType.Mono,56+            fontSize = 9.sp,57+            letterSpacing = 0.08.em,58+        )59+        Row(verticalAlignment = Alignment.CenterVertically) {60+            BasicTextField(61+                value = value,62+                onValueChange = onValueChange,63+                singleLine = singleLine,64+                textStyle = TextStyle(65+                    color = peachColors.T0,66+                    fontFamily = if (mono) PeachType.Mono else PeachType.Body,67+                    fontSize = 13.sp,68+                ),69+                cursorBrush = SolidColor(accent),70+                interactionSource = interaction,71+                keyboardOptions = KeyboardOptions(keyboardType = keyboardType),72+                visualTransformation = visualTransformation,73+                modifier = Modifier74+                    .weight(1f)75+                    .drawBehind {76+                        drawRect(77+                            color = if (focused) accent else hairline,78+                            topLeft = Offset(0f, size.height - 1.dp.toPx()),79+                            size = Size(size.width, 1.dp.toPx()),80+                        )81+                    }82+                    .padding(vertical = PeachSpacing.sm),83+            )84+            if (trailing != null) {85+                Spacer(Modifier.width(PeachSpacing.sm))86+                trailing()87+            }88+        }89+    }90+}

view file

app/src/main/java/com/peachpasswords/ui/components/PeachSegmentedControl.kt+67 −0
@@ -0,0 +1,67 @@1+package com.peachpasswords.ui.components2+3+import androidx.compose.foundation.clickable4+import androidx.compose.foundation.layout.Arrangement5+import androidx.compose.foundation.layout.Row6+import androidx.compose.foundation.layout.defaultMinSize7+import androidx.compose.foundation.layout.fillMaxWidth8+import androidx.compose.foundation.layout.padding9+import androidx.compose.foundation.shape.RoundedCornerShape10+import androidx.compose.material3.Surface11+import androidx.compose.material3.Text12+import androidx.compose.runtime.Composable13+import androidx.compose.ui.Alignment14+import androidx.compose.ui.Modifier15+import androidx.compose.ui.unit.dp16+import com.peachpasswords.ui.theme.LocalPeachAccent17+import com.peachpasswords.ui.theme.PeachSpacing18+import com.peachpasswords.ui.theme.PeachType19+import com.peachpasswords.ui.theme.peachColors20+21+data class PeachSegment(22+    val key: String,23+    val label: String,24+)25+26+@Composable27+fun PeachSegmentedControl(28+    segments: List<PeachSegment>,29+    selectedKey: String,30+    onSelected: (String) -> Unit,31+    modifier: Modifier = Modifier,32+) {33+    val accent = LocalPeachAccent.current34+    Surface(35+        modifier = modifier.fillMaxWidth(),36+        color = peachColors.S1,37+        shape = RoundedCornerShape(999.dp),38+        tonalElevation = 1.dp,39+    ) {40+        Row(41+            modifier = Modifier.padding(PeachSpacing.xs),42+            verticalAlignment = Alignment.CenterVertically,43+            horizontalArrangement = Arrangement.spacedBy(PeachSpacing.xs),44+        ) {45+            segments.forEach { segment ->46+                val selected = segment.key == selectedKey47+                Surface(48+                    modifier = Modifier49+                        .weight(1f)50+                        .defaultMinSize(minHeight = 34.dp)51+                        .clickable { onSelected(segment.key) },52+                    color = if (selected) accent else peachColors.S2,53+                    contentColor = if (selected) peachColors.Bg else peachColors.T1,54+                    shape = RoundedCornerShape(999.dp),55+                ) {56+                    Row(57+                        modifier = Modifier.padding(horizontal = PeachSpacing.sm, vertical = PeachSpacing.sm),58+                        horizontalArrangement = Arrangement.Center,59+                        verticalAlignment = Alignment.CenterVertically,60+                    ) {61+                        Text(segment.label.uppercase(), style = PeachType.sortPill)62+                    }63+                }64+            }65+        }66+    }67+}

view file

app/src/main/java/com/peachpasswords/ui/components/PeachTopBar.kt+64 −0
@@ -0,0 +1,64 @@1+package com.peachpasswords.ui.components2+3+import androidx.compose.foundation.layout.Arrangement4+import androidx.compose.foundation.layout.Box5+import androidx.compose.foundation.layout.Row6+import androidx.compose.foundation.layout.RowScope7+import androidx.compose.foundation.layout.fillMaxWidth8+import androidx.compose.foundation.layout.height9+import androidx.compose.foundation.layout.padding10+import androidx.compose.foundation.layout.widthIn11+import androidx.compose.foundation.shape.RoundedCornerShape12+import androidx.compose.material3.Surface13+import androidx.compose.material3.Text14+import androidx.compose.runtime.Composable15+import androidx.compose.ui.Alignment16+import androidx.compose.ui.Modifier17+import androidx.compose.ui.unit.dp18+import com.peachpasswords.ui.theme.PeachSpacing19+import com.peachpasswords.ui.theme.PeachType20+import com.peachpasswords.ui.theme.peachColors21+22+@Composable23+fun PeachTopBar(24+    title: String,25+    modifier: Modifier = Modifier,26+    leading: (@Composable RowScope.() -> Unit)? = null,27+    actions: (@Composable RowScope.() -> Unit)? = null,28+) {29+    Surface(30+        modifier = modifier.fillMaxWidth(),31+        color = peachColors.S1,32+        contentColor = peachColors.T0,33+        shape = RoundedCornerShape(18.dp),34+        tonalElevation = 1.dp,35+    ) {36+        Row(37+            modifier = Modifier38+                .height(56.dp)39+                .padding(horizontal = PeachSpacing.md),40+            verticalAlignment = Alignment.CenterVertically,41+            horizontalArrangement = Arrangement.spacedBy(PeachSpacing.sm),42+        ) {43+            if (leading != null) {44+                Row(verticalAlignment = Alignment.CenterVertically, content = leading)45+            }46+            Text(47+                text = title,48+                color = peachColors.T0,49+                style = PeachType.wordmark,50+                modifier = Modifier.weight(1f),51+            )52+            if (actions != null) {53+                Row(54+                    modifier = Modifier.widthIn(min = 40.dp),55+                    verticalAlignment = Alignment.CenterVertically,56+                    horizontalArrangement = Arrangement.spacedBy(PeachSpacing.xs),57+                    content = actions,58+                )59+            } else {60+                Box(Modifier.widthIn(min = 40.dp))61+            }62+        }63+    }64+}

view file

app/src/main/java/com/peachpasswords/ui/theme/PeachSpacing.kt+13 −0
@@ -0,0 +1,13 @@1+package com.peachpasswords.ui.theme2+3+import androidx.compose.ui.unit.dp4+5+/** Shared 4dp-based spacing scale extracted from the lock-screen layout. */6+object PeachSpacing {7+    val xs = 4.dp8+    val sm = 8.dp9+    val md = 14.dp10+    val lg = 18.dp11+    val xl = 24.dp12+    val xxl = 32.dp13+}

view file