# Micro-Animations Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add 19 micro-animations + 5 navigation transitions to make Swoosh feel alive with expressive, bouncy character. **Architecture:** Centralized `SwooshMotion` object provides shared spring/tween specs. Each screen gets targeted animation modifications. A reusable `AnimatedDialog` and `PulsingPlaceholder` component are shared across screens. Navigation transitions are configured per-route in `NavGraph.kt`. **Tech Stack:** Jetpack Compose Animation APIs (`animateFloatAsState`, `AnimatedVisibility`, `AnimatedContent`, `rememberInfiniteTransition`, `Animatable`), Spring physics (`spring()`), Navigation Compose transitions. **Spec:** `docs/superpowers/specs/2026-03-19-micro-animations-design.md` --- ## File Structure ### New Files | File | Responsibility | |------|---------------| | `ui/animation/SwooshMotion.kt` | Shared animation specs (Bouncy, BouncyQuick, Snappy, Gentle, Quick) + reduced motion check | | `ui/components/AnimatedDialog.kt` | Reusable scale-in dialog wrapper with backdrop fade | | `ui/components/PulsingPlaceholder.kt` | Pulsing alpha placeholder for loading states | ### Modified Files | File | Changes | |------|---------| | `ui/feed/FeedScreen.kt` | FAB animations, staggered cards, expand animation, empty state, queue chip, snackbar | | `ui/composer/ComposerScreen.kt` | Image preview, link preview, schedule chip, publish button, char counter, action buttons, error text | | `ui/detail/DetailScreen.kt` | Content reveal sequence, status badge bounce, animated delete dialog, metadata slide | | `ui/settings/SettingsScreen.kt` | "Settings saved" animation, disconnect confirmation dialog | | `ui/navigation/NavGraph.kt` | Per-route enter/exit/popEnter/popExit transitions | --- ## Task 1: SwooshMotion — Shared Animation Specs **Files:** - Create: `app/src/main/java/com/swoosh/microblog/ui/animation/SwooshMotion.kt` - [ ] **Step 1: Create SwooshMotion object** ```kotlin package com.swoosh.microblog.ui.animation import android.provider.Settings import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.Spring import androidx.compose.animation.core.snap import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.runtime.Composable import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.platform.LocalContext object SwooshMotion { // Expressive bounce — FAB entrance, chips, badges. One visible overshoot. fun bouncy(): FiniteAnimationSpec = spring( dampingRatio = 0.65f, stiffness = 400f ) // Fast snap-back — press feedback, button taps. Settles in ~150ms. fun bouncyQuick(): FiniteAnimationSpec = spring( dampingRatio = 0.7f, stiffness = 1000f ) // Controlled spring — expand/collapse, dialogs. fun snappy(): FiniteAnimationSpec = spring( dampingRatio = 0.7f, stiffness = 800f ) // Soft entrance — cards, content reveal. fun gentle(): FiniteAnimationSpec = spring( dampingRatio = 0.8f, stiffness = 300f ) // Quick tween — fade, color transitions. fun quick(): FiniteAnimationSpec = tween( durationMillis = 200, easing = FastOutSlowInEasing ) // Stagger delay per item in cascading animations. const val StaggerDelayMs = 50L // Content reveal stagger (Detail screen). const val RevealDelayMs = 80L } ``` - [ ] **Step 2: Verify it compiles** Run: `cd /Users/pawelorzech/Programowanie/Swoosh && ./gradlew compileDebugKotlin 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 3: Commit** ```bash git add app/src/main/java/com/swoosh/microblog/ui/animation/SwooshMotion.kt git commit -m "feat: add SwooshMotion shared animation specs" ``` --- ## Task 2: AnimatedDialog — Reusable Dialog Wrapper **Files:** - Create: `app/src/main/java/com/swoosh/microblog/ui/components/AnimatedDialog.kt` - [ ] **Step 1: Create AnimatedDialog composable** ```kotlin package com.swoosh.microblog.ui.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.swoosh.microblog.ui.animation.SwooshMotion @Composable fun AnimatedDialog( onDismissRequest: () -> Unit, content: @Composable () -> Unit ) { val transitionState = remember { MutableTransitionState(false).apply { targetState = true } } Dialog( onDismissRequest = onDismissRequest, properties = DialogProperties(usePlatformDefaultWidth = false) ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { // Backdrop AnimatedVisibility( visibleState = transitionState, enter = fadeIn(animationSpec = SwooshMotion.quick()), exit = fadeOut(animationSpec = SwooshMotion.quick()) ) { Box( modifier = Modifier .fillMaxSize() .background(Color.Black.copy(alpha = 0.4f)) .clickable( indication = null, interactionSource = remember { MutableInteractionSource() } ) { onDismissRequest() } ) } // Content AnimatedVisibility( visibleState = transitionState, enter = scaleIn( initialScale = 0.8f, animationSpec = SwooshMotion.snappy() ) + fadeIn(animationSpec = SwooshMotion.quick()), exit = scaleOut( targetScale = 0.8f, animationSpec = SwooshMotion.quick() ) + fadeOut(animationSpec = SwooshMotion.quick()) ) { content() } } } } ``` - [ ] **Step 2: Verify it compiles** Run: `cd /Users/pawelorzech/Programowanie/Swoosh && ./gradlew compileDebugKotlin 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 3: Commit** ```bash git add app/src/main/java/com/swoosh/microblog/ui/components/AnimatedDialog.kt git commit -m "feat: add AnimatedDialog reusable component" ``` --- ## Task 3: PulsingPlaceholder — Loading Placeholder **Files:** - Create: `app/src/main/java/com/swoosh/microblog/ui/components/PulsingPlaceholder.kt` - [ ] **Step 1: Create PulsingPlaceholder composable** ```kotlin package com.swoosh.microblog.ui.components import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @Composable fun PulsingPlaceholder( modifier: Modifier = Modifier, height: Dp = 80.dp ) { val infiniteTransition = rememberInfiniteTransition(label = "pulse") val alpha by infiniteTransition.animateFloat( initialValue = 0.12f, targetValue = 0.28f, animationSpec = infiniteRepeatable( animation = tween(800), repeatMode = RepeatMode.Reverse ), label = "pulseAlpha" ) Box( modifier = modifier .fillMaxWidth() .height(height) .clip(RoundedCornerShape(12.dp)) .background(MaterialTheme.colorScheme.onSurface.copy(alpha = alpha)) ) } ``` - [ ] **Step 2: Verify it compiles** Run: `cd /Users/pawelorzech/Programowanie/Swoosh && ./gradlew compileDebugKotlin 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 3: Commit** ```bash git add app/src/main/java/com/swoosh/microblog/ui/components/PulsingPlaceholder.kt git commit -m "feat: add PulsingPlaceholder loading component" ``` --- ## Task 4: Navigation Transitions **Files:** - Modify: `app/src/main/java/com/swoosh/microblog/ui/navigation/NavGraph.kt` - [ ] **Step 1: Add animation imports to NavGraph.kt** At the top of `NavGraph.kt`, add these imports (after existing imports around line 15): ```kotlin import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutVertically import androidx.compose.animation.core.tween ``` - [ ] **Step 2: Add transitions to Setup route** Modify the `composable(Routes.SETUP)` call (around line 37) to include transitions: ```kotlin composable( Routes.SETUP, enterTransition = { fadeIn(tween(500)) }, exitTransition = { fadeOut(tween(500)) } ) { ``` - [ ] **Step 3: Add transitions to Feed route** Modify the `composable(Routes.FEED)` call (around line 48) to include transitions: ```kotlin composable( Routes.FEED, enterTransition = { fadeIn(tween(300)) }, exitTransition = { fadeOut(tween(200)) }, popEnterTransition = { fadeIn(tween(300)) }, popExitTransition = { fadeOut(tween(200)) } ) { ``` - [ ] **Step 4: Add transitions to Composer route** Modify the `composable(Routes.COMPOSER)` call (around line 63) to include slide-up transitions: ```kotlin composable( Routes.COMPOSER, enterTransition = { slideInVertically(initialOffsetY = { it }) + fadeIn() }, exitTransition = { fadeOut(tween(200)) }, popEnterTransition = { fadeIn(tween(300)) }, popExitTransition = { slideOutVertically(targetOffsetY = { it }) + fadeOut() } ) { ``` - [ ] **Step 5: Add transitions to Detail route** Modify the `composable(Routes.DETAIL)` call (around line 75) to include slide-from-right transitions: ```kotlin composable( Routes.DETAIL, enterTransition = { slideInHorizontally(initialOffsetX = { it }) + fadeIn() }, exitTransition = { fadeOut(tween(200)) }, popEnterTransition = { fadeIn(tween(300)) }, popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) + fadeOut() } ) { ``` - [ ] **Step 6: Add transitions to Settings route** Modify the `composable(Routes.SETTINGS)` call (around line 93) to include slide-from-right transitions: ```kotlin composable( Routes.SETTINGS, enterTransition = { slideInHorizontally(initialOffsetX = { it }) }, exitTransition = { fadeOut(tween(200)) }, popEnterTransition = { fadeIn(tween(300)) }, popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) } ) { ``` - [ ] **Step 7: Verify it compiles** Run: `cd /Users/pawelorzech/Programowanie/Swoosh && ./gradlew compileDebugKotlin 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 8: Commit** ```bash git add app/src/main/java/com/swoosh/microblog/ui/navigation/NavGraph.kt git commit -m "feat: add navigation transitions between screens" ``` --- ## Task 5: Feed Screen — FAB Animations **Files:** - Modify: `app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt` (lines 77-81 for FAB) - [ ] **Step 1: Add animation imports to FeedScreen.kt** Add these imports at the top of the file: ```kotlin import androidx.compose.animation.* import androidx.compose.animation.core.* import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.ui.input.pointer.pointerInput import com.swoosh.microblog.ui.animation.SwooshMotion ``` - [ ] **Step 2: Add FAB entrance + press animation state** Before the `Scaffold` call (around line 52), add: ```kotlin // FAB entrance animation var fabVisible by remember { mutableStateOf(false) } val fabScale by animateFloatAsState( targetValue = if (fabVisible) 1f else 0f, animationSpec = SwooshMotion.bouncy(), label = "fabEntrance" ) LaunchedEffect(Unit) { fabVisible = true } // FAB press animation var fabPressed by remember { mutableStateOf(false) } val fabPressScale by animateFloatAsState( targetValue = if (fabPressed) 0.85f else 1f, animationSpec = SwooshMotion.bouncyQuick(), label = "fabPress" ) ``` - [ ] **Step 3: Replace FAB with animated version** Replace the existing FAB (lines 77-81) with: ```kotlin floatingActionButton = { FloatingActionButton( onClick = onCompose, modifier = Modifier .graphicsLayer { scaleX = fabScale * fabPressScale scaleY = fabScale * fabPressScale } .pointerInput(Unit) { detectTapGestures( onPress = { fabPressed = true tryAwaitRelease() fabPressed = false } ) } ) { Icon(Icons.Default.Add, contentDescription = "New Post") } }, ``` - [ ] **Step 4: Verify it compiles** Run: `cd /Users/pawelorzech/Programowanie/Swoosh && ./gradlew compileDebugKotlin 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 5: Commit** ```bash git add app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt git commit -m "feat: add bouncy FAB entrance and press animations" ``` --- ## Task 6: Feed Screen — Staggered Card Entrance **Files:** - Modify: `app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt` (lines 145-168 for LazyColumn) - [ ] **Step 1: Add stagger tracking state** Before the `Scaffold` call, alongside the FAB state, add: ```kotlin // Staggered entrance tracking val animatedKeys = remember { mutableStateSetOf() } var initialLoadComplete by remember { mutableStateOf(false) } ``` - [ ] **Step 2: Wrap each LazyColumn item with staggered AnimatedVisibility** Inside the `items()` block (around line 151), wrap the `PostCard` call. The item key is `post.ghostId ?: "local_${post.localId}"`. Wrap the card: ```kotlin items(state.posts, key = { it.ghostId ?: "local_${it.localId}" }) { post -> val itemKey = post.ghostId ?: "local_${post.localId}" val shouldAnimate = !initialLoadComplete && itemKey !in animatedKeys var visible by remember { mutableStateOf(!shouldAnimate) } LaunchedEffect(itemKey) { if (shouldAnimate) { val index = animatedKeys.size if (index < 8) { delay(SwooshMotion.StaggerDelayMs * index) } animatedKeys.add(itemKey) visible = true } } AnimatedVisibility( visible = visible, enter = slideInVertically( initialOffsetY = { it / 3 }, animationSpec = SwooshMotion.gentle() ) + fadeIn(animationSpec = SwooshMotion.quick()) ) { PostCard( // ... existing PostCard parameters unchanged ) } } ``` - [ ] **Step 3: Mark initial load complete after first batch** After the LazyColumn, add: ```kotlin LaunchedEffect(state.posts) { if (state.posts.isNotEmpty() && !initialLoadComplete) { delay(SwooshMotion.StaggerDelayMs * minOf(state.posts.size, 8) + 300) initialLoadComplete = true } } ``` - [ ] **Step 4: Verify it compiles** Run: `cd /Users/pawelorzech/Programowanie/Swoosh && ./gradlew compileDebugKotlin 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 5: Commit** ```bash git add app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt git commit -m "feat: add staggered card entrance animation in feed" ``` --- ## Task 7: Feed Screen — Show More, Empty State, Queue Chip, Snackbar **Files:** - Modify: `app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt` - [ ] **Step 1: Animate "Show more" expand (lines 225-260 in PostCard)** Replace the truncated text display with `AnimatedContent`: ```kotlin AnimatedContent( targetState = expanded, transitionSpec = { (fadeIn(SwooshMotion.quick()) + expandVertically(animationSpec = SwooshMotion.snappy())) .togetherWith(fadeOut(SwooshMotion.quick()) + shrinkVertically(animationSpec = SwooshMotion.snappy())) }, label = "expandText" ) { isExpanded -> Text( text = if (isExpanded) post.text else post.text.take(280) + "...", style = MaterialTheme.typography.bodyMedium ) } ``` - [ ] **Step 2: Animate empty states (lines 90-142)** Wrap both empty state blocks with `AnimatedVisibility`: ```kotlin AnimatedVisibility( visible = /* existing condition */, enter = fadeIn(SwooshMotion.quick()) + scaleIn( initialScale = 0.9f, animationSpec = SwooshMotion.quick() ), exit = fadeOut(SwooshMotion.quick()) ) { // existing empty state Column content } ``` - [ ] **Step 3: Animate queue status chip (lines 302-322)** Add pulsing animation to the queue chip when uploading: ```kotlin val isUploading = post.queueStatus == QueueStatus.UPLOADING val infiniteTransition = rememberInfiniteTransition(label = "queuePulse") val chipAlpha by if (isUploading) { infiniteTransition.animateFloat( initialValue = 0.6f, targetValue = 1f, animationSpec = infiniteRepeatable( animation = tween(600), repeatMode = RepeatMode.Reverse ), label = "uploadPulse" ) } else { remember { mutableFloatStateOf(1f) } } // Apply Modifier.graphicsLayer { alpha = chipAlpha } to the AssistChip ``` - [ ] **Step 4: Verify it compiles** Run: `cd /Users/pawelorzech/Programowanie/Swoosh && ./gradlew compileDebugKotlin 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 5: Commit** ```bash git add app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt git commit -m "feat: add expand, empty state, and queue chip animations" ``` --- ## Task 8: Composer Screen — All Animations **Files:** - Modify: `app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt` - [ ] **Step 1: Add animation imports** ```kotlin import androidx.compose.animation.* import androidx.compose.animation.core.* import com.swoosh.microblog.ui.animation.SwooshMotion import com.swoosh.microblog.ui.components.PulsingPlaceholder ``` - [ ] **Step 2: Animate image preview (lines 118-140)** Wrap the image preview section with `AnimatedVisibility`: ```kotlin AnimatedVisibility( visible = state.imageUri != null, enter = scaleIn( initialScale = 0f, animationSpec = SwooshMotion.bouncy() ) + fadeIn(SwooshMotion.quick()), exit = scaleOut(animationSpec = SwooshMotion.quick()) + fadeOut(SwooshMotion.quick()) ) { // existing Box with AsyncImage + close button } ``` - [ ] **Step 3: Animate link preview (lines 143-188)** Replace `LinearProgressIndicator` (lines 143-146) with `PulsingPlaceholder` when loading, and wrap the link preview card with `AnimatedVisibility`: ```kotlin // Loading state AnimatedVisibility( visible = state.isLoadingLink, enter = fadeIn(SwooshMotion.quick()), exit = fadeOut(SwooshMotion.quick()) ) { PulsingPlaceholder(height = 80.dp) } // Loaded link preview AnimatedVisibility( visible = state.linkPreview != null && !state.isLoadingLink, enter = slideInVertically( initialOffsetY = { it / 2 }, animationSpec = SwooshMotion.gentle() ) + fadeIn(SwooshMotion.quick()), exit = fadeOut(SwooshMotion.quick()) ) { // existing OutlinedCard with link preview } ``` - [ ] **Step 4: Animate schedule chip (lines 191-206)** Wrap schedule chip with `AnimatedVisibility`: ```kotlin AnimatedVisibility( visible = state.scheduledAt != null, enter = scaleIn(animationSpec = SwooshMotion.bouncy()) + fadeIn(SwooshMotion.quick()), exit = scaleOut(animationSpec = SwooshMotion.quick()) + fadeOut(SwooshMotion.quick()) ) { // existing AssistChip } ``` - [ ] **Step 5: Animate character counter color (lines 92-99)** Replace static color with animated color: ```kotlin val counterColor by animateColorAsState( targetValue = if (state.text.length > 280) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant, animationSpec = SwooshMotion.quick(), label = "counterColor" ) // Use counterColor in the Text composable ``` - [ ] **Step 6: Animate action buttons row (lines 234-256)** Add staggered entrance to action buttons: ```kotlin val buttonLabels = listOf("draft", "schedule", "publish") buttonLabels.forEachIndexed { index, _ -> var buttonVisible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { delay(SwooshMotion.StaggerDelayMs * index) buttonVisible = true } AnimatedVisibility( visible = buttonVisible, enter = scaleIn(animationSpec = SwooshMotion.gentle()) + fadeIn(SwooshMotion.quick()) ) { // Existing button for this index } } ``` - [ ] **Step 7: Animate error text (lines 208-215)** Wrap error text with `AnimatedVisibility`: ```kotlin AnimatedVisibility( visible = state.error != null, enter = slideInHorizontally( initialOffsetX = { -it / 4 }, animationSpec = SwooshMotion.snappy() ) + fadeIn(SwooshMotion.quick()), exit = fadeOut(SwooshMotion.quick()) ) { // existing error Text } ``` - [ ] **Step 8: Verify it compiles** Run: `cd /Users/pawelorzech/Programowanie/Swoosh && ./gradlew compileDebugKotlin 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 9: Commit** ```bash git add app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt git commit -m "feat: add all composer screen micro-animations" ``` --- ## Task 9: Detail Screen — Content Reveal & Delete Dialog **Files:** - Modify: `app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt` - [ ] **Step 1: Add animation imports** ```kotlin import androidx.compose.animation.* import androidx.compose.animation.core.* import com.swoosh.microblog.ui.animation.SwooshMotion import com.swoosh.microblog.ui.components.AnimatedDialog ``` - [ ] **Step 2: Add sequential reveal states** At the top of the `DetailScreen` composable (inside the function body, around line 40), add: ```kotlin // Sequential content reveal val revealSections = 4 // status, text, image, metadata val sectionVisible = remember { List(revealSections) { mutableStateOf(false) } } LaunchedEffect(Unit) { sectionVisible.forEachIndexed { index, state -> delay(SwooshMotion.RevealDelayMs * index) state.value = true } } ``` - [ ] **Step 3: Wrap each content section with AnimatedVisibility** Wrap sections in the Column (lines 59-145): Section 0 — Status + time row (lines 59-69): ```kotlin AnimatedVisibility( visible = sectionVisible[0].value, enter = fadeIn(SwooshMotion.quick()) + scaleIn( initialScale = 0.8f, animationSpec = SwooshMotion.bouncy() ) ) { Row(/* existing status + time */) { ... } } ``` Section 1 — Text content (lines 74-77): ```kotlin AnimatedVisibility( visible = sectionVisible[1].value, enter = fadeIn(SwooshMotion.quick()) + slideInVertically( initialOffsetY = { 20 }, animationSpec = SwooshMotion.gentle() ) ) { Text(/* existing */) } ``` Section 2 — Image (lines 80-90): ```kotlin AnimatedVisibility( visible = sectionVisible[2].value && post.imageUrl != null, enter = fadeIn(SwooshMotion.quick()) + slideInVertically( initialOffsetY = { 20 }, animationSpec = SwooshMotion.gentle() ) ) { AsyncImage(/* existing */) } ``` Section 3 — Metadata (lines 134-145): ```kotlin AnimatedVisibility( visible = sectionVisible[3].value, enter = slideInVertically( initialOffsetY = { it / 4 }, animationSpec = SwooshMotion.gentle() ) + fadeIn(SwooshMotion.quick()) ) { Column(/* existing metadata */) { ... } } ``` - [ ] **Step 4: Replace delete AlertDialog with AnimatedDialog** Replace the `AlertDialog` (lines 148-168) with: ```kotlin if (showDeleteDialog) { AnimatedDialog(onDismissRequest = { showDeleteDialog = false }) { // Same AlertDialog content but wrapped in a Card/Surface for the animated wrapper Card( modifier = Modifier.padding(horizontal = 24.dp) ) { Column(modifier = Modifier.padding(24.dp)) { Text("Delete Post", style = MaterialTheme.typography.headlineSmall) Spacer(modifier = Modifier.height(16.dp)) Text("Are you sure you want to delete this post? This action cannot be undone.") Spacer(modifier = Modifier.height(24.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End ) { TextButton(onClick = { showDeleteDialog = false }) { Text("Cancel") } Spacer(modifier = Modifier.width(8.dp)) Button( onClick = { showDeleteDialog = false onDelete(post) }, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error ) ) { Text("Delete") } } } } } } ``` - [ ] **Step 5: Verify it compiles** Run: `cd /Users/pawelorzech/Programowanie/Swoosh && ./gradlew compileDebugKotlin 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 6: Commit** ```bash git add app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt git commit -m "feat: add content reveal and animated delete dialog" ``` --- ## Task 10: Settings Screen — Saved Feedback & Disconnect Dialog **Files:** - Modify: `app/src/main/java/com/swoosh/microblog/ui/settings/SettingsScreen.kt` - [ ] **Step 1: Add animation imports** ```kotlin import androidx.compose.animation.* import androidx.compose.animation.core.* import com.swoosh.microblog.ui.animation.SwooshMotion import com.swoosh.microblog.ui.components.AnimatedDialog ``` - [ ] **Step 2: Animate "Settings saved" text (lines 84-91)** Replace the static conditional with `AnimatedVisibility`: ```kotlin AnimatedVisibility( visible = saved, enter = scaleIn( initialScale = 0f, animationSpec = SwooshMotion.bouncy() ) + fadeIn(SwooshMotion.quick()), exit = fadeOut(SwooshMotion.quick()) ) { Text( "Settings saved", color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(top = 8.dp) ) } ``` Add auto-hide after 2 seconds: ```kotlin LaunchedEffect(saved) { if (saved) { delay(2000) saved = false } } ``` - [ ] **Step 3: Add disconnect confirmation dialog (replacing direct disconnect, lines 97-109)** Add state for the dialog: ```kotlin var showDisconnectDialog by remember { mutableStateOf(false) } ``` Change the disconnect button to show dialog instead of directly disconnecting: ```kotlin OutlinedButton( onClick = { showDisconnectDialog = true }, colors = ButtonDefaults.outlinedButtonColors( contentColor = MaterialTheme.colorScheme.error ), modifier = Modifier.fillMaxWidth() ) { Text("Disconnect & Reset") } if (showDisconnectDialog) { AnimatedDialog(onDismissRequest = { showDisconnectDialog = false }) { Card(modifier = Modifier.padding(horizontal = 24.dp)) { Column(modifier = Modifier.padding(24.dp)) { Text("Disconnect?", style = MaterialTheme.typography.headlineSmall) Spacer(modifier = Modifier.height(16.dp)) Text("This will clear your Ghost credentials. You'll need to set up again.") Spacer(modifier = Modifier.height(24.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End ) { TextButton(onClick = { showDisconnectDialog = false }) { Text("Cancel") } Spacer(modifier = Modifier.width(8.dp)) Button( onClick = { showDisconnectDialog = false credentials.clear() ApiClient.resetClient() onLogout() }, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error ) ) { Text("Disconnect") } } } } } } ``` - [ ] **Step 4: Verify it compiles** Run: `cd /Users/pawelorzech/Programowanie/Swoosh && ./gradlew compileDebugKotlin 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 5: Commit** ```bash git add app/src/main/java/com/swoosh/microblog/ui/settings/SettingsScreen.kt git commit -m "feat: add settings saved animation and disconnect dialog" ``` --- ## Task 11: Run All Tests & Final Verification **Files:** None (verification only) - [ ] **Step 1: Run all unit tests** Run: `cd /Users/pawelorzech/Programowanie/Swoosh && ./gradlew test 2>&1 | tail -20` Expected: All tests pass. Animations don't affect business logic. - [ ] **Step 2: Build debug APK** Run: `cd /Users/pawelorzech/Programowanie/Swoosh && ./gradlew assembleDebug 2>&1 | tail -10` Expected: BUILD SUCCESSFUL - [ ] **Step 3: Verify no unused imports or lint issues** Run: `cd /Users/pawelorzech/Programowanie/Swoosh && ./gradlew lintDebug 2>&1 | tail -20` Expected: No new errors introduced - [ ] **Step 4: Final commit if any cleanup needed** ```bash git add -A git commit -m "chore: clean up lint and unused imports after animation additions" ```