@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.
Files changed (202) hide show
  1. package/.claude/agents/catalog-analyzer.md +57 -0
  2. package/.claude/skills/android-customizer/SKILL.md +23 -10
  3. package/.claude/skills/bug-fixer/SKILL.md +59 -0
  4. package/.claude/skills/catalog-analyzer/SKILL.md +96 -0
  5. package/.claude/skills/customization-planner/SKILL.md +44 -5
  6. package/.claude/skills/design-selector/SKILL.md +3 -1
  7. package/.claude/skills/design-system/SKILL.md +1 -1
  8. package/.claude/skills/exploratory-tester/SKILL.md +82 -0
  9. package/.claude/skills/ios-customizer/SKILL.md +29 -8
  10. package/.claude/skills/module-integrator/SKILL.md +1 -1
  11. package/.claude/skills/react-native-customizer/SKILL.md +22 -10
  12. package/.claude/skills/test-planner/SKILL.md +72 -0
  13. package/.cursor/agents/README.md +3 -1
  14. package/.cursor/agents/catalog-analyzer.md +83 -0
  15. package/.cursor/rules/safety-guardrails.mdc +1 -1
  16. package/.cursor/rules/workflow.mdc +52 -18
  17. package/.cursor/skills/android-customizer/SKILL.md +43 -19
  18. package/.cursor/skills/bug-fixer/SKILL.md +189 -0
  19. package/.cursor/skills/catalog-analyzer/SKILL.md +222 -0
  20. package/.cursor/skills/customization-planner/SKILL.md +55 -8
  21. package/.cursor/skills/design-selector/SKILL.md +6 -5
  22. package/.cursor/skills/design-system/SKILL.md +8 -7
  23. package/.cursor/skills/exploratory-tester/SKILL.md +223 -0
  24. package/.cursor/skills/ios-customizer/SKILL.md +47 -12
  25. package/.cursor/skills/module-integrator/SKILL.md +2 -2
  26. package/.cursor/skills/output-validator/SKILL.md +1 -1
  27. package/.cursor/skills/react-native-customizer/SKILL.md +46 -16
  28. package/.cursor/skills/test-planner/SKILL.md +199 -0
  29. package/.cursor/skills/web-analyzer/SKILL.md +310 -0
  30. package/.cursor/skills/web-crawler/SKILL.md +252 -0
  31. package/AGENTS.md +32 -11
  32. package/CLAUDE.md +78 -33
  33. package/README.md +77 -11
  34. package/designs/DESIGN_CATALOG.md +17 -15
  35. package/designs/DESIGN_PRINCIPLES.md +53 -0
  36. package/designs/brands/accessible-high-contrast.md +14 -0
  37. package/designs/brands/corporate-professional.md +14 -0
  38. package/designs/brands/dark-luxe.md +14 -0
  39. package/designs/brands/kids-playful.md +14 -0
  40. package/designs/brands/medical-clinical.md +14 -0
  41. package/designs/brands/modern-minimal.md +14 -0
  42. package/designs/brands/nature-organic.md +14 -0
  43. package/designs/brands/neo-brutalist.md +14 -0
  44. package/designs/brands/retro-vintage.md +14 -0
  45. package/designs/brands/soft-gradient.md +14 -0
  46. package/designs/brands/sport-athletic.md +14 -0
  47. package/designs/brands/tech-dynamic.md +14 -0
  48. package/designs/brands/vibrant-playful.md +14 -0
  49. package/dist/cli.d.ts +4 -2
  50. package/dist/cli.d.ts.map +1 -1
  51. package/dist/cli.js +123 -11
  52. package/dist/cli.js.map +1 -1
  53. package/dist/config.d.ts +8 -1
  54. package/dist/config.d.ts.map +1 -1
  55. package/dist/config.js +6 -1
  56. package/dist/config.js.map +1 -1
  57. package/dist/engines/claude-engine.d.ts.map +1 -1
  58. package/dist/engines/claude-engine.js +16 -4
  59. package/dist/engines/claude-engine.js.map +1 -1
  60. package/dist/engines/types.d.ts +1 -1
  61. package/dist/engines/types.d.ts.map +1 -1
  62. package/dist/engines/types.js +31 -2
  63. package/dist/engines/types.js.map +1 -1
  64. package/dist/github.d.ts +3 -0
  65. package/dist/github.d.ts.map +1 -1
  66. package/dist/github.js +47 -4
  67. package/dist/github.js.map +1 -1
  68. package/dist/index.js +294 -16
  69. package/dist/index.js.map +1 -1
  70. package/dist/prompt-builder.d.ts +17 -1
  71. package/dist/prompt-builder.d.ts.map +1 -1
  72. package/dist/prompt-builder.js +272 -1
  73. package/dist/prompt-builder.js.map +1 -1
  74. package/dist/validator.d.ts +7 -2
  75. package/dist/validator.d.ts.map +1 -1
  76. package/dist/validator.js +61 -41
  77. package/dist/validator.js.map +1 -1
  78. package/dist/workspace.js +2 -2
  79. package/dist/workspace.js.map +1 -1
  80. package/package.json +2 -4
  81. package/prompts/agent-prompt.md +35 -18
  82. package/prompts/deep-test-agent-prompt.md +122 -0
  83. package/prompts/fix-agent-prompt.md +90 -0
  84. package/prompts/quick-agent-prompt.md +32 -2
  85. package/prompts/scratch-agent-prompt.md +5 -8
  86. package/prompts/web-clone-agent-prompt.md +179 -0
  87. package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  88. package/templates/android/ChatTemplate/app/src/main/kotlin/com/appship/chat/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  89. package/templates/android/ChatTemplate/app/src/main/kotlin/com/appship/chat/features/conversations/ConversationsScreen.kt +1 -1
  90. package/templates/android/DashTemplate/app/src/main/kotlin/com/appship/dash/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  91. package/templates/android/DashTemplate/app/src/main/kotlin/com/appship/dash/features/navigation/MainScreen.kt +1 -0
  92. package/templates/android/FamilyTemplate/app/src/main/java/com/appship/family/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  93. package/templates/android/FamilyTemplate/app/src/main/java/com/appship/family/features/navigation/MainNavigation.kt +5 -1
  94. package/templates/android/FinanceTemplate/app/src/main/kotlin/com/appship/finance/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  95. package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  96. package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/core/animation/MotionPreferencesScreen.kt +3 -3
  97. package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/features/navigation/Navigation.kt +1 -1
  98. package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/features/settings/SettingsScreen.kt +1 -1
  99. package/templates/android/HealthTemplate/app/src/main/kotlin/com/appship/health/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  100. package/templates/android/LearnTemplate/app/src/main/kotlin/com/appship/learn/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  101. package/templates/android/MapTemplate/app/src/main/kotlin/com/appship/map/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  102. package/templates/android/MediaTemplate/app/src/main/kotlin/com/appship/media/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  103. package/templates/android/MediaTemplate/app/src/main/kotlin/com/appship/media/features/settings/SettingsScreen.kt +3 -2
  104. package/templates/android/ReferenceTemplate/app/src/main/kotlin/com/appship/reference/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  105. package/templates/android/ReferenceTemplate/app/src/main/kotlin/com/appship/reference/features/settings/SettingsScreen.kt +1 -1
  106. package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  107. package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/features/cart/CartScreen.kt +3 -2
  108. package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  109. package/templates/android/Skeleton/tests/03_detail_screen.yaml +1 -1
  110. package/templates/android/Skeleton/tests/04_favorites.yaml +1 -1
  111. package/templates/android/Skeleton/tests/08_full_e2e.yaml +7 -1
  112. package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  113. package/templates/android/TaskTemplate/app/src/main/kotlin/com/appship/task/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  114. package/templates/android/TaskTemplate/app/src/main/kotlin/com/appship/task/features/settings/SettingsScreen.kt +3 -2
  115. package/templates/android/TrackTemplate/app/src/main/kotlin/com/appship/track/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  116. package/templates/ios/BookTemplate/BookTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  117. package/templates/ios/ChatTemplate/ChatTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  118. package/templates/ios/DashTemplate/DashTemplate/App/AppConfig.swift +1 -0
  119. package/templates/ios/DashTemplate/DashTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  120. package/templates/ios/DashTemplate/DashTemplate/Core/Strings.swift +13 -0
  121. package/templates/ios/DashTemplate/DashTemplate.xcodeproj/project.pbxproj +32 -20
  122. package/templates/ios/FamilyTemplate/FamilyTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  123. package/templates/ios/FinanceTemplate/FinanceTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  124. package/templates/ios/FinanceTemplate/FinanceTemplate/Core/Strings.swift +42 -0
  125. package/templates/ios/FinanceTemplate/FinanceTemplate.xcodeproj/project.pbxproj +36 -30
  126. package/templates/ios/GameTemplate/GameTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  127. package/templates/ios/HealthTemplate/HealthTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  128. package/templates/ios/LearnTemplate/LearnTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  129. package/templates/ios/MapTemplate/MapTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  130. package/templates/ios/MediaTemplate/MediaTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  131. package/templates/ios/ReferenceTemplate/ReferenceTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  132. package/templates/ios/ReferenceTemplate/ReferenceTemplate/Core/Strings.swift +12 -0
  133. package/templates/ios/ReferenceTemplate/ReferenceTemplate/Features/SkeletonLoading/SkeletonLoadingView.swift +2 -37
  134. package/templates/ios/ShopTemplate/ShopTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  135. package/templates/ios/Skeleton/Skeleton/Core/Animation/AnimatedTransitionsView.swift +201 -0
  136. package/templates/ios/Skeleton/tests/08_full_e2e.yaml +4 -0
  137. package/templates/ios/SocialTemplate/SocialTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  138. package/templates/ios/TaskTemplate/TaskTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  139. package/templates/ios/TrackTemplate/TrackTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  140. package/templates/react-native/BookTemplate/src/animation/useAnimatedList.ts +219 -2
  141. package/templates/react-native/BookTemplate/src/animation/useMotionPreferences.ts +23 -9
  142. package/templates/react-native/BookTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  143. package/templates/react-native/ChatTemplate/src/animation/useAnimatedList.ts +219 -2
  144. package/templates/react-native/ChatTemplate/src/animation/useMotionPreferences.ts +23 -9
  145. package/templates/react-native/ChatTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  146. package/templates/react-native/DashTemplate/src/animation/useAnimatedList.ts +219 -2
  147. package/templates/react-native/DashTemplate/src/animation/useMotionPreferences.ts +23 -9
  148. package/templates/react-native/DashTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  149. package/templates/react-native/FamilyTemplate/src/animation/useAnimatedList.ts +219 -2
  150. package/templates/react-native/FamilyTemplate/src/animation/useMotionPreferences.ts +23 -9
  151. package/templates/react-native/FamilyTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  152. package/templates/react-native/FinanceTemplate/src/animation/useAnimatedList.ts +219 -2
  153. package/templates/react-native/FinanceTemplate/src/animation/useMotionPreferences.ts +23 -9
  154. package/templates/react-native/FinanceTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  155. package/templates/react-native/GameTemplate/src/animation/useAnimatedList.ts +219 -2
  156. package/templates/react-native/GameTemplate/src/animation/useMotionPreferences.ts +23 -9
  157. package/templates/react-native/GameTemplate/src/screens/GameDetail/GameDetailScreen.tsx +2 -1
  158. package/templates/react-native/GameTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  159. package/templates/react-native/HealthTemplate/src/animation/useAnimatedList.ts +219 -2
  160. package/templates/react-native/HealthTemplate/src/animation/useMotionPreferences.ts +23 -9
  161. package/templates/react-native/HealthTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  162. package/templates/react-native/HealthTemplate/src/screens/WorkoutDetail/WorkoutDetailScreen.tsx +1 -1
  163. package/templates/react-native/LearnTemplate/src/animation/useAnimatedList.ts +219 -2
  164. package/templates/react-native/LearnTemplate/src/animation/useMotionPreferences.ts +23 -9
  165. package/templates/react-native/LearnTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  166. package/templates/react-native/MapTemplate/src/animation/useAnimatedList.ts +219 -2
  167. package/templates/react-native/MapTemplate/src/animation/useMotionPreferences.ts +23 -9
  168. package/templates/react-native/MapTemplate/src/screens/Map/MapScreen.tsx +14 -0
  169. package/templates/react-native/MapTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  170. package/templates/react-native/MediaTemplate/src/animation/useAnimatedList.ts +219 -2
  171. package/templates/react-native/MediaTemplate/src/animation/useMotionPreferences.ts +23 -9
  172. package/templates/react-native/MediaTemplate/src/screens/PlaylistDetail/PlaylistDetailScreen.tsx +1 -1
  173. package/templates/react-native/MediaTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  174. package/templates/react-native/ReferenceTemplate/src/animation/useAnimatedList.ts +219 -2
  175. package/templates/react-native/ReferenceTemplate/src/animation/useMotionPreferences.ts +23 -9
  176. package/templates/react-native/ReferenceTemplate/src/screens/Settings/SettingsScreen.tsx +1 -1
  177. package/templates/react-native/ShopTemplate/src/animation/useAnimatedList.ts +219 -2
  178. package/templates/react-native/ShopTemplate/src/animation/useMotionPreferences.ts +23 -9
  179. package/templates/react-native/ShopTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  180. package/templates/react-native/Skeleton/TESTING_MANIFEST.md +1 -1
  181. package/templates/react-native/Skeleton/src/animation/useAnimatedList.ts +219 -2
  182. package/templates/react-native/Skeleton/src/animation/useMotionPreferences.ts +23 -9
  183. package/templates/react-native/Skeleton/src/screens/Profile/ProfileScreen.tsx +1 -1
  184. package/templates/react-native/Skeleton/tests/07_profile.yaml +3 -2
  185. package/templates/react-native/Skeleton/tests/08_full_e2e.yaml +12 -1
  186. package/templates/react-native/SocialTemplate/src/animation/useAnimatedList.ts +219 -2
  187. package/templates/react-native/SocialTemplate/src/animation/useMotionPreferences.ts +23 -9
  188. package/templates/react-native/SocialTemplate/src/screens/Feed/FeedScreen.tsx +1 -0
  189. package/templates/react-native/SocialTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  190. package/templates/react-native/TaskTemplate/src/animation/useAnimatedList.ts +219 -2
  191. package/templates/react-native/TaskTemplate/src/animation/useMotionPreferences.ts +23 -9
  192. package/templates/react-native/TaskTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  193. package/templates/react-native/TrackTemplate/src/animation/useAnimatedList.ts +219 -2
  194. package/templates/react-native/TrackTemplate/src/animation/useMotionPreferences.ts +23 -9
  195. package/templates/react-native/TrackTemplate/src/screens/Settings/SettingsScreen.tsx +1 -1
  196. package/templates/shared/ios/AnimatedTransitions/AnimatedTransitionsView.swift +233 -93
  197. package/.claude/agents/template-selector.md +0 -39
  198. package/.claude/skills/module-selector/SKILL.md +0 -81
  199. package/.claude/skills/template-selector/SKILL.md +0 -44
  200. package/.cursor/agents/template-selector.md +0 -52
  201. package/.cursor/skills/module-selector/SKILL.md +0 -135
  202. 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
- AsyncStorage.getItem(INTENSITY_STORAGE_KEY).then((stored: string | null) => {
30
- if (stored === 'full' || stored === 'reduced' || stored === 'off') {
31
- setIntensityState(stored);
32
- }
33
- }).catch(() => {});
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
- await AsyncStorage.setItem(INTENSITY_STORAGE_KEY, intensity);
64
+ if (_asyncStorage) {
65
+ await _asyncStorage.setItem(INTENSITY_STORAGE_KEY, intensity);
66
+ }
55
67
  setIntensityState(intensity);
56
68
  } catch (error) {
57
- console.error('Failed to save motion preference:', error);
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' as any)}>
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: - Card Flip Modifier
4
- /// Applies a 3D card flip transition using rotation around the Y-axis.
5
- struct CardFlipModifier: ViewModifier {
6
- @Binding var isFlipped: Bool
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
- .rotation3DEffect(
15
- .degrees(angle),
16
- axis: (x: 0, y: 1, z: 0),
17
- perspective: 0.5
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
- .motionAware(animation: animation)
20
- .accessibilityIdentifier("animated_transitions_card_flip")
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 horizontal or vertical slide with opacity fade transition.
91
+ /// Applies a slide-from-side + fade-in animation on appear.
26
92
  struct SlideAndFadeModifier: ViewModifier {
27
- let isVisible: Bool
28
- let direction: SlideDirection
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
- .offset(x: horizontalOffset, y: verticalOffset)
40
- .opacity(opacity)
41
- .motionAware(animation: animation)
42
- .accessibilityIdentifier("animated_transitions_slide_fade_\(direction.rawValue)")
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: - Zoom Transition Modifier
47
- /// Applies scale zoom transition with opacity fade.
48
- struct ZoomTransitionModifier: ViewModifier {
49
- let isVisible: Bool
50
- let config: TransitionConfig
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
- .scaleEffect(scale)
60
- .opacity(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: - Cross Dissolve Modifier
67
- /// Applies cross-fade transition between two views.
68
- struct CrossDissolveModifier: ViewModifier {
69
- let isShowingFirst: Bool
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(opacity)
78
- .motionAware(animation: animation)
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: - Matched Transition View
84
- /// Wrapper view for matchedGeometryEffect hero transitions.
85
- struct MatchedTransitionView<Content: View>: View {
86
- let id: String
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
- init(id: String, namespace: Namespace.ID, @ViewBuilder content: () -> Content) {
91
- self.id = id
92
- self.namespace = namespace
93
- self.content = content()
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
- var body: some View {
174
+ func body(content: Content) -> some View {
97
175
  content
98
- .matchedGeometryEffect(id: id, in: namespace)
99
- .motionAware()
100
- .accessibilityIdentifier("animated_transitions_matched_geometry_\(id)")
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 a card flip transition with 3D rotation around the Y-axis.
212
+ /// Applies staggered appear animation for list items.
108
213
  /// - Parameters:
109
- /// - isFlipped: Binding to control flip state (false = front, true = back)
110
- /// - config: Transition configuration (default: .default)
111
- /// - Returns: A view with card flip transition applied
112
- func cardFlip(isFlipped: Binding<Bool>, config: TransitionConfig = .default) -> some View {
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 a slide and fade transition.
117
- /// - Parameters:
118
- /// - isVisible: Whether the view should be visible
119
- /// - direction: Slide direction (leading, trailing, top, bottom)
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 a zoom transition with scale and opacity.
127
- /// - Parameters:
128
- /// - isVisible: Whether the view should be visible
129
- /// - config: Transition configuration (default: .default)
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 a cross-dissolve transition between two views.
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
- /// - isShowingFirst: Whether to show the first view (true) or second (false)
138
- /// - config: Transition configuration (default: .default)
139
- /// - Returns: A view with cross-dissolve transition applied
140
- func crossDissolve(isShowingFirst: Bool, config: TransitionConfig = .default) -> some View {
141
- modifier(CrossDissolveModifier(isShowingFirst: isShowingFirst, config: config))
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
  }