@retray-dev/ui-kit 10.1.0 → 12.1.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 (192) hide show
  1. package/COMPONENTS.md +419 -38
  2. package/README.md +14 -5
  3. package/dist/Accordion.js +1 -1
  4. package/dist/Accordion.mjs +3 -3
  5. package/dist/AlertBanner.js +1 -1
  6. package/dist/AlertBanner.mjs +3 -3
  7. package/dist/AppHeader.js +1 -1
  8. package/dist/AppHeader.mjs +4 -4
  9. package/dist/Avatar.mjs +2 -2
  10. package/dist/Badge.js +1 -1
  11. package/dist/Badge.mjs +3 -3
  12. package/dist/Button.js +1 -1
  13. package/dist/Button.mjs +3 -3
  14. package/dist/Card.mjs +2 -2
  15. package/dist/CategoryStrip.js +1 -1
  16. package/dist/CategoryStrip.mjs +3 -3
  17. package/dist/Checkbox.mjs +2 -2
  18. package/dist/Chip.js +1 -1
  19. package/dist/Chip.mjs +3 -3
  20. package/dist/ConfirmDialog.d.mts +1 -6
  21. package/dist/ConfirmDialog.d.ts +1 -6
  22. package/dist/ConfirmDialog.js +30 -24
  23. package/dist/ConfirmDialog.mjs +4 -4
  24. package/dist/CurrencyDisplay.mjs +2 -2
  25. package/dist/CurrencyInput.d.mts +3 -8
  26. package/dist/CurrencyInput.d.ts +3 -8
  27. package/dist/CurrencyInput.js +4 -2
  28. package/dist/CurrencyInput.mjs +4 -4
  29. package/dist/DetailRow.d.mts +1 -1
  30. package/dist/DetailRow.d.ts +1 -1
  31. package/dist/DetailRow.js +1 -1
  32. package/dist/DetailRow.mjs +3 -3
  33. package/dist/EmptyState.js +1 -1
  34. package/dist/EmptyState.mjs +4 -4
  35. package/dist/ErrorBoundary.js +1 -1
  36. package/dist/ErrorBoundary.mjs +3 -3
  37. package/dist/Form.mjs +2 -2
  38. package/dist/IconButton.js +1 -1
  39. package/dist/IconButton.mjs +3 -3
  40. package/dist/IconPicker.d.mts +17 -0
  41. package/dist/IconPicker.d.ts +17 -0
  42. package/dist/IconPicker.js +1424 -0
  43. package/dist/IconPicker.mjs +8 -0
  44. package/dist/ImageUpload.d.mts +3 -1
  45. package/dist/ImageUpload.d.ts +3 -1
  46. package/dist/ImageUpload.js +28 -10
  47. package/dist/ImageUpload.mjs +3 -3
  48. package/dist/ImageViewer.js +1 -1
  49. package/dist/ImageViewer.mjs +5 -5
  50. package/dist/Input.js +1 -1
  51. package/dist/Input.mjs +3 -3
  52. package/dist/LabelValue.js +1 -1
  53. package/dist/LabelValue.mjs +3 -3
  54. package/dist/ListGroup.mjs +2 -2
  55. package/dist/ListItem.d.mts +7 -7
  56. package/dist/ListItem.d.ts +7 -7
  57. package/dist/ListItem.js +13 -8
  58. package/dist/ListItem.mjs +3 -3
  59. package/dist/MediaCard.js +1 -1
  60. package/dist/MediaCard.mjs +3 -3
  61. package/dist/MenuGroup.mjs +2 -2
  62. package/dist/MenuItem.js +1 -1
  63. package/dist/MenuItem.mjs +3 -3
  64. package/dist/MonthPicker.mjs +2 -2
  65. package/dist/NumberStepper.d.mts +19 -0
  66. package/dist/NumberStepper.d.ts +19 -0
  67. package/dist/NumberStepper.js +410 -0
  68. package/dist/NumberStepper.mjs +9 -0
  69. package/dist/PagerDots.js +1 -1
  70. package/dist/PagerDots.mjs +3 -3
  71. package/dist/Pressable.d.mts +15 -7
  72. package/dist/Pressable.d.ts +15 -7
  73. package/dist/Pressable.js +7 -3
  74. package/dist/Pressable.mjs +1 -1
  75. package/dist/PricingCard.js +1 -1
  76. package/dist/PricingCard.mjs +5 -5
  77. package/dist/Progress.mjs +2 -2
  78. package/dist/RadioGroup.mjs +2 -2
  79. package/dist/RetrayProvider.mjs +3 -3
  80. package/dist/Select.mjs +2 -2
  81. package/dist/SelectableGrid.js +1 -1
  82. package/dist/SelectableGrid.mjs +3 -3
  83. package/dist/Separator.mjs +2 -2
  84. package/dist/Sheet.d.mts +4 -46
  85. package/dist/Sheet.d.ts +4 -46
  86. package/dist/Sheet.js +46 -114
  87. package/dist/Sheet.mjs +2 -3
  88. package/dist/SheetSelect.js +1 -1
  89. package/dist/SheetSelect.mjs +3 -3
  90. package/dist/Skeleton.mjs +2 -2
  91. package/dist/Slider.mjs +2 -2
  92. package/dist/Spinner.mjs +2 -2
  93. package/dist/Stats.d.mts +30 -0
  94. package/dist/Stats.d.ts +30 -0
  95. package/dist/Stats.js +429 -0
  96. package/dist/Stats.mjs +9 -0
  97. package/dist/Switch.mjs +2 -2
  98. package/dist/TabBar.js +1 -1
  99. package/dist/TabBar.mjs +3 -3
  100. package/dist/Tabs.mjs +2 -2
  101. package/dist/Text.d.mts +3 -1
  102. package/dist/Text.d.ts +3 -1
  103. package/dist/Text.js +3 -3
  104. package/dist/Text.mjs +2 -2
  105. package/dist/Textarea.js +1 -1
  106. package/dist/Textarea.mjs +3 -3
  107. package/dist/Toast.mjs +2 -2
  108. package/dist/Toggle.js +1 -1
  109. package/dist/Toggle.mjs +3 -3
  110. package/dist/{chunk-DJ7RN37L.mjs → chunk-265G6A46.mjs} +2 -2
  111. package/dist/{chunk-WOEYDUJZ.mjs → chunk-2A2LEFZG.mjs} +2 -2
  112. package/dist/{chunk-ID72TK46.mjs → chunk-2CBQKU7H.mjs} +1 -1
  113. package/dist/{chunk-OB4JUQ3O.mjs → chunk-2I2AYECM.mjs} +1 -1
  114. package/dist/{chunk-WJLKJMKR.mjs → chunk-357YO24D.mjs} +4 -4
  115. package/dist/{chunk-GQYFLP3D.mjs → chunk-3GEYJ7I5.mjs} +1 -1
  116. package/dist/{chunk-AV4EMIRH.mjs → chunk-3N2M3WZL.mjs} +1 -1
  117. package/dist/{chunk-TERDKCLE.mjs → chunk-3UYAZ7I4.mjs} +2 -2
  118. package/dist/{chunk-JMOZEC77.mjs → chunk-4WFMPFZB.mjs} +1 -1
  119. package/dist/chunk-5OLNXP3S.mjs +144 -0
  120. package/dist/{chunk-6OAZJ577.mjs → chunk-7HSILTC4.mjs} +3 -3
  121. package/dist/{chunk-IRRY3CRZ.mjs → chunk-AKM4EPOT.mjs} +1 -1
  122. package/dist/{chunk-VGTDN7SW.mjs → chunk-AQEVCEXV.mjs} +2 -2
  123. package/dist/{chunk-WBOOUHSS.mjs → chunk-BCWEHE34.mjs} +1 -1
  124. package/dist/{chunk-AJ7ZDNBT.mjs → chunk-BOVUP27T.mjs} +1 -1
  125. package/dist/{chunk-BRKYVJVV.mjs → chunk-BQZE3HAW.mjs} +1 -1
  126. package/dist/{chunk-MLF3EZFW.mjs → chunk-D3Y2T42P.mjs} +2 -2
  127. package/dist/{chunk-3U4SSNWP.mjs → chunk-DF6DU42P.mjs} +2 -2
  128. package/dist/{chunk-ZJKGQMYH.mjs → chunk-DI7CBDL6.mjs} +2 -2
  129. package/dist/{chunk-2TFTAWVJ.mjs → chunk-DOGIPOF5.mjs} +2 -2
  130. package/dist/{chunk-MBMXYJJV.mjs → chunk-E7NEHHXV.mjs} +7 -3
  131. package/dist/{chunk-MX6HRKMI.mjs → chunk-EFLFRAHD.mjs} +1 -1
  132. package/dist/{chunk-SOYNZDVY.mjs → chunk-EMUWGDWC.mjs} +6 -1
  133. package/dist/{chunk-4I7D47FH.mjs → chunk-F4V6XLP4.mjs} +4 -4
  134. package/dist/{chunk-UREA2GYY.mjs → chunk-FA2KMTH5.mjs} +2 -2
  135. package/dist/{chunk-Y2NS74WS.mjs → chunk-FFTYLPSB.mjs} +46 -98
  136. package/dist/{chunk-OHBNABL5.mjs → chunk-FUVYSVGR.mjs} +14 -9
  137. package/dist/{chunk-KIHCWCWL.mjs → chunk-FVTVCJAH.mjs} +2 -2
  138. package/dist/{chunk-Y4GL2MHX.mjs → chunk-GK4VRMNE.mjs} +30 -12
  139. package/dist/{chunk-6Q64UFIA.mjs → chunk-HJ46DTJE.mjs} +1 -1
  140. package/dist/{chunk-WF2XDFRK.mjs → chunk-HLMPMUK2.mjs} +1 -1
  141. package/dist/{chunk-GD6KXMG5.mjs → chunk-I4V5XZPS.mjs} +1 -1
  142. package/dist/{chunk-AZJF2BLK.mjs → chunk-ISY26JQJ.mjs} +2 -2
  143. package/dist/{chunk-X4G6APW6.mjs → chunk-J6Q2YJEV.mjs} +1 -1
  144. package/dist/{chunk-KZL5VTYK.mjs → chunk-JCZQOY4O.mjs} +31 -24
  145. package/dist/{chunk-CZCQZHG6.mjs → chunk-JNVAIDLK.mjs} +2 -2
  146. package/dist/{chunk-SOA2Z4RB.mjs → chunk-JULSIZDM.mjs} +1 -1
  147. package/dist/{chunk-T7XZ7H7Y.mjs → chunk-KA7LTET3.mjs} +17 -3
  148. package/dist/chunk-KHYX4IOM.mjs +1114 -0
  149. package/dist/{chunk-LXJIIOYQ.mjs → chunk-LRM4AVYY.mjs} +2 -2
  150. package/dist/{chunk-VQ57HWPL.mjs → chunk-MYZ2EDYU.mjs} +2 -2
  151. package/dist/chunk-N4ZPVCJH.mjs +126 -0
  152. package/dist/{chunk-NA7PARID.mjs → chunk-NXI4YDZ2.mjs} +2 -2
  153. package/dist/{chunk-4K625MVM.mjs → chunk-OULVKTWL.mjs} +2 -2
  154. package/dist/{chunk-A4MDAP7G.mjs → chunk-P64WHW4A.mjs} +2 -2
  155. package/dist/{chunk-URI2WBIV.mjs → chunk-P73V2EKS.mjs} +2 -2
  156. package/dist/{chunk-ZUR7AU5R.mjs → chunk-PGERH3P7.mjs} +2 -2
  157. package/dist/{chunk-2UYENBLV.mjs → chunk-QSFV2P7O.mjs} +1 -1
  158. package/dist/{chunk-JT7HKXRB.mjs → chunk-S3KJCPEJ.mjs} +1 -1
  159. package/dist/{chunk-6MKGPAR2.mjs → chunk-V6NFJXKO.mjs} +2 -2
  160. package/dist/{chunk-A3A6KNQN.mjs → chunk-WOEWGSTU.mjs} +1 -1
  161. package/dist/{chunk-JUXSWN54.mjs → chunk-X26S5EVZ.mjs} +4 -2
  162. package/dist/{chunk-YFZ3ELX5.mjs → chunk-XBAGGKLW.mjs} +2 -2
  163. package/dist/{chunk-JB67UOB5.mjs → chunk-ZHMSAYLT.mjs} +2 -2
  164. package/dist/fonts.d.mts +1 -7
  165. package/dist/fonts.d.ts +1 -7
  166. package/dist/fonts.js +0 -2
  167. package/dist/fonts.mjs +1 -2
  168. package/dist/index.d.mts +7 -1
  169. package/dist/index.d.ts +7 -1
  170. package/dist/index.js +1831 -475
  171. package/dist/index.mjs +54 -51
  172. package/package.json +3 -3
  173. package/src/components/ConfirmDialog/ConfirmDialog.tsx +39 -30
  174. package/src/components/CurrencyInput/CurrencyInput.tsx +4 -7
  175. package/src/components/DetailRow/DetailRow.tsx +1 -1
  176. package/src/components/IconPicker/IconPicker.tsx +395 -0
  177. package/src/components/IconPicker/index.ts +1 -0
  178. package/src/components/ImageUpload/ImageUpload.tsx +34 -12
  179. package/src/components/ListItem/ListItem.tsx +43 -28
  180. package/src/components/NumberStepper/NumberStepper.tsx +147 -0
  181. package/src/components/NumberStepper/index.ts +1 -0
  182. package/src/components/Pressable/Pressable.tsx +20 -8
  183. package/src/components/Sheet/Sheet.tsx +64 -172
  184. package/src/components/Stats/Stats.tsx +226 -0
  185. package/src/components/Stats/index.ts +2 -0
  186. package/src/components/Text/Text.tsx +4 -2
  187. package/src/fonts.ts +0 -7
  188. package/src/index.ts +7 -1
  189. package/src/theme/colorUtils.ts +9 -0
  190. package/src/utils/curatedIcons.ts +849 -0
  191. package/src/utils/fontGuard.ts +2 -1
  192. package/src/utils/icons.ts +20 -2
package/COMPONENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- # @retray-dev/ui-kit — Component Reference (v10.1.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
 
@@ -317,6 +317,24 @@ const resolved = deriveColors(myThemeColors, 'light')
317
317
 
318
318
  ---
319
319
 
320
+ ### Color Utilities
321
+
322
+ **Import:** `import { withAlpha } from '@retray-dev/ui-kit'`
323
+
324
+ Convert a hex color to rgba with the given alpha. Useful for semi-transparent backgrounds, borders, and overlays derived from theme colors.
325
+
326
+ ```tsx
327
+ import { useTheme, withAlpha } from '@retray-dev/ui-kit'
328
+
329
+ const { colors } = useTheme()
330
+
331
+ <View style={{ backgroundColor: withAlpha(colors.primary, 0.15) }}>
332
+ <Text style={{ color: colors.primary }}>Tinted background</Text>
333
+ </View>
334
+ ```
335
+
336
+ ---
337
+
320
338
  ## Design Tokens
321
339
 
322
340
  Static structural constants — no context or provider needed.
@@ -684,6 +702,132 @@ assets/fonts/sohne/
684
702
 
685
703
  ---
686
704
 
705
+ ## Migration Guide: v10 → v11
706
+
707
+ ### New — public utility
708
+
709
+ **`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.
710
+
711
+ ```tsx
712
+ import { useTheme, withAlpha } from '@retray-dev/ui-kit'
713
+
714
+ const { colors } = useTheme()
715
+ <View style={{ backgroundColor: withAlpha(colors.primary, 0.15) }} />
716
+ ```
717
+
718
+ ### Updated
719
+
720
+ - `Stats` component promoted from internal/experimental to public (`Stats` + `Stats.Group`).
721
+ - Documentation refreshed for `IconPicker` and `NumberStepper`.
722
+
723
+ No breaking changes in v11. Safe minor upgrade from v10.
724
+
725
+ ---
726
+
727
+ ## Migration Guide: v11 → v12
728
+
729
+ ### Breaking Changes
730
+
731
+ `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.
732
+
733
+ **1. Removed `responsive` / `dialogMaxWidth` props from `Sheet` and `ConfirmDialog`**
734
+
735
+ 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.
736
+
737
+ ```diff
738
+ <Sheet
739
+ open={open}
740
+ onClose={() => setOpen(false)}
741
+ title="Options"
742
+ - responsive
743
+ - dialogMaxWidth={600}
744
+ />
745
+ ```
746
+
747
+ **2. `onClose` is the only close handler**
748
+
749
+ `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.
750
+
751
+ **3. `Sheet` requires `BottomSheetModalProvider` at app root**
752
+
753
+ `RetrayProvider` already wires it. If you assemble providers manually, ensure `BottomSheetModalProvider` sits inside `GestureHandlerRootView`:
754
+
755
+ ```tsx
756
+ <SafeAreaProvider initialMetrics={initialWindowMetrics}>
757
+ <GestureHandlerRootView style={{ flex: 1 }}>
758
+ <ThemeProvider>
759
+ <BottomSheetModalProvider>
760
+ <ToastProvider>{/* app */}</ToastProvider>
761
+ </BottomSheetModalProvider>
762
+ </ThemeProvider>
763
+ </GestureHandlerRootView>
764
+ </SafeAreaProvider>
765
+ ```
766
+
767
+ **4. Keyboard prop renames (no consumer action — defaults match v11)**
768
+
769
+ | Prop | v11 | v12 default |
770
+ |------|-----|-------------|
771
+ | `keyboardBehavior` | `'interactive'` | `'interactive'` (unchanged) |
772
+ | `android_keyboardInputMode` | `'adjustPan'` | `'adjustPan'` (unchanged) |
773
+
774
+ **5. Removed top-level imports of `Modal`, `ScrollView`, `useWindowDimensions`, `BREAKPOINTS` from `Sheet.tsx`**
775
+
776
+ Internal only — no public API.
777
+
778
+ ### Behavioral changes (no code change required)
779
+
780
+ - **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.
781
+ - **`topInset={insets.top}`** is now applied automatically from safe-area context — sheet never crosses the notch / status bar on iOS or Android.
782
+ - **`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).
783
+
784
+ ### New — recommended pattern
785
+
786
+ **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").
787
+
788
+ ```tsx
789
+ <Sheet open={open} onClose={() => setOpen(false)} title="Add note">
790
+ <Input
791
+ label="Note"
792
+ placeholder="Type your note..."
793
+ value={note}
794
+ onChangeText={setNote}
795
+ sheetMode
796
+ />
797
+ <Button label="Save" fullWidth onPress={handleSave} />
798
+ </Sheet>
799
+ ```
800
+
801
+ `SheetTextInput` is still re-exported for low-level use.
802
+
803
+ ---
804
+
805
+ ## Migration Guide: v12.0 → v12.1
806
+
807
+ ### New — IconPicker feedback pattern (REGLA 4)
808
+
809
+ `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.
810
+
811
+ This is now the canonical pattern for any sheet whose content needs to measure or load before rendering — apply it to new overlay components.
812
+
813
+ ### New — race-condition fix in `Sheet` and `ConfirmDialog`
814
+
815
+ 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.
816
+
817
+ No consumer action required — the fix is internal.
818
+
819
+ ### New — expanded curated icon library
820
+
821
+ `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).
822
+
823
+ ### Updated
824
+
825
+ - `Sheet` and `ConfirmDialog` pass a stable `name` prop to `BottomSheetModal` (from `useId()`) for correct gorhom modal registry behavior when multiple modals are mounted.
826
+ - `Input` `sheetMode` prop documented in the Setup section (replaces the `SheetTextInput` boilerplate inside sheets).
827
+ - `CompositionScreen` example now uses `<Input sheetMode />` inside a `Sheet` to demonstrate the keyboard-friendly pattern.
828
+
829
+ ---
830
+
687
831
  ## Components
688
832
 
689
833
  ---
@@ -700,6 +844,7 @@ assets/fonts/sohne/
700
844
  |------|------|---------|-------|
701
845
  | variant | `TextVariant` | `'body-md'` | Sets font, size, weight, line height, letter spacing |
702
846
  | color | `string` | — | Override text color. Each variant has a semantic default (see table below) |
847
+ | uppercase | `boolean` | `false` | Force text-transform: uppercase on any variant |
703
848
  | children | `ReactNode` | required | — |
704
849
  | style | `TextStyle` | — | Additional styles (merged after variant styles) |
705
850
  | (all TextProps) | — | — | `numberOfLines`, `ellipsizeMode`, `onPress`, etc. all pass through |
@@ -1073,6 +1218,23 @@ assets/fonts/sohne/
1073
1218
  </View>
1074
1219
  ```
1075
1220
 
1221
+ **Composition — inside a Sheet (preferred pattern, v12+):**
1222
+
1223
+ Use `sheetMode` to opt the inner `TextInput` into `BottomSheetTextInput` so keyboard handling works correctly. This is the canonical replacement for raw `SheetTextInput` inside sheets.
1224
+
1225
+ ```tsx
1226
+ <Sheet open={open} onClose={() => setOpen(false)} title="Add note">
1227
+ <Input
1228
+ label="Note"
1229
+ placeholder="Write your note..."
1230
+ value={note}
1231
+ onChangeText={setNote}
1232
+ sheetMode
1233
+ />
1234
+ <Button label="Save" fullWidth onPress={handleSave} />
1235
+ </Sheet>
1236
+ ```
1237
+
1076
1238
  ---
1077
1239
 
1078
1240
  ### Textarea
@@ -1125,9 +1287,12 @@ assets/fonts/sohne/
1125
1287
  | hint | `string` | — | Helper text below (hidden when `error` is set) |
1126
1288
  | placeholder | `string` | `'$0'` | Defaults to `prefix + '0'` |
1127
1289
  | editable | `boolean` | — | Pass `false` to disable |
1290
+ | autoFocus | `boolean` | — | Auto-focus on mount (inherited from TextInputProps) |
1128
1291
  | containerStyle | `ViewStyle` | — | Outer container style |
1129
1292
  | sheetMode | `boolean` | `false` | Use inside a Sheet — forwards `sheetMode` to underlying `Input` |
1130
1293
 
1294
+ **Extends `TextInputProps` from React Native** — all TextInput props (autoFocus, onFocus, onBlur, etc.) pass through to the underlying input.
1295
+
1131
1296
  **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
1297
 
1133
1298
  **Examples:**
@@ -1175,6 +1340,69 @@ const [amount, setAmount] = useState(0)
1175
1340
 
1176
1341
  ---
1177
1342
 
1343
+ ### NumberStepper
1344
+
1345
+ **Import:** `import { NumberStepper } from '@retray-dev/ui-kit'`
1346
+
1347
+ **When to use:** Quantity +/- controls — cart item counts, guest selectors, inventory adjustments, portion sizes. Use when the user needs to increment/decrement a discrete numeric value within a constrained range (min–max).
1348
+
1349
+ | Prop | Type | Default | Notes |
1350
+ |------|------|---------|-------|
1351
+ | value | `number` | required | Current numeric value |
1352
+ | onValueChange | `(value: number) => void` | required | Called with clamped value on each step |
1353
+ | min | `number` | `1` | Minimum allowed value |
1354
+ | max | `number` | `99` | Maximum allowed value |
1355
+ | step | `number` | `1` | Increment/decrement amount. Supports decimals (e.g. `0.5`) |
1356
+ | size | `'sm' \| 'md' \| 'lg'` | `'md'` | Controls button and value text size |
1357
+ | disabled | `boolean` | `false` | Disables both buttons |
1358
+ | accessibilityLabel | `string` | — | Custom a11y label for value text. Default: `"Quantity: {value}"` |
1359
+ | style | `ViewStyle` | — | Outer container style |
1360
+
1361
+ **Styling:** Row layout with two square buttons flanking a centered value label. Buttons use `RADIUS.md` (14pt), `borderWidth: 1.5`, `surface` background with `border` outline. Value text: `Sohne-Medium`, centered, `foreground` color.
1362
+
1363
+ **Button states:** The decrement button disables (opacity 0.35, non-pressable) when `value ≤ min`. The increment button disables when `value ≥ max`. Both disable when the `disabled` prop is `true`.
1364
+
1365
+ **Haptics:** `impactLight` on each press.
1366
+
1367
+ **Accessibility:** Each button has a descriptive label (`"Decrease, current value X"` / `"Increase, current value X"`). The value text has `accessibilityRole="text"`. Disabled state passed to `accessibilityState`.
1368
+
1369
+ **Animation:** `PressableButton` (scale 0.95) on each button — matches the Button/IconButton press feel.
1370
+
1371
+ **Examples:**
1372
+ ```tsx
1373
+ // Basic quantity stepper
1374
+ const [qty, setQty] = useState(1)
1375
+ <NumberStepper value={qty} onValueChange={setQty} />
1376
+
1377
+ // Custom range and step
1378
+ const [rating, setRating] = useState(3)
1379
+ <NumberStepper value={rating} onValueChange={setRating} min={0} max={5} step={0.5} />
1380
+
1381
+ // Large size for prominent use
1382
+ const [guests, setGuests] = useState(2)
1383
+ <NumberStepper value={guests} onValueChange={setGuests} min={1} max={16} size="lg" />
1384
+
1385
+ // Disabled during async operation
1386
+ <NumberStepper
1387
+ value={qty}
1388
+ onValueChange={setQty}
1389
+ disabled={isUpdating}
1390
+ />
1391
+ ```
1392
+
1393
+ **Composition — cart item row:**
1394
+ ```tsx
1395
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
1396
+ <View>
1397
+ <Text variant="body-md">Margherita Pizza</Text>
1398
+ <Text variant="caption-sm" color={colors.foregroundMuted}>$12.000</Text>
1399
+ </View>
1400
+ <NumberStepper value={quantity} onValueChange={setQuantity} />
1401
+ </View>
1402
+ ```
1403
+
1404
+ ---
1405
+
1178
1406
  ### CurrencyDisplay
1179
1407
 
1180
1408
  **Import:** `import { CurrencyDisplay } from '@retray-dev/ui-kit'`
@@ -2337,7 +2565,6 @@ Add `react-native-worklets/plugin` (not `react-native-reanimated/plugin`) to `ba
2337
2565
  | onClose | `() => void` | required | Called on swipe-dismiss or backdrop press |
2338
2566
  | title | `string` | — | Sheet heading |
2339
2567
  | subtitle | `string` | — | Supporting text below title |
2340
- | description | `string` | — | **Deprecated alias** for `subtitle` — prefer `subtitle` |
2341
2568
  | showCloseButton | `boolean` | `false` | Show X close button in the header |
2342
2569
  | children | `ReactNode` | — | Sheet content |
2343
2570
  | style | `ViewStyle` | — | Inner scroll/content container style |
@@ -2350,14 +2577,11 @@ Add `react-native-worklets/plugin` (not `react-native-reanimated/plugin`) to `ba
2350
2577
  | 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) |
2351
2578
  | footer | `ReactNode` | — | Sticky footer below scroll area (sticky above keyboard) |
2352
2579
  | snapPoints | `(string \| number)[]` | — | Optional snap points (e.g., `['50%', '85%']`). When omitted, uses dynamic sizing (auto-fits content) |
2353
- | 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 |
2354
- | dialogMaxWidth | `number` | `480` | Max width of the centered dialog. Only applies when `responsive` |
2355
2580
 
2356
2581
  **Features:**
2357
2582
  - `enableDynamicSizing` — height auto-fits content, no `snapPoints` needed (default behavior when `snapPoints` is omitted)
2358
2583
  - `snapPoints` — optionally provide custom snap points (e.g., `['50%', '85%']`). Disables dynamic sizing when provided
2359
- - `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`
2360
- - `enablePanDownToClose` — swipe down to dismiss
2584
+ - `enablePanDownToClose` — swipe down to dismiss (always enabled)
2361
2585
  - Backdrop press dismisses
2362
2586
  - **Scrollable content:** use `scrollable` prop or `maxHeight`. Both use `BottomSheetScrollView` — do NOT use plain `ScrollView` inside Sheet
2363
2587
  - **Keyboard handling:** Full keyboard awareness via `@gorhom/bottom-sheet` v5. Professional defaults:
@@ -2366,7 +2590,7 @@ Add `react-native-worklets/plugin` (not `react-native-reanimated/plugin`) to `ba
2366
2590
  - `keyboardBlurBehavior="restore"` — returns to pre-keyboard position when keyboard dismisses
2367
2591
  - `enableBlurKeyboardOnGesture={true}` — dismisses keyboard when dragging sheet down
2368
2592
  - `topInset` — automatically applied from safe area context to prevent going above notch
2369
- - **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
2593
+ - **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.
2370
2594
  - **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)
2371
2595
 
2372
2596
  **Haptics:** `impactMedium` on open.
@@ -2385,23 +2609,39 @@ const [open, setOpen] = useState(false)
2385
2609
  {items.map((r) => <ListItem key={r.id} title={r.name} showSeparator />)}
2386
2610
  </Sheet>
2387
2611
 
2388
- // With text input — keyboard handling works automatically (platform-optimized defaults)
2389
- <Sheet
2390
- open={open}
2391
- onClose={() => setOpen(false)}
2612
+ // With text input — recommended pattern (v12+): Input sheetMode
2613
+ <Sheet
2614
+ open={open}
2615
+ onClose={() => setOpen(false)}
2616
+ title="Add note"
2617
+ subtitle="Keyboard handling is automatic with platform-optimized defaults"
2618
+ >
2619
+ <Input
2620
+ label="Note"
2621
+ placeholder="Write your note..."
2622
+ value={note}
2623
+ onChangeText={setNote}
2624
+ sheetMode
2625
+ />
2626
+ <Button label="Save" fullWidth onPress={handleSave} />
2627
+ </Sheet>
2628
+
2629
+ // Low-level alternative: SheetTextInput
2630
+ <Sheet
2631
+ open={open}
2632
+ onClose={() => setOpen(false)}
2392
2633
  title="Add note"
2393
- subtitle="Keyboard handling is automatic with platform-optimized defaults"
2394
2634
  >
2395
- <SheetTextInput
2396
- placeholder="Write your note..."
2635
+ <SheetTextInput
2636
+ placeholder="Write your note..."
2397
2637
  multiline
2398
- style={{
2399
- borderWidth: 1,
2638
+ style={{
2639
+ borderWidth: 1,
2400
2640
  borderColor: '#ddd',
2401
2641
  borderRadius: 8,
2402
2642
  padding: 12,
2403
2643
  minHeight: 80,
2404
- }}
2644
+ }}
2405
2645
  />
2406
2646
  <Button label="Save" fullWidth style={{ marginTop: 12 }} onPress={() => setOpen(false)} />
2407
2647
  </Sheet>
@@ -2430,7 +2670,7 @@ const [open, setOpen] = useState(false)
2430
2670
 
2431
2671
  **Keyboard handling notes:**
2432
2672
  - No `KeyboardAvoidingView` needed — `@gorhom/bottom-sheet` handles everything
2433
- - Always use `SheetTextInput` (not plain `TextInput`) for auto-focus/blur handling
2673
+ - Use `<Input sheetMode />` (preferred) or `SheetTextInput` (low-level) for text inputs inside the sheet
2434
2674
  - Default `keyboardBehavior="interactive"` works on both platforms
2435
2675
  - Default `android_keyboardInputMode="adjustPan"` fixes the transparent gap that occurs with `adjustResize` when keyboard dismisses
2436
2676
  - `enableBlurKeyboardOnGesture={true}` (default) dismisses keyboard when dragging sheet
@@ -2694,7 +2934,6 @@ dismiss(id)
2694
2934
  | visible | `boolean` | required | Controls dialog visibility |
2695
2935
  | title | `string` | required | Dialog heading |
2696
2936
  | subtitle | `string` | — | Secondary text below title |
2697
- | description | `string` | — | **Deprecated** — use `subtitle` instead |
2698
2937
  | confirmLabel | `string` | `'Confirm'` | Confirm button text |
2699
2938
  | cancelLabel | `string` | `'Cancel'` | Cancel button text |
2700
2939
  | confirmVariant | `'primary' \| 'destructive'` | `'primary'` | Use `'destructive'` for delete/remove actions |
@@ -2704,12 +2943,15 @@ dismiss(id)
2704
2943
  | onCancel | `() => void` | required | Called when cancel is tapped or backdrop pressed |
2705
2944
 
2706
2945
  **Notes:**
2707
- - Powered by `@gorhom/bottom-sheet` with `enableDynamicSizing`
2946
+ - Powered by `@gorhom/bottom-sheet` `BottomSheetModal` with `enableDynamicSizing` (v12 refactor — `present()` / `dismiss()` driven, lazy-mounted, no `index={-1}` + `snapToIndex(0)` timing traps)
2947
+ - `topInset={insets.top}` applied automatically — dialog never crosses the notch / status bar
2948
+ - Internal `wasOpened` ref prevents `dismiss()` being called before the sheet is mounted (race-condition fix from v12.1)
2949
+ - A stable `name` prop (from `useId()`) is passed to `BottomSheetModal` for correct gorhom modal registry behavior when multiple modals are open
2708
2950
  - Buttons are full-width, stacked vertically (confirm on top)
2709
2951
  - Cancel shows X icon, confirm shows check (primary) or trash-2 (destructive) icon
2710
2952
  - Swipe down or backdrop press calls `onCancel`
2711
2953
 
2712
- **Haptics:** `impactLight` on open.
2954
+ **Haptics:** `impactMedium` on open.
2713
2955
 
2714
2956
  **Examples:**
2715
2957
  ```tsx
@@ -2717,7 +2959,7 @@ dismiss(id)
2717
2959
  <ConfirmDialog
2718
2960
  visible={showDelete}
2719
2961
  title="Delete transaction?"
2720
- description="$45.000 · Mercado · 12 mar · This action cannot be undone."
2962
+ subtitle="$45.000 · Mercado · 12 mar · This action cannot be undone."
2721
2963
  confirmLabel="Delete"
2722
2964
  confirmVariant="destructive"
2723
2965
  onConfirm={handleDelete}
@@ -2728,7 +2970,7 @@ dismiss(id)
2728
2970
  <ConfirmDialog
2729
2971
  visible={showConfirm}
2730
2972
  title="Send payment?"
2731
- description={`Send $${amount} to ${recipientName}`}
2973
+ subtitle={`Send $${amount} to ${recipientName}`}
2732
2974
  confirmLabel="Send"
2733
2975
  onConfirm={handleSend}
2734
2976
  onCancel={() => setShowConfirm(false)}
@@ -2738,7 +2980,7 @@ dismiss(id)
2738
2980
  <ConfirmDialog
2739
2981
  visible={showDiscard}
2740
2982
  title="Discard changes?"
2741
- description="All unsaved changes will be lost."
2983
+ subtitle="All unsaved changes will be lost."
2742
2984
  confirmLabel="Discard"
2743
2985
  confirmVariant="destructive"
2744
2986
  onConfirm={() => { setShowDiscard(false); goBack() }}
@@ -2769,7 +3011,7 @@ dismiss(id)
2769
3011
  | leftIconColor | `string` | — | Override left icon color (default: `foreground`) |
2770
3012
  | rightIconColor | `string` | — | Override right icon color (default: `foregroundMuted`) |
2771
3013
  | variant | `'plain' \| 'card'` | `'plain'` | `plain`: no background. `card`: surface with border and shadow |
2772
- | showChevron | `boolean` | `false` | Right-pointing chevron. Ignored when `rightRender` is set |
3014
+ | showChevron | `boolean` | `false` | Right-pointing chevron. Ignored when `rightActions`, `rightRender`, or `rightIcon` is set |
2773
3015
  | showSeparator | `boolean` | `false` | Hairline separator at bottom. Useful for stacking plain items |
2774
3016
  | onPress | `() => void` | — | Makes row pressable (scale animation + haptics) |
2775
3017
  | disabled | `boolean` | — | Reduces opacity to 0.45 |
@@ -2778,13 +3020,12 @@ dismiss(id)
2778
3020
  | subtitleStyle | `TextStyle` | — | Override subtitle text style |
2779
3021
  | subtitleNumberOfLines | `number` | `2` | Max lines for subtitle before truncation |
2780
3022
  | captionStyle | `TextStyle` | — | Override caption text style |
2781
- | icon | `ReactNode` | — | **Deprecated** use `leftRender` |
2782
- | trailing | `string \| ReactNode` | — | **Deprecated** — use `rightRender` |
3023
+ | rightActions | `ReactNode[]` | — | Multiple action buttons on the right with 8pt gap. Takes precedence over `rightRender` |
2783
3024
 
2784
3025
  **Slots:**
2785
3026
  - `leftRender` / `leftIcon` — 44×44pt fixed container, centered. Good for Avatar, icons, thumbnails
2786
3027
  - `rightRender` / `rightIcon` — max 160pt wide, right-aligned. Good for Badge, price text, Switch
2787
- - `showChevron` — `›` chevron, 24pt, `foregroundMuted` color. Only shows when `rightRender` is absent
3028
+ - `showChevron` — `›` chevron, 24pt, `foregroundMuted` color. Only shows when `rightRender`, `rightActions`, and `rightIcon` are absent
2788
3029
 
2789
3030
  **Separator inset:** Aligns to text block — `marginLeft` adjusts to skip over left slot when present.
2790
3031
 
@@ -3077,7 +3318,7 @@ dismiss(id)
3077
3318
 
3078
3319
  **Import:** `import { Chip, ChipGroup } from '@retray-dev/ui-kit'`
3079
3320
 
3080
- **When to use:** Inline filter options, quick selections, multi-select toggles. Use `Chip` for standalone custom logic; use `ChipGroup` for managed selection (single or multi).
3321
+ **When to use:** Inline filter options, quick selections, multi-select toggles. Use `Chip` for standalone custom logic; use `ChipGroup` for managed selection (single or multi). **ChipGroup vs CategoryStrip:** ChipGroup wraps to multiple rows and supports disabled items — best for filter tags, feature toggles, and tray-style selections. CategoryStrip scrolls horizontally and supports badge counts — best for top-of-screen category browsers.
3081
3322
 
3082
3323
  **`Chip` Props:**
3083
3324
 
@@ -3149,7 +3390,7 @@ const [categories, setCategories] = useState<number[]>([1, 3])
3149
3390
 
3150
3391
  **Import:** `import { CategoryStrip } from '@retray-dev/ui-kit'`
3151
3392
 
3152
- **When to use:** Horizontal scrollable filter/category bar at the top of browse screens — marketplace categories, content type filters, location tabs. Airbnb-style pill chips that scroll horizontally.
3393
+ **When to use:** Horizontal scrollable filter/category bar at the top of browse screens — marketplace categories, content type filters, location tabs. Airbnb-style pill chips that scroll horizontally. **CategoryStrip vs ChipGroup:** CategoryStrip scrolls horizontally and supports badge counts — best for top-of-screen category browsers. ChipGroup wraps to multiple rows and supports disabled items — best for filter tags and tray-style selections.
3153
3394
 
3154
3395
  | Prop | Type | Default | Notes |
3155
3396
  |------|------|---------|-------|
@@ -3274,7 +3515,7 @@ const [categories, setCategories] = useState<number[]>([1, 3])
3274
3515
  | Prop | Type | Default | Notes |
3275
3516
  |------|------|---------|-------|
3276
3517
  | label | `string \| ReactNode` | required | Left side. Strings auto-styled in `foregroundMuted` caption |
3277
- | value | `string` | required | Right side value text |
3518
+ | value | `string \| number \| ReactNode` | required | Right side value. Strings and numbers auto-styled in `Sohne-SemiBold`; pass `ReactNode` for custom content |
3278
3519
  | separator | `'dotted' \| 'solid' \| 'dashed' \| 'none'` | `'dotted'` | Fill line between label and value |
3279
3520
  | labelWeight | `'normal' \| 'medium' \| 'semibold' \| 'bold'` | `'normal'` | Font weight of label text |
3280
3521
  | valueColor | `string` | — | Override value text color (hex or theme token value) |
@@ -3402,22 +3643,22 @@ const [period, setPeriod] = useState({
3402
3643
 
3403
3644
  **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.
3404
3645
 
3405
- **Extends:** `TouchableOpacityProps` (all native props pass through except `activeOpacity`)
3406
-
3407
3646
  | Prop | Type | Default | Notes |
3408
3647
  |------|------|---------|-------|
3409
3648
  | children | `ReactNode` | required | Content to render inside the pressable |
3410
3649
  | onPress | `() => void` | — | Press handler |
3411
3650
  | pressScale | `number` | `0.98` | Scale value on press (MediaCard-style) |
3412
- | bounciness | `number` | `4` | Spring bounciness on release |
3413
3651
  | haptics | `boolean` | `true` | Enable haptic feedback on press |
3414
3652
  | hoverScale | `number` | `1.02` | Hover scale (web only). Set to `1` to disable |
3415
3653
  | disabled | `boolean` | `false` | Disable interaction |
3654
+ | accessibilityRole | `AccessibilityRole` | `'button'` | Override the accessibility role for screen readers |
3655
+ | accessibilityState | `Record<string, unknown>` | `{ disabled: !!disabled }` | Accessibility state for selected/expanded/checked |
3656
+ | accessibilityLabel | `string` | — | Accessibility label for screen readers |
3416
3657
  | style | `ViewStyle` | — | Animated wrapper style |
3417
3658
 
3418
3659
  **Behavior:**
3419
3660
  - Press: springs to `pressScale` (default 0.98) with `speed: 40, bounciness: 0`
3420
- - Release: springs back to 1.0 with `speed: 40, bounciness: 4`
3661
+ - Release: springs back to 1.0 with `speed: 40`
3421
3662
  - Web: optional hover scale (default 1.02)
3422
3663
  - Haptics: `impactLight` on press (unless `haptics={false}`)
3423
3664
 
@@ -3436,7 +3677,7 @@ const [period, setPeriod] = useState({
3436
3677
  </Pressable>
3437
3678
 
3438
3679
  // Wrapping complex layout
3439
- <Pressable onPress={handleSelect} pressScale={0.96} bounciness={8}>
3680
+ <Pressable onPress={handleSelect} pressScale={0.96}>
3440
3681
  <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, padding: 16 }}>
3441
3682
  <Avatar src={user.avatar} size="md" />
3442
3683
  <View style={{ flex: 1 }}>
@@ -3453,7 +3694,7 @@ const [period, setPeriod] = useState({
3453
3694
  </Pressable>
3454
3695
 
3455
3696
  // Custom press scale (deeper press)
3456
- <Pressable onPress={handlePress} pressScale={0.92} bounciness={6}>
3697
+ <Pressable onPress={handlePress} pressScale={0.92}>
3457
3698
  {/* content */}
3458
3699
  </Pressable>
3459
3700
  ```
@@ -3725,6 +3966,76 @@ export default function TabLayout() {
3725
3966
 
3726
3967
  ---
3727
3968
 
3969
+ ### Stats
3970
+
3971
+ **Import:** `import { Stats } from '@retray-dev/ui-kit'`
3972
+
3973
+ **When to use:** Dashboards and analytics screens — single-stat card with a large value, icon, label, and optional description. Tappable cards wire haptics automatically.
3974
+
3975
+ | Prop | Type | Default | Notes |
3976
+ |------|------|---------|-------|
3977
+ | value | `string` | required | Large display value, e.g. `"$12,450"` or `"847"` |
3978
+ | label | `string` | required | Label below the value, e.g. `"Monthly Revenue"` |
3979
+ | description | `string` | — | Third line — smaller, muted text for trend/context |
3980
+ | icon | `React.ReactNode` | — | Custom icon node |
3981
+ | iconName | `string` | — | Icon from `@expo/vector-icons` (left of value, `colors.primary`) |
3982
+ | iconColor | `string` | `colors.primary` | Override icon color |
3983
+ | variant | `'elevated' \| 'outlined' \| 'filled'` | `'elevated'` | Card surface variant |
3984
+ | onPress | `() => void` | — | Makes the card pressable (haptic `impactLight()`) |
3985
+ | style | `ViewStyle` | — | — |
3986
+ | accessibilityLabel | `string` | — | — |
3987
+
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.
3989
+
3990
+ **`Stats.Group`** — horizontal layout wrapper that distributes children equally:
3991
+
3992
+ | Prop | Type | Default | Notes |
3993
+ |------|------|---------|-------|
3994
+ | children | `React.ReactNode` | required | `Stats` components to arrange horizontally |
3995
+ | gap | `number` | `s(12)` | Spacing between cards |
3996
+ | style | `ViewStyle` | — | — |
3997
+
3998
+ **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.
3999
+
4000
+ **Example:**
4001
+ ```tsx
4002
+ // Standalone
4003
+ <Stats
4004
+ value="$12,450"
4005
+ label="Monthly Revenue"
4006
+ description="+12% from last month"
4007
+ iconName="trending-up"
4008
+ />
4009
+
4010
+ // 3-up — short values only, no description
4011
+ <Stats.Group>
4012
+ <Stats value="847" label="Users" variant="elevated" />
4013
+ <Stats value="98%" label="Uptime" variant="outlined" />
4014
+ <Stats value="12" label="Alerts" variant="filled" />
4015
+ </Stats.Group>
4016
+
4017
+ // 2-up with icons, long values, and description
4018
+ <Stats.Group gap={s(16)}>
4019
+ <Stats
4020
+ value="$8,200"
4021
+ label="Spent"
4022
+ description="This month"
4023
+ iconName="credit-card"
4024
+ variant="elevated"
4025
+ onPress={() => navigateToBilling()}
4026
+ />
4027
+ <Stats
4028
+ value="$2,150"
4029
+ label="Saved"
4030
+ description="12% of budget"
4031
+ iconName="trending-up"
4032
+ variant="outlined"
4033
+ />
4034
+ </Stats.Group>
4035
+ ```
4036
+
4037
+ ---
4038
+
3728
4039
  ### ErrorBoundary
3729
4040
 
3730
4041
  **Import:** `import { ErrorBoundary } from '@retray-dev/ui-kit'`
@@ -3920,6 +4231,7 @@ import { HolographicCard, FOIL_PRESETS } from '@retray-dev/ui-kit/HolographicCar
3920
4231
  | onChange | `(uri: string \| null) => void` | — | Called with selected URI after picker completes |
3921
4232
  | loading | `boolean` | `false` | Show spinner overlay (e.g. while uploading to server) |
3922
4233
  | placeholder | `string` | `'Tap to add image'` | Text shown when no image selected |
4234
+ | showPlaceholderText | `boolean` | `true` | Whether to show the placeholder text. Use `false` for compact/avatar variants |
3923
4235
  | width | `number` | — | Width of the upload area. Defaults to full width |
3924
4236
  | height | `number` | `200` | Height of the upload area |
3925
4237
  | borderRadius | `number` | `RADIUS.lg` | — |
@@ -3949,18 +4261,72 @@ const handleChange = async (uri: string | null) => {
3949
4261
  height={200}
3950
4262
  />
3951
4263
 
3952
- // Avatar upload (square)
4264
+ // Avatar upload (square, no text)
3953
4265
  <ImageUpload
3954
4266
  value={avatarUri}
3955
4267
  onChange={setAvatarUri}
3956
4268
  width={100}
3957
4269
  height={100}
3958
4270
  borderRadius={50}
4271
+ showPlaceholderText={false}
3959
4272
  />
3960
4273
  ```
3961
4274
 
3962
4275
  ---
3963
4276
 
4277
+ ### IconPicker
4278
+
4279
+ **Import:** `import { IconPicker } from '@retray-dev/ui-kit'`
4280
+
4281
+ **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.
4282
+
4283
+ **Requires:** `@gorhom/bottom-sheet` (already a peer dependency of the UI kit).
4284
+
4285
+ | Prop | Type | Default | Notes |
4286
+ |------|------|---------|-------|
4287
+ | value | `string \| null` | — | Currently selected icon name |
4288
+ | onChange | `(iconName: string) => void` | — | Called when an icon is selected |
4289
+ | label | `string` | — | Optional label above the trigger |
4290
+ | error | `string` | — | Error message (red border + helper text) |
4291
+ | hint | `string` | — | Hint text below trigger |
4292
+ | disabled | `boolean` | `false` | Prevents opening |
4293
+ | numColumns | `number` | `6` | Icons per row in the sheet grid |
4294
+ | gap | `number` | `6` | Gap between cells (dp) |
4295
+ | style | `ViewStyle` | — | — |
4296
+
4297
+ **Examples:**
4298
+ ```tsx
4299
+ const [icon, setIcon] = useState<string | null>(null)
4300
+
4301
+ // Basic usage
4302
+ <IconPicker
4303
+ label="Icon"
4304
+ value={icon}
4305
+ onChange={setIcon}
4306
+ />
4307
+
4308
+ // With validation error
4309
+ <IconPicker
4310
+ label="Icon"
4311
+ value={icon}
4312
+ onChange={setIcon}
4313
+ error={!icon ? 'Please select an icon' : undefined}
4314
+ />
4315
+ ```
4316
+
4317
+ **Notes:**
4318
+ - 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.
4319
+ - 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.
4320
+ - **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.
4321
+ - 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.
4322
+ - Grid cells show only icons — clean, fast visual scanning. Cell size adapts to container width.
4323
+ - Sheet uses `enableDynamicSizing` with `maxDynamicContentSize` (70% screen height) — height auto-fits content up to that cap. `topInset={insets.top}` is applied automatically.
4324
+ - Category strip is a horizontal `ScrollView` of pill-shaped chips. The grid is rendered in rows inside a `BottomSheetScrollView`.
4325
+ - Selection closes the sheet immediately and resets category to "Todos".
4326
+ - Haptics: medium impact on open, selection on icon tap.
4327
+
4328
+ ---
4329
+
3964
4330
  ### useConfirmDialog
3965
4331
 
3966
4332
  **Import:** `import { useConfirmDialog } from '@retray-dev/ui-kit'`
@@ -4063,6 +4429,21 @@ import { renderIcon } from '@retray-dev/ui-kit'
4063
4429
  const icon = renderIcon('check', 18, colors.primary)
4064
4430
  ```
4065
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.
4446
+
4066
4447
  ### `iconName` props on components
4067
4448
 
4068
4449
  All components with icon slots accept `iconName` — auto-resolved size and color: