@retray-dev/ui-kit 12.1.0 → 13.0.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 (282) hide show
  1. package/COMPONENTS.md +183 -147
  2. package/CONSUMER.md +2 -2
  3. package/DESIGN.md +2 -2
  4. package/README.md +13 -8
  5. package/dist/Accordion.d.mts +6 -0
  6. package/dist/Accordion.d.ts +6 -0
  7. package/dist/Accordion.js +62 -208
  8. package/dist/Accordion.mjs +6 -5
  9. package/dist/AlertBanner.js +29 -151
  10. package/dist/AlertBanner.mjs +3 -3
  11. package/dist/AppHeader.js +37 -233
  12. package/dist/AppHeader.mjs +6 -7
  13. package/dist/Avatar.d.mts +17 -1
  14. package/dist/Avatar.d.ts +17 -1
  15. package/dist/Avatar.js +80 -113
  16. package/dist/Avatar.mjs +2 -2
  17. package/dist/Badge.js +24 -147
  18. package/dist/Badge.mjs +3 -3
  19. package/dist/Button.js +86 -274
  20. package/dist/Button.mjs +6 -6
  21. package/dist/Card.js +15 -198
  22. package/dist/Card.mjs +4 -5
  23. package/dist/CategoryStrip.d.mts +0 -5
  24. package/dist/CategoryStrip.d.ts +0 -5
  25. package/dist/CategoryStrip.js +47 -263
  26. package/dist/CategoryStrip.mjs +6 -6
  27. package/dist/Checkbox.js +15 -198
  28. package/dist/Checkbox.mjs +5 -5
  29. package/dist/Chip.js +44 -234
  30. package/dist/Chip.mjs +7 -6
  31. package/dist/ConfirmDialog.js +100 -296
  32. package/dist/ConfirmDialog.mjs +7 -7
  33. package/dist/CurrencyDisplay.js +1 -112
  34. package/dist/CurrencyDisplay.mjs +2 -2
  35. package/dist/CurrencyInput.js +35 -160
  36. package/dist/CurrencyInput.mjs +5 -5
  37. package/dist/DetailRow.js +25 -148
  38. package/dist/DetailRow.mjs +3 -3
  39. package/dist/EmptyState.js +87 -275
  40. package/dist/EmptyState.mjs +7 -7
  41. package/dist/ErrorBoundary.js +32 -197
  42. package/dist/ErrorBoundary.mjs +4 -4
  43. package/dist/Form.js +1 -112
  44. package/dist/Form.mjs +2 -2
  45. package/dist/HolographicCard.d.mts +0 -28
  46. package/dist/HolographicCard.d.ts +0 -28
  47. package/dist/HolographicCard.js +20 -130
  48. package/dist/HolographicCard.mjs +9 -32
  49. package/dist/IconButton.js +36 -232
  50. package/dist/IconButton.mjs +5 -6
  51. package/dist/IconPicker.js +222 -927
  52. package/dist/IconPicker.mjs +5 -5
  53. package/dist/ImageUpload.d.mts +5 -1
  54. package/dist/ImageUpload.d.ts +5 -1
  55. package/dist/ImageUpload.js +32 -215
  56. package/dist/ImageUpload.mjs +5 -6
  57. package/dist/ImageViewer.js +75 -264
  58. package/dist/ImageViewer.mjs +8 -8
  59. package/dist/Input.d.mts +1 -1
  60. package/dist/Input.d.ts +1 -1
  61. package/dist/Input.js +35 -160
  62. package/dist/Input.mjs +4 -4
  63. package/dist/LabelValue.js +24 -147
  64. package/dist/LabelValue.mjs +3 -3
  65. package/dist/ListGroup.js +1 -112
  66. package/dist/ListGroup.mjs +2 -2
  67. package/dist/ListItem.js +38 -233
  68. package/dist/ListItem.mjs +5 -6
  69. package/dist/MediaCard.d.mts +0 -14
  70. package/dist/MediaCard.d.ts +0 -14
  71. package/dist/MediaCard.js +69 -313
  72. package/dist/MediaCard.mjs +5 -6
  73. package/dist/MenuGroup.js +1 -112
  74. package/dist/MenuGroup.mjs +2 -2
  75. package/dist/MenuItem.js +36 -232
  76. package/dist/MenuItem.mjs +5 -6
  77. package/dist/MonthPicker.js +8 -161
  78. package/dist/MonthPicker.mjs +3 -3
  79. package/dist/NumberStepper.js +40 -236
  80. package/dist/NumberStepper.mjs +5 -6
  81. package/dist/PagerDots.d.mts +1 -1
  82. package/dist/PagerDots.d.ts +1 -1
  83. package/dist/PagerDots.js +69 -222
  84. package/dist/PagerDots.mjs +6 -5
  85. package/dist/Pressable.js +14 -85
  86. package/dist/Pressable.mjs +4 -4
  87. package/dist/PricingCard.js +94 -279
  88. package/dist/PricingCard.mjs +8 -8
  89. package/dist/Progress.js +3 -121
  90. package/dist/Progress.mjs +3 -3
  91. package/dist/RadioGroup.js +52 -263
  92. package/dist/RadioGroup.mjs +5 -5
  93. package/dist/RetrayProvider.d.mts +1 -1
  94. package/dist/RetrayProvider.d.ts +1 -1
  95. package/dist/RetrayProvider.js +5 -6
  96. package/dist/RetrayProvider.mjs +3 -3
  97. package/dist/Select.d.mts +2 -1
  98. package/dist/Select.d.ts +2 -1
  99. package/dist/Select.js +24 -230
  100. package/dist/Select.mjs +4 -5
  101. package/dist/SelectableCard.d.mts +27 -0
  102. package/dist/SelectableCard.d.ts +27 -0
  103. package/dist/SelectableCard.js +335 -0
  104. package/dist/SelectableCard.mjs +8 -0
  105. package/dist/SelectableGrid.d.mts +0 -21
  106. package/dist/SelectableGrid.d.ts +0 -21
  107. package/dist/SelectableGrid.js +49 -269
  108. package/dist/SelectableGrid.mjs +5 -6
  109. package/dist/Separator.js +1 -112
  110. package/dist/Separator.mjs +2 -2
  111. package/dist/Sheet.js +16 -163
  112. package/dist/Sheet.mjs +3 -3
  113. package/dist/SheetSelect.js +39 -234
  114. package/dist/SheetSelect.mjs +6 -6
  115. package/dist/Skeleton.d.mts +3 -1
  116. package/dist/Skeleton.d.ts +3 -1
  117. package/dist/Skeleton.js +7 -124
  118. package/dist/Skeleton.mjs +3 -3
  119. package/dist/Slider.js +6 -159
  120. package/dist/Slider.mjs +3 -3
  121. package/dist/Spinner.js +3 -114
  122. package/dist/Spinner.mjs +2 -2
  123. package/dist/Stats.d.mts +4 -1
  124. package/dist/Stats.d.ts +4 -1
  125. package/dist/Stats.js +60 -234
  126. package/dist/Stats.mjs +5 -6
  127. package/dist/Switch.js +24 -173
  128. package/dist/Switch.mjs +5 -4
  129. package/dist/TabBar.js +43 -198
  130. package/dist/TabBar.mjs +5 -4
  131. package/dist/Tabs.js +15 -197
  132. package/dist/Tabs.mjs +5 -5
  133. package/dist/Text.js +9 -128
  134. package/dist/Text.mjs +2 -2
  135. package/dist/Textarea.d.mts +2 -1
  136. package/dist/Textarea.d.ts +2 -1
  137. package/dist/Textarea.js +71 -217
  138. package/dist/Textarea.mjs +4 -4
  139. package/dist/Toast.js +1 -112
  140. package/dist/Toast.mjs +2 -2
  141. package/dist/Toggle.js +39 -234
  142. package/dist/Toggle.mjs +6 -6
  143. package/dist/{chunk-FFTYLPSB.mjs → chunk-2QOHHBJC.mjs} +13 -7
  144. package/dist/{chunk-BCWEHE34.mjs → chunk-2VIDP72N.mjs} +3 -3
  145. package/dist/{chunk-PGERH3P7.mjs → chunk-4NQFTHN3.mjs} +13 -7
  146. package/dist/{chunk-3N2M3WZL.mjs → chunk-4ZO5PTKF.mjs} +4 -4
  147. package/dist/{chunk-MYZ2EDYU.mjs → chunk-5MYNAAFE.mjs} +13 -17
  148. package/dist/{chunk-E7NEHHXV.mjs → chunk-62BBSSUF.mjs} +3 -3
  149. package/dist/{chunk-ISY26JQJ.mjs → chunk-6CR4S6W2.mjs} +3 -3
  150. package/dist/{chunk-FUVYSVGR.mjs → chunk-6QLBHUEG.mjs} +8 -7
  151. package/dist/chunk-ARONDO7M.mjs +40 -0
  152. package/dist/{chunk-3UYAZ7I4.mjs → chunk-AZV7KNJI.mjs} +3 -3
  153. package/dist/{chunk-HLMPMUK2.mjs → chunk-BTUW5LSG.mjs} +11 -8
  154. package/dist/chunk-BULKGOIZ.mjs +235 -0
  155. package/dist/{chunk-265G6A46.mjs → chunk-CBIZLRYH.mjs} +29 -12
  156. package/dist/chunk-CM2DG4MR.mjs +142 -0
  157. package/dist/{chunk-2I2AYECM.mjs → chunk-DBHSUUKU.mjs} +2 -2
  158. package/dist/{chunk-P64WHW4A.mjs → chunk-DE25XTVQ.mjs} +3 -3
  159. package/dist/{chunk-DI7CBDL6.mjs → chunk-E4EQSCKR.mjs} +5 -5
  160. package/dist/{chunk-357YO24D.mjs → chunk-EHGBHFMH.mjs} +9 -17
  161. package/dist/{chunk-GK4VRMNE.mjs → chunk-EROPDCB5.mjs} +24 -27
  162. package/dist/{chunk-XBAGGKLW.mjs → chunk-ERWJPVX7.mjs} +2 -2
  163. package/dist/{chunk-LRM4AVYY.mjs → chunk-ESQDPO5E.mjs} +7 -7
  164. package/dist/{chunk-EFLFRAHD.mjs → chunk-EW2FIDSM.mjs} +1 -1
  165. package/dist/{chunk-7HSILTC4.mjs → chunk-FTTI6T5Q.mjs} +4 -4
  166. package/dist/{chunk-X26S5EVZ.mjs → chunk-HUSSF6TF.mjs} +1 -1
  167. package/dist/chunk-IFYMBOEN.mjs +14 -0
  168. package/dist/{chunk-S3KJCPEJ.mjs → chunk-IGU223UM.mjs} +80 -4
  169. package/dist/chunk-IJCMPVW5.mjs +121 -0
  170. package/dist/{chunk-I4V5XZPS.mjs → chunk-ITG4JQM3.mjs} +4 -4
  171. package/dist/{chunk-F4V6XLP4.mjs → chunk-K3QX2M26.mjs} +11 -8
  172. package/dist/{chunk-V6NFJXKO.mjs → chunk-K7TKID3V.mjs} +8 -7
  173. package/dist/{chunk-ZHMSAYLT.mjs → chunk-KAGADD2O.mjs} +4 -4
  174. package/dist/{chunk-3GEYJ7I5.mjs → chunk-KC5QDYGZ.mjs} +4 -4
  175. package/dist/{chunk-HJ46DTJE.mjs → chunk-KPTY7UYQ.mjs} +1 -1
  176. package/dist/{chunk-EMUWGDWC.mjs → chunk-KSSVIFYR.mjs} +11 -12
  177. package/dist/chunk-L3YKPTJQ.mjs +119 -0
  178. package/dist/chunk-M53LC4Q7.mjs +35 -0
  179. package/dist/{chunk-NXI4YDZ2.mjs → chunk-MP7GLMIR.mjs} +17 -25
  180. package/dist/chunk-MZ6WRTD2.mjs +40 -0
  181. package/dist/chunk-NGEN2EES.mjs +581 -0
  182. package/dist/{chunk-JULSIZDM.mjs → chunk-OBV72JD4.mjs} +1 -1
  183. package/dist/{chunk-2A2LEFZG.mjs → chunk-PGQ6FMXS.mjs} +6 -5
  184. package/dist/{chunk-BQZE3HAW.mjs → chunk-PI6RULJX.mjs} +1 -1
  185. package/dist/{chunk-FA2KMTH5.mjs → chunk-RA6SAAFE.mjs} +9 -8
  186. package/dist/{chunk-FVTVCJAH.mjs → chunk-RRKM4MKB.mjs} +7 -7
  187. package/dist/{chunk-AKM4EPOT.mjs → chunk-S2VGME7X.mjs} +1 -1
  188. package/dist/{chunk-OULVKTWL.mjs → chunk-S44XWTTC.mjs} +35 -25
  189. package/dist/{chunk-QSFV2P7O.mjs → chunk-SZEKQAOY.mjs} +1 -1
  190. package/dist/{chunk-N4ZPVCJH.mjs → chunk-TETMEKZE.mjs} +9 -9
  191. package/dist/{chunk-2CBQKU7H.mjs → chunk-TMH263OK.mjs} +5 -4
  192. package/dist/{chunk-D3Y2T42P.mjs → chunk-U6DEBYU5.mjs} +10 -9
  193. package/dist/{chunk-4WFMPFZB.mjs → chunk-UOKFSFNJ.mjs} +2 -2
  194. package/dist/{chunk-WOEWGSTU.mjs → chunk-URIH43IJ.mjs} +13 -21
  195. package/dist/{chunk-JCZQOY4O.mjs → chunk-V2ZB2XNS.mjs} +16 -10
  196. package/dist/{chunk-P73V2EKS.mjs → chunk-WIPEDNSD.mjs} +7 -7
  197. package/dist/{chunk-BOVUP27T.mjs → chunk-XCIG6HT2.mjs} +6 -5
  198. package/dist/chunk-Y6YS33GM.mjs +131 -0
  199. package/dist/{chunk-5OLNXP3S.mjs → chunk-ZKDKKQCE.mjs} +29 -7
  200. package/dist/{chunk-DF6DU42P.mjs → chunk-ZTPYUU5C.mjs} +5 -5
  201. package/dist/{index-wt-orHUi.d.ts → index-CY34hxPN.d.mts} +1 -0
  202. package/dist/{index-wt-orHUi.d.mts → index-CY34hxPN.d.ts} +1 -0
  203. package/dist/index.d.mts +15 -74
  204. package/dist/index.d.ts +15 -74
  205. package/dist/index.js +1055 -1562
  206. package/dist/index.mjs +81 -84
  207. package/package.json +8 -10
  208. package/src/components/Accordion/Accordion.tsx +32 -9
  209. package/src/components/AlertBanner/AlertBanner.tsx +7 -6
  210. package/src/components/AppHeader/AppHeader.tsx +1 -1
  211. package/src/components/Avatar/Avatar.tsx +92 -1
  212. package/src/components/Avatar/index.ts +2 -2
  213. package/src/components/Badge/Badge.tsx +2 -2
  214. package/src/components/Button/Button.tsx +64 -57
  215. package/src/components/Card/Card.tsx +1 -0
  216. package/src/components/CategoryStrip/CategoryStrip.tsx +36 -49
  217. package/src/components/Chip/Chip.tsx +5 -4
  218. package/src/components/ConfirmDialog/ConfirmDialog.tsx +13 -6
  219. package/src/components/DetailRow/DetailRow.tsx +3 -3
  220. package/src/components/EmptyState/EmptyState.tsx +2 -2
  221. package/src/components/ErrorBoundary/ErrorBoundary.tsx +6 -6
  222. package/src/components/HolographicCard/HolographicCard.tsx +14 -95
  223. package/src/components/IconButton/IconButton.tsx +2 -2
  224. package/src/components/IconPicker/IconPicker.tsx +13 -12
  225. package/src/components/ImageUpload/ImageUpload.tsx +24 -28
  226. package/src/components/ImageViewer/ImageViewer.tsx +3 -3
  227. package/src/components/Input/Input.tsx +11 -5
  228. package/src/components/LabelValue/LabelValue.tsx +2 -2
  229. package/src/components/ListItem/ListItem.tsx +4 -4
  230. package/src/components/MediaCard/MediaCard.tsx +21 -59
  231. package/src/components/MenuItem/MenuItem.tsx +2 -2
  232. package/src/components/MonthPicker/MonthPicker.tsx +2 -2
  233. package/src/components/NumberStepper/NumberStepper.tsx +6 -6
  234. package/src/components/PagerDots/PagerDots.tsx +38 -28
  235. package/src/components/PricingCard/PricingCard.tsx +6 -6
  236. package/src/components/RadioGroup/RadioGroup.tsx +18 -31
  237. package/src/components/Select/Select.tsx +32 -39
  238. package/src/components/SelectableCard/SelectableCard.tsx +302 -0
  239. package/src/components/SelectableCard/index.ts +1 -0
  240. package/src/components/SelectableGrid/SelectableGrid.tsx +38 -72
  241. package/src/components/Sheet/Sheet.tsx +11 -4
  242. package/src/components/SheetSelect/SheetSelect.tsx +3 -3
  243. package/src/components/Skeleton/Skeleton.tsx +6 -3
  244. package/src/components/Spinner/Spinner.tsx +2 -2
  245. package/src/components/Stats/Stats.tsx +36 -8
  246. package/src/components/Switch/Switch.tsx +9 -6
  247. package/src/components/TabBar/TabBar.tsx +9 -8
  248. package/src/components/Text/Text.tsx +12 -1
  249. package/src/components/Textarea/Textarea.tsx +18 -32
  250. package/src/components/Toggle/Toggle.tsx +3 -3
  251. package/src/hooks/useConfirmDialog.ts +31 -42
  252. package/src/index.ts +4 -4
  253. package/src/theme/ThemeProvider.tsx +1 -4
  254. package/src/theme/colorUtils.ts +1 -72
  255. package/src/theme/colors.ts +47 -1
  256. package/src/theme/types.ts +6 -3
  257. package/src/utils/animations.ts +0 -47
  258. package/src/utils/curatedIcons.ts +93 -801
  259. package/src/utils/haptics.ts +13 -208
  260. package/src/utils/icons.ts +27 -91
  261. package/src/utils/pressable.ts +10 -61
  262. package/dist/VirtualList.d.mts +0 -19
  263. package/dist/VirtualList.d.ts +0 -19
  264. package/dist/VirtualList.js +0 -38
  265. package/dist/VirtualList.mjs +0 -2
  266. package/dist/chunk-3DKJ2GIC.mjs +0 -30
  267. package/dist/chunk-AQEVCEXV.mjs +0 -164
  268. package/dist/chunk-DOGIPOF5.mjs +0 -131
  269. package/dist/chunk-DVK4G2GT.mjs +0 -59
  270. package/dist/chunk-EJ7ZPXOH.mjs +0 -163
  271. package/dist/chunk-J6Q2YJEV.mjs +0 -134
  272. package/dist/chunk-JNVAIDLK.mjs +0 -136
  273. package/dist/chunk-KA7LTET3.mjs +0 -71
  274. package/dist/chunk-KHYX4IOM.mjs +0 -1114
  275. package/dist/chunk-NC5ZTR2Y.mjs +0 -32
  276. package/dist/chunk-YNROWHQJ.mjs +0 -46
  277. package/src/components/VirtualList/VirtualList.tsx +0 -60
  278. package/src/components/VirtualList/index.ts +0 -1
  279. package/src/utils/fontGuard.ts +0 -35
  280. package/src/utils/hover.ts +0 -25
  281. package/src/utils/useColorTransition.ts +0 -40
  282. package/src/utils/usePressScale.ts +0 -75
package/COMPONENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- # @retray-dev/ui-kit — Component Reference (v12.1.0)
1
+ # @retray-dev/ui-kit — Component Reference (v13.0.0)
2
2
 
3
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:
4
4
 
@@ -99,10 +99,10 @@ module.exports = function (api) {
99
99
  | `@expo/vector-icons` | **Required** | Icons everywhere | |
100
100
  | `react-native-size-matters` | **Required** | Responsive scaling | |
101
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` |
102
+
103
103
  | `@shopify/react-native-skia` + `expo-sensors` | **Optional** | Deep-import `HolographicCard` only | `pnpm add @shopify/react-native-skia expo-sensors` |
104
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.
105
+
106
106
 
107
107
  ### Troubleshooting: `react-native-screens` codegen on RN 0.83
108
108
 
@@ -275,8 +275,9 @@ The full palette components consume. Never supply these directly — they are co
275
275
  |-------|-------------|---------|
276
276
  | `foregroundSubtle` | `foreground` @ ~70% | Body text, subtitles, secondary content |
277
277
  | `foregroundMuted` | `foreground` @ ~62% | Captions, timestamps, placeholders |
278
- | `surface` | `background` slightly off-canvas | Chip backgrounds, input fills, tag backgrounds, skeleton |
278
+ | `surface` | `background` slightly off-canvas | Chip backgrounds, input fills, tag backgrounds |
279
279
  | `surfaceStrong` | `background` stronger offset | Pressed/hover fill states |
280
+ | `skeleton` | `background` @ ±10% | Skeleton placeholder — higher contrast than surface for visibility |
280
281
  | `destructiveTint` | `destructive` blended to bg | Alert banner background, toast background |
281
282
  | `destructiveBorder` | `destructive` @ 30% | Alert banner border, badge outline |
282
283
  | `successTint` | `success` blended to bg | Success banner background |
@@ -285,6 +286,7 @@ The full palette components consume. Never supply these directly — they are co
285
286
  | `warningBorder` | `warning` @ 30% | Warning banner border |
286
287
  | `ring` | `= primary` | Focus ring color (always matches primary) |
287
288
  | `input` | `= border` | Input field border (always matches border) |
289
+ | `separator` | `border` @ ±16-22% | Divider/separator line — deliberately darker than border |
288
290
  | `overlay` | `overlay` token or `rgba(0,0,0,0.45)` | Backdrop behind sheets and dialogs |
289
291
  | `accentResolved` | `accent` token or `= primary` | Resolved accent color — always present |
290
292
  | `accentForegroundResolved` | `accentForeground` token or `= primaryForeground` | Resolved text on accent — always present |
@@ -312,7 +314,7 @@ const { colors } = useTheme()
312
314
  import { deriveColors } from '@retray-dev/ui-kit'
313
315
 
314
316
  const resolved = deriveColors(myThemeColors, 'light')
315
- // resolved contains all 24 ResolvedColors tokens
317
+ // resolved contains all 26 ResolvedColors tokens
316
318
  ```
317
319
 
318
320
  ---
@@ -917,7 +919,7 @@ No consumer action required — the fix is internal.
917
919
  | size | `'sm' \| 'md' \| 'lg'` | `'md'` | Controls height, padding, and icon size |
918
920
  | loading | `boolean` | `false` | Replaces label with spinner, forces disabled state |
919
921
  | fullWidth | `boolean` | `false` | Stretches to container width (`alignSelf: 'stretch'`) |
920
- | disabled | `boolean` | — | Reduces opacity to 0.5 |
922
+ | disabled | `boolean` | — | Per-variant explicit colors (no opacity). `filled`→`surface` bg, `secondary`→`border` border, `text`/`outline`→`foregroundMuted` text |
921
923
  | icon | `React.ReactNode \| ((props: { label, size, variant }) => React.ReactNode)` | — | Icon rendered alongside label |
922
924
  | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
923
925
  | iconColor | `string` | — | Override icon color. Defaults to variant label color |
@@ -1559,6 +1561,65 @@ const [guests, setGuests] = useState(2)
1559
1561
 
1560
1562
  ---
1561
1563
 
1564
+ ### AvatarGroup
1565
+
1566
+ **Import:** `import { AvatarGroup } from '@retray-dev/ui-kit'`
1567
+
1568
+ **When to use:** Stacked/overlapping avatars to show multiple users in a compact space. Common for collaborator indicators, multi-waiter tables, and team members.
1569
+
1570
+ | Prop | Type | Default | Notes |
1571
+ |------|------|---------|-------|
1572
+ | users | `{ name: string; src?: string }[]` | required | User data — name used for fallback initials |
1573
+ | max | `number` | `3` | Max avatars visible before `+N` overflow badge |
1574
+ | size | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'sm'` | Avatar size preset, passed to each `Avatar` |
1575
+ | overlap | `number` | `vs(8)` | Overlap in points between consecutive avatars |
1576
+ | onOverflowPress | `() => void` | — | Tap handler for the `+N` overflow badge |
1577
+ | style | `ViewStyle` | — | Outer container style |
1578
+
1579
+ **Sizes:** Same as `Avatar` — `sm` (28px), `md` (40px), `lg` (56px), `xl` (72px).
1580
+
1581
+ **Notes:**
1582
+ - Overflow badge uses `surfaceStrong` background with `foregroundMuted` text.
1583
+ - Individual avatars are not tappable — only the `+N` badge fires `onOverflowPress`.
1584
+ - `overlap` follows the spacing grid (defaults to `spacing.sm` = 8pt).
1585
+
1586
+ **Examples:**
1587
+ ```tsx
1588
+ // Basic — 5 waiters, show max 3
1589
+ <AvatarGroup
1590
+ users={[
1591
+ { name: 'Ana', src: 'https://...' },
1592
+ { name: 'Bob', src: 'https://...' },
1593
+ { name: 'Carlos' },
1594
+ { name: 'Diana', src: 'https://...' },
1595
+ { name: 'Elena' },
1596
+ ]}
1597
+ max={3}
1598
+ />
1599
+
1600
+ // With overflow tap
1601
+ <AvatarGroup
1602
+ users={waiters}
1603
+ max={3}
1604
+ size="sm"
1605
+ onOverflowPress={() => showAllWaiters()}
1606
+ />
1607
+
1608
+ // In a ListItem
1609
+ <ListItem
1610
+ title="Table 4 — 5 waiters"
1611
+ leftRender={
1612
+ <AvatarGroup
1613
+ users={table.waiters}
1614
+ max={3}
1615
+ size="sm"
1616
+ />
1617
+ }
1618
+ />
1619
+ ```
1620
+
1621
+ ---
1622
+
1562
1623
  ### Card
1563
1624
 
1564
1625
  **Import:** `import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@retray-dev/ui-kit'`
@@ -1763,73 +1824,7 @@ const [guests, setGuests] = useState(2)
1763
1824
 
1764
1825
  ---
1765
1826
 
1766
- ### VirtualList
1767
1827
 
1768
- **Import:** `import { VirtualList } from '@retray-dev/ui-kit'`
1769
-
1770
- **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.
1771
-
1772
- | Prop | Type | Default | Notes |
1773
- |------|------|---------|-------|
1774
- | data | `T[]` | — | Array of items to render |
1775
- | renderItem | `ListRenderItem<T>` | — | Render function for each item |
1776
- | itemHeight | `number` | — | Fixed row height in px. Enables `getItemLayout` for performance with large datasets |
1777
- | keyExtractor | `(item: T, index: number) => string` | Auto | Defaults to `item.id` or index. Override for custom keys |
1778
- | ...FlatListProps | — | — | All standard FlatList props pass through |
1779
-
1780
- **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.
1781
-
1782
- **Default key extraction:** Uses `item.id` (converted to string) or falls back to index. Override with `keyExtractor` for custom logic.
1783
-
1784
- **Example — simple list:**
1785
- ```tsx
1786
- <VirtualList
1787
- data={items}
1788
- renderItem={({ item }) => (
1789
- <ListItem
1790
- title={item.title}
1791
- subtitle={item.subtitle}
1792
- onPress={() => handlePress(item.id)}
1793
- />
1794
- )}
1795
- itemHeight={56}
1796
- />
1797
- ```
1798
-
1799
- **Example — large dataset with memoized render:**
1800
- ```tsx
1801
- const renderItem = useCallback(({ item }) => (
1802
- <ListItem
1803
- title={item.title}
1804
- subtitle={item.subtitle}
1805
- leftIcon="circle"
1806
- showSeparator
1807
- onPress={() => navigate('detail', { id: item.id })}
1808
- />
1809
- ), [])
1810
-
1811
- <VirtualList
1812
- data={thousands}
1813
- renderItem={renderItem}
1814
- itemHeight={64}
1815
- />
1816
- ```
1817
-
1818
- **Example — variable height rows (omit itemHeight):**
1819
- ```tsx
1820
- <VirtualList
1821
- data={posts}
1822
- renderItem={({ item }) => (
1823
- <Card style={{ margin: SPACING.sm }}>
1824
- <CardContent>
1825
- <Text>{item.content}</Text>
1826
- </CardContent>
1827
- </Card>
1828
- )}
1829
- />
1830
- ```
1831
-
1832
- ---
1833
1828
 
1834
1829
  ### Separator
1835
1830
 
@@ -1902,6 +1897,7 @@ const renderItem = useCallback(({ item }) => (
1902
1897
  | width | `number \| string` | `'100%'` | Width of placeholder |
1903
1898
  | height | `number` | `16` | Height of placeholder |
1904
1899
  | borderRadius | `number` | `6` | Corner radius |
1900
+ | backgroundColor | `string` | `colors.skeleton` | Override placeholder background color |
1905
1901
  | preset | `'base' \| 'circle' \| 'text'` | `'base'` | Convenience preset |
1906
1902
  | diameter | `number` | `40` | Used by `'circle'` preset — overrides width/height |
1907
1903
  | style | `ViewStyle` | — | — |
@@ -1943,7 +1939,7 @@ const renderItem = useCallback(({ item }) => (
1943
1939
  // Matches <ListItem> — leading circle + title/subtitle lines
1944
1940
  <Skeleton.ListItem /> // props: showAvatar, showSubtitle, style
1945
1941
 
1946
- // Repeated list/grid placeholder — for VirtualList / FlatList while loading.
1942
+ // Repeated list/grid placeholder — for FlatList while loading.
1947
1943
  // columns=1 → stacked ListItemSkeleton; columns>1 → grid of MediaCardSkeleton.
1948
1944
  <Skeleton.List count={6} /> // stacked list
1949
1945
  <Skeleton.List count={6} columns={2} /> // 2-col card grid
@@ -1951,8 +1947,8 @@ const renderItem = useCallback(({ item }) => (
1951
1947
 
1952
1948
  // Also exported as named components: MediaCardSkeleton, ListItemSkeleton, ListSkeleton
1953
1949
 
1954
- // As a VirtualList loading state
1955
- <VirtualList
1950
+ // As a FlatList loading state
1951
+ <FlatList
1956
1952
  data={loading ? [] : rows}
1957
1953
  renderItem={renderItem}
1958
1954
  ListEmptyComponent={loading ? <Skeleton.List count={8} /> : <EmptyState .../>}
@@ -2349,6 +2345,94 @@ const [accepted, setAccepted] = useState(false)
2349
2345
 
2350
2346
  ---
2351
2347
 
2348
+ ### SelectableCard
2349
+
2350
+ **Import:** `import { SelectableCard, SelectableCardGroup } from '@retray-dev/ui-kit'`
2351
+
2352
+ **When to use:** Selectable cards with radio (single-select) or checkbox (multi-select) behavior. Each card supports an icon, title, description, and disabled state. Perfect for role selectors, plan pickers, feature/module configuration — anywhere you need descriptive options that communicate more than a simple label.
2353
+
2354
+ | Prop | Type | Default | Notes |
2355
+ |------|------|---------|-------|
2356
+ | value | `string` | required | The value this card represents |
2357
+ | title | `string` | required | Card title |
2358
+ | description | `string` | — | Secondary text below title |
2359
+ | iconName | `string` | — | Left icon from @expo/vector-icons |
2360
+ | icon | `ReactNode` | — | Custom left icon |
2361
+ | disabled | `boolean` | `false` | Grayed out, no press/haptic |
2362
+ | style | `ViewStyle` | — | — |
2363
+
2364
+ **SelectableCardGroup Props:**
2365
+
2366
+ | Prop | Type | Default | Notes |
2367
+ |------|------|---------|-------|
2368
+ | type | `'radio' \| 'checkbox'` | `'radio'` | Selection mode |
2369
+ | value | `string \| string[]` | required | Selected value(s) |
2370
+ | onValueChange | `(value: string \| string[]) => void` | required | — |
2371
+ | variant | `'elevated' \| 'outlined' \| 'filled'` | `'elevated'` | Card surface |
2372
+ | gap | `number` | `s(8)` | Spacing between cards |
2373
+ | style | `ViewStyle` | — | — |
2374
+
2375
+ **Card variants:** `elevated` (shadow depth, 2px border matches background when unselected — no layout shift on select), `outlined` (2px border), `filled` (surfaceStrong background + 2px border). All variants use a consistent 2px border width so cards don't resize when selected.
2376
+
2377
+ **Selection visual:** Selected cards get a 2px `colors.primary` border + `EaseView` animated selector indicator. Radio shows a 24×24 circle with spring-animated inner dot. Checkbox shows a 24×24 box with opacity-animated checkmark. Both follow the same visual patterns as `RadioGroup` and `Checkbox`.
2378
+
2379
+ **Haptics:** `impactLight` on selection.
2380
+
2381
+ **Disabled:** Dims content to `foregroundMuted`, removes shadow/elevation, keeps border at `colors.border`. No press, no haptic.
2382
+
2383
+ **Examples:**
2384
+ ```tsx
2385
+ // Single select (radio)
2386
+ <SelectableCardGroup
2387
+ type="radio"
2388
+ value={selectedRole}
2389
+ onValueChange={setSelectedRole}
2390
+ >
2391
+ <SelectableCard
2392
+ value="admin"
2393
+ iconName="shield"
2394
+ title="Administrator"
2395
+ description="Full access to reports, settings, and business management"
2396
+ />
2397
+ <SelectableCard
2398
+ value="waiter"
2399
+ iconName="user-check"
2400
+ title="Waiter"
2401
+ description="Take orders from tables and manage requests"
2402
+ />
2403
+ <SelectableCard
2404
+ value="cook"
2405
+ iconName="chef-hat"
2406
+ title="Cook"
2407
+ description="Receive and prepare items from each order in the kitchen"
2408
+ disabled
2409
+ />
2410
+ </SelectableCardGroup>
2411
+
2412
+ // Multi select (checkbox)
2413
+ <SelectableCardGroup
2414
+ type="checkbox"
2415
+ value={selectedRoles}
2416
+ onValueChange={setSelectedRoles}
2417
+ variant="outlined"
2418
+ >
2419
+ <SelectableCard
2420
+ value="reports"
2421
+ iconName="bar-chart-2"
2422
+ title="Reports"
2423
+ description="View sales, inventory, and performance analytics"
2424
+ />
2425
+ <SelectableCard
2426
+ value="inventory"
2427
+ iconName="package"
2428
+ title="Inventory"
2429
+ description="Manage stock levels and supplier orders"
2430
+ />
2431
+ </SelectableCardGroup>
2432
+ ```
2433
+
2434
+ ---
2435
+
2352
2436
  ### Slider
2353
2437
 
2354
2438
  **Import:** `import { Slider } from '@retray-dev/ui-kit'`
@@ -2510,6 +2594,7 @@ const [tab, setTab] = useState('profile')
2510
2594
  iconName?: string // Icon name from @expo/vector-icons
2511
2595
  icon?: ReactNode // Custom icon node
2512
2596
  iconColor?: string // Override icon color (defaults to foregroundMuted)
2597
+ triggerActions?: ReactNode // Action buttons rendered between trigger and chevron. Touch-isolated — taps on actions won't toggle the accordion.
2513
2598
  }
2514
2599
  ```
2515
2600
 
@@ -3980,12 +4065,13 @@ export default function TabLayout() {
3980
4065
  | icon | `React.ReactNode` | — | Custom icon node |
3981
4066
  | iconName | `string` | — | Icon from `@expo/vector-icons` (left of value, `colors.primary`) |
3982
4067
  | iconColor | `string` | `colors.primary` | Override icon color |
4068
+ | size | `'default' \| 'compact'` | `'default'` | `compact`: 16px SemiBold value, 11px label, reduced padding |
3983
4069
  | variant | `'elevated' \| 'outlined' \| 'filled'` | `'elevated'` | Card surface variant |
3984
4070
  | onPress | `() => void` | — | Makes the card pressable (haptic `impactLight()`) |
3985
4071
  | style | `ViewStyle` | — | — |
3986
4072
  | accessibilityLabel | `string` | — | — |
3987
4073
 
3988
- **Design notes:** Value uses `Sohne-Bold` at 28px. Label uses `Sohne-Regular` at 13px in `foregroundSubtle`. Description uses `Sohne-Regular` at 12px in `foregroundMuted`. Icon renders at 22px, left-aligned with the value. All content is centered vertically and horizontally. The card shrinks to fit its content by default (`alignSelf: 'flex-start'`). When placed inside `Stats.Group`, cards stretch to equal width. When the card is narrow (< 150dp) and has an icon, the layout switches to vertical stacking (icon → value → label → description) to avoid overflow.
4074
+ **Design notes:** Value uses `Sohne-Bold` at 21px. Label uses `Sohne-Regular` at 13px in `foregroundSubtle`. Description uses `Sohne-Regular` at 12px in `foregroundMuted`. Icon renders at 20px, left-aligned with the value. All content is centered vertically and horizontally. The card shrinks to fit its content by default (`alignSelf: 'flex-start'`). When placed inside `Stats.Group`, cards stretch to equal width. When the card is narrow (< 150dp) and has an icon, the layout switches to vertical stacking (icon → value → label → description) to avoid overflow.
3989
4075
 
3990
4076
  **`Stats.Group`** — horizontal layout wrapper that distributes children equally:
3991
4077
 
@@ -4237,6 +4323,7 @@ import { HolographicCard, FOIL_PRESETS } from '@retray-dev/ui-kit/HolographicCar
4237
4323
  | borderRadius | `number` | `RADIUS.lg` | — |
4238
4324
  | resizeMode | `'cover' \| 'contain' \| 'stretch'` | `'cover'` | Image resize mode |
4239
4325
  | disabled | `boolean` | `false` | Prevents pressing |
4326
+ | allowsEditing | `boolean` | `true` | When `true`, iOS opens the crop/editing screen after selecting an image. Set `false` to accept the image directly without cropping |
4240
4327
  | style | `ViewStyle` | — | — |
4241
4328
  | accessibilityLabel | `string` | — | — |
4242
4329
 
@@ -4331,10 +4418,12 @@ const [icon, setIcon] = useState<string | null>(null)
4331
4418
 
4332
4419
  **Import:** `import { useConfirmDialog } from '@retray-dev/ui-kit'`
4333
4420
 
4334
- **When to use:** Manage `ConfirmDialog` state without boilerplate. Replaces the `[target, setTarget]` + `visible` + `loading` state pattern that each screen would otherwise duplicate.
4421
+ **When to use:** Manage `ConfirmDialog` state without boilerplate. Replaces the `visible` + `loading` state pattern that each screen would otherwise duplicate.
4422
+
4423
+ **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.
4335
4424
 
4336
4425
  ```ts
4337
- function useConfirmDialog<T>(options: UseConfirmDialogOptions): UseConfirmDialogResult<T>
4426
+ function useConfirmDialog(options: UseConfirmDialogOptions): UseConfirmDialogResult
4338
4427
 
4339
4428
  interface UseConfirmDialogOptions {
4340
4429
  onConfirm: () => void | Promise<void>
@@ -4347,34 +4436,30 @@ interface UseConfirmDialogOptions {
4347
4436
  | Field | Type | Notes |
4348
4437
  |-------|------|-------|
4349
4438
  | visible | `boolean` | Pass to `ConfirmDialog.visible` |
4350
- | target | `T \| null` | The value passed to `open()` — e.g. the item being deleted |
4351
4439
  | loading | `boolean` | Pass to `ConfirmDialog.loading` |
4352
- | open | `(target?: T) => void` | Call to show the dialog |
4353
- | dialogProps | `object` | Spread directly onto `<ConfirmDialog>` (contains `visible`, `loading`, `onConfirm`, `onCancel`) |
4440
+ | open | `() => void` | Call to show the dialog |
4441
+ | onConfirm | `() => void` | Pass directly to `ConfirmDialog.onConfirm` |
4442
+ | onCancel | `() => void` | Pass directly to `ConfirmDialog.onCancel` |
4354
4443
 
4355
4444
  **Example:**
4356
4445
  ```tsx
4357
- const { open, target, dialogProps } = useConfirmDialog<Product>({
4446
+ const { visible, loading, open, onConfirm, onCancel } = useConfirmDialog({
4358
4447
  onConfirm: async () => {
4359
- await deleteProduct(target!.id)
4448
+ await deleteProduct(productId)
4360
4449
  toast.success('Product deleted')
4361
4450
  },
4362
4451
  })
4363
4452
 
4364
- // Somewhere in your list:
4365
- <ListItem
4366
- title={product.name}
4367
- rightIcon="trash-2"
4368
- onPress={() => open(product)}
4369
- />
4370
-
4371
- // Once in your screen's JSX:
4453
+ // Somewhere in your screen:
4372
4454
  <ConfirmDialog
4373
- title={`Delete "${target?.name}"?`}
4374
- description="This action cannot be undone."
4455
+ visible={visible}
4456
+ title="Delete product?"
4457
+ subtitle="This action cannot be undone."
4375
4458
  confirmLabel="Delete"
4376
4459
  confirmVariant="destructive"
4377
- {...dialogProps}
4460
+ loading={loading}
4461
+ onConfirm={onConfirm}
4462
+ onCancel={onCancel}
4378
4463
  />
4379
4464
  ```
4380
4465
 
@@ -4388,12 +4473,12 @@ The library ships a built-in icon resolver — pass any icon name string to comp
4388
4473
 
4389
4474
  | Priority | Family | Best for |
4390
4475
  |---|---|---|
4391
- | 1 (highest) | `Feather` | Clean line icons, UI essentials |
4476
+ | 1 | `Feather` | Clean line icons, UI essentials (first match wins) |
4392
4477
  | 2 | `AntDesign` | Semantic UI icons |
4393
4478
  | 3 | `Entypo` | Social, media, navigation icons |
4394
4479
  | 4 | `FontAwesome5` | Wide coverage |
4395
4480
  | 5 | `MaterialIcons` | Material-style icons |
4396
- | 6 (lowest) | `Ionicons` | Fallback |
4481
+ | 6 | `Ionicons` | Fallback |
4397
4482
 
4398
4483
  Browse all available icons: **https://icons.expo.fyi**
4399
4484
 
@@ -4420,29 +4505,7 @@ import { Icon } from '@retray-dev/ui-kit'
4420
4505
 
4421
4506
  Returns `null` (no crash) if name not found in any family.
4422
4507
 
4423
- ### `renderIcon` helper
4424
-
4425
- ```tsx
4426
- import { renderIcon } from '@retray-dev/ui-kit'
4427
-
4428
- // Returns ReactNode or null
4429
- const icon = renderIcon('check', 18, colors.primary)
4430
- ```
4431
-
4432
- ### `getValidIconNames` utility
4433
-
4434
- ```tsx
4435
- import { getValidIconNames } from '@retray-dev/ui-kit'
4436
-
4437
- // All icon names across all configured families
4438
- const allIcons: string[] = getValidIconNames()
4439
- // => ["home", "user", "heart", "star", ...]
4440
-
4441
- // Scoped to specific families (does not mutate global config)
4442
- const featherOnly: string[] = getValidIconNames(['Feather'])
4443
- ```
4444
-
4445
- 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.
4508
+ > **v13:** `renderIcon`, `getValidIconNames`, and `configureIconFamilies` were removed. Use `Icon` component directly for all icon rendering.
4446
4509
 
4447
4510
  ### `iconName` props on components
4448
4511
 
@@ -4506,33 +4569,6 @@ getResponsiveFontSize(text, 48, [
4506
4569
 
4507
4570
  ---
4508
4571
 
4509
- ## Hover Support (Web)
4510
-
4511
- ```tsx
4512
- import { useHover } from '@retray-dev/ui-kit'
4513
-
4514
- function MyHoverableComponent() {
4515
- const { hovered, hoverHandlers } = useHover()
4516
-
4517
- return (
4518
- <View
4519
- {...hoverHandlers}
4520
- style={[
4521
- styles.container,
4522
- hovered && { backgroundColor: colors.surfaceStrong }
4523
- ]}
4524
- />
4525
- )
4526
- }
4527
- ```
4528
-
4529
- - **Web:** Returns `{ hovered: boolean, hoverHandlers: { onMouseEnter, onMouseLeave } }`
4530
- - **Native:** Always returns `{ hovered: false, hoverHandlers: {} }` — no-op, no crashes
4531
-
4532
- Built-in web hover: `MediaCard` (shadow lift), interactive components use this internally.
4533
-
4534
- ---
4535
-
4536
4572
  ## Design System Conventions
4537
4573
 
4538
4574
  ### Touch targets
@@ -4562,7 +4598,7 @@ All interactive elements maintain ≥44pt touch height per Apple HIG:
4562
4598
  - `useNativeDriver` — `Platform.OS !== 'web'` everywhere (native driver disabled on web)
4563
4599
  - `Select` — wheel modal on iOS, dialog on Android, `<select>` on web
4564
4600
  - `Toast` — full width on mobile, 400px centered on web
4565
- - Hover states — web only via `useHover()`
4601
+ - Hover states — web only
4566
4602
  - `Skeleton` shimmer highlight — adapts opacity for light/dark mode
4567
4603
 
4568
4604
  ### Dynamic Type
package/CONSUMER.md CHANGED
@@ -32,6 +32,7 @@ npx expo install \
32
32
  expo-haptics \
33
33
  expo-linear-gradient \
34
34
  expo-font \
35
+ expo-image \
35
36
  react-native-reanimated \
36
37
  react-native-gesture-handler \
37
38
  react-native-worklets \
@@ -48,7 +49,7 @@ npx expo install \
48
49
  pressto
49
50
  ```
50
51
 
51
- > `react-native-ease` and `pressto` are hard required. Omitting either crashes every component at module load — you will see a blank screen with no error message on screen.
52
+ > `react-native-ease` and `pressto` are hard required. Omitting either crashes every component at module load — you will see a blank screen with no error message on screen. `expo-image` is required for image-based components (Avatar, MediaCard, ListItem, ImageUpload, ImageViewer).
52
53
 
53
54
  ---
54
55
 
@@ -56,7 +57,6 @@ npx expo install \
56
57
 
57
58
  ```bash
58
59
  npx expo install expo-image-picker # Required only for ImageUpload component
59
- npx expo install react-native-pulsar # Richer haptic feedback (graceful fallback to expo-haptics when absent)
60
60
 
61
61
  # HolographicCard only — deep-import, NOT in main barrel:
62
62
  npx expo install @shopify/react-native-skia expo-sensors
package/DESIGN.md CHANGED
@@ -324,7 +324,7 @@ Shape language is **soft everywhere**. Buttons use `{rounded.md}` (14px) — a f
324
324
  - Sohne type family. Display weights 600–700. Body weight 400. Modest weight is intentional.
325
325
  - 8pt spacing grid with 4pt micro-steps. Section bands at `{spacing.section}` (64px).
326
326
  - Maximum two elevation tiers — flat baseline (95% of surfaces) plus one card float tier.
327
- - Haptics on every interaction via `react-native-pulsar` with `expo-haptics` fallback.
327
+ - Haptics on every interaction via `expo-haptics`.
328
328
  - Press animations via `pressto` — main-thread, gesture-handler worklets. Color transitions via `react-native-reanimated` v4.
329
329
 
330
330
  ## Colors
@@ -602,7 +602,7 @@ All pressables: `enabled={!disabled}`, `rippleColor="transparent"`, `touchSoundD
602
602
 
603
603
  ## Haptic Feedback
604
604
 
605
- All haptics via `src/utils/haptics.ts` — `react-native-pulsar` with `expo-haptics` fallback. Pulsar loads dynamically via `import()` inside a try/catch; absent in Expo Go, graceful fallback.
605
+ All haptics via `src/utils/haptics.ts` — `expo-haptics`.
606
606
 
607
607
  | Function | Use |
608
608
  |---|---|
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  A personal React Native / Expo UI component library with a built-in design system, dark mode support, haptic feedback, and smooth animations.
4
4
 
5
5
  - 54 components across 9 categories (plus the deep-import `HolographicCard`)
6
- - Light/dark theme with 12 public tokens (25 resolved) and full customization
6
+ - Light/dark theme with 12 public tokens (26 resolved) and full customization
7
7
  - Apple HIG–compliant touch targets and haptic feedback
8
8
  - Animated interactions: spring press, sliding tabs, accordion easing, animated progress
9
9
  - Built with TypeScript — full type declarations included
@@ -23,14 +23,16 @@ pnpm add @retray-dev/ui-kit
23
23
  Install these in your app if not already present:
24
24
 
25
25
  ```bash
26
- 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 expo-image-picker
26
+ pnpm add expo-font expo-haptics expo-linear-gradient expo-image 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 expo-image-picker
27
27
  ```
28
28
 
29
29
  For Expo projects, run `npx expo install` instead to get SDK-compatible versions.
30
30
 
31
31
  `pressto` is **required** — it powers the press animations on every interactive component. Omitting it crashes the import. `sonner-native` is **required** for `Toast`.
32
32
 
33
- **Optional:** for richer haptics in a custom dev build (not Expo Go), also `pnpm add react-native-pulsar`. The kit falls back to `expo-haptics` automatically when it is absent. For `ImageUpload`, add `expo-image-picker`. For the deep-import `HolographicCard`, add `@shopify/react-native-skia expo-sensors`.
33
+ **Required:** `expo-image` is required for `Avatar`, `ImageUpload`, `ImageViewer`, `ListItem`, and `MediaCard` image rendering. It replaces React Native's built-in `Image` with caching, blurhash placeholders, and cross-fade transitions.
34
+
35
+ **Optional:** for `ImageUpload`, add `expo-image-picker`. For the deep-import `HolographicCard`, add `@shopify/react-native-skia expo-sensors`.
34
36
 
35
37
  Add the Worklets Babel plugin to `babel.config.js` (required by `@gorhom/bottom-sheet`):
36
38
 
@@ -156,7 +158,7 @@ const { colors, colorScheme } = useTheme()
156
158
 
157
159
  **Public tokens (12 — override these):** `background`, `foreground`, `card`, `primary`, `primaryForeground`, `border`, `destructive`, `destructiveForeground`, `success`, `successForeground`, `warning`, `warningForeground`. Optional: `overlay`, `accent`, `accentForeground`.
158
160
 
159
- **Derived tokens (read-only via `useTheme()`):** `foregroundSubtle`, `foregroundMuted`, `surface`, `surfaceStrong`, `destructiveTint`, `destructiveBorder`, `successTint`, `successBorder`, `warningTint`, `warningBorder`, `ring`, `input`, `separator`, `overlay`, `accentResolved`, `accentForegroundResolved`
161
+ **Derived tokens (read-only via `useTheme()`):** `foregroundSubtle`, `foregroundMuted`, `surface`, `surfaceStrong`, `skeleton`, `destructiveTint`, `destructiveBorder`, `successTint`, `successBorder`, `warningTint`, `warningBorder`, `ring`, `input`, `separator`, `overlay`, `accentResolved`, `accentForegroundResolved`
160
162
 
161
163
  ## Design Tokens
162
164
 
@@ -179,7 +181,8 @@ import { SPACING, ICON_SIZES, RADIUS, SHADOWS, BREAKPOINTS, TYPOGRAPHY } from '@
179
181
 
180
182
  ### Color Utilities
181
183
 
182
- - **`withAlpha(color: string, alpha: number)`** — takes a hex color string (e.g., `"#6366f1"`) and an opacity value (0–1), returns an `rgba()` string. Useful for semi-transparent overlays without adding a separate token.
184
+ - **`withAlpha(hex: string, alpha: number)`** — takes a hex color string (e.g., `"#6366f1"`) and an opacity value (0–1), returns an `rgba()` string. Useful for semi-transparent overlays without adding a separate token.
185
+ - **`hexToRgb(hex: string)`** — returns `{ r, g, b } | null`. Converts hex color to RGB components.
183
186
 
184
187
  ## Components
185
188
 
@@ -187,12 +190,12 @@ import { SPACING, ICON_SIZES, RADIUS, SHADOWS, BREAKPOINTS, TYPOGRAPHY } from '@
187
190
  | ----------- | ----------------------------------------------------------------------------------------------- |
188
191
  | Display | `Text`, `Badge`, `Avatar`, `Separator`, `Spinner`, `Skeleton`, `Progress`, `CurrencyDisplay`, `Stats` |
189
192
  | Surfaces | `Card`, `AlertBanner`, `EmptyState`, `MediaCard`, `PricingCard` |
190
- | Form | `Form` (+ `Form.Field` / `Form.Section` / `Form.Footer`), `Button`, `ButtonGroup`, `IconButton`, `Input`, `CurrencyInput`, `Textarea`, `Checkbox`, `Switch`, `Toggle`, `RadioGroup`, `Select`, `Slider`, `SelectableGrid`, `SheetSelect`, `ImageUpload`, `IconPicker`, `NumberStepper` |
193
+ | Form | `Form` (+ `Form.Field` / `Form.Section` / `Form.Footer`), `Button`, `ButtonGroup`, `IconButton`, `Input`, `CurrencyInput`, `Textarea`, `Checkbox`, `Switch`, `Toggle`, `RadioGroup`, `Select`, `Slider`, `SelectableGrid`, `SelectableCard` (+ `SelectableCardGroup`), `SheetSelect`, `ImageUpload`, `IconPicker`, `NumberStepper` |
191
194
  | Composition | `Tabs`, `Accordion` |
192
195
  | Navigation | `AppHeader`, `TabBar`, `PagerDots` |
193
196
  | Overlays | `Sheet`, `ConfirmDialog`, `ImageViewer` |
194
197
  | Feedback | `Toast` / `ToastProvider` / `useToast` |
195
- | Data | `ListItem`, `ListGroup` (+ `.Header` / `.Footer`), `MenuItem`, `MenuGroup` (+ `.Header` / `.Footer`), `VirtualList`, `Chip` / `ChipGroup`, `LabelValue`, `MonthPicker`, `CategoryStrip`, `DetailRow` |
198
+ | Data | `ListItem`, `ListGroup` (+ `.Header` / `.Footer`), `MenuItem`, `MenuGroup` (+ `.Header` / `.Footer`), `Chip` / `ChipGroup`, `LabelValue`, `MonthPicker`, `CategoryStrip`, `DetailRow` |
196
199
  | Utilities | `Pressable`, `Icon`, `RetrayProvider`, `ErrorBoundary` |
197
200
 
198
201
  Deep-import only: `HolographicCard` — `import { HolographicCard } from '@retray-dev/ui-kit/HolographicCard'`.
@@ -226,9 +229,11 @@ toast({ title: 'Saved', variant: 'success' })
226
229
  />
227
230
 
228
231
  // Color utilities
229
- import { useTheme, withAlpha } from '@retray-dev/ui-kit'
232
+ import { useTheme, withAlpha, hexToRgb } from '@retray-dev/ui-kit'
230
233
  const { colors } = useTheme()
231
234
  ;<View style={{ backgroundColor: withAlpha(colors.primary, 0.15) }} />
235
+ // hexToRgb — converts hex to RGB components
236
+ const rgb = hexToRgb('#6366f1') // { r: 99, g: 102, b: 241 }
232
237
  ```
233
238
 
234
239
  Full props reference and more examples are available in [COMPONENTS.md](./COMPONENTS.md), which is also shipped inside the npm package for use with AI tools:
@@ -11,6 +11,12 @@ interface AccordionItem {
11
11
  icon?: React.ReactNode;
12
12
  /** Override icon color. Defaults to foregroundMuted. */
13
13
  iconColor?: string;
14
+ /**
15
+ * Action buttons rendered after the trigger content but before the chevron.
16
+ * Automatically touch-isolated — taps on actions won't toggle the accordion.
17
+ * Use this instead of embedding interactive elements inside `trigger`.
18
+ */
19
+ triggerActions?: React.ReactNode;
14
20
  }
15
21
  interface AccordionProps {
16
22
  items: AccordionItem[];
@@ -11,6 +11,12 @@ interface AccordionItem {
11
11
  icon?: React.ReactNode;
12
12
  /** Override icon color. Defaults to foregroundMuted. */
13
13
  iconColor?: string;
14
+ /**
15
+ * Action buttons rendered after the trigger content but before the chevron.
16
+ * Automatically touch-isolated — taps on actions won't toggle the accordion.
17
+ * Use this instead of embedding interactive elements inside `trigger`.
18
+ */
19
+ triggerActions?: React.ReactNode;
14
20
  }
15
21
  interface AccordionProps {
16
22
  items: AccordionItem[];