vibefast-cli 1.2.1 → 1.3.1

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 (348) hide show
  1. package/README.md +30 -95
  2. package/dist/__tests__/recipes.test.js +94 -91
  3. package/dist/__tests__/recipes.test.js.map +1 -1
  4. package/dist/commands/add.d.ts.map +1 -1
  5. package/dist/commands/add.js +301 -125
  6. package/dist/commands/add.js.map +1 -1
  7. package/dist/commands/checklist.d.ts.map +1 -1
  8. package/dist/commands/checklist.js +85 -44
  9. package/dist/commands/checklist.js.map +1 -1
  10. package/dist/commands/health.d.ts.map +1 -1
  11. package/dist/commands/health.js +13 -4
  12. package/dist/commands/health.js.map +1 -1
  13. package/dist/commands/init.d.ts.map +1 -1
  14. package/dist/commands/init.js +118 -26
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/migrate.d.ts +3 -0
  17. package/dist/commands/migrate.d.ts.map +1 -0
  18. package/dist/commands/migrate.js +202 -0
  19. package/dist/commands/migrate.js.map +1 -0
  20. package/dist/commands/remove.d.ts.map +1 -1
  21. package/dist/commands/remove.js +61 -3
  22. package/dist/commands/remove.js.map +1 -1
  23. package/dist/core/auth.d.ts.map +1 -1
  24. package/dist/core/auth.js +20 -18
  25. package/dist/core/auth.js.map +1 -1
  26. package/dist/core/codemod.d.ts +33 -0
  27. package/dist/core/codemod.d.ts.map +1 -1
  28. package/dist/core/codemod.js +116 -0
  29. package/dist/core/codemod.js.map +1 -1
  30. package/dist/core/detect.d.ts.map +1 -1
  31. package/dist/core/detect.js +24 -7
  32. package/dist/core/detect.js.map +1 -1
  33. package/dist/core/journal.d.ts +1 -0
  34. package/dist/core/journal.d.ts.map +1 -1
  35. package/dist/core/journal.js.map +1 -1
  36. package/dist/core/recipes.d.ts.map +1 -1
  37. package/dist/core/recipes.js +25 -7
  38. package/dist/core/recipes.js.map +1 -1
  39. package/dist/index.js +2 -2
  40. package/dist/index.js.map +1 -1
  41. package/docs/architecture.md +50 -0
  42. package/docs/commands.md +78 -0
  43. package/docs/contributing.md +27 -0
  44. package/docs/quickstart.md +50 -0
  45. package/docs/recipes.md +57 -0
  46. package/docs/troubleshooting.md +31 -0
  47. package/package.json +2 -2
  48. package/recipes/0/apps/native/src/components/advanced-ui/timeline/demo.tsx +445 -0
  49. package/recipes/0/apps/native/src/components/advanced-ui/timeline/timeline-view.tsx +355 -0
  50. package/recipes/0/apps/native/src/components/advanced-ui/timeline/types.ts +31 -0
  51. package/recipes/0/recipe.json +18 -0
  52. package/recipes/animated-chip/apps/native/src/components/advanced-ui/chip/demo.tsx +2 -1
  53. package/recipes/animated-chip/recipe.json +5 -2
  54. package/recipes/animated-chip-native@latest.zip +0 -0
  55. package/recipes/animated-chip@latest.zip +0 -0
  56. package/recipes/animated-switch/apps/native/src/components/advanced-ui/switch/demo.tsx +1 -1
  57. package/recipes/animated-switch/recipe.json +5 -2
  58. package/recipes/animated-switch-native@latest.zip +0 -0
  59. package/recipes/animated-switch@latest.zip +0 -0
  60. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +2 -1
  61. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +2 -2
  62. package/recipes/audio-recorder/recipe.json +7 -2
  63. package/recipes/audio-recorder-native@latest.zip +0 -0
  64. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +2 -1
  65. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +2 -1
  66. package/recipes/audio-recorder-supabase/recipe.json +12 -16
  67. package/recipes/audio-recorder-supabase-native@latest.zip +0 -0
  68. package/recipes/audio-recorder-supabase@latest.zip +0 -0
  69. package/recipes/audio-recorder@latest.zip +0 -0
  70. package/recipes/charts/apps/native/src/app/charts/index.tsx +3 -0
  71. package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +3 -1
  72. package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +3 -1
  73. package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +3 -1
  74. package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +3 -1
  75. package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +3 -1
  76. package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +3 -1
  77. package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +3 -1
  78. package/recipes/charts/recipe.json +13 -4
  79. package/recipes/charts-native@latest.zip +0 -0
  80. package/recipes/charts@latest.zip +0 -0
  81. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +86 -86
  82. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +4 -4
  83. package/recipes/chatbot/recipe.json +3 -40
  84. package/recipes/chatbot-native@latest.zip +0 -0
  85. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-markdown.tsx +4 -1
  86. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/code-block.tsx +86 -53
  87. package/recipes/chatbot-supabase/recipe.json +3 -42
  88. package/recipes/chatbot-supabase-native@latest.zip +0 -0
  89. package/recipes/chatbot-supabase@latest.zip +0 -0
  90. package/recipes/chatbot@latest.zip +0 -0
  91. package/recipes/glowing-button/recipe.json +6 -2
  92. package/recipes/glowing-button-native@latest.zip +0 -0
  93. package/recipes/glowing-button@latest.zip +0 -0
  94. package/recipes/image-analysis/apps/native/src/app/analysis/[type]/_layout.tsx +5 -0
  95. package/recipes/image-analysis/apps/native/src/app/analysis/[type]/analysis-options.tsx +50 -0
  96. package/recipes/image-analysis/apps/native/src/app/analysis/[type]/camera.tsx +2 -0
  97. package/recipes/image-analysis/apps/native/src/app/analysis/[type]/index.tsx +50 -0
  98. package/recipes/image-analysis/apps/native/src/app/analysis/[type]/loading.tsx +50 -0
  99. package/recipes/image-analysis/apps/native/src/app/analysis/[type]/results.tsx +2 -0
  100. package/recipes/image-analysis/apps/native/src/app/analysis/[type]/trait-details.tsx +3 -0
  101. package/recipes/image-analysis/apps/native/src/features/image-analyzer/app/analysis-options-screen.tsx +2 -2
  102. package/recipes/image-analysis/apps/native/src/features/image-analyzer/app/camera.tsx +72 -65
  103. package/recipes/image-analysis/apps/native/src/features/image-analyzer/app/image-capture-screen.tsx +65 -47
  104. package/recipes/image-analysis/apps/native/src/features/image-analyzer/app/loading-screen.tsx +43 -2
  105. package/recipes/image-analysis/apps/native/src/features/image-analyzer/app/loading.tsx +34 -1
  106. package/recipes/image-analysis/apps/native/src/features/image-analyzer/hooks/use-image-analysis.ts +83 -2
  107. package/recipes/image-analysis/recipe.json +11 -19
  108. package/recipes/image-analysis-native@latest.zip +0 -0
  109. package/recipes/image-analysis-supabase/apps/native/src/app/analysis/[type]/_layout.tsx +5 -0
  110. package/recipes/image-analysis-supabase/apps/native/src/app/analysis/[type]/analysis-options.tsx +50 -0
  111. package/recipes/image-analysis-supabase/apps/native/src/app/analysis/[type]/camera.tsx +2 -0
  112. package/recipes/image-analysis-supabase/apps/native/src/app/analysis/[type]/index.tsx +50 -0
  113. package/recipes/image-analysis-supabase/apps/native/src/app/analysis/[type]/loading.tsx +50 -0
  114. package/recipes/image-analysis-supabase/apps/native/src/app/analysis/[type]/results.tsx +2 -0
  115. package/recipes/image-analysis-supabase/apps/native/src/app/analysis/[type]/trait-details.tsx +3 -0
  116. package/recipes/image-analysis-supabase/recipe.json +10 -37
  117. package/recipes/image-analysis-supabase-native@latest.zip +0 -0
  118. package/recipes/image-analysis-supabase@latest.zip +0 -0
  119. package/recipes/image-analysis@latest.zip +0 -0
  120. package/recipes/image-analyzer/apps/native/src/app/(root)/(protected)/image-analyzer/index.tsx +2 -0
  121. package/recipes/image-generator/apps/native/src/app/image-generator/gallery.tsx +3 -0
  122. package/recipes/image-generator/apps/native/src/app/image-generator/index.tsx +3 -0
  123. package/recipes/image-generator/recipe.json +8 -18
  124. package/recipes/image-generator-native@latest.zip +0 -0
  125. package/recipes/image-generator-supabase/recipe.json +6 -35
  126. package/recipes/image-generator-supabase-native@latest.zip +0 -0
  127. package/recipes/image-generator-supabase@latest.zip +0 -0
  128. package/recipes/image-generator@latest.zip +0 -0
  129. package/recipes/ios-widget/recipe.json +18 -119
  130. package/recipes/ios-widget-native@latest.zip +0 -0
  131. package/recipes/ios-widget@latest.zip +0 -0
  132. package/recipes/number-stepper/apps/native/src/components/advanced-ui/stepper/demo.tsx +1 -1
  133. package/recipes/number-stepper/recipe.json +5 -2
  134. package/recipes/number-stepper-native@latest.zip +0 -0
  135. package/recipes/number-stepper@latest.zip +0 -0
  136. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/interactive-onboarding.tsx +11 -18
  137. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/ai-tone-step.tsx +5 -7
  138. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/currency-step.tsx +9 -7
  139. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-ai-step.tsx +8 -7
  140. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-chatbot-step.tsx +6 -5
  141. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-manual-step.tsx +4 -3
  142. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-scan-step.tsx +6 -5
  143. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/main-reason-step.tsx +5 -7
  144. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/notification-step.tsx +7 -6
  145. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/overspend-step.tsx +5 -7
  146. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/personalizing-step.tsx +8 -7
  147. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/rating-step.tsx +6 -5
  148. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/reminder-step.tsx +5 -6
  149. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/safety-step.tsx +5 -4
  150. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/struggle-step.tsx +5 -7
  151. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/welcome-step.tsx +7 -6
  152. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/ui/onboarding-header.tsx +4 -3
  153. package/recipes/onboarding/recipe.json +9 -6
  154. package/recipes/onboarding-native@latest.zip +0 -0
  155. package/recipes/onboarding@latest.zip +0 -0
  156. package/recipes/payments/apps/native/src/app/paywall/index.tsx +74 -0
  157. package/recipes/payments/apps/native/src/app/paywall/local.tsx +25 -0
  158. package/recipes/payments/apps/native/src/app/paywall/remote.tsx +23 -0
  159. package/recipes/payments/packages/backend/convex/payments.ts +21 -3
  160. package/recipes/payments/recipe.json +14 -34
  161. package/recipes/payments-native@latest.zip +0 -0
  162. package/recipes/payments-supabase/apps/native/src/app/paywall/index.tsx +74 -0
  163. package/recipes/payments-supabase/apps/native/src/app/paywall/local.tsx +25 -0
  164. package/recipes/payments-supabase/apps/native/src/app/paywall/remote.tsx +23 -0
  165. package/recipes/payments-supabase/recipe.json +16 -23
  166. package/recipes/payments-supabase-native@latest.zip +0 -0
  167. package/recipes/payments-supabase@latest.zip +0 -0
  168. package/recipes/payments@latest.zip +0 -0
  169. package/recipes/posthog/apps/native/src/components/analytics/navigation-tracker.tsx +14 -0
  170. package/recipes/posthog/apps/native/src/lib/hooks/use-navigation-analytics.ts +44 -0
  171. package/recipes/posthog/apps/native/src/providers/posthog-provider.tsx +51 -0
  172. package/recipes/posthog/recipe.json +60 -0
  173. package/recipes/posthog-native@latest.zip +0 -0
  174. package/recipes/progress-circle/apps/native/src/components/advanced-ui/progress-bars/progress-circle-page.tsx +1 -1
  175. package/recipes/progress-circle/recipe.json +5 -2
  176. package/recipes/progress-circle-native@latest.zip +0 -0
  177. package/recipes/progress-circle@latest.zip +0 -0
  178. package/recipes/quiz/apps/native/src/app/quiz/index.tsx +47 -0
  179. package/recipes/quiz/recipe.json +9 -6
  180. package/recipes/quiz-native@latest.zip +0 -0
  181. package/recipes/quiz@latest.zip +0 -0
  182. package/recipes/screen-kits/apps/native/src/app/screen-kits/_layout.tsx +12 -0
  183. package/recipes/screen-kits/apps/native/src/app/screen-kits/index.tsx +114 -0
  184. package/recipes/screen-kits/apps/native/src/features/screen-kits/index.ts +1 -0
  185. package/recipes/screen-kits/apps/native/src/features/screen-kits/types.ts +28 -0
  186. package/recipes/screen-kits/recipe.json +26 -0
  187. package/recipes/screen-kits-duolingo/apps/native/src/app/screen-kits/duolingo/_layout.tsx +12 -0
  188. package/recipes/screen-kits-duolingo/apps/native/src/app/screen-kits/duolingo/home.tsx +5 -0
  189. package/recipes/screen-kits-duolingo/apps/native/src/app/screen-kits/duolingo/index.tsx +5 -0
  190. package/recipes/screen-kits-duolingo/apps/native/src/app/screen-kits/duolingo/lesson-complete.tsx +5 -0
  191. package/recipes/screen-kits-duolingo/apps/native/src/app/screen-kits/duolingo/lesson-fail.tsx +5 -0
  192. package/recipes/screen-kits-duolingo/apps/native/src/app/screen-kits/duolingo/lesson.tsx +5 -0
  193. package/recipes/screen-kits-duolingo/apps/native/src/app/screen-kits/duolingo/skill-tree.tsx +5 -0
  194. package/recipes/screen-kits-duolingo/apps/native/src/features/screen-kits/duolingo/components/duo-button.tsx +174 -0
  195. package/recipes/screen-kits-duolingo/apps/native/src/features/screen-kits/duolingo/components/skill-button.tsx +186 -0
  196. package/recipes/screen-kits-duolingo/apps/native/src/features/screen-kits/duolingo/components/xp-header.tsx +115 -0
  197. package/recipes/screen-kits-duolingo/apps/native/src/features/screen-kits/duolingo/constants.ts +89 -0
  198. package/recipes/screen-kits-duolingo/apps/native/src/features/screen-kits/duolingo/index.ts +3 -0
  199. package/recipes/screen-kits-duolingo/apps/native/src/features/screen-kits/duolingo/screens/home-screen.tsx +225 -0
  200. package/recipes/screen-kits-duolingo/apps/native/src/features/screen-kits/duolingo/screens/lesson-complete-screen.tsx +485 -0
  201. package/recipes/screen-kits-duolingo/apps/native/src/features/screen-kits/duolingo/screens/lesson-fail-screen.tsx +105 -0
  202. package/recipes/screen-kits-duolingo/apps/native/src/features/screen-kits/duolingo/screens/lesson-screen.tsx +384 -0
  203. package/recipes/screen-kits-duolingo/recipe.json +58 -0
  204. package/recipes/screen-kits-duolingo-native@latest.zip +0 -0
  205. package/recipes/screen-kits-finance/apps/native/src/app/screen-kits/finance/_layout.tsx +45 -0
  206. package/recipes/screen-kits-finance/apps/native/src/app/screen-kits/finance/asset-detail.tsx +3 -0
  207. package/recipes/screen-kits-finance/apps/native/src/app/screen-kits/finance/notifications.tsx +3 -0
  208. package/recipes/screen-kits-finance/apps/native/src/app/screen-kits/finance/receive.tsx +3 -0
  209. package/recipes/screen-kits-finance/apps/native/src/app/screen-kits/finance/send.tsx +3 -0
  210. package/recipes/screen-kits-finance/apps/native/src/app/screen-kits/finance/swap.tsx +3 -0
  211. package/recipes/screen-kits-finance/apps/native/src/app/screen-kits/finance/wallet.tsx +3 -0
  212. package/recipes/screen-kits-finance/apps/native/src/features/screen-kits/finance/components/ActionButtons.tsx +78 -0
  213. package/recipes/screen-kits-finance/apps/native/src/features/screen-kits/finance/components/AssetRow.tsx +94 -0
  214. package/recipes/screen-kits-finance/apps/native/src/features/screen-kits/finance/components/BalanceCard.tsx +118 -0
  215. package/recipes/screen-kits-finance/apps/native/src/features/screen-kits/finance/constants.ts +85 -0
  216. package/recipes/screen-kits-finance/apps/native/src/features/screen-kits/finance/screens/asset-detail.tsx +378 -0
  217. package/recipes/screen-kits-finance/apps/native/src/features/screen-kits/finance/screens/notifications.tsx +210 -0
  218. package/recipes/screen-kits-finance/apps/native/src/features/screen-kits/finance/screens/receive-modal.tsx +317 -0
  219. package/recipes/screen-kits-finance/apps/native/src/features/screen-kits/finance/screens/send-modal.tsx +420 -0
  220. package/recipes/screen-kits-finance/apps/native/src/features/screen-kits/finance/screens/swap-modal.tsx +363 -0
  221. package/recipes/screen-kits-finance/apps/native/src/features/screen-kits/finance/screens/wallet-dashboard.tsx +281 -0
  222. package/recipes/screen-kits-finance/recipe.json +46 -0
  223. package/recipes/screen-kits-finance-native@latest.zip +0 -0
  224. package/recipes/screen-kits-fitness/apps/native/assets/sounds/timer-beep.wav +0 -0
  225. package/recipes/screen-kits-fitness/apps/native/src/app/screen-kits/fitness/_layout.tsx +10 -0
  226. package/recipes/screen-kits-fitness/apps/native/src/app/screen-kits/fitness/index.tsx +6 -0
  227. package/recipes/screen-kits-fitness/apps/native/src/app/screen-kits/fitness/timer.tsx +3 -0
  228. package/recipes/screen-kits-fitness/apps/native/src/app/screen-kits/fitness/workout.tsx +3 -0
  229. package/recipes/screen-kits-fitness/apps/native/src/features/screen-kits/fitness/components/timer-components.tsx +500 -0
  230. package/recipes/screen-kits-fitness/apps/native/src/features/screen-kits/fitness/components/timer-settings-modal.tsx +352 -0
  231. package/recipes/screen-kits-fitness/apps/native/src/features/screen-kits/fitness/components/workout-card.tsx +105 -0
  232. package/recipes/screen-kits-fitness/apps/native/src/features/screen-kits/fitness/constants.ts +189 -0
  233. package/recipes/screen-kits-fitness/apps/native/src/features/screen-kits/fitness/hooks/use-timer.ts +307 -0
  234. package/recipes/screen-kits-fitness/apps/native/src/features/screen-kits/fitness/index.ts +1 -0
  235. package/recipes/screen-kits-fitness/apps/native/src/features/screen-kits/fitness/screens/timer-screen.tsx +278 -0
  236. package/recipes/screen-kits-fitness/apps/native/src/features/screen-kits/fitness/screens/workout-dashboard.tsx +350 -0
  237. package/recipes/screen-kits-fitness/recipe.json +63 -0
  238. package/recipes/screen-kits-fitness-native@latest.zip +0 -0
  239. package/recipes/screen-kits-habits/apps/native/src/app/screen-kits/productivity/habits.tsx +1 -0
  240. package/recipes/screen-kits-habits/apps/native/src/app/screen-kits/productivity/kanban.tsx +1 -0
  241. package/recipes/screen-kits-habits/apps/native/src/app/screen-kits/productivity/routes.ts +4 -0
  242. package/recipes/screen-kits-habits/apps/native/src/features/screen-kits/productivity/components/AddTaskModal.tsx +246 -0
  243. package/recipes/screen-kits-habits/apps/native/src/features/screen-kits/productivity/components/DraggableTaskCard.tsx +92 -0
  244. package/recipes/screen-kits-habits/apps/native/src/features/screen-kits/productivity/components/KanbanColumn.tsx +238 -0
  245. package/recipes/screen-kits-habits/apps/native/src/features/screen-kits/productivity/components/TaskCard.tsx +144 -0
  246. package/recipes/screen-kits-habits/apps/native/src/features/screen-kits/productivity/components/add-habit-modal.tsx +271 -0
  247. package/recipes/screen-kits-habits/apps/native/src/features/screen-kits/productivity/constants.ts +295 -0
  248. package/recipes/screen-kits-habits/apps/native/src/features/screen-kits/productivity/kanban-utils.ts +62 -0
  249. package/recipes/screen-kits-habits/apps/native/src/features/screen-kits/productivity/screens/habit-tracker.tsx +1160 -0
  250. package/recipes/screen-kits-habits/apps/native/src/features/screen-kits/productivity/screens/kanban-board.tsx +432 -0
  251. package/recipes/screen-kits-habits/recipe.json +52 -0
  252. package/recipes/screen-kits-habits-native@latest.zip +0 -0
  253. package/recipes/screen-kits-native@latest.zip +0 -0
  254. package/recipes/sentry/apps/native/src/providers/sentry-provider.tsx +64 -0
  255. package/recipes/sentry/recipe.json +39 -0
  256. package/recipes/sentry-native@latest.zip +0 -0
  257. package/recipes/swipe-slider/apps/native/src/components/advanced-ui/sliders/swipe-slider-page.tsx +1 -1
  258. package/recipes/swipe-slider/recipe.json +5 -2
  259. package/recipes/swipe-slider-native@latest.zip +0 -0
  260. package/recipes/swipe-slider@latest.zip +0 -0
  261. package/recipes/timeline/apps/native/src/components/advanced-ui/timeline/demo.tsx +2 -1
  262. package/recipes/timeline/recipe.json +5 -2
  263. package/recipes/timeline-native@latest.zip +0 -0
  264. package/recipes/timeline@latest.zip +0 -0
  265. package/recipes/tracker-app/apps/native/src/app/tracker-app/index.tsx +1 -0
  266. package/recipes/tracker-app/recipe.json +10 -7
  267. package/recipes/tracker-app-native@latest.zip +0 -0
  268. package/recipes/tracker-app@latest.zip +0 -0
  269. package/recipes/upload-all.sh +8 -31
  270. package/recipes/voice-bot/apps/native/src/app/voice-bot/index.tsx +56 -0
  271. package/recipes/voice-bot/recipe.json +31 -7
  272. package/recipes/voice-bot-native@latest.zip +0 -0
  273. package/recipes/voice-bot@latest.zip +0 -0
  274. package/recipes/wake-word/apps/native/src/app/{(root)/(protected)/test-wake-word.tsx → test-wake-word.tsx} +43 -4
  275. package/recipes/wake-word/recipe.json +16 -26
  276. package/recipes/wake-word-native@latest.zip +0 -0
  277. package/recipes/wake-word@latest.zip +0 -0
  278. package/scripts/create-advanced-ui-recipes.sh +46 -19
  279. package/scripts/create-recipes.mjs +471 -117
  280. package/scripts/package-recipes.mjs +76 -0
  281. package/scripts/publish-all.sh +6 -2
  282. package/CHANGELOG.md +0 -198
  283. package/docs/archive/AUTO-DETECT-DEPS.md +0 -607
  284. package/docs/archive/FINAL-PACKAGE-STRATEGY.md +0 -583
  285. package/docs/archive/FINAL-SIMPLE-PLAN.md +0 -487
  286. package/docs/archive/FINAL-STATUS.md +0 -144
  287. package/docs/archive/FLOW-DIAGRAM.md +0 -1629
  288. package/docs/archive/GOTCHAS-AND-RISKS.md +0 -801
  289. package/docs/archive/IMPLEMENTATION-PLAN.md +0 -1360
  290. package/docs/archive/PLAN.md +0 -453
  291. package/docs/archive/PRODUCTION-READINESS.md +0 -684
  292. package/docs/archive/PRODUCTION-TEST-RESULTS.md +0 -465
  293. package/docs/archive/SIMPLIFIED-PLAN.md +0 -578
  294. package/docs/archive/STATUS.md +0 -199
  295. package/docs/archive/SUCCESS.md +0 -259
  296. package/docs/archive/TEST-SUMMARY.md +0 -261
  297. package/docs/archive/TESTING-CHECKLIST.md +0 -450
  298. package/docs/archive/USER-MODIFICATIONS.md +0 -448
  299. package/docs/decisions.md +0 -55
  300. package/docs/manual-testing.md +0 -91
  301. package/docs/next-steps.md +0 -12
  302. package/recipes/README.md +0 -156
  303. package/recipes/audio-recorder-supabase/packages/backend/src/services/recordings.ts +0 -369
  304. package/recipes/chatbot/apps/native/src/api-client/chatbot.ts +0 -83
  305. package/recipes/chatbot/packages/backend/convex/agents.ts +0 -115
  306. package/recipes/chatbot/packages/backend/convex/tools/index.ts +0 -18
  307. package/recipes/chatbot/packages/backend/convex/tools/knowledgeRetrieval.ts +0 -97
  308. package/recipes/chatbot/packages/backend/convex/tools/tavilySearch.ts +0 -83
  309. package/recipes/chatbot/packages/backend/convex/tools/userProfile.ts +0 -72
  310. package/recipes/chatbot-supabase/apps/native/src/api-client/supabase/chatbot.ts +0 -515
  311. package/recipes/chatbot-supabase/packages/backend/src/services/conversations.ts +0 -243
  312. package/recipes/chatbot-supabase/packages/backend/src/services/messages.ts +0 -327
  313. package/recipes/image-analysis/apps/native/src/api-client/image-analyzer.ts +0 -62
  314. package/recipes/image-analysis-supabase/packages/backend/src/services/image-analyses.ts +0 -132
  315. package/recipes/image-generator/apps/native/src/api-client/image-generator.ts +0 -34
  316. package/recipes/payments/apps/native/src/api-client/payments.ts +0 -44
  317. package/recipes/payments-supabase/packages/backend/src/services/payments.ts +0 -201
  318. package/recipes/posthog.json +0 -47
  319. package/recipes/revenuecat.json +0 -43
  320. package/recipes/sentry.json +0 -47
  321. package/recipes/wake-word/apps/native/assets/vosk-model/README.md +0 -103
  322. package/recipes/wake-word/apps/native/scripts/download-vosk-model.mjs +0 -127
  323. /package/recipes/{audio-recorder/apps/native/src/app/(root)/(protected) → audio-recorder-supabase/apps/native/src/app}/audio-recorder/index.tsx +0 -0
  324. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/AppIntent.swift +0 -0
  325. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png +0 -0
  326. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png +0 -0
  327. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png +0 -0
  328. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png +0 -0
  329. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png +0 -0
  330. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png +0 -0
  331. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png +0 -0
  332. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png +0 -0
  333. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png +0 -0
  334. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png +0 -0
  335. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png +0 -0
  336. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png +0 -0
  337. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png +0 -0
  338. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png +0 -0
  339. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
  340. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png +0 -0
  341. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/CalorieTrackerWidget.swift +0 -0
  342. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/HabitTrackerWidget.swift +0 -0
  343. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/Info.plist +0 -0
  344. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/WidgetLiveActivity.swift +0 -0
  345. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/expo-target.config.js +0 -0
  346. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/generated.entitlements +0 -0
  347. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/index.swift +0 -0
  348. /package/recipes/ios-widget/{targets → apps/native/targets}/widget/widgets.swift +0 -0
@@ -0,0 +1,1160 @@
1
+ import { Ionicons } from '@expo/vector-icons';
2
+ import { BlurView } from 'expo-blur';
3
+ import * as Haptics from 'expo-haptics';
4
+ import { Stack } from 'expo-router';
5
+ import React, { useCallback, useMemo, useState } from 'react';
6
+ import {
7
+ Alert,
8
+ Dimensions,
9
+ Pressable,
10
+ ScrollView,
11
+ StyleSheet,
12
+ TouchableOpacity,
13
+ View,
14
+ type ViewStyle,
15
+ } from 'react-native';
16
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
17
+ import Animated, {
18
+ Easing,
19
+ FadeIn,
20
+ FadeInDown,
21
+ FadeInUp,
22
+ FadeOut,
23
+ interpolate,
24
+ LinearTransition,
25
+ runOnJS,
26
+ useAnimatedStyle,
27
+ useSharedValue,
28
+ withDelay,
29
+ withRepeat,
30
+ withSequence,
31
+ withSpring,
32
+ withTiming,
33
+ } from 'react-native-reanimated';
34
+
35
+ import { FocusAwareStatusBar, Text } from '@/components/ui';
36
+ import { useThemeConfig } from '@/lib/use-theme-config';
37
+
38
+ import { AddHabitModal } from '../components/add-habit-modal';
39
+ import {
40
+ HEATMAP_COLORS,
41
+ MOCK_HABITS,
42
+ STREAK_COLORS,
43
+ type Habit,
44
+ } from '../constants';
45
+
46
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
47
+ const SWIPE_THRESHOLD = SCREEN_WIDTH * 0.25;
48
+
49
+ function FireAnimation({
50
+ size = 24,
51
+ isActive = true,
52
+ delay = 0,
53
+ }: {
54
+ size?: number;
55
+ isActive?: boolean;
56
+ delay?: number;
57
+ }) {
58
+ const flame1 = useSharedValue(0);
59
+ const flame2 = useSharedValue(0);
60
+ const flame3 = useSharedValue(0);
61
+
62
+ React.useEffect(() => {
63
+ if (isActive) {
64
+ flame1.value = withDelay(
65
+ delay,
66
+ withRepeat(
67
+ withSequence(
68
+ withTiming(1, { duration: 400, easing: Easing.inOut(Easing.ease) }),
69
+ withTiming(0, { duration: 400, easing: Easing.inOut(Easing.ease) }),
70
+ ),
71
+ -1,
72
+ true,
73
+ ),
74
+ );
75
+ flame2.value = withDelay(
76
+ delay + 150,
77
+ withRepeat(
78
+ withSequence(
79
+ withTiming(1, { duration: 350, easing: Easing.inOut(Easing.ease) }),
80
+ withTiming(0, { duration: 350, easing: Easing.inOut(Easing.ease) }),
81
+ ),
82
+ -1,
83
+ true,
84
+ ),
85
+ );
86
+ flame3.value = withDelay(
87
+ delay + 300,
88
+ withRepeat(
89
+ withSequence(
90
+ withTiming(1, { duration: 300, easing: Easing.inOut(Easing.ease) }),
91
+ withTiming(0, { duration: 300, easing: Easing.inOut(Easing.ease) }),
92
+ ),
93
+ -1,
94
+ true,
95
+ ),
96
+ );
97
+ }
98
+ }, [isActive, delay, flame1, flame2, flame3]);
99
+
100
+ const flameStyle1 = useAnimatedStyle(() => {
101
+ 'worklet';
102
+ const translateY = interpolate(flame1.value, [0, 1], [0, -3]);
103
+ const scale = interpolate(flame1.value, [0, 1], [1, 1.1]);
104
+ return {
105
+ transform: [{ translateY }, { scale }] as ViewStyle['transform'],
106
+ opacity: interpolate(flame1.value, [0, 1], [0.8, 1]),
107
+ };
108
+ });
109
+
110
+ const flameStyle2 = useAnimatedStyle(() => {
111
+ 'worklet';
112
+ const translateY = interpolate(flame2.value, [0, 1], [0, -4]);
113
+ const scale = interpolate(flame2.value, [0, 1], [0.9, 1.05]);
114
+ return {
115
+ transform: [{ translateY }, { scale }] as ViewStyle['transform'],
116
+ opacity: interpolate(flame2.value, [0, 1], [0.6, 0.9]),
117
+ };
118
+ });
119
+
120
+ const flameStyle3 = useAnimatedStyle(() => {
121
+ 'worklet';
122
+ const translateY = interpolate(flame3.value, [0, 1], [0, -2]);
123
+ const scale = interpolate(flame3.value, [0, 1], [0.85, 1]);
124
+ return {
125
+ transform: [{ translateY }, { scale }] as ViewStyle['transform'],
126
+ opacity: interpolate(flame3.value, [0, 1], [0.5, 0.8]),
127
+ };
128
+ });
129
+
130
+ if (!isActive) {
131
+ return (
132
+ <View style={styles.fireContainer}>
133
+ <Ionicons
134
+ name="snow-outline"
135
+ size={size * 0.7}
136
+ color={STREAK_COLORS.cold}
137
+ />
138
+ </View>
139
+ );
140
+ }
141
+
142
+ return (
143
+ <View style={[styles.fireContainer, { width: size, height: size }]}>
144
+ <Animated.View style={[styles.flameAbsolute, flameStyle3 as ViewStyle]}>
145
+ <Ionicons
146
+ name="flame"
147
+ size={size * 0.5}
148
+ color={STREAK_COLORS.fire[2]}
149
+ />
150
+ </Animated.View>
151
+ <Animated.View style={[styles.flameAbsolute, flameStyle2 as ViewStyle]}>
152
+ <Ionicons
153
+ name="flame"
154
+ size={size * 0.7}
155
+ color={STREAK_COLORS.fire[1]}
156
+ />
157
+ </Animated.View>
158
+ <Animated.View style={[styles.flameAbsolute, flameStyle1 as ViewStyle]}>
159
+ <Ionicons
160
+ name="flame"
161
+ size={size * 0.9}
162
+ color={STREAK_COLORS.fire[0]}
163
+ />
164
+ </Animated.View>
165
+ </View>
166
+ );
167
+ }
168
+
169
+ function ConfettiParticle({
170
+ x,
171
+ color,
172
+ delay: particleDelay,
173
+ }: {
174
+ x: number;
175
+ color: string;
176
+ delay: number;
177
+ }) {
178
+ const translateY = useSharedValue(0);
179
+ const translateX = useSharedValue(0);
180
+ const rotate = useSharedValue(0);
181
+ const opacity = useSharedValue(1);
182
+
183
+ React.useEffect(() => {
184
+ translateY.value = withDelay(
185
+ particleDelay,
186
+ withTiming(-80, { duration: 800, easing: Easing.out(Easing.cubic) }),
187
+ );
188
+ translateX.value = withDelay(
189
+ particleDelay,
190
+ withTiming((Math.random() - 0.5) * 60, { duration: 800 }),
191
+ );
192
+ rotate.value = withDelay(
193
+ particleDelay,
194
+ withTiming(360 * (Math.random() > 0.5 ? 1 : -1), { duration: 800 }),
195
+ );
196
+ opacity.value = withDelay(
197
+ particleDelay + 400,
198
+ withTiming(0, { duration: 400 }),
199
+ );
200
+ }, [particleDelay, opacity, rotate, translateX, translateY]);
201
+
202
+ const animatedStyle = useAnimatedStyle(() => {
203
+ 'worklet';
204
+ const tX = translateX.value + x;
205
+ const tY = translateY.value;
206
+ const rot = `${rotate.value}deg`;
207
+ return {
208
+ transform: [
209
+ { translateX: tX },
210
+ { translateY: tY },
211
+ { rotate: rot },
212
+ ] as ViewStyle['transform'],
213
+ opacity: opacity.value,
214
+ };
215
+ });
216
+
217
+ return (
218
+ <Animated.View
219
+ style={[
220
+ styles.confettiParticle,
221
+ { backgroundColor: color },
222
+ animatedStyle as ViewStyle,
223
+ ]}
224
+ />
225
+ );
226
+ }
227
+
228
+ function CheckmarkAnimation({
229
+ isChecked,
230
+ color,
231
+ }: {
232
+ isChecked: boolean;
233
+ color: string;
234
+ }) {
235
+ const scale = useSharedValue(isChecked ? 1 : 0);
236
+ const circleScale = useSharedValue(1);
237
+ const [showConfetti, setShowConfetti] = useState(false);
238
+
239
+ React.useEffect(() => {
240
+ if (isChecked) {
241
+ circleScale.value = withSequence(
242
+ withTiming(0.9, { duration: 100 }),
243
+ withSpring(1.1, { damping: 10, stiffness: 200 }),
244
+ withSpring(1, { damping: 15 }),
245
+ );
246
+ scale.value = withDelay(
247
+ 100,
248
+ withSpring(1, { damping: 12, stiffness: 180 }),
249
+ );
250
+ setShowConfetti(true);
251
+ setTimeout(() => setShowConfetti(false), 1000);
252
+ } else {
253
+ scale.value = withTiming(0, { duration: 150 });
254
+ }
255
+ }, [isChecked, scale, circleScale]);
256
+
257
+ const checkStyle = useAnimatedStyle<ViewStyle>(() => {
258
+ 'worklet';
259
+ return {
260
+ transform: [{ scale: scale.value }],
261
+ };
262
+ });
263
+
264
+ const containerStyle = useAnimatedStyle<ViewStyle>(() => {
265
+ 'worklet';
266
+ return {
267
+ transform: [{ scale: circleScale.value }],
268
+ };
269
+ });
270
+
271
+ const confettiColors = [
272
+ '#FF6B6B',
273
+ '#4ECDC4',
274
+ '#FFE66D',
275
+ '#A78BFA',
276
+ '#22C55E',
277
+ ];
278
+
279
+ return (
280
+ <View style={styles.checkmarkContainer}>
281
+ {showConfetti && (
282
+ <View style={styles.confettiContainer}>
283
+ {confettiColors.map((c, i) => (
284
+ <ConfettiParticle
285
+ key={i}
286
+ x={(i - 2) * 10}
287
+ color={c}
288
+ delay={i * 50}
289
+ />
290
+ ))}
291
+ </View>
292
+ )}
293
+ <Animated.View
294
+ style={[
295
+ styles.checkmarkBox,
296
+ {
297
+ backgroundColor: isChecked ? color : 'transparent',
298
+ borderWidth: isChecked ? 0 : 2,
299
+ borderColor: color + '40',
300
+ },
301
+ containerStyle as ViewStyle,
302
+ ]}
303
+ >
304
+ {isChecked ? (
305
+ <Animated.View style={checkStyle as ViewStyle}>
306
+ <Ionicons name="checkmark" size={26} color="white" />
307
+ </Animated.View>
308
+ ) : (
309
+ <View style={[styles.emptyCircle, { borderColor: color + '60' }]} />
310
+ )}
311
+ </Animated.View>
312
+ </View>
313
+ );
314
+ }
315
+
316
+ function HeatmapCell({
317
+ intensity,
318
+ isDark,
319
+ dayIndex,
320
+ isToday,
321
+ onPress,
322
+ }: {
323
+ intensity: number;
324
+ isDark: boolean;
325
+ dayIndex: number;
326
+ isToday: boolean;
327
+ onPress: () => void;
328
+ }) {
329
+ const colors = isDark ? HEATMAP_COLORS.dark : HEATMAP_COLORS.light;
330
+ const scale = useSharedValue(1);
331
+
332
+ const getColor = (level: number) => {
333
+ switch (level) {
334
+ case 0:
335
+ return colors.empty;
336
+ case 1:
337
+ return colors.level1;
338
+ case 2:
339
+ return colors.level2;
340
+ case 3:
341
+ return colors.level3;
342
+ case 4:
343
+ return colors.level4;
344
+ default:
345
+ return colors.empty;
346
+ }
347
+ };
348
+
349
+ const handlePress = () => {
350
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
351
+ scale.value = withSequence(
352
+ withSpring(0.8, { damping: 10 }),
353
+ withSpring(1, { damping: 8 }),
354
+ );
355
+ onPress();
356
+ };
357
+
358
+ const animatedStyle = useAnimatedStyle<ViewStyle>(() => {
359
+ 'worklet';
360
+ return {
361
+ transform: [{ scale: scale.value }],
362
+ };
363
+ });
364
+
365
+ return (
366
+ <Pressable onPress={handlePress}>
367
+ <Animated.View
368
+ entering={FadeIn.delay(dayIndex * 15)}
369
+ style={[
370
+ styles.heatmapCell,
371
+ {
372
+ backgroundColor: getColor(intensity),
373
+ borderWidth: isToday ? 2 : 0,
374
+ borderColor: isToday ? (isDark ? '#FFF' : '#000') : 'transparent',
375
+ },
376
+ animatedStyle as ViewStyle,
377
+ ]}
378
+ />
379
+ </Pressable>
380
+ );
381
+ }
382
+
383
+ function WeekNavigator({
384
+ currentWeekOffset,
385
+ onPrevious,
386
+ onNext,
387
+ isDark,
388
+ }: {
389
+ currentWeekOffset: number;
390
+ onPrevious: () => void;
391
+ onNext: () => void;
392
+ isDark: boolean;
393
+ }) {
394
+ const theme = useThemeConfig();
395
+
396
+ const getWeekLabel = () => {
397
+ if (currentWeekOffset === 0) return 'This Week';
398
+ if (currentWeekOffset === -1) return 'Last Week';
399
+ return `${Math.abs(currentWeekOffset)} weeks ago`;
400
+ };
401
+
402
+ return (
403
+ <Animated.View entering={FadeInUp.delay(200)} style={styles.weekNavigator}>
404
+ <TouchableOpacity
405
+ onPress={onPrevious}
406
+ style={[
407
+ styles.navButton,
408
+ {
409
+ backgroundColor: isDark
410
+ ? 'rgba(255,255,255,0.1)'
411
+ : 'rgba(0,0,0,0.05)',
412
+ },
413
+ ]}
414
+ >
415
+ <Ionicons
416
+ name="chevron-back"
417
+ size={20}
418
+ color={theme.colors.foreground}
419
+ />
420
+ </TouchableOpacity>
421
+
422
+ <Text style={[styles.weekLabel, { color: theme.colors.foreground }]}>
423
+ {getWeekLabel()}
424
+ </Text>
425
+
426
+ <TouchableOpacity
427
+ onPress={onNext}
428
+ disabled={currentWeekOffset >= 0}
429
+ style={[
430
+ styles.navButton,
431
+ {
432
+ backgroundColor: isDark
433
+ ? 'rgba(255,255,255,0.1)'
434
+ : 'rgba(0,0,0,0.05)',
435
+ opacity: currentWeekOffset >= 0 ? 0.4 : 1,
436
+ },
437
+ ]}
438
+ >
439
+ <Ionicons
440
+ name="chevron-forward"
441
+ size={20}
442
+ color={theme.colors.foreground}
443
+ />
444
+ </TouchableOpacity>
445
+ </Animated.View>
446
+ );
447
+ }
448
+
449
+ function StatsSummary({
450
+ habits,
451
+ isDark,
452
+ }: {
453
+ habits: Habit[];
454
+ isDark: boolean;
455
+ }) {
456
+ const theme = useThemeConfig();
457
+
458
+ const stats = useMemo(() => {
459
+ const totalCompletions = habits.reduce(
460
+ (acc, h) => acc + h.weeklyData.filter(Boolean).length,
461
+ 0,
462
+ );
463
+ const totalPossible = habits.length * 7;
464
+ const completionRate = habits.length
465
+ ? Math.round((totalCompletions / totalPossible) * 100)
466
+ : 0;
467
+ const bestStreak = habits.length
468
+ ? Math.max(...habits.map((h) => h.bestStreak))
469
+ : 0;
470
+ const currentStreakTotal = habits.reduce((acc, h) => acc + h.streak, 0);
471
+
472
+ return { completionRate, bestStreak, currentStreakTotal };
473
+ }, [habits]);
474
+
475
+ const statItems = [
476
+ {
477
+ label: 'Completion',
478
+ value: `${stats.completionRate}%`,
479
+ icon: 'pie-chart',
480
+ color: '#22C55E',
481
+ },
482
+ {
483
+ label: 'Best Streak',
484
+ value: `${stats.bestStreak}`,
485
+ icon: 'trophy',
486
+ color: '#F59E0B',
487
+ },
488
+ {
489
+ label: 'Active Days',
490
+ value: `${stats.currentStreakTotal}`,
491
+ icon: 'flame',
492
+ color: '#EF4444',
493
+ },
494
+ ];
495
+
496
+ return (
497
+ <Animated.View
498
+ entering={FadeInDown.delay(100).springify()}
499
+ style={styles.statsContainer}
500
+ >
501
+ {statItems.map((stat, index) => (
502
+ <Animated.View
503
+ key={stat.label}
504
+ entering={FadeInUp.delay(200 + index * 100)}
505
+ style={[styles.statCardWrapper]}
506
+ >
507
+ <BlurView
508
+ intensity={isDark ? 20 : 40}
509
+ tint={isDark ? 'dark' : 'light'}
510
+ style={styles.statCardGlass}
511
+ >
512
+ <View
513
+ style={[styles.statIcon, { backgroundColor: stat.color + '20' }]}
514
+ >
515
+ <Ionicons name={stat.icon as any} size={20} color={stat.color} />
516
+ </View>
517
+ <Text
518
+ style={[styles.statValue, { color: theme.colors.foreground }]}
519
+ >
520
+ {stat.value}
521
+ </Text>
522
+ <Text
523
+ style={[
524
+ styles.statLabel,
525
+ { color: theme.colors.mutedForeground },
526
+ ]}
527
+ >
528
+ {stat.label}
529
+ </Text>
530
+ </BlurView>
531
+ </Animated.View>
532
+ ))}
533
+ </Animated.View>
534
+ );
535
+ }
536
+
537
+ function HabitCard({
538
+ habit,
539
+ index,
540
+ isDark,
541
+ onToggle,
542
+ onDelete,
543
+ }: {
544
+ habit: Habit;
545
+ index: number;
546
+ isDark: boolean;
547
+ onToggle: (id: string) => void;
548
+ onDelete: (id: string) => void;
549
+ }) {
550
+ const theme = useThemeConfig();
551
+ const isCompletedToday = habit.weeklyData[habit.weeklyData.length - 1];
552
+ const days = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
553
+
554
+ const handleToggle = useCallback(() => {
555
+ Haptics.impactAsync(
556
+ isCompletedToday
557
+ ? Haptics.ImpactFeedbackStyle.Light
558
+ : Haptics.ImpactFeedbackStyle.Heavy,
559
+ );
560
+ onToggle(habit.id);
561
+ }, [habit.id, isCompletedToday, onToggle]);
562
+
563
+ const handleLongPress = useCallback(() => {
564
+ Haptics.selectionAsync();
565
+ Alert.alert(
566
+ 'Delete Habit',
567
+ `Are you sure you want to delete "${habit.title}"?`,
568
+ [
569
+ { text: 'Cancel', style: 'cancel' },
570
+ {
571
+ text: 'Delete',
572
+ style: 'destructive',
573
+ onPress: () => onDelete(habit.id),
574
+ },
575
+ ],
576
+ );
577
+ }, [habit, onDelete]);
578
+
579
+ return (
580
+ <Animated.View
581
+ entering={FadeInDown.delay(index * 80).springify()}
582
+ exiting={FadeOut}
583
+ layout={LinearTransition.springify()}
584
+ style={styles.habitCardWrapper}
585
+ >
586
+ <BlurView
587
+ intensity={isDark ? 25 : 60}
588
+ tint={isDark ? 'dark' : 'light'}
589
+ style={styles.habitCardGlass}
590
+ >
591
+ <View style={styles.habitHeader}>
592
+ <Pressable
593
+ onPress={handleToggle}
594
+ onLongPress={handleLongPress}
595
+ style={styles.habitInfo}
596
+ >
597
+ <CheckmarkAnimation
598
+ isChecked={isCompletedToday}
599
+ color={habit.color}
600
+ />
601
+ <View style={styles.habitDetails}>
602
+ <Text
603
+ style={[styles.habitTitle, { color: theme.colors.foreground }]}
604
+ >
605
+ {habit.title}
606
+ </Text>
607
+ <View style={styles.streakRow}>
608
+ <FireAnimation
609
+ size={20}
610
+ isActive={habit.streak > 0}
611
+ delay={index * 100}
612
+ />
613
+ <Text
614
+ style={[
615
+ styles.streakText,
616
+ {
617
+ color:
618
+ habit.streak > 0
619
+ ? STREAK_COLORS.fire[0]
620
+ : theme.colors.mutedForeground,
621
+ },
622
+ ]}
623
+ >
624
+ {habit.streak} day streak
625
+ </Text>
626
+ </View>
627
+ </View>
628
+ </Pressable>
629
+
630
+ <TouchableOpacity onPress={handleLongPress} style={styles.menuButton}>
631
+ <Ionicons
632
+ name="ellipsis-vertical"
633
+ size={20}
634
+ color={theme.colors.mutedForeground}
635
+ />
636
+ </TouchableOpacity>
637
+ </View>
638
+
639
+ <View style={styles.weeklyGrid}>
640
+ {days.map((day, i) => {
641
+ const isCompleted = habit.weeklyData[i];
642
+ const isToday = i === days.length - 1;
643
+
644
+ return (
645
+ <Animated.View
646
+ key={i}
647
+ entering={FadeIn.delay(300 + i * 50)}
648
+ style={styles.dayColumn}
649
+ >
650
+ <Text
651
+ style={[
652
+ styles.dayLabel,
653
+ { color: theme.colors.mutedForeground },
654
+ ]}
655
+ >
656
+ {day}
657
+ </Text>
658
+ <View
659
+ style={[
660
+ styles.dayCell,
661
+ {
662
+ backgroundColor: isCompleted
663
+ ? habit.color
664
+ : isDark
665
+ ? 'rgba(255,255,255,0.05)'
666
+ : 'rgba(0,0,0,0.05)',
667
+ borderWidth: isToday ? 2 : 0,
668
+ borderColor: isToday
669
+ ? isDark
670
+ ? '#FFF'
671
+ : '#000'
672
+ : 'transparent',
673
+ opacity: isCompleted ? 1 : 0.4,
674
+ },
675
+ ]}
676
+ >
677
+ {isCompleted && (
678
+ <Ionicons name="checkmark" size={18} color="white" />
679
+ )}
680
+ </View>
681
+ </Animated.View>
682
+ );
683
+ })}
684
+ </View>
685
+
686
+ <View
687
+ style={[
688
+ styles.miniStats,
689
+ {
690
+ borderTopColor: isDark
691
+ ? 'rgba(255,255,255,0.1)'
692
+ : 'rgba(0,0,0,0.1)',
693
+ },
694
+ ]}
695
+ >
696
+ <View style={styles.miniStatItem}>
697
+ <Ionicons
698
+ name="trophy-outline"
699
+ size={14}
700
+ color={theme.colors.mutedForeground}
701
+ />
702
+ <Text
703
+ style={[
704
+ styles.miniStatText,
705
+ { color: theme.colors.mutedForeground },
706
+ ]}
707
+ >
708
+ Best: {habit.bestStreak} days
709
+ </Text>
710
+ </View>
711
+ <View style={styles.miniStatItem}>
712
+ <Ionicons
713
+ name="calendar-outline"
714
+ size={14}
715
+ color={theme.colors.mutedForeground}
716
+ />
717
+ <Text
718
+ style={[
719
+ styles.miniStatText,
720
+ { color: theme.colors.mutedForeground },
721
+ ]}
722
+ >
723
+ {habit.weeklyData.filter(Boolean).length}/7 this week
724
+ </Text>
725
+ </View>
726
+ </View>
727
+ </BlurView>
728
+ </Animated.View>
729
+ );
730
+ }
731
+
732
+ function ContributionGraph({
733
+ habits,
734
+ isDark,
735
+ }: {
736
+ habits: Habit[];
737
+ isDark: boolean;
738
+ }) {
739
+ const theme = useThemeConfig();
740
+
741
+ const monthlyData = useMemo(() => {
742
+ const combined: number[] = [];
743
+ for (let i = 0; i < 35; i++) {
744
+ const total = habits.reduce((acc, h) => acc + (h.monthlyData[i] || 0), 0);
745
+ const avg = habits.length
746
+ ? Math.min(4, Math.round(total / habits.length))
747
+ : 0;
748
+ combined.push(avg);
749
+ }
750
+ return combined;
751
+ }, [habits]);
752
+
753
+ const weeks = useMemo(() => {
754
+ const result: number[][] = [];
755
+ for (let w = 0; w < 5; w++) {
756
+ result.push(monthlyData.slice(w * 7, (w + 1) * 7));
757
+ }
758
+ return result;
759
+ }, [monthlyData]);
760
+
761
+ const heatmapColors = isDark ? HEATMAP_COLORS.dark : HEATMAP_COLORS.light;
762
+ const legendColors = [
763
+ heatmapColors.empty,
764
+ heatmapColors.level1,
765
+ heatmapColors.level2,
766
+ heatmapColors.level3,
767
+ heatmapColors.level4,
768
+ ];
769
+
770
+ return (
771
+ <Animated.View
772
+ entering={FadeInUp.delay(400)}
773
+ style={styles.contributionCardWrapper}
774
+ >
775
+ <BlurView
776
+ intensity={isDark ? 20 : 50}
777
+ tint={isDark ? 'dark' : 'light'}
778
+ style={styles.contributionCardGlass}
779
+ >
780
+ <View style={styles.contributionHeader}>
781
+ <Ionicons
782
+ name="grid-outline"
783
+ size={20}
784
+ color={theme.colors.foreground}
785
+ />
786
+ <Text
787
+ style={[
788
+ styles.contributionTitle,
789
+ { color: theme.colors.foreground },
790
+ ]}
791
+ >
792
+ Activity Overview
793
+ </Text>
794
+ </View>
795
+
796
+ <View style={styles.heatmapGrid}>
797
+ {weeks.map((week, weekIndex) => (
798
+ <View key={weekIndex} style={styles.heatmapWeek}>
799
+ {week.map((intensity, dayIndex) => (
800
+ <HeatmapCell
801
+ key={`${weekIndex}-${dayIndex}`}
802
+ intensity={intensity}
803
+ isDark={isDark}
804
+ dayIndex={weekIndex * 7 + dayIndex}
805
+ isToday={weekIndex === 4 && dayIndex === 6}
806
+ onPress={() => {}}
807
+ />
808
+ ))}
809
+ </View>
810
+ ))}
811
+ </View>
812
+
813
+ <View style={styles.legend}>
814
+ <Text
815
+ style={[styles.legendText, { color: theme.colors.mutedForeground }]}
816
+ >
817
+ Less
818
+ </Text>
819
+ {legendColors.map((color, idx) => (
820
+ <View
821
+ key={idx}
822
+ style={[styles.legendCell, { backgroundColor: color }]}
823
+ />
824
+ ))}
825
+ <Text
826
+ style={[styles.legendText, { color: theme.colors.mutedForeground }]}
827
+ >
828
+ More
829
+ </Text>
830
+ </View>
831
+ </BlurView>
832
+ </Animated.View>
833
+ );
834
+ }
835
+
836
+ export default function HabitTracker() {
837
+ const theme = useThemeConfig();
838
+ const isDark = theme.dark;
839
+
840
+ const [habits, setHabits] = useState<Habit[]>(MOCK_HABITS);
841
+ const [weekOffset, setWeekOffset] = useState(0);
842
+ const [showAddModal, setShowAddModal] = useState(false);
843
+
844
+ const translateX = useSharedValue(0);
845
+
846
+ const handleToggleHabit = useCallback((id: string) => {
847
+ setHabits((prev) =>
848
+ prev.map((h) => {
849
+ if (h.id === id) {
850
+ const newWeeklyData = [...h.weeklyData];
851
+ const todayIndex = newWeeklyData.length - 1;
852
+ const wasCompleted = newWeeklyData[todayIndex];
853
+ newWeeklyData[todayIndex] = !wasCompleted;
854
+
855
+ return {
856
+ ...h,
857
+ weeklyData: newWeeklyData,
858
+ streak: !wasCompleted ? h.streak + 1 : Math.max(0, h.streak - 1),
859
+ bestStreak: !wasCompleted
860
+ ? Math.max(h.bestStreak, h.streak + 1)
861
+ : h.bestStreak,
862
+ };
863
+ }
864
+ return h;
865
+ }),
866
+ );
867
+ }, []);
868
+
869
+ const handleAddHabit = useCallback(
870
+ (newHabitData: { title: string; icon: string; color: string }) => {
871
+ const newHabit: Habit = {
872
+ id: Date.now().toString(),
873
+ title: newHabitData.title,
874
+ icon: newHabitData.icon,
875
+ color: newHabitData.color,
876
+ streak: 0,
877
+ bestStreak: 0,
878
+ weeklyData: [false, false, false, false, false, false, false],
879
+ monthlyData: Array(35).fill(0),
880
+ };
881
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
882
+ setHabits((prev) => [newHabit, ...prev]);
883
+ },
884
+ [],
885
+ );
886
+
887
+ const handleDeleteHabit = useCallback((id: string) => {
888
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
889
+ setHabits((prev) => prev.filter((h) => h.id !== id));
890
+ }, []);
891
+
892
+ const handlePreviousWeek = useCallback(() => {
893
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
894
+ setWeekOffset((prev) => prev - 1);
895
+ }, []);
896
+
897
+ const handleNextWeek = useCallback(() => {
898
+ if (weekOffset < 0) {
899
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
900
+ setWeekOffset((prev) => prev + 1);
901
+ }
902
+ }, [weekOffset]);
903
+
904
+ const panGesture = Gesture.Pan()
905
+ .activeOffsetX([-20, 20])
906
+ .onUpdate((e) => {
907
+ translateX.value = e.translationX;
908
+ })
909
+ .onEnd((e) => {
910
+ if (e.translationX > SWIPE_THRESHOLD) {
911
+ runOnJS(handlePreviousWeek)();
912
+ } else if (e.translationX < -SWIPE_THRESHOLD && weekOffset < 0) {
913
+ runOnJS(handleNextWeek)();
914
+ }
915
+ translateX.value = withSpring(0, { damping: 20 });
916
+ });
917
+
918
+ const animatedContentStyle = useAnimatedStyle<ViewStyle>(() => {
919
+ 'worklet';
920
+ return {
921
+ transform: [{ translateX: translateX.value * 0.3 }],
922
+ };
923
+ });
924
+
925
+ return (
926
+ <View
927
+ style={[styles.container, { backgroundColor: theme.colors.background }]}
928
+ >
929
+ <FocusAwareStatusBar />
930
+ <Stack.Screen options={{ headerShown: false }} />
931
+
932
+ <Animated.View entering={FadeInDown.duration(600)} style={styles.header}>
933
+ <View style={styles.headerContent}>
934
+ <View>
935
+ <Text style={[styles.title, { color: theme.colors.foreground }]}>
936
+ My Habits
937
+ </Text>
938
+ <View style={styles.subtitleRow}>
939
+ <FireAnimation size={24} isActive={true} />
940
+ <Text
941
+ style={[
942
+ styles.subtitle,
943
+ { color: theme.colors.mutedForeground },
944
+ ]}
945
+ >
946
+ Keep the streak alive!
947
+ </Text>
948
+ </View>
949
+ </View>
950
+
951
+ <TouchableOpacity
952
+ onPress={() => {
953
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
954
+ setShowAddModal(true);
955
+ }}
956
+ style={[
957
+ styles.addButton,
958
+ {
959
+ backgroundColor: isDark
960
+ ? 'rgba(255,255,255,0.1)'
961
+ : 'rgba(0,0,0,0.05)',
962
+ },
963
+ ]}
964
+ >
965
+ <Ionicons name="add" size={28} color={theme.colors.foreground} />
966
+ </TouchableOpacity>
967
+ </View>
968
+ </Animated.View>
969
+
970
+ <GestureDetector gesture={panGesture}>
971
+ <Animated.View
972
+ style={[styles.contentWrapper, animatedContentStyle as ViewStyle]}
973
+ >
974
+ <ScrollView
975
+ style={styles.scrollView}
976
+ contentContainerStyle={styles.scrollContent}
977
+ showsVerticalScrollIndicator={false}
978
+ >
979
+ <StatsSummary habits={habits} isDark={isDark} />
980
+ <WeekNavigator
981
+ currentWeekOffset={weekOffset}
982
+ onPrevious={handlePreviousWeek}
983
+ onNext={handleNextWeek}
984
+ isDark={isDark}
985
+ />
986
+
987
+ {habits.map((habit, index) => (
988
+ <HabitCard
989
+ key={habit.id}
990
+ habit={habit}
991
+ index={index}
992
+ isDark={isDark}
993
+ onToggle={handleToggleHabit}
994
+ onDelete={handleDeleteHabit}
995
+ />
996
+ ))}
997
+
998
+ <ContributionGraph habits={habits} isDark={isDark} />
999
+ </ScrollView>
1000
+ </Animated.View>
1001
+ </GestureDetector>
1002
+
1003
+ <AddHabitModal
1004
+ visible={showAddModal}
1005
+ onClose={() => setShowAddModal(false)}
1006
+ onAdd={handleAddHabit}
1007
+ />
1008
+ </View>
1009
+ );
1010
+ }
1011
+
1012
+ const styles = StyleSheet.create({
1013
+ container: { flex: 1 },
1014
+ header: { paddingHorizontal: 24, paddingTop: 56, paddingBottom: 16 },
1015
+ headerContent: {
1016
+ flexDirection: 'row',
1017
+ alignItems: 'center',
1018
+ justifyContent: 'space-between',
1019
+ },
1020
+ title: { fontSize: 30, fontWeight: '900', marginBottom: 4 },
1021
+ subtitleRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
1022
+ subtitle: { fontWeight: '600' },
1023
+ addButton: {
1024
+ width: 48,
1025
+ height: 48,
1026
+ borderRadius: 16,
1027
+ alignItems: 'center',
1028
+ justifyContent: 'center',
1029
+ },
1030
+ contentWrapper: { flex: 1 },
1031
+ scrollView: { flex: 1, paddingHorizontal: 24 },
1032
+ scrollContent: { paddingBottom: 100 },
1033
+
1034
+ fireContainer: { alignItems: 'center', justifyContent: 'center' },
1035
+ flameAbsolute: { position: 'absolute' },
1036
+
1037
+ confettiParticle: {
1038
+ position: 'absolute',
1039
+ width: 8,
1040
+ height: 8,
1041
+ borderRadius: 2,
1042
+ },
1043
+
1044
+ checkmarkContainer: { alignItems: 'center', justifyContent: 'center' },
1045
+ confettiContainer: { position: 'absolute', top: 0 },
1046
+ checkmarkBox: {
1047
+ width: 44,
1048
+ height: 44,
1049
+ borderRadius: 14,
1050
+ alignItems: 'center',
1051
+ justifyContent: 'center',
1052
+ },
1053
+ emptyCircle: { width: 18, height: 18, borderRadius: 9, borderWidth: 2 },
1054
+
1055
+ heatmapCell: { width: 14, height: 14, borderRadius: 3, margin: 1.5 },
1056
+
1057
+ weekNavigator: {
1058
+ flexDirection: 'row',
1059
+ alignItems: 'center',
1060
+ justifyContent: 'space-between',
1061
+ marginBottom: 16,
1062
+ paddingHorizontal: 8,
1063
+ },
1064
+ navButton: { padding: 8, borderRadius: 12 },
1065
+ weekLabel: { fontSize: 16, fontWeight: '600' },
1066
+
1067
+ statsContainer: {
1068
+ flexDirection: 'row',
1069
+ justifyContent: 'space-between',
1070
+ marginBottom: 24,
1071
+ paddingHorizontal: 4,
1072
+ },
1073
+ statCardWrapper: {
1074
+ flex: 1,
1075
+ marginHorizontal: 4,
1076
+ borderRadius: 16,
1077
+ overflow: 'hidden',
1078
+ },
1079
+ statCardGlass: { padding: 16, alignItems: 'center' },
1080
+ statIcon: {
1081
+ width: 40,
1082
+ height: 40,
1083
+ borderRadius: 12,
1084
+ alignItems: 'center',
1085
+ justifyContent: 'center',
1086
+ marginBottom: 8,
1087
+ },
1088
+ statValue: { fontSize: 20, fontWeight: '900' },
1089
+ statLabel: { fontSize: 12, fontWeight: '500', marginTop: 4 },
1090
+
1091
+ habitCardWrapper: { marginBottom: 20, borderRadius: 24, overflow: 'hidden' },
1092
+ habitCardGlass: { padding: 20 },
1093
+ habitHeader: {
1094
+ flexDirection: 'row',
1095
+ justifyContent: 'space-between',
1096
+ alignItems: 'center',
1097
+ marginBottom: 20,
1098
+ },
1099
+ habitInfo: { flexDirection: 'row', alignItems: 'center', gap: 12, flex: 1 },
1100
+ habitDetails: { flex: 1 },
1101
+ habitTitle: { fontSize: 18, fontWeight: '700' },
1102
+ streakRow: {
1103
+ flexDirection: 'row',
1104
+ alignItems: 'center',
1105
+ gap: 8,
1106
+ marginTop: 4,
1107
+ },
1108
+ streakText: { fontSize: 14, fontWeight: '600' },
1109
+ menuButton: { padding: 8 },
1110
+
1111
+ weeklyGrid: {
1112
+ flexDirection: 'row',
1113
+ justifyContent: 'space-between',
1114
+ alignItems: 'flex-end',
1115
+ },
1116
+ dayColumn: { alignItems: 'center', flex: 1 },
1117
+ dayLabel: { fontSize: 10, fontWeight: '700', marginBottom: 8 },
1118
+ dayCell: {
1119
+ width: 36,
1120
+ height: 36,
1121
+ borderRadius: 10,
1122
+ alignItems: 'center',
1123
+ justifyContent: 'center',
1124
+ },
1125
+
1126
+ miniStats: {
1127
+ flexDirection: 'row',
1128
+ justifyContent: 'space-between',
1129
+ marginTop: 16,
1130
+ paddingTop: 16,
1131
+ borderTopWidth: 1,
1132
+ },
1133
+ miniStatItem: { flexDirection: 'row', alignItems: 'center', gap: 8 },
1134
+ miniStatText: { fontSize: 12, fontWeight: '500' },
1135
+
1136
+ contributionCardWrapper: {
1137
+ marginBottom: 24,
1138
+ borderRadius: 24,
1139
+ overflow: 'hidden',
1140
+ },
1141
+ contributionCardGlass: { padding: 20 },
1142
+ contributionHeader: {
1143
+ flexDirection: 'row',
1144
+ alignItems: 'center',
1145
+ gap: 8,
1146
+ marginBottom: 16,
1147
+ },
1148
+ contributionTitle: { fontSize: 16, fontWeight: '700' },
1149
+ heatmapGrid: { flexDirection: 'row', justifyContent: 'center' },
1150
+ heatmapWeek: { marginHorizontal: 2 },
1151
+ legend: {
1152
+ flexDirection: 'row',
1153
+ alignItems: 'center',
1154
+ justifyContent: 'flex-end',
1155
+ marginTop: 16,
1156
+ gap: 4,
1157
+ },
1158
+ legendText: { fontSize: 10 },
1159
+ legendCell: { width: 12, height: 12, borderRadius: 2 },
1160
+ });