@expokit/cli 0.0.0-stage → 0.2.0

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 (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -2
  3. package/bin/expo-kit.js +1204 -0
  4. package/bin/lib.mjs +343 -0
  5. package/bin/mcp.mjs +338 -0
  6. package/package.json +44 -4
  7. package/registry/_docs/accordion.json +99 -0
  8. package/registry/_docs/action-sheet.json +110 -0
  9. package/registry/_docs/alert.json +127 -0
  10. package/registry/_docs/avatar.json +203 -0
  11. package/registry/_docs/badge.json +117 -0
  12. package/registry/_docs/bottom-nav.json +119 -0
  13. package/registry/_docs/bottom-sheet.json +158 -0
  14. package/registry/_docs/bullet.json +24 -0
  15. package/registry/_docs/button-3d.json +122 -0
  16. package/registry/_docs/button.json +115 -0
  17. package/registry/_docs/calendar.json +182 -0
  18. package/registry/_docs/card-stack.json +108 -0
  19. package/registry/_docs/card.json +111 -0
  20. package/registry/_docs/carousel.json +102 -0
  21. package/registry/_docs/checkbox.json +93 -0
  22. package/registry/_docs/checkout.json +102 -0
  23. package/registry/_docs/chip-group.json +116 -0
  24. package/registry/_docs/color-swatch.json +66 -0
  25. package/registry/_docs/confetti.json +82 -0
  26. package/registry/_docs/coupon.json +176 -0
  27. package/registry/_docs/cube-pager.json +84 -0
  28. package/registry/_docs/dashboard.json +120 -0
  29. package/registry/_docs/date-picker.json +249 -0
  30. package/registry/_docs/dialog.json +162 -0
  31. package/registry/_docs/drawer.json +171 -0
  32. package/registry/_docs/fab.json +120 -0
  33. package/registry/_docs/flip-card.json +111 -0
  34. package/registry/_docs/floating-input.json +191 -0
  35. package/registry/_docs/gift-card.json +108 -0
  36. package/registry/_docs/glass-button.json +118 -0
  37. package/registry/_docs/glass-nav-bar.json +78 -0
  38. package/registry/_docs/glass-search-bar.json +90 -0
  39. package/registry/_docs/glass-segmented.json +66 -0
  40. package/registry/_docs/glass-sheet.json +108 -0
  41. package/registry/_docs/glass-tab-bar.json +76 -0
  42. package/registry/_docs/glass.json +78 -0
  43. package/registry/_docs/inbox.json +72 -0
  44. package/registry/_docs/isometric-bar.json +72 -0
  45. package/registry/_docs/kit.json +11 -0
  46. package/registry/_docs/kpi-card.json +11 -0
  47. package/registry/_docs/like-button.json +72 -0
  48. package/registry/_docs/liquidglass-tab.json +27 -0
  49. package/registry/_docs/list-item.json +200 -0
  50. package/registry/_docs/loyalty-card.json +108 -0
  51. package/registry/_docs/morph-button.json +108 -0
  52. package/registry/_docs/number-ticker.json +96 -0
  53. package/registry/_docs/offers.json +78 -0
  54. package/registry/_docs/onboarding.json +66 -0
  55. package/registry/_docs/otp-input.json +134 -0
  56. package/registry/_docs/pagination-dots.json +88 -0
  57. package/registry/_docs/parallax-layers.json +84 -0
  58. package/registry/_docs/parallax-scroll.json +186 -0
  59. package/registry/_docs/password-input.json +92 -0
  60. package/registry/_docs/payment-card.json +153 -0
  61. package/registry/_docs/pixel-bar.json +66 -0
  62. package/registry/_docs/polar-area.json +78 -0
  63. package/registry/_docs/profile.json +84 -0
  64. package/registry/_docs/progress-steps.json +92 -0
  65. package/registry/_docs/progress.json +132 -0
  66. package/registry/_docs/radio-group.json +92 -0
  67. package/registry/_docs/rating.json +90 -0
  68. package/registry/_docs/ring-carousel.json +102 -0
  69. package/registry/_docs/scratch-card.json +90 -0
  70. package/registry/_docs/search-bar.json +140 -0
  71. package/registry/_docs/search-results.json +120 -0
  72. package/registry/_docs/segmented-control.json +103 -0
  73. package/registry/_docs/select.json +252 -0
  74. package/registry/_docs/settings.json +72 -0
  75. package/registry/_docs/shimmer-button.json +78 -0
  76. package/registry/_docs/sign-in.json +198 -0
  77. package/registry/_docs/skeleton.json +142 -0
  78. package/registry/_docs/slide-to-confirm.json +84 -0
  79. package/registry/_docs/slider.json +157 -0
  80. package/registry/_docs/spin-wheel.json +114 -0
  81. package/registry/_docs/spinner.json +91 -0
  82. package/registry/_docs/stepper.json +168 -0
  83. package/registry/_docs/swipe-deck.json +102 -0
  84. package/registry/_docs/swipeable-row.json +72 -0
  85. package/registry/_docs/switch.json +104 -0
  86. package/registry/_docs/tabs.json +105 -0
  87. package/registry/_docs/tag-input.json +128 -0
  88. package/registry/_docs/textarea.json +175 -0
  89. package/registry/_docs/ticket-card.json +174 -0
  90. package/registry/_docs/ticket-shape.json +11 -0
  91. package/registry/_docs/tilt-card.json +141 -0
  92. package/registry/_docs/time-picker.json +253 -0
  93. package/registry/_docs/timeline-chart.json +30 -0
  94. package/registry/_docs/timeline.json +156 -0
  95. package/registry/_docs/toast.json +161 -0
  96. package/registry/_docs/treemap.json +30 -0
  97. package/registry/_docs/waterfall.json +36 -0
  98. package/registry/_docs/wheel-list.json +102 -0
  99. package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
  100. package/registry/accordion/files/src/lib/kit.tsx +219 -0
  101. package/registry/accordion.json +17 -0
  102. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
  103. package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
  104. package/registry/action-sheet.json +18 -0
  105. package/registry/alert/files/src/components/ui/alert.tsx +654 -0
  106. package/registry/alert/files/src/components/ui/button.tsx +274 -0
  107. package/registry/alert/files/src/lib/kit.tsx +219 -0
  108. package/registry/alert.json +20 -0
  109. package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
  110. package/registry/avatar/files/src/lib/kit.tsx +219 -0
  111. package/registry/avatar.json +18 -0
  112. package/registry/badge/files/src/components/ui/badge.tsx +316 -0
  113. package/registry/badge/files/src/lib/kit.tsx +219 -0
  114. package/registry/badge.json +17 -0
  115. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
  116. package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
  117. package/registry/bottom-nav.json +18 -0
  118. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
  119. package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
  120. package/registry/bottom-sheet.json +17 -0
  121. package/registry/bullet.json +14 -0
  122. package/registry/button/files/src/components/ui/button.tsx +274 -0
  123. package/registry/button/files/src/lib/kit.tsx +219 -0
  124. package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
  125. package/registry/button-3d/files/src/lib/kit.tsx +219 -0
  126. package/registry/button-3d.json +18 -0
  127. package/registry/button.json +18 -0
  128. package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
  129. package/registry/calendar/files/src/lib/kit.tsx +219 -0
  130. package/registry/calendar.json +17 -0
  131. package/registry/card/files/src/components/ui/card.tsx +300 -0
  132. package/registry/card/files/src/lib/kit.tsx +219 -0
  133. package/registry/card-stack.json +15 -0
  134. package/registry/card.json +17 -0
  135. package/registry/carousel.json +15 -0
  136. package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
  137. package/registry/checkbox/files/src/lib/kit.tsx +219 -0
  138. package/registry/checkbox.json +17 -0
  139. package/registry/checkout.json +26 -0
  140. package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
  141. package/registry/chip-group/files/src/lib/kit.tsx +219 -0
  142. package/registry/chip-group.json +17 -0
  143. package/registry/color-swatch.json +17 -0
  144. package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
  145. package/registry/confetti/files/src/lib/kit.tsx +219 -0
  146. package/registry/confetti.json +15 -0
  147. package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
  148. package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
  149. package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
  150. package/registry/coupon/files/src/lib/kit.tsx +219 -0
  151. package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
  152. package/registry/coupon.json +23 -0
  153. package/registry/cube-pager.json +15 -0
  154. package/registry/dashboard.json +32 -0
  155. package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
  156. package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
  157. package/registry/date-picker/files/src/lib/kit.tsx +219 -0
  158. package/registry/date-picker.json +19 -0
  159. package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
  160. package/registry/dialog/files/src/lib/kit.tsx +219 -0
  161. package/registry/dialog.json +18 -0
  162. package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
  163. package/registry/drawer/files/src/lib/kit.tsx +219 -0
  164. package/registry/drawer.json +18 -0
  165. package/registry/fab.json +18 -0
  166. package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
  167. package/registry/flip-card/files/src/lib/kit.tsx +219 -0
  168. package/registry/flip-card.json +15 -0
  169. package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
  170. package/registry/floating-input/files/src/lib/kit.tsx +219 -0
  171. package/registry/floating-input.json +17 -0
  172. package/registry/gift-card.json +21 -0
  173. package/registry/glass-button.json +22 -0
  174. package/registry/glass-nav-bar.json +21 -0
  175. package/registry/glass-search-bar.json +21 -0
  176. package/registry/glass-segmented.json +22 -0
  177. package/registry/glass-sheet.json +22 -0
  178. package/registry/glass-tab-bar.json +22 -0
  179. package/registry/glass.json +19 -0
  180. package/registry/inbox.json +30 -0
  181. package/registry/isometric-bar.json +14 -0
  182. package/registry/kit/files/src/lib/kit.tsx +219 -0
  183. package/registry/kit.json +11 -0
  184. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
  185. package/registry/kpi-card.json +14 -0
  186. package/registry/like-button.json +18 -0
  187. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
  188. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
  189. package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
  190. package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
  191. package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
  192. package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
  193. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
  194. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
  195. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
  196. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
  197. package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
  198. package/registry/liquidglass-tab.json +23 -0
  199. package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
  200. package/registry/list-item/files/src/lib/kit.tsx +219 -0
  201. package/registry/list-item.json +17 -0
  202. package/registry/loyalty-card.json +23 -0
  203. package/registry/morph-button.json +18 -0
  204. package/registry/number-ticker.json +15 -0
  205. package/registry/offers.json +24 -0
  206. package/registry/onboarding.json +17 -0
  207. package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
  208. package/registry/otp-input/files/src/lib/kit.tsx +219 -0
  209. package/registry/otp-input.json +17 -0
  210. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
  211. package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
  212. package/registry/pagination-dots.json +15 -0
  213. package/registry/parallax-layers.json +15 -0
  214. package/registry/parallax-scroll.json +17 -0
  215. package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
  216. package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
  217. package/registry/password-input/files/src/lib/kit.tsx +219 -0
  218. package/registry/password-input.json +19 -0
  219. package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
  220. package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
  221. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
  222. package/registry/payment-card/files/src/lib/kit.tsx +219 -0
  223. package/registry/payment-card.json +21 -0
  224. package/registry/pixel-bar.json +14 -0
  225. package/registry/polar-area.json +14 -0
  226. package/registry/profile.json +17 -0
  227. package/registry/progress/files/src/components/ui/progress.tsx +883 -0
  228. package/registry/progress/files/src/lib/kit.tsx +219 -0
  229. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
  230. package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
  231. package/registry/progress-steps.json +18 -0
  232. package/registry/progress.json +17 -0
  233. package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
  234. package/registry/radio-group/files/src/lib/kit.tsx +219 -0
  235. package/registry/radio-group.json +18 -0
  236. package/registry/rating.json +18 -0
  237. package/registry/ring-carousel.json +15 -0
  238. package/registry/scratch-card.json +18 -0
  239. package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
  240. package/registry/search-bar/files/src/lib/kit.tsx +219 -0
  241. package/registry/search-bar.json +17 -0
  242. package/registry/search-results.json +30 -0
  243. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
  244. package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
  245. package/registry/segmented-control.json +18 -0
  246. package/registry/select/files/src/components/ui/select.tsx +1398 -0
  247. package/registry/select/files/src/lib/kit.tsx +219 -0
  248. package/registry/select.json +17 -0
  249. package/registry/settings.json +26 -0
  250. package/registry/shimmer-button.json +17 -0
  251. package/registry/sign-in.json +26 -0
  252. package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
  253. package/registry/skeleton/files/src/lib/kit.tsx +219 -0
  254. package/registry/skeleton.json +17 -0
  255. package/registry/slide-to-confirm.json +18 -0
  256. package/registry/slider/files/src/components/ui/slider.tsx +716 -0
  257. package/registry/slider/files/src/lib/kit.tsx +219 -0
  258. package/registry/slider.json +17 -0
  259. package/registry/spin-wheel.json +18 -0
  260. package/registry/spinner/files/src/components/ui/spinner.tsx +335 -0
  261. package/registry/spinner/files/src/lib/kit.tsx +219 -0
  262. package/registry/spinner.json +17 -0
  263. package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
  264. package/registry/stepper/files/src/lib/kit.tsx +219 -0
  265. package/registry/stepper.json +17 -0
  266. package/registry/swipe-deck.json +15 -0
  267. package/registry/swipeable-row.json +15 -0
  268. package/registry/switch/files/src/components/ui/switch.tsx +344 -0
  269. package/registry/switch/files/src/lib/kit.tsx +219 -0
  270. package/registry/switch.json +18 -0
  271. package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
  272. package/registry/tabs/files/src/lib/kit.tsx +219 -0
  273. package/registry/tabs.json +17 -0
  274. package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
  275. package/registry/tag-input/files/src/lib/kit.tsx +219 -0
  276. package/registry/tag-input.json +17 -0
  277. package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
  278. package/registry/textarea/files/src/lib/kit.tsx +219 -0
  279. package/registry/textarea.json +17 -0
  280. package/registry/ticket-card.json +24 -0
  281. package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
  282. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
  283. package/registry/ticket-shape.json +17 -0
  284. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
  285. package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
  286. package/registry/tilt-card.json +17 -0
  287. package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
  288. package/registry/time-picker/files/src/lib/kit.tsx +219 -0
  289. package/registry/time-picker.json +17 -0
  290. package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
  291. package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
  292. package/registry/timeline/files/src/lib/kit.tsx +219 -0
  293. package/registry/timeline-chart.json +14 -0
  294. package/registry/timeline.json +20 -0
  295. package/registry/toast/files/src/components/ui/button.tsx +274 -0
  296. package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
  297. package/registry/toast/files/src/lib/kit.tsx +219 -0
  298. package/registry/toast.json +20 -0
  299. package/registry/treemap.json +14 -0
  300. package/registry/waterfall.json +14 -0
  301. package/registry/wheel-list.json +17 -0
  302. package/schema/components.schema.json +13 -0
@@ -0,0 +1,219 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+ import { Platform, useColorScheme, type TextStyle } from 'react-native';
3
+
4
+ /**
5
+ * ExpoKit design tokens. Every component reads them through useKit(), so one
6
+ * <KitProvider> (optional) re-skins the whole kit: light / dark, accent colour, radius.
7
+ * Without a provider, components follow the system colour scheme.
8
+ */
9
+
10
+ /**
11
+ * Bumped whenever components start using a new kit API (2: radius.scale / radius.capsule).
12
+ * The CLI won't keep an edited kit.tsx older than the component being added needs.
13
+ */
14
+ export const KIT_VERSION = 2;
15
+
16
+ export type ColorScheme = 'light' | 'dark';
17
+
18
+ export interface Colors {
19
+ background: string;
20
+ foreground: string;
21
+ /** Cards, sheets, inputs. */
22
+ card: string;
23
+ cardForeground: string;
24
+ /** Raised surfaces: popovers, toasts. */
25
+ popover: string;
26
+ muted: string;
27
+ mutedForeground: string;
28
+ border: string;
29
+ input: string;
30
+ primary: string;
31
+ primaryForeground: string;
32
+ secondary: string;
33
+ secondaryForeground: string;
34
+ accent: string;
35
+ accentForeground: string;
36
+ success: string;
37
+ warning: string;
38
+ danger: string;
39
+ dangerForeground: string;
40
+ ring: string;
41
+ overlay: string;
42
+ }
43
+
44
+ export interface Theme {
45
+ dark: boolean;
46
+ colors: Colors;
47
+ /** Corner radius scale. `radius.full` makes pills. */
48
+ radius: {
49
+ sm: number;
50
+ md: number;
51
+ lg: number;
52
+ xl: number;
53
+ full: number;
54
+ /** A corner designed at the default radius (12), scaled to the current one: `radius.scale(16)` is 16 at 12, 0 at 0. */
55
+ scale: (n: number) => number;
56
+ /** Corner for a bar or chip of height `h`: a pill at radius ≥ 12, softer below, square at 0. */
57
+ capsule: (h: number) => number;
58
+ };
59
+ /** 4pt spacing scale: space(4) === 16. */
60
+ space: (n: number) => number;
61
+ font: { family?: string; mono: string };
62
+ text: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', TextStyle>;
63
+ }
64
+
65
+ const light: Colors = {
66
+ background: '#ffffff',
67
+ foreground: '#09090b',
68
+ card: '#ffffff',
69
+ cardForeground: '#09090b',
70
+ popover: '#ffffff',
71
+ muted: '#f4f4f5',
72
+ mutedForeground: '#71717a',
73
+ border: '#e4e4e7',
74
+ input: '#d4d4d8',
75
+ primary: '#18181b',
76
+ primaryForeground: '#fafafa',
77
+ secondary: '#f4f4f5',
78
+ secondaryForeground: '#18181b',
79
+ accent: '#18181b',
80
+ accentForeground: '#ffffff',
81
+ success: '#16a34a',
82
+ warning: '#f59e0b',
83
+ danger: '#e5484d',
84
+ dangerForeground: '#ffffff',
85
+ ring: '#18181b',
86
+ overlay: 'rgba(0,0,0,0.4)',
87
+ };
88
+
89
+ const dark: Colors = {
90
+ background: '#09090b',
91
+ foreground: '#fafafa',
92
+ card: '#111113',
93
+ cardForeground: '#fafafa',
94
+ popover: '#18181b',
95
+ muted: '#1c1c20',
96
+ mutedForeground: '#a1a1aa',
97
+ border: '#27272a',
98
+ input: '#34343a',
99
+ primary: '#fafafa',
100
+ primaryForeground: '#18181b',
101
+ secondary: '#1f1f23',
102
+ secondaryForeground: '#fafafa',
103
+ accent: '#fafafa',
104
+ accentForeground: '#09090b',
105
+ success: '#3dd68c',
106
+ warning: '#ffb224',
107
+ danger: '#ff6369',
108
+ dangerForeground: '#ffffff',
109
+ ring: '#d4d4d8',
110
+ overlay: 'rgba(0,0,0,0.6)',
111
+ };
112
+
113
+ export const palettes = { light, dark };
114
+
115
+ function build(scheme: ColorScheme, overrides?: ThemeOverrides): Theme {
116
+ const base = scheme === 'dark' ? dark : light;
117
+ const accent = overrides?.accent;
118
+ const colors: Colors = {
119
+ ...base,
120
+ ...(accent ? { accent, ring: accent, accentForeground: readableOn(accent) } : null),
121
+ ...overrides?.colors?.[scheme],
122
+ };
123
+ const r = overrides?.radius ?? 12;
124
+ return {
125
+ dark: scheme === 'dark',
126
+ colors,
127
+ radius: {
128
+ sm: Math.round(r * 0.5),
129
+ md: r,
130
+ lg: Math.round(r * 1.35),
131
+ xl: Math.round(r * 1.8),
132
+ full: 999,
133
+ scale: (n) => Math.round((n * r) / 12),
134
+ capsule: (h) => Math.round((h / 2) * Math.min(1, r / 12)),
135
+ },
136
+ space: (n) => n * 4,
137
+ font: {
138
+ family: overrides?.fontFamily,
139
+ mono: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'ui-monospace, SFMono-Regular, Menlo, monospace' })!,
140
+ },
141
+ text: {
142
+ xs: { fontSize: 12, lineHeight: 16 },
143
+ sm: { fontSize: 14, lineHeight: 20 },
144
+ md: { fontSize: 16, lineHeight: 22 },
145
+ lg: { fontSize: 18, lineHeight: 24, fontWeight: '400' },
146
+ xl: { fontSize: 22, lineHeight: 28, fontWeight: '400', letterSpacing: -0.3 },
147
+ xxl: { fontSize: 30, lineHeight: 36, fontWeight: '400', letterSpacing: -0.8 },
148
+ },
149
+ };
150
+ }
151
+
152
+ export interface ThemeOverrides {
153
+ /** Brand colour used for focus rings, selected states and accent buttons. */
154
+ accent?: string;
155
+ /** Base corner radius in points (default 12). */
156
+ radius?: number;
157
+ fontFamily?: string;
158
+ /** Override any token per scheme. */
159
+ colors?: Partial<Record<ColorScheme, Partial<Colors>>>;
160
+ }
161
+
162
+ const KitContext = createContext<{ scheme?: ColorScheme; overrides?: ThemeOverrides } | null>(null);
163
+
164
+ export function KitProvider({
165
+ scheme,
166
+ children,
167
+ ...overrides
168
+ }: ThemeOverrides & { scheme?: ColorScheme | 'system'; children: React.ReactNode }) {
169
+ const value = useMemo(
170
+ () => ({ scheme: scheme === 'system' ? undefined : scheme, overrides }),
171
+ // eslint-disable-next-line react-hooks/exhaustive-deps
172
+ [scheme, overrides.accent, overrides.radius, overrides.fontFamily, overrides.colors]
173
+ );
174
+ return <KitContext.Provider value={value}>{children}</KitContext.Provider>;
175
+ }
176
+
177
+ export function useKit(): Theme {
178
+ const ctx = useContext(KitContext);
179
+ const system = useColorScheme();
180
+ const scheme: ColorScheme = ctx?.scheme ?? (system === 'dark' ? 'dark' : 'light');
181
+ return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
182
+ }
183
+
184
+ /** Shared motion presets so the whole kit moves the same way. */
185
+ export const motion = {
186
+ /** Toggles, selection indicators. */
187
+ snappy: { damping: 18, stiffness: 260, mass: 0.8 },
188
+ /** Press feedback, pops. */
189
+ bouncy: { damping: 11, stiffness: 220, mass: 0.7 },
190
+ /** Sheets, layout changes. */
191
+ gentle: { damping: 22, stiffness: 160, mass: 1 },
192
+ fast: 160,
193
+ normal: 240,
194
+ };
195
+
196
+ /** Adds alpha to a #rrggbb colour: alpha('#6366f1', 0.2). */
197
+ export function alpha(hex: string, a: number) {
198
+ if (!hex.startsWith('#') || (hex.length !== 7 && hex.length !== 4)) return hex;
199
+ const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
200
+ return full + Math.round(Math.max(0, Math.min(1, a)) * 255).toString(16).padStart(2, '0');
201
+ }
202
+
203
+ /** Black or white, whichever reads better on `hex`. */
204
+ export function readableOn(hex: string) {
205
+ const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
206
+ const n = parseInt(full.slice(1, 7), 16);
207
+ if (Number.isNaN(n)) return '#ffffff';
208
+ const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255];
209
+ return (r * 299 + g * 587 + b * 114) / 1000 > 150 ? '#09090b' : '#ffffff';
210
+ }
211
+
212
+ /**
213
+ * True for children that must be wrapped in <Text>: strings, numbers and arrays of them —
214
+ * `{count} new` is the array [3, ' new'], and raw text outside <Text> crashes on native.
215
+ */
216
+ export function isTextNode(node: React.ReactNode): node is string | number | (string | number)[] {
217
+ if (typeof node === 'string' || typeof node === 'number') return true;
218
+ return Array.isArray(node) && node.length > 0 && node.every((n) => typeof n === 'string' || typeof n === 'number');
219
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "stepper",
3
+ "type": "component",
4
+ "title": "Stepper",
5
+ "description": "Quantity control whose number rolls — each changed digit slides out as the new one slides in, up or down with the change. Airbnb-style outline, an iOS-style filled capsule, a compact row control, and a DoorDash-style cart button that springs open from a single + into − 1 + (with a trash can at 1). Buttons spring on press, hold to repeat with acceleration, and pushing past a limit gives a little bump.",
6
+ "tier": "free",
7
+ "dependencies": [
8
+ "react-native-svg"
9
+ ],
10
+ "registryDependencies": [
11
+ "kit"
12
+ ],
13
+ "sharedFiles": [
14
+ "lib/kit.tsx"
15
+ ],
16
+ "filesDir": "stepper/files"
17
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "swipe-deck",
3
+ "type": "component",
4
+ "title": "Swipe Deck",
5
+ "description": "A stack of photo cards you swipe left or right — the top card follows your finger, leans in 3D, washes green or red and stamps SAVE / SKIP, then flies off as the next card rises. Flick momentum, undo, and a ref for buttons.",
6
+ "tier": "pro",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "kit"
10
+ ],
11
+ "sharedFiles": [
12
+ "lib/kit.tsx"
13
+ ],
14
+ "filesDir": null
15
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "swipeable-row",
3
+ "type": "component",
4
+ "title": "Swipeable Row",
5
+ "description": "Mail-style list row: drag sideways to reveal actions that fan out and grow with your finger. A full swipe stretches the edge action across the row and fires it — destructive ones slide away and collapse the row. PanResponder only, so it works with a mouse on the web too.",
6
+ "tier": "pro",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "kit"
10
+ ],
11
+ "sharedFiles": [
12
+ "lib/kit.tsx"
13
+ ],
14
+ "filesDir": null
15
+ }
@@ -0,0 +1,344 @@
1
+ import * as Haptics from 'expo-haptics';
2
+ import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
3
+ import { Animated, PanResponder, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
4
+ import Svg, { Circle, Defs, LinearGradient, Path, Rect, Stop } from 'react-native-svg';
5
+
6
+ import { alpha, motion, readableOn, useKit } from '@/lib/kit';
7
+
8
+ export type SwitchVariant = 'ios' | 'material' | 'icon';
9
+ export type SwitchSize = 'sm' | 'md' | 'lg';
10
+
11
+ export interface SwitchProps {
12
+ /** Controlled state. */
13
+ value?: boolean;
14
+ /** Initial state when uncontrolled. */
15
+ defaultValue?: boolean;
16
+ onValueChange?: (value: boolean) => void;
17
+ variant?: SwitchVariant;
18
+ size?: SwitchSize;
19
+ /** Icons drawn inside the thumb for `variant="icon"`: sun / moon or check / ✕. */
20
+ icon?: 'theme' | 'check';
21
+ /** Turns the switch into a settings row: label (and description) on the left, switch on the right. */
22
+ label?: string;
23
+ description?: string;
24
+ /** Track colour when on (default: theme accent). */
25
+ accent?: string;
26
+ disabled?: boolean;
27
+ /** Light tap when it flips (default true). */
28
+ haptics?: boolean;
29
+ accessibilityLabel?: string;
30
+ style?: StyleProp<ViewStyle>;
31
+ }
32
+
33
+ const SIZES: Record<SwitchSize, { w: number; h: number }> = {
34
+ sm: { w: 40, h: 24 },
35
+ md: { w: 51, h: 31 },
36
+ lg: { w: 62, h: 38 },
37
+ };
38
+
39
+ const SUN_RAYS = 'M12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5.3 5.3l1.4 1.4M17.3 17.3l1.4 1.4M5.3 18.7l1.4-1.4M17.3 6.7l1.4-1.4';
40
+ const MOON = 'M20 14.2A8 8 0 1 1 9.8 4a6.4 6.4 0 0 0 10.2 10.2z';
41
+ const CHECK = 'M5.5 12.5l4 4L18.5 7.5';
42
+ const CROSS = 'M7 7l10 10M17 7L7 17';
43
+
44
+ const clamp01 = (n: number) => Math.min(1, Math.max(0, n));
45
+
46
+ /**
47
+ * Toggle switch with a spring thumb that stretches toward its destination while pressed
48
+ * (squash & stretch), can be dragged across, and throws a soft halo when it flips.
49
+ * The track cross-fades. iOS, Material 3 and icon looks.
50
+ */
51
+ export function Switch({
52
+ value,
53
+ defaultValue = false,
54
+ onValueChange,
55
+ variant = 'ios',
56
+ size = 'md',
57
+ icon = 'theme',
58
+ label,
59
+ description,
60
+ accent,
61
+ disabled,
62
+ haptics = true,
63
+ accessibilityLabel,
64
+ style,
65
+ }: SwitchProps) {
66
+ const { colors, dark } = useKit();
67
+ const gid = 'sw' + useId().replace(/[^a-zA-Z0-9]/g, '');
68
+ const tint = accent ?? colors.accent;
69
+ const [inner, setInner] = useState(defaultValue);
70
+ const on = value ?? inner;
71
+
72
+ const s = SIZES[size];
73
+ const material = variant === 'material';
74
+ const W = s.w;
75
+ const H = s.h;
76
+ const border = material ? 2 : 0;
77
+ const innerW = W - border * 2;
78
+ const innerH = H - border * 2;
79
+ const pad = 2;
80
+ const dOn = innerH - pad * 2; // thumb diameter when on (and always, except Material)
81
+ const dOff = material ? Math.round(innerH * 0.5) : dOn;
82
+ const stretchW = material ? 6 : Math.round(dOn * 0.32);
83
+ const growH = material ? 4 : 0;
84
+
85
+ // pos + stretch drive layout and colours (JS driver). glyph drives the icons, halo the glow (native driver).
86
+ const pos = useRef(new Animated.Value(on ? 1 : 0)).current;
87
+ const stretch = useRef(new Animated.Value(0)).current;
88
+ const glyph = useRef(new Animated.Value(on ? 1 : 0)).current;
89
+ const halo = useRef(new Animated.Value(0)).current;
90
+
91
+ useEffect(() => {
92
+ Animated.spring(pos, { toValue: on ? 1 : 0, useNativeDriver: false, ...motion.snappy }).start();
93
+ Animated.spring(glyph, { toValue: on ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
94
+ }, [on, pos, glyph]);
95
+
96
+ // Halo: 0 hidden · 1 held (glow around the thumb) · 2 released (expands and fades out).
97
+ const press = (to: number) => {
98
+ Animated.spring(stretch, { toValue: to, useNativeDriver: false, ...(to ? motion.snappy : motion.bouncy) }).start();
99
+ if (to) Animated.spring(halo, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
100
+ else Animated.timing(halo, { toValue: 2, duration: 420, useNativeDriver: true }).start(({ finished }) => finished && halo.setValue(0));
101
+ };
102
+
103
+ const toggle = () => {
104
+ if (haptics) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
105
+ if (value === undefined) setInner(!on);
106
+ onValueChange?.(!on);
107
+ };
108
+
109
+ // Thumb geometry. Left edge is anchored while off (grows right), right edge while on (grows left).
110
+ const c0 = pad + dOn / 2;
111
+ const c1 = innerW - pad - dOn / 2;
112
+
113
+ // Drag the thumb across: pos follows the finger, release settles on the nearer side.
114
+ // A touch that never moves is a tap and just toggles. On web the wrapping Pressable also
115
+ // gets a click after the gesture, so it ignores presses right after one (`endedAt`).
116
+ const live = useRef({ on, toggle, travel: c1 - c0 });
117
+ live.current = { on, toggle, travel: c1 - c0 };
118
+ const drag = useRef({ from: 0, moved: false, endedAt: 0 });
119
+ const responder = useMemo(
120
+ () =>
121
+ PanResponder.create({
122
+ onStartShouldSetPanResponder: () => true,
123
+ onMoveShouldSetPanResponder: () => true,
124
+ onPanResponderTerminationRequest: () => !drag.current.moved,
125
+ onPanResponderGrant: () => {
126
+ pos.stopAnimation();
127
+ drag.current = { from: live.current.on ? 1 : 0, moved: false, endedAt: 0 };
128
+ press(1);
129
+ },
130
+ onPanResponderMove: (_, g) => {
131
+ if (Math.abs(g.dx) > 3) drag.current.moved = true;
132
+ if (drag.current.moved) pos.setValue(clamp01(drag.current.from + g.dx / live.current.travel));
133
+ },
134
+ onPanResponderRelease: (_, g) => {
135
+ const { on: now, toggle: flip } = live.current;
136
+ drag.current.endedAt = Date.now();
137
+ press(0);
138
+ if (!drag.current.moved) return flip();
139
+ const next = clamp01(drag.current.from + g.dx / live.current.travel) > 0.5;
140
+ Animated.spring(pos, { toValue: next ? 1 : 0, useNativeDriver: false, ...motion.snappy }).start();
141
+ if (next !== now) flip();
142
+ },
143
+ onPanResponderTerminate: () => {
144
+ press(0);
145
+ Animated.spring(pos, { toValue: live.current.on ? 1 : 0, useNativeDriver: false, ...motion.snappy }).start();
146
+ },
147
+ }),
148
+ // eslint-disable-next-line react-hooks/exhaustive-deps
149
+ []
150
+ );
151
+ const base = pos.interpolate({ inputRange: [0, 1], outputRange: [dOff, dOn], extrapolate: 'clamp' });
152
+ const thumbW = Animated.add(base, Animated.multiply(stretch, stretchW));
153
+ const thumbH = Animated.add(base, Animated.multiply(stretch, growH));
154
+ const left = Animated.subtract(
155
+ pos.interpolate({ inputRange: [0, 1], outputRange: [c0 - dOff / 2, c1 - dOn / 2], extrapolate: 'clamp' }),
156
+ Animated.multiply(
157
+ pos.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
158
+ Animated.multiply(stretch, stretchW)
159
+ )
160
+ );
161
+ const top = Animated.subtract(
162
+ pos.interpolate({ inputRange: [0, 1], outputRange: [(innerH - dOff) / 2, (innerH - dOn) / 2], extrapolate: 'clamp' }),
163
+ Animated.multiply(stretch, growH / 2)
164
+ );
165
+
166
+ const offTrack = material ? alpha(colors.mutedForeground, 0.12) : alpha(colors.foreground, dark ? 0.16 : 0.1);
167
+ const trackBg = pos.interpolate({ inputRange: [0, 1], outputRange: [offTrack, tint], extrapolate: 'clamp' });
168
+ const trackBorder = pos.interpolate({ inputRange: [0, 1], outputRange: [colors.mutedForeground, tint], extrapolate: 'clamp' });
169
+ const onFg = readableOn(tint);
170
+ const thumbBg = material
171
+ ? pos.interpolate({ inputRange: [0, 1], outputRange: [colors.mutedForeground, onFg], extrapolate: 'clamp' })
172
+ : '#ffffff';
173
+
174
+ const iconSize = dOn * 0.62;
175
+ const sunColor = colors.warning;
176
+ const offIcon = icon === 'theme' ? sunColor : colors.mutedForeground;
177
+
178
+ const haloSize = Math.round(dOn * 1.75);
179
+
180
+ const control = (
181
+ <Animated.View
182
+ {...(disabled ? null : responder.panHandlers)}
183
+ style={[
184
+ styles.track,
185
+ { width: W, height: H, borderRadius: H / 2, backgroundColor: trackBg },
186
+ material && { borderWidth: border, borderColor: trackBorder },
187
+ ]}
188
+ >
189
+ {material ? null : (
190
+ // Depth: a soft sheen over the top half, a touch of shade at the bottom, and a hairline edge.
191
+ <View pointerEvents="none" style={StyleSheet.absoluteFill}>
192
+ <Svg width={W} height={H}>
193
+ <Defs>
194
+ <LinearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
195
+ <Stop offset="0" stopColor="#ffffff" stopOpacity={dark ? 0.12 : 0.24} />
196
+ <Stop offset="0.55" stopColor="#ffffff" stopOpacity={0} />
197
+ <Stop offset="1" stopColor="#000000" stopOpacity={0.07} />
198
+ </LinearGradient>
199
+ </Defs>
200
+ <Rect width={W} height={H} rx={H / 2} fill={`url(#${gid})`} />
201
+ <Rect
202
+ x={0.5}
203
+ y={0.5}
204
+ width={W - 1}
205
+ height={H - 1}
206
+ rx={(H - 1) / 2}
207
+ fill="none"
208
+ stroke={colors.foreground}
209
+ strokeOpacity={dark ? 0.08 : 0.06}
210
+ />
211
+ </Svg>
212
+ </View>
213
+ )}
214
+ <Animated.View pointerEvents="none" style={[styles.thumb, { width: thumbW, height: thumbH, left, top }]}>
215
+ <Animated.View
216
+ style={[
217
+ styles.halo,
218
+ {
219
+ width: haloSize,
220
+ height: haloSize,
221
+ borderRadius: haloSize / 2,
222
+ backgroundColor: alpha(on ? tint : colors.mutedForeground, 0.2),
223
+ opacity: halo.interpolate({ inputRange: [0, 1, 2], outputRange: [0, 1, 0] }),
224
+ transform: [{ scale: halo.interpolate({ inputRange: [0, 1, 2], outputRange: [0.5, 1, 1.45] }) }],
225
+ },
226
+ ]}
227
+ />
228
+ <Animated.View style={[StyleSheet.absoluteFill, !material && styles.shadow, { borderRadius: H, backgroundColor: thumbBg }]} />
229
+ {material ? (
230
+ <Animated.View
231
+ style={[
232
+ styles.glyph,
233
+ {
234
+ opacity: glyph.interpolate({ inputRange: [0, 0.5, 1], outputRange: [0, 0, 1] }),
235
+ transform: [{ scale: glyph.interpolate({ inputRange: [0, 1], outputRange: [0.3, 1] }) }],
236
+ },
237
+ ]}
238
+ >
239
+ <Svg width={dOn * 0.6} height={dOn * 0.6} viewBox="0 0 24 24" fill="none">
240
+ <Path d={CHECK} stroke={tint} strokeWidth={3.2} strokeLinecap="round" strokeLinejoin="round" />
241
+ </Svg>
242
+ </Animated.View>
243
+ ) : null}
244
+ {variant === 'icon' ? (
245
+ <>
246
+ <Animated.View
247
+ style={[
248
+ styles.glyph,
249
+ {
250
+ opacity: glyph.interpolate({ inputRange: [0, 0.5, 1], outputRange: [1, 0, 0] }),
251
+ transform: [
252
+ { rotate: glyph.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '-120deg'] }) },
253
+ { scale: glyph.interpolate({ inputRange: [0, 1], outputRange: [1, 0.4] }) },
254
+ ],
255
+ },
256
+ ]}
257
+ >
258
+ <Svg width={iconSize} height={iconSize} viewBox="0 0 24 24" fill="none">
259
+ {icon === 'theme' ? (
260
+ <>
261
+ <Circle cx={12} cy={12} r={4.2} fill={offIcon} />
262
+ <Path d={SUN_RAYS} stroke={offIcon} strokeWidth={2.2} strokeLinecap="round" />
263
+ </>
264
+ ) : (
265
+ <Path d={CROSS} stroke={offIcon} strokeWidth={2.8} strokeLinecap="round" />
266
+ )}
267
+ </Svg>
268
+ </Animated.View>
269
+ <Animated.View
270
+ style={[
271
+ styles.glyph,
272
+ {
273
+ opacity: glyph.interpolate({ inputRange: [0, 0.5, 1], outputRange: [0, 0, 1] }),
274
+ transform: [
275
+ { rotate: glyph.interpolate({ inputRange: [0, 1], outputRange: ['120deg', '0deg'] }) },
276
+ { scale: glyph.interpolate({ inputRange: [0, 1], outputRange: [0.4, 1] }) },
277
+ ],
278
+ },
279
+ ]}
280
+ >
281
+ <Svg width={iconSize} height={iconSize} viewBox="0 0 24 24" fill="none">
282
+ {icon === 'theme' ? (
283
+ <Path d={MOON} fill={tint} />
284
+ ) : (
285
+ <Path d={CHECK} stroke={tint} strokeWidth={2.8} strokeLinecap="round" strokeLinejoin="round" />
286
+ )}
287
+ </Svg>
288
+ </Animated.View>
289
+ </>
290
+ ) : null}
291
+ </Animated.View>
292
+ </Animated.View>
293
+ );
294
+
295
+ const a11y = {
296
+ accessibilityRole: 'switch' as const,
297
+ accessibilityState: { checked: on, disabled: !!disabled },
298
+ accessibilityLabel: accessibilityLabel ?? label,
299
+ disabled,
300
+ onPress: () => {
301
+ if (Date.now() - drag.current.endedAt > 400) toggle();
302
+ },
303
+ onPressIn: () => press(1),
304
+ onPressOut: () => press(0),
305
+ };
306
+
307
+ if (!label) {
308
+ return (
309
+ <Pressable {...a11y} hitSlop={Math.max(0, (44 - H) / 2)} style={[{ alignSelf: 'flex-start', opacity: disabled ? 0.45 : 1 }, style]}>
310
+ {control}
311
+ </Pressable>
312
+ );
313
+ }
314
+
315
+ return (
316
+ <Pressable {...a11y} style={[styles.row, { opacity: disabled ? 0.45 : 1 }, style]}>
317
+ <View style={styles.text}>
318
+ <Text style={[styles.label, { color: colors.foreground }]}>{label}</Text>
319
+ {description ? <Text style={[styles.description, { color: colors.mutedForeground }]}>{description}</Text> : null}
320
+ </View>
321
+ {control}
322
+ </Pressable>
323
+ );
324
+ }
325
+
326
+ const styles = StyleSheet.create({
327
+ row: { flexDirection: 'row', alignItems: 'center', gap: 16, minHeight: 44 },
328
+ text: { flex: 1 },
329
+ label: { fontSize: 15.5, fontWeight: '400' },
330
+ description: { fontSize: 13, lineHeight: 18, marginTop: 2 },
331
+ track: { justifyContent: 'center' },
332
+ thumb: { position: 'absolute', alignItems: 'center', justifyContent: 'center' },
333
+ halo: { position: 'absolute' },
334
+ shadow: {
335
+ borderWidth: StyleSheet.hairlineWidth,
336
+ borderColor: 'rgba(0,0,0,0.06)',
337
+ shadowColor: '#000',
338
+ shadowOpacity: 0.22,
339
+ shadowRadius: 4,
340
+ shadowOffset: { width: 0, height: 2 },
341
+ elevation: 3,
342
+ },
343
+ glyph: { position: 'absolute' },
344
+ });