vibefast-cli 0.1.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 (250) hide show
  1. package/FINAL-STATUS.md +144 -0
  2. package/HOW-IT-WORKS.md +559 -0
  3. package/PLAN.md +453 -0
  4. package/README.md +129 -0
  5. package/RECIPES-READY.md +172 -0
  6. package/STATUS.md +199 -0
  7. package/SUCCESS.md +259 -0
  8. package/TESTING-CHECKLIST.md +450 -0
  9. package/cloudflare-worker/.wrangler/state/v3/kv/64907821e2634080acce34618d2f3d4c/blobs/11f2769953c717e188062bc644da97c1fd1e4d6d0813a226ce7567dba759afab0000019a736fb8d4 +1 -0
  10. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite +0 -0
  11. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-shm +0 -0
  12. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-wal +0 -0
  13. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite +0 -0
  14. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-shm +0 -0
  15. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-wal +0 -0
  16. package/cloudflare-worker/.wrangler/state/v3/r2/vibefast-recipes/blobs/620e8cf7c35d9806da25dee237e1d7e8b2432bd98f755b60e2c7f08a48d2c7b90000019a73736484 +0 -0
  17. package/cloudflare-worker/MIGRATION.md +160 -0
  18. package/cloudflare-worker/QUICKSTART.md +200 -0
  19. package/cloudflare-worker/README.md +242 -0
  20. package/cloudflare-worker/generate-token.js +32 -0
  21. package/cloudflare-worker/mini-native@latest.zip +0 -0
  22. package/cloudflare-worker/setup.sh +143 -0
  23. package/cloudflare-worker/test-recipe/apps/native/src/app/mini/index.tsx +15 -0
  24. package/cloudflare-worker/test-recipe/recipe.json +16 -0
  25. package/cloudflare-worker/worker.js +308 -0
  26. package/cloudflare-worker/wrangler.toml +13 -0
  27. package/dist/commands/add.d.ts +3 -0
  28. package/dist/commands/add.d.ts.map +1 -0
  29. package/dist/commands/add.js +149 -0
  30. package/dist/commands/add.js.map +1 -0
  31. package/dist/commands/devices.d.ts +3 -0
  32. package/dist/commands/devices.d.ts.map +1 -0
  33. package/dist/commands/devices.js +35 -0
  34. package/dist/commands/devices.js.map +1 -0
  35. package/dist/commands/doctor.d.ts +3 -0
  36. package/dist/commands/doctor.d.ts.map +1 -0
  37. package/dist/commands/doctor.js +67 -0
  38. package/dist/commands/doctor.js.map +1 -0
  39. package/dist/commands/list.d.ts +3 -0
  40. package/dist/commands/list.d.ts.map +1 -0
  41. package/dist/commands/list.js +40 -0
  42. package/dist/commands/list.js.map +1 -0
  43. package/dist/commands/login.d.ts +3 -0
  44. package/dist/commands/login.d.ts.map +1 -0
  45. package/dist/commands/login.js +23 -0
  46. package/dist/commands/login.js.map +1 -0
  47. package/dist/commands/logout.d.ts +3 -0
  48. package/dist/commands/logout.d.ts.map +1 -0
  49. package/dist/commands/logout.js +16 -0
  50. package/dist/commands/logout.js.map +1 -0
  51. package/dist/commands/remove.d.ts +3 -0
  52. package/dist/commands/remove.d.ts.map +1 -0
  53. package/dist/commands/remove.js +67 -0
  54. package/dist/commands/remove.js.map +1 -0
  55. package/dist/core/__tests__/journal.test.d.ts +2 -0
  56. package/dist/core/__tests__/journal.test.d.ts.map +1 -0
  57. package/dist/core/__tests__/journal.test.js +101 -0
  58. package/dist/core/__tests__/journal.test.js.map +1 -0
  59. package/dist/core/__tests__/validate.test.d.ts +2 -0
  60. package/dist/core/__tests__/validate.test.d.ts.map +1 -0
  61. package/dist/core/__tests__/validate.test.js +53 -0
  62. package/dist/core/__tests__/validate.test.js.map +1 -0
  63. package/dist/core/archive.d.ts +2 -0
  64. package/dist/core/archive.d.ts.map +1 -0
  65. package/dist/core/archive.js +59 -0
  66. package/dist/core/archive.js.map +1 -0
  67. package/dist/core/auth.d.ts +15 -0
  68. package/dist/core/auth.d.ts.map +1 -0
  69. package/dist/core/auth.js +76 -0
  70. package/dist/core/auth.js.map +1 -0
  71. package/dist/core/codemod.d.ts +20 -0
  72. package/dist/core/codemod.d.ts.map +1 -0
  73. package/dist/core/codemod.js +150 -0
  74. package/dist/core/codemod.js.map +1 -0
  75. package/dist/core/fsx.d.ts +12 -0
  76. package/dist/core/fsx.d.ts.map +1 -0
  77. package/dist/core/fsx.js +70 -0
  78. package/dist/core/fsx.js.map +1 -0
  79. package/dist/core/http.d.ts +30 -0
  80. package/dist/core/http.d.ts.map +1 -0
  81. package/dist/core/http.js +95 -0
  82. package/dist/core/http.js.map +1 -0
  83. package/dist/core/journal.d.ts +18 -0
  84. package/dist/core/journal.d.ts.map +1 -0
  85. package/dist/core/journal.js +34 -0
  86. package/dist/core/journal.js.map +1 -0
  87. package/dist/core/log.d.ts +8 -0
  88. package/dist/core/log.d.ts.map +1 -0
  89. package/dist/core/log.js +9 -0
  90. package/dist/core/log.js.map +1 -0
  91. package/dist/core/pathGuard.d.ts +3 -0
  92. package/dist/core/pathGuard.d.ts.map +1 -0
  93. package/dist/core/pathGuard.js +18 -0
  94. package/dist/core/pathGuard.js.map +1 -0
  95. package/dist/core/paths.d.ts +11 -0
  96. package/dist/core/paths.d.ts.map +1 -0
  97. package/dist/core/paths.js +22 -0
  98. package/dist/core/paths.js.map +1 -0
  99. package/dist/core/validate.d.ts +8 -0
  100. package/dist/core/validate.d.ts.map +1 -0
  101. package/dist/core/validate.js +27 -0
  102. package/dist/core/validate.js.map +1 -0
  103. package/dist/index.d.ts +3 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +23 -0
  106. package/dist/index.js.map +1 -0
  107. package/docs/decisions.md +55 -0
  108. package/package.json +39 -0
  109. package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
  110. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
  111. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
  112. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
  113. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/index.ts +4 -0
  114. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
  115. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
  116. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
  117. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
  118. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
  119. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
  120. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
  121. package/recipes/audio-recorder/recipe.json +22 -0
  122. package/recipes/audio-recorder@latest.zip +0 -0
  123. package/recipes/charts/apps/native/src/app/charts/index.tsx +3 -0
  124. package/recipes/charts/apps/native/src/features/charts/README.md +185 -0
  125. package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +223 -0
  126. package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +40 -0
  127. package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +143 -0
  128. package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +196 -0
  129. package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +65 -0
  130. package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +143 -0
  131. package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +246 -0
  132. package/recipes/charts/apps/native/src/features/charts/components/index.ts +10 -0
  133. package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +308 -0
  134. package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +180 -0
  135. package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +188 -0
  136. package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +265 -0
  137. package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +322 -0
  138. package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +183 -0
  139. package/recipes/charts/apps/native/src/features/charts/types/index.ts +66 -0
  140. package/recipes/charts/recipe.json +22 -0
  141. package/recipes/charts@latest.zip +0 -0
  142. package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
  143. package/recipes/chatbot/apps/native/src/features/chatbot/app/index.tsx +302 -0
  144. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
  145. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +469 -0
  146. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
  147. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +246 -0
  148. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
  149. package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +115 -0
  150. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
  151. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
  152. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
  153. package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
  154. package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +173 -0
  155. package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
  156. package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
  157. package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
  158. package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +20 -0
  159. package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
  160. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +143 -0
  161. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +664 -0
  162. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +359 -0
  163. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
  164. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +79 -0
  165. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
  166. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
  167. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +207 -0
  168. package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +86 -0
  169. package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +162 -0
  170. package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +62 -0
  171. package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +40 -0
  172. package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +238 -0
  173. package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +180 -0
  174. package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +60 -0
  175. package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
  176. package/recipes/chatbot/recipe.json +22 -0
  177. package/recipes/chatbot@latest.zip +0 -0
  178. package/recipes/image-generator/apps/native/src/app/image-generator/gallery.tsx +3 -0
  179. package/recipes/image-generator/apps/native/src/app/image-generator/index.tsx +3 -0
  180. package/recipes/image-generator/apps/native/src/features/image-generator/app/_layout.tsx +25 -0
  181. package/recipes/image-generator/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
  182. package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +237 -0
  183. package/recipes/image-generator/apps/native/src/features/image-generator/components/gallery-image.tsx +26 -0
  184. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
  185. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +210 -0
  186. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
  187. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
  188. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
  189. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +93 -0
  190. package/recipes/image-generator/apps/native/src/features/image-generator/models/models.ts +66 -0
  191. package/recipes/image-generator/apps/native/src/features/image-generator/services/image-gallery-service.ts +98 -0
  192. package/recipes/image-generator/apps/native/src/features/image-generator/services/image-save-service.ts +121 -0
  193. package/recipes/image-generator/recipe.json +22 -0
  194. package/recipes/image-generator@latest.zip +0 -0
  195. package/recipes/quiz/apps/native/src/app/quiz/index.tsx +47 -0
  196. package/recipes/quiz/apps/native/src/features/quiz/components/question.tsx +67 -0
  197. package/recipes/quiz/apps/native/src/features/quiz/config.ts +11 -0
  198. package/recipes/quiz/apps/native/src/features/quiz/index.tsx +133 -0
  199. package/recipes/quiz/recipe.json +22 -0
  200. package/recipes/quiz@latest.zip +0 -0
  201. package/recipes/tracker-app/apps/native/src/app/tracker-app/index.tsx +1 -0
  202. package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +108 -0
  203. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/animated-number.tsx +102 -0
  204. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/calorie-card.tsx +66 -0
  205. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/circular-progress.tsx +97 -0
  206. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/floating-add-button.tsx +27 -0
  207. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/macro-card.tsx +80 -0
  208. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/promo-banner.tsx +98 -0
  209. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/recently-logged.tsx +64 -0
  210. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/week-calendar.tsx +68 -0
  211. package/recipes/tracker-app/recipe.json +22 -0
  212. package/recipes/tracker-app@latest.zip +0 -0
  213. package/recipes/upload-all.sh +32 -0
  214. package/recipes/voice-bot/apps/native/src/app/voice-bot/index.tsx +27 -0
  215. package/recipes/voice-bot/apps/native/src/features/voice-bot/README.md +185 -0
  216. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/conversation-status.tsx +76 -0
  217. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/index.ts +4 -0
  218. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/message-input.tsx +98 -0
  219. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-bot-screen.tsx +173 -0
  220. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-controls.tsx +73 -0
  221. package/recipes/voice-bot/apps/native/src/features/voice-bot/index.ts +3 -0
  222. package/recipes/voice-bot/apps/native/src/features/voice-bot/services/index.ts +1 -0
  223. package/recipes/voice-bot/apps/native/src/features/voice-bot/services/use-voice-bot.ts +161 -0
  224. package/recipes/voice-bot/apps/native/src/features/voice-bot/types.ts +29 -0
  225. package/recipes/voice-bot/recipe.json +22 -0
  226. package/recipes/voice-bot@latest.zip +0 -0
  227. package/scripts/create-recipes.mjs +189 -0
  228. package/src/commands/add.ts +183 -0
  229. package/src/commands/devices.ts +38 -0
  230. package/src/commands/doctor.ts +67 -0
  231. package/src/commands/list.ts +45 -0
  232. package/src/commands/login.ts +24 -0
  233. package/src/commands/logout.ts +15 -0
  234. package/src/commands/remove.ts +78 -0
  235. package/src/core/__tests__/journal.test.ts +119 -0
  236. package/src/core/__tests__/validate.test.ts +64 -0
  237. package/src/core/archive.ts +69 -0
  238. package/src/core/auth.ts +103 -0
  239. package/src/core/codemod.ts +211 -0
  240. package/src/core/fsx.ts +80 -0
  241. package/src/core/http.ts +136 -0
  242. package/src/core/journal.ts +64 -0
  243. package/src/core/log.ts +9 -0
  244. package/src/core/pathGuard.ts +22 -0
  245. package/src/core/paths.ts +33 -0
  246. package/src/core/validate.ts +44 -0
  247. package/src/index.ts +27 -0
  248. package/test-critical-cases.mjs +258 -0
  249. package/tsconfig.json +21 -0
  250. package/vitest.config.mts +12 -0
@@ -0,0 +1,180 @@
1
+ import {
2
+ Canvas,
3
+ Circle as SkCircle,
4
+ Path as SkPath,
5
+ Skia,
6
+ } from '@shopify/react-native-skia';
7
+ import React, { useEffect, useMemo, useState } from 'react';
8
+ import { type LayoutChangeEvent, View, type ViewStyle } from 'react-native';
9
+ import Svg, { Text as SvgText } from 'react-native-svg';
10
+
11
+ import { useThemeConfig } from '@/lib/use-theme-config';
12
+
13
+ interface ChartConfig {
14
+ width?: number;
15
+ height?: number;
16
+ showLabels?: boolean;
17
+ animated?: boolean;
18
+ duration?: number;
19
+ maxValue?: number;
20
+ }
21
+
22
+ interface RadarChartDataPoint {
23
+ label: string;
24
+ value: number;
25
+ }
26
+
27
+ type Props = {
28
+ data: RadarChartDataPoint[];
29
+ config?: ChartConfig;
30
+ style?: ViewStyle;
31
+ };
32
+
33
+ export const RadarChart = ({ data, config = {}, style }: Props) => {
34
+ const hasData = data.length > 0;
35
+ const [containerWidth, setContainerWidth] = useState(300);
36
+ const {
37
+ height = 200,
38
+ showLabels = true,
39
+ animated = true,
40
+ duration = 1000,
41
+ maxValue,
42
+ } = config;
43
+ const chartWidth = containerWidth || config.width || 300;
44
+
45
+ const theme = useThemeConfig();
46
+ const primaryColor = theme.colors.primary as string;
47
+ const mutedColor = theme.colors.mutedForeground as string;
48
+
49
+ const [progress, setProgress] = useState(1);
50
+
51
+ const onLayout = (e: LayoutChangeEvent) => {
52
+ const { width } = e.nativeEvent.layout;
53
+ if (width > 0) setContainerWidth(width);
54
+ };
55
+
56
+ useEffect(() => {
57
+ if (!animated) {
58
+ setProgress(1);
59
+ return;
60
+ }
61
+ let raf: number;
62
+ const start = Date.now();
63
+ const easeInOutCubic = (t: number) =>
64
+ t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
65
+ const tick = () => {
66
+ const elapsed = Date.now() - start;
67
+ const t = Math.min(1, elapsed / (duration || 1000));
68
+ setProgress(easeInOutCubic(t));
69
+ if (t < 1) raf = requestAnimationFrame(tick);
70
+ };
71
+ setProgress(0);
72
+ raf = requestAnimationFrame(tick);
73
+ return () => cancelAnimationFrame(raf);
74
+ }, [animated, duration, data]);
75
+
76
+ const centerX = chartWidth / 2;
77
+ const centerY = height / 2;
78
+ const radius = Math.min(chartWidth, height) / 2 - 40;
79
+ const maxVal =
80
+ maxValue || (hasData ? Math.max(...data.map((d) => d.value)) : 1);
81
+
82
+ const angleStep = hasData ? (2 * Math.PI) / data.length : 0;
83
+ const points = useMemo(() => {
84
+ return (hasData ? data : []).map((item, index) => {
85
+ const angle = index * angleStep - Math.PI / 2;
86
+ const dist = (item.value / maxVal) * radius * progress;
87
+ return {
88
+ x: centerX + dist * Math.cos(angle),
89
+ y: centerY + dist * Math.sin(angle),
90
+ labelX: centerX + (radius + 20) * Math.cos(angle),
91
+ labelY: centerY + (radius + 20) * Math.sin(angle),
92
+ label: item.label,
93
+ };
94
+ });
95
+ }, [data, hasData, angleStep, centerX, centerY, radius, progress, maxVal]);
96
+
97
+ const radarPath = useMemo(() => {
98
+ if (!points.length) return Skia.Path.Make();
99
+ const p = Skia.Path.Make();
100
+ p.moveTo(points[0].x, points[0].y);
101
+ for (let i = 1; i < points.length; i++) p.lineTo(points[i].x, points[i].y);
102
+ p.close();
103
+ return p;
104
+ }, [points]);
105
+
106
+ if (!hasData) return null;
107
+
108
+ return (
109
+ <View style={[{ width: '100%', height }, style]} onLayout={onLayout}>
110
+ <Canvas style={{ width: chartWidth, height }}>
111
+ {/* Grid circles */}
112
+ {[0.2, 0.4, 0.6, 0.8, 1].map((ratio, idx) => (
113
+ <SkCircle
114
+ key={`grid-${idx}`}
115
+ cx={centerX}
116
+ cy={centerY}
117
+ r={radius * ratio}
118
+ color={mutedColor}
119
+ style="stroke"
120
+ strokeWidth={0.5}
121
+ opacity={0.3}
122
+ />
123
+ ))}
124
+ {/* Grid spokes */}
125
+ {(() => {
126
+ const p = Skia.Path.Make();
127
+ data.forEach((_, index) => {
128
+ const angle = index * angleStep - Math.PI / 2;
129
+ const endX = centerX + radius * Math.cos(angle);
130
+ const endY = centerY + radius * Math.sin(angle);
131
+ p.moveTo(centerX, centerY);
132
+ p.lineTo(endX, endY);
133
+ });
134
+ return (
135
+ <SkPath
136
+ path={p}
137
+ color={mutedColor}
138
+ style="stroke"
139
+ strokeWidth={0.5}
140
+ opacity={0.3}
141
+ />
142
+ );
143
+ })()}
144
+ {/* Radar polygon */}
145
+ <SkPath
146
+ path={radarPath}
147
+ color={primaryColor}
148
+ style="stroke"
149
+ strokeWidth={2}
150
+ />
151
+ <SkPath
152
+ path={radarPath}
153
+ color={primaryColor}
154
+ style="fill"
155
+ opacity={0.3}
156
+ />
157
+ </Canvas>
158
+ {showLabels && (
159
+ <Svg
160
+ width={chartWidth}
161
+ height={height}
162
+ style={{ position: 'absolute', left: 0, top: 0 }}
163
+ >
164
+ {points.map((pt, idx) => (
165
+ <SvgText
166
+ key={`label-${idx}`}
167
+ x={pt.labelX}
168
+ y={pt.labelY}
169
+ textAnchor="middle"
170
+ fontSize={12}
171
+ fill={mutedColor}
172
+ >
173
+ {pt.label}
174
+ </SvgText>
175
+ ))}
176
+ </Svg>
177
+ )}
178
+ </View>
179
+ );
180
+ };
@@ -0,0 +1,188 @@
1
+ import {
2
+ Canvas,
3
+ Group,
4
+ Path as SkPath,
5
+ Skia,
6
+ } from '@shopify/react-native-skia';
7
+ import React, { useEffect, useState } from 'react';
8
+ import { type LayoutChangeEvent, View, type ViewStyle } from 'react-native';
9
+ import Svg, { Text as SvgText } from 'react-native-svg';
10
+
11
+ import { Text } from '@/components/ui';
12
+ import { useThemeConfig } from '@/lib/use-theme-config';
13
+
14
+ interface ChartConfig {
15
+ padding?: number;
16
+ animated?: boolean;
17
+ duration?: number;
18
+ gradient?: boolean; // not applied to stroke in Skia v1; using solid color
19
+ }
20
+
21
+ interface ChartDataPoint {
22
+ label: string;
23
+ value: number;
24
+ color?: string;
25
+ }
26
+
27
+ type Props = {
28
+ data: ChartDataPoint[];
29
+ config?: ChartConfig;
30
+ style?: ViewStyle;
31
+ };
32
+
33
+ export const RadialBarChart = ({ data, config = {}, style }: Props) => {
34
+ const [containerSize, setContainerSize] = useState(200);
35
+ const { padding = 20, animated = true, duration = 1000 } = config;
36
+
37
+ const theme = useThemeConfig();
38
+ const primaryColor = theme.colors.primary as string;
39
+ const mutedColor = theme.colors.mutedForeground as string;
40
+
41
+ const [progress, setProgress] = useState(1);
42
+
43
+ const onLayout = (e: LayoutChangeEvent) => {
44
+ const { width, height } = e.nativeEvent.layout;
45
+ const size = Math.min(width, height);
46
+ if (size > 0) setContainerSize(size);
47
+ };
48
+
49
+ useEffect(() => {
50
+ if (!animated) {
51
+ setProgress(1);
52
+ return;
53
+ }
54
+ let raf: number;
55
+ const start = Date.now();
56
+ const easeInOutCubic = (t: number) =>
57
+ t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
58
+ const tick = () => {
59
+ const elapsed = Date.now() - start;
60
+ const t = Math.min(1, elapsed / (duration || 1000));
61
+ setProgress(easeInOutCubic(t));
62
+ if (t < 1) raf = requestAnimationFrame(tick);
63
+ };
64
+ setProgress(0);
65
+ raf = requestAnimationFrame(tick);
66
+ return () => cancelAnimationFrame(raf);
67
+ }, [animated, duration, data]);
68
+
69
+ if (!data.length) return null;
70
+
71
+ const maxValue = Math.max(...data.map((d) => d.value));
72
+ const size = containerSize || 200;
73
+ const center = size / 2;
74
+ const maxRadius = (size - padding * 2) / 2;
75
+ const strokeWidth = maxRadius / (data.length + 1);
76
+
77
+ const colors = [
78
+ primaryColor,
79
+ '#3B82F6',
80
+ '#10B981',
81
+ '#F59E0B',
82
+ '#8B5CF6',
83
+ '#EC4899',
84
+ ];
85
+
86
+ return (
87
+ <View style={[{ width: '100%' }, style]}>
88
+ <View
89
+ style={{
90
+ width: '100%',
91
+ height: size,
92
+ alignItems: 'center',
93
+ justifyContent: 'center',
94
+ }}
95
+ onLayout={onLayout}
96
+ >
97
+ <Canvas style={{ width: size, height: size }}>
98
+ <Group>
99
+ {data.map((item, index) => {
100
+ const r = maxRadius - index * strokeWidth - strokeWidth / 2;
101
+ const track = Skia.Path.Make();
102
+ track.addCircle(center, center, r);
103
+ const arc = Skia.Path.Make();
104
+ arc.addCircle(center, center, r);
105
+ const color = (item.color ||
106
+ colors[index % colors.length]) as string;
107
+ const end = (item.value / maxValue) * progress;
108
+ return (
109
+ <React.Fragment key={`rb-${index}`}>
110
+ <SkPath
111
+ path={track}
112
+ color={mutedColor}
113
+ style="stroke"
114
+ strokeWidth={strokeWidth * 0.8}
115
+ opacity={0.15}
116
+ />
117
+ <SkPath
118
+ path={arc}
119
+ color={color}
120
+ style="stroke"
121
+ strokeWidth={strokeWidth * 0.8}
122
+ start={0}
123
+ end={end}
124
+ strokeCap="round"
125
+ />
126
+ </React.Fragment>
127
+ );
128
+ })}
129
+ </Group>
130
+ </Canvas>
131
+ {/* Center values */}
132
+ {data.length > 0 && (
133
+ <Svg
134
+ width={size}
135
+ height={size}
136
+ style={{ position: 'absolute', left: 0, top: 0 }}
137
+ >
138
+ <SvgText
139
+ x={center}
140
+ y={center - 5}
141
+ textAnchor="middle"
142
+ fontSize={16}
143
+ fill={primaryColor}
144
+ fontWeight="bold"
145
+ >
146
+ {data.reduce((sum, item) => sum + item.value, 0)}
147
+ </SvgText>
148
+ <SvgText
149
+ x={center}
150
+ y={center + 15}
151
+ textAnchor="middle"
152
+ fontSize={12}
153
+ fill={mutedColor}
154
+ >
155
+ Total
156
+ </SvgText>
157
+ </Svg>
158
+ )}
159
+ </View>
160
+ {/* Legend */}
161
+ <View style={{ marginTop: 15 }}>
162
+ {data.map((item, index) => (
163
+ <View
164
+ key={`legend-${index}`}
165
+ style={{
166
+ flexDirection: 'row',
167
+ alignItems: 'center',
168
+ marginBottom: 8,
169
+ }}
170
+ >
171
+ <View
172
+ style={{
173
+ width: 12,
174
+ height: 12,
175
+ borderRadius: 6,
176
+ backgroundColor: item.color || colors[index % colors.length],
177
+ marginRight: 10,
178
+ }}
179
+ />
180
+ <Text>
181
+ {item.label}: {item.value}
182
+ </Text>
183
+ </View>
184
+ ))}
185
+ </View>
186
+ </View>
187
+ );
188
+ };
@@ -0,0 +1,265 @@
1
+ import {
2
+ Canvas,
3
+ Group,
4
+ LinearGradient,
5
+ Path as SkPath,
6
+ Skia,
7
+ vec,
8
+ } from '@shopify/react-native-skia';
9
+ import React, { useEffect, useMemo, useState } from 'react';
10
+ import { type LayoutChangeEvent, View, type ViewStyle } from 'react-native';
11
+ import Svg, {
12
+ G as SvgG,
13
+ Line as SvgLine,
14
+ Text as SvgText,
15
+ } from 'react-native-svg';
16
+
17
+ import { useThemeConfig } from '@/lib/use-theme-config';
18
+
19
+ interface ChartConfig {
20
+ width?: number;
21
+ height?: number;
22
+ padding?: number;
23
+ showGrid?: boolean;
24
+ showLabels?: boolean;
25
+ animated?: boolean;
26
+ duration?: number;
27
+ }
28
+
29
+ export interface StackedAreaDataPoint {
30
+ x: number;
31
+ y: number[];
32
+ label?: string;
33
+ }
34
+
35
+ type Props = {
36
+ data: StackedAreaDataPoint[];
37
+ colors?: string[];
38
+ config?: ChartConfig;
39
+ style?: ViewStyle;
40
+ categories?: string[];
41
+ };
42
+
43
+ const smoothPath = (pts: { x: number; y: number }[]) => {
44
+ const p = Skia.Path.Make();
45
+ if (!pts.length) return p;
46
+ p.moveTo(pts[0].x, pts[0].y);
47
+ for (let i = 1; i < pts.length; i++) {
48
+ const prev = pts[i - 1];
49
+ const curr = pts[i];
50
+ const cpx = (prev.x + curr.x) / 2;
51
+ const cpy = prev.y;
52
+ p.quadTo(cpx, cpy, curr.x, curr.y);
53
+ }
54
+ return p;
55
+ };
56
+
57
+ const areaPathFromTopAndBase = (
58
+ top: { x: number; y: number }[],
59
+ baseY: number,
60
+ ) => {
61
+ const topPath = smoothPath(top);
62
+ const p = topPath.copy();
63
+ if (!top.length) return p;
64
+ const last = top[top.length - 1];
65
+ const first = top[0];
66
+ p.lineTo(last.x, baseY);
67
+ p.lineTo(first.x, baseY);
68
+ p.close();
69
+ return p;
70
+ };
71
+
72
+ export const StackedAreaChart = ({
73
+ data,
74
+ colors = [],
75
+ config = {},
76
+ style,
77
+ categories = [],
78
+ }: Props) => {
79
+ const [containerWidth, setContainerWidth] = useState(300);
80
+ const {
81
+ height = 200,
82
+ padding = 20,
83
+ showGrid = true,
84
+ showLabels = true,
85
+ animated = true,
86
+ duration = 1000,
87
+ } = config;
88
+ const chartWidth = containerWidth || config.width || 300;
89
+
90
+ const theme = useThemeConfig();
91
+ const primaryColor = theme.colors.primary as string;
92
+ const mutedColor = theme.colors.mutedForeground as string;
93
+
94
+ const [progress, setProgress] = useState(1);
95
+
96
+ const onLayout = (e: LayoutChangeEvent) => {
97
+ const { width } = e.nativeEvent.layout;
98
+ if (width > 0) setContainerWidth(width);
99
+ };
100
+
101
+ useEffect(() => {
102
+ if (!animated) {
103
+ setProgress(1);
104
+ return;
105
+ }
106
+ let raf: number;
107
+ const start = Date.now();
108
+ const easeInOutCubic = (t: number) =>
109
+ t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
110
+ const tick = () => {
111
+ const elapsed = Date.now() - start;
112
+ const t = Math.min(1, elapsed / (duration || 1000));
113
+ setProgress(easeInOutCubic(t));
114
+ if (t < 1) raf = requestAnimationFrame(tick);
115
+ };
116
+ setProgress(0);
117
+ raf = requestAnimationFrame(tick);
118
+ return () => cancelAnimationFrame(raf);
119
+ }, [animated, duration, data]);
120
+
121
+ const stackedData = data.map((point) => {
122
+ const cumulative = point.y.reduce((acc, val) => {
123
+ acc.push((acc[acc.length - 1] || 0) + val);
124
+ return acc;
125
+ }, [] as number[]);
126
+ return { ...point, cumulative };
127
+ });
128
+
129
+ const maxValue = Math.max(
130
+ ...stackedData.map((d) => Math.max(...d.cumulative)),
131
+ );
132
+ const seriesCount = data[0]?.y.length || 0;
133
+ const innerChartWidth = chartWidth - padding * 2;
134
+ const chartHeight = height - padding * 2;
135
+
136
+ const defaultColors = [
137
+ primaryColor,
138
+ '#8884d8',
139
+ '#82ca9d',
140
+ '#ffc658',
141
+ '#ff7300',
142
+ '#00ff00',
143
+ '#0088fe',
144
+ ];
145
+ const seriesColors =
146
+ colors.length >= seriesCount
147
+ ? colors
148
+ : [...colors, ...defaultColors].slice(0, seriesCount);
149
+
150
+ const topSeriesPoints = useMemo(() => {
151
+ return Array.from({ length: seriesCount }, (_, seriesIndex) =>
152
+ stackedData.map((point, pointIndex) => ({
153
+ x: padding + (pointIndex / (data.length - 1)) * innerChartWidth,
154
+ y:
155
+ padding +
156
+ ((maxValue - point.cumulative[seriesIndex]) / maxValue) * chartHeight,
157
+ })),
158
+ );
159
+ }, [
160
+ stackedData,
161
+ seriesCount,
162
+ padding,
163
+ data.length,
164
+ innerChartWidth,
165
+ maxValue,
166
+ chartHeight,
167
+ ]);
168
+
169
+ if (!data.length) return null;
170
+
171
+ return (
172
+ <View style={[{ width: '100%', height }, style]} onLayout={onLayout}>
173
+ <Canvas style={{ width: chartWidth, height }}>
174
+ {/* Grid lines (kept in SVG overlay for exact label parity) */}
175
+ {/* Areas */}
176
+ <Group>
177
+ {Array.from({ length: seriesCount }, (_, seriesIndex) => {
178
+ const top = topSeriesPoints[seriesIndex];
179
+ const path = areaPathFromTopAndBase(top, height - padding);
180
+ return (
181
+ <SkPath
182
+ key={`area-${seriesIndex}`}
183
+ path={path}
184
+ style="fill"
185
+ opacity={
186
+ 0.3 + 0.4 * (1 - seriesIndex / Math.max(1, seriesCount - 1))
187
+ }
188
+ >
189
+ <LinearGradient
190
+ start={vec(0, padding)}
191
+ end={vec(0, height - padding)}
192
+ colors={[
193
+ seriesColors[seriesIndex],
194
+ seriesColors[seriesIndex],
195
+ ]}
196
+ />
197
+ </SkPath>
198
+ );
199
+ })}
200
+ </Group>
201
+ {/* Top lines */}
202
+ <Group>
203
+ {Array.from({ length: seriesCount }, (_, seriesIndex) => {
204
+ const top = topSeriesPoints[seriesIndex];
205
+ const p = smoothPath(top);
206
+ return (
207
+ <SkPath
208
+ key={`line-${seriesIndex}`}
209
+ path={p}
210
+ color={seriesColors[seriesIndex]}
211
+ style="stroke"
212
+ strokeWidth={1.5}
213
+ end={progress}
214
+ />
215
+ );
216
+ })}
217
+ </Group>
218
+ </Canvas>
219
+ {showGrid && (
220
+ <Svg
221
+ width={chartWidth}
222
+ height={height}
223
+ style={{ position: 'absolute', left: 0, top: 0 }}
224
+ >
225
+ <SvgG>
226
+ {[0, 0.25, 0.5, 0.75, 1].map((ratio, index) => (
227
+ <SvgLine
228
+ key={`grid-${index}`}
229
+ x1={padding}
230
+ y1={padding + ratio * chartHeight}
231
+ x2={chartWidth - padding}
232
+ y2={padding + ratio * chartHeight}
233
+ stroke={mutedColor}
234
+ strokeWidth={0.5}
235
+ opacity={0.3}
236
+ />
237
+ ))}
238
+ </SvgG>
239
+ </Svg>
240
+ )}
241
+ {showLabels && (
242
+ <Svg
243
+ width={chartWidth}
244
+ height={height}
245
+ style={{ position: 'absolute', left: 0, top: 0 }}
246
+ >
247
+ <SvgG>
248
+ {data.map((point, index) => (
249
+ <SvgText
250
+ key={`x-${index}`}
251
+ x={padding + (index / (data.length - 1)) * innerChartWidth}
252
+ y={height - 5}
253
+ textAnchor="middle"
254
+ fontSize={10}
255
+ fill={mutedColor}
256
+ >
257
+ {point.label || String(point.x)}
258
+ </SvgText>
259
+ ))}
260
+ </SvgG>
261
+ </Svg>
262
+ )}
263
+ </View>
264
+ );
265
+ };