@shaykec/app-agent 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/android-customizer.md +9 -1
- package/.claude/agents/catalog-analyzer.md +57 -0
- package/.claude/agents/ios-customizer.md +9 -1
- package/.claude/agents/react-native-customizer.md +71 -0
- package/.claude/skills/android-customizer/SKILL.md +108 -23
- package/.claude/skills/bug-fixer/SKILL.md +59 -0
- package/.claude/skills/catalog-analyzer/SKILL.md +96 -0
- package/.claude/skills/customization-planner/SKILL.md +44 -5
- package/.claude/skills/design-selector/SKILL.md +3 -1
- package/.claude/skills/design-system/SKILL.md +1 -1
- package/.claude/skills/exploratory-tester/SKILL.md +82 -0
- package/.claude/skills/ios-customizer/SKILL.md +123 -23
- package/.claude/skills/module-integrator/SKILL.md +1 -1
- package/.claude/skills/react-native-customizer/SKILL.md +97 -11
- package/.claude/skills/test-planner/SKILL.md +72 -0
- package/.cursor/agents/README.md +3 -1
- package/.cursor/agents/android-customizer.md +15 -11
- package/.cursor/agents/catalog-analyzer.md +83 -0
- package/.cursor/agents/ios-customizer.md +15 -10
- package/.cursor/agents/react-native-customizer.md +170 -0
- package/.cursor/mcp.json +2 -10
- package/.cursor/rules/safety-guardrails.mdc +1 -1
- package/.cursor/rules/workflow.mdc +52 -18
- package/.cursor/skills/android-customizer/SKILL.md +46 -22
- package/.cursor/skills/bug-fixer/SKILL.md +189 -0
- package/.cursor/skills/catalog-analyzer/SKILL.md +222 -0
- package/.cursor/skills/customization-planner/SKILL.md +55 -8
- package/.cursor/skills/design-selector/SKILL.md +6 -5
- package/.cursor/skills/design-system/SKILL.md +8 -7
- package/.cursor/skills/exploratory-tester/SKILL.md +223 -0
- package/.cursor/skills/ios-customizer/SKILL.md +50 -15
- package/.cursor/skills/module-integrator/SKILL.md +2 -2
- package/.cursor/skills/output-validator/SKILL.md +1 -1
- package/.cursor/skills/react-native-customizer/SKILL.md +115 -25
- package/.cursor/skills/test-planner/SKILL.md +199 -0
- package/AGENTS.md +32 -11
- package/CLAUDE.md +78 -33
- package/README.md +77 -11
- package/designs/DESIGN_CATALOG.md +17 -15
- package/designs/DESIGN_PRINCIPLES.md +53 -0
- package/designs/brands/accessible-high-contrast.md +14 -0
- package/designs/brands/corporate-professional.md +14 -0
- package/designs/brands/dark-luxe.md +14 -0
- package/designs/brands/kids-playful.md +14 -0
- package/designs/brands/medical-clinical.md +14 -0
- package/designs/brands/modern-minimal.md +14 -0
- package/designs/brands/nature-organic.md +14 -0
- package/designs/brands/neo-brutalist.md +14 -0
- package/designs/brands/retro-vintage.md +14 -0
- package/designs/brands/soft-gradient.md +14 -0
- package/designs/brands/sport-athletic.md +14 -0
- package/designs/brands/tech-dynamic.md +14 -0
- package/designs/brands/vibrant-playful.md +14 -0
- package/dist/cli.d.ts +4 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +91 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/engines/claude-engine.d.ts.map +1 -1
- package/dist/engines/claude-engine.js +16 -4
- package/dist/engines/claude-engine.js.map +1 -1
- package/dist/engines/types.d.ts +1 -1
- package/dist/engines/types.d.ts.map +1 -1
- package/dist/engines/types.js +31 -2
- package/dist/engines/types.js.map +1 -1
- package/dist/github.d.ts +3 -0
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +47 -4
- package/dist/github.js.map +1 -1
- package/dist/index.js +217 -9
- package/dist/index.js.map +1 -1
- package/dist/prompt-builder.d.ts +11 -1
- package/dist/prompt-builder.d.ts.map +1 -1
- package/dist/prompt-builder.js +216 -1
- package/dist/prompt-builder.js.map +1 -1
- package/dist/validator.d.ts +7 -2
- package/dist/validator.d.ts.map +1 -1
- package/dist/validator.js +61 -41
- package/dist/validator.js.map +1 -1
- package/dist/workspace.js +2 -2
- package/dist/workspace.js.map +1 -1
- package/package.json +2 -2
- package/prompts/agent-prompt.md +35 -18
- package/prompts/deep-test-agent-prompt.md +122 -0
- package/prompts/fix-agent-prompt.md +90 -0
- package/prompts/quick-agent-prompt.md +32 -2
- package/prompts/scratch-agent-prompt.md +5 -8
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/ChatTemplate/app/src/main/kotlin/com/appship/chat/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/ChatTemplate/app/src/main/kotlin/com/appship/chat/features/conversations/ConversationsScreen.kt +1 -1
- package/templates/android/DashTemplate/app/src/main/kotlin/com/appship/dash/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/DashTemplate/app/src/main/kotlin/com/appship/dash/features/navigation/MainScreen.kt +1 -0
- package/templates/android/FamilyTemplate/app/src/main/java/com/appship/family/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/FamilyTemplate/app/src/main/java/com/appship/family/features/navigation/MainNavigation.kt +5 -1
- package/templates/android/FinanceTemplate/app/src/main/kotlin/com/appship/finance/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/core/animation/MotionPreferencesScreen.kt +3 -3
- package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/features/navigation/Navigation.kt +1 -1
- package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/features/settings/SettingsScreen.kt +1 -1
- package/templates/android/HealthTemplate/app/src/main/kotlin/com/appship/health/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/LearnTemplate/app/src/main/kotlin/com/appship/learn/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/MapTemplate/app/src/main/kotlin/com/appship/map/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/MediaTemplate/app/src/main/kotlin/com/appship/media/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/MediaTemplate/app/src/main/kotlin/com/appship/media/features/settings/SettingsScreen.kt +3 -2
- package/templates/android/ReferenceTemplate/app/src/main/kotlin/com/appship/reference/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/ReferenceTemplate/app/src/main/kotlin/com/appship/reference/features/settings/SettingsScreen.kt +1 -1
- package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/features/cart/CartScreen.kt +3 -2
- package/templates/android/Skeleton/TESTING_MANIFEST.md +2 -1
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/MainActivity.kt +23 -2
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/core/theme/AppearanceManager.kt +42 -0
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/profile/ProfileScreen.kt +20 -8
- package/templates/android/Skeleton/tests/03_detail_screen.yaml +3 -2
- package/templates/android/Skeleton/tests/04_favorites.yaml +3 -2
- package/templates/android/Skeleton/tests/08_full_e2e.yaml +9 -2
- package/templates/android/Skeleton/tests/09_dark_mode.yaml +50 -0
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/TaskTemplate/app/src/main/kotlin/com/appship/task/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/TaskTemplate/app/src/main/kotlin/com/appship/task/features/settings/SettingsScreen.kt +3 -2
- package/templates/android/TrackTemplate/app/src/main/kotlin/com/appship/track/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/ios/BookTemplate/BookTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/ChatTemplate/ChatTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/DashTemplate/DashTemplate/App/AppConfig.swift +1 -0
- package/templates/ios/DashTemplate/DashTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/DashTemplate/DashTemplate/Core/Strings.swift +13 -0
- package/templates/ios/DashTemplate/DashTemplate.xcodeproj/project.pbxproj +32 -20
- package/templates/ios/FamilyTemplate/FamilyTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/FinanceTemplate/FinanceTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/FinanceTemplate/FinanceTemplate/Core/Strings.swift +42 -0
- package/templates/ios/FinanceTemplate/FinanceTemplate.xcodeproj/project.pbxproj +36 -30
- package/templates/ios/GameTemplate/GameTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/HealthTemplate/HealthTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/LearnTemplate/LearnTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/MapTemplate/MapTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/MediaTemplate/MediaTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/ReferenceTemplate/ReferenceTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/ReferenceTemplate/ReferenceTemplate/Core/Strings.swift +12 -0
- package/templates/ios/ReferenceTemplate/ReferenceTemplate/Features/SkeletonLoading/SkeletonLoadingView.swift +2 -37
- package/templates/ios/ShopTemplate/ShopTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/Skeleton/Skeleton/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/Skeleton/tests/08_full_e2e.yaml +4 -0
- package/templates/ios/Skeleton/tests/09_dark_mode.yaml +52 -0
- package/templates/ios/SocialTemplate/SocialTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/TaskTemplate/TaskTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/TrackTemplate/TrackTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/react-native/BookTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/BookTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/BookTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/ChatTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/ChatTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/ChatTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/DashTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/DashTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/DashTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/FamilyTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/FamilyTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/FamilyTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/FinanceTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/FinanceTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/FinanceTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/GameTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/GameTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/GameTemplate/src/screens/GameDetail/GameDetailScreen.tsx +2 -1
- package/templates/react-native/GameTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/HealthTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/HealthTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/HealthTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/HealthTemplate/src/screens/WorkoutDetail/WorkoutDetailScreen.tsx +1 -1
- package/templates/react-native/LearnTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/LearnTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/LearnTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/MapTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/MapTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/MapTemplate/src/screens/Map/MapScreen.tsx +14 -0
- package/templates/react-native/MapTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/MediaTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/MediaTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/MediaTemplate/src/screens/PlaylistDetail/PlaylistDetailScreen.tsx +1 -1
- package/templates/react-native/MediaTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/ReferenceTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/ReferenceTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/ReferenceTemplate/src/screens/Settings/SettingsScreen.tsx +1 -1
- package/templates/react-native/ShopTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/ShopTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/ShopTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/Skeleton/TESTING_MANIFEST.md +2 -1
- package/templates/react-native/Skeleton/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/Skeleton/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/Skeleton/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/Skeleton/tests/07_profile.yaml +3 -2
- package/templates/react-native/Skeleton/tests/08_full_e2e.yaml +12 -1
- package/templates/react-native/Skeleton/tests/09_dark_mode.yaml +46 -0
- package/templates/react-native/SocialTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/SocialTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/SocialTemplate/src/screens/Feed/FeedScreen.tsx +1 -0
- package/templates/react-native/SocialTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/TaskTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/TaskTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/TaskTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/TrackTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/TrackTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/TrackTemplate/src/screens/Settings/SettingsScreen.tsx +1 -1
- package/templates/shared/ios/AnimatedTransitions/AnimatedTransitionsView.swift +233 -93
- package/.claude/agents/template-selector.md +0 -39
- package/.claude/skills/module-selector/SKILL.md +0 -81
- package/.claude/skills/template-selector/SKILL.md +0 -44
- package/.cursor/agents/template-selector.md +0 -52
- package/.cursor/skills/module-selector/SKILL.md +0 -135
- package/.cursor/skills/template-selector/SKILL.md +0 -123
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
package com.appship.finance.core.animation
|
|
2
2
|
|
|
3
|
+
import android.view.HapticFeedbackConstants
|
|
3
4
|
import androidx.compose.animation.*
|
|
4
5
|
import androidx.compose.animation.core.*
|
|
6
|
+
import androidx.compose.foundation.clickable
|
|
7
|
+
import androidx.compose.foundation.gestures.detectTapGestures
|
|
8
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
9
|
+
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
5
10
|
import androidx.compose.foundation.layout.offset
|
|
6
11
|
import androidx.compose.runtime.*
|
|
7
12
|
import androidx.compose.ui.Modifier
|
|
8
13
|
import androidx.compose.ui.composed
|
|
9
14
|
import androidx.compose.ui.draw.alpha
|
|
10
15
|
import androidx.compose.ui.draw.scale
|
|
16
|
+
import androidx.compose.ui.draw.shadow
|
|
17
|
+
import androidx.compose.ui.graphics.Color
|
|
18
|
+
import androidx.compose.ui.input.pointer.pointerInput
|
|
19
|
+
import androidx.compose.ui.platform.LocalView
|
|
11
20
|
import androidx.compose.ui.unit.IntOffset
|
|
21
|
+
import androidx.compose.ui.unit.dp
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
24
|
* Staggered appear animation for list items.
|
|
@@ -118,3 +128,181 @@ fun Modifier.bounceOnChange(
|
|
|
118
128
|
|
|
119
129
|
this.scale(bounceAnim)
|
|
120
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Scale-down press feedback for tappable elements.
|
|
134
|
+
* Scales to 0.95 on press with spring return animation.
|
|
135
|
+
*/
|
|
136
|
+
fun Modifier.scaleOnPress(): Modifier = composed {
|
|
137
|
+
val interactionSource = remember { MutableInteractionSource() }
|
|
138
|
+
val isPressed by interactionSource.collectIsPressedAsState()
|
|
139
|
+
|
|
140
|
+
val scale by animateFloatAsState(
|
|
141
|
+
targetValue = if (isPressed) 0.95f else 1f,
|
|
142
|
+
animationSpec = spring(
|
|
143
|
+
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
144
|
+
stiffness = Spring.StiffnessMedium
|
|
145
|
+
),
|
|
146
|
+
label = "pressScale"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
this
|
|
150
|
+
.scale(scale)
|
|
151
|
+
.clickable(
|
|
152
|
+
interactionSource = interactionSource,
|
|
153
|
+
indication = null
|
|
154
|
+
) { }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Triggers haptic feedback on tap.
|
|
159
|
+
*/
|
|
160
|
+
fun Modifier.hapticFeedback(): Modifier = composed {
|
|
161
|
+
val view = LocalView.current
|
|
162
|
+
this.pointerInput(Unit) {
|
|
163
|
+
detectTapGestures {
|
|
164
|
+
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Slide-from-side + fade-in animation on appear.
|
|
171
|
+
*/
|
|
172
|
+
fun Modifier.slideAndFade(
|
|
173
|
+
delayMs: Int = 0,
|
|
174
|
+
durationMs: Int = 400
|
|
175
|
+
): Modifier = composed {
|
|
176
|
+
var isVisible by remember { mutableStateOf(false) }
|
|
177
|
+
|
|
178
|
+
val alpha by animateFloatAsState(
|
|
179
|
+
targetValue = if (isVisible) 1f else 0f,
|
|
180
|
+
animationSpec = tween(
|
|
181
|
+
durationMillis = durationMs,
|
|
182
|
+
delayMillis = delayMs,
|
|
183
|
+
easing = FastOutSlowInEasing
|
|
184
|
+
),
|
|
185
|
+
label = "slideAndFadeAlpha"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
val offsetX by animateIntAsState(
|
|
189
|
+
targetValue = if (isVisible) 0 else 40,
|
|
190
|
+
animationSpec = tween(
|
|
191
|
+
durationMillis = durationMs,
|
|
192
|
+
delayMillis = delayMs,
|
|
193
|
+
easing = FastOutSlowInEasing
|
|
194
|
+
),
|
|
195
|
+
label = "slideAndFadeOffsetX"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
LaunchedEffect(Unit) {
|
|
199
|
+
isVisible = true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this
|
|
203
|
+
.alpha(alpha)
|
|
204
|
+
.offset { IntOffset(offsetX, 0) }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Subtle colored shadow for cards and elevated surfaces.
|
|
209
|
+
* Uses Material 3 elevation with optional color tinting.
|
|
210
|
+
*/
|
|
211
|
+
fun Modifier.cardShadow(
|
|
212
|
+
color: Color = Color.Black,
|
|
213
|
+
elevation: Float = 4f
|
|
214
|
+
): Modifier = composed {
|
|
215
|
+
this.shadow(
|
|
216
|
+
elevation = elevation.dp,
|
|
217
|
+
ambientColor = color.copy(alpha = 0.08f),
|
|
218
|
+
spotColor = color.copy(alpha = 0.12f),
|
|
219
|
+
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Deeper shadow for floating elements (FABs, modals).
|
|
225
|
+
*/
|
|
226
|
+
fun Modifier.elevatedShadow(
|
|
227
|
+
color: Color = Color.Black
|
|
228
|
+
): Modifier = composed {
|
|
229
|
+
this.shadow(
|
|
230
|
+
elevation = 8.dp,
|
|
231
|
+
ambientColor = color.copy(alpha = 0.12f),
|
|
232
|
+
spotColor = color.copy(alpha = 0.16f),
|
|
233
|
+
shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp)
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Shimmer loading overlay for skeleton placeholder views.
|
|
239
|
+
* Sweeps a translucent gradient across the content repeatedly.
|
|
240
|
+
*/
|
|
241
|
+
fun Modifier.shimmer(
|
|
242
|
+
durationMs: Int = 1500
|
|
243
|
+
): Modifier = composed {
|
|
244
|
+
val transition = rememberInfiniteTransition(label = "shimmer")
|
|
245
|
+
val translateAnim by transition.animateFloat(
|
|
246
|
+
initialValue = -1f,
|
|
247
|
+
targetValue = 2f,
|
|
248
|
+
animationSpec = infiniteRepeatable(
|
|
249
|
+
animation = tween(durationMillis = durationMs, easing = LinearEasing),
|
|
250
|
+
repeatMode = RepeatMode.Restart
|
|
251
|
+
),
|
|
252
|
+
label = "shimmerTranslate"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
this.alpha(0.3f + 0.7f * ((translateAnim + 1f) / 3f).coerceIn(0f, 1f))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Repeating scale pulse animation for live indicators and notifications.
|
|
260
|
+
*/
|
|
261
|
+
fun Modifier.pulse(
|
|
262
|
+
intensity: Float = 0.05f,
|
|
263
|
+
durationMs: Int = 1000
|
|
264
|
+
): Modifier = composed {
|
|
265
|
+
val transition = rememberInfiniteTransition(label = "pulse")
|
|
266
|
+
val scale by transition.animateFloat(
|
|
267
|
+
initialValue = 1f,
|
|
268
|
+
targetValue = 1f + intensity,
|
|
269
|
+
animationSpec = infiniteRepeatable(
|
|
270
|
+
animation = tween(durationMillis = durationMs, easing = FastOutSlowInEasing),
|
|
271
|
+
repeatMode = RepeatMode.Reverse
|
|
272
|
+
),
|
|
273
|
+
label = "pulseScale"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
this.scale(scale)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Heart/like bounce effect when toggled active.
|
|
281
|
+
* Scales up to 1.3x then springs back. Perfect for favorite buttons.
|
|
282
|
+
*/
|
|
283
|
+
fun Modifier.heartBounce(
|
|
284
|
+
isActive: Boolean
|
|
285
|
+
): Modifier = composed {
|
|
286
|
+
var previousActive by remember { mutableStateOf(isActive) }
|
|
287
|
+
var animatedScale by remember { mutableFloatStateOf(1f) }
|
|
288
|
+
|
|
289
|
+
val bounceAnim by animateFloatAsState(
|
|
290
|
+
targetValue = animatedScale,
|
|
291
|
+
animationSpec = spring(
|
|
292
|
+
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
293
|
+
stiffness = Spring.StiffnessMedium
|
|
294
|
+
),
|
|
295
|
+
label = "heartBounce"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
LaunchedEffect(isActive) {
|
|
299
|
+
if (isActive && !previousActive) {
|
|
300
|
+
animatedScale = 1.3f
|
|
301
|
+
kotlinx.coroutines.delay(200)
|
|
302
|
+
animatedScale = 1f
|
|
303
|
+
}
|
|
304
|
+
previousActive = isActive
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.scale(bounceAnim)
|
|
308
|
+
}
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
package com.appship.game.core.animation
|
|
2
2
|
|
|
3
|
+
import android.view.HapticFeedbackConstants
|
|
3
4
|
import androidx.compose.animation.*
|
|
4
5
|
import androidx.compose.animation.core.*
|
|
6
|
+
import androidx.compose.foundation.clickable
|
|
7
|
+
import androidx.compose.foundation.gestures.detectTapGestures
|
|
8
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
9
|
+
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
5
10
|
import androidx.compose.foundation.layout.offset
|
|
6
11
|
import androidx.compose.runtime.*
|
|
7
12
|
import androidx.compose.ui.Modifier
|
|
8
13
|
import androidx.compose.ui.composed
|
|
9
14
|
import androidx.compose.ui.draw.alpha
|
|
10
15
|
import androidx.compose.ui.draw.scale
|
|
16
|
+
import androidx.compose.ui.draw.shadow
|
|
17
|
+
import androidx.compose.ui.graphics.Color
|
|
18
|
+
import androidx.compose.ui.input.pointer.pointerInput
|
|
19
|
+
import androidx.compose.ui.platform.LocalView
|
|
11
20
|
import androidx.compose.ui.unit.IntOffset
|
|
21
|
+
import androidx.compose.ui.unit.dp
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
24
|
* Staggered appear animation for list items.
|
|
@@ -118,3 +128,181 @@ fun Modifier.bounceOnChange(
|
|
|
118
128
|
|
|
119
129
|
this.scale(bounceAnim)
|
|
120
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Scale-down press feedback for tappable elements.
|
|
134
|
+
* Scales to 0.95 on press with spring return animation.
|
|
135
|
+
*/
|
|
136
|
+
fun Modifier.scaleOnPress(): Modifier = composed {
|
|
137
|
+
val interactionSource = remember { MutableInteractionSource() }
|
|
138
|
+
val isPressed by interactionSource.collectIsPressedAsState()
|
|
139
|
+
|
|
140
|
+
val scale by animateFloatAsState(
|
|
141
|
+
targetValue = if (isPressed) 0.95f else 1f,
|
|
142
|
+
animationSpec = spring(
|
|
143
|
+
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
144
|
+
stiffness = Spring.StiffnessMedium
|
|
145
|
+
),
|
|
146
|
+
label = "pressScale"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
this
|
|
150
|
+
.scale(scale)
|
|
151
|
+
.clickable(
|
|
152
|
+
interactionSource = interactionSource,
|
|
153
|
+
indication = null
|
|
154
|
+
) { }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Triggers haptic feedback on tap.
|
|
159
|
+
*/
|
|
160
|
+
fun Modifier.hapticFeedback(): Modifier = composed {
|
|
161
|
+
val view = LocalView.current
|
|
162
|
+
this.pointerInput(Unit) {
|
|
163
|
+
detectTapGestures {
|
|
164
|
+
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Slide-from-side + fade-in animation on appear.
|
|
171
|
+
*/
|
|
172
|
+
fun Modifier.slideAndFade(
|
|
173
|
+
delayMs: Int = 0,
|
|
174
|
+
durationMs: Int = 400
|
|
175
|
+
): Modifier = composed {
|
|
176
|
+
var isVisible by remember { mutableStateOf(false) }
|
|
177
|
+
|
|
178
|
+
val alpha by animateFloatAsState(
|
|
179
|
+
targetValue = if (isVisible) 1f else 0f,
|
|
180
|
+
animationSpec = tween(
|
|
181
|
+
durationMillis = durationMs,
|
|
182
|
+
delayMillis = delayMs,
|
|
183
|
+
easing = FastOutSlowInEasing
|
|
184
|
+
),
|
|
185
|
+
label = "slideAndFadeAlpha"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
val offsetX by animateIntAsState(
|
|
189
|
+
targetValue = if (isVisible) 0 else 40,
|
|
190
|
+
animationSpec = tween(
|
|
191
|
+
durationMillis = durationMs,
|
|
192
|
+
delayMillis = delayMs,
|
|
193
|
+
easing = FastOutSlowInEasing
|
|
194
|
+
),
|
|
195
|
+
label = "slideAndFadeOffsetX"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
LaunchedEffect(Unit) {
|
|
199
|
+
isVisible = true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this
|
|
203
|
+
.alpha(alpha)
|
|
204
|
+
.offset { IntOffset(offsetX, 0) }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Subtle colored shadow for cards and elevated surfaces.
|
|
209
|
+
* Uses Material 3 elevation with optional color tinting.
|
|
210
|
+
*/
|
|
211
|
+
fun Modifier.cardShadow(
|
|
212
|
+
color: Color = Color.Black,
|
|
213
|
+
elevation: Float = 4f
|
|
214
|
+
): Modifier = composed {
|
|
215
|
+
this.shadow(
|
|
216
|
+
elevation = elevation.dp,
|
|
217
|
+
ambientColor = color.copy(alpha = 0.08f),
|
|
218
|
+
spotColor = color.copy(alpha = 0.12f),
|
|
219
|
+
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Deeper shadow for floating elements (FABs, modals).
|
|
225
|
+
*/
|
|
226
|
+
fun Modifier.elevatedShadow(
|
|
227
|
+
color: Color = Color.Black
|
|
228
|
+
): Modifier = composed {
|
|
229
|
+
this.shadow(
|
|
230
|
+
elevation = 8.dp,
|
|
231
|
+
ambientColor = color.copy(alpha = 0.12f),
|
|
232
|
+
spotColor = color.copy(alpha = 0.16f),
|
|
233
|
+
shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp)
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Shimmer loading overlay for skeleton placeholder views.
|
|
239
|
+
* Sweeps a translucent gradient across the content repeatedly.
|
|
240
|
+
*/
|
|
241
|
+
fun Modifier.shimmer(
|
|
242
|
+
durationMs: Int = 1500
|
|
243
|
+
): Modifier = composed {
|
|
244
|
+
val transition = rememberInfiniteTransition(label = "shimmer")
|
|
245
|
+
val translateAnim by transition.animateFloat(
|
|
246
|
+
initialValue = -1f,
|
|
247
|
+
targetValue = 2f,
|
|
248
|
+
animationSpec = infiniteRepeatable(
|
|
249
|
+
animation = tween(durationMillis = durationMs, easing = LinearEasing),
|
|
250
|
+
repeatMode = RepeatMode.Restart
|
|
251
|
+
),
|
|
252
|
+
label = "shimmerTranslate"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
this.alpha(0.3f + 0.7f * ((translateAnim + 1f) / 3f).coerceIn(0f, 1f))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Repeating scale pulse animation for live indicators and notifications.
|
|
260
|
+
*/
|
|
261
|
+
fun Modifier.pulse(
|
|
262
|
+
intensity: Float = 0.05f,
|
|
263
|
+
durationMs: Int = 1000
|
|
264
|
+
): Modifier = composed {
|
|
265
|
+
val transition = rememberInfiniteTransition(label = "pulse")
|
|
266
|
+
val scale by transition.animateFloat(
|
|
267
|
+
initialValue = 1f,
|
|
268
|
+
targetValue = 1f + intensity,
|
|
269
|
+
animationSpec = infiniteRepeatable(
|
|
270
|
+
animation = tween(durationMillis = durationMs, easing = FastOutSlowInEasing),
|
|
271
|
+
repeatMode = RepeatMode.Reverse
|
|
272
|
+
),
|
|
273
|
+
label = "pulseScale"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
this.scale(scale)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Heart/like bounce effect when toggled active.
|
|
281
|
+
* Scales up to 1.3x then springs back. Perfect for favorite buttons.
|
|
282
|
+
*/
|
|
283
|
+
fun Modifier.heartBounce(
|
|
284
|
+
isActive: Boolean
|
|
285
|
+
): Modifier = composed {
|
|
286
|
+
var previousActive by remember { mutableStateOf(isActive) }
|
|
287
|
+
var animatedScale by remember { mutableFloatStateOf(1f) }
|
|
288
|
+
|
|
289
|
+
val bounceAnim by animateFloatAsState(
|
|
290
|
+
targetValue = animatedScale,
|
|
291
|
+
animationSpec = spring(
|
|
292
|
+
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
293
|
+
stiffness = Spring.StiffnessMedium
|
|
294
|
+
),
|
|
295
|
+
label = "heartBounce"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
LaunchedEffect(isActive) {
|
|
299
|
+
if (isActive && !previousActive) {
|
|
300
|
+
animatedScale = 1.3f
|
|
301
|
+
kotlinx.coroutines.delay(200)
|
|
302
|
+
animatedScale = 1f
|
|
303
|
+
}
|
|
304
|
+
previousActive = isActive
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.scale(bounceAnim)
|
|
308
|
+
}
|
|
@@ -75,7 +75,7 @@ fun MotionPreferencesScreen(
|
|
|
75
75
|
IntensityRow(
|
|
76
76
|
intensity = AnimationIntensity.FULL,
|
|
77
77
|
isSelected = animationIntensity == AnimationIntensity.FULL,
|
|
78
|
-
icon = Icons.Filled.
|
|
78
|
+
icon = Icons.Filled.Star,
|
|
79
79
|
description = stringResource(R.string.motion_prefs_full_description),
|
|
80
80
|
onClick = { manager.setAnimationIntensity(AnimationIntensity.FULL) },
|
|
81
81
|
modifier = Modifier.testTag("motion_prefs_full_button")
|
|
@@ -83,7 +83,7 @@ fun MotionPreferencesScreen(
|
|
|
83
83
|
IntensityRow(
|
|
84
84
|
intensity = AnimationIntensity.REDUCED,
|
|
85
85
|
isSelected = animationIntensity == AnimationIntensity.REDUCED,
|
|
86
|
-
icon = Icons.Filled.
|
|
86
|
+
icon = Icons.Filled.Settings,
|
|
87
87
|
description = stringResource(R.string.motion_prefs_reduced_description),
|
|
88
88
|
onClick = { manager.setAnimationIntensity(AnimationIntensity.REDUCED) },
|
|
89
89
|
modifier = Modifier.testTag("motion_prefs_reduced_button")
|
|
@@ -91,7 +91,7 @@ fun MotionPreferencesScreen(
|
|
|
91
91
|
IntensityRow(
|
|
92
92
|
intensity = AnimationIntensity.OFF,
|
|
93
93
|
isSelected = animationIntensity == AnimationIntensity.OFF,
|
|
94
|
-
icon = Icons.Filled.
|
|
94
|
+
icon = Icons.Filled.Close,
|
|
95
95
|
description = stringResource(R.string.motion_prefs_off_description),
|
|
96
96
|
onClick = { manager.setAnimationIntensity(AnimationIntensity.OFF) },
|
|
97
97
|
modifier = Modifier.testTag("motion_prefs_off_button")
|
|
@@ -39,7 +39,7 @@ sealed class Screen(val route: String, val title: String, val icon: ImageVector)
|
|
|
39
39
|
object Results : Screen("results/{sessionId}", "Results", Icons.Default.Star)
|
|
40
40
|
object Achievements : Screen("achievements", "Achievements", Icons.Default.Star)
|
|
41
41
|
object Settings : Screen("settings", "Settings", Icons.Default.Settings)
|
|
42
|
-
data object MotionPreferences : Screen("motion_preferences", "Animations", Icons.Default.
|
|
42
|
+
data object MotionPreferences : Screen("motion_preferences", "Animations", Icons.Default.Star)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
@Composable
|
|
@@ -134,7 +134,7 @@ fun SettingsScreen(
|
|
|
134
134
|
headlineContent = { Text(stringResource(R.string.motion_prefs_animation_level)) },
|
|
135
135
|
leadingContent = {
|
|
136
136
|
Icon(
|
|
137
|
-
Icons.Default.
|
|
137
|
+
Icons.Default.Star,
|
|
138
138
|
contentDescription = null,
|
|
139
139
|
tint = MaterialTheme.colorScheme.primary
|
|
140
140
|
)
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
package com.appship.health.core.animation
|
|
2
2
|
|
|
3
|
+
import android.view.HapticFeedbackConstants
|
|
3
4
|
import androidx.compose.animation.*
|
|
4
5
|
import androidx.compose.animation.core.*
|
|
6
|
+
import androidx.compose.foundation.clickable
|
|
7
|
+
import androidx.compose.foundation.gestures.detectTapGestures
|
|
8
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
9
|
+
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
5
10
|
import androidx.compose.foundation.layout.offset
|
|
6
11
|
import androidx.compose.runtime.*
|
|
7
12
|
import androidx.compose.ui.Modifier
|
|
8
13
|
import androidx.compose.ui.composed
|
|
9
14
|
import androidx.compose.ui.draw.alpha
|
|
10
15
|
import androidx.compose.ui.draw.scale
|
|
16
|
+
import androidx.compose.ui.draw.shadow
|
|
17
|
+
import androidx.compose.ui.graphics.Color
|
|
18
|
+
import androidx.compose.ui.input.pointer.pointerInput
|
|
19
|
+
import androidx.compose.ui.platform.LocalView
|
|
11
20
|
import androidx.compose.ui.unit.IntOffset
|
|
21
|
+
import androidx.compose.ui.unit.dp
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
24
|
* Staggered appear animation for list items.
|
|
@@ -118,3 +128,181 @@ fun Modifier.bounceOnChange(
|
|
|
118
128
|
|
|
119
129
|
this.scale(bounceAnim)
|
|
120
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Scale-down press feedback for tappable elements.
|
|
134
|
+
* Scales to 0.95 on press with spring return animation.
|
|
135
|
+
*/
|
|
136
|
+
fun Modifier.scaleOnPress(): Modifier = composed {
|
|
137
|
+
val interactionSource = remember { MutableInteractionSource() }
|
|
138
|
+
val isPressed by interactionSource.collectIsPressedAsState()
|
|
139
|
+
|
|
140
|
+
val scale by animateFloatAsState(
|
|
141
|
+
targetValue = if (isPressed) 0.95f else 1f,
|
|
142
|
+
animationSpec = spring(
|
|
143
|
+
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
144
|
+
stiffness = Spring.StiffnessMedium
|
|
145
|
+
),
|
|
146
|
+
label = "pressScale"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
this
|
|
150
|
+
.scale(scale)
|
|
151
|
+
.clickable(
|
|
152
|
+
interactionSource = interactionSource,
|
|
153
|
+
indication = null
|
|
154
|
+
) { }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Triggers haptic feedback on tap.
|
|
159
|
+
*/
|
|
160
|
+
fun Modifier.hapticFeedback(): Modifier = composed {
|
|
161
|
+
val view = LocalView.current
|
|
162
|
+
this.pointerInput(Unit) {
|
|
163
|
+
detectTapGestures {
|
|
164
|
+
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Slide-from-side + fade-in animation on appear.
|
|
171
|
+
*/
|
|
172
|
+
fun Modifier.slideAndFade(
|
|
173
|
+
delayMs: Int = 0,
|
|
174
|
+
durationMs: Int = 400
|
|
175
|
+
): Modifier = composed {
|
|
176
|
+
var isVisible by remember { mutableStateOf(false) }
|
|
177
|
+
|
|
178
|
+
val alpha by animateFloatAsState(
|
|
179
|
+
targetValue = if (isVisible) 1f else 0f,
|
|
180
|
+
animationSpec = tween(
|
|
181
|
+
durationMillis = durationMs,
|
|
182
|
+
delayMillis = delayMs,
|
|
183
|
+
easing = FastOutSlowInEasing
|
|
184
|
+
),
|
|
185
|
+
label = "slideAndFadeAlpha"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
val offsetX by animateIntAsState(
|
|
189
|
+
targetValue = if (isVisible) 0 else 40,
|
|
190
|
+
animationSpec = tween(
|
|
191
|
+
durationMillis = durationMs,
|
|
192
|
+
delayMillis = delayMs,
|
|
193
|
+
easing = FastOutSlowInEasing
|
|
194
|
+
),
|
|
195
|
+
label = "slideAndFadeOffsetX"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
LaunchedEffect(Unit) {
|
|
199
|
+
isVisible = true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this
|
|
203
|
+
.alpha(alpha)
|
|
204
|
+
.offset { IntOffset(offsetX, 0) }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Subtle colored shadow for cards and elevated surfaces.
|
|
209
|
+
* Uses Material 3 elevation with optional color tinting.
|
|
210
|
+
*/
|
|
211
|
+
fun Modifier.cardShadow(
|
|
212
|
+
color: Color = Color.Black,
|
|
213
|
+
elevation: Float = 4f
|
|
214
|
+
): Modifier = composed {
|
|
215
|
+
this.shadow(
|
|
216
|
+
elevation = elevation.dp,
|
|
217
|
+
ambientColor = color.copy(alpha = 0.08f),
|
|
218
|
+
spotColor = color.copy(alpha = 0.12f),
|
|
219
|
+
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Deeper shadow for floating elements (FABs, modals).
|
|
225
|
+
*/
|
|
226
|
+
fun Modifier.elevatedShadow(
|
|
227
|
+
color: Color = Color.Black
|
|
228
|
+
): Modifier = composed {
|
|
229
|
+
this.shadow(
|
|
230
|
+
elevation = 8.dp,
|
|
231
|
+
ambientColor = color.copy(alpha = 0.12f),
|
|
232
|
+
spotColor = color.copy(alpha = 0.16f),
|
|
233
|
+
shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp)
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Shimmer loading overlay for skeleton placeholder views.
|
|
239
|
+
* Sweeps a translucent gradient across the content repeatedly.
|
|
240
|
+
*/
|
|
241
|
+
fun Modifier.shimmer(
|
|
242
|
+
durationMs: Int = 1500
|
|
243
|
+
): Modifier = composed {
|
|
244
|
+
val transition = rememberInfiniteTransition(label = "shimmer")
|
|
245
|
+
val translateAnim by transition.animateFloat(
|
|
246
|
+
initialValue = -1f,
|
|
247
|
+
targetValue = 2f,
|
|
248
|
+
animationSpec = infiniteRepeatable(
|
|
249
|
+
animation = tween(durationMillis = durationMs, easing = LinearEasing),
|
|
250
|
+
repeatMode = RepeatMode.Restart
|
|
251
|
+
),
|
|
252
|
+
label = "shimmerTranslate"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
this.alpha(0.3f + 0.7f * ((translateAnim + 1f) / 3f).coerceIn(0f, 1f))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Repeating scale pulse animation for live indicators and notifications.
|
|
260
|
+
*/
|
|
261
|
+
fun Modifier.pulse(
|
|
262
|
+
intensity: Float = 0.05f,
|
|
263
|
+
durationMs: Int = 1000
|
|
264
|
+
): Modifier = composed {
|
|
265
|
+
val transition = rememberInfiniteTransition(label = "pulse")
|
|
266
|
+
val scale by transition.animateFloat(
|
|
267
|
+
initialValue = 1f,
|
|
268
|
+
targetValue = 1f + intensity,
|
|
269
|
+
animationSpec = infiniteRepeatable(
|
|
270
|
+
animation = tween(durationMillis = durationMs, easing = FastOutSlowInEasing),
|
|
271
|
+
repeatMode = RepeatMode.Reverse
|
|
272
|
+
),
|
|
273
|
+
label = "pulseScale"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
this.scale(scale)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Heart/like bounce effect when toggled active.
|
|
281
|
+
* Scales up to 1.3x then springs back. Perfect for favorite buttons.
|
|
282
|
+
*/
|
|
283
|
+
fun Modifier.heartBounce(
|
|
284
|
+
isActive: Boolean
|
|
285
|
+
): Modifier = composed {
|
|
286
|
+
var previousActive by remember { mutableStateOf(isActive) }
|
|
287
|
+
var animatedScale by remember { mutableFloatStateOf(1f) }
|
|
288
|
+
|
|
289
|
+
val bounceAnim by animateFloatAsState(
|
|
290
|
+
targetValue = animatedScale,
|
|
291
|
+
animationSpec = spring(
|
|
292
|
+
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
293
|
+
stiffness = Spring.StiffnessMedium
|
|
294
|
+
),
|
|
295
|
+
label = "heartBounce"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
LaunchedEffect(isActive) {
|
|
299
|
+
if (isActive && !previousActive) {
|
|
300
|
+
animatedScale = 1.3f
|
|
301
|
+
kotlinx.coroutines.delay(200)
|
|
302
|
+
animatedScale = 1f
|
|
303
|
+
}
|
|
304
|
+
previousActive = isActive
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.scale(bounceAnim)
|
|
308
|
+
}
|