@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.
Files changed (234) hide show
  1. package/COMPONENTS.md +567 -14
  2. package/EXAMPLES.md +21 -14
  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,169 @@
1
+ import React from 'react'
2
+ import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native'
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
4
+ import { useTheme } from '../../theme'
5
+ import { renderIcon } from '../../utils/icons'
6
+ import { selectionAsync as hapticSelection } from '../../utils/haptics'
7
+ import { s, vs, ms, mvs } from '../../utils/scaling'
8
+
9
+ export interface TabBarItem {
10
+ /** Unique key for the tab. */
11
+ key: string
12
+ /** Label under the icon. Omit for an icon-only tab. */
13
+ label?: string
14
+ /** Icon name (active + inactive share the name; color signals state). */
15
+ iconName?: string
16
+ /** Custom icon node — receives no state, overrides `iconName`. */
17
+ icon?: React.ReactNode
18
+ /** Badge overlay — `true` shows a dot, a number shows a count (capped at 99). */
19
+ badge?: boolean | number
20
+ }
21
+
22
+ export interface TabBarProps {
23
+ items: TabBarItem[]
24
+ /** Key of the active tab. */
25
+ activeKey: string
26
+ onTabPress: (key: string) => void
27
+ /** Active tint. Defaults to theme `primary`. */
28
+ activeColor?: string
29
+ /** Inactive tint. Defaults to theme `foregroundMuted`. */
30
+ inactiveColor?: string
31
+ /** Apply the bottom safe-area inset as padding. Defaults to true. */
32
+ withSafeArea?: boolean
33
+ style?: ViewStyle
34
+ }
35
+
36
+ /**
37
+ * Bottom tab bar — icon + label tabs with active tint and badge support. Pair
38
+ * with your navigator, or drive it with local state for a single-screen app.
39
+ *
40
+ * @example
41
+ * <TabBar
42
+ * items={[{ key: 'home', label: 'Home', iconName: 'home' }, { key: 'profile', label: 'Profile', iconName: 'user', badge: 3 }]}
43
+ * activeKey={tab}
44
+ * onTabPress={setTab}
45
+ * />
46
+ */
47
+ export function TabBar({
48
+ items,
49
+ activeKey,
50
+ onTabPress,
51
+ activeColor,
52
+ inactiveColor,
53
+ withSafeArea = true,
54
+ style,
55
+ }: TabBarProps) {
56
+ const { colors } = useTheme()
57
+ const insets = useSafeAreaInsets()
58
+ const resolvedActive = activeColor ?? colors.primary
59
+ const resolvedInactive = inactiveColor ?? colors.foregroundMuted
60
+
61
+ return (
62
+ <View
63
+ style={[
64
+ styles.container,
65
+ {
66
+ backgroundColor: colors.card,
67
+ borderTopColor: colors.border,
68
+ paddingBottom: withSafeArea ? insets.bottom : 0,
69
+ },
70
+ style,
71
+ ]}
72
+ accessibilityRole="tablist"
73
+ >
74
+ {items.map((item) => {
75
+ const active = item.key === activeKey
76
+ const tint = active ? resolvedActive : resolvedInactive
77
+ const iconNode = item.icon ?? (item.iconName ? renderIcon(item.iconName, ms(24), tint) : null)
78
+ const showBadge = item.badge !== undefined && item.badge !== false
79
+ const badgeCount = typeof item.badge === 'number' ? item.badge : undefined
80
+
81
+ return (
82
+ <TouchableOpacity
83
+ key={item.key}
84
+ style={styles.tab}
85
+ onPress={() => {
86
+ if (!active) hapticSelection()
87
+ onTabPress(item.key)
88
+ }}
89
+ activeOpacity={0.7}
90
+ touchSoundDisabled={true}
91
+ accessibilityRole="tab"
92
+ accessibilityState={{ selected: active }}
93
+ accessibilityLabel={item.label ?? item.key}
94
+ >
95
+ <View>
96
+ {iconNode}
97
+ {showBadge ? (
98
+ <View
99
+ style={[
100
+ styles.badge,
101
+ { backgroundColor: colors.destructive, borderColor: colors.card },
102
+ badgeCount === undefined && styles.badgeDot,
103
+ ]}
104
+ >
105
+ {badgeCount !== undefined ? (
106
+ <Text style={[styles.badgeText, { color: colors.destructiveForeground }]} allowFontScaling={false}>
107
+ {badgeCount > 99 ? '99+' : badgeCount}
108
+ </Text>
109
+ ) : null}
110
+ </View>
111
+ ) : null}
112
+ </View>
113
+ {item.label ? (
114
+ <Text style={[styles.label, { color: tint }]} numberOfLines={1} allowFontScaling={true}>
115
+ {item.label}
116
+ </Text>
117
+ ) : null}
118
+ </TouchableOpacity>
119
+ )
120
+ })}
121
+ </View>
122
+ )
123
+ }
124
+
125
+ const styles = StyleSheet.create({
126
+ container: {
127
+ flexDirection: 'row',
128
+ borderTopWidth: StyleSheet.hairlineWidth,
129
+ },
130
+ tab: {
131
+ flex: 1,
132
+ alignItems: 'center',
133
+ justifyContent: 'center',
134
+ paddingTop: vs(8),
135
+ paddingBottom: vs(6),
136
+ gap: vs(2),
137
+ minHeight: vs(48),
138
+ },
139
+ label: {
140
+ fontFamily: 'Sohne-Medium',
141
+ fontSize: ms(11),
142
+ lineHeight: mvs(14),
143
+ },
144
+ badge: {
145
+ position: 'absolute',
146
+ top: -vs(4),
147
+ right: -s(10),
148
+ minWidth: s(16),
149
+ height: s(16),
150
+ borderRadius: s(8),
151
+ borderWidth: 1.5,
152
+ alignItems: 'center',
153
+ justifyContent: 'center',
154
+ paddingHorizontal: s(3),
155
+ },
156
+ badgeDot: {
157
+ minWidth: s(10),
158
+ height: s(10),
159
+ borderRadius: s(5),
160
+ top: -vs(2),
161
+ right: -s(6),
162
+ paddingHorizontal: 0,
163
+ },
164
+ badgeText: {
165
+ fontFamily: 'Sohne-SemiBold',
166
+ fontSize: ms(9),
167
+ lineHeight: ms(11),
168
+ },
169
+ })
@@ -0,0 +1 @@
1
+ export * from './TabBar'
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useRef, useEffect, useCallback } from 'react'
2
- import { View, TouchableOpacity, Text, StyleSheet, ViewStyle, LayoutChangeEvent } from 'react-native'
2
+ import { View, Text, StyleSheet, ViewStyle, LayoutChangeEvent } from 'react-native'
3
3
  import Animated, {
4
4
  useSharedValue,
5
5
  useAnimatedStyle,
@@ -8,8 +8,8 @@ import Animated, {
8
8
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
9
9
  import { useTheme } from '../../theme'
10
10
  import { s, vs, ms } from '../../utils/scaling'
11
- import { usePressScale } from '../../utils/usePressScale'
12
- import { SPRINGS, PRESS_SCALE } from '../../utils/animations'
11
+ import { SPRINGS } from '../../utils/animations'
12
+ import { PressableTab } from '../../utils/pressable'
13
13
 
14
14
  export interface TabItem {
15
15
  label: string
@@ -49,29 +49,23 @@ function TabTrigger({
49
49
  variant: TabsVariant
50
50
  }) {
51
51
  const { colors } = useTheme()
52
- const { animatedStyle, onPressIn, onPressOut } = usePressScale({
53
- pressScale: PRESS_SCALE.button,
54
- })
55
52
  const isUnderline = variant === 'underline'
56
53
 
57
54
  return (
58
- <TouchableOpacity
59
- style={[
60
- styles.trigger,
61
- isUnderline && styles.triggerUnderline,
62
- isUnderline && isActive && { borderBottomColor: colors.primary },
63
- ]}
64
- onPress={onPress}
65
- onPressIn={onPressIn}
66
- onPressOut={onPressOut}
67
- onLayout={onLayout}
68
- activeOpacity={1}
69
- touchSoundDisabled={true}
70
- accessibilityRole="tab"
71
- accessibilityState={{ selected: isActive }}
72
- accessibilityLabel={tab.label}
73
- >
74
- <Animated.View style={animatedStyle}>
55
+ <View onLayout={onLayout} style={styles.triggerWrap}>
56
+ <PressableTab
57
+ style={[
58
+ styles.trigger,
59
+ isUnderline && styles.triggerUnderline,
60
+ isUnderline && isActive && { borderBottomColor: colors.primary },
61
+ ]}
62
+ onPress={onPress}
63
+ rippleColor="transparent"
64
+ touchSoundDisabled
65
+ accessibilityRole="tab"
66
+ accessibilityState={{ selected: isActive }}
67
+ accessibilityLabel={tab.label}
68
+ >
75
69
  <View style={styles.triggerInner}>
76
70
  {tab.icon ? (
77
71
  typeof tab.icon === 'function' ? tab.icon(isActive) : tab.icon
@@ -90,8 +84,8 @@ function TabTrigger({
90
84
  {tab.label}
91
85
  </Text>
92
86
  </View>
93
- </Animated.View>
94
- </TouchableOpacity>
87
+ </PressableTab>
88
+ </View>
95
89
  )
96
90
  }
97
91
 
@@ -207,6 +201,9 @@ const styles = StyleSheet.create({
207
201
  borderBottomWidth: 1,
208
202
  },
209
203
  pill: {},
204
+ triggerWrap: {
205
+ flex: 1,
206
+ },
210
207
  trigger: {
211
208
  flex: 1,
212
209
  paddingVertical: vs(7),
@@ -217,7 +214,7 @@ const styles = StyleSheet.create({
217
214
  zIndex: 1,
218
215
  },
219
216
  triggerUnderline: {
220
- flex: 0,
217
+ flex: 1,
221
218
  paddingVertical: vs(12),
222
219
  paddingHorizontal: s(16),
223
220
  borderRadius: 0,
@@ -3,6 +3,7 @@ import { Text as RNText, TextProps as RNTextProps, TextStyle } from 'react-nativ
3
3
  import { useTheme } from '../../theme'
4
4
  import { TYPOGRAPHY } from '../../tokens'
5
5
  import { ms, mvs } from '../../utils/scaling'
6
+ import { warnIfFontsMissing } from '../../utils/fontGuard'
6
7
 
7
8
  export type TextVariant =
8
9
  | 'display-hero'
@@ -68,6 +69,7 @@ const defaultColorVariant: Partial<Record<TextVariant, 'foreground' | 'foregroun
68
69
  }
69
70
 
70
71
  function TextBase({ variant = 'body-md', color, style, children, ...props }: TextProps) {
72
+ warnIfFontsMissing()
71
73
  const { colors } = useTheme()
72
74
 
73
75
  const colorKey = defaultColorVariant[variant] ?? 'foreground'
@@ -1,17 +1,13 @@
1
1
  import React from 'react'
2
- import { TouchableOpacity, StyleSheet, TouchableOpacityProps, ViewStyle, View } from 'react-native'
3
- import Animated, {
4
- useAnimatedStyle,
5
- interpolateColor,
6
- } from 'react-native-reanimated'
2
+ import { StyleSheet, ViewStyle, View, Text } from 'react-native'
3
+ import { EaseView } from 'react-native-ease'
7
4
  import { FontAwesome5 } from '@expo/vector-icons'
8
5
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
9
6
  import { useTheme } from '../../theme'
10
7
  import { s, vs, ms } from '../../utils/scaling'
11
8
  import { renderIcon } from '../../utils/icons'
12
- import { usePressScale } from '../../utils/usePressScale'
13
- import { useColorTransition } from '../../utils/useColorTransition'
14
- import { PRESS_SCALE } from '../../utils/animations'
9
+ import { COLOR_TRANSITION } from '../../utils/animations'
10
+ import { PressableButton } from '../../utils/pressable'
15
11
 
16
12
  interface ToggleIconProps {
17
13
  pressed: boolean
@@ -50,7 +46,7 @@ function ToggleIcon({ pressed, iconName, activeIconName, icon, activeIcon, iconC
50
46
  export type ToggleVariant = 'default' | 'outline'
51
47
  export type ToggleSize = 'sm' | 'md' | 'lg'
52
48
 
53
- export interface ToggleProps extends TouchableOpacityProps {
49
+ export interface ToggleProps {
54
50
  pressed?: boolean
55
51
  onPressedChange?: (pressed: boolean) => void
56
52
  variant?: ToggleVariant
@@ -74,6 +70,9 @@ export interface ToggleProps extends TouchableOpacityProps {
74
70
  iconColor?: string
75
71
  /** Override the resolved active icon color. Defaults to `primary`. */
76
72
  activeIconColor?: string
73
+ disabled?: boolean
74
+ style?: ViewStyle
75
+ accessibilityLabel?: string
77
76
  }
78
77
 
79
78
  const sizeStyles: Record<ToggleSize, ViewStyle> = {
@@ -99,55 +98,37 @@ export function Toggle({
99
98
  disabled,
100
99
  style,
101
100
  accessibilityLabel,
102
- ...props
103
101
  }: ToggleProps) {
104
102
  const { colors } = useTheme()
105
- const { animatedStyle: scaleStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
106
- pressScale: PRESS_SCALE.button,
107
- disabled,
108
- })
109
- const progress = useColorTransition(pressed)
110
103
 
111
104
  const inactiveBorder = variant === 'outline' ? colors.border : 'transparent'
112
105
 
113
- const surfaceStyle = useAnimatedStyle(() => ({
114
- borderColor: interpolateColor(progress.value, [0, 1], [inactiveBorder, colors.primary]),
115
- backgroundColor: interpolateColor(progress.value, [0, 1], ['transparent', colors.surfaceStrong]),
116
- }))
117
-
118
- const textStyle = useAnimatedStyle(() => ({
119
- color: interpolateColor(progress.value, [0, 1], [colors.foreground, colors.primary]),
120
- }))
121
-
122
106
  const iconSize = iconSizeMap[size]
123
107
 
108
+ const handlePress = () => {
109
+ hapticSelection()
110
+ onPressedChange?.(!pressed)
111
+ }
112
+
124
113
  return (
125
- <Animated.View
126
- style={[scaleStyle, disabled && styles.disabled, style]}
127
- {...hoverHandlers}
128
- >
129
- <TouchableOpacity
130
- onPress={() => {
131
- hapticSelection()
132
- onPressedChange?.(!pressed)
133
- }}
134
- onPressIn={onPressIn}
135
- onPressOut={onPressOut}
136
- disabled={disabled}
137
- activeOpacity={1}
138
- touchSoundDisabled={true}
114
+ <View style={[disabled && styles.disabled, style]}>
115
+ <PressableButton
116
+ onPress={handlePress}
117
+ enabled={!disabled}
118
+ rippleColor="transparent"
119
+ touchSoundDisabled
120
+ activateOnHover
139
121
  accessibilityRole="button"
140
122
  accessibilityLabel={accessibilityLabel ?? label}
141
123
  accessibilityState={{ selected: pressed, disabled: !!disabled }}
142
- {...props}
143
124
  >
144
- <Animated.View
145
- style={[
146
- styles.base,
147
- sizeStyles[size],
148
- { borderWidth: 2 },
149
- surfaceStyle,
150
- ]}
125
+ <EaseView
126
+ style={[styles.base, sizeStyles[size], { borderWidth: 2 }]}
127
+ animate={{
128
+ borderColor: pressed ? colors.primary : inactiveBorder,
129
+ backgroundColor: pressed ? colors.surfaceStrong : 'transparent',
130
+ }}
131
+ transition={COLOR_TRANSITION}
151
132
  >
152
133
  <View style={styles.inner}>
153
134
  <ToggleIcon
@@ -163,14 +144,17 @@ export function Toggle({
163
144
  mutedColor={colors.foregroundMuted}
164
145
  />
165
146
  {label ? (
166
- <Animated.Text style={[styles.label, textStyle]} allowFontScaling={true}>
147
+ <Text
148
+ style={[styles.label, { color: pressed ? colors.primary : colors.foreground }]}
149
+ allowFontScaling={true}
150
+ >
167
151
  {label}
168
- </Animated.Text>
152
+ </Text>
169
153
  ) : null}
170
154
  </View>
171
- </Animated.View>
172
- </TouchableOpacity>
173
- </Animated.View>
155
+ </EaseView>
156
+ </PressableButton>
157
+ </View>
174
158
  )
175
159
  }
176
160
 
package/src/fonts.ts CHANGED
@@ -13,7 +13,10 @@
13
13
  * // render app
14
14
  * }
15
15
  */
16
- /* eslint-disable @typescript-eslint/no-require-imports */
16
+ // `.otf` assets resolve to a Metro asset module id (number) via require() at the
17
+ // consumer's build time. Declared locally so the dts build does not depend on @types/node.
18
+ declare const require: (path: string) => number
19
+
17
20
  export const SohneFonts = {
18
21
  // Sohne base
19
22
  'Sohne-ExtraLight': require('./assets/fonts/Sohne-ExtraLight.otf'),
package/src/index.ts CHANGED
@@ -31,8 +31,6 @@ export * from './components/Select'
31
31
  export * from './components/Toast'
32
32
  export * from './components/CurrencyInput'
33
33
  export * from './components/CurrencyDisplay'
34
- // CurrencyInputLarge is deprecated — use <CurrencyInput size="large" /> instead
35
- export { CurrencyInput as CurrencyInputLarge } from './components/CurrencyInput'
36
34
  export * from './components/ListItem'
37
35
  export * from './components/ListGroup'
38
36
  export * from './components/MenuItem'
@@ -47,6 +45,17 @@ export * from './components/Pressable'
47
45
  export * from './components/DetailRow'
48
46
  export * from './components/Form'
49
47
  export * from './components/VirtualList'
48
+ export * from './components/RetrayProvider'
49
+ export * from './components/ErrorBoundary'
50
+ export * from './components/PagerDots'
51
+ export * from './components/AppHeader'
52
+ export * from './components/SelectableGrid'
53
+ export * from './components/PricingCard'
54
+ export * from './components/TabBar'
55
+ export * from './components/ImageViewer'
56
+ // HolographicCard is intentionally NOT re-exported here — it depends on the
57
+ // optional peer @shopify/react-native-skia, so it must stay out of the main
58
+ // barrel's module graph. Deep-import it: '@retray-dev/ui-kit/HolographicCard'.
50
59
 
51
60
  // Icon utility
52
61
  export { Icon, renderIcon, configureIconFamilies } from './utils/icons'
@@ -55,6 +64,18 @@ export { Icon, renderIcon, configureIconFamilies } from './utils/icons'
55
64
  export { getResponsiveFontSize } from './utils/typography'
56
65
  export type { IconProps, IconFamily } from './utils/icons'
57
66
 
67
+ // Haptic feedback utilities
68
+ export {
69
+ selectionAsync,
70
+ impactLight,
71
+ impactMedium,
72
+ impactHeavy,
73
+ notificationSuccess,
74
+ notificationError,
75
+ notificationWarning,
76
+ richHaptics,
77
+ } from './utils/haptics'
78
+
58
79
  // Design tokens
59
80
  export {
60
81
  SPACING,
@@ -1,4 +1,5 @@
1
1
  import { Easing } from 'react-native-reanimated'
2
+ import type { SingleTransition } from 'react-native-ease'
2
3
 
3
4
  // ─── Spring presets ──────────────────────────────────────────────────────────
4
5
  // Tuned for the "Apple HIG / Airbnb" press-feel: snap inward fast, settle out elastically.
@@ -17,7 +18,7 @@ export const SPRINGS = {
17
18
  surfacePressOut: { stiffness: 220, damping: 20, mass: 0.95 },
18
19
 
19
20
  /** Settled transitions for moving indicators — Tabs pill, Switch thumb. */
20
- glide: { stiffness: 380, damping: 38, mass: 1.0 },
21
+ glide: { stiffness: 380, damping: 38, mass: 1 },
21
22
 
22
23
  /** Elastic indicator — Switch thumb, RadioGroup dot. */
23
24
  elastic: { stiffness: 320, damping: 22, mass: 0.7 },
@@ -48,6 +49,33 @@ export const EASINGS = {
48
49
  collapse: Easing.in(Easing.ease),
49
50
  } as const
50
51
 
52
+ // ─── EaseView transition presets ─────────────────────────────────────────────
53
+ // Equivalents of the reanimated presets above, in `react-native-ease` units.
54
+ // EaseView spring takes raw damping/stiffness/mass (same physical model). EaseView
55
+ // timing takes duration + an easing curve as a cubic-bezier tuple.
56
+
57
+ /** Color/border state transition for Toggle, Checkbox, Chip, CategoryStrip, Switch track. Mirrors TIMINGS.state + EASINGS.standard. */
58
+ export const COLOR_TRANSITION: SingleTransition = {
59
+ type: 'timing',
60
+ duration: TIMINGS.state.duration,
61
+ easing: [0.2, 0, 0, 1],
62
+ }
63
+
64
+ /** Icon/opacity crossfade. Mirrors TIMINGS.state + EASINGS.standard. */
65
+ export const OPACITY_TRANSITION: SingleTransition = {
66
+ type: 'timing',
67
+ duration: TIMINGS.state.duration,
68
+ easing: [0.2, 0, 0, 1],
69
+ }
70
+
71
+ /** Elastic indicator spring — Switch thumb, RadioGroup dot. Mirrors SPRINGS.elastic. */
72
+ export const SPRING_ELASTIC: SingleTransition = {
73
+ type: 'spring',
74
+ stiffness: 320,
75
+ damping: 22,
76
+ mass: 0.7,
77
+ }
78
+
51
79
  // ─── Press scale tokens ──────────────────────────────────────────────────────
52
80
  // Per-component press intensities — taken from DESIGN.md.
53
81
  export const PRESS_SCALE = {
@@ -0,0 +1,34 @@
1
+ import { isLoaded } from 'expo-font'
2
+
3
+ declare const __DEV__: boolean
4
+
5
+ let warned = false
6
+
7
+ /**
8
+ * Dev-only guard: warns once if the Sohne font family is not loaded.
9
+ *
10
+ * Called lazily from `Text` on first render. Without this, a missing
11
+ * `useFonts(SohneFonts)` call (or a broken font path) produces invisible /
12
+ * system-font text with no signal — a silent failure consumers reported losing
13
+ * hours to. In production (`__DEV__ === false`) this is a no-op.
14
+ */
15
+ export function warnIfFontsMissing(): void {
16
+ if (warned) return
17
+ if (typeof __DEV__ !== 'undefined' && !__DEV__) return
18
+ warned = true
19
+ try {
20
+ if (!isLoaded('Sohne-Regular')) {
21
+ console.warn(
22
+ '[retray-ui-kit] Sohne fonts are not loaded — text will fall back to the ' +
23
+ 'system font. Load them at your app root before rendering any UI kit ' +
24
+ 'component:\n\n' +
25
+ " import { useFonts } from 'expo-font'\n" +
26
+ " import { SohneFonts } from '@retray-dev/ui-kit/fonts'\n\n" +
27
+ ' const [fontsLoaded] = useFonts(SohneFonts)\n' +
28
+ ' if (!fontsLoaded) return null\n',
29
+ )
30
+ }
31
+ } catch {
32
+ // `isLoaded` not available in this expo-font version — skip the guard silently.
33
+ }
34
+ }