feat: bold expressive theme with custom green palette, elevated cards, and high-contrast swipe actions

This commit is contained in:
Paweł Orzech 2026-03-19 14:02:48 +01:00
parent 85fa89d985
commit cfaba04039
No known key found for this signature in database
10 changed files with 1231 additions and 599 deletions

View file

@ -0,0 +1 @@
{"reason":"idle timeout","timestamp":1773918437073}

View file

@ -0,0 +1,4 @@
{"type":"server-started","port":54989,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:54989","screen_dir":"/Users/pawelorzech/Programowanie/Swoosh/.superpowers/brainstorm/46859-1773916456"}
{"type":"screen-added","file":"/Users/pawelorzech/Programowanie/Swoosh/.superpowers/brainstorm/46859-1773916456/animation-map-v3.html"}
{"type":"screen-added","file":"/Users/pawelorzech/Programowanie/Swoosh/.superpowers/brainstorm/46859-1773916456/waiting-2.html"}
{"type":"server-stopped","reason":"idle timeout"}

View file

@ -0,0 +1 @@
46868

View file

@ -0,0 +1,249 @@
<h2>Zaktualizowana mapa mikro-animacji Swoosh</h2>
<p class="subtitle">Pełna mapa uwzględniająca nowe funkcje: multi-account, search, swipe-to-dismiss, galeria, stats, preview. Ekspresyjny styl.</p>
<style>
.screen-section { margin: 24px 0; }
.screen-title { font-size: 1.15em; font-weight: 700; margin-bottom: 12px; padding-bottom: 6px; border-bottom: 2px solid rgba(108,99,255,0.3); display: flex; justify-content: space-between; align-items: center; }
.badge { background: var(--accent, #6c63ff); color: white; font-size: 0.7em; padding: 2px 8px; border-radius: 10px; }
.badge-new { background: #00c853; }
.anim-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.anim-item { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 10px 12px; }
.anim-item.new { border-color: rgba(0,200,83,0.3); background: rgba(0,200,83,0.04); }
.anim-name { font-weight: 700; font-size: 0.9em; margin-bottom: 3px; }
.anim-desc { font-size: 0.8em; opacity: 0.75; line-height: 1.35; }
.t { display: inline-block; font-size: 0.6em; padding: 1px 5px; border-radius: 3px; font-weight: 700; margin-left: 4px; vertical-align: middle; }
.t-sp { background: #7c4dff; color: #fff; }
.t-fa { background: #00c853; color: #000; }
.t-sl { background: #ff9100; color: #000; }
.t-sc { background: #448aff; color: #fff; }
.t-bo { background: #ff4081; color: #fff; }
.new-tag { font-size: 0.55em; background: #00c853; color: #000; padding: 1px 4px; border-radius: 3px; font-weight: 700; margin-left: 4px; vertical-align: middle; }
.spec-box { margin-top: 24px; padding: 16px; background: rgba(108,99,255,0.08); border: 1px solid rgba(108,99,255,0.25); border-radius: 10px; }
.spec-box h3 { margin: 0 0 10px 0; font-size: 1em; }
.spec-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; }
.spec-item { background: rgba(255,255,255,0.04); border-radius: 6px; padding: 8px 10px; }
.spec-name { font-weight: 700; font-size: 0.85em; }
.spec-val { font-size: 0.75em; opacity: 0.7; margin-top: 2px; }
.nav-table { width: 100%; margin-top: 10px; font-size: 0.8em; }
.nav-table td { padding: 6px 8px; border-bottom: 1px solid rgba(255,255,255,0.06); }
.nav-table td:first-child { font-weight: 600; white-space: nowrap; width: 35%; }
.summary { margin-top: 24px; padding: 14px; background: rgba(255,255,255,0.04); border-radius: 8px; font-size: 0.85em; }
.summary strong { color: var(--accent, #6c63ff); }
</style>
<!-- ===== FEED SCREEN ===== -->
<div class="screen-section">
<div class="screen-title">📋 Feed Screen <span class="badge">12</span></div>
<div class="anim-grid">
<div class="anim-item">
<div class="anim-name">FAB wejście <span class="t t-sp">SPRING</span></div>
<div class="anim-desc">Scale 0→1 z overshoot przy otwarciu feedu. Znika gdy search aktywny.</div>
</div>
<div class="anim-item">
<div class="anim-name">FAB press <span class="t t-sp">SPRING</span></div>
<div class="anim-desc">Kurczy się do 85% na tap, sprężyście wraca. BouncyQuick — kończy przed nawigacją.</div>
</div>
<div class="anim-item">
<div class="anim-name">Karty — staggered wejście <span class="t t-sl">SLIDE</span></div>
<div class="anim-desc">Kaskadowe slide-in od dołu, 50ms delay. Tylko initial load, max 8 kart. Tracking via mutableStateMapOf.</div>
</div>
<div class="anim-item">
<div class="anim-name">"Show more" expand <span class="t t-sp">SPRING</span></div>
<div class="anim-desc">AnimatedContent z expandVertically/shrinkVertically. Sprężysta zmiana wysokości.</div>
</div>
<div class="anim-item">
<div class="anim-name">Empty state <span class="t t-fa">FADE</span></div>
<div class="anim-desc">Fade-in + scale 0.9→1.0. Dotyczy wszystkich empty state'ów (connection error, filter empty, search no results).</div>
</div>
<div class="anim-item">
<div class="anim-name">Queue status chip <span class="t t-bo">BOUNCE</span></div>
<div class="anim-desc">Puls podczas uploadu. Bounce + color crossfade na zmianę statusu. Shake na FAILED.</div>
</div>
<div class="anim-item">
<div class="anim-name">Snackbar <span class="t t-sl">SLIDE</span></div>
<div class="anim-desc">Slide-in od dołu z overshoot (error snackbar + pin/unpin confirmation).</div>
</div>
<div class="anim-item new">
<div class="anim-name">Search bar <span class="t t-sl">SLIDE</span> <span class="new-tag">NEW</span></div>
<div class="anim-desc">SearchTopBar wjeżdża od góry z fade. Zamknięcie — reverse. Focus z delay.</div>
</div>
<div class="anim-item new">
<div class="anim-name">Filter chips <span class="t t-fa">FADE</span> <span class="new-tag">NEW</span></div>
<div class="anim-desc">Chips bar fade-in/out przy przełączeniu search. Już ma animateColorAsState — rozszerzyć o wejście.</div>
</div>
<div class="anim-item new">
<div class="anim-name">Account switcher <span class="t t-sl">SLIDE</span> <span class="new-tag">NEW</span></div>
<div class="anim-desc">ModalBottomSheet ma domyślną animację — dodać staggered wejście account items.</div>
</div>
<div class="anim-item new">
<div class="anim-name">Pinned section header <span class="t t-fa">FADE</span> <span class="new-tag">NEW</span></div>
<div class="anim-desc">Header "📌 Pinned" — fade-in z slide gdy pojawiają się pinned posty.</div>
</div>
<div class="anim-item new">
<div class="anim-name">Account switch overlay <span class="t t-fa">FADE</span> <span class="new-tag">NEW</span></div>
<div class="anim-desc">Crossfade "Switching account..." — płynne przejście z fade + scale spinner.</div>
</div>
</div>
</div>
<!-- ===== COMPOSER SCREEN ===== -->
<div class="screen-section">
<div class="screen-title">✏️ Composer Screen <span class="badge">9</span></div>
<div class="anim-grid">
<div class="anim-item">
<div class="anim-name">Image grid preview <span class="t t-sc">SCALE</span></div>
<div class="anim-desc">Nowe zdjęcia scale-in z bounce. Usunięcie — scale-out z fade. Cały ImageGridPreview.</div>
</div>
<div class="anim-item">
<div class="anim-name">Link preview card <span class="t t-sl">SLIDE</span></div>
<div class="anim-desc">Slide-up + fade po załadowaniu. Pulsing placeholder zamiast LinearProgressIndicator.</div>
</div>
<div class="anim-item">
<div class="anim-name">Schedule chip <span class="t t-sp">SPRING</span></div>
<div class="anim-desc">Pop-in z bouncy spring po wybraniu daty.</div>
</div>
<div class="anim-item">
<div class="anim-name">Publish button <span class="t t-bo">BOUNCE</span></div>
<div class="anim-desc">Bounce na aktywację. Loading pulse. Checkmark scale-in na success.</div>
</div>
<div class="anim-item">
<div class="anim-name">Character counter color <span class="t t-fa">FADE</span></div>
<div class="anim-desc">Trzy-stopniowy color crossfade: neutral → tertiary (280) → error (500).</div>
</div>
<div class="anim-item">
<div class="anim-name">Action buttons <span class="t t-sc">SCALE</span></div>
<div class="anim-desc">Kaskadowy scale-in: Publish → [Draft, Schedule]. Dopasowany do Column+Row layout.</div>
</div>
<div class="anim-item">
<div class="anim-name">Error text <span class="t t-sl">SLIDE</span></div>
<div class="anim-desc">Slide-in z lewej + fade.</div>
</div>
<div class="anim-item new">
<div class="anim-name">Hashtag chips <span class="t t-sp">SPRING</span> <span class="new-tag">NEW</span></div>
<div class="anim-desc">Extracted tags FlowRow — staggered scale-in chipów gdy tagi się pojawiają.</div>
</div>
<div class="anim-item new">
<div class="anim-name">Edit/Preview tabs <span class="t t-fa">FADE</span> <span class="new-tag">NEW</span></div>
<div class="anim-desc">Crossfade między edit mode a preview mode. Płynne przejście contentu.</div>
</div>
</div>
</div>
<!-- ===== DETAIL SCREEN ===== -->
<div class="screen-section">
<div class="screen-title">📖 Detail Screen <span class="badge">5</span></div>
<div class="anim-grid">
<div class="anim-item">
<div class="anim-name">Content reveal <span class="t t-fa">FADE</span></div>
<div class="anim-desc">Sekwencyjne: status → tekst → tagi → galeria → link → stats. 80ms delay each.</div>
</div>
<div class="anim-item">
<div class="anim-name">Status badge <span class="t t-sp">SPRING</span></div>
<div class="anim-desc">Scale-in z bounce — pierwszy element w sekwencji.</div>
</div>
<div class="anim-item">
<div class="anim-name">Delete dialog <span class="t t-sc">SCALE</span></div>
<div class="anim-desc">AnimatedDialog — scale z centrum + backdrop fade.</div>
</div>
<div class="anim-item">
<div class="anim-name">PostStatsSection <span class="t t-sl">SLIDE</span></div>
<div class="anim-desc">Slide-up jako ostatni w sekwencji. Expand/collapse już animowane.</div>
</div>
<div class="anim-item new">
<div class="anim-name">Pin toggle feedback <span class="t t-bo">BOUNCE</span> <span class="new-tag">NEW</span></div>
<div class="anim-desc">Ikona pina bouncy rotate przy toggle. Zmiana filled↔outlined z crossfade.</div>
</div>
</div>
</div>
<!-- ===== SETTINGS SCREEN ===== -->
<div class="screen-section">
<div class="screen-title">⚙️ Settings Screen <span class="badge">3</span></div>
<div class="anim-grid">
<div class="anim-item new">
<div class="anim-name">Account card <span class="t t-fa">FADE</span> <span class="new-tag">NEW</span></div>
<div class="anim-desc">Card z current account — fade-in + subtle scale przy wejściu na ekran.</div>
</div>
<div class="anim-item">
<div class="anim-name">Disconnect confirm <span class="t t-sc">SCALE</span></div>
<div class="anim-desc">AnimatedDialog z scale-in. Nowe zachowanie: dialog przed disconnect.</div>
</div>
<div class="anim-item new">
<div class="anim-name">Theme chip selection <span class="t t-sp">SPRING</span> <span class="new-tag">NEW</span></div>
<div class="anim-desc">Bouncy scale pulse na wybranym theme chipie (System/Light/Dark).</div>
</div>
</div>
</div>
<!-- ===== STATS SCREEN ===== -->
<div class="screen-section">
<div class="screen-title">📊 Stats Screen <span class="badge badge-new">3 NEW</span></div>
<div class="anim-grid">
<div class="anim-item new">
<div class="anim-name">Stats cards wejście <span class="t t-sc">SCALE</span> <span class="new-tag">NEW</span></div>
<div class="anim-desc">4 StatsCards — staggered scale-in z bounce. Efekt "odliczania" statystyk.</div>
</div>
<div class="anim-item new">
<div class="anim-name">Writing stats reveal <span class="t t-sl">SLIDE</span> <span class="new-tag">NEW</span></div>
<div class="anim-desc">OutlinedCard slide-up z fade po cards. Kaskadowe pojawienie WritingStatRows.</div>
</div>
<div class="anim-item new">
<div class="anim-name">Number count-up <span class="t t-fa">FADE</span> <span class="new-tag">NEW</span></div>
<div class="anim-desc">Wartości liczbowe animowane od 0 do docelowej (animateIntAsState). Subtelne ale efektowne.</div>
</div>
</div>
</div>
<!-- ===== SHARED SPECS ===== -->
<div class="spec-box">
<h3>🎛️ SwooshMotion — wspólne parametry</h3>
<div class="spec-grid">
<div class="spec-item">
<div class="spec-name">Bouncy</div>
<div class="spec-val">damping=0.65, stiff=400<br>→ FAB, chipy, badges</div>
</div>
<div class="spec-item">
<div class="spec-name">BouncyQuick</div>
<div class="spec-val">damping=0.7, stiff=1000<br>→ press feedback</div>
</div>
<div class="spec-item">
<div class="spec-name">Snappy</div>
<div class="spec-val">damping=0.7, stiff=800<br>→ expand, dialogi</div>
</div>
<div class="spec-item">
<div class="spec-name">Gentle</div>
<div class="spec-val">damping=0.8, stiff=300<br>→ karty, reveal</div>
</div>
<div class="spec-item">
<div class="spec-name">Quick</div>
<div class="spec-val">200ms FastOutSlowIn<br>→ fade, color</div>
</div>
<div class="spec-item">
<div class="spec-name">ReducedMotion</div>
<div class="spec-val">Checks ANIMATOR_DURATION_SCALE<br>→ snap() fallback</div>
</div>
</div>
</div>
<!-- ===== NAVIGATION ===== -->
<div class="spec-box" style="margin-top: 12px;">
<h3>🔀 Przejścia nawigacyjne — 8 tras</h3>
<table class="nav-table">
<tr><td>Feed → Composer</td><td>Slide up + fade</td></tr>
<tr><td>Feed → Detail</td><td>Slide right + fade</td></tr>
<tr><td>Feed → Settings</td><td>Slide right</td></tr>
<tr><td>Settings → Stats</td><td>Slide right</td></tr>
<tr><td>Detail/Composer → Preview</td><td>Slide up + fade</td></tr>
<tr><td>Feed → AddAccount</td><td>Slide up + fade (jak Composer)</td></tr>
<tr><td>Setup → Feed</td><td>Crossfade 500ms</td></tr>
<tr><td>Back (wszystkie)</td><td>Reverse via popEnter/popExit</td></tr>
</table>
</div>
<!-- ===== SUMMARY ===== -->
<div class="summary">
<strong>Podsumowanie:</strong> 32 animacje + 8 przejść nawigacyjnych.<br>
<strong>Nowe pliki:</strong> SwooshMotion.kt, AnimatedDialog.kt, PulsingPlaceholder.kt (3 pliki)<br>
<strong>Modyfikowane:</strong> FeedScreen, ComposerScreen, DetailScreen, SettingsScreen, StatsScreen, NavGraph (6 plików)<br>
<strong>12 nowych animacji</strong> względem pierwotnego planu (zielone NEW), wynikające z nowych funkcji aplikacji.
</div>

View file

@ -0,0 +1,3 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Writing spec & plan in terminal...</p>
</div>

View file

@ -869,8 +869,8 @@ fun SwipeBackground(dismissState: SwipeToDismissBoxState) {
val color by animateColorAsState( val color by animateColorAsState(
when (direction) { when (direction) {
SwipeToDismissBoxValue.StartToEnd -> MaterialTheme.colorScheme.primary SwipeToDismissBoxValue.StartToEnd -> Color(0xFF1565C0) // Bold blue
SwipeToDismissBoxValue.EndToStart -> MaterialTheme.colorScheme.error SwipeToDismissBoxValue.EndToStart -> Color(0xFFC62828) // Bold red
SwipeToDismissBoxValue.Settled -> Color.Transparent SwipeToDismissBoxValue.Settled -> Color.Transparent
}, },
label = "swipe_bg_color" label = "swipe_bg_color"
@ -1262,7 +1262,7 @@ fun PostCardContent(
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp) .padding(horizontal = 16.dp, vertical = 6.dp)
.combinedClickable( .combinedClickable(
onClick = onClick, onClick = onClick,
onClickLabel = "View post details", onClickLabel = "View post details",
@ -1270,9 +1270,10 @@ fun PostCardContent(
showContextMenu = true showContextMenu = true
} }
), ),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = if (post.featured) containerColor = if (post.featured)
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.15f) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
else else
MaterialTheme.colorScheme.surface MaterialTheme.colorScheme.surface
) )
@ -1978,21 +1979,21 @@ fun buildHighlightedString(
@Composable @Composable
fun StatusBadge(post: FeedPost) { fun StatusBadge(post: FeedPost) {
val (label, color) = when { val (label, containerColor, labelColor) = when {
post.queueStatus != QueueStatus.NONE -> "Pending" to MaterialTheme.colorScheme.tertiary post.queueStatus != QueueStatus.NONE -> Triple("Pending", Color(0xFFFFF3E0), Color(0xFFE65100))
post.status == "published" -> "Published" to MaterialTheme.colorScheme.primary post.status == "published" -> Triple("Published", Color(0xFFE8F5E9), Color(0xFF2E7D32))
post.status == "scheduled" -> "Scheduled" to MaterialTheme.colorScheme.secondary post.status == "scheduled" -> Triple("Scheduled", Color(0xFFE3F2FD), Color(0xFF1565C0))
else -> "Draft" to MaterialTheme.colorScheme.outline else -> Triple("Draft", Color(0xFFF3E5F5), Color(0xFF7B1FA2))
} }
SuggestionChip( SuggestionChip(
onClick = {}, onClick = {},
label = { label = {
Text(label, style = MaterialTheme.typography.labelSmall) Text(label, style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.Bold))
}, },
colors = SuggestionChipDefaults.suggestionChipColors( colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = color.copy(alpha = 0.12f), containerColor = containerColor,
labelColor = color labelColor = labelColor
), ),
border = null border = null
) )

View file

@ -0,0 +1,59 @@
package com.swoosh.microblog.ui.theme
import androidx.compose.ui.graphics.Color
// Bold, expressive palette inspired by the Swoosh icon (dark green + mint)
val SwooshGreen = Color(0xFF1B4332)
val SwooshMint = Color(0xFF52B788)
val SwooshMintLight = Color(0xFF95D5B2)
val SwooshMintDark = Color(0xFF2D6A4F)
// Light theme - bold, high-contrast
val LightPrimary = Color(0xFF1B6B45)
val LightOnPrimary = Color(0xFFFFFFFF)
val LightPrimaryContainer = Color(0xFFD0F0DC)
val LightOnPrimaryContainer = Color(0xFF002111)
val LightSecondary = Color(0xFF4A635A)
val LightOnSecondary = Color(0xFFFFFFFF)
val LightSecondaryContainer = Color(0xFFCCE8DC)
val LightOnSecondaryContainer = Color(0xFF072018)
val LightTertiary = Color(0xFF3D6374)
val LightOnTertiary = Color(0xFFFFFFFF)
val LightTertiaryContainer = Color(0xFFC1E8FC)
val LightOnTertiaryContainer = Color(0xFF001F2A)
val LightError = Color(0xFFBA1A1A)
val LightOnError = Color(0xFFFFFFFF)
val LightErrorContainer = Color(0xFFFFDAD6)
val LightOnErrorContainer = Color(0xFF410002)
val LightBackground = Color(0xFFF5FBF5)
val LightOnBackground = Color(0xFF171D19)
val LightSurface = Color(0xFFF5FBF5)
val LightOnSurface = Color(0xFF171D19)
val LightSurfaceVariant = Color(0xFFDBE5DD)
val LightOnSurfaceVariant = Color(0xFF404943)
val LightOutline = Color(0xFF707973)
// Dark theme - rich, bold
val DarkPrimary = Color(0xFF52B788)
val DarkOnPrimary = Color(0xFF003822)
val DarkPrimaryContainer = Color(0xFF005233)
val DarkOnPrimaryContainer = Color(0xFFD0F0DC)
val DarkSecondary = Color(0xFFB1CCC0)
val DarkOnSecondary = Color(0xFF1C352C)
val DarkSecondaryContainer = Color(0xFF334B42)
val DarkOnSecondaryContainer = Color(0xFFCCE8DC)
val DarkTertiary = Color(0xFFA5CCE0)
val DarkOnTertiary = Color(0xFF073544)
val DarkTertiaryContainer = Color(0xFF244B5C)
val DarkOnTertiaryContainer = Color(0xFFC1E8FC)
val DarkError = Color(0xFFFFB4AB)
val DarkOnError = Color(0xFF690005)
val DarkErrorContainer = Color(0xFF93000A)
val DarkOnErrorContainer = Color(0xFFFFDAD6)
val DarkBackground = Color(0xFF0F1512)
val DarkOnBackground = Color(0xFFDFE4DE)
val DarkSurface = Color(0xFF0F1512)
val DarkOnSurface = Color(0xFFDFE4DE)
val DarkSurfaceVariant = Color(0xFF404943)
val DarkOnSurfaceVariant = Color(0xFFBFC9C1)
val DarkOutline = Color(0xFF8A938C)

View file

@ -1,15 +1,133 @@
package com.swoosh.microblog.ui.theme package com.swoosh.microblog.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
private val SwooshLightColorScheme = lightColorScheme(
primary = LightPrimary,
onPrimary = LightOnPrimary,
primaryContainer = LightPrimaryContainer,
onPrimaryContainer = LightOnPrimaryContainer,
secondary = LightSecondary,
onSecondary = LightOnSecondary,
secondaryContainer = LightSecondaryContainer,
onSecondaryContainer = LightOnSecondaryContainer,
tertiary = LightTertiary,
onTertiary = LightOnTertiary,
tertiaryContainer = LightTertiaryContainer,
onTertiaryContainer = LightOnTertiaryContainer,
error = LightError,
onError = LightOnError,
errorContainer = LightErrorContainer,
onErrorContainer = LightOnErrorContainer,
background = LightBackground,
onBackground = LightOnBackground,
surface = LightSurface,
onSurface = LightOnSurface,
surfaceVariant = LightSurfaceVariant,
onSurfaceVariant = LightOnSurfaceVariant,
outline = LightOutline
)
private val SwooshDarkColorScheme = darkColorScheme(
primary = DarkPrimary,
onPrimary = DarkOnPrimary,
primaryContainer = DarkPrimaryContainer,
onPrimaryContainer = DarkOnPrimaryContainer,
secondary = DarkSecondary,
onSecondary = DarkOnSecondary,
secondaryContainer = DarkSecondaryContainer,
onSecondaryContainer = DarkOnSecondaryContainer,
tertiary = DarkTertiary,
onTertiary = DarkOnTertiary,
tertiaryContainer = DarkTertiaryContainer,
onTertiaryContainer = DarkOnTertiaryContainer,
error = DarkError,
onError = DarkOnError,
errorContainer = DarkErrorContainer,
onErrorContainer = DarkOnErrorContainer,
background = DarkBackground,
onBackground = DarkOnBackground,
surface = DarkSurface,
onSurface = DarkOnSurface,
surfaceVariant = DarkSurfaceVariant,
onSurfaceVariant = DarkOnSurfaceVariant,
outline = DarkOutline
)
private val SwooshTypography = Typography(
headlineLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = (-0.5).sp
),
headlineMedium = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 28.sp,
lineHeight = 36.sp
),
headlineSmall = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 32.sp
),
titleLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
lineHeight = 28.sp
),
titleMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
titleSmall = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
bodyLarge = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
bodyMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
labelLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
labelMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
),
labelSmall = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
)
@Composable @Composable
fun SwooshTheme( fun SwooshTheme(
themeMode: ThemeMode = ThemeMode.SYSTEM, themeMode: ThemeMode = ThemeMode.SYSTEM,
dynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val darkTheme = when (themeMode) { val darkTheme = when (themeMode) {
@ -18,19 +136,11 @@ fun SwooshTheme(
ThemeMode.DARK -> true ThemeMode.DARK -> true
} }
val colorScheme = when { val colorScheme = if (darkTheme) SwooshDarkColorScheme else SwooshLightColorScheme
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
darkTheme -> darkColorScheme()
else -> lightColorScheme()
}
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography(), typography = SwooshTypography,
content = content content = content
) )
} }

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
# Micro-Animations Design — Swoosh # Micro-Animations Design — Swoosh (v2)
**Date:** 2026-03-19 **Date:** 2026-03-19
**Status:** Approved **Status:** Approved
@ -6,235 +6,219 @@
## Overview ## Overview
Add 19 micro-animations + 5 navigation transitions across all screens to make the app feel alive. Create a shared `SwooshMotion` object with predefined animation specs ensuring consistent character throughout the app. Add 32 micro-animations + 8 navigation transitions across all screens to make the app feel alive. Create a shared `SwooshMotion` object with predefined animation specs ensuring consistent character throughout the app.
The app now includes: multi-account support, search with filters, swipe-to-dismiss, multi-image galleries, writing statistics dashboard, HTML preview, and hashtag support.
## Shared Animation Specs — SwooshMotion ## Shared Animation Specs — SwooshMotion
Central object in a new file `ui/animation/SwooshMotion.kt` providing reusable animation specifications: Central object in `ui/animation/SwooshMotion.kt`:
| Name | Type | Parameters | Use case | | Name | Type | Parameters | Use case |
|------|------|-----------|----------| |------|------|-----------|----------|
| `Bouncy` | Spring | dampingRatio=0.65, stiffness=400 | FAB entrance, chips, badges | | `Bouncy` | Spring | dampingRatio=0.65, stiffness=400 | FAB entrance, chips, badges |
| `BouncyQuick` | Spring | dampingRatio=0.7, stiffness=1000 | Press feedback (settles in ~150ms) | | `BouncyQuick` | Spring | dampingRatio=0.7, stiffness=1000 | Press feedback (~150ms settle) |
| `Snappy` | Spring | dampingRatio=0.7, stiffness=800 | Expand/collapse, dialogs | | `Snappy` | Spring | dampingRatio=0.7, stiffness=800 | Expand/collapse, dialogs |
| `Gentle` | Spring | dampingRatio=0.8, stiffness=300 | Cards, content reveal | | `Gentle` | Spring | dampingRatio=0.8, stiffness=300 | Cards, content reveal |
| `Quick` | Tween | 200ms, FastOutSlowInEasing | Fade, color transitions | | `Quick` | Tween | 200ms, FastOutSlowInEasing | Fade, color transitions |
| `StaggerDelay` | Offset | 50ms per item | List item entrances | | `StaggerDelayMs` | Long | 50 | List item entrances |
| `RevealDelayMs` | Long | 80 | Content reveal sequences |
### Reduced Motion Support ### Reduced Motion Support
`SwooshMotion` exposes `val isReducedMotion: Boolean` that reads the system accessibility setting (`Settings.Global.ANIMATOR_DURATION_SCALE`). When `true`, all spring specs fall back to `snap()` (instant) and all tweens use 0ms duration. Every animation in this spec must route through `SwooshMotion` so the reduced-motion check is centralized. `SwooshMotion` checks `Settings.Global.ANIMATOR_DURATION_SCALE`. When reduced motion is enabled, all springs fall back to `snap()` and all tweens use 0ms duration.
### Stagger Pattern ### Stagger Pattern
For staggered entrance animations (#3, #13, #14), each item uses a local `MutableState<Boolean>` controlled by a `LaunchedEffect`: Each item uses a `MutableState<Boolean>` toggled via `LaunchedEffect(Unit) { delay(StaggerDelayMs * index); visible = true }`. `AnimatedVisibility` has no built-in delay.
```kotlin
val visible = remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
delay(SwooshMotion.StaggerDelay * index)
visible.value = true
}
AnimatedVisibility(visible = visible.value, ...) { ... }
```
`AnimatedVisibility` has no built-in delay parameter — this pattern is the canonical approach.
### Already-Animated Tracking (LazyColumn) ### Already-Animated Tracking (LazyColumn)
For staggered items inside `LazyColumn` (#3), items re-enter composition on scroll. To prevent replaying the entrance animation, hoist a `mutableStateSetOf<String>()` of already-animated item keys in `FeedScreen`: Hoist a `mutableStateMapOf<String, Boolean>()` of already-animated item keys in `FeedScreen`. Check `key !in animatedKeys` before triggering entrance. Only first ~8 visible items on initial load animate. Items from infinite scroll appear instantly.
```kotlin ---
val animatedKeys = remember { mutableStateSetOf<String>() }
// Inside each item:
val shouldAnimate = post.id !in animatedKeys
LaunchedEffect(Unit) {
if (shouldAnimate) {
delay(SwooshMotion.StaggerDelay * index)
animatedKeys.add(post.id)
}
visible.value = true // instant if already animated
}
```
Only animate the first batch visible on initial load (cap at ~8 items). Items appended via infinite scroll appear without stagger. ## Feed Screen — 12 animations
## Feed Screen — 7 animations ### F1. FAB entrance (SPRING/Bouncy)
Scale 0→1 with overshoot on screen open. Hidden during search mode.
- `animateFloatAsState` with `Bouncy` spring on scale modifier
- `LaunchedEffect(Unit)` sets target to 1f
- FAB already conditionally hidden when `isSearchActive` (line 249)
### 1. FAB entrance (SPRING/Bouncy) ### F2. FAB press (SPRING/BouncyQuick)
When the feed screen opens, the FAB scales from 0 to 1 with `Bouncy` spring. One visible overshoot — the FAB "pops" onto screen. Shrinks to 85%, snaps back. Settles before navigation fires.
- Compose API: `animateFloatAsState` with `Bouncy` spring on scale modifier - `Modifier.pointerInput` press → `animateFloatAsState` 0.85f→1f
- Trigger: `LaunchedEffect(Unit)` sets target to 1f - Navigation fires immediately; animation interrupted by transition (fine)
### 2. FAB press (SPRING/BouncyQuick) ### F3. Post cards staggered entrance (SLIDE/Gentle)
On tap, FAB shrinks to 85% and snaps back to 100%. Must settle in ~150ms so it completes before navigation fires. Cascading slide-in from bottom, 50ms delay per item. "Waterfall" effect.
- Compose API: `Modifier.pointerInput` detecting press → `animateFloatAsState` scale 0.85f → 1f - `AnimatedVisibility` per item in LazyColumn with `slideInVertically + fadeIn`
- Spring spec: `BouncyQuick` (dampingRatio=0.7, stiffness=1000) - `mutableStateMapOf<String, Boolean>` tracking (see shared section)
- Navigation fires immediately on tap — the spring animation is interrupted by the screen transition, which is fine - Applies to both `SwipeablePostCard` and search-mode `PostCard`
- Capped at first 8 items, infinite scroll items appear instantly
### 3. Post cards staggered entrance (SLIDE/Gentle) ### F4. "Show more" expand (SPRING/Snappy)
Cards slide in from bottom with cascading delay — 50ms per item. "Waterfall" effect. `AnimatedContent(targetState = expanded)` with `fadeIn + expandVertically` / `fadeOut + shrinkVertically`. Replaces discrete text swap in PostCardContent.
- Compose API: `AnimatedVisibility` per item with `slideInVertically` + `fadeIn` using `Gentle` spring
- Delay: Stagger pattern (see shared section). Capped at first ~8 visible items
- Already-animated tracking via `animatedKeys` set (see shared section)
- Items appended via infinite scroll appear without stagger (instant `visible = true`)
### 4. "Show more" expand (SPRING/Snappy) ### F5. Empty states (FADE/Quick + scale)
Card content transitions between truncated and full text with animated height change. All empty states (connection error, filter empty, search no results, normal empty) wrapped in `AnimatedVisibility` with `fadeIn + scaleIn(0.9f)`.
- Compose API: `AnimatedContent(targetState = expanded)` with `fadeIn + expandVertically` / `fadeOut + shrinkVertically` as content transform
- Height spring: `Snappy`
- This replaces the current discrete text swap with a smooth animated transition
### 5. Empty state (FADE/Quick + scale) ### F6. Queue status chip (BOUNCE/Bouncy)
"No posts yet" icon and text fade in with subtle scale from 0.9 to 1.0. - UPLOADING: `rememberInfiniteTransition` pulse alpha 0.6→1.0
- Compose API: `AnimatedVisibility` with `fadeIn + scaleIn(initialScale = 0.9f)` - Status change: bounce scale + `animateColorAsState`
- FAILED: shake via `Animatable` offset oscillation (-4dp→4dp→0dp, 3 cycles)
### 6. Queue status chip (BOUNCE/Bouncy) ### F7. Snackbar (SLIDE/Snappy)
During upload: chip pulses (infinite alpha animation 0.6→1.0). On status change (success/fail): bounce scale + color crossfade. On FAILED: shake animation (horizontal offset oscillation) to draw attention. Both error snackbar (line 578) and pin confirmation snackbar (line 562) — `AnimatedVisibility` with `slideInVertically` + overshoot.
- Compose API: `rememberInfiniteTransition` for pulse; `animateColorAsState` + scale bounce on status change
- Shake on FAILED: `Animatable` with `animateTo` offset -4dp → 4dp → 0dp (3 oscillations)
- Color transition: `Quick` tween
- Note: Each chip in UPLOADING state creates its own `rememberInfiniteTransition`. If many posts queue simultaneously, this is fine for ~5 items but could cause jank beyond that (unlikely in normal use)
### 7. Snackbar error (SLIDE/Snappy) ### F8. Search bar (SLIDE/Quick) — NEW
Slides in from bottom with slight overshoot. Fades out on timeout. `SearchTopBar` slides in from top with fade when search activates. Reverse on close.
- Compose API: `AnimatedVisibility` with `slideInVertically(initialOffsetY = { it })` + `Snappy` spring - `AnimatedVisibility` wrapping the `if (isSearchActive)` branch (line 157)
- Exit: `fadeOut` with `Quick` tween - `slideInVertically(initialOffsetY = { -it }) + fadeIn` / reverse
## Composer Screen — 7 animations ### F9. Filter chips bar (FADE/Quick) — NEW
`FilterChipsBar` fades in/out when toggling between search and normal mode.
- Already has `animateColorAsState` for chip selection (line 715)
- Add `AnimatedVisibility` wrapper for the `if (!isSearchActive)` block (line 263)
### 8. Image preview (SCALE/Bouncy) ### F10. Account switcher items (SLIDE/Gentle) — NEW
After picking an image, the preview scales from 0 with bouncy spring. Close button fades in with scale. Inside `AccountSwitcherBottomSheet` (line 954), account `ListItem`s get staggered slide-in.
- Compose API: `AnimatedVisibility` with `scaleIn` using `Bouncy` spring - ModalBottomSheet has built-in slide animation
- Close button: `fadeIn + scaleIn(initialScale = 0.5f)` — no rotation (an "X" icon should feel stable and immediately tappable) - Add stagger pattern to account items inside the sheet
### 9. Link preview card (SLIDE/Gentle) ### F11. Pinned section header (FADE/Quick) — NEW
After link loads, card slides up from below + fades in. While loading: pulsing placeholder card. "📌 Pinned" header at line 790 — `AnimatedVisibility` with `fadeIn + slideInVertically` when pinned posts exist.
- Compose API: `AnimatedVisibility` with `slideInVertically + fadeIn` using `Gentle` spring
- Loading state: Replace current `LinearProgressIndicator` with a pulsing placeholder card using `rememberInfiniteTransition` on alpha (0.3→0.7). Simpler than a full shimmer gradient — just a fading placeholder shape
### 10. Schedule chip (SPRING/Bouncy) ### F12. Account switch overlay (FADE/Quick) — NEW
After picking date, chip pops in with bouncy spring. "Switching account..." overlay (line 288) — crossfade entrance with scale on the spinner.
- Compose API: `AnimatedVisibility` with `scaleIn` using `Bouncy`
### 11. Publish button (BOUNCE/BouncyQuick) ---
Subtle bounce on activation. During publishing: loading pulse. On success: checkmark icon scales in.
- Compose API: Scale bounce on click via `animateFloatAsState` with `BouncyQuick`; `rememberInfiniteTransition` for pulse; `AnimatedContent` for icon swap with `scaleIn`
### 12. Character counter color (FADE/Quick) ## Composer Screen — 9 animations
Smooth color crossfade when exceeding 280 characters — neutral → red.
- Compose API: `animateColorAsState` with `Quick` tween
- Trigger: `text.length > 280`
### 13. Action buttons staggered entrance (SCALE/Gentle) ### C1. Image grid preview (SCALE/Bouncy)
Draft, Schedule, Publish buttons — cascading scale-in with 50ms delay each. `ImageGridPreview` (line 556) — each image thumbnail scales in with bounce when added. Scale-out on removal.
- Compose API: Stagger pattern (see shared section) with `AnimatedVisibility` + `scaleIn + fadeIn` - `AnimatedVisibility` per grid item with `scaleIn` using `Bouncy`
### NEW: 13b. Error text (SLIDE/Snappy) ### C2. Link preview card (SLIDE/Gentle)
Composer error text slides in from left + fades in, consistent with snackbar style. After link loads, card slides up + fades in. Loading: `PulsingPlaceholder` replaces `LinearProgressIndicator` (line 317).
- Compose API: `AnimatedVisibility` with `slideInHorizontally + fadeIn` using `Snappy` spring
## Detail Screen — 4 animations ### C3. Schedule chip (SPRING/Bouncy)
Chip at line 365 pops in with `scaleIn` using `Bouncy` when `state.scheduledAt != null`.
### 14. Content reveal (FADE/Gentle) ### C4. Publish button (BOUNCE/BouncyQuick)
Elements appear sequentially: status badge → text → image → metadata. 80ms delay each. Button at line 421 — bounce on click, loading pulse during submit, `AnimatedContent` icon swap to checkmark on success.
- Compose API: Stagger pattern (see shared section) with `AnimatedVisibility` per section using `fadeIn + slideInVertically(initialOffsetY = { 20 })`
- Delay: 80ms per element (4 elements total = 320ms cascade)
### 15. Status badge entrance (SPRING/Bouncy) ### C5. Character counter color (FADE/Quick)
Badge scales from 0 with bounce — first visible element, draws attention. Part of the content reveal sequence (index 0, no delay). Three-tier `animateColorAsState`: onSurfaceVariant → tertiary (>280) → error (>500). Matches current logic at line 211.
- Compose API: `animateFloatAsState` scale with `Bouncy` spring
### 16. Delete confirmation dialog (SCALE/Snappy) ### C6. Action buttons staggered (SCALE/Gentle)
Dialog scales from center (0.8→1.0) with spring + backdrop fades in. Publish button (line 421) + Row of [Draft, Schedule] (line 433). Two-step stagger: button first, then row.
- Compose API: Custom `AnimatedDialog` wrapper with `scaleIn(initialScale = 0.8f)` + `fadeIn` backdrop
- Spring spec: `Snappy`
### 17. Metadata section (SLIDE/Gentle) ### C7. Error text (SLIDE/Snappy)
Bottom metadata slides up — last in the content reveal sequence (index 3, 240ms delay). Error at line 407 — `AnimatedVisibility` with `slideInHorizontally + fadeIn`.
- Compose API: Part of stagger pattern in #14
### C8. Hashtag chips (SPRING/Bouncy) — NEW
Extracted tags FlowRow (line 225) — staggered `scaleIn` per chip as tags appear/change.
### C9. Edit/Preview crossfade (FADE/Quick) — NEW
`Crossfade(targetState = state.isPreviewMode)` wrapping the `if (state.isPreviewMode)` branch (line 167). Smooth transition between edit and preview modes.
---
## Detail Screen — 5 animations
### D1. Content reveal (FADE/Gentle)
Sequential: status row → text → tags → image gallery → link preview → PostStatsSection. 80ms delay each.
- Stagger pattern with `AnimatedVisibility` per section
- PostStatsSection (line 307) already has internal `AnimatedVisibility` for expand/collapse
### D2. Status badge entrance (SPRING/Bouncy)
First in reveal sequence (index 0). Scale-in with bounce.
### D3. Delete dialog (SCALE/Snappy)
Replace `AlertDialog` at line 312 with `AnimatedDialog` wrapper.
### D4. PostStatsSection (SLIDE/Gentle)
Last in reveal sequence. Slides up. Internal expand/collapse already animated.
### D5. Pin toggle feedback (BOUNCE/Bouncy) — NEW
Pin icon in TopAppBar (line 78 area) — bouncy scale pulse when toggling featured state. `animateFloatAsState` scale 1→1.2→1.
---
## Settings Screen — 3 animations ## Settings Screen — 3 animations
### 18. "Settings saved" feedback (SPRING/Bouncy) ### S1. Account card (FADE/Quick) — NEW
Existing "Settings saved" text (currently static conditional) gets animated: pops in with bounce (scale 0→1). After 2 seconds, fades out. Current account Card (line 78) — fade-in with subtle scale on screen entry.
- Compose API: `AnimatedVisibility` with `scaleIn` using `Bouncy` spring; `LaunchedEffect``delay(2000)` → hide
- Exit: `fadeOut` with `Quick` tween
- Text and color remain as-is: "Settings saved" with `MaterialTheme.colorScheme.primary`
### 19. Disconnect confirmation dialog (NEW behavior + FADE+SCALE/Snappy) ### S2. Disconnect confirmation dialog (SCALE/Snappy)
**New behavior:** Add a confirmation dialog before disconnect (currently the button directly clears credentials). The dialog uses the same `AnimatedDialog` wrapper as #16. "Disconnect" button has subtle red pulse. **New behavior:** Add confirmation dialog before both "Disconnect Current Account" (line 139) and "Disconnect All Accounts" (line 165). Uses `AnimatedDialog` wrapper.
- Compose API: Same `AnimatedDialog` wrapper as #16
- Red pulse: `rememberInfiniteTransition` on alpha of error color
### NEW: 19b. Disconnect dialog behavior ### S3. Theme chip selection (SPRING/Bouncy) — NEW
The `OutlinedButton` currently calls `credentials.clear()` + `onLogout()` directly. After this change, it shows a confirmation dialog first. Confirm action triggers the existing clear + logout flow. In `ThemeModeSelector` (line 184), selected chip gets a brief bouncy scale pulse on selection change.
## Navigation Transitions — 5 ---
Each `composable()` call in `NavGraph.kt` receives `enterTransition`, `exitTransition`, `popEnterTransition`, and `popExitTransition` lambdas. Back navigation uses `popEnterTransition`/`popExitTransition` (the reverse of the enter animation) — these are distinct parameters, not automatic. ## Stats Screen — 3 animations (all NEW)
### Feed → Composer ### ST1. Stats cards staggered entrance (SCALE/Bouncy)
Slide up from bottom + fade. Four `StatsCard` composables (lines 68-98) — staggered scale-in with bounce. 50ms delay per card.
- `enterTransition = slideInVertically(initialOffsetY = { it }) + fadeIn()`
- `exitTransition = fadeOut()`
- `popEnterTransition = fadeIn()`
- `popExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut()`
### Feed → Detail ### ST2. Writing stats reveal (SLIDE/Gentle)
Slide in from right + fade. `OutlinedCard` at line 109 — slide-up with fade after cards complete. Internal `WritingStatRow`s cascade.
- `enterTransition = slideInHorizontally(initialOffsetX = { it }) + fadeIn()`
- `exitTransition = fadeOut()`
- `popEnterTransition = fadeIn()`
- `popExitTransition = slideOutHorizontally(targetOffsetX = { it }) + fadeOut()`
### Detail → Composer (via Edit) ### ST3. Number count-up (FADE/Quick)
Same as Feed → Composer (slide up from bottom). The Composer always enters with the same transition regardless of source. Stat values animate from 0 to target via `animateIntAsState`. Subtle but adds life to the dashboard.
### Feed → Settings ---
Standard slide from right.
- `enterTransition = slideInHorizontally(initialOffsetX = { it })`
- `exitTransition = fadeOut()`
- `popEnterTransition = fadeIn()`
- `popExitTransition = slideOutHorizontally(targetOffsetX = { it })`
### Setup → Feed ## Navigation Transitions — 8 routes
Crossfade — smooth transition from animated setup background to feed.
- `enterTransition = fadeIn(tween(500))` Each `composable()` receives `enterTransition`, `exitTransition`, `popEnterTransition`, `popExitTransition`.
- `exitTransition = fadeOut(tween(500))`
| Route | Enter | Pop Exit |
|-------|-------|----------|
| Setup | fadeIn(500ms) | — (inclusive popUpTo) |
| Feed | fadeIn(300ms) | fadeOut(200ms) |
| Composer | slideInVertically + fadeIn | slideOutVertically + fadeOut |
| Detail | slideInHorizontally + fadeIn | slideOutHorizontally + fadeOut |
| Settings | slideInHorizontally | slideOutHorizontally |
| Stats | slideInHorizontally | slideOutHorizontally |
| Preview | slideInVertically + fadeIn | slideOutVertically + fadeOut |
| AddAccount | slideInVertically + fadeIn | slideOutVertically + fadeOut |
---
## File Structure ## File Structure
``` ```
ui/ ui/
├── animation/ ├── animation/
│ └── SwooshMotion.kt # Shared animation specs object + reduced motion check │ └── SwooshMotion.kt # Shared specs + reduced motion
├── components/ ├── components/
│ ├── AnimatedDialog.kt # Reusable animated dialog wrapper (used by #16, #19) │ ├── AnimatedDialog.kt # Scale-in dialog wrapper
│ └── PulsingPlaceholder.kt # Pulsing loading placeholder (used by #9) │ └── PulsingPlaceholder.kt # Pulsing loading placeholder
├── feed/ ├── feed/
│ └── FeedScreen.kt # Modified: #1-#7 │ └── FeedScreen.kt # F1-F12
├── composer/ ├── composer/
│ └── ComposerScreen.kt # Modified: #8-#13, #13b │ └── ComposerScreen.kt # C1-C9
├── detail/ ├── detail/
│ └── DetailScreen.kt # Modified: #14-#17 │ └── DetailScreen.kt # D1-D5
├── settings/ ├── settings/
│ └── SettingsScreen.kt # Modified: #18-#19, #19b (new disconnect dialog) │ └── SettingsScreen.kt # S1-S3
├── stats/
│ └── StatsScreen.kt # ST1-ST3
└── navigation/ └── navigation/
└── NavGraph.kt # Modified: navigation transitions with enter/exit/popEnter/popExit └── NavGraph.kt # 8 route transitions
``` ```
## New files: 3 **New files:** 3 (`SwooshMotion.kt`, `AnimatedDialog.kt`, `PulsingPlaceholder.kt`)
- `ui/animation/SwooshMotion.kt` **Modified files:** 6 (`FeedScreen.kt`, `ComposerScreen.kt`, `DetailScreen.kt`, `SettingsScreen.kt`, `StatsScreen.kt`, `NavGraph.kt`)
- `ui/components/AnimatedDialog.kt`
- `ui/components/PulsingPlaceholder.kt`
## Modified files: 5
- `FeedScreen.kt`, `ComposerScreen.kt`, `DetailScreen.kt`, `SettingsScreen.kt`, `NavGraph.kt`
## Testing Strategy ## Testing Strategy
- Existing unit tests should pass unchanged (animations don't affect business logic) - Existing unit tests pass unchanged (animations don't affect business logic)
- Manual verification: each animation visually correct on emulator - Manual verification on emulator per animation
- Compose Preview for individual animated components where feasible - Test with "Remove animations" accessibility setting for reduced-motion fallback
- Test with system "Remove animations" setting enabled to verify reduced-motion fallback