056159abcf3a
feat: Add design system primitives (v2.3.60)
- time
- 2026-07-08 19:39 UTC · 3 months ago
- machine
- koh2
- parent
- e6295790f7af
↓ peach-android-056159ab.tar.gzatlas at this save
files changed7 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 −0app/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 {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+}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+}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+}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+}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+}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+}