@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
@@ -60,7 +60,7 @@ export function Select({
60
60
  setPendingValue(value)
61
61
  setPickerVisible(true)
62
62
  } else if (isAndroid) {
63
- ;(pickerRef.current as any)?.focus()
63
+ ;(pickerRef.current as { focus?: () => void })?.focus?.()
64
64
  }
65
65
  }
66
66
 
@@ -234,7 +234,7 @@ const styles = StyleSheet.create({
234
234
  gap: vs(8),
235
235
  },
236
236
  label: {
237
- fontFamily: 'Poppins-Medium',
237
+ fontFamily: 'Sohne-Medium',
238
238
  fontSize: ms(13),
239
239
  },
240
240
  trigger: {
@@ -247,7 +247,7 @@ const styles = StyleSheet.create({
247
247
  paddingVertical: vs(11),
248
248
  },
249
249
  triggerText: {
250
- fontFamily: 'Poppins-Regular',
250
+ fontFamily: 'Sohne-Regular',
251
251
  fontSize: ms(15),
252
252
  flex: 1,
253
253
  },
@@ -255,7 +255,7 @@ const styles = StyleSheet.create({
255
255
  marginLeft: s(8),
256
256
  },
257
257
  helperText: {
258
- fontFamily: 'Poppins-Regular',
258
+ fontFamily: 'Sohne-Regular',
259
259
  fontSize: ms(13),
260
260
  },
261
261
  iosBackdrop: {
@@ -276,14 +276,14 @@ const styles = StyleSheet.create({
276
276
  borderBottomWidth: 1,
277
277
  },
278
278
  iosToolbarTitle: {
279
- fontFamily: 'Poppins-SemiBold',
279
+ fontFamily: 'Sohne-SemiBold',
280
280
  fontSize: ms(17),
281
281
  },
282
282
  iosDoneBtn: {
283
283
  padding: s(4),
284
284
  },
285
285
  iosDoneBtnText: {
286
- fontFamily: 'Poppins-SemiBold',
286
+ fontFamily: 'Sohne-SemiBold',
287
287
  fontSize: ms(17),
288
288
  },
289
289
  androidHiddenPicker: {
@@ -0,0 +1,205 @@
1
+ import React, { useState } from 'react'
2
+ import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, ScrollView } from 'react-native'
3
+ import Animated from 'react-native-reanimated'
4
+ import { useTheme } from '../../theme'
5
+ import { renderIcon } from '../../utils/icons'
6
+ import { usePressScale } from '../../utils/usePressScale'
7
+ import { selectionAsync as hapticSelection } from '../../utils/haptics'
8
+ import { PRESS_SCALE } from '../../utils/animations'
9
+ import { s, vs, ms, mvs } from '../../utils/scaling'
10
+ import { RADIUS } from '../../tokens'
11
+
12
+ export interface SelectableGridItem<T extends string | number = string> {
13
+ /** Unique value emitted on selection. */
14
+ value: T
15
+ /** Label rendered under the icon. */
16
+ label?: string
17
+ /** Icon name resolved via the icon registry. */
18
+ iconName?: string
19
+ /** Custom icon node — overrides `iconName`. */
20
+ icon?: React.ReactNode
21
+ disabled?: boolean
22
+ }
23
+
24
+ export interface SelectableGridProps<T extends string | number = string> {
25
+ items: SelectableGridItem<T>[]
26
+ /** Selected value(s). Array when `multiple`. */
27
+ value: T | T[] | null
28
+ onChange: (value: T) => void
29
+ /** Allow multiple selections. `value` should be an array. Defaults to false. */
30
+ multiple?: boolean
31
+ /** Columns per row. Defaults to 4. Ignored when `orientation='horizontal'`. */
32
+ numColumns?: number
33
+ /** Gap between cells (dp). Defaults to 12. */
34
+ gap?: number
35
+ /** Layout orientation. 'grid' (default) wraps into rows. 'horizontal' creates a single scrollable row. */
36
+ orientation?: 'grid' | 'horizontal'
37
+ style?: ViewStyle
38
+ }
39
+
40
+ function isSelected<T extends string | number>(value: T | T[] | null, candidate: T): boolean {
41
+ if (value == null) return false
42
+ return Array.isArray(value) ? value.includes(candidate) : value === candidate
43
+ }
44
+
45
+ interface CellProps<T extends string | number> {
46
+ item: SelectableGridItem<T>
47
+ selected: boolean
48
+ width: number
49
+ onPress: () => void
50
+ }
51
+
52
+ function Cell<T extends string | number>({ item, selected, width, onPress }: CellProps<T>) {
53
+ const { colors } = useTheme()
54
+ const { animatedStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
55
+ pressScale: PRESS_SCALE.chip,
56
+ disabled: item.disabled,
57
+ })
58
+
59
+ const iconColor = selected ? colors.primary : colors.foregroundSubtle
60
+ const iconNode = item.icon ?? (item.iconName ? renderIcon(item.iconName, ms(24), iconColor) : null)
61
+
62
+ return (
63
+ <Animated.View style={[{ width }, animatedStyle]}>
64
+ <TouchableOpacity
65
+ onPress={onPress}
66
+ onPressIn={onPressIn}
67
+ onPressOut={onPressOut}
68
+ disabled={item.disabled}
69
+ activeOpacity={1}
70
+ touchSoundDisabled={true}
71
+ accessibilityRole="button"
72
+ accessibilityState={{ selected, disabled: item.disabled }}
73
+ accessibilityLabel={item.label ?? String(item.value)}
74
+ {...hoverHandlers}
75
+ style={[
76
+ styles.cell,
77
+ {
78
+ backgroundColor: selected ? colors.primary + '14' : colors.surface,
79
+ borderColor: selected ? colors.primary : 'transparent',
80
+ },
81
+ item.disabled && styles.cellDisabled,
82
+ ]}
83
+ >
84
+ {iconNode}
85
+ {item.label ? (
86
+ <Text
87
+ style={[styles.label, { color: selected ? colors.primary : colors.foreground }]}
88
+ numberOfLines={1}
89
+ allowFontScaling={true}
90
+ >
91
+ {item.label}
92
+ </Text>
93
+ ) : null}
94
+ </TouchableOpacity>
95
+ </Animated.View>
96
+ )
97
+ }
98
+
99
+ /**
100
+ * Grid of selectable cells (icon + label) — for store / category / emoji pickers
101
+ * where a list would be the wrong shape. Single or multi select.
102
+ *
103
+ * @example
104
+ * <SelectableGrid
105
+ * items={categories}
106
+ * value={selected}
107
+ * onChange={setSelected}
108
+ * numColumns={4}
109
+ * />
110
+ */
111
+ export function SelectableGrid<T extends string | number = string>({
112
+ items,
113
+ value,
114
+ onChange,
115
+ multiple = false,
116
+ numColumns = 4,
117
+ gap = 12,
118
+ orientation = 'grid',
119
+ style,
120
+ }: SelectableGridProps<T>) {
121
+ const [containerWidth, setContainerWidth] = useState(0)
122
+ const gapPx = s(gap)
123
+ // Compute exact cell width so `numColumns` always fits — percentage widths + gap
124
+ // overflow and wrap one short. -0.5 guards against sub-pixel rounding overflow.
125
+ const cellWidth = containerWidth > 0 ? (containerWidth - gapPx * (numColumns - 1)) / numColumns - 0.5 : 0
126
+ // Horizontal mode: fixed 72dp cell width (same scale as grid cells)
127
+ const horizCellWidth = s(72)
128
+
129
+ const handlePress = (item: SelectableGridItem<T>) => {
130
+ if (item.disabled) return
131
+ hapticSelection()
132
+ onChange(item.value)
133
+ }
134
+
135
+ if (orientation === 'horizontal') {
136
+ return (
137
+ <ScrollView
138
+ horizontal
139
+ showsHorizontalScrollIndicator={false}
140
+ contentContainerStyle={[styles.horizontal, { gap: gapPx }, style]}
141
+ accessibilityRole={multiple ? undefined : 'radiogroup'}
142
+ >
143
+ {items.map((item) => (
144
+ <Cell
145
+ key={String(item.value)}
146
+ item={item}
147
+ selected={isSelected(value, item.value)}
148
+ width={horizCellWidth}
149
+ onPress={() => handlePress(item)}
150
+ />
151
+ ))}
152
+ </ScrollView>
153
+ )
154
+ }
155
+
156
+ return (
157
+ <View
158
+ style={[styles.grid, { gap: gapPx }, style]}
159
+ onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
160
+ accessibilityRole={multiple ? undefined : 'radiogroup'}
161
+ >
162
+ {cellWidth > 0
163
+ ? items.map((item) => (
164
+ <Cell
165
+ key={String(item.value)}
166
+ item={item}
167
+ selected={isSelected(value, item.value)}
168
+ width={cellWidth}
169
+ onPress={() => handlePress(item)}
170
+ />
171
+ ))
172
+ : null}
173
+ </View>
174
+ )
175
+ }
176
+
177
+ const styles = StyleSheet.create({
178
+ grid: {
179
+ flexDirection: 'row',
180
+ flexWrap: 'wrap',
181
+ },
182
+ horizontal: {
183
+ flexDirection: 'row',
184
+ paddingHorizontal: s(4),
185
+ },
186
+ cell: {
187
+ flex: 1,
188
+ borderRadius: RADIUS.md,
189
+ borderWidth: 2,
190
+ alignItems: 'center',
191
+ justifyContent: 'center',
192
+ gap: vs(4),
193
+ paddingHorizontal: s(12),
194
+ paddingVertical: vs(12),
195
+ },
196
+ cellDisabled: {
197
+ opacity: 0.4,
198
+ },
199
+ label: {
200
+ fontFamily: 'Sohne-Medium',
201
+ fontSize: ms(12),
202
+ lineHeight: mvs(15),
203
+ textAlign: 'center',
204
+ },
205
+ })
@@ -0,0 +1 @@
1
+ export * from './SelectableGrid'
@@ -14,7 +14,7 @@ export function Separator({ orientation = 'horizontal', style }: SeparatorProps)
14
14
  <View
15
15
  style={[
16
16
  orientation === 'horizontal' ? styles.horizontal : styles.vertical,
17
- { backgroundColor: colors.border },
17
+ { backgroundColor: colors.separator },
18
18
  style,
19
19
  ]}
20
20
  />
@@ -25,11 +25,9 @@ const styles = StyleSheet.create({
25
25
  horizontal: {
26
26
  height: 1,
27
27
  width: '100%',
28
- opacity: 0.7,
29
28
  },
30
29
  vertical: {
31
30
  width: 1,
32
31
  height: '100%',
33
- opacity: 0.7,
34
32
  },
35
33
  })
@@ -1,5 +1,5 @@
1
1
  import React, { useCallback, useEffect, useRef } from 'react'
2
- import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, Dimensions, Platform } from 'react-native'
2
+ import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, Dimensions, Platform, Modal, ScrollView, useWindowDimensions, Pressable } from 'react-native'
3
3
  import {
4
4
  BottomSheetModal,
5
5
  BottomSheetView,
@@ -16,6 +16,7 @@ import { AntDesign } from '@expo/vector-icons'
16
16
  import { impactMedium } from '../../utils/haptics'
17
17
  import { useTheme } from '../../theme'
18
18
  import { s, vs, ms, mvs } from '../../utils/scaling'
19
+ import { BREAKPOINTS, RADIUS, SHADOWS } from '../../tokens'
19
20
 
20
21
  const SCREEN_HEIGHT = Dimensions.get('window').height
21
22
  const DEFAULT_MAX_HEIGHT = SCREEN_HEIGHT * 0.85
@@ -25,6 +26,21 @@ export { BottomSheetModalProvider }
25
26
  // Re-export BottomSheetTextInput as SheetTextInput for consumer convenience
26
27
  export { BottomSheetTextInput as SheetTextInput }
27
28
 
29
+ export interface SheetHeaderProps {
30
+ children: React.ReactNode
31
+ style?: ViewStyle
32
+ }
33
+
34
+ export interface SheetContentProps {
35
+ children: React.ReactNode
36
+ style?: ViewStyle
37
+ }
38
+
39
+ export interface SheetFooterProps {
40
+ children: React.ReactNode
41
+ style?: ViewStyle
42
+ }
43
+
28
44
  export interface SheetProps {
29
45
  open: boolean
30
46
  onClose: () => void
@@ -78,6 +94,35 @@ export interface SheetProps {
78
94
  * When omitted, sheet uses dynamic sizing (auto-fits content).
79
95
  */
80
96
  snapPoints?: (string | number)[]
97
+ /**
98
+ * When true, render as a centered modal dialog on wide screens (width ≥
99
+ * `BREAKPOINTS.wide`) instead of a bottom sheet. On narrow screens it stays a
100
+ * bottom sheet. Use for store/category/picker dialogs that should feel native
101
+ * on tablets and web.
102
+ *
103
+ * Note: the centered-dialog path uses a plain RN `Modal`, so `SheetTextInput`
104
+ * is not required there — use a regular `TextInput`.
105
+ */
106
+ responsive?: boolean
107
+ /** Max width of the centered dialog (dp). Only applies when `responsive`. Defaults to 480. */
108
+ dialogMaxWidth?: number
109
+ }
110
+
111
+ export function SheetHeader({ children, style }: SheetHeaderProps) {
112
+ return <View style={[styles.header, style]}>{children}</View>
113
+ }
114
+
115
+ export function SheetContent({ children, style }: SheetContentProps) {
116
+ return <View style={[styles.sheetContent, style]}>{children}</View>
117
+ }
118
+
119
+ export function SheetFooter({ children, style }: SheetFooterProps) {
120
+ const { colors } = useTheme()
121
+ return (
122
+ <View style={[styles.sheetFooter, { backgroundColor: colors.card, borderTopColor: colors.border }, style]}>
123
+ {children}
124
+ </View>
125
+ )
81
126
  }
82
127
 
83
128
  export function Sheet({
@@ -98,10 +143,14 @@ export function Sheet({
98
143
  android_keyboardInputMode = 'adjustPan',
99
144
  footer,
100
145
  snapPoints,
146
+ responsive = false,
147
+ dialogMaxWidth = 480,
101
148
  }: SheetProps) {
102
149
  const { colors } = useTheme()
103
150
  const insets = useSafeAreaInsets()
151
+ const { width: windowWidth } = useWindowDimensions()
104
152
  const ref = useRef<BottomSheetModal>(null)
153
+ const asDialog = responsive && windowWidth >= BREAKPOINTS.wide
105
154
 
106
155
  // 'interactive' + 'adjustPan' works properly with enableDynamicSizing on both platforms
107
156
  // 'fillParent' + 'adjustResize' causes restore issues (transparent gap when keyboard dismisses)
@@ -125,20 +174,25 @@ export function Sheet({
125
174
  />
126
175
  ), [])
127
176
 
128
- const renderFooter = useCallback((props: BottomSheetFooterProps) => {
129
- if (!footer) return null
130
- return (
131
- <BottomSheetFooter {...props}>
132
- {footer}
133
- </BottomSheetFooter>
134
- )
135
- }, [footer])
177
+ // Detect compound components in children
178
+ const childArray = React.Children.toArray(children)
179
+ const customHeader = childArray.find((child) => React.isValidElement(child) && child.type === SheetHeader)
180
+ const customContent = childArray.find((child) => React.isValidElement(child) && child.type === SheetContent)
181
+ const customFooter = childArray.find((child) => React.isValidElement(child) && child.type === SheetFooter)
182
+
183
+ // If using compound components, filter them out from main children
184
+ const filteredChildren = customHeader || customContent || customFooter
185
+ ? childArray.filter(
186
+ (child) =>
187
+ !React.isValidElement(child) ||
188
+ (child.type !== SheetHeader && child.type !== SheetContent && child.type !== SheetFooter)
189
+ )
190
+ : children
136
191
 
137
192
  const effectiveSubtitle = subtitle ?? description
193
+ const showHeader = !!(title || effectiveSubtitle || showCloseButton) && !customHeader
138
194
 
139
- const showHeader = !!(title || effectiveSubtitle || showCloseButton)
140
-
141
- const headerNode = showHeader ? (
195
+ const headerNode = customHeader ? customHeader : (showHeader ? (
142
196
  <View style={styles.header} accessibilityRole="header">
143
197
  <View style={styles.headerRow}>
144
198
  {title ? (
@@ -166,7 +220,48 @@ export function Sheet({
166
220
  </Text>
167
221
  ) : null}
168
222
  </View>
169
- ) : null
223
+ ) : null)
224
+
225
+ const contentNode = customContent ? customContent : filteredChildren
226
+ const effectiveFooter = customFooter ? customFooter : footer
227
+
228
+ const renderFooter = useCallback((props: BottomSheetFooterProps) => {
229
+ if (!effectiveFooter) return null
230
+ return (
231
+ <BottomSheetFooter {...props}>
232
+ {effectiveFooter}
233
+ </BottomSheetFooter>
234
+ )
235
+ }, [effectiveFooter])
236
+
237
+ // Centered dialog path for wide screens — plain RN Modal, same header/content/footer.
238
+ if (asDialog) {
239
+ return (
240
+ <Modal visible={open} transparent animationType="fade" onRequestClose={onClose}>
241
+ <Pressable style={styles.dialogBackdrop} onPress={onClose} accessibilityRole="button" accessibilityLabel="Close">
242
+ {/* Inner Pressable swallows presses so taps inside the card don't close it. */}
243
+ <Pressable
244
+ style={[
245
+ styles.dialogCard,
246
+ { backgroundColor: colors.card, maxWidth: dialogMaxWidth, maxHeight: SCREEN_HEIGHT * 0.85 },
247
+ ]}
248
+ onPress={() => {}}
249
+ >
250
+ {headerNode}
251
+ <ScrollView
252
+ contentContainerStyle={[styles.dialogContent, style]}
253
+ style={contentStyle}
254
+ showsVerticalScrollIndicator={true}
255
+ bounces={false}
256
+ >
257
+ {contentNode}
258
+ </ScrollView>
259
+ {effectiveFooter}
260
+ </Pressable>
261
+ </Pressable>
262
+ </Modal>
263
+ )
264
+ }
170
265
 
171
266
  const useScroll = scrollable || !!maxHeight
172
267
  const effectiveMaxHeight = maxHeight ?? DEFAULT_MAX_HEIGHT
@@ -182,7 +277,7 @@ export function Sheet({
182
277
  maxDynamicContentSize={useDynamicSizing ? effectiveMaxHeight : undefined}
183
278
  onDismiss={onClose}
184
279
  backdropComponent={renderBackdrop}
185
- footerComponent={footer ? renderFooter : undefined}
280
+ footerComponent={effectiveFooter ? renderFooter : undefined}
186
281
  backgroundStyle={[styles.background, { backgroundColor: colors.card }]}
187
282
  handleIndicatorStyle={[styles.handle, { backgroundColor: colors.border }]}
188
283
  enablePanDownToClose
@@ -204,18 +299,22 @@ export function Sheet({
204
299
  persistentScrollbar={isAndroid}
205
300
  >
206
301
  {headerNode}
207
- {children}
302
+ {contentNode}
208
303
  </BottomSheetScrollView>
209
304
  ) : (
210
305
  <BottomSheetView style={[styles.content, contentStyle, style]}>
211
306
  {headerNode}
212
- {children}
307
+ {contentNode}
213
308
  </BottomSheetView>
214
309
  )}
215
310
  </BottomSheetModal>
216
311
  )
217
312
  }
218
313
 
314
+ Sheet.Header = SheetHeader
315
+ Sheet.Content = SheetContent
316
+ Sheet.Footer = SheetFooter
317
+
219
318
  const styles = StyleSheet.create({
220
319
  background: {
221
320
  borderTopLeftRadius: ms(16),
@@ -238,12 +337,12 @@ const styles = StyleSheet.create({
238
337
  justifyContent: 'space-between',
239
338
  },
240
339
  title: {
241
- fontFamily: 'Poppins-SemiBold',
340
+ fontFamily: 'Sohne-SemiBold',
242
341
  fontSize: ms(18),
243
342
  flex: 1,
244
343
  },
245
344
  subtitle: {
246
- fontFamily: 'Poppins-Regular',
345
+ fontFamily: 'Sohne-Regular',
247
346
  fontSize: ms(14),
248
347
  lineHeight: mvs(20),
249
348
  },
@@ -260,4 +359,33 @@ const styles = StyleSheet.create({
260
359
  paddingBottom: vs(32),
261
360
  paddingRight: s(16),
262
361
  },
362
+ sheetContent: {
363
+ gap: vs(16),
364
+ },
365
+ sheetFooter: {
366
+ paddingHorizontal: s(16),
367
+ paddingVertical: vs(16),
368
+ borderTopWidth: 1,
369
+ flexDirection: 'row',
370
+ gap: s(12),
371
+ },
372
+ dialogBackdrop: {
373
+ flex: 1,
374
+ backgroundColor: 'rgba(0,0,0,0.5)',
375
+ alignItems: 'center',
376
+ justifyContent: 'center',
377
+ padding: s(24),
378
+ },
379
+ dialogCard: {
380
+ width: '100%',
381
+ borderRadius: RADIUS.lg,
382
+ paddingTop: vs(16),
383
+ overflow: 'hidden',
384
+ ...SHADOWS.xl,
385
+ },
386
+ dialogContent: {
387
+ paddingHorizontal: s(16),
388
+ paddingBottom: vs(16),
389
+ },
263
390
  })
391
+
@@ -9,8 +9,9 @@ import Animated, {
9
9
  } from 'react-native-reanimated'
10
10
  import { LinearGradient } from 'expo-linear-gradient'
11
11
  import { useTheme } from '../../theme'
12
- import { s } from '../../utils/scaling'
12
+ import { s, vs } from '../../utils/scaling'
13
13
  import { TIMINGS } from '../../utils/animations'
14
+ import { RADIUS } from '../../tokens'
14
15
 
15
16
  // circle: circular avatar placeholder text: short line preset base: custom dimensions
16
17
  export type SkeletonPreset = 'base' | 'circle' | 'text'
@@ -74,7 +75,7 @@ export function Skeleton({
74
75
  <View
75
76
  style={[
76
77
  styles.base,
77
- { width: resolvedWidth as any, height: resolvedHeight, borderRadius: resolvedRadius, backgroundColor: colors.surface },
78
+ { width: resolvedWidth as number | `${number}%`, height: resolvedHeight, borderRadius: resolvedRadius, backgroundColor: colors.surface },
78
79
  style,
79
80
  ]}
80
81
  onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
@@ -94,8 +95,148 @@ export function Skeleton({
94
95
  )
95
96
  }
96
97
 
98
+ // ─── Per-component skeletons ───────────────────────────────────────────────────
99
+ // Loading placeholders that mirror a component's footprint, so grids/lists don't
100
+ // reflow when real data arrives.
101
+
102
+ const aspectRatioMap = {
103
+ '1:1': 1,
104
+ '4:3': 3 / 4,
105
+ '16:9': 9 / 16,
106
+ '4:5': 5 / 4,
107
+ '3:2': 2 / 3,
108
+ } as const
109
+
110
+ export type MediaCardSkeletonAspectRatio = keyof typeof aspectRatioMap
111
+
112
+ export interface MediaCardSkeletonProps {
113
+ /** Image aspect ratio — match your `MediaCard`. Defaults to `'4:3'`. */
114
+ aspectRatio?: MediaCardSkeletonAspectRatio
115
+ /** Show the subtitle/caption line below the title. Defaults to true. */
116
+ showSubtitle?: boolean
117
+ style?: ViewStyle
118
+ }
119
+
120
+ /** Loading placeholder matching `<MediaCard>` — image block + title/subtitle lines. */
121
+ export function MediaCardSkeleton({ aspectRatio = '4:3', showSubtitle = true, style }: MediaCardSkeletonProps) {
122
+ const ratio = aspectRatioMap[aspectRatio]
123
+ return (
124
+ <View style={style}>
125
+ <View style={{ paddingTop: `${ratio * 100}%` as `${number}%` }}>
126
+ <View style={StyleSheet.absoluteFill}>
127
+ <Skeleton width="100%" height={undefined as unknown as number} style={skeletonStyles.fill} borderRadius={RADIUS.md} />
128
+ </View>
129
+ </View>
130
+ <View style={skeletonStyles.meta}>
131
+ <Skeleton width="70%" height={vs(14)} borderRadius={RADIUS.xs} />
132
+ {showSubtitle ? <Skeleton width="45%" height={vs(12)} borderRadius={RADIUS.xs} /> : null}
133
+ </View>
134
+ </View>
135
+ )
136
+ }
137
+
138
+ export interface ListItemSkeletonProps {
139
+ /** Render a circular leading avatar placeholder. Defaults to true. */
140
+ showAvatar?: boolean
141
+ /** Render a secondary subtitle line. Defaults to true. */
142
+ showSubtitle?: boolean
143
+ style?: ViewStyle
144
+ }
145
+
146
+ /** Loading placeholder matching `<ListItem>` — leading circle + title/subtitle lines. */
147
+ export function ListItemSkeleton({ showAvatar = true, showSubtitle = true, style }: ListItemSkeletonProps) {
148
+ return (
149
+ <View style={[skeletonStyles.row, style]}>
150
+ {showAvatar ? <Skeleton preset="circle" diameter={40} /> : null}
151
+ <View style={skeletonStyles.rowText}>
152
+ <Skeleton width="60%" height={vs(14)} borderRadius={RADIUS.xs} />
153
+ {showSubtitle ? <Skeleton width="40%" height={vs(12)} borderRadius={RADIUS.xs} /> : null}
154
+ </View>
155
+ </View>
156
+ )
157
+ }
158
+
159
+ export interface ListSkeletonProps {
160
+ /** Number of placeholder rows/cells. Defaults to 6. */
161
+ count?: number
162
+ /** 1 = stacked list of `ListItemSkeleton`; >1 = grid of `MediaCardSkeleton`. Defaults to 1. */
163
+ columns?: number
164
+ /** Gap between items (dp). Defaults to 12. */
165
+ gap?: number
166
+ /** Grid only — aspect ratio of each `MediaCardSkeleton`. Defaults to `'4:3'`. */
167
+ aspectRatio?: MediaCardSkeletonAspectRatio
168
+ /** List only — show the leading avatar circle. Defaults to true. */
169
+ showAvatar?: boolean
170
+ style?: ViewStyle
171
+ }
172
+
173
+ /**
174
+ * Repeated loading placeholder for a `VirtualList` / list / grid. `columns={1}`
175
+ * renders stacked `ListItemSkeleton`s; `columns>1` renders a wrapping grid of
176
+ * `MediaCardSkeleton`s. Render this as the list's content while `data` is empty.
177
+ */
178
+ export function ListSkeleton({
179
+ count = 6,
180
+ columns = 1,
181
+ gap = 12,
182
+ aspectRatio = '4:3',
183
+ showAvatar = true,
184
+ style,
185
+ }: ListSkeletonProps) {
186
+ if (columns <= 1) {
187
+ return (
188
+ <View style={[{ gap: vs(gap) }, style]}>
189
+ {Array.from({ length: count }).map((_, i) => (
190
+ <ListItemSkeleton key={i} showAvatar={showAvatar} />
191
+ ))}
192
+ </View>
193
+ )
194
+ }
195
+ const widthPct = `${100 / columns}%` as `${number}%`
196
+ // Gutter via per-cell padding + marginBottom (not container `gap`) so percentage
197
+ // widths sum to exactly 100% and never wrap one short.
198
+ return (
199
+ <View style={[skeletonStyles.grid, { marginHorizontal: -s(gap) / 2 }, style]}>
200
+ {Array.from({ length: count }).map((_, i) => (
201
+ <View key={i} style={{ width: widthPct, paddingHorizontal: s(gap) / 2, marginBottom: vs(gap) }}>
202
+ <MediaCardSkeleton aspectRatio={aspectRatio} />
203
+ </View>
204
+ ))}
205
+ </View>
206
+ )
207
+ }
208
+
209
+ Skeleton.MediaCard = MediaCardSkeleton
210
+ Skeleton.ListItem = ListItemSkeleton
211
+ Skeleton.List = ListSkeleton
212
+
97
213
  const styles = StyleSheet.create({
98
214
  base: {
99
215
  overflow: 'hidden',
100
216
  },
101
217
  })
218
+
219
+ const skeletonStyles = StyleSheet.create({
220
+ grid: {
221
+ flexDirection: 'row',
222
+ flexWrap: 'wrap',
223
+ },
224
+ fill: {
225
+ width: '100%',
226
+ height: '100%',
227
+ },
228
+ meta: {
229
+ paddingTop: vs(8),
230
+ gap: vs(6),
231
+ },
232
+ row: {
233
+ flexDirection: 'row',
234
+ alignItems: 'center',
235
+ gap: s(12),
236
+ paddingVertical: vs(8),
237
+ },
238
+ rowText: {
239
+ flex: 1,
240
+ gap: vs(6),
241
+ },
242
+ })