@shaykec/app-agent 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/.claude/agents/android-customizer.md +9 -1
  2. package/.claude/agents/catalog-analyzer.md +57 -0
  3. package/.claude/agents/ios-customizer.md +9 -1
  4. package/.claude/agents/react-native-customizer.md +71 -0
  5. package/.claude/skills/android-customizer/SKILL.md +108 -23
  6. package/.claude/skills/bug-fixer/SKILL.md +59 -0
  7. package/.claude/skills/catalog-analyzer/SKILL.md +96 -0
  8. package/.claude/skills/customization-planner/SKILL.md +44 -5
  9. package/.claude/skills/design-selector/SKILL.md +3 -1
  10. package/.claude/skills/design-system/SKILL.md +1 -1
  11. package/.claude/skills/exploratory-tester/SKILL.md +82 -0
  12. package/.claude/skills/ios-customizer/SKILL.md +123 -23
  13. package/.claude/skills/module-integrator/SKILL.md +1 -1
  14. package/.claude/skills/react-native-customizer/SKILL.md +97 -11
  15. package/.claude/skills/test-planner/SKILL.md +72 -0
  16. package/.cursor/agents/README.md +3 -1
  17. package/.cursor/agents/android-customizer.md +15 -11
  18. package/.cursor/agents/catalog-analyzer.md +83 -0
  19. package/.cursor/agents/ios-customizer.md +15 -10
  20. package/.cursor/agents/react-native-customizer.md +170 -0
  21. package/.cursor/mcp.json +2 -10
  22. package/.cursor/rules/safety-guardrails.mdc +1 -1
  23. package/.cursor/rules/workflow.mdc +52 -18
  24. package/.cursor/skills/android-customizer/SKILL.md +46 -22
  25. package/.cursor/skills/bug-fixer/SKILL.md +189 -0
  26. package/.cursor/skills/catalog-analyzer/SKILL.md +222 -0
  27. package/.cursor/skills/customization-planner/SKILL.md +55 -8
  28. package/.cursor/skills/design-selector/SKILL.md +6 -5
  29. package/.cursor/skills/design-system/SKILL.md +8 -7
  30. package/.cursor/skills/exploratory-tester/SKILL.md +223 -0
  31. package/.cursor/skills/ios-customizer/SKILL.md +50 -15
  32. package/.cursor/skills/module-integrator/SKILL.md +2 -2
  33. package/.cursor/skills/output-validator/SKILL.md +1 -1
  34. package/.cursor/skills/react-native-customizer/SKILL.md +115 -25
  35. package/.cursor/skills/test-planner/SKILL.md +199 -0
  36. package/AGENTS.md +32 -11
  37. package/CLAUDE.md +78 -33
  38. package/README.md +77 -11
  39. package/designs/DESIGN_CATALOG.md +17 -15
  40. package/designs/DESIGN_PRINCIPLES.md +53 -0
  41. package/designs/brands/accessible-high-contrast.md +14 -0
  42. package/designs/brands/corporate-professional.md +14 -0
  43. package/designs/brands/dark-luxe.md +14 -0
  44. package/designs/brands/kids-playful.md +14 -0
  45. package/designs/brands/medical-clinical.md +14 -0
  46. package/designs/brands/modern-minimal.md +14 -0
  47. package/designs/brands/nature-organic.md +14 -0
  48. package/designs/brands/neo-brutalist.md +14 -0
  49. package/designs/brands/retro-vintage.md +14 -0
  50. package/designs/brands/soft-gradient.md +14 -0
  51. package/designs/brands/sport-athletic.md +14 -0
  52. package/designs/brands/tech-dynamic.md +14 -0
  53. package/designs/brands/vibrant-playful.md +14 -0
  54. package/dist/cli.d.ts +4 -2
  55. package/dist/cli.d.ts.map +1 -1
  56. package/dist/cli.js +91 -1
  57. package/dist/cli.js.map +1 -1
  58. package/dist/config.d.ts +2 -0
  59. package/dist/config.d.ts.map +1 -1
  60. package/dist/config.js +2 -0
  61. package/dist/config.js.map +1 -1
  62. package/dist/engines/claude-engine.d.ts.map +1 -1
  63. package/dist/engines/claude-engine.js +16 -4
  64. package/dist/engines/claude-engine.js.map +1 -1
  65. package/dist/engines/types.d.ts +1 -1
  66. package/dist/engines/types.d.ts.map +1 -1
  67. package/dist/engines/types.js +31 -2
  68. package/dist/engines/types.js.map +1 -1
  69. package/dist/github.d.ts +3 -0
  70. package/dist/github.d.ts.map +1 -1
  71. package/dist/github.js +47 -4
  72. package/dist/github.js.map +1 -1
  73. package/dist/index.js +217 -9
  74. package/dist/index.js.map +1 -1
  75. package/dist/prompt-builder.d.ts +11 -1
  76. package/dist/prompt-builder.d.ts.map +1 -1
  77. package/dist/prompt-builder.js +216 -1
  78. package/dist/prompt-builder.js.map +1 -1
  79. package/dist/validator.d.ts +7 -2
  80. package/dist/validator.d.ts.map +1 -1
  81. package/dist/validator.js +61 -41
  82. package/dist/validator.js.map +1 -1
  83. package/dist/workspace.js +2 -2
  84. package/dist/workspace.js.map +1 -1
  85. package/package.json +2 -2
  86. package/prompts/agent-prompt.md +35 -18
  87. package/prompts/deep-test-agent-prompt.md +122 -0
  88. package/prompts/fix-agent-prompt.md +90 -0
  89. package/prompts/quick-agent-prompt.md +32 -2
  90. package/prompts/scratch-agent-prompt.md +5 -8
  91. package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  92. package/templates/android/ChatTemplate/app/src/main/kotlin/com/appship/chat/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  93. package/templates/android/ChatTemplate/app/src/main/kotlin/com/appship/chat/features/conversations/ConversationsScreen.kt +1 -1
  94. package/templates/android/DashTemplate/app/src/main/kotlin/com/appship/dash/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  95. package/templates/android/DashTemplate/app/src/main/kotlin/com/appship/dash/features/navigation/MainScreen.kt +1 -0
  96. package/templates/android/FamilyTemplate/app/src/main/java/com/appship/family/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  97. package/templates/android/FamilyTemplate/app/src/main/java/com/appship/family/features/navigation/MainNavigation.kt +5 -1
  98. package/templates/android/FinanceTemplate/app/src/main/kotlin/com/appship/finance/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  99. package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  100. package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/core/animation/MotionPreferencesScreen.kt +3 -3
  101. package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/features/navigation/Navigation.kt +1 -1
  102. package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/features/settings/SettingsScreen.kt +1 -1
  103. package/templates/android/HealthTemplate/app/src/main/kotlin/com/appship/health/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  104. package/templates/android/LearnTemplate/app/src/main/kotlin/com/appship/learn/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  105. package/templates/android/MapTemplate/app/src/main/kotlin/com/appship/map/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  106. package/templates/android/MediaTemplate/app/src/main/kotlin/com/appship/media/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  107. package/templates/android/MediaTemplate/app/src/main/kotlin/com/appship/media/features/settings/SettingsScreen.kt +3 -2
  108. package/templates/android/ReferenceTemplate/app/src/main/kotlin/com/appship/reference/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  109. package/templates/android/ReferenceTemplate/app/src/main/kotlin/com/appship/reference/features/settings/SettingsScreen.kt +1 -1
  110. package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  111. package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/features/cart/CartScreen.kt +3 -2
  112. package/templates/android/Skeleton/TESTING_MANIFEST.md +2 -1
  113. package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/MainActivity.kt +23 -2
  114. package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  115. package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/core/theme/AppearanceManager.kt +42 -0
  116. package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/profile/ProfileScreen.kt +20 -8
  117. package/templates/android/Skeleton/tests/03_detail_screen.yaml +3 -2
  118. package/templates/android/Skeleton/tests/04_favorites.yaml +3 -2
  119. package/templates/android/Skeleton/tests/08_full_e2e.yaml +9 -2
  120. package/templates/android/Skeleton/tests/09_dark_mode.yaml +50 -0
  121. package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  122. package/templates/android/TaskTemplate/app/src/main/kotlin/com/appship/task/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  123. package/templates/android/TaskTemplate/app/src/main/kotlin/com/appship/task/features/settings/SettingsScreen.kt +3 -2
  124. package/templates/android/TrackTemplate/app/src/main/kotlin/com/appship/track/core/animation/AnimatedTransitionsModifiers.kt +188 -0
  125. package/templates/ios/BookTemplate/BookTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  126. package/templates/ios/ChatTemplate/ChatTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  127. package/templates/ios/DashTemplate/DashTemplate/App/AppConfig.swift +1 -0
  128. package/templates/ios/DashTemplate/DashTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  129. package/templates/ios/DashTemplate/DashTemplate/Core/Strings.swift +13 -0
  130. package/templates/ios/DashTemplate/DashTemplate.xcodeproj/project.pbxproj +32 -20
  131. package/templates/ios/FamilyTemplate/FamilyTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  132. package/templates/ios/FinanceTemplate/FinanceTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  133. package/templates/ios/FinanceTemplate/FinanceTemplate/Core/Strings.swift +42 -0
  134. package/templates/ios/FinanceTemplate/FinanceTemplate.xcodeproj/project.pbxproj +36 -30
  135. package/templates/ios/GameTemplate/GameTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  136. package/templates/ios/HealthTemplate/HealthTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  137. package/templates/ios/LearnTemplate/LearnTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  138. package/templates/ios/MapTemplate/MapTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  139. package/templates/ios/MediaTemplate/MediaTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  140. package/templates/ios/ReferenceTemplate/ReferenceTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  141. package/templates/ios/ReferenceTemplate/ReferenceTemplate/Core/Strings.swift +12 -0
  142. package/templates/ios/ReferenceTemplate/ReferenceTemplate/Features/SkeletonLoading/SkeletonLoadingView.swift +2 -37
  143. package/templates/ios/ShopTemplate/ShopTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  144. package/templates/ios/Skeleton/Skeleton/Core/Animation/AnimatedTransitionsView.swift +201 -0
  145. package/templates/ios/Skeleton/tests/08_full_e2e.yaml +4 -0
  146. package/templates/ios/Skeleton/tests/09_dark_mode.yaml +52 -0
  147. package/templates/ios/SocialTemplate/SocialTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  148. package/templates/ios/TaskTemplate/TaskTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  149. package/templates/ios/TrackTemplate/TrackTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
  150. package/templates/react-native/BookTemplate/src/animation/useAnimatedList.ts +219 -2
  151. package/templates/react-native/BookTemplate/src/animation/useMotionPreferences.ts +23 -9
  152. package/templates/react-native/BookTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  153. package/templates/react-native/ChatTemplate/src/animation/useAnimatedList.ts +219 -2
  154. package/templates/react-native/ChatTemplate/src/animation/useMotionPreferences.ts +23 -9
  155. package/templates/react-native/ChatTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  156. package/templates/react-native/DashTemplate/src/animation/useAnimatedList.ts +219 -2
  157. package/templates/react-native/DashTemplate/src/animation/useMotionPreferences.ts +23 -9
  158. package/templates/react-native/DashTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  159. package/templates/react-native/FamilyTemplate/src/animation/useAnimatedList.ts +219 -2
  160. package/templates/react-native/FamilyTemplate/src/animation/useMotionPreferences.ts +23 -9
  161. package/templates/react-native/FamilyTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  162. package/templates/react-native/FinanceTemplate/src/animation/useAnimatedList.ts +219 -2
  163. package/templates/react-native/FinanceTemplate/src/animation/useMotionPreferences.ts +23 -9
  164. package/templates/react-native/FinanceTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  165. package/templates/react-native/GameTemplate/src/animation/useAnimatedList.ts +219 -2
  166. package/templates/react-native/GameTemplate/src/animation/useMotionPreferences.ts +23 -9
  167. package/templates/react-native/GameTemplate/src/screens/GameDetail/GameDetailScreen.tsx +2 -1
  168. package/templates/react-native/GameTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  169. package/templates/react-native/HealthTemplate/src/animation/useAnimatedList.ts +219 -2
  170. package/templates/react-native/HealthTemplate/src/animation/useMotionPreferences.ts +23 -9
  171. package/templates/react-native/HealthTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  172. package/templates/react-native/HealthTemplate/src/screens/WorkoutDetail/WorkoutDetailScreen.tsx +1 -1
  173. package/templates/react-native/LearnTemplate/src/animation/useAnimatedList.ts +219 -2
  174. package/templates/react-native/LearnTemplate/src/animation/useMotionPreferences.ts +23 -9
  175. package/templates/react-native/LearnTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  176. package/templates/react-native/MapTemplate/src/animation/useAnimatedList.ts +219 -2
  177. package/templates/react-native/MapTemplate/src/animation/useMotionPreferences.ts +23 -9
  178. package/templates/react-native/MapTemplate/src/screens/Map/MapScreen.tsx +14 -0
  179. package/templates/react-native/MapTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  180. package/templates/react-native/MediaTemplate/src/animation/useAnimatedList.ts +219 -2
  181. package/templates/react-native/MediaTemplate/src/animation/useMotionPreferences.ts +23 -9
  182. package/templates/react-native/MediaTemplate/src/screens/PlaylistDetail/PlaylistDetailScreen.tsx +1 -1
  183. package/templates/react-native/MediaTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  184. package/templates/react-native/ReferenceTemplate/src/animation/useAnimatedList.ts +219 -2
  185. package/templates/react-native/ReferenceTemplate/src/animation/useMotionPreferences.ts +23 -9
  186. package/templates/react-native/ReferenceTemplate/src/screens/Settings/SettingsScreen.tsx +1 -1
  187. package/templates/react-native/ShopTemplate/src/animation/useAnimatedList.ts +219 -2
  188. package/templates/react-native/ShopTemplate/src/animation/useMotionPreferences.ts +23 -9
  189. package/templates/react-native/ShopTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  190. package/templates/react-native/Skeleton/TESTING_MANIFEST.md +2 -1
  191. package/templates/react-native/Skeleton/src/animation/useAnimatedList.ts +219 -2
  192. package/templates/react-native/Skeleton/src/animation/useMotionPreferences.ts +23 -9
  193. package/templates/react-native/Skeleton/src/screens/Profile/ProfileScreen.tsx +1 -1
  194. package/templates/react-native/Skeleton/tests/07_profile.yaml +3 -2
  195. package/templates/react-native/Skeleton/tests/08_full_e2e.yaml +12 -1
  196. package/templates/react-native/Skeleton/tests/09_dark_mode.yaml +46 -0
  197. package/templates/react-native/SocialTemplate/src/animation/useAnimatedList.ts +219 -2
  198. package/templates/react-native/SocialTemplate/src/animation/useMotionPreferences.ts +23 -9
  199. package/templates/react-native/SocialTemplate/src/screens/Feed/FeedScreen.tsx +1 -0
  200. package/templates/react-native/SocialTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  201. package/templates/react-native/TaskTemplate/src/animation/useAnimatedList.ts +219 -2
  202. package/templates/react-native/TaskTemplate/src/animation/useMotionPreferences.ts +23 -9
  203. package/templates/react-native/TaskTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
  204. package/templates/react-native/TrackTemplate/src/animation/useAnimatedList.ts +219 -2
  205. package/templates/react-native/TrackTemplate/src/animation/useMotionPreferences.ts +23 -9
  206. package/templates/react-native/TrackTemplate/src/screens/Settings/SettingsScreen.tsx +1 -1
  207. package/templates/shared/ios/AnimatedTransitions/AnimatedTransitionsView.swift +233 -93
  208. package/.claude/agents/template-selector.md +0 -39
  209. package/.claude/skills/module-selector/SKILL.md +0 -81
  210. package/.claude/skills/template-selector/SKILL.md +0 -44
  211. package/.cursor/agents/template-selector.md +0 -52
  212. package/.cursor/skills/module-selector/SKILL.md +0 -135
  213. package/.cursor/skills/template-selector/SKILL.md +0 -123
@@ -0,0 +1,46 @@
1
+ appId: ${APP_BUNDLE_ID}
2
+ ---
3
+ - launchApp
4
+ - extendedWaitUntil:
5
+ visible: "Home"
6
+ # Navigate to Profile
7
+ - tapOn:
8
+ id: "tab_bar_profile"
9
+ retryTapIfNoChange: true
10
+ - extendedWaitUntil:
11
+ visible:
12
+ id: "profile_avatar"
13
+ timeout: 2000
14
+ # Verify dark mode toggle is visible
15
+ - assertVisible:
16
+ id: "profile_appearance_toggle"
17
+ # Toggle dark mode ON
18
+ - tapOn:
19
+ id: "profile_appearance_toggle"
20
+ # Verify profile still renders
21
+ - assertVisible:
22
+ id: "profile_avatar"
23
+ - assertVisible:
24
+ id: "profile_name"
25
+ # Navigate to Home to verify dark mode persists across tabs
26
+ - tapOn:
27
+ id: "tab_bar_home"
28
+ retryTapIfNoChange: true
29
+ - extendedWaitUntil:
30
+ visible: "Home"
31
+ # Navigate back to Profile
32
+ - tapOn:
33
+ id: "tab_bar_profile"
34
+ retryTapIfNoChange: true
35
+ - extendedWaitUntil:
36
+ visible:
37
+ id: "profile_avatar"
38
+ timeout: 2000
39
+ # Toggle dark mode OFF
40
+ - tapOn:
41
+ id: "profile_appearance_toggle"
42
+ # Verify profile renders normally
43
+ - assertVisible:
44
+ id: "profile_avatar"
45
+ - assertVisible:
46
+ id: "profile_name"
@@ -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
 
@@ -111,6 +111,7 @@ const FeedScreen: React.FC = () => {
111
111
  </Text>
112
112
  </TouchableOpacity>
113
113
  </View>
114
+ </View>
114
115
  </Animated.View>
115
116
  );
116
117
  }, [colors, spacing, borderRadius, typography, toggleLike, navigateToPostDetail, navigateToUserProfile]);
@@ -138,7 +138,7 @@ const ProfileScreen: React.FC = () => {
138
138
  accessibilityRole="button"
139
139
  accessibilityLabel="Animations settings"
140
140
  style={[styles.settingsRow, {borderBottomColor: colors.border}]}
141
- onPress={() => navigation.navigate('MotionPreferences' as any)}>
141
+ onPress={() => (navigation as any).navigate('MotionPreferences')}>
142
142
  <Text style={[styles.settingsLabel, {color: colors.primaryText, fontSize: typography.body}]}>
143
143
  Animations
144
144
  </Text>
@@ -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
 
@@ -58,7 +58,7 @@ const ProfileScreen: React.FC = () => {
58
58
  accessibilityRole="button"
59
59
  accessibilityLabel="Animations settings"
60
60
  style={styles.row}
61
- onPress={() => navigation.navigate('MotionPreferences' as any)}>
61
+ onPress={() => (navigation as any).navigate('MotionPreferences')}>
62
62
  <Text style={[styles.rowLabel, {color: colors.primaryText, fontSize: typography.body}]}>
63
63
  Animations
64
64
  </Text>