@retray-dev/ui-kit 10.2.0 → 12.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 (220) hide show
  1. package/COMPONENTS.md +384 -40
  2. package/README.md +14 -5
  3. package/dist/Accordion.d.mts +6 -0
  4. package/dist/Accordion.d.ts +6 -0
  5. package/dist/Accordion.js +16 -0
  6. package/dist/Accordion.mjs +2 -2
  7. package/dist/AlertBanner.js +2 -0
  8. package/dist/AlertBanner.mjs +2 -2
  9. package/dist/AppHeader.js +2 -0
  10. package/dist/AppHeader.mjs +3 -3
  11. package/dist/Avatar.js +2 -0
  12. package/dist/Avatar.mjs +2 -2
  13. package/dist/Badge.js +2 -0
  14. package/dist/Badge.mjs +2 -2
  15. package/dist/Button.js +17 -17
  16. package/dist/Button.mjs +2 -2
  17. package/dist/Card.js +2 -0
  18. package/dist/Card.mjs +2 -2
  19. package/dist/CategoryStrip.js +2 -0
  20. package/dist/CategoryStrip.mjs +2 -2
  21. package/dist/Checkbox.js +2 -0
  22. package/dist/Checkbox.mjs +2 -2
  23. package/dist/Chip.js +2 -0
  24. package/dist/Chip.mjs +2 -2
  25. package/dist/ConfirmDialog.d.mts +1 -6
  26. package/dist/ConfirmDialog.d.ts +1 -6
  27. package/dist/ConfirmDialog.js +53 -41
  28. package/dist/ConfirmDialog.mjs +3 -3
  29. package/dist/CurrencyDisplay.js +2 -0
  30. package/dist/CurrencyDisplay.mjs +2 -2
  31. package/dist/CurrencyInput.d.mts +3 -8
  32. package/dist/CurrencyInput.d.ts +3 -8
  33. package/dist/CurrencyInput.js +5 -1
  34. package/dist/CurrencyInput.mjs +3 -3
  35. package/dist/DetailRow.js +2 -0
  36. package/dist/DetailRow.mjs +2 -2
  37. package/dist/EmptyState.js +17 -17
  38. package/dist/EmptyState.mjs +3 -3
  39. package/dist/ErrorBoundary.js +2 -0
  40. package/dist/ErrorBoundary.mjs +2 -2
  41. package/dist/Form.js +2 -0
  42. package/dist/Form.mjs +2 -2
  43. package/dist/IconButton.js +2 -0
  44. package/dist/IconButton.mjs +2 -2
  45. package/dist/IconPicker.js +677 -248
  46. package/dist/IconPicker.mjs +3 -2
  47. package/dist/ImageUpload.d.mts +3 -1
  48. package/dist/ImageUpload.d.ts +3 -1
  49. package/dist/ImageUpload.js +10 -3
  50. package/dist/ImageUpload.mjs +3 -3
  51. package/dist/ImageViewer.js +2 -0
  52. package/dist/ImageViewer.mjs +4 -4
  53. package/dist/Input.js +2 -0
  54. package/dist/Input.mjs +2 -2
  55. package/dist/LabelValue.js +2 -0
  56. package/dist/LabelValue.mjs +2 -2
  57. package/dist/ListGroup.js +2 -0
  58. package/dist/ListGroup.mjs +2 -2
  59. package/dist/ListItem.d.mts +7 -7
  60. package/dist/ListItem.d.ts +7 -7
  61. package/dist/ListItem.js +14 -7
  62. package/dist/ListItem.mjs +2 -2
  63. package/dist/MediaCard.js +2 -0
  64. package/dist/MediaCard.mjs +2 -2
  65. package/dist/MenuGroup.js +2 -0
  66. package/dist/MenuGroup.mjs +2 -2
  67. package/dist/MenuItem.js +2 -0
  68. package/dist/MenuItem.mjs +2 -2
  69. package/dist/MonthPicker.js +2 -0
  70. package/dist/MonthPicker.mjs +2 -2
  71. package/dist/NumberStepper.js +2 -0
  72. package/dist/NumberStepper.mjs +2 -2
  73. package/dist/PagerDots.js +2 -0
  74. package/dist/PagerDots.mjs +2 -2
  75. package/dist/Pressable.d.mts +15 -7
  76. package/dist/Pressable.d.ts +15 -7
  77. package/dist/Pressable.js +7 -3
  78. package/dist/Pressable.mjs +1 -1
  79. package/dist/PricingCard.js +17 -17
  80. package/dist/PricingCard.mjs +4 -4
  81. package/dist/Progress.js +2 -0
  82. package/dist/Progress.mjs +2 -2
  83. package/dist/RadioGroup.js +2 -0
  84. package/dist/RadioGroup.mjs +2 -2
  85. package/dist/RetrayProvider.d.mts +1 -1
  86. package/dist/RetrayProvider.d.ts +1 -1
  87. package/dist/RetrayProvider.js +2 -0
  88. package/dist/RetrayProvider.mjs +3 -3
  89. package/dist/Select.js +2 -0
  90. package/dist/Select.mjs +2 -2
  91. package/dist/SelectableCard.d.mts +27 -0
  92. package/dist/SelectableCard.d.ts +27 -0
  93. package/dist/SelectableCard.js +511 -0
  94. package/dist/SelectableCard.mjs +8 -0
  95. package/dist/SelectableGrid.js +2 -0
  96. package/dist/SelectableGrid.mjs +2 -2
  97. package/dist/Separator.js +2 -0
  98. package/dist/Separator.mjs +2 -2
  99. package/dist/Sheet.d.mts +4 -46
  100. package/dist/Sheet.d.ts +4 -46
  101. package/dist/Sheet.js +55 -115
  102. package/dist/Sheet.mjs +2 -3
  103. package/dist/SheetSelect.js +2 -0
  104. package/dist/SheetSelect.mjs +2 -2
  105. package/dist/Skeleton.d.mts +3 -1
  106. package/dist/Skeleton.d.ts +3 -1
  107. package/dist/Skeleton.js +5 -2
  108. package/dist/Skeleton.mjs +2 -2
  109. package/dist/Slider.js +2 -0
  110. package/dist/Slider.mjs +2 -2
  111. package/dist/Spinner.js +2 -0
  112. package/dist/Spinner.mjs +2 -2
  113. package/dist/Stats.d.mts +33 -0
  114. package/dist/Stats.d.ts +33 -0
  115. package/dist/Stats.js +453 -0
  116. package/dist/Stats.mjs +9 -0
  117. package/dist/Switch.js +2 -0
  118. package/dist/Switch.mjs +2 -2
  119. package/dist/TabBar.js +2 -0
  120. package/dist/TabBar.mjs +2 -2
  121. package/dist/Tabs.js +2 -0
  122. package/dist/Tabs.mjs +2 -2
  123. package/dist/Text.d.mts +3 -1
  124. package/dist/Text.d.ts +3 -1
  125. package/dist/Text.js +5 -3
  126. package/dist/Text.mjs +2 -2
  127. package/dist/Textarea.js +2 -0
  128. package/dist/Textarea.mjs +2 -2
  129. package/dist/Toast.js +2 -0
  130. package/dist/Toast.mjs +2 -2
  131. package/dist/Toggle.js +2 -0
  132. package/dist/Toggle.mjs +2 -2
  133. package/dist/{chunk-U2XJFYED.mjs → chunk-2BA3JMKK.mjs} +1 -1
  134. package/dist/{chunk-NMU5FMQJ.mjs → chunk-2HFD4IHU.mjs} +4 -2
  135. package/dist/{chunk-S2R7UVOE.mjs → chunk-2LG326TT.mjs} +1 -1
  136. package/dist/chunk-2P2CB235.mjs +236 -0
  137. package/dist/{chunk-6L4G6PBT.mjs → chunk-3XCFYSX4.mjs} +1 -1
  138. package/dist/{chunk-HTHGSXFG.mjs → chunk-4J2PXL36.mjs} +16 -18
  139. package/dist/{chunk-BEMIQXXU.mjs → chunk-4OORJ2DY.mjs} +1 -1
  140. package/dist/chunk-4XOB5TTD.mjs +166 -0
  141. package/dist/{chunk-FCSSQK3L.mjs → chunk-57V2LXCK.mjs} +1 -1
  142. package/dist/{chunk-6Q64UFIA.mjs → chunk-7AFZWSCI.mjs} +1 -1
  143. package/dist/{chunk-IX3NYLYQ.mjs → chunk-7ELGZ66G.mjs} +1 -1
  144. package/dist/{chunk-GD6KXMG5.mjs → chunk-AENAVIKT.mjs} +1 -1
  145. package/dist/{chunk-ID72TK46.mjs → chunk-BXF4AMHY.mjs} +1 -1
  146. package/dist/{chunk-SOA2Z4RB.mjs → chunk-C43HRKXH.mjs} +1 -1
  147. package/dist/{chunk-TZDGAP5N.mjs → chunk-CF27NBXO.mjs} +11 -6
  148. package/dist/{chunk-SXLKNTA4.mjs → chunk-DF7JA72E.mjs} +1 -1
  149. package/dist/{chunk-AJRVDP2H.mjs → chunk-E5UKLSJZ.mjs} +3 -3
  150. package/dist/{chunk-MBMXYJJV.mjs → chunk-E7NEHHXV.mjs} +7 -3
  151. package/dist/{chunk-VKID2D2I.mjs → chunk-EDLCGYIO.mjs} +13 -8
  152. package/dist/{chunk-BUMAMSTZ.mjs → chunk-ELGEOM7I.mjs} +1 -1
  153. package/dist/{chunk-DYT7BG5I.mjs → chunk-F3YTWO3T.mjs} +1 -1
  154. package/dist/{chunk-VF2ATYN3.mjs → chunk-GH67YXG6.mjs} +1 -1
  155. package/dist/{chunk-WJLKJMKR.mjs → chunk-GUTDFUNF.mjs} +4 -4
  156. package/dist/{chunk-6SECQ2ZF.mjs → chunk-HC4VVCWY.mjs} +2 -2
  157. package/dist/{chunk-A3A6KNQN.mjs → chunk-HEDQPK4I.mjs} +1 -1
  158. package/dist/{chunk-GQYFLP3D.mjs → chunk-IVSRW4HS.mjs} +1 -1
  159. package/dist/{chunk-KOO4WITD.mjs → chunk-KSUWPU2F.mjs} +1 -1
  160. package/dist/{chunk-WBOOUHSS.mjs → chunk-LIS6I5UP.mjs} +1 -1
  161. package/dist/{chunk-X4G6APW6.mjs → chunk-LNPKGWBG.mjs} +1 -1
  162. package/dist/{chunk-T2KCAHOS.mjs → chunk-LOBLCFMN.mjs} +1 -1
  163. package/dist/{chunk-ELXBDILQ.mjs → chunk-LPV4NJJK.mjs} +2 -2
  164. package/dist/{chunk-Y2NS74WS.mjs → chunk-M3C7XM2M.mjs} +53 -99
  165. package/dist/{chunk-BRKYVJVV.mjs → chunk-MEPSKGBO.mjs} +1 -1
  166. package/dist/{chunk-TBNZHU6C.mjs → chunk-MVMGPZN6.mjs} +2 -2
  167. package/dist/{chunk-YJ7I257J.mjs → chunk-NHDI3VQB.mjs} +15 -1
  168. package/dist/{chunk-Z6SFHN6T.mjs → chunk-NJG7DHVF.mjs} +1 -1
  169. package/dist/{chunk-RYZC432S.mjs → chunk-NLZY4TXU.mjs} +1 -1
  170. package/dist/{chunk-ZZ2R6KZ3.mjs → chunk-OLVJFKXS.mjs} +1 -1
  171. package/dist/{chunk-AJ7ZDNBT.mjs → chunk-QDAZGZUF.mjs} +4 -3
  172. package/dist/{chunk-JT7HKXRB.mjs → chunk-QOLWA2PW.mjs} +1 -1
  173. package/dist/{chunk-WYEUNUTP.mjs → chunk-QXDGGOLC.mjs} +38 -25
  174. package/dist/{chunk-JMOZEC77.mjs → chunk-RJNLAH76.mjs} +1 -1
  175. package/dist/{chunk-WF2XDFRK.mjs → chunk-RMRS44MQ.mjs} +1 -1
  176. package/dist/chunk-SAWUXP3A.mjs +1114 -0
  177. package/dist/{chunk-OB4JUQ3O.mjs → chunk-TS7DGUIR.mjs} +1 -1
  178. package/dist/{chunk-AV4EMIRH.mjs → chunk-UBUXUMER.mjs} +1 -1
  179. package/dist/{chunk-IRRY3CRZ.mjs → chunk-ULGNQPNE.mjs} +1 -1
  180. package/dist/{chunk-7LWRKMF5.mjs → chunk-UNNRUJTM.mjs} +1 -1
  181. package/dist/{chunk-TB6SD2FT.mjs → chunk-UQ4742ET.mjs} +1 -1
  182. package/dist/{chunk-MX6HRKMI.mjs → chunk-VJBUCITV.mjs} +1 -1
  183. package/dist/{chunk-2UYENBLV.mjs → chunk-YMYIEVZP.mjs} +1 -1
  184. package/dist/{chunk-SOYNZDVY.mjs → chunk-YTXRIXNZ.mjs} +8 -1
  185. package/dist/{chunk-YFZ3ELX5.mjs → chunk-ZIMY2QUM.mjs} +2 -2
  186. package/dist/{chunk-Z4VHZ7B5.mjs → chunk-ZR6HSEAB.mjs} +1 -1
  187. package/dist/fonts.d.mts +1 -7
  188. package/dist/fonts.d.ts +1 -7
  189. package/dist/fonts.js +0 -2
  190. package/dist/fonts.mjs +1 -2
  191. package/dist/{index-wt-orHUi.d.ts → index-CY34hxPN.d.mts} +1 -0
  192. package/dist/{index-wt-orHUi.d.mts → index-CY34hxPN.d.ts} +1 -0
  193. package/dist/index.d.mts +7 -3
  194. package/dist/index.d.ts +7 -3
  195. package/dist/index.js +1517 -761
  196. package/dist/index.mjs +54 -52
  197. package/package.json +3 -3
  198. package/src/components/Accordion/Accordion.tsx +20 -0
  199. package/src/components/Button/Button.tsx +29 -26
  200. package/src/components/ConfirmDialog/ConfirmDialog.tsx +47 -31
  201. package/src/components/CurrencyInput/CurrencyInput.tsx +4 -7
  202. package/src/components/IconPicker/IconPicker.tsx +124 -112
  203. package/src/components/ImageUpload/ImageUpload.tsx +10 -3
  204. package/src/components/ListItem/ListItem.tsx +43 -28
  205. package/src/components/Pressable/Pressable.tsx +20 -8
  206. package/src/components/SelectableCard/SelectableCard.tsx +304 -0
  207. package/src/components/SelectableCard/index.ts +1 -0
  208. package/src/components/Sheet/Sheet.tsx +72 -173
  209. package/src/components/Skeleton/Skeleton.tsx +5 -2
  210. package/src/components/Stats/Stats.tsx +254 -0
  211. package/src/components/Stats/index.ts +2 -0
  212. package/src/components/Text/Text.tsx +4 -2
  213. package/src/fonts.ts +0 -7
  214. package/src/index.ts +5 -0
  215. package/src/theme/colorUtils.ts +9 -0
  216. package/src/theme/colors.ts +7 -0
  217. package/src/theme/types.ts +4 -1
  218. package/src/utils/curatedIcons.ts +698 -135
  219. package/src/utils/fontGuard.ts +2 -1
  220. package/dist/chunk-53Z3NYGE.mjs +0 -742
package/COMPONENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- # @retray-dev/ui-kit — Component Reference (v10.2.0)
1
+ # @retray-dev/ui-kit — Component Reference (v12.1.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
 
@@ -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,25 @@ 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
318
+ ```
319
+
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>
316
336
  ```
317
337
 
318
338
  ---
@@ -684,6 +704,132 @@ assets/fonts/sohne/
684
704
 
685
705
  ---
686
706
 
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.
712
+
713
+ ```tsx
714
+ import { useTheme, withAlpha } from '@retray-dev/ui-kit'
715
+
716
+ const { colors } = useTheme()
717
+ <View style={{ backgroundColor: withAlpha(colors.primary, 0.15) }} />
718
+ ```
719
+
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.
726
+
727
+ ---
728
+
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.
734
+
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.
752
+
753
+ **3. `Sheet` requires `BottomSheetModalProvider` at app root**
754
+
755
+ `RetrayProvider` already wires it. If you assemble providers manually, ensure `BottomSheetModalProvider` sits inside `GestureHandlerRootView`:
756
+
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
+ ```
768
+
769
+ **4. Keyboard prop renames (no consumer action — defaults match v11)**
770
+
771
+ | Prop | v11 | v12 default |
772
+ |------|-----|-------------|
773
+ | `keyboardBehavior` | `'interactive'` | `'interactive'` (unchanged) |
774
+ | `android_keyboardInputMode` | `'adjustPan'` | `'adjustPan'` (unchanged) |
775
+
776
+ **5. Removed top-level imports of `Modal`, `ScrollView`, `useWindowDimensions`, `BREAKPOINTS` from `Sheet.tsx`**
777
+
778
+ Internal only — no public API.
779
+
780
+ ### Behavioral changes (no code change required)
781
+
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).
785
+
786
+ ### New — recommended pattern
787
+
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").
789
+
790
+ ```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>
801
+ ```
802
+
803
+ `SheetTextInput` is still re-exported for low-level use.
804
+
805
+ ---
806
+
807
+ ## Migration Guide: v12.0 → v12.1
808
+
809
+ ### New — IconPicker feedback pattern (REGLA 4)
810
+
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.
820
+
821
+ ### New — expanded curated icon library
822
+
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).
824
+
825
+ ### Updated
826
+
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.
830
+
831
+ ---
832
+
687
833
  ## Components
688
834
 
689
835
  ---
@@ -700,6 +846,7 @@ assets/fonts/sohne/
700
846
  |------|------|---------|-------|
701
847
  | variant | `TextVariant` | `'body-md'` | Sets font, size, weight, line height, letter spacing |
702
848
  | color | `string` | — | Override text color. Each variant has a semantic default (see table below) |
849
+ | uppercase | `boolean` | `false` | Force text-transform: uppercase on any variant |
703
850
  | children | `ReactNode` | required | — |
704
851
  | style | `TextStyle` | — | Additional styles (merged after variant styles) |
705
852
  | (all TextProps) | — | — | `numberOfLines`, `ellipsizeMode`, `onPress`, etc. all pass through |
@@ -772,7 +919,7 @@ assets/fonts/sohne/
772
919
  | size | `'sm' \| 'md' \| 'lg'` | `'md'` | Controls height, padding, and icon size |
773
920
  | loading | `boolean` | `false` | Replaces label with spinner, forces disabled state |
774
921
  | fullWidth | `boolean` | `false` | Stretches to container width (`alignSelf: 'stretch'`) |
775
- | 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 |
776
923
  | icon | `React.ReactNode \| ((props: { label, size, variant }) => React.ReactNode)` | — | Icon rendered alongside label |
777
924
  | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
778
925
  | iconColor | `string` | — | Override icon color. Defaults to variant label color |
@@ -1073,6 +1220,23 @@ assets/fonts/sohne/
1073
1220
  </View>
1074
1221
  ```
1075
1222
 
1223
+ **Composition — inside a Sheet (preferred pattern, v12+):**
1224
+
1225
+ Use `sheetMode` to opt the inner `TextInput` into `BottomSheetTextInput` so keyboard handling works correctly. This is the canonical replacement for raw `SheetTextInput` inside sheets.
1226
+
1227
+ ```tsx
1228
+ <Sheet open={open} onClose={() => setOpen(false)} title="Add note">
1229
+ <Input
1230
+ label="Note"
1231
+ placeholder="Write your note..."
1232
+ value={note}
1233
+ onChangeText={setNote}
1234
+ sheetMode
1235
+ />
1236
+ <Button label="Save" fullWidth onPress={handleSave} />
1237
+ </Sheet>
1238
+ ```
1239
+
1076
1240
  ---
1077
1241
 
1078
1242
  ### Textarea
@@ -1125,9 +1289,12 @@ assets/fonts/sohne/
1125
1289
  | hint | `string` | — | Helper text below (hidden when `error` is set) |
1126
1290
  | placeholder | `string` | `'$0'` | Defaults to `prefix + '0'` |
1127
1291
  | editable | `boolean` | — | Pass `false` to disable |
1292
+ | autoFocus | `boolean` | — | Auto-focus on mount (inherited from TextInputProps) |
1128
1293
  | containerStyle | `ViewStyle` | — | Outer container style |
1129
1294
  | sheetMode | `boolean` | `false` | Use inside a Sheet — forwards `sheetMode` to underlying `Input` |
1130
1295
 
1296
+ **Extends `TextInputProps` from React Native** — all TextInput props (autoFocus, onFocus, onBlur, etc.) pass through to the underlying input.
1297
+
1131
1298
  **Formatting behavior:** Strips all non-digit characters from input, then re-applies thousands separator every 3 digits from the right, then prepends `prefix`. Decimal point input is not supported — this is for whole-number monetary amounts.
1132
1299
 
1133
1300
  **Examples:**
@@ -1737,6 +1904,7 @@ const renderItem = useCallback(({ item }) => (
1737
1904
  | width | `number \| string` | `'100%'` | Width of placeholder |
1738
1905
  | height | `number` | `16` | Height of placeholder |
1739
1906
  | borderRadius | `number` | `6` | Corner radius |
1907
+ | backgroundColor | `string` | `colors.skeleton` | Override placeholder background color |
1740
1908
  | preset | `'base' \| 'circle' \| 'text'` | `'base'` | Convenience preset |
1741
1909
  | diameter | `number` | `40` | Used by `'circle'` preset — overrides width/height |
1742
1910
  | style | `ViewStyle` | — | — |
@@ -2184,6 +2352,94 @@ const [accepted, setAccepted] = useState(false)
2184
2352
 
2185
2353
  ---
2186
2354
 
2355
+ ### SelectableCard
2356
+
2357
+ **Import:** `import { SelectableCard, SelectableCardGroup } from '@retray-dev/ui-kit'`
2358
+
2359
+ **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.
2360
+
2361
+ | Prop | Type | Default | Notes |
2362
+ |------|------|---------|-------|
2363
+ | value | `string` | required | The value this card represents |
2364
+ | title | `string` | required | Card title |
2365
+ | description | `string` | — | Secondary text below title |
2366
+ | iconName | `string` | — | Left icon from @expo/vector-icons |
2367
+ | icon | `ReactNode` | — | Custom left icon |
2368
+ | disabled | `boolean` | `false` | Grayed out, no press/haptic |
2369
+ | style | `ViewStyle` | — | — |
2370
+
2371
+ **SelectableCardGroup Props:**
2372
+
2373
+ | Prop | Type | Default | Notes |
2374
+ |------|------|---------|-------|
2375
+ | type | `'radio' \| 'checkbox'` | `'radio'` | Selection mode |
2376
+ | value | `string \| string[]` | required | Selected value(s) |
2377
+ | onValueChange | `(value: string \| string[]) => void` | required | — |
2378
+ | variant | `'elevated' \| 'outlined' \| 'filled'` | `'elevated'` | Card surface |
2379
+ | gap | `number` | `s(8)` | Spacing between cards |
2380
+ | style | `ViewStyle` | — | — |
2381
+
2382
+ **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.
2383
+
2384
+ **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`.
2385
+
2386
+ **Haptics:** `impactLight` on selection.
2387
+
2388
+ **Disabled:** Dims content to `foregroundMuted`, removes shadow/elevation, keeps border at `colors.border`. No press, no haptic.
2389
+
2390
+ **Examples:**
2391
+ ```tsx
2392
+ // Single select (radio)
2393
+ <SelectableCardGroup
2394
+ type="radio"
2395
+ value={selectedRole}
2396
+ onValueChange={setSelectedRole}
2397
+ >
2398
+ <SelectableCard
2399
+ value="admin"
2400
+ iconName="shield"
2401
+ title="Administrator"
2402
+ description="Full access to reports, settings, and business management"
2403
+ />
2404
+ <SelectableCard
2405
+ value="waiter"
2406
+ iconName="user-check"
2407
+ title="Waiter"
2408
+ description="Take orders from tables and manage requests"
2409
+ />
2410
+ <SelectableCard
2411
+ value="cook"
2412
+ iconName="chef-hat"
2413
+ title="Cook"
2414
+ description="Receive and prepare items from each order in the kitchen"
2415
+ disabled
2416
+ />
2417
+ </SelectableCardGroup>
2418
+
2419
+ // Multi select (checkbox)
2420
+ <SelectableCardGroup
2421
+ type="checkbox"
2422
+ value={selectedRoles}
2423
+ onValueChange={setSelectedRoles}
2424
+ variant="outlined"
2425
+ >
2426
+ <SelectableCard
2427
+ value="reports"
2428
+ iconName="bar-chart-2"
2429
+ title="Reports"
2430
+ description="View sales, inventory, and performance analytics"
2431
+ />
2432
+ <SelectableCard
2433
+ value="inventory"
2434
+ iconName="package"
2435
+ title="Inventory"
2436
+ description="Manage stock levels and supplier orders"
2437
+ />
2438
+ </SelectableCardGroup>
2439
+ ```
2440
+
2441
+ ---
2442
+
2187
2443
  ### Slider
2188
2444
 
2189
2445
  **Import:** `import { Slider } from '@retray-dev/ui-kit'`
@@ -2345,6 +2601,7 @@ const [tab, setTab] = useState('profile')
2345
2601
  iconName?: string // Icon name from @expo/vector-icons
2346
2602
  icon?: ReactNode // Custom icon node
2347
2603
  iconColor?: string // Override icon color (defaults to foregroundMuted)
2604
+ triggerActions?: ReactNode // Action buttons rendered between trigger and chevron. Touch-isolated — taps on actions won't toggle the accordion.
2348
2605
  }
2349
2606
  ```
2350
2607
 
@@ -2400,7 +2657,6 @@ Add `react-native-worklets/plugin` (not `react-native-reanimated/plugin`) to `ba
2400
2657
  | onClose | `() => void` | required | Called on swipe-dismiss or backdrop press |
2401
2658
  | title | `string` | — | Sheet heading |
2402
2659
  | subtitle | `string` | — | Supporting text below title |
2403
- | description | `string` | — | **Deprecated alias** for `subtitle` — prefer `subtitle` |
2404
2660
  | showCloseButton | `boolean` | `false` | Show X close button in the header |
2405
2661
  | children | `ReactNode` | — | Sheet content |
2406
2662
  | style | `ViewStyle` | — | Inner scroll/content container style |
@@ -2413,14 +2669,11 @@ Add `react-native-worklets/plugin` (not `react-native-reanimated/plugin`) to `ba
2413
2669
  | android_keyboardInputMode | `'adjustPan' \| 'adjustResize'` | `'adjustPan'` | Android-only: `'adjustPan'` moves the window (default — fixes restore issues with dynamic sizing). `'adjustResize'` resizes the container (can cause transparent gap when keyboard dismisses) |
2414
2670
  | footer | `ReactNode` | — | Sticky footer below scroll area (sticky above keyboard) |
2415
2671
  | snapPoints | `(string \| number)[]` | — | Optional snap points (e.g., `['50%', '85%']`). When omitted, uses dynamic sizing (auto-fits content) |
2416
- | responsive | `boolean` | `false` | **(v8)** On wide screens (width ≥ `BREAKPOINTS.wide`) render a centered modal dialog instead of a bottom sheet. Stays a bottom sheet on phones |
2417
- | dialogMaxWidth | `number` | `480` | Max width of the centered dialog. Only applies when `responsive` |
2418
2672
 
2419
2673
  **Features:**
2420
2674
  - `enableDynamicSizing` — height auto-fits content, no `snapPoints` needed (default behavior when `snapPoints` is omitted)
2421
2675
  - `snapPoints` — optionally provide custom snap points (e.g., `['50%', '85%']`). Disables dynamic sizing when provided
2422
- - `responsive` — on tablets/web, becomes a centered dialog (max width `dialogMaxWidth`). Note: the dialog path uses a plain RN `Modal`, so `SheetTextInput` is **not** required there — use a regular `TextInput`
2423
- - `enablePanDownToClose` — swipe down to dismiss
2676
+ - `enablePanDownToClose` — swipe down to dismiss (always enabled)
2424
2677
  - Backdrop press dismisses
2425
2678
  - **Scrollable content:** use `scrollable` prop or `maxHeight`. Both use `BottomSheetScrollView` — do NOT use plain `ScrollView` inside Sheet
2426
2679
  - **Keyboard handling:** Full keyboard awareness via `@gorhom/bottom-sheet` v5. Professional defaults:
@@ -2429,7 +2682,7 @@ Add `react-native-worklets/plugin` (not `react-native-reanimated/plugin`) to `ba
2429
2682
  - `keyboardBlurBehavior="restore"` — returns to pre-keyboard position when keyboard dismisses
2430
2683
  - `enableBlurKeyboardOnGesture={true}` — dismisses keyboard when dragging sheet down
2431
2684
  - `topInset` — automatically applied from safe area context to prevent going above notch
2432
- - **Text inputs inside sheet:** **MUST use `SheetTextInput`** (re-exported `BottomSheetTextInput`) handles focus/blur internally, communicates with sheet's keyboard system. **Never use regular `TextInput`** inside sheets — keyboard handling will break
2685
+ - **Text inputs inside sheet:** use `<Input sheetMode />` — it transparently swaps in `BottomSheetTextInput` for keyboard-aware focus/blur handling, while preserving the full `Input` API (label, error, hint, prefix/suffix, icons, type="password"). For low-level access, `SheetTextInput` (re-exported `BottomSheetTextInput`) is also available. **Never use regular `<TextInput>`** inside sheets — keyboard handling will break.
2433
2686
  - **Custom TextInput:** If using custom input components, you must copy `handleOnFocus`/`handleOnBlur` from [BottomSheetTextInput source](https://github.com/gorhom/react-native-bottom-sheet/blob/master/src/components/bottomSheetTextInput/BottomSheetTextInput.tsx)
2434
2687
 
2435
2688
  **Haptics:** `impactMedium` on open.
@@ -2448,23 +2701,39 @@ const [open, setOpen] = useState(false)
2448
2701
  {items.map((r) => <ListItem key={r.id} title={r.name} showSeparator />)}
2449
2702
  </Sheet>
2450
2703
 
2451
- // With text input — keyboard handling works automatically (platform-optimized defaults)
2452
- <Sheet
2453
- open={open}
2454
- onClose={() => setOpen(false)}
2704
+ // With text input — recommended pattern (v12+): Input sheetMode
2705
+ <Sheet
2706
+ open={open}
2707
+ onClose={() => setOpen(false)}
2708
+ title="Add note"
2709
+ subtitle="Keyboard handling is automatic with platform-optimized defaults"
2710
+ >
2711
+ <Input
2712
+ label="Note"
2713
+ placeholder="Write your note..."
2714
+ value={note}
2715
+ onChangeText={setNote}
2716
+ sheetMode
2717
+ />
2718
+ <Button label="Save" fullWidth onPress={handleSave} />
2719
+ </Sheet>
2720
+
2721
+ // Low-level alternative: SheetTextInput
2722
+ <Sheet
2723
+ open={open}
2724
+ onClose={() => setOpen(false)}
2455
2725
  title="Add note"
2456
- subtitle="Keyboard handling is automatic with platform-optimized defaults"
2457
2726
  >
2458
- <SheetTextInput
2459
- placeholder="Write your note..."
2727
+ <SheetTextInput
2728
+ placeholder="Write your note..."
2460
2729
  multiline
2461
- style={{
2462
- borderWidth: 1,
2730
+ style={{
2731
+ borderWidth: 1,
2463
2732
  borderColor: '#ddd',
2464
2733
  borderRadius: 8,
2465
2734
  padding: 12,
2466
2735
  minHeight: 80,
2467
- }}
2736
+ }}
2468
2737
  />
2469
2738
  <Button label="Save" fullWidth style={{ marginTop: 12 }} onPress={() => setOpen(false)} />
2470
2739
  </Sheet>
@@ -2493,7 +2762,7 @@ const [open, setOpen] = useState(false)
2493
2762
 
2494
2763
  **Keyboard handling notes:**
2495
2764
  - No `KeyboardAvoidingView` needed — `@gorhom/bottom-sheet` handles everything
2496
- - Always use `SheetTextInput` (not plain `TextInput`) for auto-focus/blur handling
2765
+ - Use `<Input sheetMode />` (preferred) or `SheetTextInput` (low-level) for text inputs inside the sheet
2497
2766
  - Default `keyboardBehavior="interactive"` works on both platforms
2498
2767
  - Default `android_keyboardInputMode="adjustPan"` fixes the transparent gap that occurs with `adjustResize` when keyboard dismisses
2499
2768
  - `enableBlurKeyboardOnGesture={true}` (default) dismisses keyboard when dragging sheet
@@ -2757,7 +3026,6 @@ dismiss(id)
2757
3026
  | visible | `boolean` | required | Controls dialog visibility |
2758
3027
  | title | `string` | required | Dialog heading |
2759
3028
  | subtitle | `string` | — | Secondary text below title |
2760
- | description | `string` | — | **Deprecated** — use `subtitle` instead |
2761
3029
  | confirmLabel | `string` | `'Confirm'` | Confirm button text |
2762
3030
  | cancelLabel | `string` | `'Cancel'` | Cancel button text |
2763
3031
  | confirmVariant | `'primary' \| 'destructive'` | `'primary'` | Use `'destructive'` for delete/remove actions |
@@ -2767,12 +3035,15 @@ dismiss(id)
2767
3035
  | onCancel | `() => void` | required | Called when cancel is tapped or backdrop pressed |
2768
3036
 
2769
3037
  **Notes:**
2770
- - Powered by `@gorhom/bottom-sheet` with `enableDynamicSizing`
3038
+ - Powered by `@gorhom/bottom-sheet` `BottomSheetModal` with `enableDynamicSizing` (v12 refactor — `present()` / `dismiss()` driven, lazy-mounted, no `index={-1}` + `snapToIndex(0)` timing traps)
3039
+ - `topInset={insets.top}` applied automatically — dialog never crosses the notch / status bar
3040
+ - Internal `wasOpened` ref prevents `dismiss()` being called before the sheet is mounted (race-condition fix from v12.1)
3041
+ - A stable `name` prop (from `useId()`) is passed to `BottomSheetModal` for correct gorhom modal registry behavior when multiple modals are open
2771
3042
  - Buttons are full-width, stacked vertically (confirm on top)
2772
3043
  - Cancel shows X icon, confirm shows check (primary) or trash-2 (destructive) icon
2773
3044
  - Swipe down or backdrop press calls `onCancel`
2774
3045
 
2775
- **Haptics:** `impactLight` on open.
3046
+ **Haptics:** `impactMedium` on open.
2776
3047
 
2777
3048
  **Examples:**
2778
3049
  ```tsx
@@ -2780,7 +3051,7 @@ dismiss(id)
2780
3051
  <ConfirmDialog
2781
3052
  visible={showDelete}
2782
3053
  title="Delete transaction?"
2783
- description="$45.000 · Mercado · 12 mar · This action cannot be undone."
3054
+ subtitle="$45.000 · Mercado · 12 mar · This action cannot be undone."
2784
3055
  confirmLabel="Delete"
2785
3056
  confirmVariant="destructive"
2786
3057
  onConfirm={handleDelete}
@@ -2791,7 +3062,7 @@ dismiss(id)
2791
3062
  <ConfirmDialog
2792
3063
  visible={showConfirm}
2793
3064
  title="Send payment?"
2794
- description={`Send $${amount} to ${recipientName}`}
3065
+ subtitle={`Send $${amount} to ${recipientName}`}
2795
3066
  confirmLabel="Send"
2796
3067
  onConfirm={handleSend}
2797
3068
  onCancel={() => setShowConfirm(false)}
@@ -2801,7 +3072,7 @@ dismiss(id)
2801
3072
  <ConfirmDialog
2802
3073
  visible={showDiscard}
2803
3074
  title="Discard changes?"
2804
- description="All unsaved changes will be lost."
3075
+ subtitle="All unsaved changes will be lost."
2805
3076
  confirmLabel="Discard"
2806
3077
  confirmVariant="destructive"
2807
3078
  onConfirm={() => { setShowDiscard(false); goBack() }}
@@ -2832,7 +3103,7 @@ dismiss(id)
2832
3103
  | leftIconColor | `string` | — | Override left icon color (default: `foreground`) |
2833
3104
  | rightIconColor | `string` | — | Override right icon color (default: `foregroundMuted`) |
2834
3105
  | variant | `'plain' \| 'card'` | `'plain'` | `plain`: no background. `card`: surface with border and shadow |
2835
- | showChevron | `boolean` | `false` | Right-pointing chevron. Ignored when `rightRender` is set |
3106
+ | showChevron | `boolean` | `false` | Right-pointing chevron. Ignored when `rightActions`, `rightRender`, or `rightIcon` is set |
2836
3107
  | showSeparator | `boolean` | `false` | Hairline separator at bottom. Useful for stacking plain items |
2837
3108
  | onPress | `() => void` | — | Makes row pressable (scale animation + haptics) |
2838
3109
  | disabled | `boolean` | — | Reduces opacity to 0.45 |
@@ -2841,13 +3112,12 @@ dismiss(id)
2841
3112
  | subtitleStyle | `TextStyle` | — | Override subtitle text style |
2842
3113
  | subtitleNumberOfLines | `number` | `2` | Max lines for subtitle before truncation |
2843
3114
  | captionStyle | `TextStyle` | — | Override caption text style |
2844
- | icon | `ReactNode` | — | **Deprecated** use `leftRender` |
2845
- | trailing | `string \| ReactNode` | — | **Deprecated** — use `rightRender` |
3115
+ | rightActions | `ReactNode[]` | — | Multiple action buttons on the right with 8pt gap. Takes precedence over `rightRender` |
2846
3116
 
2847
3117
  **Slots:**
2848
3118
  - `leftRender` / `leftIcon` — 44×44pt fixed container, centered. Good for Avatar, icons, thumbnails
2849
3119
  - `rightRender` / `rightIcon` — max 160pt wide, right-aligned. Good for Badge, price text, Switch
2850
- - `showChevron` — `›` chevron, 24pt, `foregroundMuted` color. Only shows when `rightRender` is absent
3120
+ - `showChevron` — `›` chevron, 24pt, `foregroundMuted` color. Only shows when `rightRender`, `rightActions`, and `rightIcon` are absent
2851
3121
 
2852
3122
  **Separator inset:** Aligns to text block — `marginLeft` adjusts to skip over left slot when present.
2853
3123
 
@@ -3465,22 +3735,22 @@ const [period, setPeriod] = useState({
3465
3735
 
3466
3736
  **When to use:** Custom interactive content that needs beautiful spring bounce effect matching MediaCard. Use when you need a pressable wrapper around custom layouts that aren't covered by existing button components.
3467
3737
 
3468
- **Extends:** `TouchableOpacityProps` (all native props pass through except `activeOpacity`)
3469
-
3470
3738
  | Prop | Type | Default | Notes |
3471
3739
  |------|------|---------|-------|
3472
3740
  | children | `ReactNode` | required | Content to render inside the pressable |
3473
3741
  | onPress | `() => void` | — | Press handler |
3474
3742
  | pressScale | `number` | `0.98` | Scale value on press (MediaCard-style) |
3475
- | bounciness | `number` | `4` | Spring bounciness on release |
3476
3743
  | haptics | `boolean` | `true` | Enable haptic feedback on press |
3477
3744
  | hoverScale | `number` | `1.02` | Hover scale (web only). Set to `1` to disable |
3478
3745
  | disabled | `boolean` | `false` | Disable interaction |
3746
+ | accessibilityRole | `AccessibilityRole` | `'button'` | Override the accessibility role for screen readers |
3747
+ | accessibilityState | `Record<string, unknown>` | `{ disabled: !!disabled }` | Accessibility state for selected/expanded/checked |
3748
+ | accessibilityLabel | `string` | — | Accessibility label for screen readers |
3479
3749
  | style | `ViewStyle` | — | Animated wrapper style |
3480
3750
 
3481
3751
  **Behavior:**
3482
3752
  - Press: springs to `pressScale` (default 0.98) with `speed: 40, bounciness: 0`
3483
- - Release: springs back to 1.0 with `speed: 40, bounciness: 4`
3753
+ - Release: springs back to 1.0 with `speed: 40`
3484
3754
  - Web: optional hover scale (default 1.02)
3485
3755
  - Haptics: `impactLight` on press (unless `haptics={false}`)
3486
3756
 
@@ -3499,7 +3769,7 @@ const [period, setPeriod] = useState({
3499
3769
  </Pressable>
3500
3770
 
3501
3771
  // Wrapping complex layout
3502
- <Pressable onPress={handleSelect} pressScale={0.96} bounciness={8}>
3772
+ <Pressable onPress={handleSelect} pressScale={0.96}>
3503
3773
  <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, padding: 16 }}>
3504
3774
  <Avatar src={user.avatar} size="md" />
3505
3775
  <View style={{ flex: 1 }}>
@@ -3516,7 +3786,7 @@ const [period, setPeriod] = useState({
3516
3786
  </Pressable>
3517
3787
 
3518
3788
  // Custom press scale (deeper press)
3519
- <Pressable onPress={handlePress} pressScale={0.92} bounciness={6}>
3789
+ <Pressable onPress={handlePress} pressScale={0.92}>
3520
3790
  {/* content */}
3521
3791
  </Pressable>
3522
3792
  ```
@@ -3788,6 +4058,77 @@ export default function TabLayout() {
3788
4058
 
3789
4059
  ---
3790
4060
 
4061
+ ### Stats
4062
+
4063
+ **Import:** `import { Stats } from '@retray-dev/ui-kit'`
4064
+
4065
+ **When to use:** Dashboards and analytics screens — single-stat card with a large value, icon, label, and optional description. Tappable cards wire haptics automatically.
4066
+
4067
+ | Prop | Type | Default | Notes |
4068
+ |------|------|---------|-------|
4069
+ | value | `string` | required | Large display value, e.g. `"$12,450"` or `"847"` |
4070
+ | label | `string` | required | Label below the value, e.g. `"Monthly Revenue"` |
4071
+ | description | `string` | — | Third line — smaller, muted text for trend/context |
4072
+ | icon | `React.ReactNode` | — | Custom icon node |
4073
+ | iconName | `string` | — | Icon from `@expo/vector-icons` (left of value, `colors.primary`) |
4074
+ | iconColor | `string` | `colors.primary` | Override icon color |
4075
+ | size | `'default' \| 'compact'` | `'default'` | `compact`: 16px SemiBold value, 11px label, reduced padding |
4076
+ | variant | `'elevated' \| 'outlined' \| 'filled'` | `'elevated'` | Card surface variant |
4077
+ | onPress | `() => void` | — | Makes the card pressable (haptic `impactLight()`) |
4078
+ | style | `ViewStyle` | — | — |
4079
+ | accessibilityLabel | `string` | — | — |
4080
+
4081
+ **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.
4082
+
4083
+ **`Stats.Group`** — horizontal layout wrapper that distributes children equally:
4084
+
4085
+ | Prop | Type | Default | Notes |
4086
+ |------|------|---------|-------|
4087
+ | children | `React.ReactNode` | required | `Stats` components to arrange horizontally |
4088
+ | gap | `number` | `s(12)` | Spacing between cards |
4089
+ | style | `ViewStyle` | — | — |
4090
+
4091
+ **Layout guidance:** Prefer 2-up for values longer than ~3 digits (e.g. `"$12,450"`, `"1,234"`) or with descriptions. Use 3-up only for very compact metrics with short values (e.g. `"847"`, `"98%"`) and no description — otherwise values will wrap and look broken.
4092
+
4093
+ **Example:**
4094
+ ```tsx
4095
+ // Standalone
4096
+ <Stats
4097
+ value="$12,450"
4098
+ label="Monthly Revenue"
4099
+ description="+12% from last month"
4100
+ iconName="trending-up"
4101
+ />
4102
+
4103
+ // 3-up — short values only, no description
4104
+ <Stats.Group>
4105
+ <Stats value="847" label="Users" variant="elevated" />
4106
+ <Stats value="98%" label="Uptime" variant="outlined" />
4107
+ <Stats value="12" label="Alerts" variant="filled" />
4108
+ </Stats.Group>
4109
+
4110
+ // 2-up with icons, long values, and description
4111
+ <Stats.Group gap={s(16)}>
4112
+ <Stats
4113
+ value="$8,200"
4114
+ label="Spent"
4115
+ description="This month"
4116
+ iconName="credit-card"
4117
+ variant="elevated"
4118
+ onPress={() => navigateToBilling()}
4119
+ />
4120
+ <Stats
4121
+ value="$2,150"
4122
+ label="Saved"
4123
+ description="12% of budget"
4124
+ iconName="trending-up"
4125
+ variant="outlined"
4126
+ />
4127
+ </Stats.Group>
4128
+ ```
4129
+
4130
+ ---
4131
+
3791
4132
  ### ErrorBoundary
3792
4133
 
3793
4134
  **Import:** `import { ErrorBoundary } from '@retray-dev/ui-kit'`
@@ -3989,6 +4330,7 @@ import { HolographicCard, FOIL_PRESETS } from '@retray-dev/ui-kit/HolographicCar
3989
4330
  | borderRadius | `number` | `RADIUS.lg` | — |
3990
4331
  | resizeMode | `'cover' \| 'contain' \| 'stretch'` | `'cover'` | Image resize mode |
3991
4332
  | 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 |
3992
4334
  | style | `ViewStyle` | — | — |
3993
4335
  | accessibilityLabel | `string` | — | — |
3994
4336
 
@@ -4030,7 +4372,7 @@ const handleChange = async (uri: string | null) => {
4030
4372
 
4031
4373
  **Import:** `import { IconPicker } from '@retray-dev/ui-kit'`
4032
4374
 
4033
- **When to use:** Selecting an icon from a curated catalog of ~320 icons (across all 6 `@expo/vector-icons` families) organized by 12 categories. The trigger is a simple tappable square showing the selected icon (or a + placeholder). Tapping opens a bottom sheet with category chips (icon + label) and a scrollable grid. No search, no text clutter — purely visual selection.
4375
+ **When to use:** Selecting an icon from a curated catalog of ~600 themed icons (across Feather, Ionicons `-outline`, FA5 Brands/regular, Entypo, AntDesign — only outlined variants) organized by 12 categories. The trigger is a simple tappable square showing the selected icon (or a + placeholder). Tapping opens a bottom sheet with category chips (icon + label) and a scrollable grid. No search, no text clutter — purely visual selection.
4034
4376
 
4035
4377
  **Requires:** `@gorhom/bottom-sheet` (already a peer dependency of the UI kit).
4036
4378
 
@@ -4067,10 +4409,12 @@ const [icon, setIcon] = useState<string | null>(null)
4067
4409
  ```
4068
4410
 
4069
4411
  **Notes:**
4070
- - Uses a static curated list of ~320 icons in 12 categories (food, sports, business, objects, status, actions, communication, navigation, media, layout, nature, brands) — instant load, no runtime glyphMap scanning.
4412
+ - Uses a static curated list of ~600 outlined icons in 12 categories (food, sports, business, objects, status, actions, communication, navigation, media, layout, nature, brands), with at least 42 icons per category — instant load, no runtime glyphMap scanning.
4413
+ - Icon selection rules are strict: only Feather, Ionicons `-outline`, and themed FA5/Entypo/AntDesign icons are eligible. Filled variants and FA5-only-solid icons are excluded to keep the visual style consistent. See `src/utils/curatedIcons.ts` for the source of truth.
4414
+ - **Feedback pattern (v12.1):** the sheet presents **immediately** on trigger tap (no `useEffect` delay). While the grid container measures its width via `onLayout`, a centered `<Spinner />` is shown inside the sheet so the user always sees visible feedback. The grid transitions in as soon as measurement is complete. This is the canonical REGLA 4 pattern — apply it to any overlay whose content needs to measure before rendering.
4071
4415
  - Category chips use representative icons (coffee, activity, briefcase, folder, alert-circle, edit-3, message-circle, compass, image, grid, sun, globe) with Spanish labels and a "Todos" (All) chip.
4072
4416
  - Grid cells show only icons — clean, fast visual scanning. Cell size adapts to container width.
4073
- - Sheet uses `enableDynamicSizing` with `maxDynamicContentSize` (70% screen height) — height auto-fits content up to that cap.
4417
+ - Sheet uses `enableDynamicSizing` with `maxDynamicContentSize` (70% screen height) — height auto-fits content up to that cap. `topInset={insets.top}` is applied automatically.
4074
4418
  - Category strip is a horizontal `ScrollView` of pill-shaped chips. The grid is rendered in rows inside a `BottomSheetScrollView`.
4075
4419
  - Selection closes the sheet immediately and resets category to "Todos".
4076
4420
  - Haptics: medium impact on open, selection on icon tap.