@retray-dev/ui-kit 7.0.1 → 9.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 (234) hide show
  1. package/COMPONENTS.md +554 -11
  2. package/EXAMPLES.md +2 -2
  3. package/README.md +14 -8
  4. package/dist/Accordion.js +57 -5
  5. package/dist/Accordion.mjs +4 -3
  6. package/dist/AlertBanner.js +4 -1
  7. package/dist/AlertBanner.mjs +3 -2
  8. package/dist/AppHeader.d.mts +40 -0
  9. package/dist/AppHeader.d.ts +40 -0
  10. package/dist/AppHeader.js +515 -0
  11. package/dist/AppHeader.mjs +10 -0
  12. package/dist/Avatar.js +39 -29
  13. package/dist/Avatar.mjs +2 -1
  14. package/dist/Badge.js +11 -1
  15. package/dist/Badge.mjs +2 -1
  16. package/dist/Button.d.mts +8 -3
  17. package/dist/Button.d.ts +8 -3
  18. package/dist/Button.js +126 -108
  19. package/dist/Button.mjs +6 -5
  20. package/dist/ButtonGroup.mjs +1 -0
  21. package/dist/Card.js +90 -70
  22. package/dist/Card.mjs +5 -4
  23. package/dist/CategoryStrip.js +79 -22
  24. package/dist/CategoryStrip.mjs +6 -6
  25. package/dist/Checkbox.js +118 -86
  26. package/dist/Checkbox.mjs +5 -5
  27. package/dist/Chip.js +113 -80
  28. package/dist/Chip.mjs +5 -5
  29. package/dist/ConfirmDialog.js +140 -110
  30. package/dist/ConfirmDialog.mjs +7 -6
  31. package/dist/CurrencyDisplay.mjs +1 -0
  32. package/dist/CurrencyInput.d.mts +1 -1
  33. package/dist/CurrencyInput.d.ts +1 -1
  34. package/dist/CurrencyInput.js +9 -5
  35. package/dist/CurrencyInput.mjs +5 -4
  36. package/dist/DetailRow.mjs +1 -0
  37. package/dist/EmptyState.js +131 -111
  38. package/dist/EmptyState.mjs +7 -6
  39. package/dist/ErrorBoundary.d.mts +42 -0
  40. package/dist/ErrorBoundary.d.ts +42 -0
  41. package/dist/ErrorBoundary.js +351 -0
  42. package/dist/ErrorBoundary.mjs +7 -0
  43. package/dist/Form.mjs +1 -0
  44. package/dist/HolographicCard.d.mts +55 -0
  45. package/dist/HolographicCard.d.ts +55 -0
  46. package/dist/HolographicCard.js +316 -0
  47. package/dist/HolographicCard.mjs +191 -0
  48. package/dist/IconButton.d.mts +8 -3
  49. package/dist/IconButton.d.ts +8 -3
  50. package/dist/IconButton.js +115 -98
  51. package/dist/IconButton.mjs +5 -4
  52. package/dist/ImageViewer.d.mts +23 -0
  53. package/dist/ImageViewer.d.ts +23 -0
  54. package/dist/ImageViewer.js +582 -0
  55. package/dist/ImageViewer.mjs +8 -0
  56. package/dist/Input.mjs +4 -3
  57. package/dist/LabelValue.mjs +1 -0
  58. package/dist/ListGroup.mjs +1 -0
  59. package/dist/ListItem.js +131 -117
  60. package/dist/ListItem.mjs +6 -5
  61. package/dist/MediaCard.js +54 -6
  62. package/dist/MediaCard.mjs +6 -5
  63. package/dist/MenuGroup.mjs +1 -0
  64. package/dist/MenuItem.js +91 -79
  65. package/dist/MenuItem.mjs +6 -5
  66. package/dist/MonthPicker.d.mts +10 -2
  67. package/dist/MonthPicker.d.ts +10 -2
  68. package/dist/MonthPicker.js +80 -17
  69. package/dist/MonthPicker.mjs +3 -2
  70. package/dist/PagerDots.d.mts +35 -0
  71. package/dist/PagerDots.d.ts +35 -0
  72. package/dist/PagerDots.js +392 -0
  73. package/dist/PagerDots.mjs +7 -0
  74. package/dist/Pressable.d.mts +5 -5
  75. package/dist/Pressable.d.ts +5 -5
  76. package/dist/Pressable.js +97 -86
  77. package/dist/Pressable.mjs +5 -4
  78. package/dist/PricingCard.d.mts +50 -0
  79. package/dist/PricingCard.d.ts +50 -0
  80. package/dist/PricingCard.js +636 -0
  81. package/dist/PricingCard.mjs +11 -0
  82. package/dist/Progress.mjs +3 -2
  83. package/dist/RadioGroup.js +81 -30
  84. package/dist/RadioGroup.mjs +5 -5
  85. package/dist/RetrayProvider.d.mts +2 -0
  86. package/dist/RetrayProvider.d.ts +2 -0
  87. package/dist/RetrayProvider.js +214 -0
  88. package/dist/RetrayProvider.mjs +5 -0
  89. package/dist/Select.js +51 -4
  90. package/dist/Select.mjs +5 -4
  91. package/dist/SelectableGrid.d.mts +44 -0
  92. package/dist/SelectableGrid.d.ts +44 -0
  93. package/dist/SelectableGrid.js +448 -0
  94. package/dist/SelectableGrid.mjs +9 -0
  95. package/dist/Separator.mjs +1 -0
  96. package/dist/Sheet.d.mts +13 -1
  97. package/dist/Sheet.d.ts +13 -1
  98. package/dist/Sheet.js +115 -5
  99. package/dist/Sheet.mjs +4 -2
  100. package/dist/Skeleton.d.mts +50 -0
  101. package/dist/Skeleton.d.ts +50 -0
  102. package/dist/Skeleton.js +61 -0
  103. package/dist/Skeleton.mjs +4 -2
  104. package/dist/Slider.js +51 -4
  105. package/dist/Slider.mjs +3 -2
  106. package/dist/Spinner.js +28 -7
  107. package/dist/Spinner.mjs +2 -1
  108. package/dist/Switch.js +98 -48
  109. package/dist/Switch.mjs +4 -3
  110. package/dist/TabBar.d.mts +42 -0
  111. package/dist/TabBar.d.ts +42 -0
  112. package/dist/TabBar.js +361 -0
  113. package/dist/TabBar.mjs +6 -0
  114. package/dist/Tabs.js +92 -62
  115. package/dist/Tabs.mjs +5 -4
  116. package/dist/Text.js +16 -0
  117. package/dist/Text.mjs +2 -1
  118. package/dist/Textarea.mjs +4 -3
  119. package/dist/Toast.d.mts +7 -7
  120. package/dist/Toast.d.ts +7 -7
  121. package/dist/Toast.mjs +1 -0
  122. package/dist/Toggle.d.mts +6 -3
  123. package/dist/Toggle.d.ts +6 -3
  124. package/dist/Toggle.js +135 -120
  125. package/dist/Toggle.mjs +5 -5
  126. package/dist/VirtualList.mjs +1 -0
  127. package/dist/{chunk-7H2OR44A.mjs → chunk-26BCI223.mjs} +1 -1
  128. package/dist/{chunk-CRYBX2CM.mjs → chunk-2TFTAWVJ.mjs} +44 -59
  129. package/dist/chunk-3DKJ2GIC.mjs +30 -0
  130. package/dist/{chunk-KWCPOM6W.mjs → chunk-3U4SSNWP.mjs} +32 -48
  131. package/dist/chunk-4I7D47FH.mjs +139 -0
  132. package/dist/chunk-4K625MVM.mjs +142 -0
  133. package/dist/{chunk-MN7OG7IY.mjs → chunk-6OAZJ577.mjs} +6 -4
  134. package/dist/{chunk-L7E7TVEZ.mjs → chunk-756RAKE4.mjs} +2 -2
  135. package/dist/{chunk-HSPSMN6U.mjs → chunk-7QHVVCB3.mjs} +2 -2
  136. package/dist/{chunk-URLL5JBR.mjs → chunk-A3A6KNQN.mjs} +3 -3
  137. package/dist/chunk-AJ7ZDNBT.mjs +120 -0
  138. package/dist/{chunk-FTLJOUOQ.mjs → chunk-AV4EMIRH.mjs} +25 -28
  139. package/dist/chunk-AZJF2BLK.mjs +115 -0
  140. package/dist/chunk-BNP626TY.mjs +159 -0
  141. package/dist/{chunk-5IKW3VNC.mjs → chunk-DVK4G2GT.mjs} +17 -1
  142. package/dist/{chunk-6LQYY7HC.mjs → chunk-EH745HE5.mjs} +2 -2
  143. package/dist/chunk-EJ7ZPXOH.mjs +163 -0
  144. package/dist/{chunk-RKLHUDZS.mjs → chunk-GD6KXMG5.mjs} +29 -15
  145. package/dist/{chunk-RR2VQLKE.mjs → chunk-GQYFLP3D.mjs} +14 -17
  146. package/dist/{chunk-Y6MXOREN.mjs → chunk-ID72TK46.mjs} +8 -17
  147. package/dist/{chunk-NQGVLMWG.mjs → chunk-JMOZEC77.mjs} +1 -1
  148. package/dist/{chunk-GCWOGZYL.mjs → chunk-JT7HKXRB.mjs} +39 -29
  149. package/dist/{chunk-LWG526VX.mjs → chunk-KIHCWCWL.mjs} +47 -62
  150. package/dist/chunk-LXJIIOYQ.mjs +104 -0
  151. package/dist/{chunk-SBZYEV4S.mjs → chunk-M6ZXVBTK.mjs} +5 -2
  152. package/dist/{chunk-XDMN67KV.mjs → chunk-MAC465BB.mjs} +10 -8
  153. package/dist/chunk-MBMXYJJV.mjs +36 -0
  154. package/dist/chunk-MLF3EZFW.mjs +119 -0
  155. package/dist/chunk-NA7PARID.mjs +147 -0
  156. package/dist/{chunk-QXGYKWI7.mjs → chunk-O3HA6TYM.mjs} +9 -4
  157. package/dist/{chunk-63357L2X.mjs → chunk-OB4JUQ3O.mjs} +1 -1
  158. package/dist/{chunk-AU2VDY4P.mjs → chunk-PFZTM6D5.mjs} +52 -4
  159. package/dist/chunk-QKH5ZOD5.mjs +97 -0
  160. package/dist/{chunk-KZJRQOIU.mjs → chunk-TERDKCLE.mjs} +11 -1
  161. package/dist/{chunk-U4N7WF4Z.mjs → chunk-UREA2GYY.mjs} +28 -23
  162. package/dist/{chunk-TAJ2PQ2O.mjs → chunk-VGTDN7SW.mjs} +7 -6
  163. package/dist/{chunk-URDE3EUU.mjs → chunk-VQ57HWPL.mjs} +27 -15
  164. package/dist/chunk-WBOOUHSS.mjs +62 -0
  165. package/dist/{chunk-GNGLDL6Z.mjs → chunk-WJLKJMKR.mjs} +18 -0
  166. package/dist/{chunk-YZJAFS4P.mjs → chunk-X4G6APW6.mjs} +22 -19
  167. package/dist/chunk-Y6FXYEAI.mjs +8 -0
  168. package/dist/chunk-YFZ3ELX5.mjs +16 -0
  169. package/dist/{chunk-QCNARS3X.mjs → chunk-YNROWHQJ.mjs} +1 -1
  170. package/dist/chunk-Z4BVUWW6.mjs +196 -0
  171. package/dist/{chunk-GPOUINK5.mjs → chunk-ZJKGQMYH.mjs} +10 -27
  172. package/dist/index-wt-orHUi.d.mts +85 -0
  173. package/dist/index-wt-orHUi.d.ts +85 -0
  174. package/dist/index.d.mts +59 -51
  175. package/dist/index.d.ts +59 -51
  176. package/dist/index.js +1940 -744
  177. package/dist/index.mjs +49 -39
  178. package/package.json +35 -5
  179. package/src/components/Accordion/Accordion.tsx +12 -1
  180. package/src/components/AlertBanner/AlertBanner.tsx +5 -0
  181. package/src/components/AppHeader/AppHeader.tsx +172 -0
  182. package/src/components/AppHeader/index.ts +1 -0
  183. package/src/components/Avatar/Avatar.tsx +10 -2
  184. package/src/components/Badge/Badge.tsx +8 -1
  185. package/src/components/Button/Button.tsx +20 -27
  186. package/src/components/Card/Card.tsx +12 -23
  187. package/src/components/CategoryStrip/CategoryStrip.tsx +17 -21
  188. package/src/components/Checkbox/Checkbox.tsx +26 -40
  189. package/src/components/Chip/Chip.tsx +24 -33
  190. package/src/components/CurrencyInput/CurrencyInput.tsx +10 -8
  191. package/src/components/EmptyState/EmptyState.tsx +2 -1
  192. package/src/components/ErrorBoundary/ErrorBoundary.tsx +153 -0
  193. package/src/components/ErrorBoundary/index.ts +1 -0
  194. package/src/components/HolographicCard/HolographicCard.tsx +315 -0
  195. package/src/components/HolographicCard/index.ts +1 -0
  196. package/src/components/IconButton/IconButton.tsx +19 -27
  197. package/src/components/ImageViewer/ImageViewer.tsx +290 -0
  198. package/src/components/ImageViewer/index.ts +1 -0
  199. package/src/components/ListItem/ListItem.tsx +70 -67
  200. package/src/components/MediaCard/MediaCard.tsx +8 -2
  201. package/src/components/MenuItem/MenuItem.tsx +10 -25
  202. package/src/components/MonthPicker/MonthPicker.tsx +39 -13
  203. package/src/components/MonthPicker/index.ts +1 -1
  204. package/src/components/PagerDots/PagerDots.tsx +200 -0
  205. package/src/components/PagerDots/index.ts +1 -0
  206. package/src/components/Pressable/Pressable.tsx +19 -35
  207. package/src/components/PricingCard/PricingCard.tsx +220 -0
  208. package/src/components/PricingCard/index.ts +1 -0
  209. package/src/components/RadioGroup/RadioGroup.tsx +14 -27
  210. package/src/components/RetrayProvider/RetrayProvider.tsx +59 -0
  211. package/src/components/RetrayProvider/index.ts +1 -0
  212. package/src/components/SelectableGrid/SelectableGrid.tsx +205 -0
  213. package/src/components/SelectableGrid/index.ts +1 -0
  214. package/src/components/Sheet/Sheet.tsx +65 -1
  215. package/src/components/Skeleton/Skeleton.tsx +142 -1
  216. package/src/components/Spinner/Spinner.tsx +17 -2
  217. package/src/components/Switch/Switch.tsx +30 -58
  218. package/src/components/TabBar/TabBar.tsx +169 -0
  219. package/src/components/TabBar/index.ts +1 -0
  220. package/src/components/Tabs/Tabs.tsx +23 -26
  221. package/src/components/Text/Text.tsx +2 -0
  222. package/src/components/Toggle/Toggle.tsx +35 -51
  223. package/src/fonts.ts +4 -1
  224. package/src/index.ts +23 -2
  225. package/src/utils/animations.ts +29 -1
  226. package/src/utils/fontGuard.ts +34 -0
  227. package/src/utils/haptics.ts +211 -9
  228. package/src/utils/pressable.ts +66 -0
  229. package/dist/chunk-76PFOSM2.mjs +0 -41
  230. package/dist/chunk-DITNP6PL.mjs +0 -106
  231. package/dist/chunk-JBLL7U3U.mjs +0 -64
  232. package/dist/chunk-LG4DO3DK.mjs +0 -174
  233. package/dist/chunk-RMMK64W5.mjs +0 -54
  234. package/dist/chunk-RTC3CFXF.mjs +0 -29
@@ -0,0 +1,200 @@
1
+ import React, { useEffect } from 'react'
2
+ import { View, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native'
3
+ import Animated, {
4
+ useSharedValue,
5
+ useAnimatedStyle,
6
+ withSpring,
7
+ interpolateColor,
8
+ } from 'react-native-reanimated'
9
+ import { useTheme } from '../../theme'
10
+ import { s } from '../../utils/scaling'
11
+ import { SPRINGS } from '../../utils/animations'
12
+ import { selectionAsync as hapticSelection } from '../../utils/haptics'
13
+ import { renderIcon } from '../../utils/icons'
14
+
15
+ export interface PagerDotsProps {
16
+ /** Total number of pages. */
17
+ count: number
18
+ /** Index of the active page (0-based). */
19
+ activeIndex: number
20
+ /** Called when a dot is tapped — omit to make dots non-interactive. */
21
+ onDotPress?: (index: number) => void
22
+ /** Show previous/next buttons. If function provided, called on button press. If `true`, uses `onDotPress(activeIndex ± 1)`. */
23
+ showControls?: boolean | { onPrevious?: () => void; onNext?: () => void }
24
+ /** Diameter of an inactive dot (dp). Defaults to 8. */
25
+ dotSize?: number
26
+ /** Gap between dots (dp). Defaults to 8. */
27
+ spacing?: number
28
+ /** Active dot color. Defaults to theme `primary`. */
29
+ activeColor?: string
30
+ /** Inactive dot color. Defaults to theme `border`. */
31
+ inactiveColor?: string
32
+ style?: ViewStyle
33
+ }
34
+
35
+ interface DotProps {
36
+ active: boolean
37
+ size: number
38
+ activeColor: string
39
+ inactiveColor: string
40
+ onPress?: () => void
41
+ /** Index of this dot (0-based) for accessibility label. */
42
+ index: number
43
+ /** Total number of dots for accessibility label. */
44
+ total: number
45
+ }
46
+
47
+ function Dot({ active, size, activeColor, inactiveColor, onPress, index, total }: DotProps) {
48
+ const progress = useSharedValue(active ? 1 : 0)
49
+
50
+ useEffect(() => {
51
+ progress.value = withSpring(active ? 1 : 0, SPRINGS.glide)
52
+ }, [active, progress])
53
+
54
+ // Active dot stretches into a pill (width = 2.5×). Color crossfades on the UI thread.
55
+ const animatedStyle = useAnimatedStyle(() => ({
56
+ width: size + progress.value * size * 1.5,
57
+ backgroundColor: interpolateColor(progress.value, [0, 1], [inactiveColor, activeColor]),
58
+ }))
59
+
60
+ const dot = (
61
+ <Animated.View style={[{ height: size, borderRadius: size / 2 }, animatedStyle]} />
62
+ )
63
+
64
+ if (!onPress) return dot
65
+
66
+ const handlePress = () => {
67
+ hapticSelection()
68
+ onPress()
69
+ }
70
+
71
+ return (
72
+ <TouchableOpacity
73
+ onPress={handlePress}
74
+ activeOpacity={0.7}
75
+ touchSoundDisabled={true}
76
+ accessibilityRole="button"
77
+ accessibilityLabel={`Page ${index + 1} of ${total}${active ? ', current page' : ''}`}
78
+ hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
79
+ >
80
+ {dot}
81
+ </TouchableOpacity>
82
+ )
83
+ }
84
+
85
+ /**
86
+ * Animated page indicator for carousels / document pagers. The active dot
87
+ * stretches into a pill and color-crossfades — all on the UI thread.
88
+ *
89
+ * @example
90
+ * <PagerDots count={pages.length} activeIndex={page} onDotPress={setPage} />
91
+ */
92
+ export function PagerDots({
93
+ count,
94
+ activeIndex,
95
+ onDotPress,
96
+ showControls = false,
97
+ dotSize = 8,
98
+ spacing = 8,
99
+ activeColor,
100
+ inactiveColor,
101
+ style,
102
+ }: PagerDotsProps) {
103
+ const { colors } = useTheme()
104
+ const resolvedActive = activeColor ?? colors.primary
105
+ const resolvedInactive = inactiveColor ?? colors.border
106
+ const size = s(dotSize)
107
+
108
+ const hasControls = showControls !== false
109
+ const canGoPrev = activeIndex > 0
110
+ const canGoNext = activeIndex < count - 1
111
+
112
+ const handlePrevious = () => {
113
+ if (!canGoPrev) return
114
+ hapticSelection()
115
+ if (typeof showControls === 'object' && showControls.onPrevious) {
116
+ showControls.onPrevious()
117
+ } else if (onDotPress) {
118
+ onDotPress(activeIndex - 1)
119
+ }
120
+ }
121
+
122
+ const handleNext = () => {
123
+ if (!canGoNext) return
124
+ hapticSelection()
125
+ if (typeof showControls === 'object' && showControls.onNext) {
126
+ showControls.onNext()
127
+ } else if (onDotPress) {
128
+ onDotPress(activeIndex + 1)
129
+ }
130
+ }
131
+
132
+ return (
133
+ <View
134
+ style={[styles.container, { gap: s(spacing) }, style]}
135
+ accessibilityRole="adjustable"
136
+ accessibilityLabel={`Page ${activeIndex + 1} of ${count}`}
137
+ >
138
+ {hasControls && (
139
+ <TouchableOpacity
140
+ onPress={handlePrevious}
141
+ disabled={!canGoPrev}
142
+ activeOpacity={0.7}
143
+ touchSoundDisabled={true}
144
+ accessibilityRole="button"
145
+ accessibilityLabel="Previous page"
146
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
147
+ style={[styles.controlBtn, !canGoPrev && styles.controlBtnDisabled]}
148
+ >
149
+ {renderIcon('chevron-left', s(18), canGoPrev ? colors.foreground : colors.foregroundMuted)}
150
+ </TouchableOpacity>
151
+ )}
152
+ <View style={[styles.dotsRow, { gap: s(spacing) }]}>
153
+ {Array.from({ length: count }).map((_, i) => (
154
+ <Dot
155
+ key={i}
156
+ active={i === activeIndex}
157
+ size={size}
158
+ activeColor={resolvedActive}
159
+ inactiveColor={resolvedInactive}
160
+ index={i}
161
+ total={count}
162
+ onPress={onDotPress ? () => onDotPress(i) : undefined}
163
+ />
164
+ ))}
165
+ </View>
166
+ {hasControls && (
167
+ <TouchableOpacity
168
+ onPress={handleNext}
169
+ disabled={!canGoNext}
170
+ activeOpacity={0.7}
171
+ touchSoundDisabled={true}
172
+ accessibilityRole="button"
173
+ accessibilityLabel="Next page"
174
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
175
+ style={[styles.controlBtn, !canGoNext && styles.controlBtnDisabled]}
176
+ >
177
+ {renderIcon('chevron-right', s(18), canGoNext ? colors.foreground : colors.foregroundMuted)}
178
+ </TouchableOpacity>
179
+ )}
180
+ </View>
181
+ )
182
+ }
183
+
184
+ const styles = StyleSheet.create({
185
+ container: {
186
+ flexDirection: 'row',
187
+ alignItems: 'center',
188
+ justifyContent: 'center',
189
+ },
190
+ dotsRow: {
191
+ flexDirection: 'row',
192
+ alignItems: 'center',
193
+ },
194
+ controlBtn: {
195
+ padding: s(4),
196
+ },
197
+ controlBtnDisabled: {
198
+ opacity: 0.3,
199
+ },
200
+ })
@@ -0,0 +1 @@
1
+ export * from './PagerDots'
@@ -1,11 +1,10 @@
1
1
  import React from 'react'
2
- import { TouchableOpacity, Platform, ViewStyle, TouchableOpacityProps } from 'react-native'
3
- import Animated from 'react-native-reanimated'
2
+ import { ViewStyle } from 'react-native'
4
3
  import { impactLight } from '../../utils/haptics'
5
- import { usePressScale } from '../../utils/usePressScale'
6
- import { PRESS_SCALE, SPRINGS } from '../../utils/animations'
4
+ import { PressableCard } from '../../utils/pressable'
5
+ import { PRESS_SCALE } from '../../utils/animations'
7
6
 
8
- export interface PressableProps extends Omit<TouchableOpacityProps, 'activeOpacity'> {
7
+ export interface PressableProps {
9
8
  /** Children content to render inside the pressable. */
10
9
  children: React.ReactNode
11
10
  /** Called when pressed. */
@@ -19,7 +18,7 @@ export interface PressableProps extends Omit<TouchableOpacityProps, 'activeOpaci
19
18
  bounciness?: number
20
19
  /** Enable haptic feedback on press. Defaults to `true`. */
21
20
  haptics?: boolean
22
- /** Additional style for the Animated wrapper. */
21
+ /** Additional style for the wrapper. */
23
22
  style?: ViewStyle
24
23
  /** Disable interaction. */
25
24
  disabled?: boolean
@@ -29,7 +28,7 @@ export interface PressableProps extends Omit<TouchableOpacityProps, 'activeOpaci
29
28
 
30
29
  /**
31
30
  * Generic pressable with a calibrated spring bounce — Apple HIG / Airbnb feel.
32
- * All animation runs on the UI thread via Reanimated v4 worklets.
31
+ * All animation runs on the UI thread via pressto (Reanimated v4 worklets).
33
32
  *
34
33
  * Use this for any custom pressable surface that needs consistent press feel
35
34
  * (cards, list rows, image tiles, etc).
@@ -37,21 +36,12 @@ export interface PressableProps extends Omit<TouchableOpacityProps, 'activeOpaci
37
36
  export function Pressable({
38
37
  children,
39
38
  onPress,
40
- pressScale = PRESS_SCALE.card,
39
+ pressScale: _pressScale = PRESS_SCALE.card,
41
40
  haptics = true,
42
41
  style,
43
42
  disabled,
44
- hoverScale = 1.02,
45
- ...touchableProps
43
+ hoverScale: _hoverScale = 1.02,
46
44
  }: PressableProps) {
47
- const { animatedStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
48
- pressScale,
49
- hoverScale,
50
- pressInSpring: SPRINGS.surfacePressIn,
51
- pressOutSpring: SPRINGS.surfacePressOut,
52
- disabled,
53
- })
54
-
55
45
  const handlePress = () => {
56
46
  if (disabled || !onPress) return
57
47
  if (haptics) impactLight()
@@ -59,23 +49,17 @@ export function Pressable({
59
49
  }
60
50
 
61
51
  return (
62
- <Animated.View
63
- style={[animatedStyle, style]}
64
- {...(Platform.OS === 'web' ? hoverHandlers : {})}
52
+ <PressableCard
53
+ style={style}
54
+ onPress={handlePress}
55
+ enabled={!disabled}
56
+ rippleColor="transparent"
57
+ touchSoundDisabled
58
+ activateOnHover
59
+ accessibilityRole="button"
60
+ accessibilityState={{ disabled: !!disabled }}
65
61
  >
66
- <TouchableOpacity
67
- onPress={handlePress}
68
- onPressIn={onPressIn}
69
- onPressOut={onPressOut}
70
- activeOpacity={1}
71
- disabled={disabled}
72
- touchSoundDisabled={true}
73
- accessibilityRole="button"
74
- accessibilityState={{ disabled: !!disabled }}
75
- {...touchableProps}
76
- >
77
- {children}
78
- </TouchableOpacity>
79
- </Animated.View>
62
+ {children}
63
+ </PressableCard>
80
64
  )
81
65
  }
@@ -0,0 +1,220 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+ import { Button } from '../Button'
5
+ import { Badge } from '../Badge'
6
+ import { renderIcon } from '../../utils/icons'
7
+ import { s, vs, ms, mvs } from '../../utils/scaling'
8
+ import { RADIUS, SHADOWS } from '../../tokens'
9
+
10
+ export interface PricingFeature {
11
+ label: string
12
+ /** Whether the feature is included in this plan. Excluded features render dimmed with a dash. Defaults to true. */
13
+ included?: boolean
14
+ }
15
+
16
+ export interface PricingCardProps {
17
+ /** Plan name, e.g. "Pro". */
18
+ name: string
19
+ /** Formatted price, e.g. "$9" or "Free". */
20
+ price: string
21
+ /** Billing period suffix, e.g. "/mo". */
22
+ period?: string
23
+ /** Short description under the price. */
24
+ description?: string
25
+ /** Feature list. Strings are treated as included; objects allow `included: false`. */
26
+ features?: (string | PricingFeature)[]
27
+ /** CTA button label. */
28
+ ctaLabel?: string
29
+ onCtaPress?: () => void
30
+ /** Promotional badge, e.g. "Founders" or "Most popular". */
31
+ badge?: string
32
+ /** Emphasize this plan — primary border + elevation. Defaults to false. */
33
+ highlighted?: boolean
34
+ /** Small print under the CTA. */
35
+ footnote?: string
36
+ style?: ViewStyle
37
+ }
38
+
39
+ const normalize = (f: string | PricingFeature): PricingFeature =>
40
+ typeof f === 'string' ? { label: f, included: true } : { included: true, ...f }
41
+
42
+ /**
43
+ * Pricing / plan card for paywalls and freemium tiers — price header, feature
44
+ * list with check marks, optional promo badge, and a CTA. Set `highlighted` on
45
+ * the recommended plan.
46
+ *
47
+ * @example
48
+ * <PricingCard
49
+ * name="Pro"
50
+ * price="$9"
51
+ * period="/mo"
52
+ * badge="Most popular"
53
+ * highlighted
54
+ * features={['Unlimited docs', 'Priority support', { label: 'SSO', included: false }]}
55
+ * ctaLabel="Start trial"
56
+ * onCtaPress={subscribe}
57
+ * />
58
+ */
59
+ export function PricingCard({
60
+ name,
61
+ price,
62
+ period,
63
+ description,
64
+ features = [],
65
+ ctaLabel,
66
+ onCtaPress,
67
+ badge,
68
+ highlighted = false,
69
+ footnote,
70
+ style,
71
+ }: PricingCardProps) {
72
+ const { colors } = useTheme()
73
+
74
+ return (
75
+ <View
76
+ style={[
77
+ styles.card,
78
+ {
79
+ backgroundColor: colors.card,
80
+ borderColor: highlighted ? colors.primary : colors.border,
81
+ borderWidth: highlighted ? 2 : StyleSheet.hairlineWidth,
82
+ },
83
+ highlighted && SHADOWS.md,
84
+ style,
85
+ ]}
86
+ accessibilityRole="summary"
87
+ >
88
+ <View style={styles.header}>
89
+ <Text style={[styles.name, { color: colors.foreground }]} allowFontScaling={true}>
90
+ {name}
91
+ </Text>
92
+ {badge ? <Badge label={badge} variant={highlighted ? 'default' : 'secondary'} size="sm" /> : null}
93
+ </View>
94
+
95
+ <View style={styles.priceRow}>
96
+ <Text style={[styles.price, { color: colors.foreground }]} allowFontScaling={true}>
97
+ {price}
98
+ </Text>
99
+ {period ? (
100
+ <Text style={[styles.period, { color: colors.foregroundMuted }]} allowFontScaling={true}>
101
+ {period}
102
+ </Text>
103
+ ) : null}
104
+ </View>
105
+
106
+ {description ? (
107
+ <Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>
108
+ {description}
109
+ </Text>
110
+ ) : null}
111
+
112
+ {features.length > 0 ? (
113
+ <View style={styles.features}>
114
+ {features.map(normalize).map((f, i) => (
115
+ <View key={i} style={styles.featureRow}>
116
+ {renderIcon(
117
+ f.included ? 'check' : 'minus',
118
+ ms(16),
119
+ f.included ? colors.success : colors.foregroundMuted,
120
+ )}
121
+ <Text
122
+ style={[
123
+ styles.featureLabel,
124
+ { color: f.included ? colors.foreground : colors.foregroundMuted },
125
+ !f.included && styles.featureExcluded,
126
+ ]}
127
+ allowFontScaling={true}
128
+ >
129
+ {f.label}
130
+ </Text>
131
+ </View>
132
+ ))}
133
+ </View>
134
+ ) : null}
135
+
136
+ {ctaLabel ? (
137
+ <Button
138
+ label={ctaLabel}
139
+ variant={highlighted ? 'primary' : 'secondary'}
140
+ fullWidth
141
+ onPress={onCtaPress}
142
+ style={styles.cta}
143
+ />
144
+ ) : null}
145
+
146
+ {footnote ? (
147
+ <Text style={[styles.footnote, { color: colors.foregroundMuted }]} allowFontScaling={true}>
148
+ {footnote}
149
+ </Text>
150
+ ) : null}
151
+ </View>
152
+ )
153
+ }
154
+
155
+ const styles = StyleSheet.create({
156
+ card: {
157
+ borderRadius: RADIUS.md,
158
+ padding: s(16),
159
+ gap: vs(8),
160
+ },
161
+ header: {
162
+ flexDirection: 'row',
163
+ alignItems: 'center',
164
+ justifyContent: 'space-between',
165
+ gap: s(8),
166
+ },
167
+ name: {
168
+ fontFamily: 'Sohne-SemiBold',
169
+ fontSize: ms(16),
170
+ lineHeight: mvs(20),
171
+ },
172
+ priceRow: {
173
+ flexDirection: 'row',
174
+ alignItems: 'baseline',
175
+ gap: s(3),
176
+ },
177
+ price: {
178
+ fontFamily: 'Sohne-Bold',
179
+ fontSize: ms(28),
180
+ lineHeight: mvs(32),
181
+ letterSpacing: -0.5,
182
+ },
183
+ period: {
184
+ fontFamily: 'Sohne-Regular',
185
+ fontSize: ms(13),
186
+ lineHeight: mvs(16),
187
+ },
188
+ description: {
189
+ fontFamily: 'Sohne-Regular',
190
+ fontSize: ms(13),
191
+ lineHeight: mvs(18),
192
+ },
193
+ features: {
194
+ gap: vs(6),
195
+ marginTop: vs(2),
196
+ },
197
+ featureRow: {
198
+ flexDirection: 'row',
199
+ alignItems: 'center',
200
+ gap: s(6),
201
+ },
202
+ featureLabel: {
203
+ flex: 1,
204
+ fontFamily: 'Sohne-Regular',
205
+ fontSize: ms(13),
206
+ lineHeight: mvs(18),
207
+ },
208
+ featureExcluded: {
209
+ textDecorationLine: 'line-through',
210
+ },
211
+ cta: {
212
+ marginTop: vs(2),
213
+ },
214
+ footnote: {
215
+ fontFamily: 'Sohne-Regular',
216
+ fontSize: ms(11),
217
+ lineHeight: mvs(14),
218
+ textAlign: 'center',
219
+ },
220
+ })
@@ -0,0 +1 @@
1
+ export * from './PricingCard'
@@ -1,18 +1,12 @@
1
1
  import React from 'react'
2
2
  import { TouchableOpacity, View, Text, StyleSheet, ViewStyle } from 'react-native'
3
- import Animated, {
4
- useAnimatedStyle,
5
- useSharedValue,
6
- withSpring,
7
- interpolateColor,
8
- } from 'react-native-reanimated'
9
- import { useEffect } from 'react'
3
+ import Animated from 'react-native-reanimated'
4
+ import { EaseView } from 'react-native-ease'
10
5
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
11
6
  import { useTheme } from '../../theme'
12
7
  import { s, vs, ms, mvs } from '../../utils/scaling'
13
8
  import { usePressScale } from '../../utils/usePressScale'
14
- import { useColorTransition } from '../../utils/useColorTransition'
15
- import { SPRINGS, PRESS_SCALE } from '../../utils/animations'
9
+ import { COLOR_TRANSITION, SPRING_ELASTIC, PRESS_SCALE } from '../../utils/animations'
16
10
 
17
11
  export interface RadioOption {
18
12
  label: string
@@ -43,21 +37,6 @@ function RadioItem({
43
37
  pressScale: PRESS_SCALE.button,
44
38
  disabled: option.disabled,
45
39
  })
46
- const colorProgress = useColorTransition(selected)
47
-
48
- const dotScale = useSharedValue(selected ? 1 : 0)
49
- useEffect(() => {
50
- dotScale.value = withSpring(selected ? 1 : 0, SPRINGS.elastic)
51
- }, [selected, dotScale])
52
-
53
- const radioStyle = useAnimatedStyle(() => ({
54
- borderColor: interpolateColor(colorProgress.value, [0, 1], [colors.border, colors.primary]),
55
- }))
56
-
57
- const dotStyle = useAnimatedStyle(() => ({
58
- transform: [{ scale: dotScale.value }],
59
- opacity: dotScale.value,
60
- }))
61
40
 
62
41
  return (
63
42
  // AUDIT FIX: opacity was applied only to the radio circle, leaving the label
@@ -81,9 +60,17 @@ function RadioItem({
81
60
  accessibilityState={{ checked: selected, disabled: !!option.disabled }}
82
61
  >
83
62
  <Animated.View style={scaleStyle}>
84
- <Animated.View style={[styles.radio, radioStyle]}>
85
- <Animated.View style={[styles.dot, { backgroundColor: colors.primary }, dotStyle]} />
86
- </Animated.View>
63
+ <EaseView
64
+ style={styles.radio}
65
+ animate={{ borderColor: selected ? colors.primary : colors.border }}
66
+ transition={COLOR_TRANSITION}
67
+ >
68
+ <EaseView
69
+ style={[styles.dot, { backgroundColor: colors.primary }]}
70
+ animate={{ scale: selected ? 1 : 0, opacity: selected ? 1 : 0 }}
71
+ transition={SPRING_ELASTIC}
72
+ />
73
+ </EaseView>
87
74
  </Animated.View>
88
75
  <Text
89
76
  style={[styles.label, { color: colors.foreground }]}
@@ -0,0 +1,59 @@
1
+ import React from 'react'
2
+ import { StyleSheet } from 'react-native'
3
+ import { SafeAreaProvider, initialWindowMetrics } from 'react-native-safe-area-context'
4
+ import { GestureHandlerRootView } from 'react-native-gesture-handler'
5
+ import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'
6
+ import { ThemeProvider } from '../../theme'
7
+ import type { Theme, ColorScheme } from '../../theme'
8
+ import { ToastProvider } from '../Toast'
9
+
10
+ export interface RetrayProviderProps {
11
+ children: React.ReactNode
12
+ /** Optional per-scheme token overrides — forwarded to `ThemeProvider`. */
13
+ theme?: Theme
14
+ /**
15
+ * - `'system'` (default): auto-detects device setting.
16
+ * - `'light'` / `'dark'`: forces a specific scheme.
17
+ */
18
+ colorScheme?: ColorScheme
19
+ }
20
+
21
+ /**
22
+ * All-in-one provider that wires every provider the UI kit needs, in the one
23
+ * correct order:
24
+ *
25
+ * `SafeAreaProvider` → `GestureHandlerRootView` → `ThemeProvider` →
26
+ * `BottomSheetModalProvider` → `ToastProvider`
27
+ *
28
+ * Use this at your app root instead of nesting the five providers by hand — it
29
+ * removes an entire class of provider-order bugs. The individual providers stay
30
+ * exported for consumers who need a custom tree.
31
+ *
32
+ * @example
33
+ * import { RetrayProvider } from '@retray-dev/ui-kit'
34
+ *
35
+ * export default function App() {
36
+ * return (
37
+ * <RetrayProvider colorScheme="system">
38
+ * <RootNavigator />
39
+ * </RetrayProvider>
40
+ * )
41
+ * }
42
+ */
43
+ export function RetrayProvider({ children, theme, colorScheme = 'system' }: RetrayProviderProps) {
44
+ return (
45
+ <SafeAreaProvider initialMetrics={initialWindowMetrics}>
46
+ <GestureHandlerRootView style={styles.root}>
47
+ <ThemeProvider theme={theme} colorScheme={colorScheme}>
48
+ <BottomSheetModalProvider>
49
+ <ToastProvider>{children}</ToastProvider>
50
+ </BottomSheetModalProvider>
51
+ </ThemeProvider>
52
+ </GestureHandlerRootView>
53
+ </SafeAreaProvider>
54
+ )
55
+ }
56
+
57
+ const styles = StyleSheet.create({
58
+ root: { flex: 1 },
59
+ })
@@ -0,0 +1 @@
1
+ export * from './RetrayProvider'