@retray-dev/ui-kit 12.2.0 → 13.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/CONSUMER.md +26 -11
  2. package/DESIGN.md +2 -2
  3. package/README.md +15 -11
  4. package/{COMPONENTS.md → SKILL.md} +374 -996
  5. package/dist/Accordion.d.mts +2 -0
  6. package/dist/Accordion.d.ts +2 -0
  7. package/dist/Accordion.js +49 -210
  8. package/dist/Accordion.mjs +6 -6
  9. package/dist/AlertBanner.js +29 -153
  10. package/dist/AlertBanner.mjs +3 -4
  11. package/dist/AppHeader.d.mts +5 -2
  12. package/dist/AppHeader.d.ts +5 -2
  13. package/dist/AppHeader.js +45 -239
  14. package/dist/AppHeader.mjs +6 -8
  15. package/dist/Avatar.d.mts +17 -1
  16. package/dist/Avatar.d.ts +17 -1
  17. package/dist/Avatar.js +80 -115
  18. package/dist/Avatar.mjs +2 -3
  19. package/dist/Badge.js +24 -149
  20. package/dist/Badge.mjs +3 -4
  21. package/dist/Button.js +79 -267
  22. package/dist/Button.mjs +6 -7
  23. package/dist/ButtonGroup.mjs +0 -1
  24. package/dist/Card.js +15 -200
  25. package/dist/Card.mjs +4 -6
  26. package/dist/CategoryStrip.d.mts +0 -5
  27. package/dist/CategoryStrip.d.ts +0 -5
  28. package/dist/CategoryStrip.js +47 -265
  29. package/dist/CategoryStrip.mjs +6 -7
  30. package/dist/Checkbox.d.mts +2 -1
  31. package/dist/Checkbox.d.ts +2 -1
  32. package/dist/Checkbox.js +18 -201
  33. package/dist/Checkbox.mjs +5 -6
  34. package/dist/Chip.js +44 -236
  35. package/dist/Chip.mjs +7 -7
  36. package/dist/ConfirmDialog.d.mts +2 -1
  37. package/dist/ConfirmDialog.d.ts +2 -1
  38. package/dist/ConfirmDialog.js +110 -300
  39. package/dist/ConfirmDialog.mjs +7 -8
  40. package/dist/CurrencyDisplay.js +1 -114
  41. package/dist/CurrencyDisplay.mjs +2 -3
  42. package/dist/CurrencyInput.js +35 -162
  43. package/dist/CurrencyInput.mjs +5 -6
  44. package/dist/DetailRow.js +25 -150
  45. package/dist/DetailRow.mjs +3 -4
  46. package/dist/EmptyState.js +80 -268
  47. package/dist/EmptyState.mjs +7 -8
  48. package/dist/ErrorBoundary.js +32 -199
  49. package/dist/ErrorBoundary.mjs +4 -5
  50. package/dist/Form.js +1 -114
  51. package/dist/Form.mjs +2 -3
  52. package/dist/HolographicCard.d.mts +0 -28
  53. package/dist/HolographicCard.d.ts +0 -28
  54. package/dist/HolographicCard.js +20 -130
  55. package/dist/HolographicCard.mjs +9 -33
  56. package/dist/IconButton.js +36 -234
  57. package/dist/IconButton.mjs +5 -7
  58. package/dist/IconPicker.js +222 -929
  59. package/dist/IconPicker.mjs +5 -6
  60. package/dist/ImageUpload.d.mts +3 -3
  61. package/dist/ImageUpload.d.ts +3 -3
  62. package/dist/ImageUpload.js +49 -238
  63. package/dist/ImageUpload.mjs +5 -7
  64. package/dist/ImageViewer.js +75 -266
  65. package/dist/ImageViewer.mjs +8 -9
  66. package/dist/Input.d.mts +1 -1
  67. package/dist/Input.d.ts +1 -1
  68. package/dist/Input.js +35 -162
  69. package/dist/Input.mjs +4 -5
  70. package/dist/LabelValue.js +24 -149
  71. package/dist/LabelValue.mjs +3 -4
  72. package/dist/ListGroup.js +1 -114
  73. package/dist/ListGroup.mjs +2 -3
  74. package/dist/ListItem.d.mts +2 -1
  75. package/dist/ListItem.d.ts +2 -1
  76. package/dist/ListItem.js +41 -236
  77. package/dist/ListItem.mjs +5 -7
  78. package/dist/MediaCard.d.mts +0 -14
  79. package/dist/MediaCard.d.ts +0 -14
  80. package/dist/MediaCard.js +69 -315
  81. package/dist/MediaCard.mjs +5 -7
  82. package/dist/MenuGroup.js +1 -114
  83. package/dist/MenuGroup.mjs +2 -3
  84. package/dist/MenuItem.d.mts +2 -1
  85. package/dist/MenuItem.d.ts +2 -1
  86. package/dist/MenuItem.js +39 -235
  87. package/dist/MenuItem.mjs +5 -7
  88. package/dist/MonthPicker.js +8 -163
  89. package/dist/MonthPicker.mjs +3 -4
  90. package/dist/NumberStepper.d.mts +2 -1
  91. package/dist/NumberStepper.d.ts +2 -1
  92. package/dist/NumberStepper.js +44 -239
  93. package/dist/NumberStepper.mjs +5 -7
  94. package/dist/PagerDots.d.mts +1 -1
  95. package/dist/PagerDots.d.ts +1 -1
  96. package/dist/PagerDots.js +69 -224
  97. package/dist/PagerDots.mjs +6 -6
  98. package/dist/Pressable.js +14 -85
  99. package/dist/Pressable.mjs +4 -5
  100. package/dist/PricingCard.js +87 -272
  101. package/dist/PricingCard.mjs +8 -9
  102. package/dist/Progress.js +3 -123
  103. package/dist/Progress.mjs +3 -4
  104. package/dist/RadioGroup.js +52 -265
  105. package/dist/RadioGroup.mjs +5 -6
  106. package/dist/RetrayProvider.js +3 -6
  107. package/dist/RetrayProvider.mjs +3 -4
  108. package/dist/Select.d.mts +3 -1
  109. package/dist/Select.d.ts +3 -1
  110. package/dist/Select.js +27 -233
  111. package/dist/Select.mjs +4 -6
  112. package/dist/SelectableCard.js +33 -209
  113. package/dist/SelectableCard.mjs +5 -6
  114. package/dist/SelectableGrid.d.mts +0 -21
  115. package/dist/SelectableGrid.d.ts +0 -21
  116. package/dist/SelectableGrid.js +49 -272
  117. package/dist/SelectableGrid.mjs +5 -7
  118. package/dist/Separator.js +1 -114
  119. package/dist/Separator.mjs +2 -3
  120. package/dist/Sheet.d.mts +1 -1
  121. package/dist/Sheet.d.ts +1 -1
  122. package/dist/Sheet.js +33 -175
  123. package/dist/Sheet.mjs +3 -4
  124. package/dist/SheetSelect.js +39 -236
  125. package/dist/SheetSelect.mjs +6 -7
  126. package/dist/Skeleton.js +4 -124
  127. package/dist/Skeleton.mjs +3 -4
  128. package/dist/Slider.d.mts +2 -1
  129. package/dist/Slider.d.ts +2 -1
  130. package/dist/Slider.js +8 -161
  131. package/dist/Slider.mjs +3 -4
  132. package/dist/Spinner.js +3 -116
  133. package/dist/Spinner.mjs +2 -3
  134. package/dist/Stats.js +36 -234
  135. package/dist/Stats.mjs +5 -7
  136. package/dist/Switch.d.mts +2 -1
  137. package/dist/Switch.d.ts +2 -1
  138. package/dist/Switch.js +26 -176
  139. package/dist/Switch.mjs +5 -5
  140. package/dist/TabBar.js +43 -200
  141. package/dist/TabBar.mjs +5 -5
  142. package/dist/Tabs.js +15 -199
  143. package/dist/Tabs.mjs +5 -6
  144. package/dist/Text.js +9 -130
  145. package/dist/Text.mjs +2 -3
  146. package/dist/Textarea.d.mts +2 -1
  147. package/dist/Textarea.d.ts +2 -1
  148. package/dist/Textarea.js +71 -219
  149. package/dist/Textarea.mjs +4 -5
  150. package/dist/Toast.d.mts +12 -10
  151. package/dist/Toast.d.ts +12 -10
  152. package/dist/Toast.js +1 -114
  153. package/dist/Toast.mjs +2 -3
  154. package/dist/Toggle.js +39 -236
  155. package/dist/Toggle.mjs +6 -7
  156. package/dist/{chunk-ELGEOM7I.mjs → chunk-2QXJDRVU.mjs} +13 -10
  157. package/dist/{chunk-LIS6I5UP.mjs → chunk-2VIDP72N.mjs} +3 -3
  158. package/dist/{chunk-NHDI3VQB.mjs → chunk-422IVD3H.mjs} +16 -12
  159. package/dist/{chunk-DF7JA72E.mjs → chunk-4NQFTHN3.mjs} +13 -7
  160. package/dist/{chunk-3XCFYSX4.mjs → chunk-5MYNAAFE.mjs} +13 -17
  161. package/dist/{chunk-E7NEHHXV.mjs → chunk-62BBSSUF.mjs} +3 -3
  162. package/dist/{chunk-UBUXUMER.mjs → chunk-77UOVFIS.mjs} +7 -5
  163. package/dist/{chunk-M3C7XM2M.mjs → chunk-7BZJRB77.mjs} +28 -18
  164. package/dist/chunk-ARONDO7M.mjs +40 -0
  165. package/dist/{chunk-GH67YXG6.mjs → chunk-AZV7KNJI.mjs} +3 -3
  166. package/dist/{chunk-2P2CB235.mjs → chunk-BULKGOIZ.mjs} +7 -8
  167. package/dist/{chunk-RJNLAH76.mjs → chunk-C5ZRMR2E.mjs} +4 -2
  168. package/dist/chunk-CM2DG4MR.mjs +142 -0
  169. package/dist/{chunk-UQ4742ET.mjs → chunk-COA2YZOX.mjs} +8 -6
  170. package/dist/{chunk-EDLCGYIO.mjs → chunk-CZN6L2QU.mjs} +11 -8
  171. package/dist/{chunk-TS7DGUIR.mjs → chunk-DBHSUUKU.mjs} +2 -2
  172. package/dist/{chunk-57V2LXCK.mjs → chunk-DE25XTVQ.mjs} +3 -3
  173. package/dist/{chunk-RMRS44MQ.mjs → chunk-E2PONRJG.mjs} +13 -9
  174. package/dist/{chunk-GUTDFUNF.mjs → chunk-EHGBHFMH.mjs} +9 -17
  175. package/dist/{chunk-ZIMY2QUM.mjs → chunk-ERWJPVX7.mjs} +2 -2
  176. package/dist/{chunk-NLZY4TXU.mjs → chunk-ESQDPO5E.mjs} +7 -7
  177. package/dist/{chunk-VJBUCITV.mjs → chunk-EW2FIDSM.mjs} +1 -1
  178. package/dist/{chunk-HC4VVCWY.mjs → chunk-FTTI6T5Q.mjs} +4 -4
  179. package/dist/{chunk-MVMGPZN6.mjs → chunk-H6MQL7PS.mjs} +12 -7
  180. package/dist/{chunk-CF27NBXO.mjs → chunk-HHOOFDBA.mjs} +38 -41
  181. package/dist/{chunk-2HFD4IHU.mjs → chunk-HUSSF6TF.mjs} +1 -1
  182. package/dist/{chunk-HEDQPK4I.mjs → chunk-IDVUZIVY.mjs} +16 -22
  183. package/dist/chunk-IFYMBOEN.mjs +14 -0
  184. package/dist/{chunk-QOLWA2PW.mjs → chunk-IGU223UM.mjs} +80 -4
  185. package/dist/chunk-IJCMPVW5.mjs +121 -0
  186. package/dist/{chunk-AENAVIKT.mjs → chunk-ITG4JQM3.mjs} +4 -4
  187. package/dist/{chunk-E5UKLSJZ.mjs → chunk-K3QX2M26.mjs} +11 -8
  188. package/dist/{chunk-4OORJ2DY.mjs → chunk-K7TKID3V.mjs} +8 -7
  189. package/dist/{chunk-2LG326TT.mjs → chunk-KAGADD2O.mjs} +4 -4
  190. package/dist/{chunk-IVSRW4HS.mjs → chunk-KC5QDYGZ.mjs} +4 -4
  191. package/dist/{chunk-7AFZWSCI.mjs → chunk-KPTY7UYQ.mjs} +1 -1
  192. package/dist/{chunk-YTXRIXNZ.mjs → chunk-KSSVIFYR.mjs} +9 -12
  193. package/dist/chunk-L3YKPTJQ.mjs +119 -0
  194. package/dist/chunk-M53LC4Q7.mjs +35 -0
  195. package/dist/chunk-MZ6WRTD2.mjs +40 -0
  196. package/dist/chunk-NGEN2EES.mjs +581 -0
  197. package/dist/{chunk-ZR6HSEAB.mjs → chunk-NPCBNGNE.mjs} +17 -26
  198. package/dist/{chunk-C43HRKXH.mjs → chunk-OBV72JD4.mjs} +1 -1
  199. package/dist/{chunk-LPV4NJJK.mjs → chunk-PGQ6FMXS.mjs} +6 -5
  200. package/dist/{chunk-MEPSKGBO.mjs → chunk-PI6RULJX.mjs} +1 -1
  201. package/dist/{chunk-F3YTWO3T.mjs → chunk-RA6SAAFE.mjs} +9 -8
  202. package/dist/{chunk-UNNRUJTM.mjs → chunk-RRKM4MKB.mjs} +7 -7
  203. package/dist/{chunk-ULGNQPNE.mjs → chunk-S2VGME7X.mjs} +1 -1
  204. package/dist/{chunk-OLVJFKXS.mjs → chunk-S44XWTTC.mjs} +35 -25
  205. package/dist/{chunk-YMYIEVZP.mjs → chunk-SZEKQAOY.mjs} +1 -1
  206. package/dist/{chunk-BXF4AMHY.mjs → chunk-TMH263OK.mjs} +5 -4
  207. package/dist/{chunk-NJG7DHVF.mjs → chunk-U6DEBYU5.mjs} +10 -9
  208. package/dist/{chunk-QXDGGOLC.mjs → chunk-UMZTPUB3.mjs} +33 -21
  209. package/dist/{chunk-KSUWPU2F.mjs → chunk-WIPEDNSD.mjs} +7 -7
  210. package/dist/{chunk-QDAZGZUF.mjs → chunk-XCIG6HT2.mjs} +3 -3
  211. package/dist/{chunk-4J2PXL36.mjs → chunk-Y6YS33GM.mjs} +40 -38
  212. package/dist/{chunk-4XOB5TTD.mjs → chunk-ZKDKKQCE.mjs} +5 -5
  213. package/dist/{chunk-LOBLCFMN.mjs → chunk-ZTPYUU5C.mjs} +5 -5
  214. package/dist/fonts.mjs +0 -2
  215. package/dist/index.d.mts +13 -73
  216. package/dist/index.d.ts +13 -73
  217. package/dist/index.js +1149 -1892
  218. package/dist/index.mjs +81 -86
  219. package/package.json +20 -20
  220. package/src/components/Accordion/Accordion.tsx +15 -9
  221. package/src/components/AlertBanner/AlertBanner.tsx +7 -6
  222. package/src/components/AppHeader/AppHeader.tsx +25 -10
  223. package/src/components/Avatar/Avatar.tsx +92 -1
  224. package/src/components/Avatar/index.ts +2 -2
  225. package/src/components/Badge/Badge.tsx +2 -2
  226. package/src/components/Button/Button.tsx +50 -46
  227. package/src/components/Card/Card.tsx +1 -0
  228. package/src/components/CategoryStrip/CategoryStrip.tsx +36 -49
  229. package/src/components/Checkbox/Checkbox.tsx +3 -0
  230. package/src/components/Chip/Chip.tsx +5 -4
  231. package/src/components/ConfirmDialog/ConfirmDialog.tsx +33 -17
  232. package/src/components/DetailRow/DetailRow.tsx +3 -3
  233. package/src/components/EmptyState/EmptyState.tsx +2 -2
  234. package/src/components/ErrorBoundary/ErrorBoundary.tsx +6 -6
  235. package/src/components/HolographicCard/HolographicCard.tsx +14 -95
  236. package/src/components/IconButton/IconButton.tsx +2 -2
  237. package/src/components/IconPicker/IconPicker.tsx +13 -12
  238. package/src/components/ImageUpload/ImageUpload.tsx +43 -46
  239. package/src/components/ImageViewer/ImageViewer.tsx +3 -3
  240. package/src/components/Input/Input.tsx +11 -5
  241. package/src/components/LabelValue/LabelValue.tsx +2 -2
  242. package/src/components/ListItem/ListItem.tsx +7 -4
  243. package/src/components/MediaCard/MediaCard.tsx +21 -59
  244. package/src/components/MenuItem/MenuItem.tsx +5 -2
  245. package/src/components/MonthPicker/MonthPicker.tsx +2 -2
  246. package/src/components/NumberStepper/NumberStepper.tsx +10 -6
  247. package/src/components/PagerDots/PagerDots.tsx +38 -28
  248. package/src/components/PricingCard/PricingCard.tsx +6 -6
  249. package/src/components/RadioGroup/RadioGroup.tsx +18 -31
  250. package/src/components/Select/Select.tsx +35 -39
  251. package/src/components/SelectableCard/SelectableCard.tsx +4 -6
  252. package/src/components/SelectableGrid/SelectableGrid.tsx +37 -72
  253. package/src/components/Sheet/Sheet.tsx +28 -15
  254. package/src/components/Sheet/index.ts +2 -2
  255. package/src/components/SheetSelect/SheetSelect.tsx +3 -3
  256. package/src/components/Skeleton/Skeleton.tsx +1 -1
  257. package/src/components/Slider/Slider.tsx +3 -0
  258. package/src/components/Spinner/Spinner.tsx +2 -2
  259. package/src/components/Stats/Stats.tsx +2 -2
  260. package/src/components/Switch/Switch.tsx +12 -7
  261. package/src/components/TabBar/TabBar.tsx +9 -8
  262. package/src/components/Text/Text.tsx +13 -1
  263. package/src/components/Textarea/Textarea.tsx +18 -32
  264. package/src/components/Toggle/Toggle.tsx +3 -3
  265. package/src/hooks/useConfirmDialog.ts +31 -42
  266. package/src/index.ts +3 -4
  267. package/src/theme/ThemeProvider.tsx +1 -4
  268. package/src/theme/colorUtils.ts +1 -72
  269. package/src/theme/colors.ts +40 -1
  270. package/src/theme/types.ts +2 -2
  271. package/src/utils/animations.ts +0 -47
  272. package/src/utils/curatedIcons.ts +93 -801
  273. package/src/utils/haptics.ts +13 -208
  274. package/src/utils/icons.ts +27 -91
  275. package/src/utils/pressable.ts +10 -61
  276. package/dist/VirtualList.d.mts +0 -19
  277. package/dist/VirtualList.d.ts +0 -19
  278. package/dist/VirtualList.js +0 -38
  279. package/dist/VirtualList.mjs +0 -2
  280. package/dist/chunk-2BA3JMKK.mjs +0 -136
  281. package/dist/chunk-3DKJ2GIC.mjs +0 -30
  282. package/dist/chunk-7ELGZ66G.mjs +0 -164
  283. package/dist/chunk-DVK4G2GT.mjs +0 -59
  284. package/dist/chunk-EJ7ZPXOH.mjs +0 -163
  285. package/dist/chunk-KA7LTET3.mjs +0 -71
  286. package/dist/chunk-LNPKGWBG.mjs +0 -134
  287. package/dist/chunk-NC5ZTR2Y.mjs +0 -32
  288. package/dist/chunk-SAWUXP3A.mjs +0 -1114
  289. package/dist/chunk-Y6FXYEAI.mjs +0 -8
  290. package/dist/chunk-YNROWHQJ.mjs +0 -46
  291. package/src/components/VirtualList/VirtualList.tsx +0 -60
  292. package/src/components/VirtualList/index.ts +0 -1
  293. package/src/utils/fontGuard.ts +0 -35
  294. package/src/utils/hover.ts +0 -25
  295. package/src/utils/useColorTransition.ts +0 -40
  296. package/src/utils/usePressScale.ts +0 -75
@@ -1,13 +1,11 @@
1
1
  import React, { useRef, useState } from 'react'
2
2
  import { View, Text, TouchableOpacity, Modal, StyleSheet, ViewStyle, Platform } from 'react-native'
3
- import Animated from 'react-native-reanimated'
4
3
  import { Picker } from '@react-native-picker/picker'
5
4
  import { Entypo } from '@expo/vector-icons'
6
5
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
7
6
  import { useTheme } from '../../theme'
8
7
  import { s, vs, ms } from '../../utils/scaling'
9
- import { usePressScale } from '../../utils/usePressScale'
10
- import { PRESS_SCALE } from '../../utils/animations'
8
+ import { PressableButton } from '../../utils/pressable'
11
9
 
12
10
  const isIOS = Platform.OS === 'ios'
13
11
  const isAndroid = Platform.OS === 'android'
@@ -25,10 +23,12 @@ export interface SelectProps {
25
23
  onValueChange?: (value: string) => void
26
24
  placeholder?: string
27
25
  label?: string
26
+ hint?: string
28
27
  error?: string
29
28
  disabled?: boolean
30
29
  style?: ViewStyle
31
30
  accessibilityLabel?: string
31
+ accessibilityHint?: string
32
32
  }
33
33
 
34
34
  export function Select({
@@ -37,16 +37,14 @@ export function Select({
37
37
  onValueChange,
38
38
  placeholder = 'Select an option',
39
39
  label,
40
+ hint,
40
41
  error,
41
42
  disabled,
42
43
  style,
43
44
  accessibilityLabel,
45
+ accessibilityHint,
44
46
  }: SelectProps) {
45
47
  const { colors } = useTheme()
46
- const { animatedStyle, onPressIn, onPressOut } = usePressScale({
47
- pressScale: PRESS_SCALE.button,
48
- disabled,
49
- })
50
48
  const [pickerVisible, setPickerVisible] = useState(false)
51
49
  const [pendingValue, setPendingValue] = useState<string | undefined>(value)
52
50
  const pickerRef = useRef<React.ElementRef<typeof Picker>>(null)
@@ -82,38 +80,37 @@ export function Select({
82
80
 
83
81
  {/* Trigger button — shown on iOS and Android only */}
84
82
  {!isWeb ? (
85
- <Animated.View style={[animatedStyle, { opacity: disabled ? 0.45 : 1 }]}>
86
- <TouchableOpacity
83
+ <PressableButton
84
+ style={[
85
+ styles.trigger,
86
+ {
87
+ borderColor: error ? colors.destructive : colors.border,
88
+ backgroundColor: colors.background,
89
+ },
90
+ disabled && { opacity: 0.45 },
91
+ ]}
92
+ onPress={handleOpen}
93
+ enabled={!disabled}
94
+ rippleColor="transparent"
95
+ touchSoundDisabled
96
+ accessibilityRole="combobox"
97
+ accessibilityLabel={accessibilityLabel ?? label}
98
+ accessibilityHint={accessibilityHint}
99
+ accessibilityValue={{ text: selected?.label ?? placeholder }}
100
+ accessibilityState={{ disabled: !!disabled, expanded: pickerVisible }}
101
+ >
102
+ <Text
87
103
  style={[
88
- styles.trigger,
89
- {
90
- borderColor: error ? colors.destructive : colors.border,
91
- backgroundColor: colors.background,
92
- },
104
+ styles.triggerText,
105
+ { color: selected ? colors.foreground : colors.foregroundMuted },
93
106
  ]}
94
- onPress={handleOpen}
95
- onPressIn={onPressIn}
96
- onPressOut={onPressOut}
97
- activeOpacity={1}
98
- touchSoundDisabled={true}
99
- accessibilityRole="combobox"
100
- accessibilityLabel={accessibilityLabel ?? label}
101
- accessibilityValue={{ text: selected?.label ?? placeholder }}
102
- accessibilityState={{ disabled: !!disabled, expanded: pickerVisible }}
107
+ numberOfLines={1}
108
+ allowFontScaling={true}
103
109
  >
104
- <Text
105
- style={[
106
- styles.triggerText,
107
- { color: selected ? colors.foreground : colors.foregroundMuted },
108
- ]}
109
- numberOfLines={1}
110
- allowFontScaling={true}
111
- >
112
- {selected?.label ?? placeholder}
113
- </Text>
114
- <Entypo name="chevron-down" size={20} color={colors.foregroundMuted} />
115
- </TouchableOpacity>
116
- </Animated.View>
110
+ {selected?.label ?? placeholder}
111
+ </Text>
112
+ <Entypo name="chevron-down" size={20} color={colors.foregroundMuted} />
113
+ </PressableButton>
117
114
  ) : null}
118
115
 
119
116
  {/* iOS: Modal with wheel Picker */}
@@ -224,6 +221,8 @@ export function Select({
224
221
 
225
222
  {error ? (
226
223
  <Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
224
+ ) : !error && hint ? (
225
+ <Text style={[styles.helperText, { color: colors.foregroundMuted }]} allowFontScaling={true}>{hint}</Text>
227
226
  ) : null}
228
227
  </View>
229
228
  )
@@ -251,9 +250,6 @@ const styles = StyleSheet.create({
251
250
  fontSize: ms(15),
252
251
  flex: 1,
253
252
  },
254
- chevron: {
255
- marginLeft: s(8),
256
- },
257
253
  helperText: {
258
254
  fontFamily: 'Sohne-Regular',
259
255
  fontSize: ms(13),
@@ -5,7 +5,7 @@ import { impactLight } from '../../utils/haptics'
5
5
  import { useTheme } from '../../theme'
6
6
  import { s, vs, ms, mvs } from '../../utils/scaling'
7
7
  import { RADIUS } from '../../tokens'
8
- import { renderIcon } from '../../utils/icons'
8
+ import { Icon } from '../../utils/icons'
9
9
  import { COLOR_TRANSITION, OPACITY_TRANSITION, SPRING_ELASTIC } from '../../utils/animations'
10
10
 
11
11
  type SelectType = 'radio' | 'checkbox'
@@ -146,20 +146,18 @@ export function SelectableCard({
146
146
  })()
147
147
 
148
148
  const resolvedIcon = iconName
149
- ? renderIcon(iconName, ms(22), disabled ? colors.foregroundMuted : colors.foregroundMuted)
149
+ ? <Icon name={iconName} size={ms(22)} color={disabled ? colors.foregroundMuted : colors.foregroundMuted} />
150
150
  : icon
151
151
 
152
152
  const resolvedIconElement = resolvedIcon ? (
153
153
  <View style={[styles.iconWrapper, disabled && { opacity: 0.45 }]}>{resolvedIcon}</View>
154
154
  ) : null
155
155
 
156
- const selectorAccessibilityRole = type === 'radio' ? 'radio' : 'checkbox'
157
-
158
156
  return (
159
157
  <Pressable
160
158
  onPress={handlePress}
161
159
  disabled={disabled}
162
- accessibilityRole="button"
160
+ accessibilityRole={type === 'radio' ? 'radio' : 'checkbox'}
163
161
  accessibilityLabel={`${title}${description ? `, ${description}` : ''}`}
164
162
  accessibilityState={{ selected: isSelected, disabled }}
165
163
  style={[
@@ -171,7 +169,7 @@ export function SelectableCard({
171
169
  >
172
170
  <View style={styles.row}>
173
171
  {/* Selection indicator */}
174
- <View style={styles.selectorContainer} accessibilityRole={selectorAccessibilityRole} accessibilityState={{ selected: isSelected, disabled }}>
172
+ <View style={styles.selectorContainer}>
175
173
  {type === 'radio' ? (
176
174
  <EaseView
177
175
  style={styles.radioCircle}
@@ -1,38 +1,27 @@
1
1
  import React, { useState } from 'react'
2
- import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, ScrollView } from 'react-native'
3
- import Animated from 'react-native-reanimated'
2
+ import { View, Text, StyleSheet, ViewStyle, ScrollView } from 'react-native'
4
3
  import { useTheme } from '../../theme'
5
- import { renderIcon } from '../../utils/icons'
6
- import { usePressScale } from '../../utils/usePressScale'
4
+ import { Icon } from '../../utils/icons'
5
+ import { PressableChip } from '../../utils/pressable'
7
6
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
8
- import { PRESS_SCALE } from '../../utils/animations'
9
7
  import { s, vs, ms, mvs } from '../../utils/scaling'
10
8
  import { RADIUS } from '../../tokens'
11
9
 
12
10
  export interface SelectableGridItem<T extends string | number = string> {
13
- /** Unique value emitted on selection. */
14
11
  value: T
15
- /** Label rendered under the icon. */
16
12
  label?: string
17
- /** Icon name resolved via the icon registry. */
18
13
  iconName?: string
19
- /** Custom icon node — overrides `iconName`. */
20
14
  icon?: React.ReactNode
21
15
  disabled?: boolean
22
16
  }
23
17
 
24
18
  export interface SelectableGridProps<T extends string | number = string> {
25
19
  items: SelectableGridItem<T>[]
26
- /** Selected value(s). Array when `multiple`. */
27
20
  value: T | T[] | null
28
21
  onChange: (value: T) => void
29
- /** Allow multiple selections. `value` should be an array. Defaults to false. */
30
22
  multiple?: boolean
31
- /** Columns per row. Defaults to 4. Ignored when `orientation='horizontal'`. */
32
23
  numColumns?: number
33
- /** Gap between cells (dp). Defaults to 12. */
34
24
  gap?: number
35
- /** Layout orientation. 'grid' (default) wraps into rows. 'horizontal' creates a single scrollable row. */
36
25
  orientation?: 'grid' | 'horizontal'
37
26
  style?: ViewStyle
38
27
  }
@@ -51,63 +40,43 @@ interface CellProps<T extends string | number> {
51
40
 
52
41
  function Cell<T extends string | number>({ item, selected, width, onPress }: CellProps<T>) {
53
42
  const { colors } = useTheme()
54
- const { animatedStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
55
- pressScale: PRESS_SCALE.chip,
56
- disabled: item.disabled,
57
- })
58
43
 
59
44
  const iconColor = selected ? colors.primary : colors.foregroundSubtle
60
- const iconNode = item.icon ?? (item.iconName ? renderIcon(item.iconName, ms(24), iconColor) : null)
45
+ const iconNode = item.icon ?? (item.iconName ? <Icon name={item.iconName} size={ms(24)} color={iconColor} /> : null)
61
46
 
62
47
  return (
63
- <Animated.View style={[{ width }, animatedStyle]}>
64
- <TouchableOpacity
65
- onPress={onPress}
66
- onPressIn={onPressIn}
67
- onPressOut={onPressOut}
68
- disabled={item.disabled}
69
- activeOpacity={1}
70
- touchSoundDisabled={true}
71
- accessibilityRole="button"
72
- accessibilityState={{ selected, disabled: item.disabled }}
73
- accessibilityLabel={item.label ?? String(item.value)}
74
- {...hoverHandlers}
75
- style={[
76
- styles.cell,
77
- {
78
- backgroundColor: selected ? colors.primary + '14' : colors.surface,
79
- borderColor: selected ? colors.primary : 'transparent',
80
- },
81
- item.disabled && styles.cellDisabled,
82
- ]}
83
- >
84
- {iconNode}
85
- {item.label ? (
86
- <Text
87
- style={[styles.label, { color: selected ? colors.primary : colors.foreground }]}
88
- numberOfLines={1}
89
- allowFontScaling={true}
90
- >
91
- {item.label}
92
- </Text>
93
- ) : null}
94
- </TouchableOpacity>
95
- </Animated.View>
48
+ <PressableChip
49
+ onPress={onPress}
50
+ enabled={!item.disabled}
51
+ rippleColor="transparent"
52
+ touchSoundDisabled
53
+ activateOnHover
54
+ accessibilityRole="button"
55
+ accessibilityState={{ selected, disabled: item.disabled }}
56
+ accessibilityLabel={item.label ?? String(item.value)}
57
+ style={[
58
+ { width },
59
+ styles.cell,
60
+ {
61
+ borderColor: selected ? colors.primary : 'transparent',
62
+ },
63
+ item.disabled && styles.cellDisabled,
64
+ ]}
65
+ >
66
+ {iconNode}
67
+ {item.label ? (
68
+ <Text
69
+ style={[styles.label, { color: selected ? colors.primary : colors.foreground }]}
70
+ numberOfLines={1}
71
+ allowFontScaling={true}
72
+ >
73
+ {item.label}
74
+ </Text>
75
+ ) : null}
76
+ </PressableChip>
96
77
  )
97
78
  }
98
79
 
99
- /**
100
- * Grid of selectable cells (icon + label) — for store / category / emoji pickers
101
- * where a list would be the wrong shape. Single or multi select.
102
- *
103
- * @example
104
- * <SelectableGrid
105
- * items={categories}
106
- * value={selected}
107
- * onChange={setSelected}
108
- * numColumns={4}
109
- * />
110
- */
111
80
  export function SelectableGrid<T extends string | number = string>({
112
81
  items,
113
82
  value,
@@ -120,10 +89,7 @@ export function SelectableGrid<T extends string | number = string>({
120
89
  }: SelectableGridProps<T>) {
121
90
  const [containerWidth, setContainerWidth] = useState(0)
122
91
  const gapPx = s(gap)
123
- // Compute exact cell width so `numColumns` always fits — percentage widths + gap
124
- // overflow and wrap one short. -0.5 guards against sub-pixel rounding overflow.
125
92
  const cellWidth = containerWidth > 0 ? (containerWidth - gapPx * (numColumns - 1)) / numColumns - 0.5 : 0
126
- // Horizontal mode: fixed 72dp cell width (same scale as grid cells)
127
93
  const horizCellWidth = s(72)
128
94
 
129
95
  const handlePress = (item: SelectableGridItem<T>) => {
@@ -184,22 +150,21 @@ const styles = StyleSheet.create({
184
150
  paddingHorizontal: s(4),
185
151
  },
186
152
  cell: {
187
- flex: 1,
188
153
  borderRadius: RADIUS.md,
189
154
  borderWidth: 2,
190
155
  alignItems: 'center',
191
156
  justifyContent: 'center',
192
157
  gap: vs(4),
193
- paddingHorizontal: s(12),
194
- paddingVertical: vs(12),
158
+ paddingHorizontal: s(8),
159
+ paddingVertical: vs(10),
195
160
  },
196
161
  cellDisabled: {
197
162
  opacity: 0.4,
198
163
  },
199
164
  label: {
200
165
  fontFamily: 'Sohne-Medium',
201
- fontSize: ms(12),
202
- lineHeight: mvs(15),
166
+ fontSize: ms(11),
167
+ lineHeight: mvs(14),
203
168
  textAlign: 'center',
204
169
  },
205
170
  })
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useRef, useId } from 'react'
1
+ import React, { useCallback, useEffect, useRef, useState, useId } from 'react'
2
2
  import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native'
3
3
  import {
4
4
  BottomSheetModal,
@@ -95,28 +95,41 @@ export function Sheet({
95
95
  footer,
96
96
  snapPoints,
97
97
  }: SheetProps) {
98
+ type SheetState = 'idle' | 'presenting' | 'presented' | 'dismissing'
99
+
98
100
  const { colors } = useTheme()
99
101
  const insets = useSafeAreaInsets()
100
102
  const ref = useRef<BottomSheetModal>(null)
101
- const wasOpened = useRef(false)
102
- const isPresentedRef = useRef(false)
103
+ const sheetState = useRef<SheetState>('idle')
104
+ const onCloseRef = useRef(onClose)
103
105
  const name = useId()
106
+ const [tick, setTick] = useState(0)
104
107
 
105
- const handleDismiss = useCallback(() => {
106
- isPresentedRef.current = false
107
- onClose?.()
108
+ useEffect(() => {
109
+ onCloseRef.current = onClose
108
110
  }, [onClose])
109
111
 
112
+ const handleDismiss = useCallback(() => {
113
+ sheetState.current = 'idle'
114
+ onCloseRef.current?.()
115
+ setTick((t) => t + 1)
116
+ }, [])
117
+
110
118
  useEffect(() => {
111
- if (open && !isPresentedRef.current) {
112
- impactMedium()
113
- ref.current?.present()
114
- wasOpened.current = true
115
- isPresentedRef.current = true
116
- } else if (!open && wasOpened.current && isPresentedRef.current) {
117
- ref.current?.dismiss()
119
+ if (open) {
120
+ if (sheetState.current === 'idle') {
121
+ sheetState.current = 'presenting'
122
+ impactMedium()
123
+ ref.current?.present()
124
+ sheetState.current = 'presented'
125
+ }
126
+ } else {
127
+ if (sheetState.current === 'presented' || sheetState.current === 'presenting') {
128
+ sheetState.current = 'dismissing'
129
+ ref.current?.dismiss()
130
+ }
118
131
  }
119
- }, [open])
132
+ }, [open, tick])
120
133
 
121
134
  const renderBackdrop = useCallback(
122
135
  (props: BottomSheetBackdropProps) => (
@@ -163,7 +176,7 @@ export function Sheet({
163
176
  activeOpacity={0.6}
164
177
  touchSoundDisabled={true}
165
178
  accessibilityRole="button"
166
- accessibilityLabel="Close"
179
+ accessibilityLabel="Cerrar"
167
180
  hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
168
181
  >
169
182
  <AntDesign name="close" size={ms(18)} color={colors.foregroundMuted} />
@@ -1,2 +1,2 @@
1
- export { Sheet, BottomSheetModalProvider, SheetTextInput } from './Sheet'
2
- export type { SheetProps } from './Sheet'
1
+ export { Sheet, SheetHeader, SheetContent, SheetFooter, BottomSheetModalProvider, SheetTextInput } from './Sheet'
2
+ export type { SheetProps, SheetHeaderProps, SheetContentProps, SheetFooterProps } from './Sheet'
@@ -4,7 +4,7 @@ import { EaseView } from 'react-native-ease'
4
4
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
5
5
  import { useTheme } from '../../theme'
6
6
  import { s, vs, ms, mvs } from '../../utils/scaling'
7
- import { renderIcon } from '../../utils/icons'
7
+ import { Icon } from '../../utils/icons'
8
8
  import { COLOR_TRANSITION } from '../../utils/animations'
9
9
  import { PressableChip } from '../../utils/pressable'
10
10
  import { RADIUS } from '../../tokens'
@@ -47,7 +47,7 @@ function SheetSelectChip({
47
47
  }
48
48
 
49
49
  const iconColor = selected ? colors.primaryForeground : colors.foreground
50
- const resolvedIcon = option.iconName ? renderIcon(option.iconName, ms(13), iconColor) : null
50
+ const resolvedIcon = option.iconName ? <Icon name={option.iconName} size={ms(13)} color={iconColor} /> : null
51
51
 
52
52
  return (
53
53
  <PressableChip
@@ -55,7 +55,7 @@ function SheetSelectChip({
55
55
  rippleColor="transparent"
56
56
  touchSoundDisabled
57
57
  accessibilityRole="button"
58
- accessibilityLabel={option.disabled ? `${option.label}, unavailable` : option.label}
58
+ accessibilityLabel={option.disabled ? `${option.label}, no disponible` : option.label}
59
59
  accessibilityState={{ selected, disabled: option.disabled }}
60
60
  >
61
61
  <EaseView
@@ -83,7 +83,7 @@ export function Skeleton({
83
83
  ]}
84
84
  onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
85
85
  accessibilityRole="progressbar"
86
- accessibilityLabel="Loading"
86
+ accessibilityLabel="Cargando"
87
87
  accessibilityState={{ busy: true }}
88
88
  >
89
89
  <Animated.View style={[StyleSheet.absoluteFill, shimmerStyle]}>
@@ -16,6 +16,7 @@ export interface SliderProps {
16
16
  showValue?: boolean
17
17
  formatValue?: (value: number) => string
18
18
  accessibilityLabel?: string
19
+ accessibilityHint?: string
19
20
  disabled?: boolean
20
21
  style?: ViewStyle
21
22
  }
@@ -31,6 +32,7 @@ export function Slider({
31
32
  showValue = false,
32
33
  formatValue = (v: number) => v.toFixed(2),
33
34
  accessibilityLabel,
35
+ accessibilityHint,
34
36
  disabled,
35
37
  style,
36
38
  }: SliderProps) {
@@ -50,6 +52,7 @@ export function Slider({
50
52
  style={[styles.wrapper, style]}
51
53
  accessibilityRole="adjustable"
52
54
  accessibilityLabel={accessibilityLabel ?? label}
55
+ accessibilityHint={accessibilityHint}
53
56
  accessibilityValue={{
54
57
  min: minimumValue,
55
58
  max: maximumValue,
@@ -13,7 +13,7 @@ export interface SpinnerProps extends Omit<ActivityIndicatorProps, 'size'> {
13
13
 
14
14
  const sizeMap: Record<SpinnerSize, 'small' | 'large'> = {
15
15
  sm: 'small',
16
- md: 'small',
16
+ md: 'large',
17
17
  lg: 'large',
18
18
  }
19
19
 
@@ -25,7 +25,7 @@ const labelFontSize: Record<SpinnerSize, number> = {
25
25
 
26
26
  export function Spinner({ size = 'md', color, label, ...props }: SpinnerProps) {
27
27
  const { colors } = useTheme()
28
- const a11yLabel = label || 'Loading'
28
+ const a11yLabel = label || 'Cargando'
29
29
 
30
30
  if (label) {
31
31
  return (
@@ -4,7 +4,7 @@ import { impactLight } from '../../utils/haptics'
4
4
  import { useTheme } from '../../theme'
5
5
  import { s, vs, ms, mvs } from '../../utils/scaling'
6
6
  import { RADIUS } from '../../tokens'
7
- import { renderIcon } from '../../utils/icons'
7
+ import { Icon } from '../../utils/icons'
8
8
  import { PressableCard } from '../../utils/pressable'
9
9
 
10
10
  export type StatsVariant = 'elevated' | 'outlined' | 'filled'
@@ -114,7 +114,7 @@ function StatsComponent({
114
114
 
115
115
  const iconColorResolved = iconColor ?? colors.primary
116
116
 
117
- const resolvedIcon = iconName ? renderIcon(iconName, sizeStyles.iconSize, iconColorResolved) : icon
117
+ const resolvedIcon = iconName ? <Icon name={iconName} size={sizeStyles.iconSize} color={iconColorResolved} /> : icon
118
118
 
119
119
  const iconElement = resolvedIcon ? (
120
120
  <View style={styles.iconWrapper}>{resolvedIcon}</View>
@@ -1,11 +1,12 @@
1
1
  import React from 'react'
2
- import { TouchableOpacity, StyleSheet, ViewStyle, View } from 'react-native'
2
+ import { StyleSheet, ViewStyle, View } from 'react-native'
3
3
  import { EaseView } from 'react-native-ease'
4
4
  import { Feather } from '@expo/vector-icons'
5
5
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
6
6
  import { useTheme } from '../../theme'
7
7
  import { s } from '../../utils/scaling'
8
8
  import { COLOR_TRANSITION, OPACITY_TRANSITION, SPRING_ELASTIC } from '../../utils/animations'
9
+ import { PressableButton } from '../../utils/pressable'
9
10
 
10
11
  const TRACK_WIDTH = s(52)
11
12
  const TRACK_HEIGHT = s(30)
@@ -22,24 +23,26 @@ export interface SwitchProps {
22
23
  disabled?: boolean
23
24
  style?: ViewStyle
24
25
  accessibilityLabel?: string
26
+ accessibilityHint?: string
25
27
  }
26
28
 
27
- export function Switch({ checked = false, onCheckedChange, disabled, style, accessibilityLabel }: SwitchProps) {
29
+ export function Switch({ checked = false, onCheckedChange, disabled, style, accessibilityLabel, accessibilityHint }: SwitchProps) {
28
30
  const { colors } = useTheme()
29
31
  const isDisabled = !!disabled
30
32
 
31
33
  return (
32
34
  <View style={[{ alignSelf: 'flex-start' }, style]}>
33
- <TouchableOpacity
35
+ <PressableButton
34
36
  onPress={() => {
35
37
  hapticSelection()
36
38
  onCheckedChange?.(!checked)
37
39
  }}
38
- disabled={disabled}
39
- activeOpacity={0.8}
40
- touchSoundDisabled={true}
40
+ enabled={!disabled}
41
+ rippleColor="transparent"
42
+ touchSoundDisabled
41
43
  accessibilityRole="switch"
42
44
  accessibilityLabel={accessibilityLabel}
45
+ accessibilityHint={accessibilityHint}
43
46
  accessibilityState={{ checked, disabled: isDisabled }}
44
47
  style={styles.touchable}
45
48
  >
@@ -76,7 +79,7 @@ export function Switch({ checked = false, onCheckedChange, disabled, style, acce
76
79
  </EaseView>
77
80
  </EaseView>
78
81
  </View>
79
- </TouchableOpacity>
82
+ </PressableButton>
80
83
  </View>
81
84
  )
82
85
  }
@@ -84,6 +87,8 @@ export function Switch({ checked = false, onCheckedChange, disabled, style, acce
84
87
  const styles = StyleSheet.create({
85
88
  touchable: {
86
89
  alignSelf: 'flex-start',
90
+ minHeight: 44,
91
+ justifyContent: 'center',
87
92
  },
88
93
  trackContainer: {
89
94
  position: 'relative',
@@ -1,10 +1,11 @@
1
1
  import React from 'react'
2
- import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
3
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
4
4
  import { useTheme } from '../../theme'
5
- import { renderIcon } from '../../utils/icons'
5
+ import { Icon } from '../../utils/icons'
6
6
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
7
7
  import { s, vs, ms, mvs } from '../../utils/scaling'
8
+ import { PressableTab } from '../../utils/pressable'
8
9
 
9
10
  export interface TabBarItem {
10
11
  /** Unique key for the tab. */
@@ -74,23 +75,23 @@ export function TabBar({
74
75
  {items.map((item) => {
75
76
  const active = item.key === activeKey
76
77
  const tint = active ? resolvedActive : resolvedInactive
77
- const iconNode = item.icon ?? (item.iconName ? renderIcon(item.iconName, ms(24), tint) : null)
78
+ const iconNode = item.icon ?? (item.iconName ? <Icon name={item.iconName} size={ms(24)} color={tint} /> : null)
78
79
  const showBadge = item.badge !== undefined && item.badge !== false
79
80
  const badgeCount = typeof item.badge === 'number' ? item.badge : undefined
80
81
 
81
82
  return (
82
- <TouchableOpacity
83
+ <PressableTab
83
84
  key={item.key}
84
- style={styles.tab}
85
85
  onPress={() => {
86
86
  if (!active) hapticSelection()
87
87
  onTabPress(item.key)
88
88
  }}
89
- activeOpacity={0.7}
90
- touchSoundDisabled={true}
89
+ rippleColor="transparent"
90
+ touchSoundDisabled
91
91
  accessibilityRole="tab"
92
92
  accessibilityState={{ selected: active }}
93
93
  accessibilityLabel={item.label ?? item.key}
94
+ style={styles.tab}
94
95
  >
95
96
  <View>
96
97
  {iconNode}
@@ -115,7 +116,7 @@ export function TabBar({
115
116
  {item.label}
116
117
  </Text>
117
118
  ) : null}
118
- </TouchableOpacity>
119
+ </PressableTab>
119
120
  )
120
121
  })}
121
122
  </View>
@@ -1,9 +1,11 @@
1
1
  import React from 'react'
2
2
  import { Text as RNText, TextProps as RNTextProps, TextStyle } from 'react-native'
3
+
4
+ declare const __DEV__: boolean | undefined
3
5
  import { useTheme } from '../../theme'
4
6
  import { TYPOGRAPHY } from '../../tokens'
5
7
  import { ms, mvs } from '../../utils/scaling'
6
- import { warnIfFontsMissing } from '../../utils/fontGuard'
8
+ import { isLoaded as expoFontIsLoaded } from 'expo-font'
7
9
 
8
10
  export type TextVariant =
9
11
  | 'display-hero'
@@ -70,6 +72,16 @@ const defaultColorVariant: Partial<Record<TextVariant, 'foreground' | 'foregroun
70
72
  'button-sm': 'foreground',
71
73
  }
72
74
 
75
+ let fontWarned = false
76
+ function warnIfFontsMissing(): void {
77
+ if (fontWarned || typeof __DEV__ === 'undefined' || !__DEV__) return
78
+ fontWarned = true
79
+ if (!expoFontIsLoaded('Sohne-Regular')) {
80
+ // eslint-disable-next-line no-console
81
+ console.warn('[retray-ui-kit] Sohne fonts not loaded — text falls back to system font.')
82
+ }
83
+ }
84
+
73
85
  function TextBase({ variant = 'body-md', color, style, uppercase, children, ...props }: TextProps) {
74
86
  warnIfFontsMissing()
75
87
  const { colors } = useTheme()