@retray-dev/ui-kit 12.1.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 (190) hide show
  1. package/COMPONENTS.md +98 -4
  2. package/README.md +4 -4
  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.js +26 -20
  26. package/dist/ConfirmDialog.mjs +3 -3
  27. package/dist/CurrencyDisplay.js +2 -0
  28. package/dist/CurrencyDisplay.mjs +2 -2
  29. package/dist/CurrencyInput.js +2 -0
  30. package/dist/CurrencyInput.mjs +3 -3
  31. package/dist/DetailRow.js +2 -0
  32. package/dist/DetailRow.mjs +2 -2
  33. package/dist/EmptyState.js +17 -17
  34. package/dist/EmptyState.mjs +3 -3
  35. package/dist/ErrorBoundary.js +2 -0
  36. package/dist/ErrorBoundary.mjs +2 -2
  37. package/dist/Form.js +2 -0
  38. package/dist/Form.mjs +2 -2
  39. package/dist/IconButton.js +2 -0
  40. package/dist/IconButton.mjs +2 -2
  41. package/dist/IconPicker.js +2 -0
  42. package/dist/IconPicker.mjs +3 -3
  43. package/dist/ImageUpload.d.mts +3 -1
  44. package/dist/ImageUpload.d.ts +3 -1
  45. package/dist/ImageUpload.js +10 -3
  46. package/dist/ImageUpload.mjs +3 -3
  47. package/dist/ImageViewer.js +2 -0
  48. package/dist/ImageViewer.mjs +4 -4
  49. package/dist/Input.js +2 -0
  50. package/dist/Input.mjs +2 -2
  51. package/dist/LabelValue.js +2 -0
  52. package/dist/LabelValue.mjs +2 -2
  53. package/dist/ListGroup.js +2 -0
  54. package/dist/ListGroup.mjs +2 -2
  55. package/dist/ListItem.js +2 -0
  56. package/dist/ListItem.mjs +2 -2
  57. package/dist/MediaCard.js +2 -0
  58. package/dist/MediaCard.mjs +2 -2
  59. package/dist/MenuGroup.js +2 -0
  60. package/dist/MenuGroup.mjs +2 -2
  61. package/dist/MenuItem.js +2 -0
  62. package/dist/MenuItem.mjs +2 -2
  63. package/dist/MonthPicker.js +2 -0
  64. package/dist/MonthPicker.mjs +2 -2
  65. package/dist/NumberStepper.js +2 -0
  66. package/dist/NumberStepper.mjs +2 -2
  67. package/dist/PagerDots.js +2 -0
  68. package/dist/PagerDots.mjs +2 -2
  69. package/dist/PricingCard.js +17 -17
  70. package/dist/PricingCard.mjs +4 -4
  71. package/dist/Progress.js +2 -0
  72. package/dist/Progress.mjs +2 -2
  73. package/dist/RadioGroup.js +2 -0
  74. package/dist/RadioGroup.mjs +2 -2
  75. package/dist/RetrayProvider.d.mts +1 -1
  76. package/dist/RetrayProvider.d.ts +1 -1
  77. package/dist/RetrayProvider.js +2 -0
  78. package/dist/RetrayProvider.mjs +3 -3
  79. package/dist/Select.js +2 -0
  80. package/dist/Select.mjs +2 -2
  81. package/dist/SelectableCard.d.mts +27 -0
  82. package/dist/SelectableCard.d.ts +27 -0
  83. package/dist/SelectableCard.js +511 -0
  84. package/dist/SelectableCard.mjs +8 -0
  85. package/dist/SelectableGrid.js +2 -0
  86. package/dist/SelectableGrid.mjs +2 -2
  87. package/dist/Separator.js +2 -0
  88. package/dist/Separator.mjs +2 -2
  89. package/dist/Sheet.js +11 -3
  90. package/dist/Sheet.mjs +2 -2
  91. package/dist/SheetSelect.js +2 -0
  92. package/dist/SheetSelect.mjs +2 -2
  93. package/dist/Skeleton.d.mts +3 -1
  94. package/dist/Skeleton.d.ts +3 -1
  95. package/dist/Skeleton.js +5 -2
  96. package/dist/Skeleton.mjs +2 -2
  97. package/dist/Slider.js +2 -0
  98. package/dist/Slider.mjs +2 -2
  99. package/dist/Spinner.js +2 -0
  100. package/dist/Spinner.mjs +2 -2
  101. package/dist/Stats.d.mts +4 -1
  102. package/dist/Stats.d.ts +4 -1
  103. package/dist/Stats.js +27 -3
  104. package/dist/Stats.mjs +2 -2
  105. package/dist/Switch.js +2 -0
  106. package/dist/Switch.mjs +2 -2
  107. package/dist/TabBar.js +2 -0
  108. package/dist/TabBar.mjs +2 -2
  109. package/dist/Tabs.js +2 -0
  110. package/dist/Tabs.mjs +2 -2
  111. package/dist/Text.js +2 -0
  112. package/dist/Text.mjs +2 -2
  113. package/dist/Textarea.js +2 -0
  114. package/dist/Textarea.mjs +2 -2
  115. package/dist/Toast.js +2 -0
  116. package/dist/Toast.mjs +2 -2
  117. package/dist/Toggle.js +2 -0
  118. package/dist/Toggle.mjs +2 -2
  119. package/dist/{chunk-JNVAIDLK.mjs → chunk-2BA3JMKK.mjs} +1 -1
  120. package/dist/{chunk-X26S5EVZ.mjs → chunk-2HFD4IHU.mjs} +1 -1
  121. package/dist/{chunk-ZHMSAYLT.mjs → chunk-2LG326TT.mjs} +1 -1
  122. package/dist/chunk-2P2CB235.mjs +236 -0
  123. package/dist/{chunk-MYZ2EDYU.mjs → chunk-3XCFYSX4.mjs} +1 -1
  124. package/dist/{chunk-DOGIPOF5.mjs → chunk-4J2PXL36.mjs} +16 -18
  125. package/dist/{chunk-V6NFJXKO.mjs → chunk-4OORJ2DY.mjs} +1 -1
  126. package/dist/{chunk-5OLNXP3S.mjs → chunk-4XOB5TTD.mjs} +26 -4
  127. package/dist/{chunk-P64WHW4A.mjs → chunk-57V2LXCK.mjs} +1 -1
  128. package/dist/{chunk-HJ46DTJE.mjs → chunk-7AFZWSCI.mjs} +1 -1
  129. package/dist/{chunk-AQEVCEXV.mjs → chunk-7ELGZ66G.mjs} +1 -1
  130. package/dist/{chunk-I4V5XZPS.mjs → chunk-AENAVIKT.mjs} +1 -1
  131. package/dist/{chunk-2CBQKU7H.mjs → chunk-BXF4AMHY.mjs} +1 -1
  132. package/dist/{chunk-JULSIZDM.mjs → chunk-C43HRKXH.mjs} +1 -1
  133. package/dist/{chunk-GK4VRMNE.mjs → chunk-CF27NBXO.mjs} +11 -6
  134. package/dist/{chunk-PGERH3P7.mjs → chunk-DF7JA72E.mjs} +1 -1
  135. package/dist/{chunk-F4V6XLP4.mjs → chunk-E5UKLSJZ.mjs} +3 -3
  136. package/dist/{chunk-FUVYSVGR.mjs → chunk-EDLCGYIO.mjs} +1 -1
  137. package/dist/{chunk-N4ZPVCJH.mjs → chunk-ELGEOM7I.mjs} +1 -1
  138. package/dist/{chunk-FA2KMTH5.mjs → chunk-F3YTWO3T.mjs} +1 -1
  139. package/dist/{chunk-3UYAZ7I4.mjs → chunk-GH67YXG6.mjs} +1 -1
  140. package/dist/{chunk-357YO24D.mjs → chunk-GUTDFUNF.mjs} +1 -1
  141. package/dist/{chunk-7HSILTC4.mjs → chunk-HC4VVCWY.mjs} +2 -2
  142. package/dist/{chunk-WOEWGSTU.mjs → chunk-HEDQPK4I.mjs} +1 -1
  143. package/dist/{chunk-3GEYJ7I5.mjs → chunk-IVSRW4HS.mjs} +1 -1
  144. package/dist/{chunk-P73V2EKS.mjs → chunk-KSUWPU2F.mjs} +1 -1
  145. package/dist/{chunk-BCWEHE34.mjs → chunk-LIS6I5UP.mjs} +1 -1
  146. package/dist/{chunk-J6Q2YJEV.mjs → chunk-LNPKGWBG.mjs} +1 -1
  147. package/dist/{chunk-DF6DU42P.mjs → chunk-LOBLCFMN.mjs} +1 -1
  148. package/dist/{chunk-2A2LEFZG.mjs → chunk-LPV4NJJK.mjs} +2 -2
  149. package/dist/{chunk-FFTYLPSB.mjs → chunk-M3C7XM2M.mjs} +11 -5
  150. package/dist/{chunk-BQZE3HAW.mjs → chunk-MEPSKGBO.mjs} +1 -1
  151. package/dist/{chunk-ISY26JQJ.mjs → chunk-MVMGPZN6.mjs} +2 -2
  152. package/dist/{chunk-265G6A46.mjs → chunk-NHDI3VQB.mjs} +15 -1
  153. package/dist/{chunk-D3Y2T42P.mjs → chunk-NJG7DHVF.mjs} +1 -1
  154. package/dist/{chunk-LRM4AVYY.mjs → chunk-NLZY4TXU.mjs} +1 -1
  155. package/dist/{chunk-OULVKTWL.mjs → chunk-OLVJFKXS.mjs} +1 -1
  156. package/dist/{chunk-BOVUP27T.mjs → chunk-QDAZGZUF.mjs} +4 -3
  157. package/dist/{chunk-S3KJCPEJ.mjs → chunk-QOLWA2PW.mjs} +1 -1
  158. package/dist/{chunk-JCZQOY4O.mjs → chunk-QXDGGOLC.mjs} +12 -6
  159. package/dist/{chunk-4WFMPFZB.mjs → chunk-RJNLAH76.mjs} +1 -1
  160. package/dist/{chunk-HLMPMUK2.mjs → chunk-RMRS44MQ.mjs} +1 -1
  161. package/dist/{chunk-KHYX4IOM.mjs → chunk-SAWUXP3A.mjs} +2 -2
  162. package/dist/{chunk-2I2AYECM.mjs → chunk-TS7DGUIR.mjs} +1 -1
  163. package/dist/{chunk-3N2M3WZL.mjs → chunk-UBUXUMER.mjs} +1 -1
  164. package/dist/{chunk-AKM4EPOT.mjs → chunk-ULGNQPNE.mjs} +1 -1
  165. package/dist/{chunk-FVTVCJAH.mjs → chunk-UNNRUJTM.mjs} +1 -1
  166. package/dist/{chunk-DI7CBDL6.mjs → chunk-UQ4742ET.mjs} +1 -1
  167. package/dist/{chunk-EFLFRAHD.mjs → chunk-VJBUCITV.mjs} +1 -1
  168. package/dist/{chunk-QSFV2P7O.mjs → chunk-YMYIEVZP.mjs} +1 -1
  169. package/dist/{chunk-EMUWGDWC.mjs → chunk-YTXRIXNZ.mjs} +2 -0
  170. package/dist/{chunk-XBAGGKLW.mjs → chunk-ZIMY2QUM.mjs} +2 -2
  171. package/dist/{chunk-NXI4YDZ2.mjs → chunk-ZR6HSEAB.mjs} +1 -1
  172. package/dist/{index-wt-orHUi.d.ts → index-CY34hxPN.d.mts} +1 -0
  173. package/dist/{index-wt-orHUi.d.mts → index-CY34hxPN.d.ts} +1 -0
  174. package/dist/index.d.mts +3 -2
  175. package/dist/index.d.ts +3 -2
  176. package/dist/index.js +733 -453
  177. package/dist/index.mjs +53 -52
  178. package/package.json +1 -1
  179. package/src/components/Accordion/Accordion.tsx +20 -0
  180. package/src/components/Button/Button.tsx +29 -26
  181. package/src/components/ConfirmDialog/ConfirmDialog.tsx +10 -3
  182. package/src/components/ImageUpload/ImageUpload.tsx +10 -3
  183. package/src/components/SelectableCard/SelectableCard.tsx +304 -0
  184. package/src/components/SelectableCard/index.ts +1 -0
  185. package/src/components/Sheet/Sheet.tsx +10 -3
  186. package/src/components/Skeleton/Skeleton.tsx +5 -2
  187. package/src/components/Stats/Stats.tsx +35 -7
  188. package/src/index.ts +1 -0
  189. package/src/theme/colors.ts +7 -0
  190. package/src/theme/types.ts +4 -1
package/dist/index.mjs CHANGED
@@ -1,64 +1,65 @@
1
+ export { Toggle } from './chunk-UNNRUJTM.mjs';
1
2
  export { VirtualList } from './chunk-NC5ZTR2Y.mjs';
2
- export { Stats } from './chunk-5OLNXP3S.mjs';
3
- export { Switch } from './chunk-HLMPMUK2.mjs';
4
- export { TabBar } from './chunk-D3Y2T42P.mjs';
5
- export { Tabs, TabsContent } from './chunk-3GEYJ7I5.mjs';
6
- export { Text } from './chunk-357YO24D.mjs';
7
- export { Textarea } from './chunk-JNVAIDLK.mjs';
8
- export { Toggle } from './chunk-FVTVCJAH.mjs';
9
- export { Select } from './chunk-WOEWGSTU.mjs';
10
- export { SelectableGrid } from './chunk-NXI4YDZ2.mjs';
11
- export { Separator } from './chunk-EFLFRAHD.mjs';
12
- export { BottomSheetModalProvider, Sheet, BottomSheetTextInput as SheetTextInput } from './chunk-FFTYLPSB.mjs';
13
- export { SheetSelect } from './chunk-P73V2EKS.mjs';
14
- export { Skeleton } from './chunk-BOVUP27T.mjs';
15
- export { Slider } from './chunk-4WFMPFZB.mjs';
16
- export { MonthPicker, dateToMonthPickerValue, monthPickerValueToDate } from './chunk-I4V5XZPS.mjs';
17
- export { NumberStepper } from './chunk-N4ZPVCJH.mjs';
3
+ export { Stats } from './chunk-4XOB5TTD.mjs';
4
+ export { Switch } from './chunk-RMRS44MQ.mjs';
5
+ export { TabBar } from './chunk-NJG7DHVF.mjs';
6
+ export { Tabs, TabsContent } from './chunk-IVSRW4HS.mjs';
7
+ export { Text } from './chunk-GUTDFUNF.mjs';
8
+ export { Textarea } from './chunk-2BA3JMKK.mjs';
9
+ export { Select } from './chunk-HEDQPK4I.mjs';
10
+ export { SelectableCard, SelectableCardGroup } from './chunk-2P2CB235.mjs';
11
+ export { SelectableGrid } from './chunk-ZR6HSEAB.mjs';
12
+ export { Separator } from './chunk-VJBUCITV.mjs';
13
+ export { BottomSheetModalProvider, Sheet, BottomSheetTextInput as SheetTextInput } from './chunk-M3C7XM2M.mjs';
14
+ export { SheetSelect } from './chunk-KSUWPU2F.mjs';
15
+ export { Skeleton } from './chunk-QDAZGZUF.mjs';
16
+ export { Slider } from './chunk-RJNLAH76.mjs';
17
+ export { MonthPicker, dateToMonthPickerValue, monthPickerValueToDate } from './chunk-AENAVIKT.mjs';
18
+ export { NumberStepper } from './chunk-ELGEOM7I.mjs';
18
19
  export { Pressable } from './chunk-E7NEHHXV.mjs';
19
- export { PricingCard } from './chunk-F4V6XLP4.mjs';
20
- export { Progress } from './chunk-2I2AYECM.mjs';
21
- export { RadioGroup } from './chunk-J6Q2YJEV.mjs';
22
- export { RetrayProvider } from './chunk-XBAGGKLW.mjs';
23
- export { ToastProvider, sonnerToast as toast, useToast } from './chunk-QSFV2P7O.mjs';
24
- export { ImageViewer } from './chunk-2A2LEFZG.mjs';
25
- export { PagerDots } from './chunk-OULVKTWL.mjs';
26
- export { LabelValue } from './chunk-P64WHW4A.mjs';
27
- export { ListGroup, ListGroupFooter, ListGroupHeader } from './chunk-JULSIZDM.mjs';
28
- export { ListItem } from './chunk-FUVYSVGR.mjs';
29
- export { MediaCard } from './chunk-AQEVCEXV.mjs';
30
- export { MenuGroup, MenuGroupFooter, MenuGroupHeader } from './chunk-AKM4EPOT.mjs';
31
- export { MenuItem } from './chunk-DI7CBDL6.mjs';
32
- export { DetailRow } from './chunk-ZHMSAYLT.mjs';
33
- export { EmptyState } from './chunk-7HSILTC4.mjs';
34
- export { ErrorBoundary } from './chunk-LRM4AVYY.mjs';
35
- export { Form, FormField, FormFooter, FormSection } from './chunk-HJ46DTJE.mjs';
36
- export { IconPicker } from './chunk-KHYX4IOM.mjs';
37
- export { ImageUpload } from './chunk-GK4VRMNE.mjs';
38
- export { Spinner } from './chunk-BCWEHE34.mjs';
20
+ export { PricingCard } from './chunk-E5UKLSJZ.mjs';
21
+ export { Progress } from './chunk-TS7DGUIR.mjs';
22
+ export { RadioGroup } from './chunk-LNPKGWBG.mjs';
23
+ export { RetrayProvider } from './chunk-ZIMY2QUM.mjs';
24
+ export { ToastProvider, sonnerToast as toast, useToast } from './chunk-YMYIEVZP.mjs';
25
+ export { ImageViewer } from './chunk-LPV4NJJK.mjs';
26
+ export { PagerDots } from './chunk-OLVJFKXS.mjs';
27
+ export { LabelValue } from './chunk-57V2LXCK.mjs';
28
+ export { ListGroup, ListGroupFooter, ListGroupHeader } from './chunk-C43HRKXH.mjs';
29
+ export { ListItem } from './chunk-EDLCGYIO.mjs';
30
+ export { MediaCard } from './chunk-7ELGZ66G.mjs';
31
+ export { MenuGroup, MenuGroupFooter, MenuGroupHeader } from './chunk-ULGNQPNE.mjs';
32
+ export { MenuItem } from './chunk-UQ4742ET.mjs';
33
+ export { DetailRow } from './chunk-2LG326TT.mjs';
34
+ export { EmptyState } from './chunk-HC4VVCWY.mjs';
35
+ export { ErrorBoundary } from './chunk-NLZY4TXU.mjs';
36
+ export { Form, FormField, FormFooter, FormSection } from './chunk-7AFZWSCI.mjs';
37
+ export { IconPicker } from './chunk-SAWUXP3A.mjs';
38
+ export { ImageUpload } from './chunk-CF27NBXO.mjs';
39
+ export { Spinner } from './chunk-LIS6I5UP.mjs';
39
40
  export { ButtonGroup } from './chunk-3BBOZ3OQ.mjs';
40
- export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './chunk-2CBQKU7H.mjs';
41
- export { CategoryStrip } from './chunk-MYZ2EDYU.mjs';
41
+ export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './chunk-BXF4AMHY.mjs';
42
+ export { CategoryStrip } from './chunk-3XCFYSX4.mjs';
42
43
  import './chunk-YNROWHQJ.mjs';
43
- export { Checkbox } from './chunk-3N2M3WZL.mjs';
44
- export { Chip, ChipGroup } from './chunk-FA2KMTH5.mjs';
45
- export { ConfirmDialog } from './chunk-JCZQOY4O.mjs';
46
- export { CurrencyDisplay } from './chunk-BQZE3HAW.mjs';
47
- export { CurrencyInput } from './chunk-X26S5EVZ.mjs';
48
- export { Input } from './chunk-PGERH3P7.mjs';
49
- export { Accordion } from './chunk-265G6A46.mjs';
50
- export { AlertBanner } from './chunk-V6NFJXKO.mjs';
51
- export { AppHeader } from './chunk-ISY26JQJ.mjs';
52
- export { IconButton } from './chunk-DF6DU42P.mjs';
53
- export { Avatar } from './chunk-S3KJCPEJ.mjs';
54
- export { Badge } from './chunk-3UYAZ7I4.mjs';
55
- export { Button } from './chunk-DOGIPOF5.mjs';
44
+ export { Checkbox } from './chunk-UBUXUMER.mjs';
45
+ export { Chip, ChipGroup } from './chunk-F3YTWO3T.mjs';
46
+ export { ConfirmDialog } from './chunk-QXDGGOLC.mjs';
47
+ export { CurrencyDisplay } from './chunk-MEPSKGBO.mjs';
48
+ export { CurrencyInput } from './chunk-2HFD4IHU.mjs';
49
+ export { Input } from './chunk-DF7JA72E.mjs';
50
+ export { Accordion } from './chunk-NHDI3VQB.mjs';
51
+ export { AlertBanner } from './chunk-4OORJ2DY.mjs';
52
+ export { AppHeader } from './chunk-MVMGPZN6.mjs';
53
+ export { IconButton } from './chunk-LOBLCFMN.mjs';
54
+ export { Avatar } from './chunk-QOLWA2PW.mjs';
55
+ export { Badge } from './chunk-GH67YXG6.mjs';
56
+ export { Button } from './chunk-4J2PXL36.mjs';
56
57
  import './chunk-3DKJ2GIC.mjs';
57
58
  export { impactHeavy, impactLight, impactMedium, notificationError, notificationSuccess, notificationWarning, richHaptics, selectionAsync } from './chunk-EJ7ZPXOH.mjs';
58
59
  import './chunk-DVK4G2GT.mjs';
59
60
  export { BREAKPOINTS, ICON_SIZES, RADIUS, SHADOWS, SPACING, TYPOGRAPHY } from './chunk-QY3X2UYR.mjs';
60
61
  export { Icon, configureIconFamilies, getValidIconNames, renderIcon } from './chunk-KA7LTET3.mjs';
61
- export { ThemeProvider, defaultDark, defaultLight, deriveColors, useTheme, withAlpha } from './chunk-EMUWGDWC.mjs';
62
+ export { ThemeProvider, defaultDark, defaultLight, deriveColors, useTheme, withAlpha } from './chunk-YTXRIXNZ.mjs';
62
63
  import './chunk-2CE3TQVY.mjs';
63
64
  import './chunk-Y6FXYEAI.mjs';
64
65
  import { useState, useCallback } from 'react';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retray-dev/ui-kit",
3
- "version": "12.1.0",
3
+ "version": "12.2.0",
4
4
  "description": "Personal UI Kit for React Native / Expo",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -29,6 +29,12 @@ export interface AccordionItem {
29
29
  icon?: React.ReactNode
30
30
  /** Override icon color. Defaults to foregroundMuted. */
31
31
  iconColor?: string
32
+ /**
33
+ * Action buttons rendered after the trigger content but before the chevron.
34
+ * Automatically touch-isolated — taps on actions won't toggle the accordion.
35
+ * Use this instead of embedding interactive elements inside `trigger`.
36
+ */
37
+ triggerActions?: React.ReactNode
32
38
  }
33
39
 
34
40
  export interface AccordionProps {
@@ -112,6 +118,14 @@ function AccordionItemComponent({
112
118
  item.trigger
113
119
  )}
114
120
  </View>
121
+ {item.triggerActions ? (
122
+ <View
123
+ style={styles.triggerActions}
124
+ onTouchEnd={(e) => e.stopPropagation()}
125
+ >
126
+ {item.triggerActions}
127
+ </View>
128
+ ) : null}
115
129
  <Animated.View style={[styles.chevron, rotationStyle]}>
116
130
  <Entypo name="chevron-down" size={18} color={colors.foregroundMuted} />
117
131
  </Animated.View>
@@ -206,6 +220,12 @@ const styles = StyleSheet.create({
206
220
  chevron: {
207
221
  marginLeft: s(8),
208
222
  },
223
+ triggerActions: {
224
+ flexDirection: 'row',
225
+ alignItems: 'center',
226
+ gap: s(4),
227
+ marginLeft: s(8),
228
+ },
209
229
  // position:'absolute' is the key — the inner View escapes the animated wrapper's
210
230
  // clipped height so onLayout always reports the true content height.
211
231
  content: {
@@ -73,31 +73,38 @@ function ButtonBase({
73
73
  onPress?.()
74
74
  }
75
75
 
76
- const containerVariantStyle: ViewStyle = {
77
- primary: { backgroundColor: colors.primary },
78
- secondary: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: colors.primary },
79
- text: { backgroundColor: 'transparent' },
80
- destructive: { backgroundColor: colors.destructive },
81
- }[variant]
82
-
83
- const labelVariantStyle: TextStyle = {
84
- primary: { color: colors.primaryForeground },
85
- secondary: { color: colors.primary },
86
- // AUDIT FIX: was colors.foreground — visually indistinguishable from plain text,
87
- // no affordance that it's a CTA. Now uses accentResolved so text-only buttons
88
- // carry the brand voltage. Falls back to primary when no accent is defined.
89
- text: { color: colors.accentResolved },
90
- destructive: { color: colors.destructiveForeground },
91
- }[variant]
92
-
93
- const textColor = iconColor ?? (labelVariantStyle.color as string)
76
+ const containerVariantStyle: ViewStyle = isDisabled
77
+ ? {
78
+ primary: { backgroundColor: colors.surface },
79
+ secondary: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: colors.border },
80
+ text: { backgroundColor: 'transparent' },
81
+ destructive: { backgroundColor: colors.surface },
82
+ }[variant]
83
+ : {
84
+ primary: { backgroundColor: colors.primary },
85
+ secondary: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: colors.primary },
86
+ text: { backgroundColor: 'transparent' },
87
+ destructive: { backgroundColor: colors.destructive },
88
+ }[variant]
89
+
90
+ const labelColor = isDisabled
91
+ ? colors.foregroundMuted
92
+ : {
93
+ primary: colors.primaryForeground,
94
+ secondary: colors.primary,
95
+ text: colors.accentResolved,
96
+ destructive: colors.destructiveForeground,
97
+ }[variant]
98
+
99
+ const textColor = iconColor ?? labelColor
94
100
 
95
101
  const effectiveIcon: React.ReactNode = iconName
96
102
  ? renderIcon(iconName, iconSizeMap[size], textColor)
97
103
  : typeof icon === 'function' ? icon({ label, size, variant, color: textColor }) : icon
98
104
 
99
- const spinnerColor =
100
- variant === 'destructive' ? colors.destructiveForeground
105
+ const spinnerColor = isDisabled
106
+ ? colors.foregroundMuted
107
+ : variant === 'destructive' ? colors.destructiveForeground
101
108
  : variant === 'primary' ? colors.primaryForeground
102
109
  : colors.accentResolved
103
110
 
@@ -113,7 +120,6 @@ function ButtonBase({
113
120
  containerVariantStyle,
114
121
  containerSizeStyles[size],
115
122
  fullWidth && styles.fullWidth,
116
- isDisabled && styles.disabled,
117
123
  restStyle,
118
124
  ]}
119
125
  enabled={!isDisabled}
@@ -130,7 +136,7 @@ function ButtonBase({
130
136
  <>
131
137
  <ActivityIndicator size="small" color={spinnerColor} style={{ marginRight: s(6) }} />
132
138
  <Text
133
- style={[styles.label, labelVariantStyle, labelSizeStyles[size], styles.labelLoading]}
139
+ style={[styles.label, { color: labelColor }, labelSizeStyles[size], styles.labelLoading]}
134
140
  allowFontScaling={true}
135
141
  numberOfLines={1}
136
142
  >
@@ -141,7 +147,7 @@ function ButtonBase({
141
147
  <>
142
148
  {effectiveIcon && iconPosition === 'left' && <>{effectiveIcon}</>}
143
149
  <Text
144
- style={[styles.label, labelVariantStyle, labelSizeStyles[size], effectiveIcon ? styles.labelWithIcon : undefined]}
150
+ style={[styles.label, { color: labelColor }, labelSizeStyles[size], effectiveIcon ? styles.labelWithIcon : undefined]}
145
151
  allowFontScaling={true}
146
152
  numberOfLines={1}
147
153
  >
@@ -167,9 +173,6 @@ const styles = StyleSheet.create({
167
173
  fullWidth: {
168
174
  width: '100%',
169
175
  },
170
- disabled: {
171
- opacity: 0.45,
172
- },
173
176
  label: {
174
177
  fontFamily: 'Sohne-Medium',
175
178
  flexShrink: 1,
@@ -42,14 +42,21 @@ export function ConfirmDialog({
42
42
  const insets = useSafeAreaInsets()
43
43
  const ref = useRef<BottomSheetModal>(null)
44
44
  const wasOpened = useRef(false)
45
+ const isPresentedRef = useRef(false)
45
46
  const name = useId()
46
47
 
48
+ const handleDismiss = useCallback(() => {
49
+ isPresentedRef.current = false
50
+ onCancel()
51
+ }, [onCancel])
52
+
47
53
  useEffect(() => {
48
- if (visible) {
54
+ if (visible && !isPresentedRef.current) {
49
55
  impactMedium()
50
56
  ref.current?.present()
51
57
  wasOpened.current = true
52
- } else if (wasOpened.current) {
58
+ isPresentedRef.current = true
59
+ } else if (!visible && wasOpened.current && isPresentedRef.current) {
53
60
  ref.current?.dismiss()
54
61
  }
55
62
  }, [visible])
@@ -70,7 +77,7 @@ export function ConfirmDialog({
70
77
  <BottomSheetModal
71
78
  ref={ref}
72
79
  name={name}
73
- onDismiss={onCancel}
80
+ onDismiss={handleDismiss}
74
81
  enableDynamicSizing
75
82
  backdropComponent={renderBackdrop}
76
83
  backgroundStyle={{ ...styles.background, backgroundColor: colors.card }}
@@ -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}>
@@ -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'