@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,24 +1,24 @@
1
- import React, { useRef, useState } from 'react'
1
+ import React from 'react'
2
2
  import {
3
3
  View,
4
4
  Image,
5
5
  Text,
6
6
  TouchableOpacity,
7
- Animated,
8
7
  StyleSheet,
9
8
  ViewStyle,
10
9
  ImageSourcePropType,
11
10
  Platform,
12
11
  } from 'react-native'
12
+ import Animated from 'react-native-reanimated'
13
13
  import { impactLight } from '../../utils/haptics'
14
14
  import { useTheme } from '../../theme'
15
15
  import { s, vs, ms, mvs } from '../../utils/scaling'
16
16
  import { renderIcon } from '../../utils/icons'
17
17
  import { useHover } from '../../utils/hover'
18
+ import { usePressScale } from '../../utils/usePressScale'
19
+ import { SPRINGS, PRESS_SCALE } from '../../utils/animations'
18
20
  import { RADIUS, SHADOWS } from '../../tokens'
19
21
 
20
- const nativeDriver = Platform.OS !== 'web'
21
-
22
22
  export type MediaCardAspectRatio = '1:1' | '4:3' | '16:9' | '4:5' | '3:2'
23
23
 
24
24
  const aspectRatioMap: Record<MediaCardAspectRatio, number> = {
@@ -57,9 +57,11 @@ export interface MediaCardProps {
57
57
  imageStyle?: ViewStyle
58
58
  /** Additional content rendered below caption. */
59
59
  footer?: React.ReactNode
60
+ /** Accessibility label override. Defaults to title (and subtitle if present). */
61
+ accessibilityLabel?: string
60
62
  }
61
63
 
62
- export function MediaCard({
64
+ function MediaCardBase({
63
65
  imageSource,
64
66
  aspectRatio = '4:3',
65
67
  badge,
@@ -74,20 +76,16 @@ export function MediaCard({
74
76
  style,
75
77
  imageStyle,
76
78
  footer,
79
+ accessibilityLabel,
77
80
  }: MediaCardProps) {
78
81
  const { colors } = useTheme()
79
- const scale = useRef(new Animated.Value(1)).current
80
82
  const { hovered, hoverHandlers } = useHover()
81
-
82
- const handlePressIn = () => {
83
- if (!onPress) return
84
- Animated.spring(scale, { toValue: 0.98, useNativeDriver: nativeDriver, speed: 40, bounciness: 0 }).start()
85
- }
86
-
87
- const handlePressOut = () => {
88
- if (!onPress) return
89
- Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, speed: 40, bounciness: 4 }).start()
90
- }
83
+ const { animatedStyle, onPressIn, onPressOut } = usePressScale({
84
+ pressScale: PRESS_SCALE.card,
85
+ pressInSpring: SPRINGS.surfacePressIn,
86
+ pressOutSpring: SPRINGS.surfacePressOut,
87
+ disabled: !onPress,
88
+ })
91
89
 
92
90
  const handlePress = () => {
93
91
  if (!onPress) return
@@ -102,6 +100,8 @@ export function MediaCard({
102
100
  ? renderIcon(actionIconName, 18, actionActive ? colors.primary : colors.background)
103
101
  : actionIcon ?? renderIcon('heart', 18, actionActive ? colors.primary : colors.background)
104
102
 
103
+ const a11yLabel = accessibilityLabel ?? [title, subtitle].filter(Boolean).join('. ')
104
+
105
105
  const cardContent = (
106
106
  <View
107
107
  style={[
@@ -111,9 +111,8 @@ export function MediaCard({
111
111
  ]}
112
112
  {...(Platform.OS === 'web' ? hoverHandlers : {})}
113
113
  >
114
- {/* Image area */}
115
114
  <View style={[styles.imageContainer, imageStyle]}>
116
- <View style={{ paddingTop: `${ratio * 100}%` as any }}>
115
+ <View style={{ paddingTop: `${ratio * 100}%` as `${number}%` }}>
117
116
  <View style={StyleSheet.absoluteFill}>
118
117
  {imageSource ? (
119
118
  <Image
@@ -127,27 +126,27 @@ export function MediaCard({
127
126
  </View>
128
127
  </View>
129
128
 
130
- {/* Badge — top left */}
131
129
  {badge && (
132
130
  <View style={styles.badgeContainer}>
133
131
  {badge}
134
132
  </View>
135
133
  )}
136
134
 
137
- {/* Action icon — top right */}
138
135
  {(onActionPress || actionIcon || actionIconName) && (
139
136
  <TouchableOpacity
140
137
  style={[styles.actionButton, { backgroundColor: 'rgba(0,0,0,0.24)' }]}
141
138
  onPress={() => { impactLight(); onActionPress?.() }}
142
139
  activeOpacity={0.8}
143
140
  touchSoundDisabled={true}
141
+ accessibilityRole="button"
142
+ accessibilityLabel={actionIconName ?? 'action'}
143
+ accessibilityState={{ selected: actionActive }}
144
144
  >
145
145
  {resolvedActionIcon}
146
146
  </TouchableOpacity>
147
147
  )}
148
148
  </View>
149
149
 
150
- {/* Metadata */}
151
150
  {(title || subtitle || caption || footer) && (
152
151
  <View style={styles.meta}>
153
152
  {title ? (
@@ -173,13 +172,15 @@ export function MediaCard({
173
172
 
174
173
  if (onPress) {
175
174
  return (
176
- <Animated.View style={{ transform: [{ scale }] }}>
175
+ <Animated.View style={animatedStyle}>
177
176
  <TouchableOpacity
178
177
  onPress={handlePress}
179
- onPressIn={handlePressIn}
180
- onPressOut={handlePressOut}
178
+ onPressIn={onPressIn}
179
+ onPressOut={onPressOut}
181
180
  activeOpacity={1}
182
181
  touchSoundDisabled={true}
182
+ accessibilityRole="button"
183
+ accessibilityLabel={a11yLabel}
183
184
  >
184
185
  {cardContent}
185
186
  </TouchableOpacity>
@@ -190,14 +191,15 @@ export function MediaCard({
190
191
  return cardContent
191
192
  }
192
193
 
194
+ export const MediaCard = React.memo(MediaCardBase)
195
+
193
196
  const styles = StyleSheet.create({
194
197
  card: {
195
- borderRadius: RADIUS.md, // 14px — Airbnb property card spec
198
+ borderRadius: RADIUS.md,
196
199
  overflow: 'hidden',
197
200
  backgroundColor: 'transparent',
198
201
  },
199
202
  cardHovered: {
200
- // Web hover: lift shadow
201
203
  ...SHADOWS.md,
202
204
  },
203
205
  imageContainer: {
@@ -233,17 +235,17 @@ const styles = StyleSheet.create({
233
235
  gap: vs(2),
234
236
  },
235
237
  title: {
236
- fontFamily: 'Poppins-SemiBold',
238
+ fontFamily: 'Sohne-SemiBold',
237
239
  fontSize: ms(14),
238
240
  lineHeight: mvs(20),
239
241
  },
240
242
  subtitle: {
241
- fontFamily: 'Poppins-Regular',
243
+ fontFamily: 'Sohne-Regular',
242
244
  fontSize: ms(13),
243
245
  lineHeight: mvs(18),
244
246
  },
245
247
  caption: {
246
- fontFamily: 'Poppins-Regular',
248
+ fontFamily: 'Sohne-Regular',
247
249
  fontSize: ms(12),
248
250
  lineHeight: mvs(16),
249
251
  },
@@ -0,0 +1,145 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+ import { s, vs } from '../../utils/scaling'
5
+ import { RADIUS } from '../../tokens'
6
+
7
+ export type MenuGroupVariant = 'plain' | 'card'
8
+
9
+ export interface MenuGroupProps {
10
+ children: React.ReactNode
11
+ /**
12
+ * - `plain` (default): no background, plain MenuItems inside.
13
+ * - `card`: card surface with background + border wrapping plain MenuItems.
14
+ */
15
+ variant?: MenuGroupVariant
16
+ style?: ViewStyle
17
+ }
18
+
19
+ export interface MenuGroupHeaderProps {
20
+ children: React.ReactNode
21
+ style?: ViewStyle
22
+ }
23
+
24
+ export interface MenuGroupFooterProps {
25
+ children: React.ReactNode
26
+ style?: ViewStyle
27
+ }
28
+
29
+ /**
30
+ * MenuGroup wraps multiple MenuItems and auto-adds separators between them.
31
+ * Use variant="card" for a standalone surface or "plain" for items inside another container.
32
+ */
33
+ export function MenuGroup({ children, variant = 'plain', style }: MenuGroupProps) {
34
+ const { colors } = useTheme()
35
+
36
+ // Auto-inject showSeparator={true} to all MenuItem children except the last
37
+ const processedChildren = React.Children.map(children, (child, index) => {
38
+ if (!React.isValidElement(child)) return child
39
+
40
+ // Skip MenuGroup.Header and MenuGroup.Footer
41
+ if (child.type === MenuGroupHeader || child.type === MenuGroupFooter) {
42
+ return child
43
+ }
44
+
45
+ // Check if it's a MenuItem (has onPress prop as a heuristic)
46
+ const childProps = child.props as Record<string, unknown>
47
+ const isMenuItem = 'onPress' in childProps
48
+ if (!isMenuItem) return child
49
+
50
+ const isLast = index === React.Children.count(children) - 1
51
+
52
+ // Only add separator if not already explicitly set and not last item
53
+ if (childProps['showSeparator'] === undefined && !isLast) {
54
+ return React.cloneElement(child as React.ReactElement<Record<string, unknown>>, {
55
+ showSeparator: true,
56
+ })
57
+ }
58
+
59
+ return child
60
+ })
61
+
62
+ const cardStyle: ViewStyle =
63
+ variant === 'card'
64
+ ? {
65
+ backgroundColor: colors.card,
66
+ borderRadius: RADIUS.md,
67
+ borderWidth: 1,
68
+ borderColor: colors.border,
69
+ shadowColor: '#000',
70
+ shadowOffset: { width: 0, height: 2 },
71
+ shadowOpacity: 0.06,
72
+ shadowRadius: 6,
73
+ elevation: 2,
74
+ paddingVertical: vs(4),
75
+ }
76
+ : {}
77
+
78
+ return (
79
+ <View style={[styles.container, cardStyle, style]}>
80
+ {processedChildren}
81
+ </View>
82
+ )
83
+ }
84
+
85
+ export function MenuGroupHeader({ children, style }: MenuGroupHeaderProps) {
86
+ const { colors } = useTheme()
87
+
88
+ if (typeof children === 'string') {
89
+ return (
90
+ <View style={[styles.header, { borderBottomColor: colors.separator }, style]}>
91
+ <Text style={[styles.headerText, { color: colors.foregroundMuted }]} allowFontScaling={true}>
92
+ {children}
93
+ </Text>
94
+ </View>
95
+ )
96
+ }
97
+
98
+ return <View style={[styles.header, { borderBottomColor: colors.separator }, style]}>{children}</View>
99
+ }
100
+
101
+ export function MenuGroupFooter({ children, style }: MenuGroupFooterProps) {
102
+ const { colors } = useTheme()
103
+
104
+ if (typeof children === 'string') {
105
+ return (
106
+ <View style={[styles.footer, style]}>
107
+ <Text style={[styles.footerText, { color: colors.foregroundMuted }]} allowFontScaling={true}>
108
+ {children}
109
+ </Text>
110
+ </View>
111
+ )
112
+ }
113
+
114
+ return <View style={[styles.footer, style]}>{children}</View>
115
+ }
116
+
117
+ MenuGroup.Header = MenuGroupHeader
118
+ MenuGroup.Footer = MenuGroupFooter
119
+
120
+ const styles = StyleSheet.create({
121
+ container: {
122
+ overflow: 'hidden',
123
+ },
124
+ header: {
125
+ paddingHorizontal: s(16),
126
+ paddingTop: vs(12),
127
+ paddingBottom: vs(8),
128
+ borderBottomWidth: StyleSheet.hairlineWidth,
129
+ },
130
+ headerText: {
131
+ fontFamily: 'Sohne-SemiBold',
132
+ fontSize: 13,
133
+ letterSpacing: 0.32,
134
+ textTransform: 'uppercase',
135
+ },
136
+ footer: {
137
+ paddingHorizontal: s(16),
138
+ paddingTop: vs(8),
139
+ paddingBottom: vs(12),
140
+ },
141
+ footerText: {
142
+ fontFamily: 'Sohne-Regular',
143
+ fontSize: 12,
144
+ },
145
+ })
@@ -0,0 +1 @@
1
+ export * from './MenuGroup'
@@ -1,22 +1,21 @@
1
- import React, { useRef } from 'react'
1
+ import React from 'react'
2
2
  import {
3
3
  TouchableOpacity,
4
- Animated,
5
4
  View,
6
5
  Text,
7
6
  StyleSheet,
8
7
  ViewStyle,
9
8
  TextStyle,
10
- Platform,
11
9
  } from 'react-native'
10
+ import Animated from 'react-native-reanimated'
12
11
  import { Entypo } from '@expo/vector-icons'
13
12
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
14
13
  import { useTheme } from '../../theme'
15
14
  import { s, vs, ms } from '../../utils/scaling'
16
15
  import { renderIcon } from '../../utils/icons'
17
16
  import { RADIUS } from '../../tokens'
18
-
19
- const nativeDriver = Platform.OS !== 'web'
17
+ import { usePressScale } from '../../utils/usePressScale'
18
+ import { SPRINGS, PRESS_SCALE } from '../../utils/animations'
20
19
 
21
20
  export type MenuItemVariant = 'plain' | 'card'
22
21
 
@@ -57,9 +56,11 @@ export interface MenuItemProps {
57
56
  style?: ViewStyle
58
57
  /** Style applied to the label Text. */
59
58
  labelStyle?: TextStyle
59
+ /** Accessibility label override. Defaults to label. */
60
+ accessibilityLabel?: string
60
61
  }
61
62
 
62
- export function MenuItem({
63
+ function MenuItemBase({
63
64
  label,
64
65
  subtitle,
65
66
  iconName,
@@ -73,30 +74,15 @@ export function MenuItem({
73
74
  showSeparator = false,
74
75
  style,
75
76
  labelStyle,
77
+ accessibilityLabel,
76
78
  }: MenuItemProps) {
77
79
  const { colors } = useTheme()
78
- const scale = useRef(new Animated.Value(1)).current
79
-
80
- const handlePressIn = () => {
81
- if (disabled) return
82
- Animated.spring(scale, {
83
- toValue: 0.97,
84
- useNativeDriver: nativeDriver,
85
- stiffness: 350,
86
- damping: 28,
87
- mass: 0.9,
88
- }).start()
89
- }
90
-
91
- const handlePressOut = () => {
92
- Animated.spring(scale, {
93
- toValue: 1,
94
- useNativeDriver: nativeDriver,
95
- stiffness: 220,
96
- damping: 20,
97
- mass: 0.9,
98
- }).start()
99
- }
80
+ const { animatedStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
81
+ pressScale: PRESS_SCALE.row,
82
+ pressInSpring: SPRINGS.surfacePressIn,
83
+ pressOutSpring: SPRINGS.surfacePressOut,
84
+ disabled,
85
+ })
100
86
 
101
87
  const handlePress = () => {
102
88
  hapticSelection()
@@ -122,16 +108,21 @@ export function MenuItem({
122
108
  }
123
109
  : {}
124
110
 
111
+ const a11yLabel = accessibilityLabel ?? (subtitle ? `${label}. ${subtitle}` : label)
112
+
125
113
  return (
126
- <Animated.View style={[{ transform: [{ scale }] }, disabled && styles.disabled]}>
114
+ <Animated.View style={[animatedStyle, disabled && styles.disabled]} {...hoverHandlers}>
127
115
  <TouchableOpacity
128
116
  style={[styles.container, cardStyle, style]}
129
117
  onPress={handlePress}
130
- onPressIn={handlePressIn}
131
- onPressOut={handlePressOut}
118
+ onPressIn={onPressIn}
119
+ onPressOut={onPressOut}
132
120
  disabled={disabled}
133
121
  activeOpacity={1}
134
122
  touchSoundDisabled={true}
123
+ accessibilityRole="button"
124
+ accessibilityLabel={a11yLabel}
125
+ accessibilityState={{ disabled }}
135
126
  >
136
127
  {resolvedIcon ? (
137
128
  <View style={styles.iconContainer}>{resolvedIcon}</View>
@@ -157,7 +148,7 @@ export function MenuItem({
157
148
  </View>
158
149
 
159
150
  {rightRender !== undefined ? (
160
- <View
151
+ <View
161
152
  style={styles.rightContainer}
162
153
  onStartShouldSetResponder={() => true}
163
154
  onResponderRelease={() => {}}
@@ -173,11 +164,7 @@ export function MenuItem({
173
164
  <View
174
165
  style={[
175
166
  styles.separator,
176
- {
177
- backgroundColor: colors.border,
178
- marginLeft: resolvedIcon ? s(22) + s(12) : 0,
179
- opacity: 0.6,
180
- },
167
+ { backgroundColor: colors.separator },
181
168
  ]}
182
169
  />
183
170
  ) : null}
@@ -185,11 +172,13 @@ export function MenuItem({
185
172
  )
186
173
  }
187
174
 
175
+ export const MenuItem = React.memo(MenuItemBase)
176
+
188
177
  const styles = StyleSheet.create({
189
178
  container: {
190
179
  flexDirection: 'row',
191
180
  alignItems: 'center',
192
- paddingHorizontal: 0,
181
+ paddingHorizontal: s(16),
193
182
  paddingVertical: vs(16),
194
183
  minHeight: vs(54),
195
184
  gap: s(12),
@@ -205,11 +194,11 @@ const styles = StyleSheet.create({
205
194
  justifyContent: 'center',
206
195
  },
207
196
  label: {
208
- fontFamily: 'Poppins-Medium',
197
+ fontFamily: 'Sohne-Medium',
209
198
  fontSize: ms(15),
210
199
  },
211
200
  subtitle: {
212
- fontFamily: 'Poppins-Regular',
201
+ fontFamily: 'Sohne-Regular',
213
202
  fontSize: ms(12),
214
203
  marginTop: vs(1),
215
204
  },
@@ -3,7 +3,7 @@ import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-nativ
3
3
  import { Entypo } from '@expo/vector-icons'
4
4
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
5
5
  import { useTheme } from '../../theme'
6
- import { s, vs, ms, mvs } from '../../utils/scaling'
6
+ import { s, ms, mvs } from '../../utils/scaling'
7
7
 
8
8
  const MONTH_NAMES: Record<string, string[]> = {
9
9
  en: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
@@ -56,16 +56,23 @@ export function MonthPicker({ value, onChange, locale = 'en', formatLabel, style
56
56
  }
57
57
 
58
58
  return (
59
- <View style={[styles.container, style]}>
59
+ <View style={[styles.container, style]} accessibilityRole="adjustable" accessibilityLabel={getLabel()}>
60
60
  <TouchableOpacity
61
61
  style={styles.arrow}
62
62
  onPress={handlePrev}
63
63
  activeOpacity={0.6}
64
64
  touchSoundDisabled={true}
65
+ accessibilityRole="button"
66
+ accessibilityLabel="Previous month"
67
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
65
68
  >
66
69
  <Entypo name="chevron-left" size={22} color={colors.foreground} />
67
70
  </TouchableOpacity>
68
- <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>
71
+ <Text
72
+ style={[styles.label, { color: colors.foreground }]}
73
+ allowFontScaling={true}
74
+ accessibilityLiveRegion="polite"
75
+ >
69
76
  {getLabel()}
70
77
  </Text>
71
78
  <TouchableOpacity
@@ -73,6 +80,9 @@ export function MonthPicker({ value, onChange, locale = 'en', formatLabel, style
73
80
  onPress={handleNext}
74
81
  activeOpacity={0.6}
75
82
  touchSoundDisabled={true}
83
+ accessibilityRole="button"
84
+ accessibilityLabel="Next month"
85
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
76
86
  >
77
87
  <Entypo name="chevron-right" size={22} color={colors.foreground} />
78
88
  </TouchableOpacity>
@@ -93,7 +103,7 @@ const styles = StyleSheet.create({
93
103
  justifyContent: 'center',
94
104
  },
95
105
  label: {
96
- fontFamily: 'Poppins-Medium',
106
+ fontFamily: 'Sohne-Medium',
97
107
  fontSize: ms(17),
98
108
  lineHeight: mvs(24),
99
109
  textAlign: 'center',
@@ -1,15 +1,9 @@
1
- import React, { useRef } from 'react'
2
- import {
3
- TouchableOpacity,
4
- Animated,
5
- ViewStyle,
6
- Platform,
7
- TouchableOpacityProps,
8
- } from 'react-native'
1
+ import React from 'react'
2
+ import { TouchableOpacity, Platform, ViewStyle, TouchableOpacityProps } from 'react-native'
3
+ import Animated from 'react-native-reanimated'
9
4
  import { impactLight } from '../../utils/haptics'
10
- import { useHover } from '../../utils/hover'
11
-
12
- const nativeDriver = Platform.OS !== 'web'
5
+ import { usePressScale } from '../../utils/usePressScale'
6
+ import { PRESS_SCALE, SPRINGS } from '../../utils/animations'
13
7
 
14
8
  export interface PressableProps extends Omit<TouchableOpacityProps, 'activeOpacity'> {
15
9
  /** Children content to render inside the pressable. */
@@ -18,7 +12,10 @@ export interface PressableProps extends Omit<TouchableOpacityProps, 'activeOpaci
18
12
  onPress?: () => void
19
13
  /** Scale value on press. Defaults to `0.98` (MediaCard-style). */
20
14
  pressScale?: number
21
- /** Bounciness of the spring animation on release. Defaults to `4`. */
15
+ /**
16
+ * @deprecated Use Reanimated spring config via `pressOutSpring` instead. Ignored.
17
+ * Kept for backwards compatibility with v6.x consumers.
18
+ */
22
19
  bounciness?: number
23
20
  /** Enable haptic feedback on press. Defaults to `true`. */
24
21
  haptics?: boolean
@@ -31,42 +28,29 @@ export interface PressableProps extends Omit<TouchableOpacityProps, 'activeOpaci
31
28
  }
32
29
 
33
30
  /**
34
- * Generic pressable with beautiful spring bounce effect matching MediaCard interaction.
35
- * Use for custom pressable content that needs consistent press feel.
31
+ * Generic pressable with a calibrated spring bounce Apple HIG / Airbnb feel.
32
+ * All animation runs on the UI thread via Reanimated v4 worklets.
33
+ *
34
+ * Use this for any custom pressable surface that needs consistent press feel
35
+ * (cards, list rows, image tiles, etc).
36
36
  */
37
37
  export function Pressable({
38
38
  children,
39
39
  onPress,
40
- pressScale = 0.98,
41
- bounciness = 4,
40
+ pressScale = PRESS_SCALE.card,
42
41
  haptics = true,
43
42
  style,
44
43
  disabled,
45
44
  hoverScale = 1.02,
46
45
  ...touchableProps
47
46
  }: PressableProps) {
48
- const scale = useRef(new Animated.Value(1)).current
49
- const { hovered, hoverHandlers } = useHover()
50
-
51
- const handlePressIn = () => {
52
- if (disabled) return
53
- Animated.spring(scale, {
54
- toValue: pressScale,
55
- useNativeDriver: nativeDriver,
56
- speed: 40,
57
- bounciness: 0,
58
- }).start()
59
- }
60
-
61
- const handlePressOut = () => {
62
- if (disabled) return
63
- Animated.spring(scale, {
64
- toValue: 1,
65
- useNativeDriver: nativeDriver,
66
- speed: 40,
67
- bounciness,
68
- }).start()
69
- }
47
+ const { animatedStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
48
+ pressScale,
49
+ hoverScale,
50
+ pressInSpring: SPRINGS.surfacePressIn,
51
+ pressOutSpring: SPRINGS.surfacePressOut,
52
+ disabled,
53
+ })
70
54
 
71
55
  const handlePress = () => {
72
56
  if (disabled || !onPress) return
@@ -74,23 +58,20 @@ export function Pressable({
74
58
  onPress()
75
59
  }
76
60
 
77
- const hoverScaleValue = hovered && hoverScale !== 1 ? hoverScale : 1
78
-
79
61
  return (
80
62
  <Animated.View
81
- style={[
82
- { transform: [{ scale: Animated.multiply(scale, hoverScaleValue) }] },
83
- style,
84
- ]}
63
+ style={[animatedStyle, style]}
85
64
  {...(Platform.OS === 'web' ? hoverHandlers : {})}
86
65
  >
87
66
  <TouchableOpacity
88
67
  onPress={handlePress}
89
- onPressIn={handlePressIn}
90
- onPressOut={handlePressOut}
68
+ onPressIn={onPressIn}
69
+ onPressOut={onPressOut}
91
70
  activeOpacity={1}
92
71
  disabled={disabled}
93
72
  touchSoundDisabled={true}
73
+ accessibilityRole="button"
74
+ accessibilityState={{ disabled: !!disabled }}
94
75
  {...touchableProps}
95
76
  >
96
77
  {children}