@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
@@ -0,0 +1,304 @@
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 { renderIcon } 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
+ ? renderIcon(iconName, ms(22), 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
+ const selectorAccessibilityRole = type === 'radio' ? 'radio' : 'checkbox'
157
+
158
+ return (
159
+ <Pressable
160
+ onPress={handlePress}
161
+ disabled={disabled}
162
+ accessibilityRole="button"
163
+ accessibilityLabel={`${title}${description ? `, ${description}` : ''}`}
164
+ accessibilityState={{ selected: isSelected, disabled }}
165
+ style={[
166
+ styles.card,
167
+ variantStyle,
168
+ isSelected && !disabled && styles.cardSelected,
169
+ style,
170
+ ]}
171
+ >
172
+ <View style={styles.row}>
173
+ {/* Selection indicator */}
174
+ <View style={styles.selectorContainer} accessibilityRole={selectorAccessibilityRole} accessibilityState={{ selected: isSelected, disabled }}>
175
+ {type === 'radio' ? (
176
+ <EaseView
177
+ style={styles.radioCircle}
178
+ animate={{ borderColor: !disabled && isSelected ? colors.primary : colors.border }}
179
+ transition={COLOR_TRANSITION}
180
+ >
181
+ <EaseView
182
+ animate={{
183
+ scale: !disabled && isSelected ? 1 : 0,
184
+ opacity: !disabled && isSelected ? 1 : 0,
185
+ }}
186
+ transition={SPRING_ELASTIC}
187
+ >
188
+ <View style={[styles.radioDot, { backgroundColor: colors.primary }]} />
189
+ </EaseView>
190
+ </EaseView>
191
+ ) : (
192
+ <EaseView
193
+ style={styles.checkboxBox}
194
+ animate={{
195
+ borderColor: !disabled && isSelected ? colors.primary : colors.border,
196
+ backgroundColor: !disabled && isSelected ? colors.primary : 'transparent',
197
+ }}
198
+ transition={COLOR_TRANSITION}
199
+ >
200
+ <EaseView
201
+ animate={{ opacity: !disabled && isSelected ? 1 : 0 }}
202
+ transition={OPACITY_TRANSITION}
203
+ >
204
+ <View style={[styles.checkmark, { borderColor: colors.primaryForeground }]} />
205
+ </EaseView>
206
+ </EaseView>
207
+ )}
208
+ </View>
209
+
210
+ {/* Optional icon */}
211
+ {resolvedIconElement}
212
+
213
+ {/* Title + description */}
214
+ <View style={styles.textArea}>
215
+ <Text
216
+ style={[styles.title, { color: disabled ? colors.foregroundMuted : colors.foreground }]}
217
+ allowFontScaling={true}
218
+ numberOfLines={2}
219
+ >
220
+ {title}
221
+ </Text>
222
+ {description ? (
223
+ <Text
224
+ style={[styles.description, { color: disabled ? colors.foregroundMuted : colors.foregroundSubtle }]}
225
+ allowFontScaling={true}
226
+ numberOfLines={4}
227
+ >
228
+ {description}
229
+ </Text>
230
+ ) : null}
231
+ </View>
232
+ </View>
233
+ </Pressable>
234
+ )
235
+ }
236
+
237
+ // ─── Styles ───────────────────────────────────────────────────────────────────
238
+
239
+ const styles = StyleSheet.create({
240
+ group: {
241
+ width: '100%',
242
+ },
243
+ card: {
244
+ borderRadius: RADIUS.md,
245
+ borderWidth: 1,
246
+ },
247
+ cardSelected: {
248
+ backgroundColor: undefined, // overridden by variantStyle
249
+ },
250
+ row: {
251
+ flexDirection: 'row',
252
+ alignItems: 'flex-start',
253
+ padding: s(16),
254
+ gap: s(12),
255
+ },
256
+ selectorContainer: {
257
+ paddingTop: vs(1),
258
+ },
259
+ radioCircle: {
260
+ width: s(24),
261
+ height: s(24),
262
+ borderRadius: s(12),
263
+ borderWidth: 2,
264
+ alignItems: 'center',
265
+ justifyContent: 'center',
266
+ },
267
+ radioDot: {
268
+ width: s(10),
269
+ height: s(10),
270
+ borderRadius: s(5),
271
+ },
272
+ checkboxBox: {
273
+ width: s(24),
274
+ height: s(24),
275
+ borderRadius: ms(4),
276
+ borderWidth: 2,
277
+ alignItems: 'center',
278
+ justifyContent: 'center',
279
+ },
280
+ checkmark: {
281
+ width: s(12),
282
+ height: vs(7),
283
+ borderLeftWidth: 2,
284
+ borderBottomWidth: 2,
285
+ transform: [{ rotate: '-45deg' }, { translateY: -1 }],
286
+ },
287
+ iconWrapper: {
288
+ paddingTop: vs(1),
289
+ },
290
+ textArea: {
291
+ flex: 1,
292
+ gap: vs(4),
293
+ },
294
+ title: {
295
+ fontFamily: 'Sohne-SemiBold',
296
+ fontSize: ms(16),
297
+ lineHeight: mvs(22),
298
+ },
299
+ description: {
300
+ fontFamily: 'Sohne-Regular',
301
+ fontSize: ms(13),
302
+ lineHeight: mvs(18),
303
+ },
304
+ })
@@ -0,0 +1 @@
1
+ export * from './SelectableCard'
@@ -1,6 +1,7 @@
1
- import React, { useCallback, useEffect, useRef } from 'react'
2
- import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, Dimensions, Platform, Modal, ScrollView, useWindowDimensions, Pressable } from 'react-native'
3
- import BottomSheet, {
1
+ import React, { useCallback, useEffect, useRef, useId } from 'react'
2
+ import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native'
3
+ import {
4
+ BottomSheetModal,
4
5
  BottomSheetView,
5
6
  BottomSheetScrollView,
6
7
  BottomSheetBackdrop,
@@ -15,14 +16,8 @@ import { AntDesign } from '@expo/vector-icons'
15
16
  import { impactMedium } from '../../utils/haptics'
16
17
  import { useTheme } from '../../theme'
17
18
  import { s, vs, ms, mvs } from '../../utils/scaling'
18
- import { BREAKPOINTS, RADIUS, SHADOWS } from '../../tokens'
19
-
20
- const SCREEN_HEIGHT = Dimensions.get('window').height
21
- const DEFAULT_MAX_HEIGHT = SCREEN_HEIGHT * 0.85
22
- const isAndroid = Platform.OS === 'android'
23
19
 
24
20
  export { BottomSheetModalProvider }
25
- // Re-export BottomSheetTextInput as SheetTextInput for consumer convenience
26
21
  export { BottomSheetTextInput as SheetTextInput }
27
22
 
28
23
  export interface SheetHeaderProps {
@@ -44,67 +39,25 @@ export interface SheetProps {
44
39
  open: boolean
45
40
  onClose: () => void
46
41
  title?: string
47
- /** Secondary text below title. */
48
42
  subtitle?: string
49
- /** @deprecated Use `subtitle` instead. */
50
- description?: string
51
- /** Show an X close button in the header. */
52
43
  showCloseButton?: boolean
53
44
  children?: React.ReactNode
54
- /** Style for the inner content container. */
55
45
  style?: ViewStyle
56
- /** Style for the content wrapper (outside the scroll container). */
57
46
  contentStyle?: ViewStyle
58
- /** Render children inside BottomSheetScrollView. */
47
+ /** Render children inside BottomSheetScrollView instead of BottomSheetView. */
59
48
  scrollable?: boolean
60
- /** Cap sheet height (dp). Children scroll when content exceeds this value. */
49
+ /** Cap sheet height (dp). Content scrolls when exceeding this value. Requires `scrollable`. */
61
50
  maxHeight?: number
62
- /**
63
- * Keyboard behavior — how the sheet responds to keyboard appearance.
64
- * - 'interactive': offset sheet by keyboard size (default, works on both platforms)
65
- * - 'fillParent': extend sheet to fill parent view (can cause restore issues with dynamic sizing)
66
- * - 'extend': extend sheet to maximum snap point
67
- *
68
- * Default: 'interactive' on both platforms.
69
- */
70
51
  keyboardBehavior?: 'extend' | 'fillParent' | 'interactive'
71
- /**
72
- * Keyboard blur behavior — what happens when keyboard dismisses.
73
- * - 'none': do nothing
74
- * - 'restore': restore sheet to previous position (default)
75
- */
76
52
  keyboardBlurBehavior?: 'none' | 'restore'
77
- /**
78
- * Blur keyboard when user starts dragging the sheet down.
79
- * Default: true (recommended for better UX)
80
- */
81
53
  enableBlurKeyboardOnGesture?: boolean
82
- /**
83
- * Android-only: defines keyboard input mode.
84
- * - 'adjustPan': pan the sheet content (default, fixes restore issues with dynamic sizing)
85
- * - 'adjustResize': resize the sheet container (can cause transparent gap on dismiss)
86
- */
87
54
  android_keyboardInputMode?: 'adjustPan' | 'adjustResize'
88
- /** Sticky footer rendered below the scroll area. */
89
55
  footer?: React.ReactNode
90
56
  /**
91
- * Array of snap points for the sheet (e.g., ['50%', '85%'] or [200, 500]).
57
+ * Array of snap points (e.g., ['50%', '85%'] or [200, 500]).
92
58
  * When provided, disables enableDynamicSizing.
93
- * When omitted, sheet uses dynamic sizing (auto-fits content).
94
59
  */
95
60
  snapPoints?: (string | number)[]
96
- /**
97
- * When true, render as a centered modal dialog on wide screens (width ≥
98
- * `BREAKPOINTS.wide`) instead of a bottom sheet. On narrow screens it stays a
99
- * bottom sheet. Use for store/category/picker dialogs that should feel native
100
- * on tablets and web.
101
- *
102
- * Note: the centered-dialog path uses a plain RN `Modal`, so `SheetTextInput`
103
- * is not required there — use a regular `TextInput`.
104
- */
105
- responsive?: boolean
106
- /** Max width of the centered dialog (dp). Only applies when `responsive`. Defaults to 480. */
107
- dialogMaxWidth?: number
108
61
  }
109
62
 
110
63
  export function SheetHeader({ children, style }: SheetHeaderProps) {
@@ -129,76 +82,80 @@ export function Sheet({
129
82
  onClose,
130
83
  title,
131
84
  subtitle,
132
- description,
133
85
  showCloseButton = false,
134
86
  children,
135
87
  style,
136
88
  contentStyle,
137
- scrollable,
89
+ scrollable = false,
138
90
  maxHeight,
139
- keyboardBehavior,
91
+ keyboardBehavior = 'interactive',
140
92
  keyboardBlurBehavior = 'restore',
141
93
  enableBlurKeyboardOnGesture = true,
142
94
  android_keyboardInputMode = 'adjustPan',
143
95
  footer,
144
96
  snapPoints,
145
- responsive = false,
146
- dialogMaxWidth = 480,
147
97
  }: SheetProps) {
148
98
  const { colors } = useTheme()
149
99
  const insets = useSafeAreaInsets()
150
- const { width: windowWidth } = useWindowDimensions()
151
- const ref = useRef<BottomSheet>(null)
152
- const asDialog = responsive && windowWidth >= BREAKPOINTS.wide
153
-
154
- // 'interactive' + 'adjustPan' works properly with enableDynamicSizing on both platforms
155
- // 'fillParent' + 'adjustResize' causes restore issues (transparent gap when keyboard dismisses)
156
- const effectiveKeyboardBehavior = keyboardBehavior ?? 'interactive'
100
+ const ref = useRef<BottomSheetModal>(null)
101
+ const wasOpened = useRef(false)
102
+ const isPresentedRef = useRef(false)
103
+ const name = useId()
104
+
105
+ const handleDismiss = useCallback(() => {
106
+ isPresentedRef.current = false
107
+ onClose?.()
108
+ }, [onClose])
157
109
 
158
110
  useEffect(() => {
159
- if (open) {
111
+ if (open && !isPresentedRef.current) {
160
112
  impactMedium()
161
- ref.current?.snapToIndex(0)
162
- } else {
163
- ref.current?.close()
113
+ ref.current?.present()
114
+ wasOpened.current = true
115
+ isPresentedRef.current = true
116
+ } else if (!open && wasOpened.current && isPresentedRef.current) {
117
+ ref.current?.dismiss()
164
118
  }
165
119
  }, [open])
166
120
 
167
- const renderBackdrop = useCallback((props: BottomSheetBackdropProps) => (
168
- <BottomSheetBackdrop
169
- {...props}
170
- disappearsOnIndex={-1}
171
- appearsOnIndex={0}
172
- pressBehavior="close"
173
- />
174
- ), [])
121
+ const renderBackdrop = useCallback(
122
+ (props: BottomSheetBackdropProps) => (
123
+ <BottomSheetBackdrop
124
+ {...props}
125
+ disappearsOnIndex={-1}
126
+ appearsOnIndex={0}
127
+ pressBehavior="close"
128
+ />
129
+ ),
130
+ []
131
+ )
175
132
 
176
- // Detect compound components in children
177
133
  const childArray = React.Children.toArray(children)
178
134
  const customHeader = childArray.find((child) => React.isValidElement(child) && child.type === SheetHeader)
179
135
  const customContent = childArray.find((child) => React.isValidElement(child) && child.type === SheetContent)
180
136
  const customFooter = childArray.find((child) => React.isValidElement(child) && child.type === SheetFooter)
181
-
182
- // If using compound components, filter them out from main children
183
- const filteredChildren = customHeader || customContent || customFooter
184
- ? childArray.filter(
185
- (child) =>
186
- !React.isValidElement(child) ||
187
- (child.type !== SheetHeader && child.type !== SheetContent && child.type !== SheetFooter)
188
- )
189
- : children
190
137
 
191
- const effectiveSubtitle = subtitle ?? description
192
- const showHeader = !!(title || effectiveSubtitle || showCloseButton) && !customHeader
138
+ const filteredChildren =
139
+ customHeader || customContent || customFooter
140
+ ? childArray.filter(
141
+ (child) =>
142
+ !React.isValidElement(child) ||
143
+ (child.type !== SheetHeader && child.type !== SheetContent && child.type !== SheetFooter)
144
+ )
145
+ : children
193
146
 
194
- const headerNode = customHeader ? customHeader : (showHeader ? (
147
+ const showHeader = !!(title || subtitle || showCloseButton) && !customHeader
148
+
149
+ const headerNode = customHeader ? customHeader : showHeader ? (
195
150
  <View style={[styles.header, { backgroundColor: colors.card }]} accessibilityRole="header">
196
151
  <View style={styles.headerRow}>
197
152
  {title ? (
198
153
  <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
199
154
  {title}
200
155
  </Text>
201
- ) : <View style={{ flex: 1 }} />}
156
+ ) : (
157
+ <View style={{ flex: 1 }} />
158
+ )}
202
159
  {showCloseButton ? (
203
160
  <TouchableOpacity
204
161
  onPress={onClose}
@@ -213,90 +170,52 @@ export function Sheet({
213
170
  </TouchableOpacity>
214
171
  ) : null}
215
172
  </View>
216
- {effectiveSubtitle ? (
173
+ {subtitle ? (
217
174
  <Text style={[styles.subtitle, { color: colors.foregroundMuted }]} allowFontScaling={true}>
218
- {effectiveSubtitle}
175
+ {subtitle}
219
176
  </Text>
220
177
  ) : null}
221
178
  </View>
222
- ) : null)
179
+ ) : null
223
180
 
224
181
  const contentNode = customContent ? customContent : filteredChildren
225
182
  const effectiveFooter = customFooter ? customFooter : footer
226
183
 
227
- const renderFooter = useCallback((props: BottomSheetFooterProps) => {
228
- if (!effectiveFooter) return null
229
- return (
230
- <BottomSheetFooter {...props}>
231
- {effectiveFooter}
232
- </BottomSheetFooter>
233
- )
234
- }, [effectiveFooter])
235
-
236
- // Centered dialog path for wide screens — plain RN Modal, same header/content/footer.
237
- if (asDialog) {
238
- return (
239
- <Modal visible={open} transparent animationType="fade" onRequestClose={onClose}>
240
- <Pressable style={styles.dialogBackdrop} onPress={onClose} accessibilityRole="button" accessibilityLabel="Close">
241
- {/* Inner Pressable swallows presses so taps inside the card don't close it. */}
242
- <Pressable
243
- style={[
244
- styles.dialogCard,
245
- { backgroundColor: colors.card, maxWidth: dialogMaxWidth, maxHeight: SCREEN_HEIGHT * 0.85 },
246
- ]}
247
- onPress={() => {}}
248
- >
249
- {headerNode}
250
- <ScrollView
251
- contentContainerStyle={[styles.dialogContent, style]}
252
- style={contentStyle}
253
- showsVerticalScrollIndicator={true}
254
- bounces={false}
255
- >
256
- {contentNode}
257
- </ScrollView>
258
- {effectiveFooter}
259
- </Pressable>
260
- </Pressable>
261
- </Modal>
262
- )
263
- }
184
+ const renderFooter = useCallback(
185
+ (props: BottomSheetFooterProps) => {
186
+ if (!effectiveFooter) return null
187
+ return <BottomSheetFooter {...props}>{effectiveFooter}</BottomSheetFooter>
188
+ },
189
+ [effectiveFooter]
190
+ )
264
191
 
265
- const useScroll = scrollable || !!maxHeight
266
- const effectiveMaxHeight = maxHeight ?? DEFAULT_MAX_HEIGHT
267
-
268
- // If snapPoints provided, disable dynamic sizing. Otherwise use dynamic sizing.
269
192
  const useDynamicSizing = !snapPoints
270
-
193
+
271
194
  return (
272
- <BottomSheet
195
+ <BottomSheetModal
273
196
  ref={ref}
274
- index={-1}
275
- onClose={onClose}
197
+ name={name}
198
+ onDismiss={handleDismiss}
276
199
  enableDynamicSizing={useDynamicSizing}
277
200
  snapPoints={snapPoints}
278
- maxDynamicContentSize={useDynamicSizing ? effectiveMaxHeight : undefined}
201
+ maxDynamicContentSize={useDynamicSizing && maxHeight ? maxHeight : undefined}
279
202
  backdropComponent={renderBackdrop}
280
203
  footerComponent={effectiveFooter ? renderFooter : undefined}
281
- backgroundStyle={[styles.background, { backgroundColor: colors.card }]}
282
- handleIndicatorStyle={[styles.handle, { backgroundColor: colors.border }]}
204
+ backgroundStyle={{ ...styles.background, backgroundColor: colors.card }}
205
+ handleIndicatorStyle={{ ...styles.handle, backgroundColor: colors.border }}
283
206
  enablePanDownToClose
284
207
  topInset={insets.top}
285
- keyboardBehavior={effectiveKeyboardBehavior}
208
+ keyboardBehavior={keyboardBehavior}
286
209
  keyboardBlurBehavior={keyboardBlurBehavior}
287
210
  android_keyboardInputMode={android_keyboardInputMode}
288
211
  enableBlurKeyboardOnGesture={enableBlurKeyboardOnGesture}
289
212
  >
290
- {useScroll ? (
213
+ {scrollable ? (
291
214
  <BottomSheetScrollView
292
- contentContainerStyle={[
293
- styles.scrollContent,
294
- style,
295
- ]}
215
+ contentContainerStyle={[styles.scrollContent, style]}
296
216
  style={contentStyle}
297
- showsVerticalScrollIndicator={true}
298
- indicatorStyle="black"
299
- persistentScrollbar={isAndroid}
217
+ showsVerticalScrollIndicator
218
+ bounces={false}
300
219
  stickyHeaderIndices={headerNode ? [0] : undefined}
301
220
  >
302
221
  {headerNode}
@@ -308,7 +227,7 @@ export function Sheet({
308
227
  {contentNode}
309
228
  </BottomSheetView>
310
229
  )}
311
- </BottomSheet>
230
+ </BottomSheetModal>
312
231
  )
313
232
  }
314
233
 
@@ -358,7 +277,6 @@ const styles = StyleSheet.create({
358
277
  scrollContent: {
359
278
  paddingHorizontal: s(16),
360
279
  paddingBottom: vs(32),
361
- paddingRight: s(16),
362
280
  },
363
281
  sheetContent: {
364
282
  gap: vs(16),
@@ -370,23 +288,4 @@ const styles = StyleSheet.create({
370
288
  flexDirection: 'row',
371
289
  gap: s(12),
372
290
  },
373
- dialogBackdrop: {
374
- flex: 1,
375
- backgroundColor: 'rgba(0,0,0,0.5)',
376
- alignItems: 'center',
377
- justifyContent: 'center',
378
- padding: s(24),
379
- },
380
- dialogCard: {
381
- width: '100%',
382
- borderRadius: RADIUS.lg,
383
- paddingTop: vs(16),
384
- overflow: 'hidden',
385
- ...SHADOWS.xl,
386
- },
387
- dialogContent: {
388
- paddingHorizontal: s(16),
389
- paddingBottom: vs(16),
390
- },
391
291
  })
392
-