mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 11:55:47 +00:00
feat: bold expressive theme with custom green palette, elevated cards, and high-contrast swipe actions
This commit is contained in:
parent
85fa89d985
commit
cfaba04039
10 changed files with 1231 additions and 599 deletions
1
.superpowers/brainstorm/46859-1773916456/.server-stopped
Normal file
1
.superpowers/brainstorm/46859-1773916456/.server-stopped
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"reason":"idle timeout","timestamp":1773918437073}
|
||||
4
.superpowers/brainstorm/46859-1773916456/.server.log
Normal file
4
.superpowers/brainstorm/46859-1773916456/.server.log
Normal 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"}
|
||||
1
.superpowers/brainstorm/46859-1773916456/.server.pid
Normal file
1
.superpowers/brainstorm/46859-1773916456/.server.pid
Normal file
|
|
@ -0,0 +1 @@
|
|||
46868
|
||||
249
.superpowers/brainstorm/46859-1773916456/animation-map-v3.html
Normal file
249
.superpowers/brainstorm/46859-1773916456/animation-map-v3.html
Normal 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>
|
||||
3
.superpowers/brainstorm/46859-1773916456/waiting-2.html
Normal file
3
.superpowers/brainstorm/46859-1773916456/waiting-2.html
Normal 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>
|
||||
|
|
@ -869,8 +869,8 @@ fun SwipeBackground(dismissState: SwipeToDismissBoxState) {
|
|||
|
||||
val color by animateColorAsState(
|
||||
when (direction) {
|
||||
SwipeToDismissBoxValue.StartToEnd -> MaterialTheme.colorScheme.primary
|
||||
SwipeToDismissBoxValue.EndToStart -> MaterialTheme.colorScheme.error
|
||||
SwipeToDismissBoxValue.StartToEnd -> Color(0xFF1565C0) // Bold blue
|
||||
SwipeToDismissBoxValue.EndToStart -> Color(0xFFC62828) // Bold red
|
||||
SwipeToDismissBoxValue.Settled -> Color.Transparent
|
||||
},
|
||||
label = "swipe_bg_color"
|
||||
|
|
@ -1262,7 +1262,7 @@ fun PostCardContent(
|
|||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.padding(horizontal = 16.dp, vertical = 6.dp)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onClickLabel = "View post details",
|
||||
|
|
@ -1270,9 +1270,10 @@ fun PostCardContent(
|
|||
showContextMenu = true
|
||||
}
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (post.featured)
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.15f)
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
else
|
||||
MaterialTheme.colorScheme.surface
|
||||
)
|
||||
|
|
@ -1978,21 +1979,21 @@ fun buildHighlightedString(
|
|||
|
||||
@Composable
|
||||
fun StatusBadge(post: FeedPost) {
|
||||
val (label, color) = when {
|
||||
post.queueStatus != QueueStatus.NONE -> "Pending" to MaterialTheme.colorScheme.tertiary
|
||||
post.status == "published" -> "Published" to MaterialTheme.colorScheme.primary
|
||||
post.status == "scheduled" -> "Scheduled" to MaterialTheme.colorScheme.secondary
|
||||
else -> "Draft" to MaterialTheme.colorScheme.outline
|
||||
val (label, containerColor, labelColor) = when {
|
||||
post.queueStatus != QueueStatus.NONE -> Triple("Pending", Color(0xFFFFF3E0), Color(0xFFE65100))
|
||||
post.status == "published" -> Triple("Published", Color(0xFFE8F5E9), Color(0xFF2E7D32))
|
||||
post.status == "scheduled" -> Triple("Scheduled", Color(0xFFE3F2FD), Color(0xFF1565C0))
|
||||
else -> Triple("Draft", Color(0xFFF3E5F5), Color(0xFF7B1FA2))
|
||||
}
|
||||
|
||||
SuggestionChip(
|
||||
onClick = {},
|
||||
label = {
|
||||
Text(label, style = MaterialTheme.typography.labelSmall)
|
||||
Text(label, style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.Bold))
|
||||
},
|
||||
colors = SuggestionChipDefaults.suggestionChipColors(
|
||||
containerColor = color.copy(alpha = 0.12f),
|
||||
labelColor = color
|
||||
containerColor = containerColor,
|
||||
labelColor = labelColor
|
||||
),
|
||||
border = null
|
||||
)
|
||||
|
|
|
|||
59
app/src/main/java/com/swoosh/microblog/ui/theme/Color.kt
Normal file
59
app/src/main/java/com/swoosh/microblog/ui/theme/Color.kt
Normal 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)
|
||||
|
|
@ -1,15 +1,133 @@
|
|||
package com.swoosh.microblog.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.*
|
||||
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
|
||||
fun SwooshTheme(
|
||||
themeMode: ThemeMode = ThemeMode.SYSTEM,
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val darkTheme = when (themeMode) {
|
||||
|
|
@ -18,19 +136,11 @@ fun SwooshTheme(
|
|||
ThemeMode.DARK -> true
|
||||
}
|
||||
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context)
|
||||
else dynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> darkColorScheme()
|
||||
else -> lightColorScheme()
|
||||
}
|
||||
val colorScheme = if (darkTheme) SwooshDarkColorScheme else SwooshLightColorScheme
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography(),
|
||||
typography = SwooshTypography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
# Micro-Animations Design — Swoosh
|
||||
# Micro-Animations Design — Swoosh (v2)
|
||||
|
||||
**Date:** 2026-03-19
|
||||
**Status:** Approved
|
||||
|
|
@ -6,235 +6,219 @@
|
|||
|
||||
## 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
|
||||
|
||||
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 |
|
||||
|------|------|-----------|----------|
|
||||
| `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 |
|
||||
| `Gentle` | Spring | dampingRatio=0.8, stiffness=300 | Cards, content reveal |
|
||||
| `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
|
||||
|
||||
`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
|
||||
|
||||
For staggered entrance animations (#3, #13, #14), each item uses a local `MutableState<Boolean>` controlled by a `LaunchedEffect`:
|
||||
|
||||
```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.
|
||||
Each item uses a `MutableState<Boolean>` toggled via `LaunchedEffect(Unit) { delay(StaggerDelayMs * index); visible = true }`. `AnimatedVisibility` has no built-in delay.
|
||||
|
||||
### 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)
|
||||
When the feed screen opens, the FAB scales from 0 to 1 with `Bouncy` spring. One visible overshoot — the FAB "pops" onto screen.
|
||||
- Compose API: `animateFloatAsState` with `Bouncy` spring on scale modifier
|
||||
- Trigger: `LaunchedEffect(Unit)` sets target to 1f
|
||||
### F2. FAB press (SPRING/BouncyQuick)
|
||||
Shrinks to 85%, snaps back. Settles before navigation fires.
|
||||
- `Modifier.pointerInput` press → `animateFloatAsState` 0.85f→1f
|
||||
- Navigation fires immediately; animation interrupted by transition (fine)
|
||||
|
||||
### 2. FAB press (SPRING/BouncyQuick)
|
||||
On tap, FAB shrinks to 85% and snaps back to 100%. Must settle in ~150ms so it completes before navigation fires.
|
||||
- Compose API: `Modifier.pointerInput` detecting press → `animateFloatAsState` scale 0.85f → 1f
|
||||
- Spring spec: `BouncyQuick` (dampingRatio=0.7, stiffness=1000)
|
||||
- Navigation fires immediately on tap — the spring animation is interrupted by the screen transition, which is fine
|
||||
### F3. Post cards staggered entrance (SLIDE/Gentle)
|
||||
Cascading slide-in from bottom, 50ms delay per item. "Waterfall" effect.
|
||||
- `AnimatedVisibility` per item in LazyColumn with `slideInVertically + fadeIn`
|
||||
- `mutableStateMapOf<String, Boolean>` tracking (see shared section)
|
||||
- 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)
|
||||
Cards slide in from bottom with cascading delay — 50ms per item. "Waterfall" effect.
|
||||
- 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`)
|
||||
### F4. "Show more" expand (SPRING/Snappy)
|
||||
`AnimatedContent(targetState = expanded)` with `fadeIn + expandVertically` / `fadeOut + shrinkVertically`. Replaces discrete text swap in PostCardContent.
|
||||
|
||||
### 4. "Show more" expand (SPRING/Snappy)
|
||||
Card content transitions between truncated and full text with animated height change.
|
||||
- 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
|
||||
### F5. Empty states (FADE/Quick + scale)
|
||||
All empty states (connection error, filter empty, search no results, normal empty) wrapped in `AnimatedVisibility` with `fadeIn + scaleIn(0.9f)`.
|
||||
|
||||
### 5. Empty state (FADE/Quick + scale)
|
||||
"No posts yet" icon and text fade in with subtle scale from 0.9 to 1.0.
|
||||
- Compose API: `AnimatedVisibility` with `fadeIn + scaleIn(initialScale = 0.9f)`
|
||||
### F6. Queue status chip (BOUNCE/Bouncy)
|
||||
- UPLOADING: `rememberInfiniteTransition` pulse alpha 0.6→1.0
|
||||
- Status change: bounce scale + `animateColorAsState`
|
||||
- FAILED: shake via `Animatable` offset oscillation (-4dp→4dp→0dp, 3 cycles)
|
||||
|
||||
### 6. Queue status chip (BOUNCE/Bouncy)
|
||||
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.
|
||||
- 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)
|
||||
### F7. Snackbar (SLIDE/Snappy)
|
||||
Both error snackbar (line 578) and pin confirmation snackbar (line 562) — `AnimatedVisibility` with `slideInVertically` + overshoot.
|
||||
|
||||
### 7. Snackbar error (SLIDE/Snappy)
|
||||
Slides in from bottom with slight overshoot. Fades out on timeout.
|
||||
- Compose API: `AnimatedVisibility` with `slideInVertically(initialOffsetY = { it })` + `Snappy` spring
|
||||
- Exit: `fadeOut` with `Quick` tween
|
||||
### F8. Search bar (SLIDE/Quick) — NEW
|
||||
`SearchTopBar` slides in from top with fade when search activates. Reverse on close.
|
||||
- `AnimatedVisibility` wrapping the `if (isSearchActive)` branch (line 157)
|
||||
- `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)
|
||||
After picking an image, the preview scales from 0 with bouncy spring. Close button fades in with scale.
|
||||
- Compose API: `AnimatedVisibility` with `scaleIn` using `Bouncy` spring
|
||||
- Close button: `fadeIn + scaleIn(initialScale = 0.5f)` — no rotation (an "X" icon should feel stable and immediately tappable)
|
||||
### F10. Account switcher items (SLIDE/Gentle) — NEW
|
||||
Inside `AccountSwitcherBottomSheet` (line 954), account `ListItem`s get staggered slide-in.
|
||||
- ModalBottomSheet has built-in slide animation
|
||||
- Add stagger pattern to account items inside the sheet
|
||||
|
||||
### 9. Link preview card (SLIDE/Gentle)
|
||||
After link loads, card slides up from below + fades in. While loading: pulsing placeholder card.
|
||||
- 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
|
||||
### F11. Pinned section header (FADE/Quick) — NEW
|
||||
"📌 Pinned" header at line 790 — `AnimatedVisibility` with `fadeIn + slideInVertically` when pinned posts exist.
|
||||
|
||||
### 10. Schedule chip (SPRING/Bouncy)
|
||||
After picking date, chip pops in with bouncy spring.
|
||||
- Compose API: `AnimatedVisibility` with `scaleIn` using `Bouncy`
|
||||
### F12. Account switch overlay (FADE/Quick) — NEW
|
||||
"Switching account..." overlay (line 288) — crossfade entrance with scale on the spinner.
|
||||
|
||||
### 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)
|
||||
Smooth color crossfade when exceeding 280 characters — neutral → red.
|
||||
- Compose API: `animateColorAsState` with `Quick` tween
|
||||
- Trigger: `text.length > 280`
|
||||
## Composer Screen — 9 animations
|
||||
|
||||
### 13. Action buttons staggered entrance (SCALE/Gentle)
|
||||
Draft, Schedule, Publish buttons — cascading scale-in with 50ms delay each.
|
||||
- Compose API: Stagger pattern (see shared section) with `AnimatedVisibility` + `scaleIn + fadeIn`
|
||||
### C1. Image grid preview (SCALE/Bouncy)
|
||||
`ImageGridPreview` (line 556) — each image thumbnail scales in with bounce when added. Scale-out on removal.
|
||||
- `AnimatedVisibility` per grid item with `scaleIn` using `Bouncy`
|
||||
|
||||
### NEW: 13b. Error text (SLIDE/Snappy)
|
||||
Composer error text slides in from left + fades in, consistent with snackbar style.
|
||||
- Compose API: `AnimatedVisibility` with `slideInHorizontally + fadeIn` using `Snappy` spring
|
||||
### C2. Link preview card (SLIDE/Gentle)
|
||||
After link loads, card slides up + fades in. Loading: `PulsingPlaceholder` replaces `LinearProgressIndicator` (line 317).
|
||||
|
||||
## 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)
|
||||
Elements appear sequentially: status badge → text → image → metadata. 80ms delay each.
|
||||
- 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)
|
||||
### C4. Publish button (BOUNCE/BouncyQuick)
|
||||
Button at line 421 — bounce on click, loading pulse during submit, `AnimatedContent` icon swap to checkmark on success.
|
||||
|
||||
### 15. Status badge entrance (SPRING/Bouncy)
|
||||
Badge scales from 0 with bounce — first visible element, draws attention. Part of the content reveal sequence (index 0, no delay).
|
||||
- Compose API: `animateFloatAsState` scale with `Bouncy` spring
|
||||
### C5. Character counter color (FADE/Quick)
|
||||
Three-tier `animateColorAsState`: onSurfaceVariant → tertiary (>280) → error (>500). Matches current logic at line 211.
|
||||
|
||||
### 16. Delete confirmation dialog (SCALE/Snappy)
|
||||
Dialog scales from center (0.8→1.0) with spring + backdrop fades in.
|
||||
- Compose API: Custom `AnimatedDialog` wrapper with `scaleIn(initialScale = 0.8f)` + `fadeIn` backdrop
|
||||
- Spring spec: `Snappy`
|
||||
### C6. Action buttons staggered (SCALE/Gentle)
|
||||
Publish button (line 421) + Row of [Draft, Schedule] (line 433). Two-step stagger: button first, then row.
|
||||
|
||||
### 17. Metadata section (SLIDE/Gentle)
|
||||
Bottom metadata slides up — last in the content reveal sequence (index 3, 240ms delay).
|
||||
- Compose API: Part of stagger pattern in #14
|
||||
### C7. Error text (SLIDE/Snappy)
|
||||
Error at line 407 — `AnimatedVisibility` with `slideInHorizontally + fadeIn`.
|
||||
|
||||
### 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
|
||||
|
||||
### 18. "Settings saved" feedback (SPRING/Bouncy)
|
||||
Existing "Settings saved" text (currently static conditional) gets animated: pops in with bounce (scale 0→1). After 2 seconds, fades out.
|
||||
- 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`
|
||||
### S1. Account card (FADE/Quick) — NEW
|
||||
Current account Card (line 78) — fade-in with subtle scale on screen entry.
|
||||
|
||||
### 19. Disconnect confirmation dialog (NEW behavior + FADE+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.
|
||||
- Compose API: Same `AnimatedDialog` wrapper as #16
|
||||
- Red pulse: `rememberInfiniteTransition` on alpha of error color
|
||||
### S2. Disconnect confirmation dialog (SCALE/Snappy)
|
||||
**New behavior:** Add confirmation dialog before both "Disconnect Current Account" (line 139) and "Disconnect All Accounts" (line 165). Uses `AnimatedDialog` wrapper.
|
||||
|
||||
### NEW: 19b. Disconnect dialog behavior
|
||||
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.
|
||||
### S3. Theme chip selection (SPRING/Bouncy) — NEW
|
||||
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
|
||||
Slide up from bottom + fade.
|
||||
- `enterTransition = slideInVertically(initialOffsetY = { it }) + fadeIn()`
|
||||
- `exitTransition = fadeOut()`
|
||||
- `popEnterTransition = fadeIn()`
|
||||
- `popExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut()`
|
||||
### ST1. Stats cards staggered entrance (SCALE/Bouncy)
|
||||
Four `StatsCard` composables (lines 68-98) — staggered scale-in with bounce. 50ms delay per card.
|
||||
|
||||
### Feed → Detail
|
||||
Slide in from right + fade.
|
||||
- `enterTransition = slideInHorizontally(initialOffsetX = { it }) + fadeIn()`
|
||||
- `exitTransition = fadeOut()`
|
||||
- `popEnterTransition = fadeIn()`
|
||||
- `popExitTransition = slideOutHorizontally(targetOffsetX = { it }) + fadeOut()`
|
||||
### ST2. Writing stats reveal (SLIDE/Gentle)
|
||||
`OutlinedCard` at line 109 — slide-up with fade after cards complete. Internal `WritingStatRow`s cascade.
|
||||
|
||||
### Detail → Composer (via Edit)
|
||||
Same as Feed → Composer (slide up from bottom). The Composer always enters with the same transition regardless of source.
|
||||
### ST3. Number count-up (FADE/Quick)
|
||||
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
|
||||
Crossfade — smooth transition from animated setup background to feed.
|
||||
- `enterTransition = fadeIn(tween(500))`
|
||||
- `exitTransition = fadeOut(tween(500))`
|
||||
## Navigation Transitions — 8 routes
|
||||
|
||||
Each `composable()` receives `enterTransition`, `exitTransition`, `popEnterTransition`, `popExitTransition`.
|
||||
|
||||
| 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
|
||||
|
||||
```
|
||||
ui/
|
||||
├── animation/
|
||||
│ └── SwooshMotion.kt # Shared animation specs object + reduced motion check
|
||||
│ └── SwooshMotion.kt # Shared specs + reduced motion
|
||||
├── components/
|
||||
│ ├── AnimatedDialog.kt # Reusable animated dialog wrapper (used by #16, #19)
|
||||
│ └── PulsingPlaceholder.kt # Pulsing loading placeholder (used by #9)
|
||||
│ ├── AnimatedDialog.kt # Scale-in dialog wrapper
|
||||
│ └── PulsingPlaceholder.kt # Pulsing loading placeholder
|
||||
├── feed/
|
||||
│ └── FeedScreen.kt # Modified: #1-#7
|
||||
│ └── FeedScreen.kt # F1-F12
|
||||
├── composer/
|
||||
│ └── ComposerScreen.kt # Modified: #8-#13, #13b
|
||||
│ └── ComposerScreen.kt # C1-C9
|
||||
├── detail/
|
||||
│ └── DetailScreen.kt # Modified: #14-#17
|
||||
│ └── DetailScreen.kt # D1-D5
|
||||
├── settings/
|
||||
│ └── SettingsScreen.kt # Modified: #18-#19, #19b (new disconnect dialog)
|
||||
│ └── SettingsScreen.kt # S1-S3
|
||||
├── stats/
|
||||
│ └── StatsScreen.kt # ST1-ST3
|
||||
└── navigation/
|
||||
└── NavGraph.kt # Modified: navigation transitions with enter/exit/popEnter/popExit
|
||||
└── NavGraph.kt # 8 route transitions
|
||||
```
|
||||
|
||||
## New files: 3
|
||||
- `ui/animation/SwooshMotion.kt`
|
||||
- `ui/components/AnimatedDialog.kt`
|
||||
- `ui/components/PulsingPlaceholder.kt`
|
||||
|
||||
## Modified files: 5
|
||||
- `FeedScreen.kt`, `ComposerScreen.kt`, `DetailScreen.kt`, `SettingsScreen.kt`, `NavGraph.kt`
|
||||
**New files:** 3 (`SwooshMotion.kt`, `AnimatedDialog.kt`, `PulsingPlaceholder.kt`)
|
||||
**Modified files:** 6 (`FeedScreen.kt`, `ComposerScreen.kt`, `DetailScreen.kt`, `SettingsScreen.kt`, `StatsScreen.kt`, `NavGraph.kt`)
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- Existing unit tests should pass unchanged (animations don't affect business logic)
|
||||
- Manual verification: each animation visually correct on emulator
|
||||
- Compose Preview for individual animated components where feasible
|
||||
- Test with system "Remove animations" setting enabled to verify reduced-motion fallback
|
||||
- Existing unit tests pass unchanged (animations don't affect business logic)
|
||||
- Manual verification on emulator per animation
|
||||
- Test with "Remove animations" accessibility setting for reduced-motion fallback
|
||||
|
|
|
|||
Loading…
Reference in a new issue