@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
@@ -1,4 +1,4 @@
1
- import React, { useRef, useState, useCallback, useEffect, useMemo, useId } from 'react'
1
+ import React, { useRef, useState, useCallback, useMemo, useId } from 'react'
2
2
  import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, Dimensions } from 'react-native'
3
3
  import { ScrollView } from 'react-native-gesture-handler'
4
4
  import {
@@ -13,6 +13,7 @@ import { CURATED_ICONS, ALL_CURATED_ICONS } from '../../utils/curatedIcons'
13
13
  import { selectionAsync as hapticSelection, impactMedium } from '../../utils/haptics'
14
14
  import { s, vs, ms } from '../../utils/scaling'
15
15
  import { RADIUS } from '../../tokens'
16
+ import { Spinner } from '../Spinner'
16
17
  import type { BottomSheetBackdropProps, BottomSheetModal as BottomSheetModalType } from '@gorhom/bottom-sheet'
17
18
 
18
19
  const NUM_COLUMNS = 6
@@ -74,10 +75,10 @@ export function IconPicker({
74
75
  const { colors } = useTheme()
75
76
  const insets = useSafeAreaInsets()
76
77
  const sheetRef = useRef<BottomSheetModalType>(null)
77
- const catScrollRef = useRef<any>(null)
78
- const [open, setOpen] = useState(false)
78
+ const catScrollRef = useRef<ScrollView>(null)
79
79
  const [activeCategory, setActiveCategory] = useState<string | null>(null)
80
80
  const [containerWidth, setContainerWidth] = useState(() => Dimensions.get('window').width - s(16) * 2)
81
+ const [ready, setReady] = useState(false)
81
82
 
82
83
  const sheetName = useId()
83
84
 
@@ -101,35 +102,26 @@ export function IconPicker({
101
102
  return result
102
103
  }, [activeIcons, numColumns])
103
104
 
104
- useEffect(() => {
105
- if (open) {
106
- impactMedium()
107
- sheetRef.current?.present()
108
- } else {
109
- sheetRef.current?.dismiss()
110
- }
111
- }, [open])
105
+ const handleDismiss = useCallback(() => {
106
+ setActiveCategory(null)
107
+ setReady(false)
108
+ }, [])
112
109
 
113
110
  const handleSelect = useCallback(
114
111
  (iconName: string) => {
115
112
  onChange(iconName)
116
- setOpen(false)
117
- setActiveCategory(null)
118
113
  },
119
114
  [onChange],
120
115
  )
121
116
 
122
117
  const handleOpen = useCallback(() => {
123
118
  if (disabled) return
119
+ impactMedium()
124
120
  setActiveCategory(null)
125
- setOpen(true)
121
+ setReady(false)
122
+ sheetRef.current?.present()
126
123
  }, [disabled])
127
124
 
128
- const handleClose = useCallback(() => {
129
- setOpen(false)
130
- setActiveCategory(null)
131
- }, [])
132
-
133
125
  const renderBackdrop = useCallback(
134
126
  (props: BottomSheetBackdropProps) => (
135
127
  <BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} pressBehavior="close" />
@@ -185,12 +177,12 @@ export function IconPicker({
185
177
  <BottomSheetModal
186
178
  ref={sheetRef}
187
179
  name={sheetName}
188
- onDismiss={handleClose}
180
+ onDismiss={handleDismiss}
189
181
  enableDynamicSizing={true}
190
182
  maxDynamicContentSize={SCREEN_HEIGHT * 0.7}
191
183
  backdropComponent={renderBackdrop}
192
- backgroundStyle={[styles.sheetBackground, { backgroundColor: colors.card }]}
193
- handleIndicatorStyle={[styles.handle, { backgroundColor: colors.border }]}
184
+ backgroundStyle={{ ...styles.sheetBackground, backgroundColor: colors.card }}
185
+ handleIndicatorStyle={{ ...styles.handle, backgroundColor: colors.border }}
194
186
  enablePanDownToClose
195
187
  topInset={insets.top}
196
188
  android_keyboardInputMode="adjustPan"
@@ -199,103 +191,118 @@ export function IconPicker({
199
191
  contentContainerStyle={styles.sheetContent}
200
192
  showsVerticalScrollIndicator={true}
201
193
  >
202
- {/* Category section label */}
203
- <Text style={[styles.sectionLabel, { color: colors.foregroundSubtle }]} allowFontScaling={true}>
204
- Categorías
205
- </Text>
206
-
207
- {/* Horizontal scrollable category chips */}
208
- <ScrollView
209
- ref={catScrollRef}
210
- horizontal
211
- showsHorizontalScrollIndicator={false}
212
- contentContainerStyle={styles.categoryStrip}
213
- style={styles.categoryScroll}
194
+ {/* Measuring container always rendered so onLayout fires */}
195
+ <View
196
+ style={styles.gridContainer}
197
+ onLayout={(e) => {
198
+ setContainerWidth(e.nativeEvent.layout.width)
199
+ setReady(true)
200
+ }}
214
201
  >
215
- <TouchableOpacity
216
- onPress={() => setActiveCategory(null)}
217
- activeOpacity={0.7}
218
- touchSoundDisabled={true}
219
- accessibilityRole="button"
220
- accessibilityLabel="Todos"
221
- accessibilityState={{ selected: activeCategory === null }}
222
- style={[
223
- styles.categoryChip,
224
- {
225
- backgroundColor: activeCategory === null ? colors.primary : colors.surface,
226
- borderColor: activeCategory === null ? colors.primary : colors.border,
227
- },
228
- ]}
229
- >
230
- <View style={styles.categoryChipInner}>
231
- {renderIcon('grid', ms(14), activeCategory === null ? colors.primaryForeground : colors.foregroundSubtle)}
232
- <Text
233
- style={[
234
- styles.categoryChipText,
235
- { color: activeCategory === null ? colors.primaryForeground : colors.foreground },
236
- ]}
237
- allowFontScaling={true}
238
- numberOfLines={1}
239
- >
240
- Todos
241
- </Text>
202
+ {!ready ? (
203
+ <View style={styles.loader}>
204
+ <Spinner size="md" color={colors.primary} label="Cargando iconos..." />
242
205
  </View>
243
- </TouchableOpacity>
244
- {CURATED_ICONS.map((cat) => (
245
- <TouchableOpacity
246
- key={cat.name}
247
- onPress={() => setActiveCategory(cat.name)}
248
- activeOpacity={0.7}
249
- touchSoundDisabled={true}
250
- accessibilityRole="button"
251
- accessibilityLabel={cat.labelEs}
252
- accessibilityState={{ selected: activeCategory === cat.name }}
253
- style={[
254
- styles.categoryChip,
255
- {
256
- backgroundColor: activeCategory === cat.name ? colors.primary : colors.surface,
257
- borderColor: activeCategory === cat.name ? colors.primary : colors.border,
258
- },
259
- ]}
260
- >
261
- <View style={styles.categoryChipInner}>
262
- {renderIcon(cat.categoryIcon, ms(14), activeCategory === cat.name ? colors.primaryForeground : colors.foregroundSubtle)}
263
- <Text
206
+ ) : (
207
+ <>
208
+ {/* Category section label */}
209
+ <Text style={[styles.sectionLabel, { color: colors.foregroundSubtle }]} allowFontScaling={true}>
210
+ Categorías
211
+ </Text>
212
+
213
+ {/* Horizontal scrollable category chips */}
214
+ <ScrollView
215
+ ref={catScrollRef}
216
+ horizontal
217
+ showsHorizontalScrollIndicator={false}
218
+ contentContainerStyle={styles.categoryStrip}
219
+ style={styles.categoryScroll}
220
+ >
221
+ <TouchableOpacity
222
+ onPress={() => setActiveCategory(null)}
223
+ activeOpacity={0.7}
224
+ touchSoundDisabled={true}
225
+ accessibilityRole="button"
226
+ accessibilityLabel="Todos"
227
+ accessibilityState={{ selected: activeCategory === null }}
264
228
  style={[
265
- styles.categoryChipText,
266
- { color: activeCategory === cat.name ? colors.primaryForeground : colors.foreground },
229
+ styles.categoryChip,
230
+ {
231
+ backgroundColor: activeCategory === null ? colors.primary : colors.surface,
232
+ borderColor: activeCategory === null ? colors.primary : colors.border,
233
+ },
267
234
  ]}
268
- allowFontScaling={true}
269
- numberOfLines={1}
270
235
  >
271
- {cat.labelEs}
272
- </Text>
273
- </View>
274
- </TouchableOpacity>
275
- ))}
276
- </ScrollView>
236
+ <View style={styles.categoryChipInner}>
237
+ {renderIcon('grid', ms(14), activeCategory === null ? colors.primaryForeground : colors.foregroundSubtle)}
238
+ <Text
239
+ style={[
240
+ styles.categoryChipText,
241
+ { color: activeCategory === null ? colors.primaryForeground : colors.foreground },
242
+ ]}
243
+ allowFontScaling={true}
244
+ numberOfLines={1}
245
+ >
246
+ Todos
247
+ </Text>
248
+ </View>
249
+ </TouchableOpacity>
250
+ {CURATED_ICONS.map((cat) => (
251
+ <TouchableOpacity
252
+ key={cat.name}
253
+ onPress={() => setActiveCategory(cat.name)}
254
+ activeOpacity={0.7}
255
+ touchSoundDisabled={true}
256
+ accessibilityRole="button"
257
+ accessibilityLabel={cat.labelEs}
258
+ accessibilityState={{ selected: activeCategory === cat.name }}
259
+ style={[
260
+ styles.categoryChip,
261
+ {
262
+ backgroundColor: activeCategory === cat.name ? colors.primary : colors.surface,
263
+ borderColor: activeCategory === cat.name ? colors.primary : colors.border,
264
+ },
265
+ ]}
266
+ >
267
+ <View style={styles.categoryChipInner}>
268
+ {renderIcon(cat.categoryIcon, ms(14), activeCategory === cat.name ? colors.primaryForeground : colors.foregroundSubtle)}
269
+ <Text
270
+ style={[
271
+ styles.categoryChipText,
272
+ { color: activeCategory === cat.name ? colors.primaryForeground : colors.foreground },
273
+ ]}
274
+ allowFontScaling={true}
275
+ numberOfLines={1}
276
+ >
277
+ {cat.labelEs}
278
+ </Text>
279
+ </View>
280
+ </TouchableOpacity>
281
+ ))}
282
+ </ScrollView>
277
283
 
278
- {/* Separator */}
279
- <View style={[styles.separator, { backgroundColor: colors.border }]} />
284
+ {/* Separator */}
285
+ <View style={[styles.separator, { backgroundColor: colors.border }]} />
280
286
 
281
- {/* Icon grid — rendered directly inside BottomSheetScrollView */}
282
- <View style={styles.gridContainer} onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}>
283
- {cellSize > 0 ? rows.map((row, i) => (
284
- <View key={row[0] ?? `row-${i}`} style={[styles.row, { marginBottom: gapPx }]}>
285
- {row.map((name) => (
286
- <IconCellMemo
287
- key={name}
288
- name={name}
289
- selected={value === name}
290
- size={cellSize}
291
- onPress={() => handleSelect(name)}
292
- />
293
- ))}
294
- {Array.from({ length: numColumns - row.length }).map((_, j) => (
295
- <View key={`spacer-${j}`} style={{ width: cellSize, height: cellSize }} />
296
- ))}
297
- </View>
298
- )) : null}
287
+ {/* Icon grid */}
288
+ {cellSize > 0 ? rows.map((row, i) => (
289
+ <View key={row[0] ?? `row-${i}`} style={[styles.row, { marginBottom: gapPx }]}>
290
+ {row.map((name) => (
291
+ <IconCellMemo
292
+ key={name}
293
+ name={name}
294
+ selected={value === name}
295
+ size={cellSize}
296
+ onPress={() => { handleSelect(name); sheetRef.current?.dismiss() }}
297
+ />
298
+ ))}
299
+ {Array.from({ length: numColumns - row.length }).map((_, j) => (
300
+ <View key={`spacer-${j}`} style={{ width: cellSize, height: cellSize }} />
301
+ ))}
302
+ </View>
303
+ )) : null}
304
+ </>
305
+ )}
299
306
  </View>
300
307
  </BottomSheetScrollView>
301
308
  </BottomSheetModal>
@@ -380,4 +387,9 @@ const styles = StyleSheet.create({
380
387
  alignItems: 'center',
381
388
  justifyContent: 'center',
382
389
  },
390
+ loader: {
391
+ minHeight: vs(200),
392
+ alignItems: 'center',
393
+ justifyContent: 'center',
394
+ },
383
395
  })
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import React, { useState } from 'react'
2
2
  import { View, Text, Image, StyleSheet, ViewStyle, Platform } from 'react-native'
3
3
  import { Feather } from '@expo/vector-icons'
4
4
  import { impactLight } from '../../utils/haptics'
@@ -27,6 +27,8 @@ export interface ImageUploadProps {
27
27
  borderRadius?: number
28
28
  /** Aspect ratio for the selected image. Defaults to 'cover'. */
29
29
  resizeMode?: 'cover' | 'contain' | 'stretch'
30
+ /** Whether to allow the user to crop the image after selecting. Defaults to true. */
31
+ allowsEditing?: boolean
30
32
  disabled?: boolean
31
33
  style?: ViewStyle
32
34
  accessibilityLabel?: string
@@ -42,11 +44,13 @@ export function ImageUpload({
42
44
  height = 200,
43
45
  borderRadius = RADIUS.lg,
44
46
  resizeMode = 'cover',
47
+ allowsEditing = true,
45
48
  disabled = false,
46
49
  style,
47
50
  accessibilityLabel,
48
51
  }: ImageUploadProps) {
49
52
  const { colors } = useTheme()
53
+ const [imageLoaded, setImageLoaded] = useState(false)
50
54
 
51
55
  const handlePress = async () => {
52
56
  if (disabled || loading) return
@@ -81,11 +85,12 @@ export function ImageUpload({
81
85
 
82
86
  const result = await picker.launchImageLibraryAsync({
83
87
  mediaTypes: ['images'],
84
- allowsEditing: true,
88
+ allowsEditing,
85
89
  quality: 0.8,
86
90
  })
87
91
 
88
92
  if (!result.canceled && result.assets?.[0]) {
93
+ setImageLoaded(false)
89
94
  onChange?.(result.assets[0].uri)
90
95
  }
91
96
  }
@@ -97,7 +102,7 @@ export function ImageUpload({
97
102
  borderWidth: value ? 0 : 1,
98
103
  borderStyle: 'dashed',
99
104
  borderColor: colors.border,
100
- backgroundColor: value ? 'transparent' : colors.surface,
105
+ backgroundColor: (value && imageLoaded) ? 'transparent' : colors.surface,
101
106
  overflow: 'hidden',
102
107
  }
103
108
 
@@ -117,6 +122,8 @@ export function ImageUpload({
117
122
  source={{ uri: value }}
118
123
  style={[StyleSheet.absoluteFillObject, { borderRadius }]}
119
124
  resizeMode={resizeMode}
125
+ onLoad={() => setImageLoaded(true)}
126
+ onError={() => setImageLoaded(true)}
120
127
  />
121
128
  ) : (
122
129
  <View style={styles.placeholder}>
@@ -31,13 +31,13 @@ export interface ListItemProps {
31
31
  leftRender?: React.ReactNode
32
32
  /**
33
33
  * Arbitrary content rendered on the right (badge, price, chevron, switch, etc.).
34
- * Replaces the old `trailing` prop (still accepted as an alias).
35
34
  */
36
35
  rightRender?: React.ReactNode | string
37
- /** @deprecated Use `rightRender` instead. */
38
- trailing?: React.ReactNode | string
39
- /** @deprecated Use `leftRender` instead. */
40
- icon?: React.ReactNode
36
+ /**
37
+ * Multiple action buttons rendered on the right with automatic gap.
38
+ * Takes precedence over `rightRender` and `showChevron`.
39
+ */
40
+ rightActions?: React.ReactNode[]
41
41
  /**
42
42
  * Icon name from `@expo/vector-icons` rendered in the left slot.
43
43
  * See https://icons.expo.fyi. Takes precedence over `leftRender`.
@@ -65,7 +65,7 @@ export interface ListItemProps {
65
65
  */
66
66
  variant?: ListItemVariant
67
67
 
68
- /** Show a right-pointing chevron on the far right. Ignored when `rightRender` / `trailing` is set. */
68
+ /** Show a right-pointing chevron on the far right. Ignored when `rightRender` / `rightActions` / `rightIcon` is set. */
69
69
  showChevron?: boolean
70
70
 
71
71
  /** Visual separator line at the bottom of the item. Useful when rendering multiple plain items in a list. */
@@ -91,8 +91,7 @@ function ListItemBase({
91
91
  imageSource,
92
92
  leftRender,
93
93
  rightRender,
94
- trailing,
95
- icon,
94
+ rightActions,
96
95
  leftIcon,
97
96
  rightIcon,
98
97
  leftIconColor,
@@ -119,16 +118,14 @@ function ListItemBase({
119
118
  onPress?.()
120
119
  }
121
120
 
122
- // imageSource takes precedence, then leftIcon, then leftRender/icon
121
+ // imageSource takes precedence, then leftIcon, then leftRender
123
122
  const effectiveLeft: React.ReactNode = imageSource
124
123
  ? <Image source={imageSource} style={styles.image} />
125
124
  : leftIcon
126
125
  ? renderIcon(leftIcon, 24, leftIconColor ?? colors.foreground)
127
- : leftRender ?? icon
126
+ : leftRender
128
127
 
129
- const effectiveRight: React.ReactNode | string | undefined = rightIcon
130
- ? renderIcon(rightIcon, 24, rightIconColor ?? colors.foregroundMuted)
131
- : rightRender ?? trailing
128
+ const hasRightContent = !!(rightIcon || (rightActions && rightActions.length > 0) || rightRender !== undefined || showChevron)
132
129
 
133
130
  const cardStyle: ViewStyle =
134
131
  variant === 'card'
@@ -181,21 +178,33 @@ function ListItemBase({
181
178
  ) : null}
182
179
  </View>
183
180
 
184
- {effectiveRight !== undefined ? (
185
- <View style={styles.rightContainer}>
186
- {typeof effectiveRight === 'string' ? (
187
- <Text
188
- style={[styles.rightText, { color: colors.foregroundMuted }]}
189
- allowFontScaling={true}
190
- >
191
- {effectiveRight}
192
- </Text>
193
- ) : (
194
- effectiveRight
195
- )}
196
- </View>
197
- ) : showChevron ? (
198
- <Entypo name="chevron-with-circle-right" size={20} color={colors.foregroundMuted} />
181
+ {hasRightContent ? (
182
+ rightIcon ? (
183
+ <View style={styles.rightContainer}>
184
+ {renderIcon(rightIcon, 24, rightIconColor ?? colors.foregroundMuted)}
185
+ </View>
186
+ ) : rightActions && rightActions.length > 0 ? (
187
+ <View style={styles.rightActionsContainer}>
188
+ {rightActions.map((action, i) => (
189
+ <React.Fragment key={i}>{action}</React.Fragment>
190
+ ))}
191
+ </View>
192
+ ) : rightRender !== undefined ? (
193
+ <View style={styles.rightContainer}>
194
+ {typeof rightRender === 'string' ? (
195
+ <Text
196
+ style={[styles.rightText, { color: colors.foregroundMuted }]}
197
+ allowFontScaling={true}
198
+ >
199
+ {rightRender}
200
+ </Text>
201
+ ) : (
202
+ rightRender
203
+ )}
204
+ </View>
205
+ ) : showChevron ? (
206
+ <Entypo name="chevron-with-circle-right" size={20} color={colors.foregroundMuted} />
207
+ ) : null
199
208
  ) : null}
200
209
  </>
201
210
  )
@@ -283,6 +292,12 @@ const styles = StyleSheet.create({
283
292
  flexShrink: 0,
284
293
  maxWidth: s(160),
285
294
  },
295
+ rightActionsContainer: {
296
+ flexDirection: 'row',
297
+ alignItems: 'center',
298
+ gap: s(8),
299
+ flexShrink: 0,
300
+ },
286
301
  rightText: {
287
302
  fontFamily: 'Sohne-Regular',
288
303
  fontSize: ms(14),
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { ViewStyle } from 'react-native'
2
+ import { ViewStyle, type AccessibilityRole } from 'react-native'
3
3
  import { impactLight } from '../../utils/haptics'
4
4
  import { PressableCard } from '../../utils/pressable'
5
5
  import { PRESS_SCALE } from '../../utils/animations'
@@ -11,11 +11,6 @@ export interface PressableProps {
11
11
  onPress?: () => void
12
12
  /** Scale value on press. Defaults to `0.98` (MediaCard-style). */
13
13
  pressScale?: number
14
- /**
15
- * @deprecated Use Reanimated spring config via `pressOutSpring` instead. Ignored.
16
- * Kept for backwards compatibility with v6.x consumers.
17
- */
18
- bounciness?: number
19
14
  /** Enable haptic feedback on press. Defaults to `true`. */
20
15
  haptics?: boolean
21
16
  /** Additional style for the wrapper. */
@@ -24,6 +19,19 @@ export interface PressableProps {
24
19
  disabled?: boolean
25
20
  /** Hover scale (web only). Defaults to `1.02`. Set to `1` to disable. */
26
21
  hoverScale?: number
22
+ /**
23
+ * Accessibility role for the pressable element.
24
+ * Defaults to `"button"`.
25
+ */
26
+ accessibilityRole?: AccessibilityRole
27
+ /**
28
+ * Accessibility state for screen readers.
29
+ * Used to communicate states like `selected`, `expanded`, `checked`, etc.
30
+ * Defaults to `{ disabled: !!disabled }`.
31
+ */
32
+ accessibilityState?: Record<string, unknown>
33
+ /** Accessibility label for screen readers. */
34
+ accessibilityLabel?: string
27
35
  }
28
36
 
29
37
  /**
@@ -41,6 +49,9 @@ export function Pressable({
41
49
  style,
42
50
  disabled,
43
51
  hoverScale: _hoverScale = 1.02,
52
+ accessibilityRole,
53
+ accessibilityState,
54
+ accessibilityLabel,
44
55
  }: PressableProps) {
45
56
  const handlePress = () => {
46
57
  if (disabled || !onPress) return
@@ -56,8 +67,9 @@ export function Pressable({
56
67
  rippleColor="transparent"
57
68
  touchSoundDisabled
58
69
  activateOnHover
59
- accessibilityRole="button"
60
- accessibilityState={{ disabled: !!disabled }}
70
+ accessibilityRole={accessibilityRole ?? 'button'}
71
+ accessibilityState={accessibilityState ?? { disabled: !!disabled }}
72
+ accessibilityLabel={accessibilityLabel}
61
73
  >
62
74
  {children}
63
75
  </PressableCard>