@retray-dev/ui-kit 12.2.0 → 13.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 (296) hide show
  1. package/CONSUMER.md +26 -11
  2. package/DESIGN.md +2 -2
  3. package/README.md +15 -11
  4. package/{COMPONENTS.md → SKILL.md} +374 -996
  5. package/dist/Accordion.d.mts +2 -0
  6. package/dist/Accordion.d.ts +2 -0
  7. package/dist/Accordion.js +49 -210
  8. package/dist/Accordion.mjs +6 -6
  9. package/dist/AlertBanner.js +29 -153
  10. package/dist/AlertBanner.mjs +3 -4
  11. package/dist/AppHeader.d.mts +5 -2
  12. package/dist/AppHeader.d.ts +5 -2
  13. package/dist/AppHeader.js +45 -239
  14. package/dist/AppHeader.mjs +6 -8
  15. package/dist/Avatar.d.mts +17 -1
  16. package/dist/Avatar.d.ts +17 -1
  17. package/dist/Avatar.js +80 -115
  18. package/dist/Avatar.mjs +2 -3
  19. package/dist/Badge.js +24 -149
  20. package/dist/Badge.mjs +3 -4
  21. package/dist/Button.js +79 -267
  22. package/dist/Button.mjs +6 -7
  23. package/dist/ButtonGroup.mjs +0 -1
  24. package/dist/Card.js +15 -200
  25. package/dist/Card.mjs +4 -6
  26. package/dist/CategoryStrip.d.mts +0 -5
  27. package/dist/CategoryStrip.d.ts +0 -5
  28. package/dist/CategoryStrip.js +47 -265
  29. package/dist/CategoryStrip.mjs +6 -7
  30. package/dist/Checkbox.d.mts +2 -1
  31. package/dist/Checkbox.d.ts +2 -1
  32. package/dist/Checkbox.js +18 -201
  33. package/dist/Checkbox.mjs +5 -6
  34. package/dist/Chip.js +44 -236
  35. package/dist/Chip.mjs +7 -7
  36. package/dist/ConfirmDialog.d.mts +2 -1
  37. package/dist/ConfirmDialog.d.ts +2 -1
  38. package/dist/ConfirmDialog.js +110 -300
  39. package/dist/ConfirmDialog.mjs +7 -8
  40. package/dist/CurrencyDisplay.js +1 -114
  41. package/dist/CurrencyDisplay.mjs +2 -3
  42. package/dist/CurrencyInput.js +35 -162
  43. package/dist/CurrencyInput.mjs +5 -6
  44. package/dist/DetailRow.js +25 -150
  45. package/dist/DetailRow.mjs +3 -4
  46. package/dist/EmptyState.js +80 -268
  47. package/dist/EmptyState.mjs +7 -8
  48. package/dist/ErrorBoundary.js +32 -199
  49. package/dist/ErrorBoundary.mjs +4 -5
  50. package/dist/Form.js +1 -114
  51. package/dist/Form.mjs +2 -3
  52. package/dist/HolographicCard.d.mts +0 -28
  53. package/dist/HolographicCard.d.ts +0 -28
  54. package/dist/HolographicCard.js +20 -130
  55. package/dist/HolographicCard.mjs +9 -33
  56. package/dist/IconButton.js +36 -234
  57. package/dist/IconButton.mjs +5 -7
  58. package/dist/IconPicker.js +222 -929
  59. package/dist/IconPicker.mjs +5 -6
  60. package/dist/ImageUpload.d.mts +3 -3
  61. package/dist/ImageUpload.d.ts +3 -3
  62. package/dist/ImageUpload.js +49 -238
  63. package/dist/ImageUpload.mjs +5 -7
  64. package/dist/ImageViewer.js +75 -266
  65. package/dist/ImageViewer.mjs +8 -9
  66. package/dist/Input.d.mts +1 -1
  67. package/dist/Input.d.ts +1 -1
  68. package/dist/Input.js +35 -162
  69. package/dist/Input.mjs +4 -5
  70. package/dist/LabelValue.js +24 -149
  71. package/dist/LabelValue.mjs +3 -4
  72. package/dist/ListGroup.js +1 -114
  73. package/dist/ListGroup.mjs +2 -3
  74. package/dist/ListItem.d.mts +2 -1
  75. package/dist/ListItem.d.ts +2 -1
  76. package/dist/ListItem.js +41 -236
  77. package/dist/ListItem.mjs +5 -7
  78. package/dist/MediaCard.d.mts +0 -14
  79. package/dist/MediaCard.d.ts +0 -14
  80. package/dist/MediaCard.js +69 -315
  81. package/dist/MediaCard.mjs +5 -7
  82. package/dist/MenuGroup.js +1 -114
  83. package/dist/MenuGroup.mjs +2 -3
  84. package/dist/MenuItem.d.mts +2 -1
  85. package/dist/MenuItem.d.ts +2 -1
  86. package/dist/MenuItem.js +39 -235
  87. package/dist/MenuItem.mjs +5 -7
  88. package/dist/MonthPicker.js +8 -163
  89. package/dist/MonthPicker.mjs +3 -4
  90. package/dist/NumberStepper.d.mts +2 -1
  91. package/dist/NumberStepper.d.ts +2 -1
  92. package/dist/NumberStepper.js +44 -239
  93. package/dist/NumberStepper.mjs +5 -7
  94. package/dist/PagerDots.d.mts +1 -1
  95. package/dist/PagerDots.d.ts +1 -1
  96. package/dist/PagerDots.js +69 -224
  97. package/dist/PagerDots.mjs +6 -6
  98. package/dist/Pressable.js +14 -85
  99. package/dist/Pressable.mjs +4 -5
  100. package/dist/PricingCard.js +87 -272
  101. package/dist/PricingCard.mjs +8 -9
  102. package/dist/Progress.js +3 -123
  103. package/dist/Progress.mjs +3 -4
  104. package/dist/RadioGroup.js +52 -265
  105. package/dist/RadioGroup.mjs +5 -6
  106. package/dist/RetrayProvider.js +3 -6
  107. package/dist/RetrayProvider.mjs +3 -4
  108. package/dist/Select.d.mts +3 -1
  109. package/dist/Select.d.ts +3 -1
  110. package/dist/Select.js +27 -233
  111. package/dist/Select.mjs +4 -6
  112. package/dist/SelectableCard.js +33 -209
  113. package/dist/SelectableCard.mjs +5 -6
  114. package/dist/SelectableGrid.d.mts +0 -21
  115. package/dist/SelectableGrid.d.ts +0 -21
  116. package/dist/SelectableGrid.js +49 -272
  117. package/dist/SelectableGrid.mjs +5 -7
  118. package/dist/Separator.js +1 -114
  119. package/dist/Separator.mjs +2 -3
  120. package/dist/Sheet.d.mts +1 -1
  121. package/dist/Sheet.d.ts +1 -1
  122. package/dist/Sheet.js +33 -175
  123. package/dist/Sheet.mjs +3 -4
  124. package/dist/SheetSelect.js +39 -236
  125. package/dist/SheetSelect.mjs +6 -7
  126. package/dist/Skeleton.js +4 -124
  127. package/dist/Skeleton.mjs +3 -4
  128. package/dist/Slider.d.mts +2 -1
  129. package/dist/Slider.d.ts +2 -1
  130. package/dist/Slider.js +8 -161
  131. package/dist/Slider.mjs +3 -4
  132. package/dist/Spinner.js +3 -116
  133. package/dist/Spinner.mjs +2 -3
  134. package/dist/Stats.js +36 -234
  135. package/dist/Stats.mjs +5 -7
  136. package/dist/Switch.d.mts +2 -1
  137. package/dist/Switch.d.ts +2 -1
  138. package/dist/Switch.js +26 -176
  139. package/dist/Switch.mjs +5 -5
  140. package/dist/TabBar.js +43 -200
  141. package/dist/TabBar.mjs +5 -5
  142. package/dist/Tabs.js +15 -199
  143. package/dist/Tabs.mjs +5 -6
  144. package/dist/Text.js +9 -130
  145. package/dist/Text.mjs +2 -3
  146. package/dist/Textarea.d.mts +2 -1
  147. package/dist/Textarea.d.ts +2 -1
  148. package/dist/Textarea.js +71 -219
  149. package/dist/Textarea.mjs +4 -5
  150. package/dist/Toast.d.mts +12 -10
  151. package/dist/Toast.d.ts +12 -10
  152. package/dist/Toast.js +1 -114
  153. package/dist/Toast.mjs +2 -3
  154. package/dist/Toggle.js +39 -236
  155. package/dist/Toggle.mjs +6 -7
  156. package/dist/{chunk-ELGEOM7I.mjs → chunk-2QXJDRVU.mjs} +13 -10
  157. package/dist/{chunk-LIS6I5UP.mjs → chunk-2VIDP72N.mjs} +3 -3
  158. package/dist/{chunk-NHDI3VQB.mjs → chunk-422IVD3H.mjs} +16 -12
  159. package/dist/{chunk-DF7JA72E.mjs → chunk-4NQFTHN3.mjs} +13 -7
  160. package/dist/{chunk-3XCFYSX4.mjs → chunk-5MYNAAFE.mjs} +13 -17
  161. package/dist/{chunk-E7NEHHXV.mjs → chunk-62BBSSUF.mjs} +3 -3
  162. package/dist/{chunk-UBUXUMER.mjs → chunk-77UOVFIS.mjs} +7 -5
  163. package/dist/{chunk-M3C7XM2M.mjs → chunk-7BZJRB77.mjs} +28 -18
  164. package/dist/chunk-ARONDO7M.mjs +40 -0
  165. package/dist/{chunk-GH67YXG6.mjs → chunk-AZV7KNJI.mjs} +3 -3
  166. package/dist/{chunk-2P2CB235.mjs → chunk-BULKGOIZ.mjs} +7 -8
  167. package/dist/{chunk-RJNLAH76.mjs → chunk-C5ZRMR2E.mjs} +4 -2
  168. package/dist/chunk-CM2DG4MR.mjs +142 -0
  169. package/dist/{chunk-UQ4742ET.mjs → chunk-COA2YZOX.mjs} +8 -6
  170. package/dist/{chunk-EDLCGYIO.mjs → chunk-CZN6L2QU.mjs} +11 -8
  171. package/dist/{chunk-TS7DGUIR.mjs → chunk-DBHSUUKU.mjs} +2 -2
  172. package/dist/{chunk-57V2LXCK.mjs → chunk-DE25XTVQ.mjs} +3 -3
  173. package/dist/{chunk-RMRS44MQ.mjs → chunk-E2PONRJG.mjs} +13 -9
  174. package/dist/{chunk-GUTDFUNF.mjs → chunk-EHGBHFMH.mjs} +9 -17
  175. package/dist/{chunk-ZIMY2QUM.mjs → chunk-ERWJPVX7.mjs} +2 -2
  176. package/dist/{chunk-NLZY4TXU.mjs → chunk-ESQDPO5E.mjs} +7 -7
  177. package/dist/{chunk-VJBUCITV.mjs → chunk-EW2FIDSM.mjs} +1 -1
  178. package/dist/{chunk-HC4VVCWY.mjs → chunk-FTTI6T5Q.mjs} +4 -4
  179. package/dist/{chunk-MVMGPZN6.mjs → chunk-H6MQL7PS.mjs} +12 -7
  180. package/dist/{chunk-CF27NBXO.mjs → chunk-HHOOFDBA.mjs} +38 -41
  181. package/dist/{chunk-2HFD4IHU.mjs → chunk-HUSSF6TF.mjs} +1 -1
  182. package/dist/{chunk-HEDQPK4I.mjs → chunk-IDVUZIVY.mjs} +16 -22
  183. package/dist/chunk-IFYMBOEN.mjs +14 -0
  184. package/dist/{chunk-QOLWA2PW.mjs → chunk-IGU223UM.mjs} +80 -4
  185. package/dist/chunk-IJCMPVW5.mjs +121 -0
  186. package/dist/{chunk-AENAVIKT.mjs → chunk-ITG4JQM3.mjs} +4 -4
  187. package/dist/{chunk-E5UKLSJZ.mjs → chunk-K3QX2M26.mjs} +11 -8
  188. package/dist/{chunk-4OORJ2DY.mjs → chunk-K7TKID3V.mjs} +8 -7
  189. package/dist/{chunk-2LG326TT.mjs → chunk-KAGADD2O.mjs} +4 -4
  190. package/dist/{chunk-IVSRW4HS.mjs → chunk-KC5QDYGZ.mjs} +4 -4
  191. package/dist/{chunk-7AFZWSCI.mjs → chunk-KPTY7UYQ.mjs} +1 -1
  192. package/dist/{chunk-YTXRIXNZ.mjs → chunk-KSSVIFYR.mjs} +9 -12
  193. package/dist/chunk-L3YKPTJQ.mjs +119 -0
  194. package/dist/chunk-M53LC4Q7.mjs +35 -0
  195. package/dist/chunk-MZ6WRTD2.mjs +40 -0
  196. package/dist/chunk-NGEN2EES.mjs +581 -0
  197. package/dist/{chunk-ZR6HSEAB.mjs → chunk-NPCBNGNE.mjs} +17 -26
  198. package/dist/{chunk-C43HRKXH.mjs → chunk-OBV72JD4.mjs} +1 -1
  199. package/dist/{chunk-LPV4NJJK.mjs → chunk-PGQ6FMXS.mjs} +6 -5
  200. package/dist/{chunk-MEPSKGBO.mjs → chunk-PI6RULJX.mjs} +1 -1
  201. package/dist/{chunk-F3YTWO3T.mjs → chunk-RA6SAAFE.mjs} +9 -8
  202. package/dist/{chunk-UNNRUJTM.mjs → chunk-RRKM4MKB.mjs} +7 -7
  203. package/dist/{chunk-ULGNQPNE.mjs → chunk-S2VGME7X.mjs} +1 -1
  204. package/dist/{chunk-OLVJFKXS.mjs → chunk-S44XWTTC.mjs} +35 -25
  205. package/dist/{chunk-YMYIEVZP.mjs → chunk-SZEKQAOY.mjs} +1 -1
  206. package/dist/{chunk-BXF4AMHY.mjs → chunk-TMH263OK.mjs} +5 -4
  207. package/dist/{chunk-NJG7DHVF.mjs → chunk-U6DEBYU5.mjs} +10 -9
  208. package/dist/{chunk-QXDGGOLC.mjs → chunk-UMZTPUB3.mjs} +33 -21
  209. package/dist/{chunk-KSUWPU2F.mjs → chunk-WIPEDNSD.mjs} +7 -7
  210. package/dist/{chunk-QDAZGZUF.mjs → chunk-XCIG6HT2.mjs} +3 -3
  211. package/dist/{chunk-4J2PXL36.mjs → chunk-Y6YS33GM.mjs} +40 -38
  212. package/dist/{chunk-4XOB5TTD.mjs → chunk-ZKDKKQCE.mjs} +5 -5
  213. package/dist/{chunk-LOBLCFMN.mjs → chunk-ZTPYUU5C.mjs} +5 -5
  214. package/dist/fonts.mjs +0 -2
  215. package/dist/index.d.mts +13 -73
  216. package/dist/index.d.ts +13 -73
  217. package/dist/index.js +1149 -1892
  218. package/dist/index.mjs +81 -86
  219. package/package.json +20 -20
  220. package/src/components/Accordion/Accordion.tsx +15 -9
  221. package/src/components/AlertBanner/AlertBanner.tsx +7 -6
  222. package/src/components/AppHeader/AppHeader.tsx +25 -10
  223. package/src/components/Avatar/Avatar.tsx +92 -1
  224. package/src/components/Avatar/index.ts +2 -2
  225. package/src/components/Badge/Badge.tsx +2 -2
  226. package/src/components/Button/Button.tsx +50 -46
  227. package/src/components/Card/Card.tsx +1 -0
  228. package/src/components/CategoryStrip/CategoryStrip.tsx +36 -49
  229. package/src/components/Checkbox/Checkbox.tsx +3 -0
  230. package/src/components/Chip/Chip.tsx +5 -4
  231. package/src/components/ConfirmDialog/ConfirmDialog.tsx +33 -17
  232. package/src/components/DetailRow/DetailRow.tsx +3 -3
  233. package/src/components/EmptyState/EmptyState.tsx +2 -2
  234. package/src/components/ErrorBoundary/ErrorBoundary.tsx +6 -6
  235. package/src/components/HolographicCard/HolographicCard.tsx +14 -95
  236. package/src/components/IconButton/IconButton.tsx +2 -2
  237. package/src/components/IconPicker/IconPicker.tsx +13 -12
  238. package/src/components/ImageUpload/ImageUpload.tsx +43 -46
  239. package/src/components/ImageViewer/ImageViewer.tsx +3 -3
  240. package/src/components/Input/Input.tsx +11 -5
  241. package/src/components/LabelValue/LabelValue.tsx +2 -2
  242. package/src/components/ListItem/ListItem.tsx +7 -4
  243. package/src/components/MediaCard/MediaCard.tsx +21 -59
  244. package/src/components/MenuItem/MenuItem.tsx +5 -2
  245. package/src/components/MonthPicker/MonthPicker.tsx +2 -2
  246. package/src/components/NumberStepper/NumberStepper.tsx +10 -6
  247. package/src/components/PagerDots/PagerDots.tsx +38 -28
  248. package/src/components/PricingCard/PricingCard.tsx +6 -6
  249. package/src/components/RadioGroup/RadioGroup.tsx +18 -31
  250. package/src/components/Select/Select.tsx +35 -39
  251. package/src/components/SelectableCard/SelectableCard.tsx +4 -6
  252. package/src/components/SelectableGrid/SelectableGrid.tsx +37 -72
  253. package/src/components/Sheet/Sheet.tsx +28 -15
  254. package/src/components/Sheet/index.ts +2 -2
  255. package/src/components/SheetSelect/SheetSelect.tsx +3 -3
  256. package/src/components/Skeleton/Skeleton.tsx +1 -1
  257. package/src/components/Slider/Slider.tsx +3 -0
  258. package/src/components/Spinner/Spinner.tsx +2 -2
  259. package/src/components/Stats/Stats.tsx +2 -2
  260. package/src/components/Switch/Switch.tsx +12 -7
  261. package/src/components/TabBar/TabBar.tsx +9 -8
  262. package/src/components/Text/Text.tsx +13 -1
  263. package/src/components/Textarea/Textarea.tsx +18 -32
  264. package/src/components/Toggle/Toggle.tsx +3 -3
  265. package/src/hooks/useConfirmDialog.ts +31 -42
  266. package/src/index.ts +3 -4
  267. package/src/theme/ThemeProvider.tsx +1 -4
  268. package/src/theme/colorUtils.ts +1 -72
  269. package/src/theme/colors.ts +40 -1
  270. package/src/theme/types.ts +2 -2
  271. package/src/utils/animations.ts +0 -47
  272. package/src/utils/curatedIcons.ts +93 -801
  273. package/src/utils/haptics.ts +13 -208
  274. package/src/utils/icons.ts +27 -91
  275. package/src/utils/pressable.ts +10 -61
  276. package/dist/VirtualList.d.mts +0 -19
  277. package/dist/VirtualList.d.ts +0 -19
  278. package/dist/VirtualList.js +0 -38
  279. package/dist/VirtualList.mjs +0 -2
  280. package/dist/chunk-2BA3JMKK.mjs +0 -136
  281. package/dist/chunk-3DKJ2GIC.mjs +0 -30
  282. package/dist/chunk-7ELGZ66G.mjs +0 -164
  283. package/dist/chunk-DVK4G2GT.mjs +0 -59
  284. package/dist/chunk-EJ7ZPXOH.mjs +0 -163
  285. package/dist/chunk-KA7LTET3.mjs +0 -71
  286. package/dist/chunk-LNPKGWBG.mjs +0 -134
  287. package/dist/chunk-NC5ZTR2Y.mjs +0 -32
  288. package/dist/chunk-SAWUXP3A.mjs +0 -1114
  289. package/dist/chunk-Y6FXYEAI.mjs +0 -8
  290. package/dist/chunk-YNROWHQJ.mjs +0 -46
  291. package/src/components/VirtualList/VirtualList.tsx +0 -60
  292. package/src/components/VirtualList/index.ts +0 -1
  293. package/src/utils/fontGuard.ts +0 -35
  294. package/src/utils/hover.ts +0 -25
  295. package/src/utils/useColorTransition.ts +0 -40
  296. package/src/utils/usePressScale.ts +0 -75
@@ -1,836 +1,431 @@
1
- # @retray-dev/ui-kit — Component Reference (v12.1.0)
1
+ ---
2
+ name: retray-ui-kit
3
+ description: >
4
+ Complete guide for @retray-dev/ui-kit — a React Native / Expo component
5
+ library (~55 components). Airbnb-inspired design, Sohne typography,
6
+ haptic-rich interactions, animated with pressto + react-native-ease.
7
+ Theme system, BottomSheet patterns, icon resolution, and conventions.
8
+ ---
2
9
 
3
- This file is the AI reference for this package. Add all three lines below to your project's `CLAUDE.md` to give Claude full context components, setup guide, and usage examples:
10
+ # @retray-dev/ui-kitAgent Skill
4
11
 
5
- ```markdown
6
- ## UI Components
7
- @./node_modules/@retray-dev/ui-kit/COMPONENTS.md
8
- @./node_modules/@retray-dev/ui-kit/CONSUMER.md
9
- @./node_modules/@retray-dev/ui-kit/EXAMPLES.md
10
- ```
12
+ > **Recommended — copy to repo root (no dependency on node_modules existing):**
13
+ > ```bash
14
+ > cp node_modules/@retray-dev/ui-kit/SKILL.md SKILL.md
15
+ > cp node_modules/@retray-dev/ui-kit/CONSUMER.md CONSUMER.md
16
+ > cp node_modules/@retray-dev/ui-kit/EXAMPLES.md EXAMPLES.md
17
+ > ```
18
+ >
19
+ > Then add to `CLAUDE.md`:
20
+ > ```markdown
21
+ > ## UI Kit
22
+ > @./SKILL.md
23
+ > @./CONSUMER.md
24
+ > @./EXAMPLES.md
25
+ > ```
11
26
 
12
27
  ---
13
28
 
14
- ## Setup (Required)
15
-
16
- ### Recommended: `RetrayProvider` (one wrapper)
29
+ ## Required Setup
17
30
 
18
- Since v8, a single `RetrayProvider` wires all five required providers in the correct order — use this and skip the manual nesting:
31
+ ### Providers
32
+ Use `RetrayProvider` (single wrapper) or manual tree. Order is mandatory:
19
33
 
20
- ```tsx
21
- import { RetrayProvider } from '@retray-dev/ui-kit'
22
-
23
- export default function App() {
24
- const [fontsLoaded] = useFonts(SohneFonts)
25
- if (!fontsLoaded) return null
26
- return (
27
- <RetrayProvider colorScheme="system">
28
- {/* your app */}
29
- </RetrayProvider>
30
- )
31
- }
32
34
  ```
33
-
34
- ### Manual (equivalent) — if you need a custom tree
35
-
36
- `RetrayProvider` is exactly this; the individual providers stay exported. Order is mandatory:
37
-
38
- ```tsx
39
- import { SafeAreaProvider, initialWindowMetrics } from 'react-native-safe-area-context'
40
- import { GestureHandlerRootView } from 'react-native-gesture-handler'
41
- import { ThemeProvider, BottomSheetModalProvider, ToastProvider } from '@retray-dev/ui-kit'
42
-
43
- export default function App() {
44
- return (
45
- <SafeAreaProvider initialMetrics={initialWindowMetrics}>
46
- <GestureHandlerRootView style={{ flex: 1 }}>
47
- <ThemeProvider colorScheme="system">
48
- <BottomSheetModalProvider>
49
- <ToastProvider>
50
- {/* your app */}
51
- </ToastProvider>
52
- </BottomSheetModalProvider>
53
- </ThemeProvider>
54
- </GestureHandlerRootView>
55
- </SafeAreaProvider>
56
- )
57
- }
35
+ SafeAreaProvider > GestureHandlerRootView > ThemeProvider > BottomSheetModalProvider > ToastProvider
58
36
  ```
59
37
 
60
- **Provider order is mandatory:**
61
- - `SafeAreaProvider` must be outermost — required by `@gorhom/bottom-sheet`
62
- - `initialMetrics={initialWindowMetrics}` is required on Android to avoid a "No safe area value available" crash on first render
63
- - `GestureHandlerRootView` must wrap everything — required by `@gorhom/bottom-sheet`
64
- - `BottomSheetModalProvider` must be inside `GestureHandlerRootView`
65
- - `ToastProvider` wraps children and renders `Toaster` (from `sonner-native`) internally
66
-
67
- ### Peer dependencies
38
+ `initialMetrics={initialWindowMetrics}` required on Android.
68
39
 
69
- Install all required peers (Expo projects: swap `pnpm add` for `npx expo install` to get SDK-pinned versions):
70
-
71
- ```bash
72
- pnpm add expo-font expo-haptics expo-linear-gradient react-native-safe-area-context @gorhom/bottom-sheet react-native-reanimated react-native-gesture-handler react-native-worklets @react-native-picker/picker @react-native-community/slider @expo/vector-icons react-native-size-matters react-native-svg react-native-screens sonner-native pressto react-native-ease
73
- ```
74
-
75
- Then add the Worklets Babel plugin to `babel.config.js` (required by `@gorhom/bottom-sheet` and the pressable animations):
40
+ ### Peer Dependencies (all required unless noted)
41
+ - **Hard:** `expo-haptics`, `expo-linear-gradient`, `expo-font`, `expo-image`, `react-native-reanimated`, `react-native-gesture-handler`, `react-native-worklets`, `react-native-safe-area-context`, `react-native-screens`, `react-native-svg`, `@gorhom/bottom-sheet`, `@react-native-picker/picker`, `@react-native-community/slider`, `@expo/vector-icons`, `react-native-size-matters`, `sonner-native`, `react-native-ease`, `pressto`
42
+ - **Optional:** `react-native-image-picker` (ImageUpload), `@shopify/react-native-skia` + `expo-sensors` (HolographicCard deep-import only)
76
43
 
44
+ ### Babel Config
77
45
  ```js
78
- module.exports = function (api) {
79
- api.cache(true)
80
- return {
81
- presets: ['babel-preset-expo'],
82
- plugins: ['react-native-worklets/plugin'], // NOT react-native-reanimated/plugin
83
- }
84
- }
46
+ plugins: ['react-native-worklets/plugin']
47
+ // NOT react-native-reanimated/plugin
85
48
  ```
86
49
 
87
- | Peer | Required? | Used by | Notes |
88
- |------|-----------|---------|-------|
89
- | `pressto` | **Required** | Every interactive component | Press-scale animations (`src/utils/pressable.ts`). Static import via the barrel — omitting it crashes the module load, not just one component. |
90
- | `sonner-native` | **Required** | `Toast` | Also pulls `react-native-svg` + `react-native-screens`. |
91
- | `react-native-reanimated` (≥4.0) | **Required** | Animations, `Sheet`, pressables | Needs the `react-native-worklets/plugin` Babel plugin. |
92
- | `react-native-gesture-handler` | **Required** | `Sheet`, pressables | Wrap app in `GestureHandlerRootView`. |
93
- | `@gorhom/bottom-sheet` (≥5.2.0) | **Required** | `Sheet`, `ConfirmDialog` | 5.1.x crashes on Reanimated v4. |
94
- | `expo-haptics` | **Required** | Haptic feedback (all interactions) | Web-safe wrapper, no-op on web. |
95
- | `expo-font` | **Required** | All `Text` | Load `SohneFonts` at app root before rendering. |
96
- | `expo-linear-gradient` | **Required** | `Skeleton` shimmer | |
97
- | `@react-native-picker/picker` | **Required** | `Select` | |
98
- | `@react-native-community/slider` | **Required** | `Slider` | |
99
- | `@expo/vector-icons` | **Required** | Icons everywhere | |
100
- | `react-native-size-matters` | **Required** | Responsive scaling | |
101
- | `react-native-ease` | **Required** | `Checkbox`, `Chip`, `CategoryStrip`, `Switch`, `RadioGroup`, `Toggle` | Declarative `EaseView` animation layer. Static import in source — omitting it crashes the module load. `pnpm add react-native-ease` |
102
- | `react-native-pulsar` | **Optional** | Rich haptics on dev builds | Loaded via dynamic `import()` + try/catch; falls back to `expo-haptics`. Skip in Expo Go. `pnpm add react-native-pulsar` |
103
- | `@shopify/react-native-skia` + `expo-sensors` | **Optional** | Deep-import `HolographicCard` only | `pnpm add @shopify/react-native-skia expo-sensors` |
104
-
105
- > **Dev build vs Expo Go:** Everything in the kit runs in **Expo Go** — `react-native-pulsar` is the only native module that needs a custom dev build, and it degrades gracefully (basic haptics) when absent. No component is broken by running in Expo Go.
106
-
107
- ### Troubleshooting: `react-native-screens` codegen on RN 0.83
108
-
109
- Toast pulls `sonner-native`, which imports `react-native-screens` via its `src/*` path. On **React Native 0.83** that source trips a `@react-native/codegen` parse error (`CT.WithDefault` not parseable). It is a `react-native-screens` issue, not the kit — but since Toast surfaces it, add this resolver to your `metro.config.js` to force the compiled `lib/commonjs` build:
110
-
111
- ```js
112
- // metro.config.js
113
- const { getDefaultConfig } = require('expo/metro-config')
114
- const config = getDefaultConfig(__dirname)
115
-
116
- const defaultResolveRequest = config.resolver.resolveRequest
117
- config.resolver.resolveRequest = (context, moduleName, platform) => {
118
- // Force react-native-screens to its compiled output, bypassing the src codegen bug.
119
- if (moduleName.startsWith('react-native-screens/src')) {
120
- moduleName = moduleName.replace('react-native-screens/src', 'react-native-screens/lib/commonjs')
121
- }
122
- return (defaultResolveRequest ?? context.resolveRequest)(context, moduleName, platform)
123
- }
124
-
125
- module.exports = config
126
- ```
127
-
128
- ## Typography — Sohne (Required)
129
-
130
- All components use **Sohne** as the font family. You **must** load it before rendering any UI kit component.
131
-
132
- ### How it works
133
-
134
- 1. When you install `@retray-dev/ui-kit`, the **postinstall script** automatically copies 28 `.otf` font files to `assets/fonts/sohne/` in your project root
135
- 2. You must define `SohneFonts` with static `require()` calls in your `App.tsx` (see boilerplate below)
136
- 3. Pass it to `expo-font`'s `useFonts()` hook at your app root
137
- 4. All library components reference fonts by family name (e.g., `fontFamily: 'Sohne-SemiBold'`)
138
-
139
- ### SohneFonts boilerplate — copy this into your App.tsx
140
-
141
- ```tsx
142
- import { useFonts } from 'expo-font'
143
-
144
- // Fonts copied to assets/fonts/sohne/ by @retray-dev/ui-kit postinstall
145
- const SohneFonts = {
146
- 'Sohne-ExtraLight': require('./assets/fonts/sohne/Sohne-ExtraLight.otf'),
147
- 'Sohne-ExtraLightItalic': require('./assets/fonts/sohne/Sohne-ExtraLightItalic.otf'),
148
- 'Sohne-Light': require('./assets/fonts/sohne/Sohne-Light.otf'),
149
- 'Sohne-LightItalic': require('./assets/fonts/sohne/Sohne-LightItalic.otf'),
150
- 'Sohne-Regular': require('./assets/fonts/sohne/Sohne-Regular.otf'),
151
- 'Sohne-Italic': require('./assets/fonts/sohne/Sohne-Italic.otf'),
152
- 'Sohne-Medium': require('./assets/fonts/sohne/Sohne-Medium.otf'),
153
- 'Sohne-MediumItalic': require('./assets/fonts/sohne/Sohne-MediumItalic.otf'),
154
- 'Sohne-SemiBold': require('./assets/fonts/sohne/Sohne-SemiBold.otf'),
155
- 'Sohne-SemiBoldItalic': require('./assets/fonts/sohne/Sohne-SemiBoldItalic.otf'),
156
- 'Sohne-Bold': require('./assets/fonts/sohne/Sohne-Bold.otf'),
157
- 'Sohne-BoldItalic': require('./assets/fonts/sohne/Sohne-BoldItalic.otf'),
158
- 'Sohne-ExtraBold': require('./assets/fonts/sohne/Sohne-ExtraBold.otf'),
159
- 'Sohne-ExtraBoldItalic': require('./assets/fonts/sohne/Sohne-ExtraBoldItalic.otf'),
160
- 'SohneMono-ExtraLight': require('./assets/fonts/sohne/SohneMono-ExtraLight.otf'),
161
- 'SohneMono-ExtraLightItalic': require('./assets/fonts/sohne/SohneMono-ExtraLightItalic.otf'),
162
- 'SohneMono-Light': require('./assets/fonts/sohne/SohneMono-Light.otf'),
163
- 'SohneMono-LightItalic': require('./assets/fonts/sohne/SohneMono-LightItalic.otf'),
164
- 'SohneMono-Regular': require('./assets/fonts/sohne/SohneMono-Regular.otf'),
165
- 'SohneMono-Italic': require('./assets/fonts/sohne/SohneMono-Italic.otf'),
166
- 'SohneMono-Medium': require('./assets/fonts/sohne/SohneMono-Medium.otf'),
167
- 'SohneMono-MediumItalic': require('./assets/fonts/sohne/SohneMono-MediumItalic.otf'),
168
- 'SohneMono-SemiBold': require('./assets/fonts/sohne/SohneMono-SemiBold.otf'),
169
- 'SohneMono-SemiBoldItalic': require('./assets/fonts/sohne/SohneMono-SemiBoldItalic.otf'),
170
- 'SohneMono-Bold': require('./assets/fonts/sohne/SohneMono-Bold.otf'),
171
- 'SohneMono-BoldItalic': require('./assets/fonts/sohne/SohneMono-BoldItalic.otf'),
172
- 'SohneMono-ExtraBold': require('./assets/fonts/sohne/SohneMono-ExtraBold.otf'),
173
- 'SohneMono-ExtraBoldItalic': require('./assets/fonts/sohne/SohneMono-ExtraBoldItalic.otf'),
174
- }
175
-
176
- export default function App() {
177
- const [fontsLoaded] = useFonts(SohneFonts)
178
- if (!fontsLoaded) return null
179
- return (
180
- // ... your providers and app
181
- )
182
- }
183
- ```
184
-
185
- ### .gitignore recommendation
186
-
187
- Fonts are copied to `assets/fonts/sohne/` on install. You can either:
188
- - **Commit them** (no network needed during CI builds)
189
- - **Ignore them** (re-copied on every `pnpm install`)
190
-
191
- ```gitignore
192
- # Sohne fonts — copied by @retray-dev/ui-kit postinstall
193
- # Either commit these or ignore them (re-copied on install)
194
- assets/fonts/sohne/
195
- ```
196
-
197
- **Included weights (28 files):**
198
- - Sohne: `Sohne-ExtraLight`, `Sohne-Light`, `Sohne-Regular`, `Sohne-Medium`, `Sohne-SemiBold`, `Sohne-Bold`, `Sohne-ExtraBold` + italic variants
199
- - SohneMono: `SohneMono-ExtraLight`, `SohneMono-Light`, `SohneMono-Regular`, `SohneMono-Medium`, `SohneMono-SemiBold`, `SohneMono-Bold`, `SohneMono-ExtraBold` + italic variants
200
-
201
- Pair with `expo-splash-screen` in production:
202
- ```tsx
203
- import * as SplashScreen from 'expo-splash-screen'
204
- SplashScreen.preventAutoHideAsync()
205
-
206
- useEffect(() => {
207
- if (fontsLoaded) SplashScreen.hideAsync()
208
- }, [fontsLoaded])
209
- ```
210
-
211
- ---
212
-
213
- ## Theme System
214
-
215
- ### ThemeProvider Props
216
-
217
- | Prop | Type | Default | Notes |
218
- |------|------|---------|-------|
219
- | colorScheme | `'light' \| 'dark' \| 'system'` | `'system'` | `'system'` auto-detects device setting and updates when it changes |
220
- | theme | `{ light?: Partial<ThemeColors>, dark?: Partial<ThemeColors> }` | — | Override any subset of the 12 public tokens per scheme |
221
-
222
- **Custom theme example:**
223
- ```tsx
224
- const myTheme = {
225
- light: { primary: '#ff385c', primaryForeground: '#ffffff' },
226
- dark: { primary: '#ff385c', primaryForeground: '#ffffff' },
227
- }
228
- <ThemeProvider theme={myTheme} colorScheme="system">
229
- ```
230
-
231
- ### useTheme Hook
232
-
233
- ```tsx
234
- import { useTheme } from '@retray-dev/ui-kit'
235
-
236
- function MyComponent() {
237
- const { colors, colorScheme } = useTheme()
238
- return <View style={{ backgroundColor: colors.background }} />
239
- }
240
- ```
241
-
242
- Returns `colors` (full `ResolvedColors` palette) and `colorScheme` (`'light' | 'dark'`).
50
+ ### Fonts (Sohne)
51
+ Postinstall copies 28 `.otf` to `assets/fonts/sohne/`. Consumer defines static `require()` calls in App.tsx with `expo-font`'s `useFonts()`.
243
52
 
244
53
  ---
245
54
 
246
- ## Theme Tokens
55
+ ## Design System (Airbnb-inspired)
247
56
 
248
- ### Public Tokens (ThemeColors) — 12 tokens consumer can override
57
+ | Token | Rule |
58
+ |---|---|
59
+ | **Touch targets** | ≥44pt height on all interactive elements |
60
+ | **Spacing** | 8pt grid, 4pt micro-steps |
61
+ | **Radius** | sm=6, md=8, lg=12, xl=16 |
62
+ | **Colors** | 12 public tokens → `deriveColors()` computes 26 resolved tokens |
63
+ | **Typography** | Sohne, modest weights (500-600 display), `allowFontScaling={true}` everywhere |
64
+ | **Shadows** | Max 2 tiers. Flat baseline + single float tier |
65
+ | **Elevation** | Depth from rounded corners + surface separation, not shadows |
249
66
 
250
- These are the only values you need to supply when customizing the theme. The library derives all other colors internally.
67
+ ### Theme Tokens 12 Public (consumer can override)
251
68
 
252
- | Token | Light Default | Dark Default | Semantic Role |
253
- |-------|--------------|--------------|---------------|
69
+ | Token | Light Default | Dark Default | Role |
70
+ |-------|--------------|--------------|------|
254
71
  | `background` | `#ffffff` | `#0f0f0f` | Screen / page background |
255
- | `foreground` | `#1a1a1a` | `#fafafa` | Primary text — deep near-black, not pure black |
256
- | `card` | `#ffffff` | `#1c1c1c` | Card / elevated surface background |
257
- | `primary` | `#1a1a1a` | `#fafafa` | Primary action (buttons, selected states, active indicators) |
258
- | `primaryForeground` | `#ffffff` | `#0f0f0f` | Text/icon placed on primary-colored backgrounds |
72
+ | `foreground` | `#1a1a1a` | `#fafafa` | Primary text |
73
+ | `card` | `#ffffff` | `#1c1c1c` | Card / elevated surface |
74
+ | `primary` | `#1a1a1a` | `#fafafa` | Primary action (buttons, selected states) |
75
+ | `primaryForeground` | `#ffffff` | `#0f0f0f` | Text/icon on primary |
259
76
  | `border` | `#dddddd` | `#303030` | Borders, dividers, input outlines |
260
- | `destructive` | `#c72828` | `#ef5350` | Error / danger / delete actions |
261
- | `destructiveForeground` | `#ffffff` | `#ffffff` | Text/icon on destructive backgrounds |
262
- | `success` | `#1a7a45` | `#2e7d52` | Success / confirmation states |
263
- | `successForeground` | `#ffffff` | `#ffffff` | Text/icon on success backgrounds |
264
- | `warning` | `#9a5200` | `#f5a623` | Warning / caution states |
265
- | `warningForeground` | `#ffffff` | `#0f0f0f` | Text/icon on warning backgrounds |
266
- | `overlay` *(optional)* | `rgba(0,0,0,0.45)` | `rgba(0,0,0,0.45)` | Backdrop/overlay color behind sheets and dialogs |
267
- | `accent` *(optional)* | `#d4561d` | `#e87645` | Secondary brand accent (e.g. Airbnb coral). Falls back to `primary` |
268
- | `accentForeground` *(optional)* | same as `primaryForeground` | same as `primaryForeground` | Text/icon on accent backgrounds. Falls back to `primaryForeground` |
269
-
270
- ### Derived Tokens (ResolvedColors) read-only via useTheme().colors
271
-
272
- The full palette components consume. Never supply these directly — they are computed by `deriveColors()` from the 12 public tokens above.
273
-
274
- | Token | Derived From | Purpose |
275
- |-------|-------------|---------|
276
- | `foregroundSubtle` | `foreground` @ ~70% | Body text, subtitles, secondary content |
277
- | `foregroundMuted` | `foreground` @ ~62% | Captions, timestamps, placeholders |
278
- | `surface` | `background` slightly off-canvas | Chip backgrounds, input fills, tag backgrounds |
279
- | `surfaceStrong` | `background` stronger offset | Pressed/hover fill states |
280
- | `skeleton` | `background` @ ±10% | Skeleton placeholder higher contrast than surface for visibility |
281
- | `destructiveTint` | `destructive` blended to bg | Alert banner background, toast background |
282
- | `destructiveBorder` | `destructive` @ 30% | Alert banner border, badge outline |
283
- | `successTint` | `success` blended to bg | Success banner background |
77
+ | `destructive` | `#c72828` | `#ef5350` | Error / danger / delete |
78
+ | `destructiveForeground` | `#ffffff` | `#ffffff` | Text on destructive |
79
+ | `success` | `#1a7a45` | `#2e7d52` | Success / confirmation |
80
+ | `successForeground` | `#ffffff` | `#ffffff` | Text on success |
81
+ | `warning` | `#9a5200` | `#f5a623` | Warning / caution |
82
+ | `warningForeground` | `#ffffff` | `#0f0f0f` | Text on warning |
83
+ | `overlay` *(optional)* | `rgba(0,0,0,0.45)` | `rgba(0,0,0,0.45)` | Backdrop behind sheets |
84
+ | `accent` *(optional)* | `#d4561d` | `#e87645` | Brand accent (falls back to `primary`) |
85
+ | `accentForeground` *(optional)* | `primaryForeground` | `primaryForeground` | Text on accent |
86
+
87
+ ### Derived Tokens — 26 ResolvedColors (read-only via `useTheme().colors`)
88
+
89
+ | Token | Source | Purpose |
90
+ |-------|--------|---------|
91
+ | `foregroundSubtle` | `foreground` @ ~70% | Body text, subtitles |
92
+ | `foregroundMuted` | `foreground` @ ~62% | Captions, placeholders (WCAG AA 4.5:1) |
93
+ | `surface` | `background` off-canvas | Chip backgrounds, input fills |
94
+ | `surfaceStrong` | `background` stronger offset | Pressed/hover states |
95
+ | `skeleton` | `background` @ ±10% | Skeleton placeholder |
96
+ | `destructiveTint` | `destructive` to bg | Alert banner bg |
97
+ | `destructiveBorder` | `destructive` @ 30% | Alert banner border |
98
+ | `successTint` | `success` to bg | Success banner bg |
284
99
  | `successBorder` | `success` @ 30% | Success banner border |
285
- | `warningTint` | `warning` blended to bg | Warning banner background |
100
+ | `warningTint` | `warning` to bg | Warning banner bg |
286
101
  | `warningBorder` | `warning` @ 30% | Warning banner border |
287
- | `ring` | `= primary` | Focus ring color (always matches primary) |
288
- | `input` | `= border` | Input field border (always matches border) |
289
- | `separator` | `border` @ ±16-22% | Divider/separator line — deliberately darker than border |
290
- | `overlay` | `overlay` token or `rgba(0,0,0,0.45)` | Backdrop behind sheets and dialogs |
291
- | `accentResolved` | `accent` token or `= primary` | Resolved accent color — always present |
292
- | `accentForegroundResolved` | `accentForeground` token or `= primaryForeground` | Resolved text on accent — always present |
293
-
294
- **Usage example — building a custom component using derived tokens:**
295
- ```tsx
296
- const { colors } = useTheme()
297
-
298
- // Text hierarchy
299
- <Text style={{ color: colors.foreground }}>Primary text</Text>
300
- <Text style={{ color: colors.foregroundSubtle }}>Secondary text</Text>
301
- <Text style={{ color: colors.foregroundMuted }}>Caption / timestamp</Text>
302
-
303
- // Surface fills (unselected chips, inactive backgrounds)
304
- <View style={{ backgroundColor: colors.surface }}>
305
-
306
- // Warning alert banner
307
- <View style={{ backgroundColor: colors.warningTint, borderColor: colors.warningBorder }}>
308
- <Text style={{ color: colors.warning }}>Warning message</Text>
309
- ```
310
-
311
- ### deriveColors export
102
+ | `ring` | `= primary` | Focus ring |
103
+ | `input` | `= border` | Input border |
104
+ | `separator` | `border` @ ±16-22% | Divider line |
105
+ | `overlay` | `overlay` token or `rgba(0,0,0,0.45)` | Sheet backdrop |
106
+ | `accentResolved` | `accent` token or `= primary` | Resolved accent |
107
+ | `accentForegroundResolved` | `accentForeground` or `= primaryForeground` | Resolved text on accent |
312
108
 
109
+ ### Color Utility
313
110
  ```tsx
314
- import { deriveColors } from '@retray-dev/ui-kit'
315
-
316
- const resolved = deriveColors(myThemeColors, 'light')
317
- // resolved contains all 26 ResolvedColors tokens
111
+ import { withAlpha } from '@retray-dev/ui-kit'
112
+ // hex → rgba: withAlpha('#1a1a1a', 0.15) → 'rgba(26,26,26,0.15)'
318
113
  ```
319
114
 
320
- ---
321
-
322
- ### Color Utilities
323
-
324
- **Import:** `import { withAlpha } from '@retray-dev/ui-kit'`
325
-
326
- Convert a hex color to rgba with the given alpha. Useful for semi-transparent backgrounds, borders, and overlays derived from theme colors.
327
-
328
- ```tsx
329
- import { useTheme, withAlpha } from '@retray-dev/ui-kit'
330
-
331
- const { colors } = useTheme()
332
-
333
- <View style={{ backgroundColor: withAlpha(colors.primary, 0.15) }}>
334
- <Text style={{ color: colors.primary }}>Tinted background</Text>
335
- </View>
336
- ```
115
+ ### Shape Language
116
+ Soft rounded rects everywhere. Buttons: `RADIUS.md=14px` (NOT pill). IconButton: `RADIUS.full` (circle). Inputs: 8px. Cards: 14px. Modals: 16px top corners only.
337
117
 
338
118
  ---
339
119
 
340
- ## Design Tokens
341
-
342
- Static structural constants — no context or provider needed.
120
+ ### Design Tokens — Static Constants, No Provider Needed
343
121
 
344
122
  ```ts
345
123
  import { SPACING, ICON_SIZES, RADIUS, SHADOWS, BREAKPOINTS, TYPOGRAPHY } from '@retray-dev/ui-kit'
346
124
  ```
347
125
 
348
- ### SPACING — 8pt grid with 2pt micro-step
126
+ **SPACING — 8pt grid**
349
127
 
350
128
  | Key | Value | Use |
351
129
  |-----|-------|-----|
352
- | `xxs` | 2 | Micro gaps (icon to text in badges) |
130
+ | `xxs` | 2 | Micro gaps |
353
131
  | `xs` | 4 | Tight internal gaps |
354
132
  | `sm` | 8 | Component internal padding |
355
133
  | `md` | 12 | Medium gaps |
356
134
  | `base` | 16 | Default content padding |
357
135
  | `lg` | 24 | Section internal padding |
358
- | `xl` | 32 | Between major content blocks |
136
+ | `xl` | 32 | Between major blocks |
359
137
  | `xxl` | 48 | Section padding |
360
- | `section` | 64 | Major band separators (Airbnb hero → grid rhythm) |
361
-
362
- **Types:** `Spacing`, `SpacingKey`
363
-
364
- ```tsx
365
- <View style={{ gap: SPACING.md, padding: SPACING.base }} />
366
- ```
138
+ | `section` | 64 | Major band separators |
367
139
 
368
- ### ICON_SIZES
140
+ **ICON_SIZES**
369
141
 
370
142
  | Key | Value | Use |
371
143
  |-----|-------|-----|
372
- | `sm` | 14 | Badge icons, inline micro icons |
144
+ | `sm` | 14 | Badge icons |
373
145
  | `md` | 18 | Standard component icons |
374
146
  | `lg` | 22 | Larger inline icons |
375
147
  | `xl` | 28 | Feature icons |
376
148
  | `2xl` | 32 | Display icons |
377
149
 
378
- **Types:** `IconSize`, `IconSizeKey`
379
-
380
- ### RADIUS — Airbnb shape language
150
+ **RADIUS Airbnb shape language**
381
151
 
382
152
  | Key | Value | Used in |
383
153
  |-----|-------|---------|
384
- | `none` | 0 | No rounding |
154
+ | `none` | 0 | |
385
155
  | `xs` | 4 | Micro chips, tags |
386
156
  | `sm` | 8 | Inputs, Textarea, Select, Checkbox |
387
- | `md` | 14 | Cards, Buttons (all variants), MediaCard, AlertBanner, Toast, EmptyState |
157
+ | `md` | 14 | Cards, Buttons, MediaCard, AlertBanner, Toast |
388
158
  | `lg` | 20 | Sheet top corners |
389
159
  | `xl` | 32 | Large decorative elements |
390
160
  | `full` | 9999 | IconButton (circle), CategoryStrip chips |
391
161
 
392
- **Types:** `Radius`, `RadiusKey`
393
-
394
- ```tsx
395
- <View style={{ borderRadius: RADIUS.md }} />
396
- ```
397
-
398
- ### SHADOWS — Cross-platform shadow presets
399
-
400
- | Key | shadowOffset.height | shadowOpacity | shadowRadius | elevation | Use |
401
- |-----|---------------------|---------------|--------------|-----------|-----|
402
- | `sm` | 1 | 0.06 | 4 | 2 | Default card shadow |
403
- | `md` | 2 | 0.10 | 8 | 5 | Hover float / elevated elements |
404
- | `lg` | 6 | 0.16 | 16 | 10 | Modals, overlays |
405
- | `xl` | 12 | 0.24 | 24 | 18 | High-elevation dialogs |
162
+ **SHADOWS Cross-platform**
406
163
 
407
- ```tsx
408
- <View style={[styles.card, SHADOWS.sm]} />
409
- // Hover state
410
- <View style={[styles.card, hovered ? SHADOWS.md : SHADOWS.sm]} />
411
- ```
164
+ | Key | opacity | radius | elevation | Use |
165
+ |-----|---------|--------|-----------|-----|
166
+ | `sm` | 0.06 | 4 | 2 | Default card |
167
+ | `md` | 0.10 | 8 | 5 | Hover float |
168
+ | `lg` | 0.16 | 16 | 10 | Modals |
169
+ | `xl` | 0.24 | 24 | 18 | High-elevation |
412
170
 
413
- ### BREAKPOINTS
171
+ **BREAKPOINTS**
414
172
 
415
173
  | Key | Value | Use |
416
174
  |-----|-------|-----|
417
175
  | `wide` | 700 | Tablet / wide layout threshold |
418
176
 
419
- ```tsx
420
- const isWide = useWindowDimensions().width >= BREAKPOINTS.wide
421
- ```
177
+ **TYPOGRAPHY — 17 variants**
422
178
 
423
- ### TYPOGRAPHY 16 Airbnb-aligned variants
424
-
425
- All components use these tokens for text styling. Import and use in custom components for consistency.
426
-
427
- | Key | Size | Weight | lineHeight | letterSpacing | Use |
428
- |-----|------|--------|-----------|---------------|-----|
429
- | `display-hero` | 64 | 700 | 70 | -1 | Large number display, balance totals |
179
+ | Key | Size | Weight | lh | ls | Use |
180
+ |-----|------|--------|----|----|-----|
181
+ | `display-hero` | 64 | 700 | 70 | -1 | Large number display |
430
182
  | `display-xl` | 28 | 700 | 40 | 0 | Screen titles, hero headings |
431
- | `display-lg` | 22 | 500 | 26 | -0.44 | Section headings |
432
- | `display-md` | 21 | 700 | 30 | 0 | Card titles, dialog titles |
433
- | `display-sm` | 20 | 600 | 24 | -0.18 | Sub-section headings |
434
- | `title-md` | 16 | 600 | 20 | 0 | Row titles, list item titles |
435
- | `title-sm` | 16 | 500 | 20 | 0 | Secondary titles |
183
+ | `display-lg` | 24 | 600 | 32 | -0.3 | Section headings |
184
+ | `display-md` | 20 | 600 | 28 | 0 | Card titles |
185
+ | `display-sm` | 18 | 600 | 24 | -0.18 | Sub-section headings |
186
+ | `title-md` | 17 | 600 | 22 | 0 | Row titles |
187
+ | `title-sm` | 15 | 500 | 20 | 0 | Secondary titles |
436
188
  | `body-md` | 16 | 400 | 24 | 0 | Primary body copy |
437
- | `body-sm` | 14 | 400 | 20 | 0 | Secondary body, descriptions |
438
- | `caption` | 14 | 500 | 18 | 0 | Labels above inputs, item captions |
439
- | `caption-sm` | 13 | 400 | 16 | 0 | Timestamps, metadata |
440
- | `badge-text` | 11 | 600 | 13 | 0 | Badge labels, small tags |
441
- | `micro-label` | 12 | 700 | 16 | 0 | Micro labels, overlines |
442
- | `uppercase-tag` | 10 | 700 | 13 | 0.8 | Uppercase decorative tags (auto-uppercase) |
443
- | `button-lg` | 16 | 500 | 20 | 0 | Button labels (md/lg size) |
444
- | `button-sm` | 14 | 500 | 18 | 0 | Button labels (sm size) |
445
-
446
- **Types:** `Typography`, `TypographyKey`
447
-
448
- ```tsx
449
- import { TYPOGRAPHY } from '@retray-dev/ui-kit'
450
-
451
- // Use in StyleSheet
452
- const styles = StyleSheet.create({
453
- heading: {
454
- ...TYPOGRAPHY['display-xl'],
455
- color: colors.foreground,
456
- },
457
- })
458
- ```
459
-
460
- ---
461
-
462
- ## Migration Guide: v4 → v5
463
-
464
- ### Breaking Changes
465
-
466
- **Button variants renamed:**
467
- | v4 | v5 |
468
- |----|-----|
469
- | `outline` | `secondary` |
470
- | `ghost` | `text` |
471
- | `secondary` (filled gray) | removed — use Card/surface instead |
472
-
473
- **Text variants replaced:**
474
- | v4 | v5 equivalent |
475
- |----|---------------|
476
- | `h1` | `display-xl` |
477
- | `h2` | `display-lg` or `display-md` |
478
- | `h3` | `display-sm` |
479
- | `body` | `body-md` |
480
- | `label` | `title-sm` or `caption` |
481
- | `caption` | `caption-sm` |
482
-
483
- **Theme tokens changed:**
484
- | v4 | v5 |
485
- |----|-----|
486
- | `secondary`, `secondaryForeground` | removed (use `surface` derived token) |
487
- | `accent`, `accentForeground` | removed (use `surfaceStrong`) |
488
- | `muted` | removed → `surface` (derived) |
489
- | `mutedForeground` | removed → `foregroundSubtle` / `foregroundMuted` (derived) |
490
- | Added | `warning`, `warningForeground` |
491
- | Added (derived) | `warningTint`, `warningBorder` |
492
-
493
- **IconButton variant:**
494
- | v4 | v5 |
495
- |----|-----|
496
- | `ghost` | `text` |
189
+ | `body-sm` | 14 | 400 | 20 | 0 | Secondary body |
190
+ | `caption` | 14 | 500 | 18 | 0 | Input labels |
191
+ | `caption-sm` | 13 | 400 | 16 | 0 | Timestamps |
192
+ | `badge-text` | 11 | 600 | 14 | 0 | Badge labels |
193
+ | `badge-text-md` | 13 | 600 | 16 | 0 | Badge labels (md size) |
194
+ | `micro-label` | 12 | 700 | 16 | 0 | Overlines |
195
+ | `uppercase-tag` | 11 | 700 | 14 | 0.6 | Decorative tags (auto-uppercase) |
196
+ | `button-lg` | 16 | 500 | 22 | 0 | Button (md/lg) |
197
+ | `button-sm` | 14 | 500 | 18 | 0 | Button (sm) |
497
198
 
498
199
  ---
499
200
 
500
- ## Migration Guide: v5 → v6
501
-
502
- ### New Components
503
-
504
- **`MenuItem`** — Navigation row with icon, label, and optional right slot (`rightRender`). Replaces ad-hoc `ListItem` usage for settings/nav menus. Zero horizontal padding by design — consumer controls spacing.
201
+ ## Icons
505
202
 
203
+ ### Icon Component
506
204
  ```tsx
507
- import { MenuItem } from '@retray-dev/ui-kit'
508
- // variants: 'plain' (default) | 'card'
509
- <MenuItem label="Profile" subtitle="Edit your details" iconName="user" onPress={() => {}} />
510
- <MenuItem label="Notifications" iconName="bell" rightRender={<Switch value={on} onValueChange={setOn} />} showChevron={false} onPress={() => {}} />
205
+ import { Icon } from '@retray-dev/ui-kit'
206
+ <Icon name="home" size={24} color="#000" />
207
+ // Returns null if name not found no crash
511
208
  ```
512
209
 
513
- ### Breaking Changes
210
+ ### Resolution Order (first-match wins)
211
+ Feather > AntDesign > Entypo > FontAwesome5 > MaterialIcons > Ionicons
514
212
 
515
- **Sheet keyboard defaults changed:**
516
- | Prop | v5 default | v6 default |
517
- |------|-----------|-----------|
518
- | `keyboardBehavior` | `'fillParent'` (Android) / `'interactive'` (iOS) | `'interactive'` (both) |
519
- | `android_keyboardInputMode` | `'adjustResize'` | `'adjustPan'` |
520
-
521
- If you relied on `adjustResize`, set it explicitly: `android_keyboardInputMode="adjustResize"`.
213
+ **Feather is first** — most names resolve there (outlined style). Names with `-outline` suffix resolve to Ionicons (no Feather collision).
522
214
 
523
- **Toast API simplified:**
215
+ ### `iconName` props on components
216
+ | Component | Prop(s) | Color |
217
+ |-----------|---------|-------|
218
+ | `Button` | `iconName`, `iconColor` | Variant label |
219
+ | `IconButton` | `iconName`, `iconColor` | Variant foreground |
220
+ | `Input` | `prefixIcon`, `prefixIconColor`, `suffixIcon`, `suffixIconColor` | `foregroundMuted` |
221
+ | `ListItem` | `leftIcon`, `leftIconColor`, `rightIcon`, `rightIconColor` | `foreground` / `foregroundMuted` |
222
+ | `Badge` | `iconName`, `iconColor` | Variant foreground |
223
+ | `Toggle` | `iconName`, `iconColor`, `activeIconName`, `activeIconColor` | `foregroundMuted` / `primary` |
224
+ | `AlertBanner` | `iconName`, `iconColor` | Title color |
225
+ | `EmptyState` | `iconName`, `iconColor` | `foregroundMuted` |
226
+ | `MediaCard` | `actionIconName` | White |
227
+ | `AvatarGroup` | — | — (avatars use own colors) |
228
+ | `Chip` | `iconName` | Variant foreground |
229
+ | `DetailRow` | `leftIconName`, `leftIconColor`, `rightIconName`, `rightIconColor` | `foregroundMuted` |
230
+ | `MenuItem` | `iconName`, `iconColor` | `foreground` |
231
+
232
+ ### curatedIcons.ts
233
+ 8 categories, ~20 icons each, all Feather (outlined). Actions, comunicación, navegación, comida, negocios, perfil, multimedia, texto.
234
+
235
+ ### Rules
236
+ - Prefer Feather (outlined) over FA5 solid (filled). FA5 `defaultStyle='regular'` but some names only exist in `solid` — avoid those.
237
+ - Icon lookup uses lazy singleton cache from `glyphMap` introspection.
524
238
 
239
+ ### `getResponsiveFontSize` utility
525
240
  ```tsx
526
- // v5 hook-only API
527
- const { toast } = useToast()
528
- toast.success('Done')
529
-
530
- // v6 — direct import preferred (hook still works for compat)
531
- import { toast } from '@retray-dev/ui-kit'
532
- toast.success('Done')
241
+ import { getResponsiveFontSize } from '@retray-dev/ui-kit'
242
+ const fontSize = getResponsiveFontSize(text, 48)
243
+ // Steps: ≤10→max, ≤12→max-4, ≤14→max-6, >14→max-8
533
244
  ```
534
245
 
535
- ### New Theme Tokens
536
-
537
- Three optional `ThemeColors` tokens — fall back gracefully if omitted:
538
-
539
- | Token | Default | Purpose |
540
- |-------|---------|---------|
541
- | `overlay` | `rgba(0,0,0,0.45)` | Backdrop color behind sheets and dialogs |
542
- | `accent` | `= primary` | Secondary brand accent color |
543
- | `accentForeground` | `= primaryForeground` | Text on accent backgrounds |
544
-
545
- Access resolved values via `useTheme().colors.overlay`, `.accentResolved`, `.accentForegroundResolved`.
546
-
547
- ### Token Corrections
548
-
549
- - `TYPOGRAPHY['uppercase-tag']` — size corrected to `10` (was documented as `8`, actual value was always `10`)
550
-
551
246
  ---
552
247
 
553
- ## Migration Guide: v6 → v7
554
-
555
- ### Breaking Changes
556
-
557
- **No prop API changes** — all component imports and props are identical. This is a breaking version bump due to visual behavior changes that affect rendered output.
248
+ ## Animations & Interactions
558
249
 
559
- **Typography scale corrected:**
560
-
561
- | Token | v6 | v7 | Reason |
562
- |-------|----|----|--------|
563
- | `display-lg` | 22px / 500 | 24px / 600 | Removed weight inversion vs display-md |
564
- | `display-md` | 21px / 700 | 20px / 600 | 4px gap preserved; weight normalised |
565
- | `title-md` | 16px / 600 | 17px / 600 | Now visibly distinct from title-sm |
566
- | `title-sm` | 16px / 500 | 15px / 500 | Was same px as title-md |
567
- | `uppercase-tag` | 10px / 0.8 letterSpacing | 11px / 0.6 | Below Apple HIG 11pt minimum |
568
- | `badge-text-md` | (missing) | 13px / 600 | New canonical token for Badge md |
569
-
570
- **Default color values changed (WCAG AA fixes):**
571
-
572
- | Token | v6 | v7 | WCAG impact |
573
- |-------|----|----|-------------|
574
- | `foregroundMuted` opacity | 0.38 (~#ababab) | 0.62 (~#767676) | 2.2:1 ❌ → 4.5:1 ✓ |
575
- | `foregroundSubtle` opacity | 0.55 (~#858585) | 0.70 (~#646464) | 3.5:1 ❌ → 5.9:1 ✓ |
576
- | `warning` (light) | `#e67e00` | `#9a5200` | 2.86:1 ❌ → 5.86:1 ✓ |
577
- | `warningForeground` (dark) | `#ffffff` | `#0f0f0f` | Dark text on amber, 8.6:1 ✓ |
578
- | `destructive` (light) | `#e53935` | `#c72828` | 4.22:1 ❌ → 5.59:1 ✓ |
579
- | `accent` (light) | `= primary` | `#d4561d` | Explicit brand accent |
580
- | `accent` (dark) | `= primary` | `#e87645` | Warm accent for dark surfaces |
250
+ ### Pressables (pressto)
251
+ | Pressable | Scale | Used by |
252
+ |---|---|---|
253
+ | `PressableButton` | 0.95 | Button, IconButton, Toggle, Checkbox |
254
+ | `PressableCard` | 0.98 | Card, MediaCard, Stats, Pressable |
255
+ | `PressableRow` | 0.97 | ListItem, MenuItem |
256
+ | `PressableChip` | 0.94 | Chip |
257
+ | `PressableTab` | 0.95 | Tabs triggers |
581
258
 
582
- **Component visual behavior changes:**
259
+ All use `enabled={!disabled}`, `rippleColor="transparent"`, `touchSoundDisabled`.
583
260
 
584
- | Component | Change |
585
- |-----------|--------|
586
- | `AlertBanner` | Background now uses semantic tint per variant (was white card); 1px semantic border added |
587
- | `Button` (text variant) | Label color: `foreground` → `accentResolved` — clearer CTA signal |
588
- | `Card` (elevated variant) | `borderWidth: 0` — shadow is sole depth signal; border was redundant |
589
- | `Chip` | `paddingVertical` doubled to hit 44pt WCAG 2.5.5 tap target |
590
- | `Input` / `Textarea` | Border: 1px at rest → animates to 2px on focus (was always 2px) |
591
- | `Switch` | Off-state now shows animated 1.5px border (invisible on white surfaces before) |
592
- | `Tabs` | Labels always `Sohne-SemiBold`; active = color only — eliminates layout reflow on selection |
593
- | `Checkbox` / `RadioGroup` | Disabled `opacity: 0.45` now on full row (was box only) |
594
- | `Toast` | `richColors={true}` — semantic variants now visually distinct by color |
261
+ ### Spring Presets
262
+ - `glide`: `{ stiffness: 380, damping: 38, mass: 1.0 }` — sliding indicators
263
+ - `elastic`: `{ stiffness: 320, damping: 22, mass: 0.7 }` Switch thumb, RadioGroup dot
595
264
 
596
- **If you customised any of these values** via `ThemeProvider` overrides, your overrides continue to take precedence — default palette changes only affect apps using the out-of-the-box defaults.
265
+ ### Timing Presets
266
+ - `state`: 160ms (checkbox/toggle color)
267
+ - `expand` / `collapse`: 240ms / 200ms (Accordion)
268
+ - `shimmer`: 1400ms (Skeleton)
597
269
 
598
- ### New Compound Component Section
270
+ ### EaseView (react-native-ease)
271
+ Declarative color/opacity transitions: Switch track, Checkbox fill, RadioGroup dot. UI thread.
599
272
 
600
- The example app now has a dedicated **Compound Components** section showcasing `Card`, `ButtonGroup`, `Form`, `ListGroup`, and `MenuGroup` with all sub-components and variants.
273
+ ### Reanimated v4
274
+ Complex animations only (gestures, layout interpolations). Simple opacity: use RN `Animated` + `useNativeDriver`.
601
275
 
602
276
  ---
603
277
 
604
- ## Migration Guide: v7 → v8
605
-
606
- ### Breaking Changes
278
+ ## Sheets (BottomSheetModal)
607
279
 
608
- **1. New required peer dependency: `sonner-native`**
609
- Toast (`ToastProvider` / `toast` / `useToast`) imports `sonner-native`, but it was never declared as a peer — installs silently lacked it and Metro failed with `Unable to resolve "sonner-native"`. It is now a declared peer. Install it:
610
- ```bash
611
- pnpm add sonner-native
612
- ```
613
- (You already need its companions `react-native-svg` and `react-native-screens`.)
614
-
615
- **2. `@gorhom/bottom-sheet` peer range tightened to `>=5.2.0`**
616
- 5.1.x crashes with Reanimated v4 (`useWorkletCallback is not a function`), which broke `Sheet`, `ConfirmDialog`, and `Select` on open. The range no longer admits broken versions. Pin `5.2.8` if you also use Worklets `0.5.x`.
617
-
618
- **3. `./fonts` stays at `src/fonts.ts` (unchanged)**
619
- The `./fonts` export deliberately points at `src/fonts.ts`, not `dist`. The `.otf` files ship as raw assets in `src/assets/fonts/` and are resolved by Metro at your build time via `require()`. Compiling this entry to `dist` and repointing the `require()` paths breaks Metro asset resolution under pnpm symlinked workspaces (`requiring unknown module ../src/assets/fonts/...`), so it is intentionally left as source. No API change — import as before:
620
- ```ts
621
- import { SohneFonts } from '@retray-dev/ui-kit/fonts'
622
- ```
280
+ ### Strict rules (from @gorhom/bottom-sheet)
281
+ 1. **Always `BottomSheetModal`** never `BottomSheet` base. `present()` in handler directly (not `useEffect`).
282
+ 2. **`enableDynamicSizing`** — no `snapPoints` (mutually exclusive)
283
+ 3. **`topInset={insets.top}`** — prevents notch overlap
284
+ 4. **`keyboardBehavior="interactive"`** + **`android_keyboardInputMode="adjustPan"`** — keyboard
285
+ 5. **`SheetTextInput`** inside sheets never `<TextInput />`
286
+ 6. **`onDismiss`** for cleanup, not `onClose`
287
+ 7. **`renderBackdrop`** wrapped in `useCallback`
288
+ 8. **`enableBlurKeyboardOnGesture={true}`** dismiss keyboard on drag
289
+ 9. **`BottomSheetModalProvider`** re-exported, included in `RetrayProvider`
623
290
 
624
- ### New packaging & DX
291
+ ### Input inside Sheet
292
+ Use `<Input sheetMode />` — transparently swaps to `BottomSheetTextInput`. For low-level: `SheetTextInput`.
625
293
 
626
- - **`RetrayProvider`** — one wrapper replacing the five-provider boilerplate (see Setup below).
627
- - **Dev font guard** — if you render a UI kit component without loading `SohneFonts`, `Text` now logs a one-time `console.warn` in dev (silent in production) instead of failing invisibly.
628
-
629
- ### New components
294
+ ---
630
295
 
631
- `RetrayProvider`, `AppHeader`, `TabBar`, `PagerDots`, `SelectableGrid`, `PricingCard`, `ErrorBoundary`, `ImageViewer`, plus `Skeleton.MediaCard` / `Skeleton.ListItem` sub-skeletons and the optional deep-import-only `HolographicCard` (Skia foil card). `MonthPicker` gained `minValue`/`maxValue` and `Date` bridge helpers.
296
+ ## Haptics (expo-haptics)
632
297
 
633
- ### Optional peers (only if you use the matching component)
298
+ Web-safe via `Platform.OS !== 'web'` guard + dynamic `import()`.
634
299
 
635
- - `@shopify/react-native-skia` + `expo-sensors` → `HolographicCard` (deep-import only, not in the main barrel).
300
+ | Function | Usage |
301
+ |---|---|
302
+ | `selectionAsync()` | Checkbox, Switch, Toggle, RadioGroup, Select, Slider (step), Accordion, ListItem, MenuItem, ConfirmDialog cancel |
303
+ | `impactLight()` / `impactMedium()` / `impactHeavy()` | Button, Sheet open, ConfirmDialog open, Stats |
304
+ | `notificationSuccess()` / `notificationError()` / `notificationWarning()` | ConfirmDialog confirm, form validation |
636
305
 
637
306
  ---
638
307
 
639
- ## Migration Guide: v9 → v10
640
-
641
- ### Breaking Changes
642
-
643
- **1. `SohneFonts` export removed from `@retray-dev/ui-kit/fonts`**
644
-
645
- The `SohneFonts` object with `require()` calls is no longer exported. Metro cannot reliably resolve `require()` from inside `node_modules`, especially in monorepos. The export now returns `undefined` and logs a deprecation warning.
646
-
647
- **New approach — postinstall script:**
648
- 1. When you install `@retray-dev/ui-kit`, a **postinstall script** automatically copies 28 `.otf` font files to `assets/fonts/sohne/` in your project root
649
- 2. Define `SohneFonts` locally in your `App.tsx` with static `require()` calls (see below)
650
- 3. Pass it to `expo-font`'s `useFonts()` hook
651
-
652
- **Migration:**
653
-
654
- ```diff
655
- // App.tsx
656
- import { useFonts } from 'expo-font'
657
- -import { SohneFonts } from '@retray-dev/ui-kit/fonts'
658
-
659
- +// Fonts copied to assets/fonts/sohne/ by @retray-dev/ui-kit postinstall
660
- +const SohneFonts = {
661
- + 'Sohne-ExtraLight': require('./assets/fonts/sohne/Sohne-ExtraLight.otf'),
662
- + 'Sohne-ExtraLightItalic': require('./assets/fonts/sohne/Sohne-ExtraLightItalic.otf'),
663
- + 'Sohne-Light': require('./assets/fonts/sohne/Sohne-Light.otf'),
664
- + 'Sohne-LightItalic': require('./assets/fonts/sohne/Sohne-LightItalic.otf'),
665
- + 'Sohne-Regular': require('./assets/fonts/sohne/Sohne-Regular.otf'),
666
- + 'Sohne-Italic': require('./assets/fonts/sohne/Sohne-Italic.otf'),
667
- + 'Sohne-Medium': require('./assets/fonts/sohne/Sohne-Medium.otf'),
668
- + 'Sohne-MediumItalic': require('./assets/fonts/sohne/Sohne-MediumItalic.otf'),
669
- + 'Sohne-SemiBold': require('./assets/fonts/sohne/Sohne-SemiBold.otf'),
670
- + 'Sohne-SemiBoldItalic': require('./assets/fonts/sohne/Sohne-SemiBoldItalic.otf'),
671
- + 'Sohne-Bold': require('./assets/fonts/sohne/Sohne-Bold.otf'),
672
- + 'Sohne-BoldItalic': require('./assets/fonts/sohne/Sohne-BoldItalic.otf'),
673
- + 'Sohne-ExtraBold': require('./assets/fonts/sohne/Sohne-ExtraBold.otf'),
674
- + 'Sohne-ExtraBoldItalic': require('./assets/fonts/sohne/Sohne-ExtraBoldItalic.otf'),
675
- + 'SohneMono-ExtraLight': require('./assets/fonts/sohne/SohneMono-ExtraLight.otf'),
676
- + 'SohneMono-ExtraLightItalic': require('./assets/fonts/sohne/SohneMono-ExtraLightItalic.otf'),
677
- + 'SohneMono-Light': require('./assets/fonts/sohne/SohneMono-Light.otf'),
678
- + 'SohneMono-LightItalic': require('./assets/fonts/sohne/SohneMono-LightItalic.otf'),
679
- + 'SohneMono-Regular': require('./assets/fonts/sohne/SohneMono-Regular.otf'),
680
- + 'SohneMono-Italic': require('./assets/fonts/sohne/SohneMono-Italic.otf'),
681
- + 'SohneMono-Medium': require('./assets/fonts/sohne/SohneMono-Medium.otf'),
682
- + 'SohneMono-MediumItalic': require('./assets/fonts/sohne/SohneMono-MediumItalic.otf'),
683
- + 'SohneMono-SemiBold': require('./assets/fonts/sohne/SohneMono-SemiBold.otf'),
684
- + 'SohneMono-SemiBoldItalic': require('./assets/fonts/sohne/SohneMono-SemiBoldItalic.otf'),
685
- + 'SohneMono-Bold': require('./assets/fonts/sohne/SohneMono-Bold.otf'),
686
- + 'SohneMono-BoldItalic': require('./assets/fonts/sohne/SohneMono-BoldItalic.otf'),
687
- + 'SohneMono-ExtraBold': require('./assets/fonts/sohne/SohneMono-ExtraBold.otf'),
688
- + 'SohneMono-ExtraBoldItalic': require('./assets/fonts/sohne/SohneMono-ExtraBoldItalic.otf'),
689
- +}
308
+ ## Theming
690
309
 
691
- export default function App() {
692
- const [fontsLoaded] = useFonts(SohneFonts)
693
- // ...
310
+ ### Consumer supplies 12 ThemeColors
311
+ ```tsx
312
+ import { ThemeProvider, ThemeColors } from '@retray-dev/ui-kit'
313
+
314
+ const customLight: ThemeColors = {
315
+ background: '#ffffff',
316
+ foreground: '#1a1a1a',
317
+ primary: '#d4561d',
694
318
  }
695
319
  ```
696
320
 
697
- **.gitignore recommendation:**
698
- ```gitignore
699
- # Sohne fonts — copied by @retray-dev/ui-kit postinstall
700
- assets/fonts/sohne/
701
- ```
321
+ ### deriveColors()
322
+ Computes 26 `ResolvedColors` from 12 `ThemeColors`. Components consume `useTheme().colors` — never raw `ThemeColors`.
702
323
 
703
- **Why this change:** Metro bundler requires `require()` calls to originate from the consumer's source tree, not from `node_modules`. The previous approach worked in simple setups but failed in monorepos and symlinked workspaces.
324
+ ### Structural tokens (not in theme)
325
+ Spacing, radii, shadows, typography = static `as const` exports from `tokens.ts`. No provider needed.
704
326
 
705
327
  ---
706
328
 
707
- ## Migration Guide: v10 → v11
708
-
709
- ### New — public utility
710
-
711
- **`withAlpha(hex, alpha)`** — hex-to-rgba color helper, now exported from the package root. Useful for semi-transparent overlays derived from theme colors without adding a new token.
329
+ ## Toast (sonner-native)
712
330
 
713
331
  ```tsx
714
- import { useTheme, withAlpha } from '@retray-dev/ui-kit'
332
+ import { toast } from 'sonner-native'
333
+ // or
334
+ import { useToast } from '@retray-dev/ui-kit'
335
+ const { toast } = useToast()
715
336
 
716
- const { colors } = useTheme()
717
- <View style={{ backgroundColor: withAlpha(colors.primary, 0.15) }} />
337
+ toast('Hello', { description: 'World' })
338
+ toast.success('Saved')
339
+ toast.error('Failed', { description: 'Check network' })
340
+ toast.promise(save(), { loading: 'Saving...', success: 'Done', error: 'Failed' })
718
341
  ```
719
342
 
720
- ### Updated
721
-
722
- - `Stats` component promoted from internal/experimental to public (`Stats` + `Stats.Group`).
723
- - Documentation refreshed for `IconPicker` and `NumberStepper`.
724
-
725
- No breaking changes in v11. Safe minor upgrade from v10.
343
+ `Toaster` rendered inside `ToastProvider`. Config: `richColors: false`, `swipeToDismissDirection="up"`, `duration: 4000`.
726
344
 
727
345
  ---
728
346
 
729
- ## Migration Guide: v11 → v12
730
-
731
- ### Breaking Changes
732
-
733
- `Sheet` and `ConfirmDialog` were rewritten on top of `@gorhom/bottom-sheet`'s **`BottomSheetModal`** (lazy-mounted, `present()` / `dismiss()` driven) — replacing the old `BottomSheet` + `index={-1}` + `snapToIndex(0)` pattern. This fixes timing issues with `enableDynamicSizing` and unifies the API with the rest of the gorhom ecosystem.
347
+ ## Data Display Patterns
734
348
 
735
- **1. Removed `responsive` / `dialogMaxWidth` props from `Sheet` and `ConfirmDialog`**
736
-
737
- The previous wide-screen fallback that bypassed gorhom with a native `Modal` + `ScrollView` was a partial workaround (see REGLA 1). It has been deleted. `@gorhom/bottom-sheet` handles responsive behavior natively — the modal simply renders inside its modal layer at the device width.
738
-
739
- ```diff
740
- <Sheet
741
- open={open}
742
- onClose={() => setOpen(false)}
743
- title="Options"
744
- - responsive
745
- - dialogMaxWidth={600}
746
- />
747
- ```
748
-
749
- **2. `onClose` is the only close handler**
750
-
751
- `onClose` is called from `BottomSheetModal.onDismiss` — the native gorhom callback. The previous `onClose` prop (passed directly to `BottomSheet`) was redundant. No public-API change for consumers; this is an internal alignment.
349
+ ### ListItem
350
+ - `rightActions` prop for swipe-to-reveal (iOS Mail style)
351
+ - Uses `PressableRow` (scale 0.97)
352
+ - Haptic: `selectionAsync()` on press
353
+ - Zero horizontal padding by design
752
354
 
753
- **3. `Sheet` requires `BottomSheetModalProvider` at app root**
355
+ ### ListGroup
356
+ - Optional `Header` / `Footer` sub-components
357
+ - Auto-separators between items except last
754
358
 
755
- `RetrayProvider` already wires it. If you assemble providers manually, ensure `BottomSheetModalProvider` sits inside `GestureHandlerRootView`:
359
+ ### MenuItem / MenuGroup
360
+ - Settings/nav rows. Auto-separators.
361
+ - `rightRender` for Switch, Badge, etc.
362
+ - `variant="card"` for standalone surface
756
363
 
757
- ```tsx
758
- <SafeAreaProvider initialMetrics={initialWindowMetrics}>
759
- <GestureHandlerRootView style={{ flex: 1 }}>
760
- <ThemeProvider>
761
- <BottomSheetModalProvider>
762
- <ToastProvider>{/* app */}</ToastProvider>
763
- </BottomSheetModalProvider>
764
- </ThemeProvider>
765
- </GestureHandlerRootView>
766
- </SafeAreaProvider>
767
- ```
364
+ ### Chip / ChipGroup
365
+ - Multi-select toggle chips. `PressableChip` (scale 0.94)
366
+ - `ChipGroup` wraps to rows; `CategoryStrip` scrolls horizontally
768
367
 
769
- **4. Keyboard prop renames (no consumer action — defaults match v11)**
368
+ ### LabelValue
369
+ - Key-value rows (label left, value right)
370
+ - Icon support
770
371
 
771
- | Prop | v11 | v12 default |
772
- |------|-----|-------------|
773
- | `keyboardBehavior` | `'interactive'` | `'interactive'` (unchanged) |
774
- | `android_keyboardInputMode` | `'adjustPan'` | `'adjustPan'` (unchanged) |
372
+ ### DetailRow
373
+ - Ticket/receipt rows with `···` dotted separator
374
+ - Icon + label + value
775
375
 
776
- **5. Removed top-level imports of `Modal`, `ScrollView`, `useWindowDimensions`, `BREAKPOINTS` from `Sheet.tsx`**
376
+ ### Skeleton
377
+ - `Skeleton.MediaCard`, `Skeleton.ListItem`, `Skeleton.List` (6 items, 2 columns)
378
+ - Shimmer animation loops every 1200ms
777
379
 
778
- Internal only — no public API.
380
+ ---
779
381
 
780
- ### Behavioral changes (no code change required)
382
+ ## Accessibility (WCAG AA)
781
383
 
782
- - **Backdrop / swipe close is now driven by gorhom's modal lifecycle** — `onClose` fires once on full dismiss instead of on every interaction. State-setter closures (e.g. `setOpen(false)`) are safe to call from `onClose` without causing "dismiss on unmounted" loops.
783
- - **`topInset={insets.top}`** is now applied automatically from safe-area context — sheet never crosses the notch / status bar on iOS or Android.
784
- - **`snapPoints` + `enableDynamicSizing` are mutually exclusive.** If you pass `snapPoints`, dynamic sizing is disabled and the sheet snaps to those points. Omit `snapPoints` to use dynamic sizing (default).
384
+ - `allowFontScaling={true}` on all `<Text>` and `<TextInput>`
385
+ - Touch targets ≥44pt
386
+ - `foregroundMuted` = `#9a9a9a` (dark) / `#a2a2a2` (light) ≥4.5:1 contrast
387
+ - Accessibility props passed through via `...props`
785
388
 
786
- ### New — recommended pattern
389
+ ---
787
390
 
788
- **Use `Input` with `sheetMode` inside a Sheet** instead of `SheetTextInput` directly. The new `sheetMode` prop on `Input` swaps in `BottomSheetTextInput` for keyboard-aware focus/blur handling while preserving the full `Input` API (label, error, hint, prefix/suffix, icons, type="password").
391
+ ## Deep Import Pattern
789
392
 
393
+ Prefer deep imports in production for smaller bundle:
790
394
  ```tsx
791
- <Sheet open={open} onClose={() => setOpen(false)} title="Add note">
792
- <Input
793
- label="Note"
794
- placeholder="Type your note..."
795
- value={note}
796
- onChangeText={setNote}
797
- sheetMode
798
- />
799
- <Button label="Save" fullWidth onPress={handleSave} />
800
- </Sheet>
395
+ import { Button } from '@retray-dev/ui-kit/Button' // deep
396
+ // vs
397
+ import { Button } from '@retray-dev/ui-kit' // barrel
801
398
  ```
802
399
 
803
- `SheetTextInput` is still re-exported for low-level use.
400
+ Deep-import only (NOT in barrel):
401
+ ```tsx
402
+ import { HolographicCard } from '@retray-dev/ui-kit/HolographicCard'
403
+ ```
804
404
 
805
405
  ---
806
406
 
807
- ## Migration Guide: v12.0 → v12.1
808
-
809
- ### New — IconPicker feedback pattern (REGLA 4)
407
+ ## Anti-Patterns
810
408
 
811
- `IconPicker` now follows the "no frozen screen" rule. When the user taps the trigger, the sheet presents **immediately** (no `useEffect` delay). While the inner grid measures its container, a centered `<Spinner />` is shown inside the sheet so the user sees visible feedback. The grid fades in as soon as `onLayout` fires.
812
-
813
- This is now the canonical pattern for any sheet whose content needs to measure or load before rendering — apply it to new overlay components.
814
-
815
- ### New race-condition fix in `Sheet` and `ConfirmDialog`
816
-
817
- Both components now track a `wasOpened` ref so `dismiss()` is only called after the sheet has been presented at least once. This eliminates a class of crashes that occurred when the parent component unmounted (or the `open`/`visible` prop flipped) before the gorham modal had a chance to mount.
818
-
819
- No consumer action required — the fix is internal.
409
+ | Anti-pattern | Why | Fix |
410
+ |---|---|---|
411
+ | `<TextInput />` inside Sheet | Keyboard broken | Use `<Input sheetMode />` or `<SheetTextInput />` |
412
+ | `BottomSheet` base with `snapToIndex` | Fragile timing | Use `BottomSheetModal` with `present()` |
413
+ | `snapPoints` + `enableDynamicSizing` | Runtime error | Use one or the other |
414
+ | Static import of optional peer | Crash at module init | Dynamic `import()` in handler |
415
+ | Bundling native modules | Duplicate Context | Externalize in tsup |
416
+ | `--legacy-peer-deps` | Hides conflicts | Use `overrides` in package.json |
820
417
 
821
- ### New — expanded curated icon library
418
+ ---
822
419
 
823
- `src/utils/curatedIcons.ts` has been expanded: every category now carries 42+ icons (some up to 66), totaling ~600 themed icons across the 12 categories. Selection rules are now codified in REGLA 5 — only Feather, Ionicons `-outline`, and themed FA5/Entypo/AntDesign icons are eligible (no filled variants).
420
+ ## Version
824
421
 
825
- ### Updated
422
+ **Current: 13.1.0.** Requires Expo SDK 54+, React Native 0.81+, React 19.
826
423
 
827
- - `Sheet` and `ConfirmDialog` pass a stable `name` prop to `BottomSheetModal` (from `useId()`) for correct gorhom modal registry behavior when multiple modals are mounted.
828
- - `Input` `sheetMode` prop documented in the Setup section (replaces the `SheetTextInput` boilerplate inside sheets).
829
- - `CompositionScreen` example now uses `<Input sheetMode />` inside a `Sheet` to demonstrate the keyboard-friendly pattern.
424
+ For full setup guide, see `CONSUMER.md`. For usage examples, see `EXAMPLES.md`.
830
425
 
831
426
  ---
832
427
 
833
- ## Components
428
+ ## Component Reference
834
429
 
835
430
  ---
836
431
 
@@ -857,18 +452,19 @@ No consumer action required — the fix is internal.
857
452
  |---------|------|--------|--------------|-------------|
858
453
  | `display-hero` | 64 | 700 | `foreground` | Large numeric displays — balance totals, stats, hero numbers |
859
454
  | `display-xl` | 28 | 700 | `foreground` | Screen-level titles, onboarding hero headings |
860
- | `display-lg` | 22 | 500 | `foreground` | Section headings, card group labels |
861
- | `display-md` | 21 | 700 | `foreground` | Card titles, dialog headings |
862
- | `display-sm` | 20 | 600 | `foreground` | Sub-section titles |
863
- | `title-md` | 16 | 600 | `foreground` | List row titles, navigation labels |
864
- | `title-sm` | 16 | 500 | `foreground` | Secondary row titles |
455
+ | `display-lg` | 24 | 600 | `foreground` | Section headings, card group labels |
456
+ | `display-md` | 20 | 600 | `foreground` | Card titles, dialog headings |
457
+ | `display-sm` | 18 | 600 | `foreground` | Sub-section titles |
458
+ | `title-md` | 17 | 600 | `foreground` | List row titles, navigation labels |
459
+ | `title-sm` | 15 | 500 | `foreground` | Secondary row titles |
865
460
  | `body-md` | 16 | 400 | `foregroundSubtle` | Main body copy, descriptions |
866
461
  | `body-sm` | 14 | 400 | `foregroundSubtle` | Secondary descriptions, detail text |
867
462
  | `caption` | 14 | 500 | `foreground` | Labels above inputs, item captions |
868
463
  | `caption-sm` | 13 | 400 | `foregroundMuted` | Timestamps, metadata, helper text |
869
464
  | `badge-text` | 11 | 600 | `foreground` | Badge labels, small status tags |
465
+ | `badge-text-md` | 13 | 600 | `foreground` | Badge labels (md size) |
870
466
  | `micro-label` | 12 | 700 | `foreground` | Micro labels, overlines |
871
- | `uppercase-tag` | 10 | 700 | `foreground` | Decorative uppercase category labels (auto-transforms text) |
467
+ | `uppercase-tag` | 11 | 700 | `foreground` | Decorative uppercase category labels (auto-transforms text) |
872
468
  | `button-lg` | 16 | 500 | `foreground` | Internal use in Button component (md/lg) |
873
469
  | `button-sm` | 14 | 500 | `foreground` | Internal use in Button component (sm) |
874
470
 
@@ -1561,6 +1157,65 @@ const [guests, setGuests] = useState(2)
1561
1157
 
1562
1158
  ---
1563
1159
 
1160
+ ### AvatarGroup
1161
+
1162
+ **Import:** `import { AvatarGroup } from '@retray-dev/ui-kit'`
1163
+
1164
+ **When to use:** Stacked/overlapping avatars to show multiple users in a compact space. Common for collaborator indicators, multi-waiter tables, and team members.
1165
+
1166
+ | Prop | Type | Default | Notes |
1167
+ |------|------|---------|-------|
1168
+ | users | `{ name: string; src?: string }[]` | required | User data — name used for fallback initials |
1169
+ | max | `number` | `3` | Max avatars visible before `+N` overflow badge |
1170
+ | size | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'sm'` | Avatar size preset, passed to each `Avatar` |
1171
+ | overlap | `number` | `vs(8)` | Overlap in points between consecutive avatars |
1172
+ | onOverflowPress | `() => void` | — | Tap handler for the `+N` overflow badge |
1173
+ | style | `ViewStyle` | — | Outer container style |
1174
+
1175
+ **Sizes:** Same as `Avatar` — `sm` (28px), `md` (40px), `lg` (56px), `xl` (72px).
1176
+
1177
+ **Notes:**
1178
+ - Overflow badge uses `surfaceStrong` background with `foregroundMuted` text.
1179
+ - Individual avatars are not tappable — only the `+N` badge fires `onOverflowPress`.
1180
+ - `overlap` follows the spacing grid (defaults to `spacing.sm` = 8pt).
1181
+
1182
+ **Examples:**
1183
+ ```tsx
1184
+ // Basic — 5 waiters, show max 3
1185
+ <AvatarGroup
1186
+ users={[
1187
+ { name: 'Ana', src: 'https://...' },
1188
+ { name: 'Bob', src: 'https://...' },
1189
+ { name: 'Carlos' },
1190
+ { name: 'Diana', src: 'https://...' },
1191
+ { name: 'Elena' },
1192
+ ]}
1193
+ max={3}
1194
+ />
1195
+
1196
+ // With overflow tap
1197
+ <AvatarGroup
1198
+ users={waiters}
1199
+ max={3}
1200
+ size="sm"
1201
+ onOverflowPress={() => showAllWaiters()}
1202
+ />
1203
+
1204
+ // In a ListItem
1205
+ <ListItem
1206
+ title="Table 4 — 5 waiters"
1207
+ leftRender={
1208
+ <AvatarGroup
1209
+ users={table.waiters}
1210
+ max={3}
1211
+ size="sm"
1212
+ />
1213
+ }
1214
+ />
1215
+ ```
1216
+
1217
+ ---
1218
+
1564
1219
  ### Card
1565
1220
 
1566
1221
  **Import:** `import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@retray-dev/ui-kit'`
@@ -1765,73 +1420,7 @@ const [guests, setGuests] = useState(2)
1765
1420
 
1766
1421
  ---
1767
1422
 
1768
- ### VirtualList
1769
-
1770
- **Import:** `import { VirtualList } from '@retray-dev/ui-kit'`
1771
-
1772
- **When to use:** Large lists (100+ items) where you need efficient rendering. Thin wrapper over `FlatList` with sane defaults for stable keys and optional fixed-height fast path.
1773
-
1774
- | Prop | Type | Default | Notes |
1775
- |------|------|---------|-------|
1776
- | data | `T[]` | — | Array of items to render |
1777
- | renderItem | `ListRenderItem<T>` | — | Render function for each item |
1778
- | itemHeight | `number` | — | Fixed row height in px. Enables `getItemLayout` for performance with large datasets |
1779
- | keyExtractor | `(item: T, index: number) => string` | Auto | Defaults to `item.id` or index. Override for custom keys |
1780
- | ...FlatListProps | — | — | All standard FlatList props pass through |
1781
-
1782
- **Performance optimization:** When `itemHeight` is provided, `VirtualList` enables `getItemLayout` so FlatList skips async measurement. For 10k+ rows, combine with `React.memo`-wrapped `renderItem` so only on-screen rows mount and re-render.
1783
-
1784
- **Default key extraction:** Uses `item.id` (converted to string) or falls back to index. Override with `keyExtractor` for custom logic.
1785
-
1786
- **Example — simple list:**
1787
- ```tsx
1788
- <VirtualList
1789
- data={items}
1790
- renderItem={({ item }) => (
1791
- <ListItem
1792
- title={item.title}
1793
- subtitle={item.subtitle}
1794
- onPress={() => handlePress(item.id)}
1795
- />
1796
- )}
1797
- itemHeight={56}
1798
- />
1799
- ```
1800
-
1801
- **Example — large dataset with memoized render:**
1802
- ```tsx
1803
- const renderItem = useCallback(({ item }) => (
1804
- <ListItem
1805
- title={item.title}
1806
- subtitle={item.subtitle}
1807
- leftIcon="circle"
1808
- showSeparator
1809
- onPress={() => navigate('detail', { id: item.id })}
1810
- />
1811
- ), [])
1812
-
1813
- <VirtualList
1814
- data={thousands}
1815
- renderItem={renderItem}
1816
- itemHeight={64}
1817
- />
1818
- ```
1819
-
1820
- **Example — variable height rows (omit itemHeight):**
1821
- ```tsx
1822
- <VirtualList
1823
- data={posts}
1824
- renderItem={({ item }) => (
1825
- <Card style={{ margin: SPACING.sm }}>
1826
- <CardContent>
1827
- <Text>{item.content}</Text>
1828
- </CardContent>
1829
- </Card>
1830
- )}
1831
- />
1832
- ```
1833
1423
 
1834
- ---
1835
1424
 
1836
1425
  ### Separator
1837
1426
 
@@ -1946,7 +1535,7 @@ const renderItem = useCallback(({ item }) => (
1946
1535
  // Matches <ListItem> — leading circle + title/subtitle lines
1947
1536
  <Skeleton.ListItem /> // props: showAvatar, showSubtitle, style
1948
1537
 
1949
- // Repeated list/grid placeholder — for VirtualList / FlatList while loading.
1538
+ // Repeated list/grid placeholder — for FlatList while loading.
1950
1539
  // columns=1 → stacked ListItemSkeleton; columns>1 → grid of MediaCardSkeleton.
1951
1540
  <Skeleton.List count={6} /> // stacked list
1952
1541
  <Skeleton.List count={6} columns={2} /> // 2-col card grid
@@ -1954,8 +1543,8 @@ const renderItem = useCallback(({ item }) => (
1954
1543
 
1955
1544
  // Also exported as named components: MediaCardSkeleton, ListItemSkeleton, ListSkeleton
1956
1545
 
1957
- // As a VirtualList loading state
1958
- <VirtualList
1546
+ // As a FlatList loading state
1547
+ <FlatList
1959
1548
  data={loading ? [] : rows}
1960
1549
  renderItem={renderItem}
1961
1550
  ListEmptyComponent={loading ? <Skeleton.List count={8} /> : <EmptyState .../>}
@@ -3854,7 +3443,8 @@ export default function Screen() {
3854
3443
  | subtitle | `string` | — | Secondary line under the title |
3855
3444
  | onBack | `() => void` | — | Shows a back button on the left when provided |
3856
3445
  | backIconName | `string` | `'chevron-left'` | Icon for the back button |
3857
- | left | `ReactNode` | — | Custom left content overrides the back button |
3446
+ | iconName | `string` | — | Decorative icon left of title, after back button. Ignored when `left` is provided |
3447
+ | left | `ReactNode` | — | Custom left content — overrides the back button and `iconName` |
3858
3448
  | right | `ReactNode` | — | Custom right content (actions) |
3859
3449
  | titleAlign | `'auto' \| 'left' \| 'center'` | `'auto'` | `auto` = left on narrow, centered on wide |
3860
3450
  | bordered | `boolean` | `true` | Hairline border underneath |
@@ -3869,6 +3459,13 @@ export default function Screen() {
3869
3459
  onBack={navigation.goBack}
3870
3460
  right={<IconButton iconName="more-horizontal" variant="text" onPress={openMenu} />}
3871
3461
  />
3462
+
3463
+ {/* With decorative icon and back button */}
3464
+ <AppHeader
3465
+ title="Perfil"
3466
+ iconName="user"
3467
+ onBack={() => router.back()}
3468
+ />
3872
3469
  ```
3873
3470
 
3874
3471
  ---
@@ -4316,7 +3913,7 @@ import { HolographicCard, FOIL_PRESETS } from '@retray-dev/ui-kit/HolographicCar
4316
3913
 
4317
3914
  **When to use:** Image selection from the device library — product photos, profile avatars, tenant logos. Shows a dashed placeholder when empty, the selected image when filled, and a loading overlay while uploading.
4318
3915
 
4319
- **Requires:** `expo-image-picker` installed in the consuming app (`pnpm add expo-image-picker`). It is an optional peer dep — consumers who don't use `ImageUpload` never pull it.
3916
+ **Requires:** `react-native-image-picker` installed in the consuming app (`pnpm add react-native-image-picker`). It is an optional peer dep — consumers who don't use `ImageUpload` never pull it. It is dynamically imported at tap time.
4320
3917
 
4321
3918
  | Prop | Type | Default | Notes |
4322
3919
  |------|------|---------|-------|
@@ -4330,9 +3927,9 @@ import { HolographicCard, FOIL_PRESETS } from '@retray-dev/ui-kit/HolographicCar
4330
3927
  | borderRadius | `number` | `RADIUS.lg` | — |
4331
3928
  | resizeMode | `'cover' \| 'contain' \| 'stretch'` | `'cover'` | Image resize mode |
4332
3929
  | disabled | `boolean` | `false` | Prevents pressing |
4333
- | allowsEditing | `boolean` | `true` | When `true`, iOS opens the crop/editing screen after selecting an image. Set `false` to accept the image directly without cropping |
4334
3930
  | style | `ViewStyle` | — | — |
4335
3931
  | accessibilityLabel | `string` | — | — |
3932
+ | onPickerStarting | `() => void` | — | Called synchronously when user taps the upload area, before dynamic import and permission request |
4336
3933
 
4337
3934
  **Examples:**
4338
3935
  ```tsx
@@ -4425,10 +4022,12 @@ const [icon, setIcon] = useState<string | null>(null)
4425
4022
 
4426
4023
  **Import:** `import { useConfirmDialog } from '@retray-dev/ui-kit'`
4427
4024
 
4428
- **When to use:** Manage `ConfirmDialog` state without boilerplate. Replaces the `[target, setTarget]` + `visible` + `loading` state pattern that each screen would otherwise duplicate.
4025
+ **When to use:** Manage `ConfirmDialog` state without boilerplate. Replaces the `visible` + `loading` state pattern that each screen would otherwise duplicate.
4026
+
4027
+ **Note (v13):** The generic `T`, `target`, and `dialogProps` convenience object were removed. Returns `onConfirm`/`onCancel` directly. Uses refs for callback stability and `mountedRef` for safe async cleanup.
4429
4028
 
4430
4029
  ```ts
4431
- function useConfirmDialog<T>(options: UseConfirmDialogOptions): UseConfirmDialogResult<T>
4030
+ function useConfirmDialog(options: UseConfirmDialogOptions): UseConfirmDialogResult
4432
4031
 
4433
4032
  interface UseConfirmDialogOptions {
4434
4033
  onConfirm: () => void | Promise<void>
@@ -4441,253 +4040,32 @@ interface UseConfirmDialogOptions {
4441
4040
  | Field | Type | Notes |
4442
4041
  |-------|------|-------|
4443
4042
  | visible | `boolean` | Pass to `ConfirmDialog.visible` |
4444
- | target | `T \| null` | The value passed to `open()` — e.g. the item being deleted |
4445
4043
  | loading | `boolean` | Pass to `ConfirmDialog.loading` |
4446
- | open | `(target?: T) => void` | Call to show the dialog |
4447
- | dialogProps | `object` | Spread directly onto `<ConfirmDialog>` (contains `visible`, `loading`, `onConfirm`, `onCancel`) |
4044
+ | open | `() => void` | Call to show the dialog |
4045
+ | onConfirm | `() => void` | Pass directly to `ConfirmDialog.onConfirm` |
4046
+ | onCancel | `() => void` | Pass directly to `ConfirmDialog.onCancel` |
4448
4047
 
4449
4048
  **Example:**
4450
4049
  ```tsx
4451
- const { open, target, dialogProps } = useConfirmDialog<Product>({
4050
+ const { visible, loading, open, onConfirm, onCancel } = useConfirmDialog({
4452
4051
  onConfirm: async () => {
4453
- await deleteProduct(target!.id)
4052
+ await deleteProduct(productId)
4454
4053
  toast.success('Product deleted')
4455
4054
  },
4456
4055
  })
4457
4056
 
4458
- // Somewhere in your list:
4459
- <ListItem
4460
- title={product.name}
4461
- rightIcon="trash-2"
4462
- onPress={() => open(product)}
4463
- />
4464
-
4465
- // Once in your screen's JSX:
4057
+ // Somewhere in your screen:
4466
4058
  <ConfirmDialog
4467
- title={`Delete "${target?.name}"?`}
4468
- description="This action cannot be undone."
4059
+ visible={visible}
4060
+ title="Delete product?"
4061
+ subtitle="This action cannot be undone."
4469
4062
  confirmLabel="Delete"
4470
4063
  confirmVariant="destructive"
4471
- {...dialogProps}
4064
+ loading={loading}
4065
+ onConfirm={onConfirm}
4066
+ onCancel={onCancel}
4472
4067
  />
4473
4068
  ```
4474
4069
 
4475
4070
  ---
4476
4071
 
4477
- ## Icon System
4478
-
4479
- The library ships a built-in icon resolver — pass any icon name string to components without manual imports.
4480
-
4481
- ### Supported families (priority order — first match wins)
4482
-
4483
- | Priority | Family | Best for |
4484
- |---|---|---|
4485
- | 1 (highest) | `Feather` | Clean line icons, UI essentials |
4486
- | 2 | `AntDesign` | Semantic UI icons |
4487
- | 3 | `Entypo` | Social, media, navigation icons |
4488
- | 4 | `FontAwesome5` | Wide coverage |
4489
- | 5 | `MaterialIcons` | Material-style icons |
4490
- | 6 (lowest) | `Ionicons` | Fallback |
4491
-
4492
- Browse all available icons: **https://icons.expo.fyi**
4493
-
4494
- ### Standalone `Icon` component
4495
-
4496
- ```tsx
4497
- import { Icon } from '@retray-dev/ui-kit'
4498
-
4499
- <Icon name="home" size={24} color={colors.foreground} />
4500
- <Icon name="star" size={20} color={colors.primary} />
4501
-
4502
- // Force a specific family when same name exists in multiple families:
4503
- <Icon name="heart" size={24} color="red" family="FontAwesome5" />
4504
- ```
4505
-
4506
- **Props:**
4507
-
4508
- | Prop | Type | Required | Notes |
4509
- |------|------|----------|-------|
4510
- | name | `string` | yes | Icon name (e.g. `"home"`, `"arrow-right"`) |
4511
- | size | `number` | yes | Icon size in points |
4512
- | color | `string` | yes | Icon color |
4513
- | family | `IconFamily` | no | Force a specific family |
4514
-
4515
- Returns `null` (no crash) if name not found in any family.
4516
-
4517
- ### `renderIcon` helper
4518
-
4519
- ```tsx
4520
- import { renderIcon } from '@retray-dev/ui-kit'
4521
-
4522
- // Returns ReactNode or null
4523
- const icon = renderIcon('check', 18, colors.primary)
4524
- ```
4525
-
4526
- ### `getValidIconNames` utility
4527
-
4528
- ```tsx
4529
- import { getValidIconNames } from '@retray-dev/ui-kit'
4530
-
4531
- // All icon names across all configured families
4532
- const allIcons: string[] = getValidIconNames()
4533
- // => ["home", "user", "heart", "star", ...]
4534
-
4535
- // Scoped to specific families (does not mutate global config)
4536
- const featherOnly: string[] = getValidIconNames(['Feather'])
4537
- ```
4538
-
4539
- Returns `string[]` — all resolvable icon names from the configured families. Respects `configureIconFamilies()` global config. The optional `families` parameter builds a temporary cache scoped to those families without mutating global state. Use to build icon pickers, search, and galleries.
4540
-
4541
- ### `iconName` props on components
4542
-
4543
- All components with icon slots accept `iconName` — auto-resolved size and color:
4544
-
4545
- | Component | Prop(s) | Slot | Default color |
4546
- |-----------|---------|------|---------------|
4547
- | `Button` | `iconName`, `iconColor` | Left or right of label | Variant label color |
4548
- | `IconButton` | `iconName`, `iconColor` | Center | Variant foreground |
4549
- | `Input` | `prefixIcon`, `prefixIconColor` | Before input | `foregroundMuted` |
4550
- | `Input` | `suffixIcon`, `suffixIconColor` | After input | `foregroundMuted` |
4551
- | `ListItem` | `leftIcon`, `leftIconColor` | Left 44×44 slot | `foreground` |
4552
- | `ListItem` | `rightIcon`, `rightIconColor` | Right slot | `foregroundMuted` |
4553
- | `Badge` | `iconName`, `iconColor` | Before label | Variant foreground |
4554
- | `Toggle` | `iconName`, `iconColor` | When not pressed | `foregroundMuted` |
4555
- | `Toggle` | `activeIconName`, `activeIconColor` | When pressed | `primary` |
4556
- | `AlertBanner` | `iconName`, `iconColor` | Left of content | Variant title color |
4557
- | `EmptyState` | `iconName`, `iconColor` | Center icon slot | `foregroundMuted` |
4558
- | `Toast` | `iconName`, `iconColor` | Left of message | Variant text color |
4559
- | `MediaCard` | `actionIconName` | Top-right of image | `#ffffff` |
4560
- | `Chip` | `iconName` | Before label | Variant foreground |
4561
-
4562
- **Precedence:** `iconName` always takes precedence over the corresponding `ReactNode` prop when both are supplied.
4563
-
4564
- ---
4565
-
4566
- ### `getResponsiveFontSize` utility
4567
-
4568
- Calculates a stepped-down font size for long numeric strings (currency amounts, large numbers) that would overflow at full size.
4569
-
4570
- ```tsx
4571
- import { getResponsiveFontSize } from '@retray-dev/ui-kit'
4572
-
4573
- // Default steps: ≤10 chars → max, ≤12 → max-4, ≤14 → max-6, >14 → max-8
4574
- const fontSize = getResponsiveFontSize(formatCOP(amount), 48)
4575
-
4576
- <Text style={{ fontSize }} numberOfLines={1} adjustsFontSizeToFit>
4577
- {formatCOP(amount)}
4578
- </Text>
4579
- ```
4580
-
4581
- **Signature:**
4582
- ```ts
4583
- getResponsiveFontSize(
4584
- text: string,
4585
- maxSize: number,
4586
- steps?: { maxLen: number; subtract: number }[]
4587
- ): number
4588
- ```
4589
-
4590
- Custom steps example:
4591
- ```tsx
4592
- getResponsiveFontSize(text, 48, [
4593
- { maxLen: 8, subtract: 0 },
4594
- { maxLen: 11, subtract: 6 },
4595
- { maxLen: 14, subtract: 10 },
4596
- ])
4597
- ```
4598
-
4599
- **Alternative:** Use `<CurrencyDisplay autoScale maxFontSize={48} />` for currency values — no manual sizing needed.
4600
-
4601
- ---
4602
-
4603
- ## Hover Support (Web)
4604
-
4605
- ```tsx
4606
- import { useHover } from '@retray-dev/ui-kit'
4607
-
4608
- function MyHoverableComponent() {
4609
- const { hovered, hoverHandlers } = useHover()
4610
-
4611
- return (
4612
- <View
4613
- {...hoverHandlers}
4614
- style={[
4615
- styles.container,
4616
- hovered && { backgroundColor: colors.surfaceStrong }
4617
- ]}
4618
- />
4619
- )
4620
- }
4621
- ```
4622
-
4623
- - **Web:** Returns `{ hovered: boolean, hoverHandlers: { onMouseEnter, onMouseLeave } }`
4624
- - **Native:** Always returns `{ hovered: false, hoverHandlers: {} }` — no-op, no crashes
4625
-
4626
- Built-in web hover: `MediaCard` (shadow lift), interactive components use this internally.
4627
-
4628
- ---
4629
-
4630
- ## Design System Conventions
4631
-
4632
- ### Touch targets
4633
- All interactive elements maintain ≥44pt touch height per Apple HIG:
4634
- - Button md: 48px, sm: 40px, lg: 56px
4635
- - Input / Textarea: 56px (14px vertical padding)
4636
- - IconButton md: 44px, lg: 52px
4637
- - Checkbox: 24×24px box
4638
- - Switch: 30px track height
4639
- - MonthPicker arrows: 44×44px
4640
-
4641
- ### Haptic patterns
4642
- - `impactLight` — Button, Card (press), Sheet open, Toast, MediaCard action
4643
- - `selectionAsync` — Checkbox, Switch, Toggle, RadioGroup, Select, Slider steps, Accordion, ListItem, Chip, CategoryStrip, Tabs, MonthPicker
4644
- - `notificationSuccess` — Toast `success`
4645
- - `notificationError` — Toast `destructive`, Toast `warning`
4646
-
4647
- ### Animation press scales
4648
- - `Button` → 0.95 (strong spring feedback)
4649
- - `Card` → 0.98 (subtle, appropriate for large surfaces)
4650
- - `ListItem` → 0.97 (between — medium row targets)
4651
- - `MediaCard` → 0.98 (large surface)
4652
- - `IconButton` → 0.95
4653
- - `Chip`, `CategoryStrip chip` → 0.95
4654
-
4655
- ### Platform differences
4656
- - `useNativeDriver` — `Platform.OS !== 'web'` everywhere (native driver disabled on web)
4657
- - `Select` — wheel modal on iOS, dialog on Android, `<select>` on web
4658
- - `Toast` — full width on mobile, 400px centered on web
4659
- - Hover states — web only via `useHover()`
4660
- - `Skeleton` shimmer highlight — adapts opacity for light/dark mode
4661
-
4662
- ### Dynamic Type
4663
- All `Text` and `TextInput` components have `allowFontScaling={true}` — respects user font size accessibility settings.
4664
-
4665
- ### Scaling utilities (internal)
4666
- Used internally — not exported. `s()` horizontal scale, `vs()` vertical scale, `ms()` moderate scale relative to 350×680 base.
4667
-
4668
- ---
4669
-
4670
- ## Full Composition Examples
4671
-
4672
- The package includes **EXAMPLES.md** with complete, working code for 4 real-world app screens:
4673
-
4674
- 1. **Finance Dashboard** — MonthPicker, CurrencyDisplay, Progress, CategoryStrip, ListItem, DetailRow
4675
- 2. **Edit Profile** — Avatar, Badge, Input, Select, Switch, Card, AlertBanner
4676
- 3. **Onboarding Flow** — Badge, Progress, Input, RadioGroup, EmptyState (multi-step wizard)
4677
- 4. **Settings Screen** — MenuGroup, ListGroup, Form, MenuItem, ListItem, compound component patterns
4678
-
4679
- **For AI agents:** Import EXAMPLES.md from the package for full source code and patterns:
4680
-
4681
- ```markdown
4682
- ## UI Kit Composition Examples
4683
- @./node_modules/@retray-dev/ui-kit/EXAMPLES.md
4684
- ```
4685
-
4686
- Each example includes:
4687
- - Complete component code (copy-paste ready)
4688
- - State management setup
4689
- - Proper imports and theme usage
4690
- - Common patterns (spacing, colors, navigation, toast feedback)
4691
- - StyleSheet definitions
4692
-
4693
- **For human developers:** Examples are also live in the `example/` app. Clone the repo, run `pnpm install && pnpm build`, then `cd example && pnpm start` to see them in action.