@retray-dev/ui-kit 7.0.1 → 9.1.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.
- package/COMPONENTS.md +567 -14
- package/EXAMPLES.md +21 -14
- package/README.md +14 -8
- package/dist/Accordion.js +57 -5
- package/dist/Accordion.mjs +4 -3
- package/dist/AlertBanner.js +4 -1
- package/dist/AlertBanner.mjs +3 -2
- package/dist/AppHeader.d.mts +40 -0
- package/dist/AppHeader.d.ts +40 -0
- package/dist/AppHeader.js +515 -0
- package/dist/AppHeader.mjs +10 -0
- package/dist/Avatar.js +39 -29
- package/dist/Avatar.mjs +2 -1
- package/dist/Badge.js +11 -1
- package/dist/Badge.mjs +2 -1
- package/dist/Button.d.mts +8 -3
- package/dist/Button.d.ts +8 -3
- package/dist/Button.js +126 -108
- package/dist/Button.mjs +6 -5
- package/dist/ButtonGroup.mjs +1 -0
- package/dist/Card.js +90 -70
- package/dist/Card.mjs +5 -4
- package/dist/CategoryStrip.js +79 -22
- package/dist/CategoryStrip.mjs +6 -6
- package/dist/Checkbox.js +118 -86
- package/dist/Checkbox.mjs +5 -5
- package/dist/Chip.js +113 -80
- package/dist/Chip.mjs +5 -5
- package/dist/ConfirmDialog.js +140 -110
- package/dist/ConfirmDialog.mjs +7 -6
- package/dist/CurrencyDisplay.mjs +1 -0
- package/dist/CurrencyInput.d.mts +1 -1
- package/dist/CurrencyInput.d.ts +1 -1
- package/dist/CurrencyInput.js +9 -5
- package/dist/CurrencyInput.mjs +5 -4
- package/dist/DetailRow.mjs +1 -0
- package/dist/EmptyState.js +131 -111
- package/dist/EmptyState.mjs +7 -6
- package/dist/ErrorBoundary.d.mts +42 -0
- package/dist/ErrorBoundary.d.ts +42 -0
- package/dist/ErrorBoundary.js +351 -0
- package/dist/ErrorBoundary.mjs +7 -0
- package/dist/Form.mjs +1 -0
- package/dist/HolographicCard.d.mts +55 -0
- package/dist/HolographicCard.d.ts +55 -0
- package/dist/HolographicCard.js +316 -0
- package/dist/HolographicCard.mjs +191 -0
- package/dist/IconButton.d.mts +8 -3
- package/dist/IconButton.d.ts +8 -3
- package/dist/IconButton.js +115 -98
- package/dist/IconButton.mjs +5 -4
- package/dist/ImageViewer.d.mts +23 -0
- package/dist/ImageViewer.d.ts +23 -0
- package/dist/ImageViewer.js +582 -0
- package/dist/ImageViewer.mjs +8 -0
- package/dist/Input.mjs +4 -3
- package/dist/LabelValue.mjs +1 -0
- package/dist/ListGroup.mjs +1 -0
- package/dist/ListItem.js +131 -117
- package/dist/ListItem.mjs +6 -5
- package/dist/MediaCard.js +54 -6
- package/dist/MediaCard.mjs +6 -5
- package/dist/MenuGroup.mjs +1 -0
- package/dist/MenuItem.js +91 -79
- package/dist/MenuItem.mjs +6 -5
- package/dist/MonthPicker.d.mts +10 -2
- package/dist/MonthPicker.d.ts +10 -2
- package/dist/MonthPicker.js +80 -17
- package/dist/MonthPicker.mjs +3 -2
- package/dist/PagerDots.d.mts +35 -0
- package/dist/PagerDots.d.ts +35 -0
- package/dist/PagerDots.js +392 -0
- package/dist/PagerDots.mjs +7 -0
- package/dist/Pressable.d.mts +5 -5
- package/dist/Pressable.d.ts +5 -5
- package/dist/Pressable.js +97 -86
- package/dist/Pressable.mjs +5 -4
- package/dist/PricingCard.d.mts +50 -0
- package/dist/PricingCard.d.ts +50 -0
- package/dist/PricingCard.js +636 -0
- package/dist/PricingCard.mjs +11 -0
- package/dist/Progress.mjs +3 -2
- package/dist/RadioGroup.js +81 -30
- package/dist/RadioGroup.mjs +5 -5
- package/dist/RetrayProvider.d.mts +2 -0
- package/dist/RetrayProvider.d.ts +2 -0
- package/dist/RetrayProvider.js +214 -0
- package/dist/RetrayProvider.mjs +5 -0
- package/dist/Select.js +51 -4
- package/dist/Select.mjs +5 -4
- package/dist/SelectableGrid.d.mts +44 -0
- package/dist/SelectableGrid.d.ts +44 -0
- package/dist/SelectableGrid.js +448 -0
- package/dist/SelectableGrid.mjs +9 -0
- package/dist/Separator.mjs +1 -0
- package/dist/Sheet.d.mts +13 -1
- package/dist/Sheet.d.ts +13 -1
- package/dist/Sheet.js +115 -5
- package/dist/Sheet.mjs +4 -2
- package/dist/Skeleton.d.mts +50 -0
- package/dist/Skeleton.d.ts +50 -0
- package/dist/Skeleton.js +61 -0
- package/dist/Skeleton.mjs +4 -2
- package/dist/Slider.js +51 -4
- package/dist/Slider.mjs +3 -2
- package/dist/Spinner.js +28 -7
- package/dist/Spinner.mjs +2 -1
- package/dist/Switch.js +98 -48
- package/dist/Switch.mjs +4 -3
- package/dist/TabBar.d.mts +42 -0
- package/dist/TabBar.d.ts +42 -0
- package/dist/TabBar.js +361 -0
- package/dist/TabBar.mjs +6 -0
- package/dist/Tabs.js +92 -62
- package/dist/Tabs.mjs +5 -4
- package/dist/Text.js +16 -0
- package/dist/Text.mjs +2 -1
- package/dist/Textarea.mjs +4 -3
- package/dist/Toast.d.mts +7 -7
- package/dist/Toast.d.ts +7 -7
- package/dist/Toast.mjs +1 -0
- package/dist/Toggle.d.mts +6 -3
- package/dist/Toggle.d.ts +6 -3
- package/dist/Toggle.js +135 -120
- package/dist/Toggle.mjs +5 -5
- package/dist/VirtualList.mjs +1 -0
- package/dist/{chunk-7H2OR44A.mjs → chunk-26BCI223.mjs} +1 -1
- package/dist/{chunk-CRYBX2CM.mjs → chunk-2TFTAWVJ.mjs} +44 -59
- package/dist/chunk-3DKJ2GIC.mjs +30 -0
- package/dist/{chunk-KWCPOM6W.mjs → chunk-3U4SSNWP.mjs} +32 -48
- package/dist/chunk-4I7D47FH.mjs +139 -0
- package/dist/chunk-4K625MVM.mjs +142 -0
- package/dist/{chunk-MN7OG7IY.mjs → chunk-6OAZJ577.mjs} +6 -4
- package/dist/{chunk-L7E7TVEZ.mjs → chunk-756RAKE4.mjs} +2 -2
- package/dist/{chunk-HSPSMN6U.mjs → chunk-7QHVVCB3.mjs} +2 -2
- package/dist/{chunk-URLL5JBR.mjs → chunk-A3A6KNQN.mjs} +3 -3
- package/dist/chunk-AJ7ZDNBT.mjs +120 -0
- package/dist/{chunk-FTLJOUOQ.mjs → chunk-AV4EMIRH.mjs} +25 -28
- package/dist/chunk-AZJF2BLK.mjs +115 -0
- package/dist/chunk-BNP626TY.mjs +159 -0
- package/dist/{chunk-5IKW3VNC.mjs → chunk-DVK4G2GT.mjs} +17 -1
- package/dist/{chunk-6LQYY7HC.mjs → chunk-EH745HE5.mjs} +2 -2
- package/dist/chunk-EJ7ZPXOH.mjs +163 -0
- package/dist/{chunk-RKLHUDZS.mjs → chunk-GD6KXMG5.mjs} +29 -15
- package/dist/{chunk-RR2VQLKE.mjs → chunk-GQYFLP3D.mjs} +14 -17
- package/dist/{chunk-Y6MXOREN.mjs → chunk-ID72TK46.mjs} +8 -17
- package/dist/{chunk-NQGVLMWG.mjs → chunk-JMOZEC77.mjs} +1 -1
- package/dist/{chunk-GCWOGZYL.mjs → chunk-JT7HKXRB.mjs} +39 -29
- package/dist/{chunk-LWG526VX.mjs → chunk-KIHCWCWL.mjs} +47 -62
- package/dist/chunk-LXJIIOYQ.mjs +104 -0
- package/dist/{chunk-SBZYEV4S.mjs → chunk-M6ZXVBTK.mjs} +5 -2
- package/dist/{chunk-XDMN67KV.mjs → chunk-MAC465BB.mjs} +10 -8
- package/dist/chunk-MBMXYJJV.mjs +36 -0
- package/dist/chunk-MLF3EZFW.mjs +119 -0
- package/dist/chunk-NA7PARID.mjs +147 -0
- package/dist/{chunk-QXGYKWI7.mjs → chunk-O3HA6TYM.mjs} +9 -4
- package/dist/{chunk-63357L2X.mjs → chunk-OB4JUQ3O.mjs} +1 -1
- package/dist/{chunk-AU2VDY4P.mjs → chunk-PFZTM6D5.mjs} +52 -4
- package/dist/chunk-QKH5ZOD5.mjs +97 -0
- package/dist/{chunk-KZJRQOIU.mjs → chunk-TERDKCLE.mjs} +11 -1
- package/dist/{chunk-U4N7WF4Z.mjs → chunk-UREA2GYY.mjs} +28 -23
- package/dist/{chunk-TAJ2PQ2O.mjs → chunk-VGTDN7SW.mjs} +7 -6
- package/dist/{chunk-URDE3EUU.mjs → chunk-VQ57HWPL.mjs} +27 -15
- package/dist/chunk-WBOOUHSS.mjs +62 -0
- package/dist/{chunk-GNGLDL6Z.mjs → chunk-WJLKJMKR.mjs} +18 -0
- package/dist/{chunk-YZJAFS4P.mjs → chunk-X4G6APW6.mjs} +22 -19
- package/dist/chunk-Y6FXYEAI.mjs +8 -0
- package/dist/chunk-YFZ3ELX5.mjs +16 -0
- package/dist/{chunk-QCNARS3X.mjs → chunk-YNROWHQJ.mjs} +1 -1
- package/dist/chunk-Z4BVUWW6.mjs +196 -0
- package/dist/{chunk-GPOUINK5.mjs → chunk-ZJKGQMYH.mjs} +10 -27
- package/dist/index-wt-orHUi.d.mts +85 -0
- package/dist/index-wt-orHUi.d.ts +85 -0
- package/dist/index.d.mts +59 -51
- package/dist/index.d.ts +59 -51
- package/dist/index.js +1940 -744
- package/dist/index.mjs +49 -39
- package/package.json +35 -5
- package/src/components/Accordion/Accordion.tsx +12 -1
- package/src/components/AlertBanner/AlertBanner.tsx +5 -0
- package/src/components/AppHeader/AppHeader.tsx +172 -0
- package/src/components/AppHeader/index.ts +1 -0
- package/src/components/Avatar/Avatar.tsx +10 -2
- package/src/components/Badge/Badge.tsx +8 -1
- package/src/components/Button/Button.tsx +20 -27
- package/src/components/Card/Card.tsx +12 -23
- package/src/components/CategoryStrip/CategoryStrip.tsx +17 -21
- package/src/components/Checkbox/Checkbox.tsx +26 -40
- package/src/components/Chip/Chip.tsx +24 -33
- package/src/components/CurrencyInput/CurrencyInput.tsx +10 -8
- package/src/components/EmptyState/EmptyState.tsx +2 -1
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +153 -0
- package/src/components/ErrorBoundary/index.ts +1 -0
- package/src/components/HolographicCard/HolographicCard.tsx +315 -0
- package/src/components/HolographicCard/index.ts +1 -0
- package/src/components/IconButton/IconButton.tsx +19 -27
- package/src/components/ImageViewer/ImageViewer.tsx +290 -0
- package/src/components/ImageViewer/index.ts +1 -0
- package/src/components/ListItem/ListItem.tsx +70 -67
- package/src/components/MediaCard/MediaCard.tsx +8 -2
- package/src/components/MenuItem/MenuItem.tsx +10 -25
- package/src/components/MonthPicker/MonthPicker.tsx +39 -13
- package/src/components/MonthPicker/index.ts +1 -1
- package/src/components/PagerDots/PagerDots.tsx +200 -0
- package/src/components/PagerDots/index.ts +1 -0
- package/src/components/Pressable/Pressable.tsx +19 -35
- package/src/components/PricingCard/PricingCard.tsx +220 -0
- package/src/components/PricingCard/index.ts +1 -0
- package/src/components/RadioGroup/RadioGroup.tsx +14 -27
- package/src/components/RetrayProvider/RetrayProvider.tsx +59 -0
- package/src/components/RetrayProvider/index.ts +1 -0
- package/src/components/SelectableGrid/SelectableGrid.tsx +205 -0
- package/src/components/SelectableGrid/index.ts +1 -0
- package/src/components/Sheet/Sheet.tsx +65 -1
- package/src/components/Skeleton/Skeleton.tsx +142 -1
- package/src/components/Spinner/Spinner.tsx +17 -2
- package/src/components/Switch/Switch.tsx +30 -58
- package/src/components/TabBar/TabBar.tsx +169 -0
- package/src/components/TabBar/index.ts +1 -0
- package/src/components/Tabs/Tabs.tsx +23 -26
- package/src/components/Text/Text.tsx +2 -0
- package/src/components/Toggle/Toggle.tsx +35 -51
- package/src/fonts.ts +4 -1
- package/src/index.ts +23 -2
- package/src/utils/animations.ts +29 -1
- package/src/utils/fontGuard.ts +34 -0
- package/src/utils/haptics.ts +211 -9
- package/src/utils/pressable.ts +66 -0
- package/dist/chunk-76PFOSM2.mjs +0 -41
- package/dist/chunk-DITNP6PL.mjs +0 -106
- package/dist/chunk-JBLL7U3U.mjs +0 -64
- package/dist/chunk-LG4DO3DK.mjs +0 -174
- package/dist/chunk-RMMK64W5.mjs +0 -54
- 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 {
|
|
3
|
-
import Animated from 'react-native-reanimated'
|
|
2
|
+
import { ViewStyle } from 'react-native'
|
|
4
3
|
import { impactLight } from '../../utils/haptics'
|
|
5
|
-
import {
|
|
6
|
-
import { PRESS_SCALE
|
|
4
|
+
import { PressableCard } from '../../utils/pressable'
|
|
5
|
+
import { PRESS_SCALE } from '../../utils/animations'
|
|
7
6
|
|
|
8
|
-
export interface PressableProps
|
|
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
|
|
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
|
-
<
|
|
63
|
-
style={
|
|
64
|
-
{
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
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'
|