689a906288b0

compose import screen's 'choose an import source' prompt into the remaining viewport height instead of trailing into dead space below the two option cards; scoped narrowly to the non-scrolling default landing state so the scrollable preview/error states are untouched

time
2026-07-24 03:01 UTC · 3 months ago
machine
koh2
agent
claude-sonnet-5
session
gate1-gate2-verification-pass
parent
0cf07b8d1c5c
1 file app/src/main/java/com/peachpasswords/ui/vault/ImportScreen.kt+74 −45
app/src/main/java/com/peachpasswords/ui/vault/ImportScreen.kt+74 −45
@@ -333,39 +333,48 @@333333 334334             Spacer(Modifier.size(PeachSpacing.lg))335335 336-            Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {337-                when (val s = state) {338-                    is ImportUiState.Preview -> PreviewView(339-                        preview = s.preview,340-                        onConfirm = { mode -> vm.confirmImport(s.preview, mode) },341-                        onCancel = { vm.reset() },342-                    )343-                    is ImportUiState.Done -> DoneView(count = s.count, onDone = onBack)344-                    is ImportUiState.Failed -> {345-                        ErrorBanner(s.message)346-                        Spacer(Modifier.size(16.dp))347-                        when (route) {336+            if (state == ImportUiState.Idle && route == ImportRoute.CHOOSE) {337+                // The default landing state (two option cards + a prompt) never338+                // needs to scroll — give it the real remaining height so the339+                // prompt composes into it instead of trailing into dead space.340+                ChooseView(341+                    onPeachScript = { route = ImportRoute.PEACHSCRIPT },342+                    onFile = { route = ImportRoute.FILE },343+                    modifier = Modifier.weight(1f).fillMaxWidth(),344+                    composeRemainingSpace = true,345+                )346+            } else {347+                Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {348+                    when (val s = state) {349+                        is ImportUiState.Preview -> PreviewView(350+                            preview = s.preview,351+                            onConfirm = { mode -> vm.confirmImport(s.preview, mode) },352+                            onCancel = { vm.reset() },353+                        )354+                        is ImportUiState.Done -> DoneView(count = s.count, onDone = onBack)355+                        is ImportUiState.Failed -> {356+                            ErrorBanner(s.message)357+                            Spacer(Modifier.size(16.dp))358+                            when (route) {359+                                ImportRoute.PEACHSCRIPT -> PeachScriptView(vm)360+                                ImportRoute.FILE -> FileView(vm)361+                                ImportRoute.CHOOSE -> ChooseView(362+                                    onPeachScript = { route = ImportRoute.PEACHSCRIPT },363+                                    onFile = { route = ImportRoute.FILE },364+                                )365+                            }366+                        }367+                        ImportUiState.Working -> WorkingView()368+                        ImportUiState.Idle -> when (route) {369+                            ImportRoute.CHOOSE -> Unit // handled above, outside the scroll container348370                             ImportRoute.PEACHSCRIPT -> PeachScriptView(vm)349371                             ImportRoute.FILE -> FileView(vm)350-                            ImportRoute.CHOOSE -> ChooseView(351-                                onPeachScript = { route = ImportRoute.PEACHSCRIPT },352-                                onFile = { route = ImportRoute.FILE },353-                            )354372                         }373+                        is ImportUiState.PeachScriptFile -> {374+                            LaunchedEffect(s.text) { route = ImportRoute.PEACHSCRIPT }375+                            PeachScriptView(vm, initialPaste = s.text)376+                        }355377                     }356-                    ImportUiState.Working -> WorkingView()357-                    ImportUiState.Idle -> when (route) {358-                        ImportRoute.CHOOSE -> ChooseView(359-                            onPeachScript = { route = ImportRoute.PEACHSCRIPT },360-                            onFile = { route = ImportRoute.FILE },361-                        )362-                        ImportRoute.PEACHSCRIPT -> PeachScriptView(vm)363-                        ImportRoute.FILE -> FileView(vm)364-                    }365-                    is ImportUiState.PeachScriptFile -> {366-                        LaunchedEffect(s.text) { route = ImportRoute.PEACHSCRIPT }367-                        PeachScriptView(vm, initialPaste = s.text)368-                    }369378                 }370379             }371380         }@@ -375,22 +384,42 @@375384 // ---- Choose view ----------------------------------------------------------376385 377386 @Composable378-private fun ChooseView(onPeachScript: () -> Unit, onFile: () -> Unit) {379-    OptionCard(380-        title = "From PeachScript",381-        subtitle = "Paste an encrypted Peach export",382-        onClick = onPeachScript,383-    )384-    Spacer(Modifier.size(12.dp))385-    OptionCard(386-        title = "From file",387-        subtitle = "Bitwarden, Proton Pass, 1Password, Chrome, Safari, Edge CSV",388-        onClick = onFile,389-    )390-    PeachEmptyOrEndState(391-        message = "Choose an import source.",392-        detail = "Peach will preview everything before it changes your vault.",393-    )387+private fun ChooseView(388+    onPeachScript: () -> Unit,389+    onFile: () -> Unit,390+    modifier: Modifier = Modifier,391+    // Only true from the non-scrolling call site, where `modifier` carries a392+    // Column-scope weight and the remaining height is real and boundable.393+    // The scrolling call site (inside a verticalScroll Column, unbounded394+    // height) must keep the plain flowing layout or `weight()` would crash.395+    composeRemainingSpace: Boolean = false,396+) {397+    Column(modifier) {398+        OptionCard(399+            title = "From PeachScript",400+            subtitle = "Paste an encrypted Peach export",401+            onClick = onPeachScript,402+        )403+        Spacer(Modifier.size(12.dp))404+        OptionCard(405+            title = "From file",406+            subtitle = "Bitwarden, Proton Pass, 1Password, Chrome, Safari, Edge CSV",407+            onClick = onFile,408+        )409+        if (composeRemainingSpace) {410+            Box(Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {411+                PeachEmptyOrEndState(412+                    message = "Choose an import source.",413+                    detail = "Peach will preview everything before it changes your vault.",414+                )415+            }416+        } else {417+            PeachEmptyOrEndState(418+                message = "Choose an import source.",419+                detail = "Peach will preview everything before it changes your vault.",420+            )421+        }422+    }394423 }395424 396425 @Composable

view file