@retray-dev/ui-kit 6.1.0 → 7.0.1

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 (321) hide show
  1. package/COMPONENTS.md +447 -13
  2. package/EXAMPLES.md +248 -0
  3. package/README.md +11 -10
  4. package/dist/Accordion.d.mts +28 -0
  5. package/dist/Accordion.d.ts +28 -0
  6. package/dist/Accordion.js +340 -0
  7. package/dist/Accordion.mjs +6 -0
  8. package/dist/AlertBanner.d.mts +16 -0
  9. package/dist/AlertBanner.d.ts +16 -0
  10. package/dist/AlertBanner.js +247 -0
  11. package/dist/AlertBanner.mjs +5 -0
  12. package/dist/Avatar.d.mts +20 -0
  13. package/dist/Avatar.d.ts +20 -0
  14. package/dist/Avatar.js +234 -0
  15. package/dist/Avatar.mjs +3 -0
  16. package/dist/Badge.d.mts +26 -0
  17. package/dist/Badge.d.ts +26 -0
  18. package/dist/Badge.js +247 -0
  19. package/dist/Badge.mjs +4 -0
  20. package/dist/Button.d.mts +25 -0
  21. package/dist/Button.d.ts +25 -0
  22. package/dist/Button.js +414 -0
  23. package/dist/Button.mjs +8 -0
  24. package/dist/ButtonGroup.d.mts +26 -0
  25. package/dist/ButtonGroup.d.ts +26 -0
  26. package/dist/ButtonGroup.js +52 -0
  27. package/dist/ButtonGroup.mjs +2 -0
  28. package/dist/Card.d.mts +39 -0
  29. package/dist/Card.d.ts +39 -0
  30. package/dist/Card.js +329 -0
  31. package/dist/Card.mjs +7 -0
  32. package/dist/CategoryStrip.d.mts +26 -0
  33. package/dist/CategoryStrip.d.ts +26 -0
  34. package/dist/CategoryStrip.js +396 -0
  35. package/dist/CategoryStrip.mjs +9 -0
  36. package/dist/Checkbox.d.mts +14 -0
  37. package/dist/Checkbox.d.ts +14 -0
  38. package/dist/Checkbox.js +304 -0
  39. package/dist/Checkbox.mjs +7 -0
  40. package/dist/Chip.d.mts +31 -0
  41. package/dist/Chip.d.ts +31 -0
  42. package/dist/Chip.js +370 -0
  43. package/dist/Chip.mjs +8 -0
  44. package/dist/ConfirmDialog.d.mts +15 -0
  45. package/dist/ConfirmDialog.d.ts +15 -0
  46. package/dist/ConfirmDialog.js +530 -0
  47. package/dist/ConfirmDialog.mjs +9 -0
  48. package/dist/CurrencyDisplay.d.mts +24 -0
  49. package/dist/CurrencyDisplay.d.ts +24 -0
  50. package/dist/CurrencyDisplay.js +189 -0
  51. package/dist/CurrencyDisplay.mjs +3 -0
  52. package/dist/CurrencyInput.d.mts +26 -0
  53. package/dist/CurrencyInput.d.ts +26 -0
  54. package/dist/CurrencyInput.js +404 -0
  55. package/dist/CurrencyInput.mjs +7 -0
  56. package/dist/DetailRow.d.mts +32 -0
  57. package/dist/DetailRow.d.ts +32 -0
  58. package/dist/DetailRow.js +275 -0
  59. package/dist/DetailRow.mjs +4 -0
  60. package/dist/EmptyState.d.mts +27 -0
  61. package/dist/EmptyState.d.ts +27 -0
  62. package/dist/EmptyState.js +503 -0
  63. package/dist/EmptyState.mjs +9 -0
  64. package/dist/Form.d.mts +52 -0
  65. package/dist/Form.d.ts +52 -0
  66. package/dist/Form.js +204 -0
  67. package/dist/Form.mjs +3 -0
  68. package/dist/IconButton.d.mts +22 -0
  69. package/dist/IconButton.d.ts +22 -0
  70. package/dist/IconButton.js +383 -0
  71. package/dist/IconButton.mjs +7 -0
  72. package/dist/Input.d.mts +23 -0
  73. package/dist/Input.d.ts +23 -0
  74. package/dist/Input.js +351 -0
  75. package/dist/Input.mjs +6 -0
  76. package/dist/LabelValue.d.mts +16 -0
  77. package/dist/LabelValue.d.ts +16 -0
  78. package/dist/LabelValue.js +225 -0
  79. package/dist/LabelValue.mjs +4 -0
  80. package/dist/ListGroup.d.mts +34 -0
  81. package/dist/ListGroup.d.ts +34 -0
  82. package/dist/ListGroup.js +217 -0
  83. package/dist/ListGroup.mjs +4 -0
  84. package/dist/ListItem.d.mts +64 -0
  85. package/dist/ListItem.d.ts +64 -0
  86. package/dist/ListItem.js +430 -0
  87. package/dist/ListItem.mjs +8 -0
  88. package/dist/MediaCard.d.mts +39 -0
  89. package/dist/MediaCard.d.ts +39 -0
  90. package/dist/MediaCard.js +427 -0
  91. package/dist/MediaCard.mjs +8 -0
  92. package/dist/MenuGroup.d.mts +34 -0
  93. package/dist/MenuGroup.d.ts +34 -0
  94. package/dist/MenuGroup.js +217 -0
  95. package/dist/MenuGroup.mjs +4 -0
  96. package/dist/MenuItem.d.mts +48 -0
  97. package/dist/MenuItem.d.ts +48 -0
  98. package/dist/MenuItem.js +403 -0
  99. package/dist/MenuItem.mjs +8 -0
  100. package/dist/MonthPicker.d.mts +20 -0
  101. package/dist/MonthPicker.d.ts +20 -0
  102. package/dist/MonthPicker.js +234 -0
  103. package/dist/MonthPicker.mjs +4 -0
  104. package/dist/Pressable.d.mts +34 -0
  105. package/dist/Pressable.d.ts +34 -0
  106. package/dist/Pressable.js +132 -0
  107. package/dist/Pressable.mjs +4 -0
  108. package/dist/Progress.d.mts +14 -0
  109. package/dist/Progress.d.ts +14 -0
  110. package/dist/Progress.js +191 -0
  111. package/dist/Progress.mjs +4 -0
  112. package/dist/RadioGroup.d.mts +19 -0
  113. package/dist/RadioGroup.d.ts +19 -0
  114. package/dist/RadioGroup.js +341 -0
  115. package/dist/RadioGroup.mjs +7 -0
  116. package/dist/Select.d.mts +22 -0
  117. package/dist/Select.d.ts +22 -0
  118. package/dist/Select.js +441 -0
  119. package/dist/Select.mjs +6 -0
  120. package/dist/Separator.d.mts +10 -0
  121. package/dist/Separator.d.ts +10 -0
  122. package/dist/Separator.js +156 -0
  123. package/dist/Separator.mjs +2 -0
  124. package/dist/Sheet.d.mts +81 -0
  125. package/dist/Sheet.d.ts +81 -0
  126. package/dist/Sheet.js +340 -0
  127. package/dist/Sheet.mjs +4 -0
  128. package/dist/Skeleton.d.mts +17 -0
  129. package/dist/Skeleton.d.ts +17 -0
  130. package/dist/Skeleton.js +205 -0
  131. package/dist/Skeleton.mjs +4 -0
  132. package/dist/Slider.d.mts +20 -0
  133. package/dist/Slider.d.ts +20 -0
  134. package/dist/Slider.js +232 -0
  135. package/dist/Slider.mjs +4 -0
  136. package/dist/Spinner.d.mts +12 -0
  137. package/dist/Spinner.d.ts +12 -0
  138. package/dist/Spinner.js +172 -0
  139. package/dist/Spinner.mjs +3 -0
  140. package/dist/Switch.d.mts +13 -0
  141. package/dist/Switch.d.ts +13 -0
  142. package/dist/Switch.js +261 -0
  143. package/dist/Switch.mjs +5 -0
  144. package/dist/Tabs.d.mts +27 -0
  145. package/dist/Tabs.d.ts +27 -0
  146. package/dist/Tabs.js +389 -0
  147. package/dist/Tabs.mjs +6 -0
  148. package/dist/Text.d.mts +12 -0
  149. package/dist/Text.d.ts +12 -0
  150. package/dist/Text.js +311 -0
  151. package/dist/Text.mjs +4 -0
  152. package/dist/Textarea.d.mts +16 -0
  153. package/dist/Textarea.d.ts +16 -0
  154. package/dist/Textarea.js +333 -0
  155. package/dist/Textarea.mjs +6 -0
  156. package/dist/Toast.d.mts +47 -0
  157. package/dist/Toast.d.ts +47 -0
  158. package/dist/Toast.js +185 -0
  159. package/dist/Toast.mjs +3 -0
  160. package/dist/Toggle.d.mts +33 -0
  161. package/dist/Toggle.d.ts +33 -0
  162. package/dist/Toggle.js +397 -0
  163. package/dist/Toggle.mjs +8 -0
  164. package/dist/VirtualList.d.mts +19 -0
  165. package/dist/VirtualList.d.ts +19 -0
  166. package/dist/VirtualList.js +38 -0
  167. package/dist/VirtualList.mjs +1 -0
  168. package/dist/chunk-2CE3TQVY.mjs +11 -0
  169. package/dist/chunk-2UYENBLV.mjs +49 -0
  170. package/dist/chunk-3BBOZ3OQ.mjs +41 -0
  171. package/dist/chunk-5IKW3VNC.mjs +43 -0
  172. package/dist/chunk-63357L2X.mjs +51 -0
  173. package/dist/chunk-6LQYY7HC.mjs +127 -0
  174. package/dist/chunk-6Q64UFIA.mjs +71 -0
  175. package/dist/chunk-76PFOSM2.mjs +41 -0
  176. package/dist/chunk-7H2OR44A.mjs +14 -0
  177. package/dist/chunk-A4MDAP7G.mjs +42 -0
  178. package/dist/chunk-AU2VDY4P.mjs +190 -0
  179. package/dist/chunk-BRKYVJVV.mjs +60 -0
  180. package/dist/chunk-CRYBX2CM.mjs +146 -0
  181. package/dist/chunk-DITNP6PL.mjs +106 -0
  182. package/dist/chunk-FTLJOUOQ.mjs +97 -0
  183. package/dist/chunk-GCWOGZYL.mjs +104 -0
  184. package/dist/chunk-GNGLDL6Z.mjs +60 -0
  185. package/dist/chunk-GPOUINK5.mjs +148 -0
  186. package/dist/chunk-HSPSMN6U.mjs +115 -0
  187. package/dist/chunk-IRRY3CRZ.mjs +82 -0
  188. package/dist/chunk-JB67UOB5.mjs +92 -0
  189. package/dist/chunk-JBLL7U3U.mjs +64 -0
  190. package/dist/chunk-KWCPOM6W.mjs +136 -0
  191. package/dist/chunk-KZJRQOIU.mjs +64 -0
  192. package/dist/chunk-L7E7TVEZ.mjs +145 -0
  193. package/dist/chunk-LG4DO3DK.mjs +174 -0
  194. package/dist/chunk-LWG526VX.mjs +139 -0
  195. package/dist/chunk-MN7OG7IY.mjs +96 -0
  196. package/dist/chunk-MX6HRKMI.mjs +29 -0
  197. package/dist/chunk-NC5ZTR2Y.mjs +32 -0
  198. package/dist/chunk-NQGVLMWG.mjs +90 -0
  199. package/dist/chunk-QCNARS3X.mjs +46 -0
  200. package/dist/chunk-QXGYKWI7.mjs +134 -0
  201. package/dist/chunk-QY3X2UYR.mjs +191 -0
  202. package/dist/chunk-RKLHUDZS.mjs +92 -0
  203. package/dist/chunk-RMMK64W5.mjs +54 -0
  204. package/dist/chunk-RR2VQLKE.mjs +190 -0
  205. package/dist/chunk-RTC3CFXF.mjs +29 -0
  206. package/dist/chunk-SBZYEV4S.mjs +61 -0
  207. package/dist/chunk-SOA2Z4RB.mjs +82 -0
  208. package/dist/chunk-SOYNZDVY.mjs +151 -0
  209. package/dist/chunk-T7XZ7H7Y.mjs +57 -0
  210. package/dist/chunk-TAJ2PQ2O.mjs +163 -0
  211. package/dist/chunk-U4N7WF4Z.mjs +108 -0
  212. package/dist/chunk-URDE3EUU.mjs +132 -0
  213. package/dist/chunk-URLL5JBR.mjs +245 -0
  214. package/dist/chunk-XDMN67KV.mjs +59 -0
  215. package/dist/chunk-Y6MXOREN.mjs +120 -0
  216. package/dist/chunk-YZJAFS4P.mjs +131 -0
  217. package/dist/index.d.mts +94 -852
  218. package/dist/index.d.ts +94 -852
  219. package/dist/index.js +1387 -942
  220. package/dist/index.mjs +50 -3844
  221. package/package.json +23 -14
  222. package/src/assets/fonts/Sohne-Bold.otf +0 -0
  223. package/src/assets/fonts/Sohne-BoldItalic.otf +0 -0
  224. package/src/assets/fonts/Sohne-ExtraBold.otf +0 -0
  225. package/src/assets/fonts/Sohne-ExtraBoldItalic.otf +0 -0
  226. package/src/assets/fonts/Sohne-ExtraLight.otf +0 -0
  227. package/src/assets/fonts/Sohne-ExtraLightItalic.otf +0 -0
  228. package/src/assets/fonts/Sohne-Italic.otf +0 -0
  229. package/src/assets/fonts/Sohne-Light.otf +0 -0
  230. package/src/assets/fonts/Sohne-LightItalic.otf +0 -0
  231. package/src/assets/fonts/Sohne-Medium.otf +0 -0
  232. package/src/assets/fonts/Sohne-MediumItalic.otf +0 -0
  233. package/src/assets/fonts/Sohne-Regular.otf +0 -0
  234. package/src/assets/fonts/Sohne-SemiBold.otf +0 -0
  235. package/src/assets/fonts/Sohne-SemiBoldItalic.otf +0 -0
  236. package/src/assets/fonts/SohneMono-Bold.otf +0 -0
  237. package/src/assets/fonts/SohneMono-BoldItalic.otf +0 -0
  238. package/src/assets/fonts/SohneMono-ExtraBold.otf +0 -0
  239. package/src/assets/fonts/SohneMono-ExtraBoldItalic.otf +0 -0
  240. package/src/assets/fonts/SohneMono-ExtraLight.otf +0 -0
  241. package/src/assets/fonts/SohneMono-ExtraLightItalic.otf +0 -0
  242. package/src/assets/fonts/SohneMono-Italic.otf +0 -0
  243. package/src/assets/fonts/SohneMono-Light.otf +0 -0
  244. package/src/assets/fonts/SohneMono-LightItalic.otf +0 -0
  245. package/src/assets/fonts/SohneMono-Medium.otf +0 -0
  246. package/src/assets/fonts/SohneMono-MediumItalic.otf +0 -0
  247. package/src/assets/fonts/SohneMono-Regular.otf +0 -0
  248. package/src/assets/fonts/SohneMono-SemiBold.otf +0 -0
  249. package/src/assets/fonts/SohneMono-SemiBoldItalic.otf +0 -0
  250. package/src/components/Accordion/Accordion.tsx +13 -15
  251. package/src/components/AlertBanner/AlertBanner.tsx +33 -12
  252. package/src/components/Avatar/Avatar.tsx +4 -2
  253. package/src/components/Badge/Badge.tsx +4 -2
  254. package/src/components/Button/Button.tsx +30 -29
  255. package/src/components/ButtonGroup/ButtonGroup.tsx +13 -10
  256. package/src/components/Card/Card.tsx +36 -65
  257. package/src/components/CategoryStrip/CategoryStrip.tsx +68 -58
  258. package/src/components/Checkbox/Checkbox.tsx +41 -55
  259. package/src/components/Chip/Chip.tsx +49 -84
  260. package/src/components/ConfirmDialog/ConfirmDialog.tsx +2 -2
  261. package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +4 -2
  262. package/src/components/CurrencyInput/CurrencyInput.tsx +2 -2
  263. package/src/components/DetailRow/DetailRow.tsx +9 -7
  264. package/src/components/EmptyState/EmptyState.tsx +2 -2
  265. package/src/components/Form/Form.tsx +149 -0
  266. package/src/components/Form/index.ts +1 -0
  267. package/src/components/IconButton/IconButton.tsx +24 -20
  268. package/src/components/Input/Input.tsx +63 -50
  269. package/src/components/LabelValue/LabelValue.tsx +6 -4
  270. package/src/components/ListGroup/ListGroup.tsx +145 -0
  271. package/src/components/ListGroup/index.ts +1 -0
  272. package/src/components/ListItem/ListItem.tsx +30 -43
  273. package/src/components/MediaCard/MediaCard.tsx +31 -29
  274. package/src/components/MenuGroup/MenuGroup.tsx +145 -0
  275. package/src/components/MenuGroup/index.ts +1 -0
  276. package/src/components/MenuItem/MenuItem.tsx +29 -40
  277. package/src/components/MonthPicker/MonthPicker.tsx +14 -4
  278. package/src/components/Pressable/Pressable.tsx +27 -46
  279. package/src/components/Progress/Progress.tsx +21 -12
  280. package/src/components/RadioGroup/RadioGroup.tsx +55 -32
  281. package/src/components/Select/Select.tsx +23 -21
  282. package/src/components/Separator/Separator.tsx +1 -3
  283. package/src/components/Sheet/Sheet.tsx +85 -18
  284. package/src/components/Skeleton/Skeleton.tsx +25 -14
  285. package/src/components/Slider/Slider.tsx +13 -3
  286. package/src/components/Spinner/Spinner.tsx +1 -1
  287. package/src/components/Switch/Switch.tsx +70 -52
  288. package/src/components/Tabs/Tabs.tsx +59 -47
  289. package/src/components/Text/Text.tsx +3 -1
  290. package/src/components/Textarea/Textarea.tsx +44 -23
  291. package/src/components/Toast/Toast.tsx +6 -6
  292. package/src/components/Toggle/Toggle.tsx +86 -68
  293. package/src/components/VirtualList/VirtualList.tsx +60 -0
  294. package/src/components/VirtualList/index.ts +1 -0
  295. package/src/fonts.ts +38 -20
  296. package/src/index.ts +5 -1
  297. package/src/theme/colors.ts +53 -39
  298. package/src/theme/types.ts +3 -0
  299. package/src/tokens.ts +49 -39
  300. package/src/utils/animations.ts +58 -0
  301. package/src/utils/icons.ts +47 -20
  302. package/src/utils/useColorTransition.ts +40 -0
  303. package/src/utils/usePressScale.ts +75 -0
  304. package/src/assets/fonts/Poppins-Black.ttf +0 -0
  305. package/src/assets/fonts/Poppins-BlackItalic.ttf +0 -0
  306. package/src/assets/fonts/Poppins-Bold.ttf +0 -0
  307. package/src/assets/fonts/Poppins-BoldItalic.ttf +0 -0
  308. package/src/assets/fonts/Poppins-ExtraBold.ttf +0 -0
  309. package/src/assets/fonts/Poppins-ExtraBoldItalic.ttf +0 -0
  310. package/src/assets/fonts/Poppins-ExtraLight.ttf +0 -0
  311. package/src/assets/fonts/Poppins-ExtraLightItalic.ttf +0 -0
  312. package/src/assets/fonts/Poppins-Italic.ttf +0 -0
  313. package/src/assets/fonts/Poppins-Light.ttf +0 -0
  314. package/src/assets/fonts/Poppins-LightItalic.ttf +0 -0
  315. package/src/assets/fonts/Poppins-Medium.ttf +0 -0
  316. package/src/assets/fonts/Poppins-MediumItalic.ttf +0 -0
  317. package/src/assets/fonts/Poppins-Regular.ttf +0 -0
  318. package/src/assets/fonts/Poppins-SemiBold.ttf +0 -0
  319. package/src/assets/fonts/Poppins-SemiBoldItalic.ttf +0 -0
  320. package/src/assets/fonts/Poppins-Thin.ttf +0 -0
  321. package/src/assets/fonts/Poppins-ThinItalic.ttf +0 -0
@@ -1,66 +1,77 @@
1
- import React, { useEffect, useRef } from 'react'
2
- import { TouchableOpacity, Animated, StyleSheet, ViewStyle, Platform, View } from 'react-native'
1
+ import React, { useEffect } from 'react'
2
+ import { TouchableOpacity, StyleSheet, ViewStyle, View } from 'react-native'
3
+ import Animated, {
4
+ useSharedValue,
5
+ useAnimatedStyle,
6
+ withSpring,
7
+ withTiming,
8
+ interpolateColor,
9
+ } from 'react-native-reanimated'
3
10
  import { Feather } from '@expo/vector-icons'
4
-
5
- const nativeDriver = Platform.OS !== 'web'
6
11
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
7
12
  import { useTheme } from '../../theme'
8
13
  import { s } from '../../utils/scaling'
14
+ import { SPRINGS, TIMINGS, EASINGS } from '../../utils/animations'
9
15
 
10
- const TRACK_WIDTH = s(52)
16
+ const TRACK_WIDTH = s(52)
11
17
  const TRACK_HEIGHT = s(30)
12
- const THUMB_SIZE = s(24)
18
+ const THUMB_SIZE = s(24)
13
19
  const THUMB_OFFSET = s(3)
14
20
  const THUMB_TRAVEL = TRACK_WIDTH - THUMB_SIZE - THUMB_OFFSET * 2
21
+ const ICON_SIZE = s(13)
15
22
 
16
23
  export interface SwitchProps {
17
24
  checked?: boolean
18
25
  onCheckedChange?: (checked: boolean) => void
19
26
  disabled?: boolean
20
27
  style?: ViewStyle
28
+ accessibilityLabel?: string
21
29
  }
22
30
 
23
- const ICON_SIZE = s(13)
24
-
25
- export function Switch({ checked = false, onCheckedChange, disabled, style }: SwitchProps) {
31
+ export function Switch({ checked = false, onCheckedChange, disabled, style, accessibilityLabel }: SwitchProps) {
26
32
  const { colors } = useTheme()
27
- const translateX = useRef(new Animated.Value(checked ? THUMB_TRAVEL : 0)).current
28
- const trackOpacity = useRef(new Animated.Value(checked ? 1 : 0)).current
29
- const checkOpacity = useRef(new Animated.Value(checked ? 1 : 0)).current
30
- const crossOpacity = useRef(new Animated.Value(checked ? 0 : 1)).current
33
+
34
+ const progress = useSharedValue(checked ? 1 : 0)
31
35
 
32
36
  useEffect(() => {
33
- Animated.parallel([
34
- Animated.spring(translateX, {
35
- toValue: checked ? THUMB_TRAVEL : 0,
36
- useNativeDriver: nativeDriver,
37
- bounciness: 4,
38
- }),
39
- Animated.timing(trackOpacity, {
40
- toValue: checked ? 1 : 0,
41
- duration: 150,
42
- useNativeDriver: false,
43
- }),
44
- Animated.timing(checkOpacity, {
45
- toValue: checked ? 1 : 0,
46
- duration: 120,
47
- useNativeDriver: true,
48
- }),
49
- Animated.timing(crossOpacity, {
50
- toValue: checked ? 0 : 1,
51
- duration: 120,
52
- useNativeDriver: true,
53
- }),
54
- ]).start()
55
- }, [checked, translateX, trackOpacity, checkOpacity, crossOpacity])
37
+ progress.value = withSpring(checked ? 1 : 0, SPRINGS.elastic)
38
+ }, [checked, progress])
39
+
40
+ const thumbStyle = useAnimatedStyle(() => ({
41
+ transform: [{ translateX: progress.value * THUMB_TRAVEL }],
42
+ }))
43
+
44
+ const trackStyle = useAnimatedStyle(() => ({
45
+ backgroundColor: interpolateColor(
46
+ progress.value,
47
+ [0, 1],
48
+ [colors.surfaceStrong, colors.primary],
49
+ ),
50
+ }))
51
+
52
+ // AUDIT FIX: the off-state track used surfaceStrong (~#ebebeb in light mode)
53
+ // with no border — nearly invisible on white page/card surfaces. A 1.5px border
54
+ // that fades out as the track fills gives the off state clear visual definition
55
+ // without adding visual weight to the on state.
56
+ const trackBorderStyle = useAnimatedStyle(() => ({
57
+ borderWidth: 1.5,
58
+ borderColor: interpolateColor(
59
+ progress.value,
60
+ [0, 1],
61
+ [colors.border, 'transparent'],
62
+ ),
63
+ }))
56
64
 
57
- const trackColor = trackOpacity.interpolate({
58
- inputRange: [0, 1],
59
- outputRange: [colors.surface, colors.primary],
60
- })
65
+ const checkIconStyle = useAnimatedStyle(() => ({
66
+ opacity: withTiming(checked ? 1 : 0, { duration: TIMINGS.state.duration, easing: EASINGS.standard }),
67
+ }))
68
+
69
+ const crossIconStyle = useAnimatedStyle(() => ({
70
+ opacity: withTiming(checked ? 0 : 1, { duration: TIMINGS.state.duration, easing: EASINGS.standard }),
71
+ }))
61
72
 
62
73
  return (
63
- <View style={[{ opacity: disabled ? 0.45 : 1 }, style]}>
74
+ <View style={[{ opacity: disabled ? 0.45 : 1, alignSelf: 'flex-start' }, style]}>
64
75
  <TouchableOpacity
65
76
  onPress={() => {
66
77
  hapticSelection()
@@ -69,19 +80,20 @@ export function Switch({ checked = false, onCheckedChange, disabled, style }: Sw
69
80
  disabled={disabled}
70
81
  activeOpacity={0.8}
71
82
  touchSoundDisabled={true}
72
- style={styles.wrapper}
83
+ accessibilityRole="switch"
84
+ accessibilityLabel={accessibilityLabel}
85
+ accessibilityState={{ checked, disabled: !!disabled }}
86
+ style={styles.touchable}
73
87
  >
74
- <Animated.View style={[styles.track, { backgroundColor: trackColor }]}>
88
+ <Animated.View style={[styles.track, trackStyle]}>
89
+ <Animated.View style={[styles.trackBorder, trackBorderStyle]} pointerEvents="none" />
75
90
  <Animated.View
76
- style={[
77
- styles.thumb,
78
- { backgroundColor: colors.primaryForeground, transform: [{ translateX }] },
79
- ]}
91
+ style={[styles.thumb, { backgroundColor: colors.primaryForeground }, thumbStyle]}
80
92
  >
81
- <Animated.View style={[styles.iconWrapper, { opacity: checkOpacity }]}>
93
+ <Animated.View style={[styles.iconWrapper, checkIconStyle]}>
82
94
  <Feather name="check" size={ICON_SIZE} color={colors.primary} />
83
95
  </Animated.View>
84
- <Animated.View style={[styles.iconWrapper, { opacity: crossOpacity }]}>
96
+ <Animated.View style={[styles.iconWrapper, crossIconStyle]}>
85
97
  <Feather name="x" size={ICON_SIZE} color={colors.foregroundMuted} />
86
98
  </Animated.View>
87
99
  </Animated.View>
@@ -92,13 +104,17 @@ export function Switch({ checked = false, onCheckedChange, disabled, style }: Sw
92
104
  }
93
105
 
94
106
  const styles = StyleSheet.create({
95
- wrapper: {},
107
+ touchable: {
108
+ alignSelf: 'flex-start',
109
+ },
96
110
  track: {
97
111
  width: TRACK_WIDTH,
98
112
  height: TRACK_HEIGHT,
99
113
  borderRadius: TRACK_HEIGHT / 2,
100
- // No justifyContent/alignItems — thumb uses absolute positioning
101
- // so the track's flex layout doesn't interfere with translateX animation
114
+ },
115
+ trackBorder: {
116
+ ...StyleSheet.absoluteFillObject,
117
+ borderRadius: TRACK_HEIGHT / 2,
102
118
  },
103
119
  thumb: {
104
120
  position: 'absolute',
@@ -117,5 +133,7 @@ const styles = StyleSheet.create({
117
133
  },
118
134
  iconWrapper: {
119
135
  position: 'absolute',
136
+ alignItems: 'center',
137
+ justifyContent: 'center',
120
138
  },
121
139
  })
@@ -1,10 +1,15 @@
1
- import React, { useState, useRef, useEffect } from 'react'
2
- import { View, TouchableOpacity, Text, Animated, StyleSheet, ViewStyle, Platform } from 'react-native'
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react'
2
+ import { View, TouchableOpacity, Text, StyleSheet, ViewStyle, LayoutChangeEvent } from 'react-native'
3
+ import Animated, {
4
+ useSharedValue,
5
+ useAnimatedStyle,
6
+ withSpring,
7
+ } from 'react-native-reanimated'
3
8
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
4
-
5
- const nativeDriver = Platform.OS !== 'web'
6
9
  import { useTheme } from '../../theme'
7
10
  import { s, vs, ms } from '../../utils/scaling'
11
+ import { usePressScale } from '../../utils/usePressScale'
12
+ import { SPRINGS, PRESS_SCALE } from '../../utils/animations'
8
13
 
9
14
  export interface TabItem {
10
15
  label: string
@@ -12,8 +17,6 @@ export interface TabItem {
12
17
  icon?: React.ReactNode | ((active: boolean) => React.ReactNode)
13
18
  }
14
19
 
15
- // pill: animated sliding pill background (default)
16
- // underline: 2px bottom border on active tab — Airbnb product-tab style
17
20
  export type TabsVariant = 'pill' | 'underline'
18
21
 
19
22
  export interface TabsProps {
@@ -42,20 +45,13 @@ function TabTrigger({
42
45
  tab: TabItem
43
46
  isActive: boolean
44
47
  onPress: () => void
45
- onLayout: (e: any) => void
48
+ onLayout: (e: LayoutChangeEvent) => void
46
49
  variant: TabsVariant
47
50
  }) {
48
51
  const { colors } = useTheme()
49
- const scale = useRef(new Animated.Value(1)).current
50
-
51
- const handlePressIn = () => {
52
- Animated.spring(scale, { toValue: 0.95, useNativeDriver: nativeDriver, stiffness: 600, damping: 35, mass: 0.8 }).start()
53
- }
54
-
55
- const handlePressOut = () => {
56
- Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, stiffness: 280, damping: 22, mass: 0.8 }).start()
57
- }
58
-
52
+ const { animatedStyle, onPressIn, onPressOut } = usePressScale({
53
+ pressScale: PRESS_SCALE.button,
54
+ })
59
55
  const isUnderline = variant === 'underline'
60
56
 
61
57
  return (
@@ -66,22 +62,28 @@ function TabTrigger({
66
62
  isUnderline && isActive && { borderBottomColor: colors.primary },
67
63
  ]}
68
64
  onPress={onPress}
69
- onPressIn={handlePressIn}
70
- onPressOut={handlePressOut}
65
+ onPressIn={onPressIn}
66
+ onPressOut={onPressOut}
71
67
  onLayout={onLayout}
72
68
  activeOpacity={1}
73
69
  touchSoundDisabled={true}
70
+ accessibilityRole="tab"
71
+ accessibilityState={{ selected: isActive }}
72
+ accessibilityLabel={tab.label}
74
73
  >
75
- <Animated.View style={{ transform: [{ scale }] }}>
74
+ <Animated.View style={animatedStyle}>
76
75
  <View style={styles.triggerInner}>
77
76
  {tab.icon ? (
78
- (typeof tab.icon === 'function' ? (tab.icon as any)(isActive) : tab.icon) as React.ReactNode
77
+ typeof tab.icon === 'function' ? tab.icon(isActive) : tab.icon
79
78
  ) : null}
80
79
  <Text
81
80
  style={[
82
81
  styles.triggerLabel,
82
+ // AUDIT FIX: active state now only changes color, never font metrics.
83
+ // Previously: inactive=Regular, active=Medium (pill) or SemiBold+fontSize14 (underline)
84
+ // The weight/size change caused measurable layout reflow every tab switch.
85
+ // Solution: all labels render at SemiBold always; active = foreground, inactive = foregroundMuted.
83
86
  { color: isActive ? colors.foreground : colors.foregroundMuted },
84
- isActive && (isUnderline ? styles.activeTriggerLabelUnderline : styles.activeTriggerLabel),
85
87
  ]}
86
88
  allowFontScaling={true}
87
89
  >
@@ -99,27 +101,27 @@ export function Tabs({ tabs, variant = 'pill', value, onValueChange, children, s
99
101
  const active = value ?? internal
100
102
 
101
103
  const tabLayouts = useRef<Record<string, { x: number; width: number }>>({})
102
- const pillX = useRef(new Animated.Value(0)).current
103
- const pillWidth = useRef(new Animated.Value(0)).current
104
+ const pillX = useSharedValue(0)
105
+ const pillWidth = useSharedValue(0)
104
106
  const initialised = useRef(false)
105
107
 
106
- const animatePill = (tabValue: string, animate: boolean) => {
108
+ const animatePill = useCallback((tabValue: string, animate: boolean) => {
107
109
  const layout = tabLayouts.current[tabValue]
108
110
  if (!layout) return
109
111
  if (animate) {
110
- Animated.parallel([
111
- Animated.spring(pillX, { toValue: layout.x, useNativeDriver: false, stiffness: 380, damping: 38, mass: 1.0 }),
112
- Animated.spring(pillWidth, { toValue: layout.width, useNativeDriver: false, stiffness: 380, damping: 38, mass: 1.0 }),
113
- ]).start()
112
+ // eslint-disable-next-line react-hooks/immutability
113
+ pillX.value = withSpring(layout.x, SPRINGS.glide)
114
+ // eslint-disable-next-line react-hooks/immutability
115
+ pillWidth.value = withSpring(layout.width, SPRINGS.glide)
114
116
  } else {
115
- pillX.setValue(layout.x)
116
- pillWidth.setValue(layout.width)
117
+ pillX.value = layout.x
118
+ pillWidth.value = layout.width
117
119
  }
118
- }
120
+ }, [pillX, pillWidth])
119
121
 
120
122
  useEffect(() => {
121
123
  if (initialised.current) animatePill(active, true)
122
- }, [active])
124
+ }, [active, animatePill])
123
125
 
124
126
  const handlePress = (v: string) => {
125
127
  hapticSelection()
@@ -127,11 +129,21 @@ export function Tabs({ tabs, variant = 'pill', value, onValueChange, children, s
127
129
  onValueChange?.(v)
128
130
  }
129
131
 
132
+ const pillAnimatedStyle = useAnimatedStyle(() => ({
133
+ transform: [{ translateX: pillX.value }],
134
+ width: pillWidth.value,
135
+ }))
136
+
130
137
  return (
131
138
  <View style={style}>
132
- <View style={[
133
- variant === 'pill' ? [styles.list, { backgroundColor: colors.surface }] : styles.listUnderline,
134
- ]}>
139
+ <View
140
+ style={[
141
+ variant === 'pill'
142
+ ? [styles.list, { backgroundColor: colors.surface }]
143
+ : styles.listUnderline,
144
+ ]}
145
+ accessibilityRole="tablist"
146
+ >
135
147
  {variant === 'pill' && (
136
148
  <Animated.View
137
149
  style={[
@@ -141,8 +153,7 @@ export function Tabs({ tabs, variant = 'pill', value, onValueChange, children, s
141
153
  position: 'absolute',
142
154
  top: 4,
143
155
  bottom: 4,
144
- left: pillX,
145
- width: pillWidth,
156
+ left: 0,
146
157
  borderRadius: 8,
147
158
  shadowColor: '#000',
148
159
  shadowOffset: { width: 0, height: 1 },
@@ -150,6 +161,7 @@ export function Tabs({ tabs, variant = 'pill', value, onValueChange, children, s
150
161
  shadowRadius: 2,
151
162
  elevation: 2,
152
163
  },
164
+ pillAnimatedStyle,
153
165
  ]}
154
166
  />
155
167
  )}
@@ -178,7 +190,7 @@ export function Tabs({ tabs, variant = 'pill', value, onValueChange, children, s
178
190
 
179
191
  export function TabsContent({ value, activeValue, children, style }: TabsContentProps) {
180
192
  if (value !== activeValue) return null
181
- return <View style={style}>{children}</View>
193
+ return <View style={style} accessibilityRole="none">{children}</View>
182
194
  }
183
195
 
184
196
  const styles = StyleSheet.create({
@@ -190,6 +202,8 @@ const styles = StyleSheet.create({
190
202
  },
191
203
  listUnderline: {
192
204
  flexDirection: 'row',
205
+ // AUDIT FIX: was missing borderBottomColor — the 1px hairline would render
206
+ // as transparent on some platforms. Explicit token reference ensures visibility.
193
207
  borderBottomWidth: 1,
194
208
  },
195
209
  pill: {},
@@ -216,15 +230,13 @@ const styles = StyleSheet.create({
216
230
  justifyContent: 'center',
217
231
  gap: s(4),
218
232
  },
233
+ // AUDIT FIX: was Sohne-Regular at rest, Sohne-Medium/SemiBold when active.
234
+ // Font-weight changes at runtime cause advance-width shifts → the tab bar would
235
+ // visibly jump/reflow on every selection. Now always SemiBold; active state
236
+ // is communicated by color alone (foreground vs foregroundMuted). The pill
237
+ // indicator provides additional active signal without text layout side-effects.
219
238
  triggerLabel: {
220
- fontFamily: 'Poppins-Regular',
239
+ fontFamily: 'Sohne-SemiBold',
221
240
  fontSize: ms(13),
222
241
  },
223
- activeTriggerLabel: {
224
- fontFamily: 'Poppins-Medium',
225
- },
226
- activeTriggerLabelUnderline: {
227
- fontFamily: 'Poppins-SemiBold',
228
- fontSize: ms(14),
229
- },
230
242
  })
@@ -67,7 +67,7 @@ const defaultColorVariant: Partial<Record<TextVariant, 'foreground' | 'foregroun
67
67
  'button-sm': 'foreground',
68
68
  }
69
69
 
70
- export function Text({ variant = 'body-md', color, style, children, ...props }: TextProps) {
70
+ function TextBase({ variant = 'body-md', color, style, children, ...props }: TextProps) {
71
71
  const { colors } = useTheme()
72
72
 
73
73
  const colorKey = defaultColorVariant[variant] ?? 'foreground'
@@ -83,3 +83,5 @@ export function Text({ variant = 'body-md', color, style, children, ...props }:
83
83
  </RNText>
84
84
  )
85
85
  }
86
+
87
+ export const Text = React.memo(TextBase)
@@ -1,29 +1,29 @@
1
1
  import React, { useState } from 'react'
2
2
  import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, Platform } from 'react-native'
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ interpolateColor,
6
+ interpolate,
7
+ } from 'react-native-reanimated'
3
8
  import { useTheme } from '../../theme'
4
9
  import { s, vs, ms } from '../../utils/scaling'
5
10
  import { renderIcon } from '../../utils/icons'
11
+ import { useColorTransition } from '../../utils/useColorTransition'
12
+ import { TIMINGS } from '../../utils/animations'
6
13
 
7
- const webInputResetStyle: any =
14
+ const webInputResetStyle: Record<string, unknown> =
8
15
  Platform.OS === 'web'
9
16
  ? { outlineStyle: 'none', outlineWidth: 0, outlineColor: 'transparent', boxShadow: 'none' }
10
17
  : {}
11
18
 
12
19
  export interface TextareaProps extends TextInputProps {
13
20
  label?: string
14
- /** Red helper text below the textarea; also changes border to `destructive` color. Takes priority over `hint`. */
15
21
  error?: string
16
- /** Helper text shown below the textarea when there is no error. */
17
22
  hint?: string
18
- /** Number of visible text rows. Defaults to `4`. Controls `numberOfLines` and `minHeight`. */
19
23
  rows?: number
20
- /** Icon name from @expo/vector-icons rendered inside top-left corner. */
21
24
  prefixIcon?: string
22
- /** Custom icon node rendered top-left. */
23
25
  prefixIconNode?: React.ReactNode
24
- /** Override prefix icon color. Defaults to foregroundMuted. */
25
26
  prefixIconColor?: string
26
- /** Style for the outer container `View`. Use `style` (from `TextInputProps`) to style the `TextInput` itself. */
27
27
  containerStyle?: ViewStyle
28
28
  }
29
29
 
@@ -39,31 +39,40 @@ export function Textarea({
39
39
  style,
40
40
  onFocus,
41
41
  onBlur,
42
+ accessibilityLabel,
42
43
  ...props
43
44
  }: TextareaProps) {
44
45
  const { colors } = useTheme()
45
46
  const [focused, setFocused] = useState(false)
47
+ const focusProgress = useColorTransition(focused, {
48
+ duration: focused ? TIMINGS.focusIn.duration : TIMINGS.focusOut.duration,
49
+ })
46
50
 
47
51
  const resolvedPrefixIcon = prefixIcon
48
52
  ? renderIcon(prefixIcon, ms(16), prefixIconColor ?? colors.foregroundMuted)
49
53
  : prefixIconNode
50
54
 
55
+ // Border drawn on an absolute overlay (mirrors Input.tsx) so the 1px→2px
56
+ // focus weight change never resizes the box / reflows content.
57
+ const borderAnimStyle = useAnimatedStyle(() => ({
58
+ borderColor: error
59
+ ? colors.destructive
60
+ : interpolateColor(focusProgress.value, [0, 1], [colors.border, colors.primary]),
61
+ borderWidth: error
62
+ ? 2
63
+ : interpolate(focusProgress.value, [0, 1], [1, 2]),
64
+ }))
65
+
51
66
  return (
52
67
  <View style={[styles.container, containerStyle]}>
53
68
  {label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
54
- <View
69
+ <Animated.View
55
70
  style={[
56
71
  styles.inputWrapper,
57
- {
58
- borderColor: error
59
- ? colors.destructive
60
- : focused
61
- ? (colors.ring ?? colors.primary)
62
- : colors.border,
63
- backgroundColor: colors.background,
64
- },
72
+ { backgroundColor: colors.background },
65
73
  ]}
66
74
  >
75
+ <Animated.View style={[styles.borderOverlay, borderAnimStyle]} pointerEvents="none" />
67
76
  {resolvedPrefixIcon ? <View style={styles.prefixIcon}>{resolvedPrefixIcon}</View> : null}
68
77
  <TextInput
69
78
  multiline
@@ -88,11 +97,18 @@ export function Textarea({
88
97
  }}
89
98
  placeholderTextColor={colors.foregroundMuted}
90
99
  allowFontScaling={true}
100
+ accessibilityLabel={accessibilityLabel ?? label}
91
101
  {...props}
92
102
  />
93
- </View>
103
+ </Animated.View>
94
104
  {error ? (
95
- <Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
105
+ <Text
106
+ style={[styles.helperText, { color: colors.destructive }]}
107
+ allowFontScaling={true}
108
+ accessibilityLiveRegion="polite"
109
+ >
110
+ {error}
111
+ </Text>
96
112
  ) : null}
97
113
  {!error && hint ? (
98
114
  <Text style={[styles.helperText, { color: colors.foregroundMuted }]} allowFontScaling={true}>{hint}</Text>
@@ -106,32 +122,37 @@ const styles = StyleSheet.create({
106
122
  gap: vs(4),
107
123
  },
108
124
  label: {
109
- fontFamily: 'Poppins-Medium',
125
+ fontFamily: 'Sohne-Medium',
110
126
  fontSize: ms(13),
111
127
  lineHeight: vs(18),
112
128
  marginBottom: vs(2),
113
129
  },
114
130
  inputWrapper: {
115
- borderWidth: 1,
131
+ // Border lives on borderOverlay (absolute); wrapper carries none so the
132
+ // focus weight change never reflows content.
116
133
  borderRadius: 8,
117
134
  paddingHorizontal: s(14),
118
135
  paddingVertical: vs(11),
119
136
  gap: s(8),
120
137
  },
138
+ borderOverlay: {
139
+ ...StyleSheet.absoluteFillObject,
140
+ borderRadius: 8,
141
+ },
121
142
  prefixIcon: {
122
143
  alignItems: 'flex-start',
123
144
  justifyContent: 'flex-start',
124
145
  paddingTop: vs(2),
125
146
  },
126
147
  input: {
127
- fontFamily: 'Poppins-Regular',
148
+ fontFamily: 'Sohne-Regular',
128
149
  fontSize: ms(14),
129
150
  lineHeight: vs(22),
130
151
  padding: 0,
131
152
  margin: 0,
132
153
  },
133
154
  helperText: {
134
- fontFamily: 'Poppins-Regular',
155
+ fontFamily: 'Sohne-Regular',
135
156
  fontSize: ms(12),
136
157
  lineHeight: vs(16),
137
158
  marginTop: vs(4),
@@ -4,10 +4,8 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'
4
4
  import { useTheme } from '../../theme'
5
5
  import { s, vs, ms } from '../../utils/scaling'
6
6
 
7
- // Direct function API — no hook required
8
7
  export { sonnerToast as toast }
9
8
 
10
- // useToast — backward-compat wrapper
11
9
  export function useToast() {
12
10
  return {
13
11
  toast: sonnerToast,
@@ -15,7 +13,6 @@ export function useToast() {
15
13
  }
16
14
  }
17
15
 
18
- // ToastProvider — wraps children + renders the Toaster
19
16
  export function ToastProvider({ children }: { children: React.ReactNode }) {
20
17
  const { colorScheme } = useTheme()
21
18
  const insets = useSafeAreaInsets()
@@ -26,7 +23,10 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
26
23
  <Toaster
27
24
  theme={colorScheme}
28
25
  position="top-center"
29
- richColors={false}
26
+ // AUDIT FIX: was richColors={false} — all semantic variants (error, success,
27
+ // warning) were visually identical. richColors={true} restores correct
28
+ // semantic coloring so users immediately recognise severity from colour alone.
29
+ richColors={true}
30
30
  gap={vs(8)}
31
31
  offset={insets.top + vs(8)}
32
32
  visibleToasts={3}
@@ -40,11 +40,11 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
40
40
  paddingVertical: vs(10),
41
41
  },
42
42
  titleStyle: {
43
- fontFamily: 'Poppins-Medium',
43
+ fontFamily: 'Sohne-Medium',
44
44
  fontSize: ms(13),
45
45
  },
46
46
  descriptionStyle: {
47
- fontFamily: 'Poppins-Regular',
47
+ fontFamily: 'Sohne-Regular',
48
48
  fontSize: ms(12),
49
49
  opacity: 0.85,
50
50
  },