@shaykec/app-agent 1.0.9 → 1.0.11
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/catalog-analyzer.md +57 -0
- package/.claude/skills/android-customizer/SKILL.md +23 -10
- 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 +29 -8
- package/.claude/skills/module-integrator/SKILL.md +1 -1
- package/.claude/skills/react-native-customizer/SKILL.md +22 -10
- package/.claude/skills/test-planner/SKILL.md +72 -0
- package/.cursor/agents/README.md +3 -1
- package/.cursor/agents/catalog-analyzer.md +83 -0
- package/.cursor/rules/safety-guardrails.mdc +1 -1
- package/.cursor/rules/workflow.mdc +52 -18
- package/.cursor/skills/android-customizer/SKILL.md +43 -19
- 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 +47 -12
- 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 +46 -16
- package/.cursor/skills/test-planner/SKILL.md +199 -0
- package/.cursor/skills/web-analyzer/SKILL.md +310 -0
- package/.cursor/skills/web-crawler/SKILL.md +252 -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 +123 -11
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +8 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -1
- 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 +294 -16
- package/dist/index.js.map +1 -1
- package/dist/prompt-builder.d.ts +17 -1
- package/dist/prompt-builder.d.ts.map +1 -1
- package/dist/prompt-builder.js +272 -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 -4
- 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/prompts/web-clone-agent-prompt.md +179 -0
- 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/app/src/main/kotlin/com/appship/skeleton/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/Skeleton/tests/03_detail_screen.yaml +1 -1
- package/templates/android/Skeleton/tests/04_favorites.yaml +1 -1
- package/templates/android/Skeleton/tests/08_full_e2e.yaml +7 -1
- 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/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 +1 -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/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.shop.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
|
+
}
|
|
@@ -294,9 +294,10 @@ fun OrderSummaryCard(
|
|
|
294
294
|
discountCode: String?,
|
|
295
295
|
shipping: Double,
|
|
296
296
|
tax: Double,
|
|
297
|
-
total: Double
|
|
297
|
+
total: Double,
|
|
298
|
+
modifier: Modifier = Modifier
|
|
298
299
|
) {
|
|
299
|
-
Card {
|
|
300
|
+
Card(modifier = modifier) {
|
|
300
301
|
Column(modifier = Modifier.padding(16.dp)) {
|
|
301
302
|
Text(
|
|
302
303
|
"Order Summary",
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
package com.appship.skeleton.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
|
+
}
|
|
@@ -10,7 +10,7 @@ appId: ${APP_PACKAGE_NAME}
|
|
|
10
10
|
- extendedWaitUntil:
|
|
11
11
|
visible:
|
|
12
12
|
id: "home_item_0_card"
|
|
13
|
-
timeout:
|
|
13
|
+
timeout: 5000
|
|
14
14
|
# Detail from home
|
|
15
15
|
- tapOn:
|
|
16
16
|
id: "home_item_0_card"
|
|
@@ -45,6 +45,12 @@ appId: ${APP_PACKAGE_NAME}
|
|
|
45
45
|
- assertVisible:
|
|
46
46
|
id: "motion_prefs_full_button"
|
|
47
47
|
optional: true
|
|
48
|
+
- assertVisible:
|
|
49
|
+
id: "motion_prefs_reduced_button"
|
|
50
|
+
optional: true
|
|
51
|
+
- assertVisible:
|
|
52
|
+
id: "motion_prefs_off_button"
|
|
53
|
+
optional: true
|
|
48
54
|
- tapOn:
|
|
49
55
|
id: "motion_prefs_reduced_button"
|
|
50
56
|
- back
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
package com.appship.social.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
|
+
}
|