@retray-dev/ui-kit 6.2.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (389) hide show
  1. package/COMPONENTS.md +997 -20
  2. package/EXAMPLES.md +250 -2
  3. package/README.md +21 -14
  4. package/dist/Accordion.d.mts +28 -0
  5. package/dist/Accordion.d.ts +28 -0
  6. package/dist/Accordion.js +392 -0
  7. package/dist/Accordion.mjs +7 -0
  8. package/dist/AlertBanner.d.mts +16 -0
  9. package/dist/AlertBanner.d.ts +16 -0
  10. package/dist/AlertBanner.js +250 -0
  11. package/dist/AlertBanner.mjs +6 -0
  12. package/dist/AppHeader.d.mts +40 -0
  13. package/dist/AppHeader.d.ts +40 -0
  14. package/dist/AppHeader.js +515 -0
  15. package/dist/AppHeader.mjs +10 -0
  16. package/dist/Avatar.d.mts +20 -0
  17. package/dist/Avatar.d.ts +20 -0
  18. package/dist/Avatar.js +244 -0
  19. package/dist/Avatar.mjs +4 -0
  20. package/dist/Badge.d.mts +26 -0
  21. package/dist/Badge.d.ts +26 -0
  22. package/dist/Badge.js +257 -0
  23. package/dist/Badge.mjs +5 -0
  24. package/dist/Button.d.mts +30 -0
  25. package/dist/Button.d.ts +30 -0
  26. package/dist/Button.js +432 -0
  27. package/dist/Button.mjs +9 -0
  28. package/dist/ButtonGroup.d.mts +26 -0
  29. package/dist/ButtonGroup.d.ts +26 -0
  30. package/dist/ButtonGroup.js +52 -0
  31. package/dist/ButtonGroup.mjs +3 -0
  32. package/dist/Card.d.mts +39 -0
  33. package/dist/Card.d.ts +39 -0
  34. package/dist/Card.js +349 -0
  35. package/dist/Card.mjs +8 -0
  36. package/dist/CategoryStrip.d.mts +26 -0
  37. package/dist/CategoryStrip.d.ts +26 -0
  38. package/dist/CategoryStrip.js +453 -0
  39. package/dist/CategoryStrip.mjs +9 -0
  40. package/dist/Checkbox.d.mts +14 -0
  41. package/dist/Checkbox.d.ts +14 -0
  42. package/dist/Checkbox.js +336 -0
  43. package/dist/Checkbox.mjs +7 -0
  44. package/dist/Chip.d.mts +31 -0
  45. package/dist/Chip.d.ts +31 -0
  46. package/dist/Chip.js +403 -0
  47. package/dist/Chip.mjs +8 -0
  48. package/dist/ConfirmDialog.d.mts +15 -0
  49. package/dist/ConfirmDialog.d.ts +15 -0
  50. package/dist/ConfirmDialog.js +560 -0
  51. package/dist/ConfirmDialog.mjs +10 -0
  52. package/dist/CurrencyDisplay.d.mts +24 -0
  53. package/dist/CurrencyDisplay.d.ts +24 -0
  54. package/dist/CurrencyDisplay.js +189 -0
  55. package/dist/CurrencyDisplay.mjs +4 -0
  56. package/dist/CurrencyInput.d.mts +26 -0
  57. package/dist/CurrencyInput.d.ts +26 -0
  58. package/dist/CurrencyInput.js +408 -0
  59. package/dist/CurrencyInput.mjs +8 -0
  60. package/dist/DetailRow.d.mts +32 -0
  61. package/dist/DetailRow.d.ts +32 -0
  62. package/dist/DetailRow.js +275 -0
  63. package/dist/DetailRow.mjs +5 -0
  64. package/dist/EmptyState.d.mts +27 -0
  65. package/dist/EmptyState.d.ts +27 -0
  66. package/dist/EmptyState.js +523 -0
  67. package/dist/EmptyState.mjs +10 -0
  68. package/dist/ErrorBoundary.d.mts +42 -0
  69. package/dist/ErrorBoundary.d.ts +42 -0
  70. package/dist/ErrorBoundary.js +351 -0
  71. package/dist/ErrorBoundary.mjs +7 -0
  72. package/dist/Form.d.mts +52 -0
  73. package/dist/Form.d.ts +52 -0
  74. package/dist/Form.js +204 -0
  75. package/dist/Form.mjs +4 -0
  76. package/dist/HolographicCard.d.mts +55 -0
  77. package/dist/HolographicCard.d.ts +55 -0
  78. package/dist/HolographicCard.js +316 -0
  79. package/dist/HolographicCard.mjs +191 -0
  80. package/dist/IconButton.d.mts +27 -0
  81. package/dist/IconButton.d.ts +27 -0
  82. package/dist/IconButton.js +400 -0
  83. package/dist/IconButton.mjs +8 -0
  84. package/dist/ImageViewer.d.mts +23 -0
  85. package/dist/ImageViewer.d.ts +23 -0
  86. package/dist/ImageViewer.js +582 -0
  87. package/dist/ImageViewer.mjs +8 -0
  88. package/dist/Input.d.mts +23 -0
  89. package/dist/Input.d.ts +23 -0
  90. package/dist/Input.js +351 -0
  91. package/dist/Input.mjs +7 -0
  92. package/dist/LabelValue.d.mts +16 -0
  93. package/dist/LabelValue.d.ts +16 -0
  94. package/dist/LabelValue.js +225 -0
  95. package/dist/LabelValue.mjs +5 -0
  96. package/dist/ListGroup.d.mts +34 -0
  97. package/dist/ListGroup.d.ts +34 -0
  98. package/dist/ListGroup.js +217 -0
  99. package/dist/ListGroup.mjs +5 -0
  100. package/dist/ListItem.d.mts +64 -0
  101. package/dist/ListItem.d.ts +64 -0
  102. package/dist/ListItem.js +444 -0
  103. package/dist/ListItem.mjs +9 -0
  104. package/dist/MediaCard.d.mts +39 -0
  105. package/dist/MediaCard.d.ts +39 -0
  106. package/dist/MediaCard.js +475 -0
  107. package/dist/MediaCard.mjs +9 -0
  108. package/dist/MenuGroup.d.mts +34 -0
  109. package/dist/MenuGroup.d.ts +34 -0
  110. package/dist/MenuGroup.js +217 -0
  111. package/dist/MenuGroup.mjs +5 -0
  112. package/dist/MenuItem.d.mts +48 -0
  113. package/dist/MenuItem.d.ts +48 -0
  114. package/dist/MenuItem.js +415 -0
  115. package/dist/MenuItem.mjs +9 -0
  116. package/dist/MonthPicker.d.mts +28 -0
  117. package/dist/MonthPicker.d.ts +28 -0
  118. package/dist/MonthPicker.js +297 -0
  119. package/dist/MonthPicker.mjs +5 -0
  120. package/dist/PagerDots.d.mts +35 -0
  121. package/dist/PagerDots.d.ts +35 -0
  122. package/dist/PagerDots.js +392 -0
  123. package/dist/PagerDots.mjs +7 -0
  124. package/dist/Pressable.d.mts +34 -0
  125. package/dist/Pressable.d.ts +34 -0
  126. package/dist/Pressable.js +143 -0
  127. package/dist/Pressable.mjs +5 -0
  128. package/dist/PricingCard.d.mts +50 -0
  129. package/dist/PricingCard.d.ts +50 -0
  130. package/dist/PricingCard.js +636 -0
  131. package/dist/PricingCard.mjs +11 -0
  132. package/dist/Progress.d.mts +14 -0
  133. package/dist/Progress.d.ts +14 -0
  134. package/dist/Progress.js +191 -0
  135. package/dist/Progress.mjs +5 -0
  136. package/dist/RadioGroup.d.mts +19 -0
  137. package/dist/RadioGroup.d.ts +19 -0
  138. package/dist/RadioGroup.js +392 -0
  139. package/dist/RadioGroup.mjs +7 -0
  140. package/dist/RetrayProvider.d.mts +2 -0
  141. package/dist/RetrayProvider.d.ts +2 -0
  142. package/dist/RetrayProvider.js +214 -0
  143. package/dist/RetrayProvider.mjs +5 -0
  144. package/dist/Select.d.mts +22 -0
  145. package/dist/Select.d.ts +22 -0
  146. package/dist/Select.js +488 -0
  147. package/dist/Select.mjs +7 -0
  148. package/dist/SelectableGrid.d.mts +44 -0
  149. package/dist/SelectableGrid.d.ts +44 -0
  150. package/dist/SelectableGrid.js +448 -0
  151. package/dist/SelectableGrid.mjs +9 -0
  152. package/dist/Separator.d.mts +10 -0
  153. package/dist/Separator.d.ts +10 -0
  154. package/dist/Separator.js +156 -0
  155. package/dist/Separator.mjs +3 -0
  156. package/dist/Sheet.d.mts +93 -0
  157. package/dist/Sheet.d.ts +93 -0
  158. package/dist/Sheet.js +450 -0
  159. package/dist/Sheet.mjs +6 -0
  160. package/dist/Skeleton.d.mts +67 -0
  161. package/dist/Skeleton.d.ts +67 -0
  162. package/dist/Skeleton.js +266 -0
  163. package/dist/Skeleton.mjs +6 -0
  164. package/dist/Slider.d.mts +20 -0
  165. package/dist/Slider.d.ts +20 -0
  166. package/dist/Slider.js +279 -0
  167. package/dist/Slider.mjs +5 -0
  168. package/dist/Spinner.d.mts +12 -0
  169. package/dist/Spinner.d.ts +12 -0
  170. package/dist/Spinner.js +193 -0
  171. package/dist/Spinner.mjs +4 -0
  172. package/dist/Switch.d.mts +13 -0
  173. package/dist/Switch.d.ts +13 -0
  174. package/dist/Switch.js +311 -0
  175. package/dist/Switch.mjs +6 -0
  176. package/dist/TabBar.d.mts +42 -0
  177. package/dist/TabBar.d.ts +42 -0
  178. package/dist/TabBar.js +361 -0
  179. package/dist/TabBar.mjs +6 -0
  180. package/dist/Tabs.d.mts +27 -0
  181. package/dist/Tabs.d.ts +27 -0
  182. package/dist/Tabs.js +419 -0
  183. package/dist/Tabs.mjs +7 -0
  184. package/dist/Text.d.mts +12 -0
  185. package/dist/Text.d.ts +12 -0
  186. package/dist/Text.js +327 -0
  187. package/dist/Text.mjs +5 -0
  188. package/dist/Textarea.d.mts +16 -0
  189. package/dist/Textarea.d.ts +16 -0
  190. package/dist/Textarea.js +333 -0
  191. package/dist/Textarea.mjs +7 -0
  192. package/dist/Toast.d.mts +47 -0
  193. package/dist/Toast.d.ts +47 -0
  194. package/dist/Toast.js +185 -0
  195. package/dist/Toast.mjs +4 -0
  196. package/dist/Toggle.d.mts +36 -0
  197. package/dist/Toggle.d.ts +36 -0
  198. package/dist/Toggle.js +412 -0
  199. package/dist/Toggle.mjs +8 -0
  200. package/dist/VirtualList.d.mts +19 -0
  201. package/dist/VirtualList.d.ts +19 -0
  202. package/dist/VirtualList.js +38 -0
  203. package/dist/VirtualList.mjs +2 -0
  204. package/dist/chunk-26BCI223.mjs +14 -0
  205. package/dist/chunk-2CE3TQVY.mjs +11 -0
  206. package/dist/chunk-2TFTAWVJ.mjs +131 -0
  207. package/dist/chunk-2UYENBLV.mjs +49 -0
  208. package/dist/chunk-3BBOZ3OQ.mjs +41 -0
  209. package/dist/chunk-3DKJ2GIC.mjs +30 -0
  210. package/dist/chunk-3U4SSNWP.mjs +120 -0
  211. package/dist/chunk-4I7D47FH.mjs +139 -0
  212. package/dist/chunk-4K625MVM.mjs +142 -0
  213. package/dist/chunk-6OAZJ577.mjs +98 -0
  214. package/dist/chunk-6Q64UFIA.mjs +71 -0
  215. package/dist/chunk-756RAKE4.mjs +145 -0
  216. package/dist/chunk-7QHVVCB3.mjs +115 -0
  217. package/dist/chunk-A3A6KNQN.mjs +245 -0
  218. package/dist/chunk-A4MDAP7G.mjs +42 -0
  219. package/dist/chunk-AJ7ZDNBT.mjs +120 -0
  220. package/dist/chunk-AV4EMIRH.mjs +94 -0
  221. package/dist/chunk-AZJF2BLK.mjs +115 -0
  222. package/dist/chunk-BNP626TY.mjs +159 -0
  223. package/dist/chunk-BRKYVJVV.mjs +60 -0
  224. package/dist/chunk-DVK4G2GT.mjs +59 -0
  225. package/dist/chunk-EH745HE5.mjs +127 -0
  226. package/dist/chunk-EJ7ZPXOH.mjs +163 -0
  227. package/dist/chunk-GD6KXMG5.mjs +106 -0
  228. package/dist/chunk-GQYFLP3D.mjs +187 -0
  229. package/dist/chunk-ID72TK46.mjs +111 -0
  230. package/dist/chunk-IRRY3CRZ.mjs +82 -0
  231. package/dist/chunk-JB67UOB5.mjs +92 -0
  232. package/dist/chunk-JMOZEC77.mjs +90 -0
  233. package/dist/chunk-JT7HKXRB.mjs +114 -0
  234. package/dist/chunk-KIHCWCWL.mjs +124 -0
  235. package/dist/chunk-LXJIIOYQ.mjs +104 -0
  236. package/dist/chunk-M6ZXVBTK.mjs +64 -0
  237. package/dist/chunk-MAC465BB.mjs +61 -0
  238. package/dist/chunk-MBMXYJJV.mjs +36 -0
  239. package/dist/chunk-MLF3EZFW.mjs +119 -0
  240. package/dist/chunk-MX6HRKMI.mjs +29 -0
  241. package/dist/chunk-NA7PARID.mjs +147 -0
  242. package/dist/chunk-NC5ZTR2Y.mjs +32 -0
  243. package/dist/chunk-O3HA6TYM.mjs +139 -0
  244. package/dist/chunk-OB4JUQ3O.mjs +51 -0
  245. package/dist/chunk-PFZTM6D5.mjs +238 -0
  246. package/dist/chunk-QKH5ZOD5.mjs +97 -0
  247. package/dist/chunk-QY3X2UYR.mjs +191 -0
  248. package/dist/chunk-SOA2Z4RB.mjs +82 -0
  249. package/dist/chunk-SOYNZDVY.mjs +151 -0
  250. package/dist/chunk-T7XZ7H7Y.mjs +57 -0
  251. package/dist/chunk-TERDKCLE.mjs +74 -0
  252. package/dist/chunk-UREA2GYY.mjs +113 -0
  253. package/dist/chunk-VGTDN7SW.mjs +164 -0
  254. package/dist/chunk-VQ57HWPL.mjs +144 -0
  255. package/dist/chunk-WBOOUHSS.mjs +62 -0
  256. package/dist/chunk-WJLKJMKR.mjs +78 -0
  257. package/dist/chunk-X4G6APW6.mjs +134 -0
  258. package/dist/chunk-Y6FXYEAI.mjs +8 -0
  259. package/dist/chunk-YFZ3ELX5.mjs +16 -0
  260. package/dist/chunk-YNROWHQJ.mjs +46 -0
  261. package/dist/chunk-Z4BVUWW6.mjs +196 -0
  262. package/dist/chunk-ZJKGQMYH.mjs +131 -0
  263. package/dist/index-wt-orHUi.d.mts +85 -0
  264. package/dist/index-wt-orHUi.d.ts +85 -0
  265. package/dist/index.d.mts +149 -920
  266. package/dist/index.d.ts +149 -920
  267. package/dist/index.js +2560 -970
  268. package/dist/index.mjs +60 -3895
  269. package/package.json +55 -16
  270. package/src/assets/fonts/Sohne-Bold.otf +0 -0
  271. package/src/assets/fonts/Sohne-BoldItalic.otf +0 -0
  272. package/src/assets/fonts/Sohne-ExtraBold.otf +0 -0
  273. package/src/assets/fonts/Sohne-ExtraBoldItalic.otf +0 -0
  274. package/src/assets/fonts/Sohne-ExtraLight.otf +0 -0
  275. package/src/assets/fonts/Sohne-ExtraLightItalic.otf +0 -0
  276. package/src/assets/fonts/Sohne-Italic.otf +0 -0
  277. package/src/assets/fonts/Sohne-Light.otf +0 -0
  278. package/src/assets/fonts/Sohne-LightItalic.otf +0 -0
  279. package/src/assets/fonts/Sohne-Medium.otf +0 -0
  280. package/src/assets/fonts/Sohne-MediumItalic.otf +0 -0
  281. package/src/assets/fonts/Sohne-Regular.otf +0 -0
  282. package/src/assets/fonts/Sohne-SemiBold.otf +0 -0
  283. package/src/assets/fonts/Sohne-SemiBoldItalic.otf +0 -0
  284. package/src/assets/fonts/SohneMono-Bold.otf +0 -0
  285. package/src/assets/fonts/SohneMono-BoldItalic.otf +0 -0
  286. package/src/assets/fonts/SohneMono-ExtraBold.otf +0 -0
  287. package/src/assets/fonts/SohneMono-ExtraBoldItalic.otf +0 -0
  288. package/src/assets/fonts/SohneMono-ExtraLight.otf +0 -0
  289. package/src/assets/fonts/SohneMono-ExtraLightItalic.otf +0 -0
  290. package/src/assets/fonts/SohneMono-Italic.otf +0 -0
  291. package/src/assets/fonts/SohneMono-Light.otf +0 -0
  292. package/src/assets/fonts/SohneMono-LightItalic.otf +0 -0
  293. package/src/assets/fonts/SohneMono-Medium.otf +0 -0
  294. package/src/assets/fonts/SohneMono-MediumItalic.otf +0 -0
  295. package/src/assets/fonts/SohneMono-Regular.otf +0 -0
  296. package/src/assets/fonts/SohneMono-SemiBold.otf +0 -0
  297. package/src/assets/fonts/SohneMono-SemiBoldItalic.otf +0 -0
  298. package/src/components/Accordion/Accordion.tsx +15 -4
  299. package/src/components/AlertBanner/AlertBanner.tsx +38 -12
  300. package/src/components/AppHeader/AppHeader.tsx +172 -0
  301. package/src/components/AppHeader/index.ts +1 -0
  302. package/src/components/Avatar/Avatar.tsx +14 -4
  303. package/src/components/Badge/Badge.tsx +12 -3
  304. package/src/components/Button/Button.tsx +30 -38
  305. package/src/components/ButtonGroup/ButtonGroup.tsx +13 -10
  306. package/src/components/Card/Card.tsx +29 -57
  307. package/src/components/CategoryStrip/CategoryStrip.tsx +41 -42
  308. package/src/components/Checkbox/Checkbox.tsx +36 -45
  309. package/src/components/Chip/Chip.tsx +41 -48
  310. package/src/components/ConfirmDialog/ConfirmDialog.tsx +2 -2
  311. package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +4 -2
  312. package/src/components/CurrencyInput/CurrencyInput.tsx +12 -10
  313. package/src/components/DetailRow/DetailRow.tsx +9 -7
  314. package/src/components/EmptyState/EmptyState.tsx +4 -3
  315. package/src/components/ErrorBoundary/ErrorBoundary.tsx +153 -0
  316. package/src/components/ErrorBoundary/index.ts +1 -0
  317. package/src/components/Form/Form.tsx +149 -0
  318. package/src/components/Form/index.ts +1 -0
  319. package/src/components/HolographicCard/HolographicCard.tsx +315 -0
  320. package/src/components/HolographicCard/index.ts +1 -0
  321. package/src/components/IconButton/IconButton.tsx +23 -29
  322. package/src/components/ImageViewer/ImageViewer.tsx +290 -0
  323. package/src/components/ImageViewer/index.ts +1 -0
  324. package/src/components/Input/Input.tsx +27 -31
  325. package/src/components/LabelValue/LabelValue.tsx +6 -4
  326. package/src/components/ListGroup/ListGroup.tsx +145 -0
  327. package/src/components/ListGroup/index.ts +1 -0
  328. package/src/components/ListItem/ListItem.tsx +78 -76
  329. package/src/components/MediaCard/MediaCard.tsx +15 -7
  330. package/src/components/MenuGroup/MenuGroup.tsx +145 -0
  331. package/src/components/MenuGroup/index.ts +1 -0
  332. package/src/components/MenuItem/MenuItem.tsx +16 -33
  333. package/src/components/MonthPicker/MonthPicker.tsx +41 -15
  334. package/src/components/MonthPicker/index.ts +1 -1
  335. package/src/components/PagerDots/PagerDots.tsx +200 -0
  336. package/src/components/PagerDots/index.ts +1 -0
  337. package/src/components/Pressable/Pressable.tsx +19 -35
  338. package/src/components/PricingCard/PricingCard.tsx +220 -0
  339. package/src/components/PricingCard/index.ts +1 -0
  340. package/src/components/RadioGroup/RadioGroup.tsx +23 -39
  341. package/src/components/RetrayProvider/RetrayProvider.tsx +59 -0
  342. package/src/components/RetrayProvider/index.ts +1 -0
  343. package/src/components/Select/Select.tsx +6 -6
  344. package/src/components/SelectableGrid/SelectableGrid.tsx +205 -0
  345. package/src/components/SelectableGrid/index.ts +1 -0
  346. package/src/components/Separator/Separator.tsx +1 -3
  347. package/src/components/Sheet/Sheet.tsx +146 -18
  348. package/src/components/Skeleton/Skeleton.tsx +143 -2
  349. package/src/components/Slider/Slider.tsx +2 -2
  350. package/src/components/Spinner/Spinner.tsx +18 -3
  351. package/src/components/Switch/Switch.tsx +44 -49
  352. package/src/components/TabBar/TabBar.tsx +169 -0
  353. package/src/components/TabBar/index.ts +1 -0
  354. package/src/components/Tabs/Tabs.tsx +45 -44
  355. package/src/components/Text/Text.tsx +5 -1
  356. package/src/components/Textarea/Textarea.tsx +18 -14
  357. package/src/components/Toast/Toast.tsx +6 -6
  358. package/src/components/Toggle/Toggle.tsx +80 -72
  359. package/src/components/VirtualList/VirtualList.tsx +60 -0
  360. package/src/components/VirtualList/index.ts +1 -0
  361. package/src/fonts.ts +41 -20
  362. package/src/index.ts +28 -3
  363. package/src/theme/colors.ts +53 -39
  364. package/src/theme/types.ts +3 -0
  365. package/src/tokens.ts +49 -39
  366. package/src/utils/animations.ts +29 -1
  367. package/src/utils/fontGuard.ts +34 -0
  368. package/src/utils/haptics.ts +211 -9
  369. package/src/utils/icons.ts +47 -20
  370. package/src/utils/pressable.ts +66 -0
  371. package/src/utils/usePressScale.ts +2 -0
  372. package/src/assets/fonts/Poppins-Black.ttf +0 -0
  373. package/src/assets/fonts/Poppins-BlackItalic.ttf +0 -0
  374. package/src/assets/fonts/Poppins-Bold.ttf +0 -0
  375. package/src/assets/fonts/Poppins-BoldItalic.ttf +0 -0
  376. package/src/assets/fonts/Poppins-ExtraBold.ttf +0 -0
  377. package/src/assets/fonts/Poppins-ExtraBoldItalic.ttf +0 -0
  378. package/src/assets/fonts/Poppins-ExtraLight.ttf +0 -0
  379. package/src/assets/fonts/Poppins-ExtraLightItalic.ttf +0 -0
  380. package/src/assets/fonts/Poppins-Italic.ttf +0 -0
  381. package/src/assets/fonts/Poppins-Light.ttf +0 -0
  382. package/src/assets/fonts/Poppins-LightItalic.ttf +0 -0
  383. package/src/assets/fonts/Poppins-Medium.ttf +0 -0
  384. package/src/assets/fonts/Poppins-MediumItalic.ttf +0 -0
  385. package/src/assets/fonts/Poppins-Regular.ttf +0 -0
  386. package/src/assets/fonts/Poppins-SemiBold.ttf +0 -0
  387. package/src/assets/fonts/Poppins-SemiBoldItalic.ttf +0 -0
  388. package/src/assets/fonts/Poppins-Thin.ttf +0 -0
  389. package/src/assets/fonts/Poppins-ThinItalic.ttf +0 -0
@@ -101,11 +101,11 @@ const styles = StyleSheet.create({
101
101
  alignItems: 'center',
102
102
  },
103
103
  label: {
104
- fontFamily: 'Poppins-Medium',
104
+ fontFamily: 'Sohne-Medium',
105
105
  fontSize: ms(15),
106
106
  },
107
107
  valueText: {
108
- fontFamily: 'Poppins-Medium',
108
+ fontFamily: 'Sohne-Medium',
109
109
  fontSize: ms(14),
110
110
  },
111
111
  slider: {
@@ -25,10 +25,16 @@ const labelFontSize: Record<SpinnerSize, number> = {
25
25
 
26
26
  export function Spinner({ size = 'md', color, label, ...props }: SpinnerProps) {
27
27
  const { colors } = useTheme()
28
+ const a11yLabel = label || 'Loading'
28
29
 
29
30
  if (label) {
30
31
  return (
31
- <View style={styles.wrapper}>
32
+ <View
33
+ style={styles.wrapper}
34
+ accessibilityRole="progressbar"
35
+ accessibilityLabel={a11yLabel}
36
+ accessibilityState={{ busy: true }}
37
+ >
32
38
  <ActivityIndicator size={sizeMap[size]} color={color ?? colors.primary} {...props} />
33
39
  <Text
34
40
  style={[styles.label, { color: colors.foregroundMuted, fontSize: labelFontSize[size] }]}
@@ -40,7 +46,16 @@ export function Spinner({ size = 'md', color, label, ...props }: SpinnerProps) {
40
46
  )
41
47
  }
42
48
 
43
- return <ActivityIndicator size={sizeMap[size]} color={color ?? colors.primary} {...props} />
49
+ return (
50
+ <ActivityIndicator
51
+ size={sizeMap[size]}
52
+ color={color ?? colors.primary}
53
+ accessibilityRole="progressbar"
54
+ accessibilityLabel={a11yLabel}
55
+ accessibilityState={{ busy: true }}
56
+ {...props}
57
+ />
58
+ )
44
59
  }
45
60
 
46
61
  const styles = StyleSheet.create({
@@ -49,7 +64,7 @@ const styles = StyleSheet.create({
49
64
  gap: vs(6),
50
65
  },
51
66
  label: {
52
- fontFamily: 'Poppins-Regular',
67
+ fontFamily: 'Sohne-Regular',
53
68
  lineHeight: mvs(18),
54
69
  },
55
70
  })
@@ -1,24 +1,18 @@
1
- import React, { useEffect } from 'react'
1
+ import React from 'react'
2
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
+ import { EaseView } from 'react-native-ease'
10
4
  import { Feather } from '@expo/vector-icons'
11
5
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
12
6
  import { useTheme } from '../../theme'
13
7
  import { s } from '../../utils/scaling'
14
- import { SPRINGS, TIMINGS, EASINGS } from '../../utils/animations'
8
+ import { COLOR_TRANSITION, OPACITY_TRANSITION, SPRING_ELASTIC } from '../../utils/animations'
15
9
 
16
- const TRACK_WIDTH = s(52)
10
+ const TRACK_WIDTH = s(52)
17
11
  const TRACK_HEIGHT = s(30)
18
- const THUMB_SIZE = s(24)
12
+ const THUMB_SIZE = s(24)
19
13
  const THUMB_OFFSET = s(3)
20
14
  const THUMB_TRAVEL = TRACK_WIDTH - THUMB_SIZE - THUMB_OFFSET * 2
21
- const ICON_SIZE = s(13)
15
+ const ICON_SIZE = s(13)
22
16
 
23
17
  export interface SwitchProps {
24
18
  checked?: boolean
@@ -31,35 +25,8 @@ export interface SwitchProps {
31
25
  export function Switch({ checked = false, onCheckedChange, disabled, style, accessibilityLabel }: SwitchProps) {
32
26
  const { colors } = useTheme()
33
27
 
34
- // Single 0→1 progress drives thumb position, track color, and icon crossfade — all UI thread.
35
- const progress = useSharedValue(checked ? 1 : 0)
36
-
37
- useEffect(() => {
38
- progress.value = withSpring(checked ? 1 : 0, SPRINGS.elastic)
39
- }, [checked, progress])
40
-
41
- const thumbStyle = useAnimatedStyle(() => ({
42
- transform: [{ translateX: progress.value * THUMB_TRAVEL }],
43
- }))
44
-
45
- const trackStyle = useAnimatedStyle(() => ({
46
- backgroundColor: interpolateColor(
47
- progress.value,
48
- [0, 1],
49
- [colors.surfaceStrong, colors.primary],
50
- ),
51
- }))
52
-
53
- const checkIconStyle = useAnimatedStyle(() => ({
54
- opacity: withTiming(checked ? 1 : 0, { duration: TIMINGS.state.duration, easing: EASINGS.standard }),
55
- }))
56
-
57
- const crossIconStyle = useAnimatedStyle(() => ({
58
- opacity: withTiming(checked ? 0 : 1, { duration: TIMINGS.state.duration, easing: EASINGS.standard }),
59
- }))
60
-
61
28
  return (
62
- <View style={[{ opacity: disabled ? 0.45 : 1 }, style]}>
29
+ <View style={[{ opacity: disabled ? 0.45 : 1, alignSelf: 'flex-start' }, style]}>
63
30
  <TouchableOpacity
64
31
  onPress={() => {
65
32
  hapticSelection()
@@ -71,30 +38,56 @@ export function Switch({ checked = false, onCheckedChange, disabled, style, acce
71
38
  accessibilityRole="switch"
72
39
  accessibilityLabel={accessibilityLabel}
73
40
  accessibilityState={{ checked, disabled: !!disabled }}
41
+ style={styles.touchable}
74
42
  >
75
- <Animated.View style={[styles.track, trackStyle]}>
76
- <Animated.View
77
- style={[styles.thumb, { backgroundColor: colors.primaryForeground }, thumbStyle]}
43
+ <EaseView
44
+ style={styles.track}
45
+ animate={{ backgroundColor: checked ? colors.primary : colors.surfaceStrong }}
46
+ transition={COLOR_TRANSITION}
47
+ >
48
+ {/*
49
+ AUDIT FIX: the off-state track used surfaceStrong (~#ebebeb in light mode)
50
+ with no border — nearly invisible on white page/card surfaces. A 1.5px border
51
+ that fades out as the track fills gives the off state clear visual definition
52
+ without adding visual weight to the on state.
53
+ */}
54
+ <EaseView
55
+ style={[styles.trackBorder, { borderWidth: 1.5 }]}
56
+ pointerEvents="none"
57
+ animate={{ borderColor: checked ? 'transparent' : colors.border }}
58
+ transition={COLOR_TRANSITION}
59
+ />
60
+ <EaseView
61
+ style={[styles.thumb, { backgroundColor: colors.primaryForeground }]}
62
+ animate={{ translateX: checked ? THUMB_TRAVEL : 0 }}
63
+ transition={SPRING_ELASTIC}
78
64
  >
79
- <Animated.View style={[styles.iconWrapper, checkIconStyle]}>
65
+ <EaseView style={styles.iconWrapper} animate={{ opacity: checked ? 1 : 0 }} transition={OPACITY_TRANSITION}>
80
66
  <Feather name="check" size={ICON_SIZE} color={colors.primary} />
81
- </Animated.View>
82
- <Animated.View style={[styles.iconWrapper, crossIconStyle]}>
67
+ </EaseView>
68
+ <EaseView style={styles.iconWrapper} animate={{ opacity: checked ? 0 : 1 }} transition={OPACITY_TRANSITION}>
83
69
  <Feather name="x" size={ICON_SIZE} color={colors.foregroundMuted} />
84
- </Animated.View>
85
- </Animated.View>
86
- </Animated.View>
70
+ </EaseView>
71
+ </EaseView>
72
+ </EaseView>
87
73
  </TouchableOpacity>
88
74
  </View>
89
75
  )
90
76
  }
91
77
 
92
78
  const styles = StyleSheet.create({
79
+ touchable: {
80
+ alignSelf: 'flex-start',
81
+ },
93
82
  track: {
94
83
  width: TRACK_WIDTH,
95
84
  height: TRACK_HEIGHT,
96
85
  borderRadius: TRACK_HEIGHT / 2,
97
86
  },
87
+ trackBorder: {
88
+ ...StyleSheet.absoluteFillObject,
89
+ borderRadius: TRACK_HEIGHT / 2,
90
+ },
98
91
  thumb: {
99
92
  position: 'absolute',
100
93
  top: THUMB_OFFSET,
@@ -112,5 +105,7 @@ const styles = StyleSheet.create({
112
105
  },
113
106
  iconWrapper: {
114
107
  position: 'absolute',
108
+ alignItems: 'center',
109
+ justifyContent: 'center',
115
110
  },
116
111
  })
@@ -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
- import React, { useState, useRef, useEffect } from 'react'
2
- import { View, TouchableOpacity, Text, StyleSheet, ViewStyle, LayoutChangeEvent } from 'react-native'
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react'
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
@@ -17,8 +17,6 @@ export interface TabItem {
17
17
  icon?: React.ReactNode | ((active: boolean) => React.ReactNode)
18
18
  }
19
19
 
20
- // pill: animated sliding pill background (default)
21
- // underline: 2px bottom border on active tab — Airbnb product-tab style
22
20
  export type TabsVariant = 'pill' | 'underline'
23
21
 
24
22
  export interface TabsProps {
@@ -51,46 +49,43 @@ function TabTrigger({
51
49
  variant: TabsVariant
52
50
  }) {
53
51
  const { colors } = useTheme()
54
- const { animatedStyle, onPressIn, onPressOut } = usePressScale({
55
- pressScale: PRESS_SCALE.button,
56
- })
57
52
  const isUnderline = variant === 'underline'
58
53
 
59
54
  return (
60
- <TouchableOpacity
61
- style={[
62
- styles.trigger,
63
- isUnderline && styles.triggerUnderline,
64
- isUnderline && isActive && { borderBottomColor: colors.primary },
65
- ]}
66
- onPress={onPress}
67
- onPressIn={onPressIn}
68
- onPressOut={onPressOut}
69
- onLayout={onLayout}
70
- activeOpacity={1}
71
- touchSoundDisabled={true}
72
- accessibilityRole="tab"
73
- accessibilityState={{ selected: isActive }}
74
- accessibilityLabel={tab.label}
75
- >
76
- <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
+ >
77
69
  <View style={styles.triggerInner}>
78
70
  {tab.icon ? (
79
- (typeof tab.icon === 'function' ? (tab.icon as any)(isActive) : tab.icon) as React.ReactNode
71
+ typeof tab.icon === 'function' ? tab.icon(isActive) : tab.icon
80
72
  ) : null}
81
73
  <Text
82
74
  style={[
83
75
  styles.triggerLabel,
76
+ // AUDIT FIX: active state now only changes color, never font metrics.
77
+ // Previously: inactive=Regular, active=Medium (pill) or SemiBold+fontSize14 (underline)
78
+ // The weight/size change caused measurable layout reflow every tab switch.
79
+ // Solution: all labels render at SemiBold always; active = foreground, inactive = foregroundMuted.
84
80
  { color: isActive ? colors.foreground : colors.foregroundMuted },
85
- isActive && (isUnderline ? styles.activeTriggerLabelUnderline : styles.activeTriggerLabel),
86
81
  ]}
87
82
  allowFontScaling={true}
88
83
  >
89
84
  {tab.label}
90
85
  </Text>
91
86
  </View>
92
- </Animated.View>
93
- </TouchableOpacity>
87
+ </PressableTab>
88
+ </View>
94
89
  )
95
90
  }
96
91
 
@@ -100,26 +95,27 @@ export function Tabs({ tabs, variant = 'pill', value, onValueChange, children, s
100
95
  const active = value ?? internal
101
96
 
102
97
  const tabLayouts = useRef<Record<string, { x: number; width: number }>>({})
103
- // Shared values drive the pill position on the UI thread — no JS bridge cost on slide.
104
98
  const pillX = useSharedValue(0)
105
99
  const pillWidth = useSharedValue(0)
106
100
  const initialised = useRef(false)
107
101
 
108
- const animatePill = (tabValue: string, animate: boolean) => {
102
+ const animatePill = useCallback((tabValue: string, animate: boolean) => {
109
103
  const layout = tabLayouts.current[tabValue]
110
104
  if (!layout) return
111
105
  if (animate) {
106
+ // eslint-disable-next-line react-hooks/immutability
112
107
  pillX.value = withSpring(layout.x, SPRINGS.glide)
108
+ // eslint-disable-next-line react-hooks/immutability
113
109
  pillWidth.value = withSpring(layout.width, SPRINGS.glide)
114
110
  } else {
115
111
  pillX.value = layout.x
116
112
  pillWidth.value = layout.width
117
113
  }
118
- }
114
+ }, [pillX, pillWidth])
119
115
 
120
116
  useEffect(() => {
121
117
  if (initialised.current) animatePill(active, true)
122
- }, [active])
118
+ }, [active, animatePill])
123
119
 
124
120
  const handlePress = (v: string) => {
125
121
  hapticSelection()
@@ -136,7 +132,9 @@ export function Tabs({ tabs, variant = 'pill', value, onValueChange, children, s
136
132
  <View style={style}>
137
133
  <View
138
134
  style={[
139
- variant === 'pill' ? [styles.list, { backgroundColor: colors.surface }] : styles.listUnderline,
135
+ variant === 'pill'
136
+ ? [styles.list, { backgroundColor: colors.surface }]
137
+ : styles.listUnderline,
140
138
  ]}
141
139
  accessibilityRole="tablist"
142
140
  >
@@ -198,9 +196,14 @@ const styles = StyleSheet.create({
198
196
  },
199
197
  listUnderline: {
200
198
  flexDirection: 'row',
199
+ // AUDIT FIX: was missing borderBottomColor — the 1px hairline would render
200
+ // as transparent on some platforms. Explicit token reference ensures visibility.
201
201
  borderBottomWidth: 1,
202
202
  },
203
203
  pill: {},
204
+ triggerWrap: {
205
+ flex: 1,
206
+ },
204
207
  trigger: {
205
208
  flex: 1,
206
209
  paddingVertical: vs(7),
@@ -211,7 +214,7 @@ const styles = StyleSheet.create({
211
214
  zIndex: 1,
212
215
  },
213
216
  triggerUnderline: {
214
- flex: 0,
217
+ flex: 1,
215
218
  paddingVertical: vs(12),
216
219
  paddingHorizontal: s(16),
217
220
  borderRadius: 0,
@@ -224,15 +227,13 @@ const styles = StyleSheet.create({
224
227
  justifyContent: 'center',
225
228
  gap: s(4),
226
229
  },
230
+ // AUDIT FIX: was Sohne-Regular at rest, Sohne-Medium/SemiBold when active.
231
+ // Font-weight changes at runtime cause advance-width shifts → the tab bar would
232
+ // visibly jump/reflow on every selection. Now always SemiBold; active state
233
+ // is communicated by color alone (foreground vs foregroundMuted). The pill
234
+ // indicator provides additional active signal without text layout side-effects.
227
235
  triggerLabel: {
228
- fontFamily: 'Poppins-Regular',
236
+ fontFamily: 'Sohne-SemiBold',
229
237
  fontSize: ms(13),
230
238
  },
231
- activeTriggerLabel: {
232
- fontFamily: 'Poppins-Medium',
233
- },
234
- activeTriggerLabelUnderline: {
235
- fontFamily: 'Poppins-SemiBold',
236
- fontSize: ms(14),
237
- },
238
239
  })
@@ -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'
@@ -67,7 +68,8 @@ const defaultColorVariant: Partial<Record<TextVariant, 'foreground' | 'foregroun
67
68
  'button-sm': 'foreground',
68
69
  }
69
70
 
70
- export function Text({ variant = 'body-md', color, style, children, ...props }: TextProps) {
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'
@@ -83,3 +85,5 @@ export function Text({ variant = 'body-md', color, style, children, ...props }:
83
85
  </RNText>
84
86
  )
85
87
  }
88
+
89
+ export const Text = React.memo(TextBase)
@@ -3,6 +3,7 @@ import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, Platform
3
3
  import Animated, {
4
4
  useAnimatedStyle,
5
5
  interpolateColor,
6
+ interpolate,
6
7
  } from 'react-native-reanimated'
7
8
  import { useTheme } from '../../theme'
8
9
  import { s, vs, ms } from '../../utils/scaling'
@@ -10,26 +11,19 @@ import { renderIcon } from '../../utils/icons'
10
11
  import { useColorTransition } from '../../utils/useColorTransition'
11
12
  import { TIMINGS } from '../../utils/animations'
12
13
 
13
- const webInputResetStyle: any =
14
+ const webInputResetStyle: Record<string, unknown> =
14
15
  Platform.OS === 'web'
15
16
  ? { outlineStyle: 'none', outlineWidth: 0, outlineColor: 'transparent', boxShadow: 'none' }
16
17
  : {}
17
18
 
18
19
  export interface TextareaProps extends TextInputProps {
19
20
  label?: string
20
- /** Red helper text below the textarea; also changes border to `destructive` color. Takes priority over `hint`. */
21
21
  error?: string
22
- /** Helper text shown below the textarea when there is no error. */
23
22
  hint?: string
24
- /** Number of visible text rows. Defaults to `4`. Controls `numberOfLines` and `minHeight`. */
25
23
  rows?: number
26
- /** Icon name from @expo/vector-icons rendered inside top-left corner. */
27
24
  prefixIcon?: string
28
- /** Custom icon node rendered top-left. */
29
25
  prefixIconNode?: React.ReactNode
30
- /** Override prefix icon color. Defaults to foregroundMuted. */
31
26
  prefixIconColor?: string
32
- /** Style for the outer container `View`. Use `style` (from `TextInputProps`) to style the `TextInput` itself. */
33
27
  containerStyle?: ViewStyle
34
28
  }
35
29
 
@@ -58,10 +52,15 @@ export function Textarea({
58
52
  ? renderIcon(prefixIcon, ms(16), prefixIconColor ?? colors.foregroundMuted)
59
53
  : prefixIconNode
60
54
 
61
- const borderColorStyle = useAnimatedStyle(() => ({
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(() => ({
62
58
  borderColor: error
63
59
  ? colors.destructive
64
60
  : interpolateColor(focusProgress.value, [0, 1], [colors.border, colors.primary]),
61
+ borderWidth: error
62
+ ? 2
63
+ : interpolate(focusProgress.value, [0, 1], [1, 2]),
65
64
  }))
66
65
 
67
66
  return (
@@ -71,9 +70,9 @@ export function Textarea({
71
70
  style={[
72
71
  styles.inputWrapper,
73
72
  { backgroundColor: colors.background },
74
- borderColorStyle,
75
73
  ]}
76
74
  >
75
+ <Animated.View style={[styles.borderOverlay, borderAnimStyle]} pointerEvents="none" />
77
76
  {resolvedPrefixIcon ? <View style={styles.prefixIcon}>{resolvedPrefixIcon}</View> : null}
78
77
  <TextInput
79
78
  multiline
@@ -123,32 +122,37 @@ const styles = StyleSheet.create({
123
122
  gap: vs(4),
124
123
  },
125
124
  label: {
126
- fontFamily: 'Poppins-Medium',
125
+ fontFamily: 'Sohne-Medium',
127
126
  fontSize: ms(13),
128
127
  lineHeight: vs(18),
129
128
  marginBottom: vs(2),
130
129
  },
131
130
  inputWrapper: {
132
- borderWidth: 2,
131
+ // Border lives on borderOverlay (absolute); wrapper carries none so the
132
+ // focus weight change never reflows content.
133
133
  borderRadius: 8,
134
134
  paddingHorizontal: s(14),
135
135
  paddingVertical: vs(11),
136
136
  gap: s(8),
137
137
  },
138
+ borderOverlay: {
139
+ ...StyleSheet.absoluteFillObject,
140
+ borderRadius: 8,
141
+ },
138
142
  prefixIcon: {
139
143
  alignItems: 'flex-start',
140
144
  justifyContent: 'flex-start',
141
145
  paddingTop: vs(2),
142
146
  },
143
147
  input: {
144
- fontFamily: 'Poppins-Regular',
148
+ fontFamily: 'Sohne-Regular',
145
149
  fontSize: ms(14),
146
150
  lineHeight: vs(22),
147
151
  padding: 0,
148
152
  margin: 0,
149
153
  },
150
154
  helperText: {
151
- fontFamily: 'Poppins-Regular',
155
+ fontFamily: 'Sohne-Regular',
152
156
  fontSize: ms(12),
153
157
  lineHeight: vs(16),
154
158
  marginTop: vs(4),