@retray-dev/ui-kit 12.1.0 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/COMPONENTS.md +183 -147
  2. package/CONSUMER.md +2 -2
  3. package/DESIGN.md +2 -2
  4. package/README.md +13 -8
  5. package/dist/Accordion.d.mts +6 -0
  6. package/dist/Accordion.d.ts +6 -0
  7. package/dist/Accordion.js +62 -208
  8. package/dist/Accordion.mjs +6 -5
  9. package/dist/AlertBanner.js +29 -151
  10. package/dist/AlertBanner.mjs +3 -3
  11. package/dist/AppHeader.js +37 -233
  12. package/dist/AppHeader.mjs +6 -7
  13. package/dist/Avatar.d.mts +17 -1
  14. package/dist/Avatar.d.ts +17 -1
  15. package/dist/Avatar.js +80 -113
  16. package/dist/Avatar.mjs +2 -2
  17. package/dist/Badge.js +24 -147
  18. package/dist/Badge.mjs +3 -3
  19. package/dist/Button.js +86 -274
  20. package/dist/Button.mjs +6 -6
  21. package/dist/Card.js +15 -198
  22. package/dist/Card.mjs +4 -5
  23. package/dist/CategoryStrip.d.mts +0 -5
  24. package/dist/CategoryStrip.d.ts +0 -5
  25. package/dist/CategoryStrip.js +47 -263
  26. package/dist/CategoryStrip.mjs +6 -6
  27. package/dist/Checkbox.js +15 -198
  28. package/dist/Checkbox.mjs +5 -5
  29. package/dist/Chip.js +44 -234
  30. package/dist/Chip.mjs +7 -6
  31. package/dist/ConfirmDialog.js +100 -296
  32. package/dist/ConfirmDialog.mjs +7 -7
  33. package/dist/CurrencyDisplay.js +1 -112
  34. package/dist/CurrencyDisplay.mjs +2 -2
  35. package/dist/CurrencyInput.js +35 -160
  36. package/dist/CurrencyInput.mjs +5 -5
  37. package/dist/DetailRow.js +25 -148
  38. package/dist/DetailRow.mjs +3 -3
  39. package/dist/EmptyState.js +87 -275
  40. package/dist/EmptyState.mjs +7 -7
  41. package/dist/ErrorBoundary.js +32 -197
  42. package/dist/ErrorBoundary.mjs +4 -4
  43. package/dist/Form.js +1 -112
  44. package/dist/Form.mjs +2 -2
  45. package/dist/HolographicCard.d.mts +0 -28
  46. package/dist/HolographicCard.d.ts +0 -28
  47. package/dist/HolographicCard.js +20 -130
  48. package/dist/HolographicCard.mjs +9 -32
  49. package/dist/IconButton.js +36 -232
  50. package/dist/IconButton.mjs +5 -6
  51. package/dist/IconPicker.js +222 -927
  52. package/dist/IconPicker.mjs +5 -5
  53. package/dist/ImageUpload.d.mts +5 -1
  54. package/dist/ImageUpload.d.ts +5 -1
  55. package/dist/ImageUpload.js +32 -215
  56. package/dist/ImageUpload.mjs +5 -6
  57. package/dist/ImageViewer.js +75 -264
  58. package/dist/ImageViewer.mjs +8 -8
  59. package/dist/Input.d.mts +1 -1
  60. package/dist/Input.d.ts +1 -1
  61. package/dist/Input.js +35 -160
  62. package/dist/Input.mjs +4 -4
  63. package/dist/LabelValue.js +24 -147
  64. package/dist/LabelValue.mjs +3 -3
  65. package/dist/ListGroup.js +1 -112
  66. package/dist/ListGroup.mjs +2 -2
  67. package/dist/ListItem.js +38 -233
  68. package/dist/ListItem.mjs +5 -6
  69. package/dist/MediaCard.d.mts +0 -14
  70. package/dist/MediaCard.d.ts +0 -14
  71. package/dist/MediaCard.js +69 -313
  72. package/dist/MediaCard.mjs +5 -6
  73. package/dist/MenuGroup.js +1 -112
  74. package/dist/MenuGroup.mjs +2 -2
  75. package/dist/MenuItem.js +36 -232
  76. package/dist/MenuItem.mjs +5 -6
  77. package/dist/MonthPicker.js +8 -161
  78. package/dist/MonthPicker.mjs +3 -3
  79. package/dist/NumberStepper.js +40 -236
  80. package/dist/NumberStepper.mjs +5 -6
  81. package/dist/PagerDots.d.mts +1 -1
  82. package/dist/PagerDots.d.ts +1 -1
  83. package/dist/PagerDots.js +69 -222
  84. package/dist/PagerDots.mjs +6 -5
  85. package/dist/Pressable.js +14 -85
  86. package/dist/Pressable.mjs +4 -4
  87. package/dist/PricingCard.js +94 -279
  88. package/dist/PricingCard.mjs +8 -8
  89. package/dist/Progress.js +3 -121
  90. package/dist/Progress.mjs +3 -3
  91. package/dist/RadioGroup.js +52 -263
  92. package/dist/RadioGroup.mjs +5 -5
  93. package/dist/RetrayProvider.d.mts +1 -1
  94. package/dist/RetrayProvider.d.ts +1 -1
  95. package/dist/RetrayProvider.js +5 -6
  96. package/dist/RetrayProvider.mjs +3 -3
  97. package/dist/Select.d.mts +2 -1
  98. package/dist/Select.d.ts +2 -1
  99. package/dist/Select.js +24 -230
  100. package/dist/Select.mjs +4 -5
  101. package/dist/SelectableCard.d.mts +27 -0
  102. package/dist/SelectableCard.d.ts +27 -0
  103. package/dist/SelectableCard.js +335 -0
  104. package/dist/SelectableCard.mjs +8 -0
  105. package/dist/SelectableGrid.d.mts +0 -21
  106. package/dist/SelectableGrid.d.ts +0 -21
  107. package/dist/SelectableGrid.js +49 -269
  108. package/dist/SelectableGrid.mjs +5 -6
  109. package/dist/Separator.js +1 -112
  110. package/dist/Separator.mjs +2 -2
  111. package/dist/Sheet.js +16 -163
  112. package/dist/Sheet.mjs +3 -3
  113. package/dist/SheetSelect.js +39 -234
  114. package/dist/SheetSelect.mjs +6 -6
  115. package/dist/Skeleton.d.mts +3 -1
  116. package/dist/Skeleton.d.ts +3 -1
  117. package/dist/Skeleton.js +7 -124
  118. package/dist/Skeleton.mjs +3 -3
  119. package/dist/Slider.js +6 -159
  120. package/dist/Slider.mjs +3 -3
  121. package/dist/Spinner.js +3 -114
  122. package/dist/Spinner.mjs +2 -2
  123. package/dist/Stats.d.mts +4 -1
  124. package/dist/Stats.d.ts +4 -1
  125. package/dist/Stats.js +60 -234
  126. package/dist/Stats.mjs +5 -6
  127. package/dist/Switch.js +24 -173
  128. package/dist/Switch.mjs +5 -4
  129. package/dist/TabBar.js +43 -198
  130. package/dist/TabBar.mjs +5 -4
  131. package/dist/Tabs.js +15 -197
  132. package/dist/Tabs.mjs +5 -5
  133. package/dist/Text.js +9 -128
  134. package/dist/Text.mjs +2 -2
  135. package/dist/Textarea.d.mts +2 -1
  136. package/dist/Textarea.d.ts +2 -1
  137. package/dist/Textarea.js +71 -217
  138. package/dist/Textarea.mjs +4 -4
  139. package/dist/Toast.js +1 -112
  140. package/dist/Toast.mjs +2 -2
  141. package/dist/Toggle.js +39 -234
  142. package/dist/Toggle.mjs +6 -6
  143. package/dist/{chunk-FFTYLPSB.mjs → chunk-2QOHHBJC.mjs} +13 -7
  144. package/dist/{chunk-BCWEHE34.mjs → chunk-2VIDP72N.mjs} +3 -3
  145. package/dist/{chunk-PGERH3P7.mjs → chunk-4NQFTHN3.mjs} +13 -7
  146. package/dist/{chunk-3N2M3WZL.mjs → chunk-4ZO5PTKF.mjs} +4 -4
  147. package/dist/{chunk-MYZ2EDYU.mjs → chunk-5MYNAAFE.mjs} +13 -17
  148. package/dist/{chunk-E7NEHHXV.mjs → chunk-62BBSSUF.mjs} +3 -3
  149. package/dist/{chunk-ISY26JQJ.mjs → chunk-6CR4S6W2.mjs} +3 -3
  150. package/dist/{chunk-FUVYSVGR.mjs → chunk-6QLBHUEG.mjs} +8 -7
  151. package/dist/chunk-ARONDO7M.mjs +40 -0
  152. package/dist/{chunk-3UYAZ7I4.mjs → chunk-AZV7KNJI.mjs} +3 -3
  153. package/dist/{chunk-HLMPMUK2.mjs → chunk-BTUW5LSG.mjs} +11 -8
  154. package/dist/chunk-BULKGOIZ.mjs +235 -0
  155. package/dist/{chunk-265G6A46.mjs → chunk-CBIZLRYH.mjs} +29 -12
  156. package/dist/chunk-CM2DG4MR.mjs +142 -0
  157. package/dist/{chunk-2I2AYECM.mjs → chunk-DBHSUUKU.mjs} +2 -2
  158. package/dist/{chunk-P64WHW4A.mjs → chunk-DE25XTVQ.mjs} +3 -3
  159. package/dist/{chunk-DI7CBDL6.mjs → chunk-E4EQSCKR.mjs} +5 -5
  160. package/dist/{chunk-357YO24D.mjs → chunk-EHGBHFMH.mjs} +9 -17
  161. package/dist/{chunk-GK4VRMNE.mjs → chunk-EROPDCB5.mjs} +24 -27
  162. package/dist/{chunk-XBAGGKLW.mjs → chunk-ERWJPVX7.mjs} +2 -2
  163. package/dist/{chunk-LRM4AVYY.mjs → chunk-ESQDPO5E.mjs} +7 -7
  164. package/dist/{chunk-EFLFRAHD.mjs → chunk-EW2FIDSM.mjs} +1 -1
  165. package/dist/{chunk-7HSILTC4.mjs → chunk-FTTI6T5Q.mjs} +4 -4
  166. package/dist/{chunk-X26S5EVZ.mjs → chunk-HUSSF6TF.mjs} +1 -1
  167. package/dist/chunk-IFYMBOEN.mjs +14 -0
  168. package/dist/{chunk-S3KJCPEJ.mjs → chunk-IGU223UM.mjs} +80 -4
  169. package/dist/chunk-IJCMPVW5.mjs +121 -0
  170. package/dist/{chunk-I4V5XZPS.mjs → chunk-ITG4JQM3.mjs} +4 -4
  171. package/dist/{chunk-F4V6XLP4.mjs → chunk-K3QX2M26.mjs} +11 -8
  172. package/dist/{chunk-V6NFJXKO.mjs → chunk-K7TKID3V.mjs} +8 -7
  173. package/dist/{chunk-ZHMSAYLT.mjs → chunk-KAGADD2O.mjs} +4 -4
  174. package/dist/{chunk-3GEYJ7I5.mjs → chunk-KC5QDYGZ.mjs} +4 -4
  175. package/dist/{chunk-HJ46DTJE.mjs → chunk-KPTY7UYQ.mjs} +1 -1
  176. package/dist/{chunk-EMUWGDWC.mjs → chunk-KSSVIFYR.mjs} +11 -12
  177. package/dist/chunk-L3YKPTJQ.mjs +119 -0
  178. package/dist/chunk-M53LC4Q7.mjs +35 -0
  179. package/dist/{chunk-NXI4YDZ2.mjs → chunk-MP7GLMIR.mjs} +17 -25
  180. package/dist/chunk-MZ6WRTD2.mjs +40 -0
  181. package/dist/chunk-NGEN2EES.mjs +581 -0
  182. package/dist/{chunk-JULSIZDM.mjs → chunk-OBV72JD4.mjs} +1 -1
  183. package/dist/{chunk-2A2LEFZG.mjs → chunk-PGQ6FMXS.mjs} +6 -5
  184. package/dist/{chunk-BQZE3HAW.mjs → chunk-PI6RULJX.mjs} +1 -1
  185. package/dist/{chunk-FA2KMTH5.mjs → chunk-RA6SAAFE.mjs} +9 -8
  186. package/dist/{chunk-FVTVCJAH.mjs → chunk-RRKM4MKB.mjs} +7 -7
  187. package/dist/{chunk-AKM4EPOT.mjs → chunk-S2VGME7X.mjs} +1 -1
  188. package/dist/{chunk-OULVKTWL.mjs → chunk-S44XWTTC.mjs} +35 -25
  189. package/dist/{chunk-QSFV2P7O.mjs → chunk-SZEKQAOY.mjs} +1 -1
  190. package/dist/{chunk-N4ZPVCJH.mjs → chunk-TETMEKZE.mjs} +9 -9
  191. package/dist/{chunk-2CBQKU7H.mjs → chunk-TMH263OK.mjs} +5 -4
  192. package/dist/{chunk-D3Y2T42P.mjs → chunk-U6DEBYU5.mjs} +10 -9
  193. package/dist/{chunk-4WFMPFZB.mjs → chunk-UOKFSFNJ.mjs} +2 -2
  194. package/dist/{chunk-WOEWGSTU.mjs → chunk-URIH43IJ.mjs} +13 -21
  195. package/dist/{chunk-JCZQOY4O.mjs → chunk-V2ZB2XNS.mjs} +16 -10
  196. package/dist/{chunk-P73V2EKS.mjs → chunk-WIPEDNSD.mjs} +7 -7
  197. package/dist/{chunk-BOVUP27T.mjs → chunk-XCIG6HT2.mjs} +6 -5
  198. package/dist/chunk-Y6YS33GM.mjs +131 -0
  199. package/dist/{chunk-5OLNXP3S.mjs → chunk-ZKDKKQCE.mjs} +29 -7
  200. package/dist/{chunk-DF6DU42P.mjs → chunk-ZTPYUU5C.mjs} +5 -5
  201. package/dist/{index-wt-orHUi.d.ts → index-CY34hxPN.d.mts} +1 -0
  202. package/dist/{index-wt-orHUi.d.mts → index-CY34hxPN.d.ts} +1 -0
  203. package/dist/index.d.mts +15 -74
  204. package/dist/index.d.ts +15 -74
  205. package/dist/index.js +1055 -1562
  206. package/dist/index.mjs +81 -84
  207. package/package.json +8 -10
  208. package/src/components/Accordion/Accordion.tsx +32 -9
  209. package/src/components/AlertBanner/AlertBanner.tsx +7 -6
  210. package/src/components/AppHeader/AppHeader.tsx +1 -1
  211. package/src/components/Avatar/Avatar.tsx +92 -1
  212. package/src/components/Avatar/index.ts +2 -2
  213. package/src/components/Badge/Badge.tsx +2 -2
  214. package/src/components/Button/Button.tsx +64 -57
  215. package/src/components/Card/Card.tsx +1 -0
  216. package/src/components/CategoryStrip/CategoryStrip.tsx +36 -49
  217. package/src/components/Chip/Chip.tsx +5 -4
  218. package/src/components/ConfirmDialog/ConfirmDialog.tsx +13 -6
  219. package/src/components/DetailRow/DetailRow.tsx +3 -3
  220. package/src/components/EmptyState/EmptyState.tsx +2 -2
  221. package/src/components/ErrorBoundary/ErrorBoundary.tsx +6 -6
  222. package/src/components/HolographicCard/HolographicCard.tsx +14 -95
  223. package/src/components/IconButton/IconButton.tsx +2 -2
  224. package/src/components/IconPicker/IconPicker.tsx +13 -12
  225. package/src/components/ImageUpload/ImageUpload.tsx +24 -28
  226. package/src/components/ImageViewer/ImageViewer.tsx +3 -3
  227. package/src/components/Input/Input.tsx +11 -5
  228. package/src/components/LabelValue/LabelValue.tsx +2 -2
  229. package/src/components/ListItem/ListItem.tsx +4 -4
  230. package/src/components/MediaCard/MediaCard.tsx +21 -59
  231. package/src/components/MenuItem/MenuItem.tsx +2 -2
  232. package/src/components/MonthPicker/MonthPicker.tsx +2 -2
  233. package/src/components/NumberStepper/NumberStepper.tsx +6 -6
  234. package/src/components/PagerDots/PagerDots.tsx +38 -28
  235. package/src/components/PricingCard/PricingCard.tsx +6 -6
  236. package/src/components/RadioGroup/RadioGroup.tsx +18 -31
  237. package/src/components/Select/Select.tsx +32 -39
  238. package/src/components/SelectableCard/SelectableCard.tsx +302 -0
  239. package/src/components/SelectableCard/index.ts +1 -0
  240. package/src/components/SelectableGrid/SelectableGrid.tsx +38 -72
  241. package/src/components/Sheet/Sheet.tsx +11 -4
  242. package/src/components/SheetSelect/SheetSelect.tsx +3 -3
  243. package/src/components/Skeleton/Skeleton.tsx +6 -3
  244. package/src/components/Spinner/Spinner.tsx +2 -2
  245. package/src/components/Stats/Stats.tsx +36 -8
  246. package/src/components/Switch/Switch.tsx +9 -6
  247. package/src/components/TabBar/TabBar.tsx +9 -8
  248. package/src/components/Text/Text.tsx +12 -1
  249. package/src/components/Textarea/Textarea.tsx +18 -32
  250. package/src/components/Toggle/Toggle.tsx +3 -3
  251. package/src/hooks/useConfirmDialog.ts +31 -42
  252. package/src/index.ts +4 -4
  253. package/src/theme/ThemeProvider.tsx +1 -4
  254. package/src/theme/colorUtils.ts +1 -72
  255. package/src/theme/colors.ts +47 -1
  256. package/src/theme/types.ts +6 -3
  257. package/src/utils/animations.ts +0 -47
  258. package/src/utils/curatedIcons.ts +93 -801
  259. package/src/utils/haptics.ts +13 -208
  260. package/src/utils/icons.ts +27 -91
  261. package/src/utils/pressable.ts +10 -61
  262. package/dist/VirtualList.d.mts +0 -19
  263. package/dist/VirtualList.d.ts +0 -19
  264. package/dist/VirtualList.js +0 -38
  265. package/dist/VirtualList.mjs +0 -2
  266. package/dist/chunk-3DKJ2GIC.mjs +0 -30
  267. package/dist/chunk-AQEVCEXV.mjs +0 -164
  268. package/dist/chunk-DOGIPOF5.mjs +0 -131
  269. package/dist/chunk-DVK4G2GT.mjs +0 -59
  270. package/dist/chunk-EJ7ZPXOH.mjs +0 -163
  271. package/dist/chunk-J6Q2YJEV.mjs +0 -134
  272. package/dist/chunk-JNVAIDLK.mjs +0 -136
  273. package/dist/chunk-KA7LTET3.mjs +0 -71
  274. package/dist/chunk-KHYX4IOM.mjs +0 -1114
  275. package/dist/chunk-NC5ZTR2Y.mjs +0 -32
  276. package/dist/chunk-YNROWHQJ.mjs +0 -46
  277. package/src/components/VirtualList/VirtualList.tsx +0 -60
  278. package/src/components/VirtualList/index.ts +0 -1
  279. package/src/utils/fontGuard.ts +0 -35
  280. package/src/utils/hover.ts +0 -25
  281. package/src/utils/useColorTransition.ts +0 -40
  282. package/src/utils/usePressScale.ts +0 -75
@@ -0,0 +1,302 @@
1
+ import React, { createContext, useContext } from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle, Pressable } from 'react-native'
3
+ import { EaseView } from 'react-native-ease'
4
+ import { impactLight } from '../../utils/haptics'
5
+ import { useTheme } from '../../theme'
6
+ import { s, vs, ms, mvs } from '../../utils/scaling'
7
+ import { RADIUS } from '../../tokens'
8
+ import { Icon } from '../../utils/icons'
9
+ import { COLOR_TRANSITION, OPACITY_TRANSITION, SPRING_ELASTIC } from '../../utils/animations'
10
+
11
+ type SelectType = 'radio' | 'checkbox'
12
+ type CardVariant = 'elevated' | 'outlined' | 'filled'
13
+
14
+ interface SelectableCardContextValue {
15
+ type: SelectType
16
+ value: string | string[]
17
+ onValueChange: (value: string | string[]) => void
18
+ variant: CardVariant
19
+ }
20
+
21
+ const SelectableCardContext = createContext<SelectableCardContextValue | null>(null)
22
+
23
+ // ─── Group ────────────────────────────────────────────────────────────────────
24
+
25
+ export interface SelectableCardGroupProps {
26
+ type: SelectType
27
+ value: string | string[]
28
+ onValueChange: (value: string | string[]) => void
29
+ variant?: CardVariant
30
+ gap?: number
31
+ style?: ViewStyle
32
+ children: React.ReactNode
33
+ }
34
+
35
+ export function SelectableCardGroup({
36
+ type,
37
+ value,
38
+ onValueChange,
39
+ variant = 'elevated',
40
+ gap = s(8),
41
+ style,
42
+ children,
43
+ }: SelectableCardGroupProps) {
44
+ return (
45
+ <SelectableCardContext.Provider value={{ type, value, onValueChange, variant }}>
46
+ <View style={[styles.group, { gap }, style]}>
47
+ {children}
48
+ </View>
49
+ </SelectableCardContext.Provider>
50
+ )
51
+ }
52
+
53
+ // ─── Card ─────────────────────────────────────────────────────────────────────
54
+
55
+ export interface SelectableCardProps {
56
+ value: string
57
+ title: string
58
+ description?: string
59
+ iconName?: string
60
+ icon?: React.ReactNode
61
+ disabled?: boolean
62
+ style?: ViewStyle
63
+ }
64
+
65
+ export function SelectableCard({
66
+ value,
67
+ title,
68
+ description,
69
+ iconName,
70
+ icon,
71
+ disabled = false,
72
+ style,
73
+ }: SelectableCardProps) {
74
+ const ctx = useContext(SelectableCardContext)
75
+ if (!ctx) {
76
+ throw new Error('SelectableCard must be used inside <SelectableCard.Group>')
77
+ }
78
+
79
+ const { colors } = useTheme()
80
+ const { type, value: selectedValue, onValueChange, variant } = ctx
81
+
82
+ const isSelected =
83
+ type === 'radio'
84
+ ? selectedValue === value
85
+ : Array.isArray(selectedValue) && selectedValue.includes(value)
86
+
87
+ const handlePress = () => {
88
+ if (disabled) return
89
+ impactLight()
90
+ if (type === 'radio') {
91
+ onValueChange(value)
92
+ } else {
93
+ const arr = Array.isArray(selectedValue) ? selectedValue : []
94
+ if (arr.includes(value)) {
95
+ onValueChange(arr.filter((v) => v !== value))
96
+ } else {
97
+ onValueChange([...arr, value])
98
+ }
99
+ }
100
+ }
101
+
102
+ const variantStyle: ViewStyle = (() => {
103
+ const borderWidth = 2 // always 2 — no layout shift on select
104
+
105
+ const base = {
106
+ elevated: {
107
+ backgroundColor: colors.card,
108
+ borderWidth,
109
+ borderColor: 'transparent', // reserve space for selected border
110
+ },
111
+ outlined: {
112
+ backgroundColor: colors.card,
113
+ borderWidth,
114
+ borderColor: colors.border,
115
+ },
116
+ filled: {
117
+ backgroundColor: colors.surfaceStrong,
118
+ borderWidth,
119
+ borderColor: colors.border,
120
+ },
121
+ }[variant]
122
+
123
+ if (isSelected && !disabled) {
124
+ return {
125
+ ...base,
126
+ borderColor: colors.primary,
127
+ shadowColor: 'transparent',
128
+ shadowOpacity: 0,
129
+ shadowRadius: 0,
130
+ elevation: 0,
131
+ }
132
+ }
133
+
134
+ if (disabled) {
135
+ return {
136
+ ...base,
137
+ shadowColor: 'transparent',
138
+ shadowOpacity: 0,
139
+ shadowRadius: 0,
140
+ elevation: 0,
141
+ borderColor: colors.border,
142
+ }
143
+ }
144
+
145
+ return base
146
+ })()
147
+
148
+ const resolvedIcon = iconName
149
+ ? <Icon name={iconName} size={ms(22)} color={disabled ? colors.foregroundMuted : colors.foregroundMuted} />
150
+ : icon
151
+
152
+ const resolvedIconElement = resolvedIcon ? (
153
+ <View style={[styles.iconWrapper, disabled && { opacity: 0.45 }]}>{resolvedIcon}</View>
154
+ ) : null
155
+
156
+ return (
157
+ <Pressable
158
+ onPress={handlePress}
159
+ disabled={disabled}
160
+ accessibilityRole={type === 'radio' ? 'radio' : 'checkbox'}
161
+ accessibilityLabel={`${title}${description ? `, ${description}` : ''}`}
162
+ accessibilityState={{ selected: isSelected, disabled }}
163
+ style={[
164
+ styles.card,
165
+ variantStyle,
166
+ isSelected && !disabled && styles.cardSelected,
167
+ style,
168
+ ]}
169
+ >
170
+ <View style={styles.row}>
171
+ {/* Selection indicator */}
172
+ <View style={styles.selectorContainer}>
173
+ {type === 'radio' ? (
174
+ <EaseView
175
+ style={styles.radioCircle}
176
+ animate={{ borderColor: !disabled && isSelected ? colors.primary : colors.border }}
177
+ transition={COLOR_TRANSITION}
178
+ >
179
+ <EaseView
180
+ animate={{
181
+ scale: !disabled && isSelected ? 1 : 0,
182
+ opacity: !disabled && isSelected ? 1 : 0,
183
+ }}
184
+ transition={SPRING_ELASTIC}
185
+ >
186
+ <View style={[styles.radioDot, { backgroundColor: colors.primary }]} />
187
+ </EaseView>
188
+ </EaseView>
189
+ ) : (
190
+ <EaseView
191
+ style={styles.checkboxBox}
192
+ animate={{
193
+ borderColor: !disabled && isSelected ? colors.primary : colors.border,
194
+ backgroundColor: !disabled && isSelected ? colors.primary : 'transparent',
195
+ }}
196
+ transition={COLOR_TRANSITION}
197
+ >
198
+ <EaseView
199
+ animate={{ opacity: !disabled && isSelected ? 1 : 0 }}
200
+ transition={OPACITY_TRANSITION}
201
+ >
202
+ <View style={[styles.checkmark, { borderColor: colors.primaryForeground }]} />
203
+ </EaseView>
204
+ </EaseView>
205
+ )}
206
+ </View>
207
+
208
+ {/* Optional icon */}
209
+ {resolvedIconElement}
210
+
211
+ {/* Title + description */}
212
+ <View style={styles.textArea}>
213
+ <Text
214
+ style={[styles.title, { color: disabled ? colors.foregroundMuted : colors.foreground }]}
215
+ allowFontScaling={true}
216
+ numberOfLines={2}
217
+ >
218
+ {title}
219
+ </Text>
220
+ {description ? (
221
+ <Text
222
+ style={[styles.description, { color: disabled ? colors.foregroundMuted : colors.foregroundSubtle }]}
223
+ allowFontScaling={true}
224
+ numberOfLines={4}
225
+ >
226
+ {description}
227
+ </Text>
228
+ ) : null}
229
+ </View>
230
+ </View>
231
+ </Pressable>
232
+ )
233
+ }
234
+
235
+ // ─── Styles ───────────────────────────────────────────────────────────────────
236
+
237
+ const styles = StyleSheet.create({
238
+ group: {
239
+ width: '100%',
240
+ },
241
+ card: {
242
+ borderRadius: RADIUS.md,
243
+ borderWidth: 1,
244
+ },
245
+ cardSelected: {
246
+ backgroundColor: undefined, // overridden by variantStyle
247
+ },
248
+ row: {
249
+ flexDirection: 'row',
250
+ alignItems: 'flex-start',
251
+ padding: s(16),
252
+ gap: s(12),
253
+ },
254
+ selectorContainer: {
255
+ paddingTop: vs(1),
256
+ },
257
+ radioCircle: {
258
+ width: s(24),
259
+ height: s(24),
260
+ borderRadius: s(12),
261
+ borderWidth: 2,
262
+ alignItems: 'center',
263
+ justifyContent: 'center',
264
+ },
265
+ radioDot: {
266
+ width: s(10),
267
+ height: s(10),
268
+ borderRadius: s(5),
269
+ },
270
+ checkboxBox: {
271
+ width: s(24),
272
+ height: s(24),
273
+ borderRadius: ms(4),
274
+ borderWidth: 2,
275
+ alignItems: 'center',
276
+ justifyContent: 'center',
277
+ },
278
+ checkmark: {
279
+ width: s(12),
280
+ height: vs(7),
281
+ borderLeftWidth: 2,
282
+ borderBottomWidth: 2,
283
+ transform: [{ rotate: '-45deg' }, { translateY: -1 }],
284
+ },
285
+ iconWrapper: {
286
+ paddingTop: vs(1),
287
+ },
288
+ textArea: {
289
+ flex: 1,
290
+ gap: vs(4),
291
+ },
292
+ title: {
293
+ fontFamily: 'Sohne-SemiBold',
294
+ fontSize: ms(16),
295
+ lineHeight: mvs(22),
296
+ },
297
+ description: {
298
+ fontFamily: 'Sohne-Regular',
299
+ fontSize: ms(13),
300
+ lineHeight: mvs(18),
301
+ },
302
+ })
@@ -0,0 +1 @@
1
+ export * from './SelectableCard'
@@ -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,44 @@ 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
+ backgroundColor: selected ? colors.primary + '14' : colors.surface,
62
+ borderColor: selected ? colors.primary : 'transparent',
63
+ },
64
+ item.disabled && styles.cellDisabled,
65
+ ]}
66
+ >
67
+ {iconNode}
68
+ {item.label ? (
69
+ <Text
70
+ style={[styles.label, { color: selected ? colors.primary : colors.foreground }]}
71
+ numberOfLines={1}
72
+ allowFontScaling={true}
73
+ >
74
+ {item.label}
75
+ </Text>
76
+ ) : null}
77
+ </PressableChip>
96
78
  )
97
79
  }
98
80
 
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
81
  export function SelectableGrid<T extends string | number = string>({
112
82
  items,
113
83
  value,
@@ -120,10 +90,7 @@ export function SelectableGrid<T extends string | number = string>({
120
90
  }: SelectableGridProps<T>) {
121
91
  const [containerWidth, setContainerWidth] = useState(0)
122
92
  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
93
  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
94
  const horizCellWidth = s(72)
128
95
 
129
96
  const handlePress = (item: SelectableGridItem<T>) => {
@@ -184,22 +151,21 @@ const styles = StyleSheet.create({
184
151
  paddingHorizontal: s(4),
185
152
  },
186
153
  cell: {
187
- flex: 1,
188
154
  borderRadius: RADIUS.md,
189
155
  borderWidth: 2,
190
156
  alignItems: 'center',
191
157
  justifyContent: 'center',
192
158
  gap: vs(4),
193
- paddingHorizontal: s(12),
194
- paddingVertical: vs(12),
159
+ paddingHorizontal: s(8),
160
+ paddingVertical: vs(10),
195
161
  },
196
162
  cellDisabled: {
197
163
  opacity: 0.4,
198
164
  },
199
165
  label: {
200
166
  fontFamily: 'Sohne-Medium',
201
- fontSize: ms(12),
202
- lineHeight: mvs(15),
167
+ fontSize: ms(11),
168
+ lineHeight: mvs(14),
203
169
  textAlign: 'center',
204
170
  },
205
171
  })
@@ -99,14 +99,21 @@ export function Sheet({
99
99
  const insets = useSafeAreaInsets()
100
100
  const ref = useRef<BottomSheetModal>(null)
101
101
  const wasOpened = useRef(false)
102
+ const isPresentedRef = useRef(false)
102
103
  const name = useId()
103
104
 
105
+ const handleDismiss = useCallback(() => {
106
+ isPresentedRef.current = false
107
+ onClose?.()
108
+ }, [onClose])
109
+
104
110
  useEffect(() => {
105
- if (open) {
111
+ if (open && !isPresentedRef.current) {
106
112
  impactMedium()
107
113
  ref.current?.present()
108
114
  wasOpened.current = true
109
- } else if (wasOpened.current) {
115
+ isPresentedRef.current = true
116
+ } else if (!open && wasOpened.current && isPresentedRef.current) {
110
117
  ref.current?.dismiss()
111
118
  }
112
119
  }, [open])
@@ -156,7 +163,7 @@ export function Sheet({
156
163
  activeOpacity={0.6}
157
164
  touchSoundDisabled={true}
158
165
  accessibilityRole="button"
159
- accessibilityLabel="Close"
166
+ accessibilityLabel="Cerrar"
160
167
  hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
161
168
  >
162
169
  <AntDesign name="close" size={ms(18)} color={colors.foregroundMuted} />
@@ -188,7 +195,7 @@ export function Sheet({
188
195
  <BottomSheetModal
189
196
  ref={ref}
190
197
  name={name}
191
- onDismiss={onClose}
198
+ onDismiss={handleDismiss}
192
199
  enableDynamicSizing={useDynamicSizing}
193
200
  snapPoints={snapPoints}
194
201
  maxDynamicContentSize={useDynamicSizing && maxHeight ? maxHeight : undefined}
@@ -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
@@ -24,6 +24,8 @@ export interface SkeletonProps {
24
24
  preset?: SkeletonPreset
25
25
  /** Only used with `preset='circle'` — overrides the diameter. Defaults to 40. */
26
26
  diameter?: number
27
+ /** Override the skeleton background color. Defaults to `colors.skeleton`. */
28
+ backgroundColor?: string
27
29
  style?: ViewStyle
28
30
  }
29
31
 
@@ -33,6 +35,7 @@ export function Skeleton({
33
35
  borderRadius = 6,
34
36
  preset = 'base',
35
37
  diameter = 40,
38
+ backgroundColor,
36
39
  style,
37
40
  }: SkeletonProps) {
38
41
  const { colors, colorScheme } = useTheme()
@@ -40,7 +43,7 @@ export function Skeleton({
40
43
  const [containerWidth, setContainerWidth] = useState(300)
41
44
 
42
45
  const shimmerHighlight =
43
- colorScheme === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'
46
+ colorScheme === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.07)'
44
47
 
45
48
  useEffect(() => {
46
49
  // Repeats indefinitely on the UI thread — zero JS bridge cost per frame.
@@ -75,12 +78,12 @@ export function Skeleton({
75
78
  <View
76
79
  style={[
77
80
  styles.base,
78
- { width: resolvedWidth as number | `${number}%`, height: resolvedHeight, borderRadius: resolvedRadius, backgroundColor: colors.surface },
81
+ { width: resolvedWidth as number | `${number}%`, height: resolvedHeight, borderRadius: resolvedRadius, backgroundColor: backgroundColor ?? colors.skeleton },
79
82
  style,
80
83
  ]}
81
84
  onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
82
85
  accessibilityRole="progressbar"
83
- accessibilityLabel="Loading"
86
+ accessibilityLabel="Cargando"
84
87
  accessibilityState={{ busy: true }}
85
88
  >
86
89
  <Animated.View style={[StyleSheet.absoluteFill, shimmerStyle]}>
@@ -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 (