@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,5 +1,5 @@
|
|
|
1
|
-
import { useRef, useEffect } from 'react';
|
|
2
|
-
import { Animated } from 'react-native';
|
|
1
|
+
import { useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import { Animated, Platform, StyleSheet, ViewStyle } from 'react-native';
|
|
3
3
|
import { useMotionPreferences } from './useMotionPreferences';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -39,6 +39,7 @@ export function useStaggeredAppear(index: number) {
|
|
|
39
39
|
|
|
40
40
|
return {
|
|
41
41
|
opacity,
|
|
42
|
+
translateY,
|
|
42
43
|
transform: [{ translateY }],
|
|
43
44
|
};
|
|
44
45
|
}
|
|
@@ -78,6 +79,7 @@ export function useSlideIn(delay: number = 0) {
|
|
|
78
79
|
|
|
79
80
|
return {
|
|
80
81
|
opacity,
|
|
82
|
+
translateY,
|
|
81
83
|
transform: [{ translateY }],
|
|
82
84
|
};
|
|
83
85
|
}
|
|
@@ -118,3 +120,218 @@ export function useBounceScale(trigger: boolean) {
|
|
|
118
120
|
transform: [{ scale }],
|
|
119
121
|
};
|
|
120
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Hook for scale-down press feedback on tappable elements.
|
|
126
|
+
* Returns animated style + press handlers to spread on Pressable.
|
|
127
|
+
*/
|
|
128
|
+
export function useScaleOnPress(scaleValue: number = 0.95) {
|
|
129
|
+
const { shouldSkipAnimation } = useMotionPreferences();
|
|
130
|
+
const scale = useRef(new Animated.Value(1)).current;
|
|
131
|
+
|
|
132
|
+
const onPressIn = useCallback(() => {
|
|
133
|
+
if (shouldSkipAnimation) return;
|
|
134
|
+
Animated.spring(scale, {
|
|
135
|
+
toValue: scaleValue,
|
|
136
|
+
useNativeDriver: true,
|
|
137
|
+
speed: 50,
|
|
138
|
+
bounciness: 4,
|
|
139
|
+
}).start();
|
|
140
|
+
}, [shouldSkipAnimation, scale, scaleValue]);
|
|
141
|
+
|
|
142
|
+
const onPressOut = useCallback(() => {
|
|
143
|
+
if (shouldSkipAnimation) return;
|
|
144
|
+
Animated.spring(scale, {
|
|
145
|
+
toValue: 1,
|
|
146
|
+
useNativeDriver: true,
|
|
147
|
+
speed: 50,
|
|
148
|
+
bounciness: 4,
|
|
149
|
+
}).start();
|
|
150
|
+
}, [shouldSkipAnimation, scale]);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
style: { transform: [{ scale }] },
|
|
154
|
+
onPressIn,
|
|
155
|
+
onPressOut,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Hook for slide-from-side + fade-in animation on appear.
|
|
161
|
+
*/
|
|
162
|
+
export function useSlideAndFade(delay: number = 0) {
|
|
163
|
+
const { shouldAnimate, shouldSkipAnimation } = useMotionPreferences();
|
|
164
|
+
const opacity = useRef(new Animated.Value(shouldSkipAnimation ? 1 : 0)).current;
|
|
165
|
+
const translateX = useRef(new Animated.Value(shouldSkipAnimation ? 0 : 20)).current;
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (shouldSkipAnimation) {
|
|
169
|
+
opacity.setValue(1);
|
|
170
|
+
translateX.setValue(0);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const duration = shouldAnimate ? 400 : 200;
|
|
175
|
+
|
|
176
|
+
Animated.parallel([
|
|
177
|
+
Animated.timing(opacity, {
|
|
178
|
+
toValue: 1,
|
|
179
|
+
duration,
|
|
180
|
+
delay,
|
|
181
|
+
useNativeDriver: true,
|
|
182
|
+
}),
|
|
183
|
+
Animated.timing(translateX, {
|
|
184
|
+
toValue: 0,
|
|
185
|
+
duration,
|
|
186
|
+
delay,
|
|
187
|
+
useNativeDriver: true,
|
|
188
|
+
}),
|
|
189
|
+
]).start();
|
|
190
|
+
}, [delay, shouldAnimate, shouldSkipAnimation, opacity, translateX]);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
opacity,
|
|
194
|
+
transform: [{ translateX }],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Card shadow style for cards and elevated surfaces.
|
|
200
|
+
* Uses platform-specific shadow implementation.
|
|
201
|
+
*/
|
|
202
|
+
export function cardShadowStyle(color: string = '#000000'): ViewStyle {
|
|
203
|
+
return Platform.select({
|
|
204
|
+
ios: {
|
|
205
|
+
shadowColor: color,
|
|
206
|
+
shadowOffset: { width: 0, height: 4 },
|
|
207
|
+
shadowOpacity: 0.08,
|
|
208
|
+
shadowRadius: 8,
|
|
209
|
+
},
|
|
210
|
+
android: {
|
|
211
|
+
elevation: 4,
|
|
212
|
+
},
|
|
213
|
+
default: {
|
|
214
|
+
elevation: 4,
|
|
215
|
+
},
|
|
216
|
+
}) as ViewStyle;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Elevated shadow style for FABs and floating elements.
|
|
221
|
+
*/
|
|
222
|
+
export function elevatedShadowStyle(color: string = '#000000'): ViewStyle {
|
|
223
|
+
return Platform.select({
|
|
224
|
+
ios: {
|
|
225
|
+
shadowColor: color,
|
|
226
|
+
shadowOffset: { width: 0, height: 8 },
|
|
227
|
+
shadowOpacity: 0.12,
|
|
228
|
+
shadowRadius: 16,
|
|
229
|
+
},
|
|
230
|
+
android: {
|
|
231
|
+
elevation: 8,
|
|
232
|
+
},
|
|
233
|
+
default: {
|
|
234
|
+
elevation: 8,
|
|
235
|
+
},
|
|
236
|
+
}) as ViewStyle;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Hook for shimmer/skeleton loading animation.
|
|
241
|
+
* Returns an animated opacity style that pulses for loading placeholders.
|
|
242
|
+
*/
|
|
243
|
+
export function useShimmer(durationMs: number = 1500) {
|
|
244
|
+
const { shouldSkipAnimation } = useMotionPreferences();
|
|
245
|
+
const opacity = useRef(new Animated.Value(shouldSkipAnimation ? 1 : 0.3)).current;
|
|
246
|
+
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (shouldSkipAnimation) {
|
|
249
|
+
opacity.setValue(1);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const animation = Animated.loop(
|
|
254
|
+
Animated.sequence([
|
|
255
|
+
Animated.timing(opacity, {
|
|
256
|
+
toValue: 1,
|
|
257
|
+
duration: durationMs / 2,
|
|
258
|
+
useNativeDriver: true,
|
|
259
|
+
}),
|
|
260
|
+
Animated.timing(opacity, {
|
|
261
|
+
toValue: 0.3,
|
|
262
|
+
duration: durationMs / 2,
|
|
263
|
+
useNativeDriver: true,
|
|
264
|
+
}),
|
|
265
|
+
])
|
|
266
|
+
);
|
|
267
|
+
animation.start();
|
|
268
|
+
|
|
269
|
+
return () => animation.stop();
|
|
270
|
+
}, [durationMs, shouldSkipAnimation, opacity]);
|
|
271
|
+
|
|
272
|
+
return { opacity };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Hook for repeating pulse scale animation (notifications, live indicators).
|
|
277
|
+
*/
|
|
278
|
+
export function usePulse(intensity: number = 0.05) {
|
|
279
|
+
const { shouldSkipAnimation } = useMotionPreferences();
|
|
280
|
+
const scale = useRef(new Animated.Value(1)).current;
|
|
281
|
+
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
if (shouldSkipAnimation) return;
|
|
284
|
+
|
|
285
|
+
const animation = Animated.loop(
|
|
286
|
+
Animated.sequence([
|
|
287
|
+
Animated.timing(scale, {
|
|
288
|
+
toValue: 1 + intensity,
|
|
289
|
+
duration: 1000,
|
|
290
|
+
useNativeDriver: true,
|
|
291
|
+
}),
|
|
292
|
+
Animated.timing(scale, {
|
|
293
|
+
toValue: 1,
|
|
294
|
+
duration: 1000,
|
|
295
|
+
useNativeDriver: true,
|
|
296
|
+
}),
|
|
297
|
+
])
|
|
298
|
+
);
|
|
299
|
+
animation.start();
|
|
300
|
+
|
|
301
|
+
return () => animation.stop();
|
|
302
|
+
}, [intensity, shouldSkipAnimation, scale]);
|
|
303
|
+
|
|
304
|
+
return { transform: [{ scale }] };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Hook for heart/like bounce effect when toggled active.
|
|
309
|
+
* Scales up to 1.3x then springs back.
|
|
310
|
+
*/
|
|
311
|
+
export function useHeartBounce(isActive: boolean) {
|
|
312
|
+
const { shouldSkipAnimation } = useMotionPreferences();
|
|
313
|
+
const scale = useRef(new Animated.Value(1)).current;
|
|
314
|
+
const prevActive = useRef(isActive);
|
|
315
|
+
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
if (isActive && !prevActive.current && !shouldSkipAnimation) {
|
|
318
|
+
Animated.sequence([
|
|
319
|
+
Animated.spring(scale, {
|
|
320
|
+
toValue: 1.3,
|
|
321
|
+
useNativeDriver: true,
|
|
322
|
+
speed: 50,
|
|
323
|
+
bounciness: 12,
|
|
324
|
+
}),
|
|
325
|
+
Animated.spring(scale, {
|
|
326
|
+
toValue: 1,
|
|
327
|
+
useNativeDriver: true,
|
|
328
|
+
speed: 50,
|
|
329
|
+
bounciness: 12,
|
|
330
|
+
}),
|
|
331
|
+
]).start();
|
|
332
|
+
}
|
|
333
|
+
prevActive.current = isActive;
|
|
334
|
+
}, [isActive, shouldSkipAnimation, scale]);
|
|
335
|
+
|
|
336
|
+
return { transform: [{ scale }] };
|
|
337
|
+
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
2
|
import { AccessibilityInfo } from 'react-native';
|
|
3
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
4
3
|
|
|
5
4
|
const INTENSITY_STORAGE_KEY = '@motion_preferences_intensity';
|
|
6
5
|
|
|
6
|
+
// Safely load AsyncStorage — the native module may not be available on all
|
|
7
|
+
// simulator runtimes (e.g. iOS 26 beta). Fall back to in-memory storage.
|
|
8
|
+
let _asyncStorage: any = null;
|
|
9
|
+
try {
|
|
10
|
+
_asyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
11
|
+
} catch (_) {
|
|
12
|
+
// Native module unavailable — preferences won't persist across restarts.
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
export type AnimationIntensity = 'full' | 'reduced' | 'off';
|
|
8
16
|
|
|
9
17
|
export interface UseMotionPreferencesReturn {
|
|
@@ -25,12 +33,14 @@ export function useMotionPreferences(): UseMotionPreferencesReturn {
|
|
|
25
33
|
const [animationIntensity, setIntensityState] = useState<AnimationIntensity>('full');
|
|
26
34
|
|
|
27
35
|
useEffect(() => {
|
|
28
|
-
// Load persisted preference
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
// Load persisted preference (skip if AsyncStorage unavailable)
|
|
37
|
+
if (_asyncStorage) {
|
|
38
|
+
_asyncStorage.getItem(INTENSITY_STORAGE_KEY).then((stored: string | null) => {
|
|
39
|
+
if (stored === 'full' || stored === 'reduced' || stored === 'off') {
|
|
40
|
+
setIntensityState(stored);
|
|
41
|
+
}
|
|
42
|
+
}).catch(() => {});
|
|
43
|
+
}
|
|
34
44
|
|
|
35
45
|
// Check system Reduce Motion
|
|
36
46
|
AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {
|
|
@@ -51,10 +61,14 @@ export function useMotionPreferences(): UseMotionPreferencesReturn {
|
|
|
51
61
|
|
|
52
62
|
const setAnimationIntensity = useCallback(async (intensity: AnimationIntensity) => {
|
|
53
63
|
try {
|
|
54
|
-
|
|
64
|
+
if (_asyncStorage) {
|
|
65
|
+
await _asyncStorage.setItem(INTENSITY_STORAGE_KEY, intensity);
|
|
66
|
+
}
|
|
55
67
|
setIntensityState(intensity);
|
|
56
68
|
} catch (error) {
|
|
57
|
-
|
|
69
|
+
// Still update in-memory state even if persistence fails
|
|
70
|
+
setIntensityState(intensity);
|
|
71
|
+
console.warn('Failed to save motion preference:', error);
|
|
58
72
|
}
|
|
59
73
|
}, []);
|
|
60
74
|
|
|
@@ -46,7 +46,7 @@ const SettingsScreen: React.FC = () => {
|
|
|
46
46
|
accessibilityRole="button"
|
|
47
47
|
accessibilityLabel="Animations settings"
|
|
48
48
|
style={styles.row}
|
|
49
|
-
onPress={() => navigation.navigate('MotionPreferences'
|
|
49
|
+
onPress={() => (navigation as any).navigate('MotionPreferences')}>
|
|
50
50
|
<Text style={[styles.rowLabel, {color: colors.primaryText, fontSize: typography.body}]}>
|
|
51
51
|
Animations
|
|
52
52
|
</Text>
|
|
@@ -1,143 +1,283 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
2
|
|
|
3
|
-
// MARK: -
|
|
4
|
-
/// Applies a
|
|
5
|
-
struct
|
|
6
|
-
|
|
3
|
+
// MARK: - Staggered Appear Modifier
|
|
4
|
+
/// Applies a staggered fade-in + slide-up animation to list items.
|
|
5
|
+
struct StaggeredAppearModifier: ViewModifier {
|
|
6
|
+
let index: Int
|
|
7
7
|
let config: TransitionConfig
|
|
8
|
+
@State private var isVisible = false
|
|
8
9
|
|
|
9
10
|
func body(content: Content) -> some View {
|
|
10
|
-
let angle = AnimatedTransitionsService.shared.cardFlipAngle(isFlipped: isFlipped)
|
|
11
|
-
let animation = AnimatedTransitionsService.shared.springAnimation(config: config)
|
|
12
|
-
|
|
13
11
|
content
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
.opacity(isVisible ? 1 : 0)
|
|
13
|
+
.offset(y: isVisible ? 0 : 20)
|
|
14
|
+
.motionAware(animation: .spring(
|
|
15
|
+
response: config.springResponse,
|
|
16
|
+
dampingFraction: config.springDamping
|
|
17
|
+
).delay(AnimatedTransitionsService.shared.staggerDelay(index: index)))
|
|
18
|
+
.onAppear {
|
|
19
|
+
isVisible = true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// MARK: - Slide In Modifier
|
|
25
|
+
/// Applies a slide-in-from-bottom animation on appear.
|
|
26
|
+
struct SlideInModifier: ViewModifier {
|
|
27
|
+
let delay: Double
|
|
28
|
+
@State private var isVisible = false
|
|
29
|
+
|
|
30
|
+
func body(content: Content) -> some View {
|
|
31
|
+
content
|
|
32
|
+
.opacity(isVisible ? 1 : 0)
|
|
33
|
+
.offset(y: isVisible ? 0 : 30)
|
|
34
|
+
.motionAware(animation: .spring(response: 0.5, dampingFraction: 0.8).delay(delay))
|
|
35
|
+
.onAppear {
|
|
36
|
+
isVisible = true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// MARK: - Bounce Scale Modifier
|
|
42
|
+
/// Applies a scale bounce animation on state change (e.g., favorite toggle).
|
|
43
|
+
struct BounceScaleModifier: ViewModifier {
|
|
44
|
+
let isActive: Bool
|
|
45
|
+
@State private var scale: CGFloat = 1.0
|
|
46
|
+
|
|
47
|
+
func body(content: Content) -> some View {
|
|
48
|
+
content
|
|
49
|
+
.scaleEffect(scale)
|
|
50
|
+
.motionAware(animation: .spring(response: 0.3, dampingFraction: 0.5))
|
|
51
|
+
.onChange(of: isActive) { _, _ in
|
|
52
|
+
scale = 1.3
|
|
53
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
54
|
+
scale = 1.0
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// MARK: - Scale on Press Modifier
|
|
61
|
+
/// Applies scale-down + spring-back animation when pressed (like a button press).
|
|
62
|
+
struct ScaleOnPressModifier: ViewModifier {
|
|
63
|
+
@State private var isPressed = false
|
|
64
|
+
|
|
65
|
+
func body(content: Content) -> some View {
|
|
66
|
+
content
|
|
67
|
+
.scaleEffect(isPressed ? 0.95 : 1.0)
|
|
68
|
+
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: isPressed)
|
|
69
|
+
.simultaneousGesture(
|
|
70
|
+
DragGesture(minimumDistance: 0)
|
|
71
|
+
.onChanged { _ in isPressed = true }
|
|
72
|
+
.onEnded { _ in isPressed = false }
|
|
18
73
|
)
|
|
19
|
-
|
|
20
|
-
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// MARK: - Haptic Feedback Modifier
|
|
78
|
+
/// Triggers haptic feedback on tap.
|
|
79
|
+
struct HapticFeedbackModifier: ViewModifier {
|
|
80
|
+
let style: UIImpactFeedbackGenerator.FeedbackStyle
|
|
81
|
+
|
|
82
|
+
func body(content: Content) -> some View {
|
|
83
|
+
content
|
|
84
|
+
.onTapGesture {
|
|
85
|
+
UIImpactFeedbackGenerator(style: style).impactOccurred()
|
|
86
|
+
}
|
|
21
87
|
}
|
|
22
88
|
}
|
|
23
89
|
|
|
24
90
|
// MARK: - Slide and Fade Modifier
|
|
25
|
-
/// Applies
|
|
91
|
+
/// Applies a slide-from-side + fade-in animation on appear.
|
|
26
92
|
struct SlideAndFadeModifier: ViewModifier {
|
|
27
|
-
let
|
|
28
|
-
|
|
29
|
-
let config: TransitionConfig
|
|
93
|
+
let delay: Double
|
|
94
|
+
@State private var isVisible = false
|
|
30
95
|
|
|
31
96
|
func body(content: Content) -> some View {
|
|
32
|
-
let service = AnimatedTransitionsService.shared
|
|
33
|
-
let horizontalOffset = service.slideHorizontalOffset(isVisible: isVisible, direction: direction)
|
|
34
|
-
let verticalOffset = service.slideVerticalOffset(isVisible: isVisible, direction: direction)
|
|
35
|
-
let opacity = service.fadeOpacity(isVisible: isVisible)
|
|
36
|
-
let animation = service.springAnimation(config: config)
|
|
37
|
-
|
|
38
97
|
content
|
|
39
|
-
.
|
|
40
|
-
.
|
|
41
|
-
.motionAware(animation:
|
|
42
|
-
.
|
|
98
|
+
.opacity(isVisible ? 1 : 0)
|
|
99
|
+
.offset(x: isVisible ? 0 : 20)
|
|
100
|
+
.motionAware(animation: .spring(response: 0.5, dampingFraction: 0.8).delay(delay))
|
|
101
|
+
.onAppear {
|
|
102
|
+
isVisible = true
|
|
103
|
+
}
|
|
43
104
|
}
|
|
44
105
|
}
|
|
45
106
|
|
|
46
|
-
// MARK: -
|
|
47
|
-
/// Applies
|
|
48
|
-
struct
|
|
49
|
-
let
|
|
50
|
-
let
|
|
107
|
+
// MARK: - Card Shadow Modifier
|
|
108
|
+
/// Applies a subtle colored shadow to cards and elevated surfaces.
|
|
109
|
+
struct CardShadowModifier: ViewModifier {
|
|
110
|
+
let color: Color
|
|
111
|
+
let radius: CGFloat
|
|
112
|
+
let y: CGFloat
|
|
51
113
|
|
|
52
114
|
func body(content: Content) -> some View {
|
|
53
|
-
let service = AnimatedTransitionsService.shared
|
|
54
|
-
let scale = service.zoomScale(isVisible: isVisible)
|
|
55
|
-
let opacity = service.fadeOpacity(isVisible: isVisible)
|
|
56
|
-
let animation = service.springAnimation(config: config)
|
|
57
|
-
|
|
58
115
|
content
|
|
59
|
-
.
|
|
60
|
-
.opacity(
|
|
61
|
-
.motionAware(animation: animation)
|
|
62
|
-
.accessibilityIdentifier("animated_transitions_zoom")
|
|
116
|
+
.shadow(color: color.opacity(0.08), radius: radius, x: 0, y: y)
|
|
117
|
+
.shadow(color: Color.black.opacity(0.04), radius: radius / 2, x: 0, y: y / 2)
|
|
63
118
|
}
|
|
64
119
|
}
|
|
65
120
|
|
|
66
|
-
// MARK: -
|
|
67
|
-
/// Applies
|
|
68
|
-
struct
|
|
69
|
-
let
|
|
70
|
-
let config: TransitionConfig
|
|
121
|
+
// MARK: - Elevated Shadow Modifier
|
|
122
|
+
/// Applies a deeper shadow for FABs and floating elements.
|
|
123
|
+
struct ElevatedShadowModifier: ViewModifier {
|
|
124
|
+
let color: Color
|
|
71
125
|
|
|
72
126
|
func body(content: Content) -> some View {
|
|
73
|
-
let opacity = AnimatedTransitionsService.shared.fadeOpacity(isVisible: isShowingFirst)
|
|
74
|
-
let animation = AnimatedTransitionsService.shared.springAnimation(config: config)
|
|
75
|
-
|
|
76
127
|
content
|
|
77
|
-
.opacity(
|
|
78
|
-
.
|
|
79
|
-
.accessibilityIdentifier("animated_transitions_cross_dissolve")
|
|
128
|
+
.shadow(color: color.opacity(0.12), radius: 16, x: 0, y: 8)
|
|
129
|
+
.shadow(color: Color.black.opacity(0.06), radius: 8, x: 0, y: 4)
|
|
80
130
|
}
|
|
81
131
|
}
|
|
82
132
|
|
|
83
|
-
// MARK: -
|
|
84
|
-
///
|
|
85
|
-
struct
|
|
86
|
-
|
|
87
|
-
let namespace: Namespace.ID
|
|
88
|
-
let content: Content
|
|
133
|
+
// MARK: - Shimmer Modifier
|
|
134
|
+
/// Applies a shimmer/skeleton-loading gradient overlay for loading states.
|
|
135
|
+
struct ShimmerModifier: ViewModifier {
|
|
136
|
+
@State private var isAnimating = false
|
|
89
137
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
138
|
+
func body(content: Content) -> some View {
|
|
139
|
+
content
|
|
140
|
+
.overlay(
|
|
141
|
+
GeometryReader { geometry in
|
|
142
|
+
if isAnimating {
|
|
143
|
+
LinearGradient(
|
|
144
|
+
colors: [
|
|
145
|
+
Color.clear,
|
|
146
|
+
Color.white.opacity(0.3),
|
|
147
|
+
Color.clear
|
|
148
|
+
],
|
|
149
|
+
startPoint: .leading,
|
|
150
|
+
endPoint: .trailing
|
|
151
|
+
)
|
|
152
|
+
.offset(x: isAnimating ? geometry.size.width : -geometry.size.width)
|
|
153
|
+
.animation(
|
|
154
|
+
Animation.linear(duration: 1.5)
|
|
155
|
+
.repeatForever(autoreverses: false),
|
|
156
|
+
value: isAnimating
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
.clipped()
|
|
162
|
+
.onAppear {
|
|
163
|
+
isAnimating = true
|
|
164
|
+
}
|
|
94
165
|
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// MARK: - Pulse Modifier
|
|
169
|
+
/// Applies a repeating scale pulse effect (e.g., for notifications, active indicators).
|
|
170
|
+
struct PulseModifier: ViewModifier {
|
|
171
|
+
let intensity: CGFloat
|
|
172
|
+
@State private var scale: CGFloat = 1.0
|
|
95
173
|
|
|
96
|
-
|
|
174
|
+
func body(content: Content) -> some View {
|
|
97
175
|
content
|
|
98
|
-
.
|
|
99
|
-
.
|
|
100
|
-
|
|
176
|
+
.scaleEffect(scale)
|
|
177
|
+
.onAppear {
|
|
178
|
+
withAnimation(
|
|
179
|
+
.easeInOut(duration: 1.0)
|
|
180
|
+
.repeatForever(autoreverses: true)
|
|
181
|
+
) {
|
|
182
|
+
scale = 1.0 + intensity
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// MARK: - Heart Bounce Modifier
|
|
189
|
+
/// Applies a scale-up bounce then return when toggled (e.g., favorite/like buttons).
|
|
190
|
+
struct HeartBounceModifier: ViewModifier {
|
|
191
|
+
let isActive: Bool
|
|
192
|
+
@State private var scale: CGFloat = 1.0
|
|
193
|
+
|
|
194
|
+
func body(content: Content) -> some View {
|
|
195
|
+
content
|
|
196
|
+
.scaleEffect(scale)
|
|
197
|
+
.motionAware(animation: .spring(response: 0.3, dampingFraction: 0.5))
|
|
198
|
+
.onChange(of: isActive) { _, newValue in
|
|
199
|
+
if newValue {
|
|
200
|
+
scale = 1.3
|
|
201
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
202
|
+
scale = 1.0
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
101
206
|
}
|
|
102
207
|
}
|
|
103
208
|
|
|
104
209
|
// MARK: - View Extensions
|
|
105
210
|
|
|
106
211
|
extension View {
|
|
107
|
-
/// Applies
|
|
212
|
+
/// Applies staggered appear animation for list items.
|
|
108
213
|
/// - Parameters:
|
|
109
|
-
/// -
|
|
110
|
-
/// - config: Transition configuration (default: .
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
modifier(CardFlipModifier(isFlipped: isFlipped, config: config))
|
|
214
|
+
/// - index: The item's position in the list (for stagger delay)
|
|
215
|
+
/// - config: Transition configuration (default: .fast)
|
|
216
|
+
func staggeredAppear(index: Int, config: TransitionConfig = .fast) -> some View {
|
|
217
|
+
modifier(StaggeredAppearModifier(index: index, config: config))
|
|
114
218
|
}
|
|
115
219
|
|
|
116
|
-
/// Applies
|
|
117
|
-
/// -
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
/// - config: Transition configuration (default: .default)
|
|
121
|
-
/// - Returns: A view with slide and fade transition applied
|
|
122
|
-
func slideAndFade(isVisible: Bool, direction: SlideDirection, config: TransitionConfig = .default) -> some View {
|
|
123
|
-
modifier(SlideAndFadeModifier(isVisible: isVisible, direction: direction, config: config))
|
|
220
|
+
/// Applies slide-in-from-bottom animation on appear.
|
|
221
|
+
/// - Parameter delay: Delay before animation starts (default: 0)
|
|
222
|
+
func slideIn(delay: Double = 0) -> some View {
|
|
223
|
+
modifier(SlideInModifier(delay: delay))
|
|
124
224
|
}
|
|
125
225
|
|
|
126
|
-
/// Applies
|
|
127
|
-
/// -
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
/// - Returns: A view with zoom transition applied
|
|
131
|
-
func zoomTransition(isVisible: Bool, config: TransitionConfig = .default) -> some View {
|
|
132
|
-
modifier(ZoomTransitionModifier(isVisible: isVisible, config: config))
|
|
226
|
+
/// Applies scale bounce on state change (e.g., favorite toggle).
|
|
227
|
+
/// - Parameter isActive: The boolean state that triggers the bounce
|
|
228
|
+
func bounceOnChange(isActive: Bool) -> some View {
|
|
229
|
+
modifier(BounceScaleModifier(isActive: isActive))
|
|
133
230
|
}
|
|
134
231
|
|
|
135
|
-
/// Applies
|
|
232
|
+
/// Applies scale-down press feedback to any tappable element.
|
|
233
|
+
/// Scales to 0.95 on press with spring return animation.
|
|
234
|
+
func scaleOnPress() -> some View {
|
|
235
|
+
modifier(ScaleOnPressModifier())
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Triggers haptic feedback on tap.
|
|
239
|
+
/// - Parameter style: Feedback intensity (.light, .medium, .heavy)
|
|
240
|
+
func hapticFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle = .light) -> some View {
|
|
241
|
+
modifier(HapticFeedbackModifier(style: style))
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Applies a slide-from-side + fade-in animation on appear.
|
|
245
|
+
/// - Parameter delay: Delay before animation starts (default: 0)
|
|
246
|
+
func slideAndFade(delay: Double = 0) -> some View {
|
|
247
|
+
modifier(SlideAndFadeModifier(delay: delay))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// Applies subtle colored shadow to cards and elevated surfaces.
|
|
251
|
+
/// Uses dual-shadow technique: primary-tinted shadow + neutral shadow for depth.
|
|
136
252
|
/// - Parameters:
|
|
137
|
-
/// -
|
|
138
|
-
/// -
|
|
139
|
-
///
|
|
140
|
-
func
|
|
141
|
-
modifier(
|
|
253
|
+
/// - color: Shadow tint color (default: .primary)
|
|
254
|
+
/// - radius: Shadow blur radius (default: 8)
|
|
255
|
+
/// - y: Vertical offset (default: 4)
|
|
256
|
+
func cardShadow(color: Color = .primary, radius: CGFloat = 8, y: CGFloat = 4) -> some View {
|
|
257
|
+
modifier(CardShadowModifier(color: color, radius: radius, y: y))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// Applies deeper shadow for floating elements (FABs, modals).
|
|
261
|
+
/// - Parameter color: Shadow tint color (default: .primary)
|
|
262
|
+
func elevatedShadow(color: Color = .primary) -> some View {
|
|
263
|
+
modifier(ElevatedShadowModifier(color: color))
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// Applies a shimmer loading overlay (e.g., for skeleton placeholder views).
|
|
267
|
+
func shimmer() -> some View {
|
|
268
|
+
modifier(ShimmerModifier())
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Applies a repeating pulse animation (e.g., for notifications, live indicators).
|
|
272
|
+
/// - Parameter intensity: Scale increase per pulse (default: 0.05 = 5% bigger)
|
|
273
|
+
func pulse(intensity: CGFloat = 0.05) -> some View {
|
|
274
|
+
modifier(PulseModifier(intensity: intensity))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// Applies a heart/like bounce effect when toggled active.
|
|
278
|
+
/// Scales up to 1.3x then springs back. Perfect for favorite buttons.
|
|
279
|
+
/// - Parameter isActive: Boolean that triggers the bounce when it becomes true
|
|
280
|
+
func heartBounce(isActive: Bool) -> some View {
|
|
281
|
+
modifier(HeartBounceModifier(isActive: isActive))
|
|
142
282
|
}
|
|
143
283
|
}
|