c072a6b4976e

four onboarding fixes from live review: (1) rune background was frozen solid — it was driven by animateFloat/infiniteRepeatable, which the system animator_duration_scale can zero out, so the ambient field stopped dead wherever that setting is turned down; moved it to the frame clock like the browser's rAF loop and gave each row its own drift speed (8/11/6, RuneBackground.tsx SPEEDS) so the field shears horizontally instead of sliding as one block, verified moving with animator scale still at 0. (2) onboarding cards were vertically centred, leaving dead space above and below on a tall phone — card now sits high with the nav pinned to the bottom, and the card's type/padding scaled up (body 13 to 16sp, vertical padding 28 to 44dp) since it was sized for a 520px browser popup and read as a small box adrift on a 570dp screen; tried the browser's literal flex-1 full-height card first, it looked wrong, reverted to content height. (3) restore chooser was missing Codex — added a third From Peach Codex option handing off to the existing CodexRestoreScreen, which already works pre-vault, and a completed Codex restore now lands in the vault instead of popping back to the chooser underneath it. (4) onboarding's wordmark had lost its peach mark; extracted the mark-plus-text lockup into PeachWordmark and used it on both Lock and Onboarding, which is where the two copies had drifted apart.

time
2026-07-24 19:57 UTC · 3 months ago
machine
koh2
agent
claude-opus-5
session
onboarding polish
parent
270e5244bf8f
6 files app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+11 −1app/src/main/java/com/peachpasswords/ui/components/PeachMark.kt+21 −0app/src/main/java/com/peachpasswords/ui/components/RuneGridBackground.kt+28 −32app/src/main/java/com/peachpasswords/ui/lock/LockScreen.kt+3 −11app/src/main/java/com/peachpasswords/ui/onboarding/OnboardingScreen.kt+16 −14app/src/main/java/com/peachpasswords/ui/vault/ImportScreen.kt+12 −0
app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+11 −1
@@ -219,7 +219,16 @@219219                 onUpgradePro = { nav(PeachNav.Settings) },220220             )221221         }222-        composable(PeachNav.CodexRestore.route) { CodexRestoreScreen(onBack = back, onRestored = back) }222+        composable(PeachNav.CodexRestore.route) {223+            CodexRestoreScreen(224+                onBack = back,225+                onRestored = {226+                    navController.navigate(PeachNav.VaultList.route) {227+                        popUpTo(0) { inclusive = true }228+                    }229+                },230+            )231+        }223232         composable(PeachNav.ChirpPair.route) { ChirpPairScreen(onBack = back) }224233         composable(PeachNav.Health.route) {225234             VaultHealthScreen(onBack = back, onEditEntry = { nav(PeachNav.EntryDetail(it)) })@@ -228,6 +237,7 @@228237         composable(PeachNav.Import.route) {229238             ImportScreen(230239                 onBack = back,240+                onCodex = { nav(PeachNav.CodexRestore) },231241                 onVaultCreated = {232242                     navController.navigate(PeachNav.RecoverySetup.route) {233243                         popUpTo(0) { inclusive = true }

view file

app/src/main/java/com/peachpasswords/ui/components/PeachMark.kt+21 −0
@@ -2,6 +2,7 @@22 33 import androidx.compose.foundation.Canvas44 import androidx.compose.foundation.layout.size5+import androidx.compose.foundation.layout.padding56 import androidx.compose.runtime.Composable67 import androidx.compose.runtime.remember78 import androidx.compose.ui.Modifier@@ -49,5 +50,25 @@4950             drawPath(leaf, Color(0xFF92AC65), alpha = alpha)5051             drawPath(vein, Color(0xFF5E7A42), alpha = alpha, style = Stroke(width = 2.6f))5152         }53+    }54+}55+56+/**57+ * Mark + "peach" lockup, top-left on the pre-vault screens. Extracted because58+ * Lock and Onboarding each hand-rolled it and drifted apart — onboarding lost59+ * the mark entirely.60+ */61+@Composable62+fun PeachWordmark(modifier: Modifier = Modifier) {63+    androidx.compose.foundation.layout.Row(64+        modifier = modifier,65+        verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,66+    ) {67+        PeachMark(modifier = Modifier.padding(end = com.peachpasswords.ui.theme.PeachSpacing.xs))68+        androidx.compose.material3.Text(69+            text = "peach",70+            color = com.peachpasswords.ui.theme.LocalPeachAccent.current,71+            style = com.peachpasswords.ui.theme.PeachType.wordmark,72+        )5273     }5374 }

view file

app/src/main/java/com/peachpasswords/ui/components/RuneGridBackground.kt+28 −32
@@ -1,13 +1,10 @@11 package com.peachpasswords.ui.components22 3-import androidx.compose.animation.core.LinearEasing4-import androidx.compose.animation.core.animateFloat5-import androidx.compose.animation.core.infiniteRepeatable6-import androidx.compose.animation.core.rememberInfiniteTransition7-import androidx.compose.animation.core.tween83 import androidx.compose.foundation.Canvas94 import androidx.compose.runtime.Composable10-import androidx.compose.runtime.getValue5+import androidx.compose.runtime.LaunchedEffect6+import androidx.compose.runtime.mutableFloatStateOf7+import androidx.compose.runtime.withFrameMillis118 import androidx.compose.runtime.remember129 import androidx.compose.ui.Modifier1310 import androidx.compose.ui.geometry.Offset@@ -43,13 +40,27 @@4340         fontSize = 24,4441         tileWidth = 54.dp,4542         rowHeight = 42.dp,46-        speedDpPerSecond = 8f,4743         seed = 19,4844     )49-    val offset = rememberRuneOffset(layer.patternWidthDp, layer.speedDpPerSecond, reduceMotion)5045 46+    // Driven off the frame clock, not animateFloat/infiniteRepeatable: those47+    // are scaled by the system animator duration setting, so an ambient48+    // background silently froze solid wherever that's turned down to 0 (the49+    // browser's requestAnimationFrame loop has no such dependency). Read in50+    // the draw scope so a frame is a redraw, not a recomposition.51+    val elapsedSeconds = remember { mutableFloatStateOf(0f) }52+    LaunchedEffect(reduceMotion) {53+        if (reduceMotion) return@LaunchedEffect54+        var last = withFrameMillis { it }55+        while (true) {56+            val now = withFrameMillis { it }57+            elapsedSeconds.floatValue += (now - last).coerceAtMost(64L) / 1000f58+            last = now59+        }60+    }61+5162     Canvas(modifier) {52-        drawRuneLayer(layer, offset)63+        drawRuneLayer(layer, elapsedSeconds.floatValue)5364     }5465 }5566 @@ -59,7 +70,6 @@5970     fontSize: Int,6071     tileWidth: Dp,6172     rowHeight: Dp,62-    speedDpPerSecond: Float,6373     seed: Int,6474 ): RuneLayer {6575     val measurer = rememberTextMeasurer()@@ -76,52 +86,38 @@7686         tileWidth = tileWidth,7787         rowHeight = rowHeight,7888         patternWidthDp = tileWidth * PATTERN_COLUMNS,79-        speedDpPerSecond = speedDpPerSecond,8089         seed = seed,8190     )8291 }8392 84-@Composable85-private fun rememberRuneOffset(86-    patternWidth: Dp,87-    speedDpPerSecond: Float,88-    reduceMotion: Boolean,89-): Float {90-    if (reduceMotion) return 0f91-    val durationMillis = ((patternWidth.value / speedDpPerSecond) * 1000f).toInt().coerceAtLeast(1)92-    val transition = rememberInfiniteTransition(label = "rune_grid")93-    val offset by transition.animateFloat(94-        initialValue = 0f,95-        targetValue = patternWidth.value,96-        animationSpec = infiniteRepeatable(tween(durationMillis, easing = LinearEasing)),97-        label = "rune_grid_offset",98-    )99-    return offset100-}101-10293 private data class RuneLayer(10394     val glyphs: List<androidx.compose.ui.text.TextLayoutResult>,10495     val tileWidth: Dp,10596     val rowHeight: Dp,10697     val patternWidthDp: Dp,107-    val speedDpPerSecond: Float,10898     val seed: Int,10999 )110100 101+/** Browser parity (RuneBackground.tsx SPEEDS): three drift rates, cycled by row. */102+private val ROW_SPEEDS_DP = floatArrayOf(8f, 11f, 6f)103+111104 private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawRuneLayer(112105     layer: RuneLayer,113-    offsetDp: Float,106+    elapsedSeconds: Float,114107 ) {115108     val tileWidth = layer.tileWidth.toPx()116109     val rowHeight = layer.rowHeight.toPx()117110     val patternWidth = layer.patternWidthDp.toPx()118-    val offset = (offsetDp / layer.patternWidthDp.value) * patternWidth119111     val columns = ceil(size.width / tileWidth).toInt() + PATTERN_COLUMNS * 2120112     val rows = ceil(size.height / rowHeight).toInt() + 2121113     val startColumn = -PATTERN_COLUMNS122114 123115     for (row in -1 until rows) {124116         val y = row * rowHeight117+        // Each row drifts at its own speed, so the field shears instead of118+        // sliding as one rigid block.119+        val speedPx = ROW_SPEEDS_DP[positiveMod(row, ROW_SPEEDS_DP.size)] * density120+        val offset = (elapsedSeconds * speedPx) % patternWidth125121         for (i in 0..columns) {126122             val column = startColumn + i127123             val x = column * tileWidth - offset

view file

app/src/main/java/com/peachpasswords/ui/lock/LockScreen.kt+3 −11
@@ -57,6 +57,7 @@5757 import com.peachpasswords.ui.PeachNav5858 import com.peachpasswords.ui.components.LockRuneString5959 import com.peachpasswords.ui.components.PeachMark60+import com.peachpasswords.ui.components.PeachWordmark6061 import com.peachpasswords.ui.theme.LocalPeachAccent6162 import com.peachpasswords.ui.theme.PeachSpacing6263 import com.peachpasswords.ui.theme.peachColors@@ -120,18 +121,9 @@120121     }121122 122123     ShellBackground {123-        // Wordmark, top-left.124-        Row(124+        PeachWordmark(125125             modifier = Modifier.align(Alignment.TopStart).systemBarsPadding().padding(start = 20.dp, top = 8.dp),126-            verticalAlignment = Alignment.CenterVertically,127-        ) {128-            PeachMark(modifier = Modifier.padding(end = PeachSpacing.xs))129-            Text(130-                text = "peach",131-                color = accent,132-                style = PeachType.wordmark,133-            )134-        }126+        )135127 136128         Column(137129             modifier = Modifier

view file

app/src/main/java/com/peachpasswords/ui/onboarding/OnboardingScreen.kt+16 −14
@@ -46,6 +46,7 @@4646 import androidx.compose.ui.unit.em4747 import androidx.compose.ui.unit.sp4848 import androidx.compose.material3.Text49+import com.peachpasswords.ui.components.PeachWordmark4950 import com.peachpasswords.ui.components.RuneGridBackground5051 import com.peachpasswords.ui.theme.LocalPeachAccent5152 import com.peachpasswords.ui.theme.peachColors@@ -139,11 +140,7 @@139140     ShellBackground {140141         RuneGridBackground(modifier = Modifier.fillMaxSize())141142 142-        // Wordmark, top-left — matches LockScreen.143-        Text(144-            text = "peach",145-            color = accent,146-            style = PeachType.wordmark,143+        PeachWordmark(147144             modifier = Modifier148145                 .align(Alignment.TopStart)149146                 .systemBarsPadding()@@ -161,9 +158,12 @@161158                 .fillMaxWidth()162159                 .systemBarsPadding()163160                 .padding(horizontal = 28.dp),164-            verticalArrangement = Arrangement.Center,165161             horizontalAlignment = Alignment.CenterHorizontally,166162         ) {163+            // Card sits high, nav pinned to the bottom edge — the browser's164+            // flex column (header / card / nav), not a centred block that165+            // leaves dead space above and below on a tall phone.166+            Spacer(Modifier.height(40.dp))167167             CardSwitcher(card, onClick = advance) { active ->168168                 GlassCardPanel {169169                     CardContent(ONBOARDING_CARDS[active])@@ -175,7 +175,7 @@175175                 }176176             }177177 178-            Spacer(Modifier.height(28.dp))178+            Spacer(Modifier.weight(1f))179179 180180             NavRow(181181                 card = card,@@ -183,6 +183,7 @@183183                 onBack = { if (card > 0) card-- },184184                 onNext = advance,185185             )186+            Spacer(Modifier.height(12.dp))186187         }187188     }188189 }@@ -206,6 +207,7 @@206207 private fun CardSwitcher(207208     target: Int,208209     onClick: () -> Unit,210+    modifier: Modifier = Modifier,209211     content: @Composable (Int) -> Unit,210212 ) {211213     val density = LocalDensity.current@@ -230,7 +232,7 @@230232     }231233 232234     Box(233-        modifier = Modifier235+        modifier = modifier234236             .fillMaxWidth()235237             .offset { IntOffset(translate.value.toInt(), 0) }236238             .alpha(fade.value)@@ -256,7 +258,7 @@256258             .clip(RoundedCornerShape(18.dp))257259             .background(peachColors.SurfaceElevated)258260             .border(1.dp, peachColors.T3, RoundedCornerShape(18.dp))259-            .padding(horizontal = 24.dp, vertical = 28.dp),261+            .padding(horizontal = 26.dp, vertical = 44.dp),260262         content = content,261263     )262264 }@@ -266,20 +268,20 @@266268 private fun headline() = TextStyle(267269     fontFamily = PeachType.Display,268270     fontStyle = FontStyle.Italic,269-    fontSize = 40.sp,270-    lineHeight = (40 * 0.98).sp,271+    fontSize = 44.sp,272+    lineHeight = (44 * 1.0).sp,271273 )272274 273275 @Composable274276 private fun CardContent(copy: OnboardingCardCopy) {275277     Text(copy.headline, color = peachColors.T0, style = headline())276-    Spacer(Modifier.height(18.dp))278+    Spacer(Modifier.height(24.dp))277279     copy.body.forEachIndexed { i, para ->278-        if (i > 0) Spacer(Modifier.height(12.dp))280+        if (i > 0) Spacer(Modifier.height(18.dp))279281         Text(280282             para,281283             color = peachColors.T1,282-            style = PeachType.body.copy(fontSize = 13.sp, lineHeight = (13 * 1.5).sp),284+            style = PeachType.body.copy(fontSize = 16.sp, lineHeight = (16 * 1.55).sp),283285         )284286     }285287     copy.warning?.let { WarningRow(it) }

view file

app/src/main/java/com/peachpasswords/ui/vault/ImportScreen.kt+12 −0
@@ -337,6 +337,9 @@337337 @Composable338338 fun ImportScreen(339339     onBack: () -> Unit,340+    // Codex is the third restore format (browser RestoreTab: symbols|codex|csv).341+    // It has its own screen, so this hands off rather than rendering inline.342+    onCodex: () -> Unit = {},340343     // Only reached when confirmImport had to create a new vault first (fresh341344     // install or "forgot password" recovery) — routes to recovery-word setup,342345     // same as LockScreen.create, instead of popping back to a stale Lock/Import@@ -388,6 +391,7 @@388391                 ChooseView(389392                     onPeachScript = { route = ImportRoute.PEACHSCRIPT },390393                     onFile = { route = ImportRoute.FILE },394+                    onCodex = onCodex,391395                     modifier = Modifier.weight(1f).fillMaxWidth(),392396                     composeRemainingSpace = true,393397                 )@@ -426,6 +430,7 @@426430                                 ImportRoute.CHOOSE -> ChooseView(427431                                     onPeachScript = { route = ImportRoute.PEACHSCRIPT },428432                                     onFile = { route = ImportRoute.FILE },433+                                    onCodex = onCodex,429434                                 )430435                             }431436                         }@@ -453,6 +458,7 @@453458 private fun ChooseView(454459     onPeachScript: () -> Unit,455460     onFile: () -> Unit,461+    onCodex: () -> Unit,456462     modifier: Modifier = Modifier,457463     // Only true from the non-scrolling call site, where `modifier` carries a458464     // Column-scope weight and the remaining height is real and boundable.@@ -471,6 +477,12 @@471477             title = "From file",472478             subtitle = "Bitwarden, Proton Pass, 1Password, Chrome, Safari, Edge CSV",473479             onClick = onFile,480+        )481+        Spacer(Modifier.size(12.dp))482+        OptionCard(483+            title = "From Peach Codex",484+            subtitle = "Scan or open a Codex PDF you printed",485+            onClick = onCodex,474486         )475487         if (composeRemainingSpace) {476488             Box(Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {

view file