@delacour/react-native-ui 0.1.0-alpha.4 → 0.2.0-alpha.20261010061731

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 (493) hide show
  1. package/README.md +10 -6
  2. package/package.json +49 -5
  3. package/src/components/accordion/AGENTS.md +21 -9
  4. package/src/components/accordion/accordion-content.tsx +7 -4
  5. package/src/components/accordion/accordion-item.tsx +15 -13
  6. package/src/components/accordion/accordion.context.tsx +11 -5
  7. package/src/components/accordion/accordion.variants.test.ts +64 -3
  8. package/src/components/accordion/accordion.variants.ts +33 -9
  9. package/src/components/alert/AGENTS.md +102 -0
  10. package/src/components/alert/alert-action.tsx +17 -0
  11. package/src/components/alert/alert-close-button.tsx +50 -0
  12. package/src/components/alert/alert-content.tsx +15 -0
  13. package/src/components/alert/alert-description.tsx +16 -0
  14. package/src/components/alert/alert-glyphs.ts +25 -0
  15. package/src/components/alert/alert-indicator.tsx +40 -0
  16. package/src/components/alert/alert-title.tsx +18 -0
  17. package/src/components/alert/alert.context.tsx +69 -0
  18. package/src/components/alert/alert.tsx +160 -0
  19. package/src/components/alert/alert.types.ts +10 -0
  20. package/src/components/alert/alert.variants.test.ts +218 -0
  21. package/src/components/alert/alert.variants.ts +180 -0
  22. package/src/components/alert/index.ts +23 -0
  23. package/src/components/avatar/AGENTS.md +106 -0
  24. package/src/components/avatar/avatar-badge.tsx +51 -0
  25. package/src/components/avatar/avatar-group.tsx +139 -0
  26. package/src/components/avatar/avatar.context.tsx +93 -0
  27. package/src/components/avatar/avatar.tsx +201 -0
  28. package/src/components/avatar/avatar.utils.test.ts +174 -0
  29. package/src/components/avatar/avatar.utils.ts +141 -0
  30. package/src/components/avatar/avatar.variants.test.ts +251 -0
  31. package/src/components/avatar/avatar.variants.ts +270 -0
  32. package/src/components/avatar/index.ts +39 -0
  33. package/src/components/badge/AGENTS.md +10 -0
  34. package/src/components/badge/badge-close-button.tsx +2 -2
  35. package/src/components/badge/badge.context.tsx +2 -1
  36. package/src/components/badge/badge.tsx +13 -4
  37. package/src/components/badge/badge.variants.test.ts +26 -0
  38. package/src/components/badge/badge.variants.ts +21 -0
  39. package/src/components/bottom-sheet/AGENTS.md +186 -231
  40. package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
  41. package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
  42. package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -208
  43. package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
  44. package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
  45. package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
  46. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -122
  47. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
  48. package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
  49. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
  50. package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
  51. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -62
  52. package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
  53. package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
  54. package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
  55. package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
  56. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
  57. package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
  58. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -87
  59. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
  60. package/src/components/bottom-sheet/index.ts +39 -17
  61. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
  62. package/src/components/button/AGENTS.md +51 -11
  63. package/src/components/button/button-group.tsx +44 -8
  64. package/src/components/button/button.context.tsx +10 -0
  65. package/src/components/button/button.tsx +7 -1
  66. package/src/components/button/button.variants.test.ts +190 -0
  67. package/src/components/button/button.variants.ts +87 -6
  68. package/src/components/button/index.ts +2 -0
  69. package/src/components/calendar/AGENTS.md +142 -0
  70. package/src/components/calendar/calendar-caption.tsx +116 -0
  71. package/src/components/calendar/calendar-day.tsx +103 -0
  72. package/src/components/calendar/calendar-grid.tsx +183 -0
  73. package/src/components/calendar/calendar-header.tsx +34 -0
  74. package/src/components/calendar/calendar-nav.tsx +51 -0
  75. package/src/components/calendar/calendar-picker.tsx +128 -0
  76. package/src/components/calendar/calendar-weekdays.tsx +48 -0
  77. package/src/components/calendar/calendar.context.tsx +108 -0
  78. package/src/components/calendar/calendar.date.test.ts +456 -0
  79. package/src/components/calendar/calendar.date.ts +584 -0
  80. package/src/components/calendar/calendar.tsx +389 -0
  81. package/src/components/calendar/calendar.types.ts +87 -0
  82. package/src/components/calendar/calendar.variants.test.ts +363 -0
  83. package/src/components/calendar/calendar.variants.ts +426 -0
  84. package/src/components/calendar/index.ts +96 -0
  85. package/src/components/card/AGENTS.md +92 -0
  86. package/src/components/card/card-action.tsx +16 -0
  87. package/src/components/card/card-content.tsx +12 -0
  88. package/src/components/card/card-description.tsx +12 -0
  89. package/src/components/card/card-footer.tsx +30 -0
  90. package/src/components/card/card-header.tsx +45 -0
  91. package/src/components/card/card-title.tsx +25 -0
  92. package/src/components/card/card.context.tsx +64 -0
  93. package/src/components/card/card.tsx +100 -0
  94. package/src/components/card/card.types.ts +13 -0
  95. package/src/components/card/card.variants.test.ts +240 -0
  96. package/src/components/card/card.variants.ts +177 -0
  97. package/src/components/card/index.ts +15 -0
  98. package/src/components/chart/AGENTS.md +8 -0
  99. package/src/components/chart/chart.tsx +8 -1
  100. package/src/components/chip/AGENTS.md +113 -0
  101. package/src/components/chip/chip-close-button.tsx +48 -0
  102. package/src/components/chip/chip-end-content.tsx +15 -0
  103. package/src/components/chip/chip-label.tsx +21 -0
  104. package/src/components/chip/chip-start-content.tsx +16 -0
  105. package/src/components/chip/chip.context.tsx +66 -0
  106. package/src/components/chip/chip.tsx +268 -0
  107. package/src/components/chip/chip.types.ts +10 -0
  108. package/src/components/chip/chip.variants.test.ts +335 -0
  109. package/src/components/chip/chip.variants.ts +321 -0
  110. package/src/components/chip/index.ts +24 -0
  111. package/src/components/collapsible/AGENTS.md +97 -0
  112. package/src/components/collapsible/collapsible-content.tsx +97 -0
  113. package/src/components/collapsible/collapsible-description.tsx +17 -0
  114. package/src/components/collapsible/collapsible-indicator.tsx +92 -0
  115. package/src/components/collapsible/collapsible-title.tsx +17 -0
  116. package/src/components/collapsible/collapsible-trigger.tsx +133 -0
  117. package/src/components/collapsible/collapsible.context.tsx +91 -0
  118. package/src/components/collapsible/collapsible.tsx +167 -0
  119. package/src/components/collapsible/collapsible.types.ts +9 -0
  120. package/src/components/collapsible/collapsible.variants.test.ts +338 -0
  121. package/src/components/collapsible/collapsible.variants.ts +228 -0
  122. package/src/components/collapsible/index.ts +32 -0
  123. package/src/components/context-menu/AGENTS.md +103 -0
  124. package/src/components/context-menu/context-menu-content.tsx +64 -0
  125. package/src/components/context-menu/context-menu-item.tsx +22 -0
  126. package/src/components/context-menu/context-menu-preview.tsx +60 -0
  127. package/src/components/context-menu/context-menu-trigger.tsx +160 -0
  128. package/src/components/context-menu/context-menu.context.tsx +91 -0
  129. package/src/components/context-menu/context-menu.tsx +90 -0
  130. package/src/components/context-menu/context-menu.variants.test.ts +266 -0
  131. package/src/components/context-menu/context-menu.variants.ts +147 -0
  132. package/src/components/context-menu/index.ts +18 -0
  133. package/src/components/dialog/AGENTS.md +97 -0
  134. package/src/components/dialog/dialog-body.tsx +21 -0
  135. package/src/components/dialog/dialog-close.tsx +82 -0
  136. package/src/components/dialog/dialog-content.tsx +153 -0
  137. package/src/components/dialog/dialog-description.tsx +23 -0
  138. package/src/components/dialog/dialog-footer.tsx +32 -0
  139. package/src/components/dialog/dialog-header.tsx +20 -0
  140. package/src/components/dialog/dialog-title.tsx +42 -0
  141. package/src/components/dialog/dialog-trigger.tsx +54 -0
  142. package/src/components/dialog/dialog.context.tsx +90 -0
  143. package/src/components/dialog/dialog.tsx +119 -0
  144. package/src/components/dialog/dialog.variants.test.ts +224 -0
  145. package/src/components/dialog/dialog.variants.ts +173 -0
  146. package/src/components/dialog/index.ts +30 -0
  147. package/src/components/drawer/AGENTS.md +123 -0
  148. package/src/components/drawer/drawer-body.tsx +46 -0
  149. package/src/components/drawer/drawer-close.tsx +76 -0
  150. package/src/components/drawer/drawer-content.tsx +186 -0
  151. package/src/components/drawer/drawer-description.tsx +23 -0
  152. package/src/components/drawer/drawer-footer.tsx +21 -0
  153. package/src/components/drawer/drawer-header.tsx +42 -0
  154. package/src/components/drawer/drawer-title.tsx +35 -0
  155. package/src/components/drawer/drawer-trigger.tsx +54 -0
  156. package/src/components/drawer/drawer.context.tsx +114 -0
  157. package/src/components/drawer/drawer.tsx +119 -0
  158. package/src/components/drawer/drawer.variants.test.ts +392 -0
  159. package/src/components/drawer/drawer.variants.ts +244 -0
  160. package/src/components/drawer/index.ts +41 -0
  161. package/src/components/drawer/use-drawer-pan.ts +108 -0
  162. package/src/components/empty-state/AGENTS.md +90 -0
  163. package/src/components/empty-state/empty-state-content.tsx +17 -0
  164. package/src/components/empty-state/empty-state-description.tsx +12 -0
  165. package/src/components/empty-state/empty-state-header.tsx +17 -0
  166. package/src/components/empty-state/empty-state-media.tsx +56 -0
  167. package/src/components/empty-state/empty-state-title.tsx +18 -0
  168. package/src/components/empty-state/empty-state.context.tsx +65 -0
  169. package/src/components/empty-state/empty-state.tsx +84 -0
  170. package/src/components/empty-state/empty-state.types.ts +14 -0
  171. package/src/components/empty-state/empty-state.variants.test.ts +245 -0
  172. package/src/components/empty-state/empty-state.variants.ts +132 -0
  173. package/src/components/empty-state/index.ts +20 -0
  174. package/src/components/fab/AGENTS.md +119 -0
  175. package/src/components/fab/fab-action.tsx +118 -0
  176. package/src/components/fab/fab-group.tsx +214 -0
  177. package/src/components/fab/fab-label.tsx +18 -0
  178. package/src/components/fab/fab.context.tsx +125 -0
  179. package/src/components/fab/fab.tsx +156 -0
  180. package/src/components/fab/fab.types.ts +17 -0
  181. package/src/components/fab/fab.variants.test.ts +274 -0
  182. package/src/components/fab/fab.variants.ts +234 -0
  183. package/src/components/fab/index.ts +37 -0
  184. package/src/components/feedback/AGENTS.md +95 -0
  185. package/src/components/feedback/feedback-action.tsx +16 -0
  186. package/src/components/feedback/feedback-cancel.tsx +34 -0
  187. package/src/components/feedback/feedback-close.tsx +21 -0
  188. package/src/components/feedback/feedback-content.tsx +24 -0
  189. package/src/components/feedback/feedback-field.tsx +70 -0
  190. package/src/components/feedback/feedback-footer.tsx +21 -0
  191. package/src/components/feedback/feedback-panel.tsx +54 -0
  192. package/src/components/feedback/feedback-submit.tsx +74 -0
  193. package/src/components/feedback/feedback-title.tsx +34 -0
  194. package/src/components/feedback/feedback-trigger.tsx +20 -0
  195. package/src/components/feedback/feedback.context.tsx +73 -0
  196. package/src/components/feedback/feedback.tsx +135 -0
  197. package/src/components/feedback/feedback.variants.test.ts +185 -0
  198. package/src/components/feedback/feedback.variants.ts +128 -0
  199. package/src/components/feedback/index.ts +24 -0
  200. package/src/components/field/AGENTS.md +19 -0
  201. package/src/components/field/field-label.tsx +19 -5
  202. package/src/components/field/field.context.tsx +17 -0
  203. package/src/components/field/field.tsx +15 -2
  204. package/src/components/field/field.variants.test.ts +24 -0
  205. package/src/components/field/field.variants.ts +20 -0
  206. package/src/components/field/index.ts +1 -0
  207. package/src/components/input/AGENTS.md +5 -0
  208. package/src/components/input/input.variants.test.ts +32 -0
  209. package/src/components/input/input.variants.ts +13 -1
  210. package/src/components/item/AGENTS.md +92 -0
  211. package/src/components/item/index.ts +25 -0
  212. package/src/components/item/item-actions.tsx +28 -0
  213. package/src/components/item/item-content.tsx +15 -0
  214. package/src/components/item/item-description.tsx +12 -0
  215. package/src/components/item/item-footer.tsx +13 -0
  216. package/src/components/item/item-group.tsx +32 -0
  217. package/src/components/item/item-header.tsx +13 -0
  218. package/src/components/item/item-media.tsx +37 -0
  219. package/src/components/item/item-separator.tsx +13 -0
  220. package/src/components/item/item-title.tsx +17 -0
  221. package/src/components/item/item.context.tsx +66 -0
  222. package/src/components/item/item.tsx +218 -0
  223. package/src/components/item/item.types.ts +12 -0
  224. package/src/components/item/item.variants.test.ts +263 -0
  225. package/src/components/item/item.variants.ts +200 -0
  226. package/src/components/kpi/AGENTS.md +125 -0
  227. package/src/components/kpi/index.ts +45 -0
  228. package/src/components/kpi/kpi-action.tsx +12 -0
  229. package/src/components/kpi/kpi-content.tsx +35 -0
  230. package/src/components/kpi/kpi-footer.tsx +52 -0
  231. package/src/components/kpi/kpi-group.tsx +70 -0
  232. package/src/components/kpi/kpi-header.tsx +25 -0
  233. package/src/components/kpi/kpi-icon.tsx +28 -0
  234. package/src/components/kpi/kpi-sparkline.tsx +145 -0
  235. package/src/components/kpi/kpi-stat.tsx +20 -0
  236. package/src/components/kpi/kpi-title.tsx +19 -0
  237. package/src/components/kpi/kpi-trend.tsx +105 -0
  238. package/src/components/kpi/kpi-value.tsx +32 -0
  239. package/src/components/kpi/kpi.context.tsx +111 -0
  240. package/src/components/kpi/kpi.tsx +154 -0
  241. package/src/components/kpi/kpi.types.ts +9 -0
  242. package/src/components/kpi/kpi.variants.test.ts +428 -0
  243. package/src/components/kpi/kpi.variants.ts +347 -0
  244. package/src/components/label/AGENTS.md +89 -0
  245. package/src/components/label/index.ts +10 -0
  246. package/src/components/label/label.tsx +97 -0
  247. package/src/components/label/label.variants.test.ts +176 -0
  248. package/src/components/label/label.variants.ts +123 -0
  249. package/src/components/menu/AGENTS.md +176 -0
  250. package/src/components/menu/index.ts +44 -0
  251. package/src/components/menu/menu-background.tsx +17 -0
  252. package/src/components/menu/menu-checkbox-item.tsx +57 -0
  253. package/src/components/menu/menu-content.tsx +303 -0
  254. package/src/components/menu/menu-item.tsx +162 -0
  255. package/src/components/menu/menu-label.tsx +14 -0
  256. package/src/components/menu/menu-radio-group.tsx +50 -0
  257. package/src/components/menu/menu-radio-item.tsx +57 -0
  258. package/src/components/menu/menu-separator.tsx +11 -0
  259. package/src/components/menu/menu-sub-content.tsx +72 -0
  260. package/src/components/menu/menu-sub-trigger.tsx +50 -0
  261. package/src/components/menu/menu-sub.tsx +75 -0
  262. package/src/components/menu/menu-trigger.tsx +57 -0
  263. package/src/components/menu/menu.context.tsx +214 -0
  264. package/src/components/menu/menu.tsx +88 -0
  265. package/src/components/menu/menu.types.ts +33 -0
  266. package/src/components/menu/menu.variants.test.ts +400 -0
  267. package/src/components/menu/menu.variants.ts +353 -0
  268. package/src/components/meter/AGENTS.md +135 -0
  269. package/src/components/meter/index.ts +43 -0
  270. package/src/components/meter/meter-fill.tsx +19 -0
  271. package/src/components/meter/meter-header.tsx +17 -0
  272. package/src/components/meter/meter-label.tsx +18 -0
  273. package/src/components/meter/meter-output.tsx +42 -0
  274. package/src/components/meter/meter-segments.tsx +83 -0
  275. package/src/components/meter/meter-track.tsx +16 -0
  276. package/src/components/meter/meter.context.tsx +62 -0
  277. package/src/components/meter/meter.tsx +286 -0
  278. package/src/components/meter/meter.types.ts +30 -0
  279. package/src/components/meter/meter.variants.test.ts +430 -0
  280. package/src/components/meter/meter.variants.ts +347 -0
  281. package/src/components/overlay/AGENTS.md +89 -0
  282. package/src/components/overlay/index.ts +27 -0
  283. package/src/components/overlay/overlay-portal.tsx +84 -0
  284. package/src/components/overlay/overlay-presence.test.ts +46 -0
  285. package/src/components/overlay/overlay-presence.ts +49 -0
  286. package/src/components/overlay/overlay-registry.test.ts +96 -0
  287. package/src/components/overlay/overlay-registry.ts +99 -0
  288. package/src/components/overlay/overlay-scrim.tsx +49 -0
  289. package/src/components/overlay/overlay.context.tsx +48 -0
  290. package/src/components/overlay/overlay.tsx +118 -0
  291. package/src/components/overlay/overlay.variants.test.ts +37 -0
  292. package/src/components/overlay/overlay.variants.ts +33 -0
  293. package/src/components/overlay/use-overlay-back-handler.ts +34 -0
  294. package/src/components/overlay/use-overlay-presence.ts +87 -0
  295. package/src/components/popover/AGENTS.md +138 -0
  296. package/src/components/popover/index.ts +48 -0
  297. package/src/components/popover/popover-anchor.tsx +49 -0
  298. package/src/components/popover/popover-arrow.tsx +89 -0
  299. package/src/components/popover/popover-close.tsx +73 -0
  300. package/src/components/popover/popover-content.tsx +235 -0
  301. package/src/components/popover/popover-description.tsx +18 -0
  302. package/src/components/popover/popover-title.tsx +33 -0
  303. package/src/components/popover/popover-trigger.tsx +56 -0
  304. package/src/components/popover/popover.context.tsx +71 -0
  305. package/src/components/popover/popover.position.test.ts +354 -0
  306. package/src/components/popover/popover.position.ts +286 -0
  307. package/src/components/popover/popover.tsx +123 -0
  308. package/src/components/popover/popover.variants.test.ts +135 -0
  309. package/src/components/popover/popover.variants.ts +85 -0
  310. package/src/components/popover/use-anchor-measure.ts +76 -0
  311. package/src/components/popover/use-anchored-content.ts +219 -0
  312. package/src/components/pressable/AGENTS.md +11 -1
  313. package/src/components/pressable/pressable.tsx +7 -2
  314. package/src/components/pressable/pressable.variants.test.ts +29 -0
  315. package/src/components/pressable/pressable.variants.ts +17 -0
  316. package/src/components/progress/AGENTS.md +120 -0
  317. package/src/components/progress/index.ts +40 -0
  318. package/src/components/progress/progress-fill.tsx +152 -0
  319. package/src/components/progress/progress-header.tsx +19 -0
  320. package/src/components/progress/progress-label.tsx +27 -0
  321. package/src/components/progress/progress-output.tsx +47 -0
  322. package/src/components/progress/progress-track.tsx +38 -0
  323. package/src/components/progress/progress.context.tsx +79 -0
  324. package/src/components/progress/progress.tsx +171 -0
  325. package/src/components/progress/progress.types.ts +23 -0
  326. package/src/components/progress/progress.variants.test.ts +335 -0
  327. package/src/components/progress/progress.variants.ts +284 -0
  328. package/src/components/progress-button/AGENTS.md +116 -0
  329. package/src/components/progress-button/index.ts +36 -0
  330. package/src/components/progress-button/progress-button-done.tsx +33 -0
  331. package/src/components/progress-button/progress-button-label.tsx +27 -0
  332. package/src/components/progress-button/progress-button.context.tsx +93 -0
  333. package/src/components/progress-button/progress-button.tsx +414 -0
  334. package/src/components/progress-button/progress-button.variants.test.ts +314 -0
  335. package/src/components/progress-button/progress-button.variants.ts +236 -0
  336. package/src/components/provider/AGENTS.md +38 -25
  337. package/src/components/provider/provider.tsx +38 -11
  338. package/src/components/radio/AGENTS.md +8 -10
  339. package/src/components/radio/radio.variants.test.ts +5 -6
  340. package/src/components/radio/radio.variants.ts +6 -7
  341. package/src/components/rating/AGENTS.md +143 -0
  342. package/src/components/rating/index.ts +43 -0
  343. package/src/components/rating/rating-output.tsx +39 -0
  344. package/src/components/rating/rating-star.tsx +70 -0
  345. package/src/components/rating/rating-stars.tsx +219 -0
  346. package/src/components/rating/rating.context.tsx +79 -0
  347. package/src/components/rating/rating.tsx +163 -0
  348. package/src/components/rating/rating.variants.test.ts +347 -0
  349. package/src/components/rating/rating.variants.ts +354 -0
  350. package/src/components/selection-mode/AGENTS.md +155 -0
  351. package/src/components/selection-mode/index.ts +43 -0
  352. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  353. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  354. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  355. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  356. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  357. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  358. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  359. package/src/components/selection-mode/selection-mode.tsx +207 -0
  360. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  361. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  362. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  363. package/src/components/skeleton/AGENTS.md +120 -0
  364. package/src/components/skeleton/index.ts +15 -0
  365. package/src/components/skeleton/skeleton-clock.ts +48 -0
  366. package/src/components/skeleton/skeleton-group.tsx +64 -0
  367. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  368. package/src/components/skeleton/skeleton.context.tsx +45 -0
  369. package/src/components/skeleton/skeleton.tsx +227 -0
  370. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  371. package/src/components/skeleton/skeleton.variants.ts +222 -0
  372. package/src/components/slide-button/AGENTS.md +122 -0
  373. package/src/components/slide-button/index.ts +43 -0
  374. package/src/components/slide-button/slide-button-label.tsx +70 -0
  375. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  376. package/src/components/slide-button/slide-button.context.tsx +76 -0
  377. package/src/components/slide-button/slide-button.tsx +457 -0
  378. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  379. package/src/components/slide-button/slide-button.variants.ts +324 -0
  380. package/src/components/slider/index.ts +2 -0
  381. package/src/components/slider/slider-thumb.tsx +33 -1
  382. package/src/components/slider/slider.variants.test.ts +52 -0
  383. package/src/components/slider/slider.variants.ts +56 -2
  384. package/src/components/stack-card/AGENTS.md +169 -0
  385. package/src/components/stack-card/index.ts +37 -0
  386. package/src/components/stack-card/stack-card-action.tsx +66 -0
  387. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  388. package/src/components/stack-card/stack-card-card.tsx +71 -0
  389. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  390. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  391. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  392. package/src/components/stack-card/stack-card.context.tsx +117 -0
  393. package/src/components/stack-card/stack-card.tsx +557 -0
  394. package/src/components/stack-card/stack-card.types.ts +26 -0
  395. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  396. package/src/components/stack-card/stack-card.variants.ts +360 -0
  397. package/src/components/steps/AGENTS.md +153 -0
  398. package/src/components/steps/index.ts +45 -0
  399. package/src/components/steps/steps-connector.tsx +31 -0
  400. package/src/components/steps/steps-description.tsx +26 -0
  401. package/src/components/steps/steps-indicator.tsx +106 -0
  402. package/src/components/steps/steps-item.tsx +225 -0
  403. package/src/components/steps/steps-panel.tsx +41 -0
  404. package/src/components/steps/steps-title.tsx +32 -0
  405. package/src/components/steps/steps-track.tsx +59 -0
  406. package/src/components/steps/steps.context.tsx +123 -0
  407. package/src/components/steps/steps.tsx +166 -0
  408. package/src/components/steps/steps.variants.test.ts +388 -0
  409. package/src/components/steps/steps.variants.ts +468 -0
  410. package/src/components/surface/AGENTS.md +82 -0
  411. package/src/components/surface/index.ts +15 -0
  412. package/src/components/surface/surface.context.tsx +56 -0
  413. package/src/components/surface/surface.tsx +87 -0
  414. package/src/components/surface/surface.variants.test.ts +227 -0
  415. package/src/components/surface/surface.variants.ts +165 -0
  416. package/src/components/swipe/AGENTS.md +140 -0
  417. package/src/components/swipe/index.ts +31 -0
  418. package/src/components/swipe/swipe-action.tsx +99 -0
  419. package/src/components/swipe/swipe-end.tsx +14 -0
  420. package/src/components/swipe/swipe-group.tsx +55 -0
  421. package/src/components/swipe/swipe-panel.tsx +55 -0
  422. package/src/components/swipe/swipe-start.tsx +15 -0
  423. package/src/components/swipe/swipe.context.tsx +135 -0
  424. package/src/components/swipe/swipe.tsx +393 -0
  425. package/src/components/swipe/swipe.types.ts +7 -0
  426. package/src/components/swipe/swipe.variants.test.ts +337 -0
  427. package/src/components/swipe/swipe.variants.ts +290 -0
  428. package/src/components/switch/switch.variants.ts +2 -2
  429. package/src/components/tabs/AGENTS.md +4 -3
  430. package/src/components/tabs/tabs.variants.test.ts +1 -1
  431. package/src/components/tabs/tabs.variants.ts +4 -6
  432. package/src/components/text/AGENTS.md +8 -2
  433. package/src/components/text/text.tsx +7 -0
  434. package/src/components/text/text.variants.test.ts +24 -1
  435. package/src/components/text/text.variants.ts +5 -0
  436. package/src/components/textarea/AGENTS.md +89 -0
  437. package/src/components/textarea/index.ts +16 -0
  438. package/src/components/textarea/textarea.tsx +157 -0
  439. package/src/components/textarea/textarea.variants.test.ts +219 -0
  440. package/src/components/textarea/textarea.variants.ts +171 -0
  441. package/src/components/toast/AGENTS.md +140 -0
  442. package/src/components/toast/index.ts +74 -0
  443. package/src/components/toast/toast-action.tsx +59 -0
  444. package/src/components/toast/toast-api.ts +32 -0
  445. package/src/components/toast/toast-close.tsx +44 -0
  446. package/src/components/toast/toast-content.tsx +11 -0
  447. package/src/components/toast/toast-description.tsx +11 -0
  448. package/src/components/toast/toast-indicator.tsx +34 -0
  449. package/src/components/toast/toast-item.tsx +308 -0
  450. package/src/components/toast/toast-title.tsx +13 -0
  451. package/src/components/toast/toast-viewport.tsx +135 -0
  452. package/src/components/toast/toast.context.tsx +69 -0
  453. package/src/components/toast/toast.store.test.ts +259 -0
  454. package/src/components/toast/toast.store.ts +338 -0
  455. package/src/components/toast/toast.timer.test.ts +64 -0
  456. package/src/components/toast/toast.timer.ts +51 -0
  457. package/src/components/toast/toast.tsx +132 -0
  458. package/src/components/toast/toast.variants.test.ts +393 -0
  459. package/src/components/toast/toast.variants.ts +304 -0
  460. package/src/components/toggle-button/AGENTS.md +106 -0
  461. package/src/components/toggle-button/index.ts +30 -0
  462. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  463. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  464. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  465. package/src/components/toggle-button/toggle-button.tsx +172 -0
  466. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  467. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  468. package/src/components/tooltip/AGENTS.md +133 -0
  469. package/src/components/tooltip/index.ts +28 -0
  470. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  471. package/src/components/tooltip/tooltip-content.tsx +179 -0
  472. package/src/components/tooltip/tooltip-description.tsx +19 -0
  473. package/src/components/tooltip/tooltip-text.tsx +21 -0
  474. package/src/components/tooltip/tooltip-title.tsx +20 -0
  475. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  476. package/src/components/tooltip/tooltip.context.tsx +68 -0
  477. package/src/components/tooltip/tooltip.tsx +213 -0
  478. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  479. package/src/components/tooltip/tooltip.variants.ts +177 -0
  480. package/src/hooks/use-calm-motion.tsx +31 -0
  481. package/src/lib/calm-motion.test.ts +17 -0
  482. package/src/lib/calm-motion.ts +23 -0
  483. package/src/lib/cn.parity.test.ts +62 -0
  484. package/src/lib/cn.test.ts +14 -0
  485. package/src/lib/cn.ts +11 -6
  486. package/src/no-gorhom.test.ts +54 -0
  487. package/src/styles/geometry.test.ts +1 -0
  488. package/src/styles/theme.css +4 -0
  489. package/src/styles/tokens.css +44 -0
  490. package/src/styles/tokens.test.ts +56 -0
  491. package/src/styles/tokens.ts +29 -1
  492. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  493. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -0,0 +1,251 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { declaredTokens } from "../../styles/theme-tokens.test";
3
+ import { ICON_SIZE_TOKENS } from "../../styles/tokens";
4
+ import {
5
+ AVATAR_BADGE_PLACEMENTS,
6
+ AVATAR_COLORS,
7
+ AVATAR_FOREGROUND_TOKEN,
8
+ AVATAR_SIZE_POINTS,
9
+ AVATAR_SIZES,
10
+ AVATAR_VARIANTS,
11
+ avatarVariants,
12
+ resolveAvatarInteractive,
13
+ resolveAvatarOverlap,
14
+ resolveAvatarSize,
15
+ } from "./avatar.variants";
16
+
17
+ const LIGHT = declaredTokens("light");
18
+ const DARK = declaredTokens("dark");
19
+
20
+ /** The `size-N` step a class string sets its edge from, in points (Tailwind's 4pt spacing unit). */
21
+ function edgeOf(cls: string): number {
22
+ return Number(cls.match(/\bsize-(\d+)\b/)?.[1]) * 4;
23
+ }
24
+
25
+ /** The `text-*` colour token a class string resolves to, minus the prefix. */
26
+ function colorToken(cls: string): string | undefined {
27
+ return cls.match(/\btext-((?:[a-z]+-)*(?:foreground|background))\b/)?.[1];
28
+ }
29
+
30
+ /** The `bg-*` token a class string fills with, minus the prefix. */
31
+ function fillToken(cls: string): string | undefined {
32
+ return cls.match(/\bbg-([a-z-]+)\b/)?.[1];
33
+ }
34
+
35
+ /** Position of a slot's `size-icon-*` token on the shared icon scale. */
36
+ function iconStep(cls: string): number {
37
+ const token = cls.match(/\bsize-(icon-[\w-]+)\b/)?.[1];
38
+ return ICON_SIZE_TOKENS.indexOf(token as (typeof ICON_SIZE_TOKENS)[number]);
39
+ }
40
+
41
+ const TEXT_SCALE = ["text-xs", "text-sm", "text-base", "text-lg", "text-xl", "text-2xl"] as const;
42
+ function textStep(cls: string): number {
43
+ return TEXT_SCALE.findIndex((step) => new RegExp(`\\b${step}\\b`).test(cls));
44
+ }
45
+
46
+ describe("sizes", () => {
47
+ test("the face's class and AVATAR_SIZE_POINTS agree at every size", () => {
48
+ // The group computes its overlap in points; a class that drifted from the
49
+ // number would slide faces too far or not far enough.
50
+ for (const size of AVATAR_SIZES) {
51
+ expect(edgeOf(avatarVariants({ size }).root())).toBe(AVATAR_SIZE_POINTS[size]);
52
+ }
53
+ });
54
+
55
+ test("the scale ascends", () => {
56
+ const points = AVATAR_SIZES.map((size) => AVATAR_SIZE_POINTS[size]);
57
+ expect([...points].sort((a, b) => a - b)).toEqual(points);
58
+ expect(new Set(points).size).toBe(points.length);
59
+ });
60
+
61
+ test("the fallback text and glyph grow with the face", () => {
62
+ const text = AVATAR_SIZES.map((size) => textStep(avatarVariants({ size }).fallbackLabel()));
63
+ const icons = AVATAR_SIZES.map((size) => iconStep(avatarVariants({ size }).icon()));
64
+ for (const step of [...text, ...icons]) expect(step).toBeGreaterThanOrEqual(0);
65
+ expect([...text].sort((a, b) => a - b)).toEqual(text);
66
+ expect([...icons].sort((a, b) => a - b)).toEqual(icons);
67
+ });
68
+
69
+ test("the overflow tile is the same edge as a face", () => {
70
+ for (const size of AVATAR_SIZES) {
71
+ const slots = avatarVariants({ size });
72
+ expect(edgeOf(slots.overflow())).toBe(edgeOf(slots.root()));
73
+ }
74
+ });
75
+ });
76
+
77
+ describe("colour matrix", () => {
78
+ test("every variant × colour cell is distinct", () => {
79
+ const seen = new Set<string>();
80
+ for (const variant of AVATAR_VARIANTS) {
81
+ for (const color of AVATAR_COLORS) {
82
+ const slots = avatarVariants({ variant, color });
83
+ seen.add(`${slots.face()}|${slots.fallbackLabel()}`);
84
+ }
85
+ }
86
+ expect(seen.size).toBe(AVATAR_VARIANTS.length * AVATAR_COLORS.length);
87
+ });
88
+
89
+ test("every cell fills its face and colours its label", () => {
90
+ for (const variant of AVATAR_VARIANTS) {
91
+ for (const color of AVATAR_COLORS) {
92
+ const slots = avatarVariants({ variant, color });
93
+ expect(fillToken(slots.face())).toBeDefined();
94
+ expect(colorToken(slots.fallbackLabel())).toBeDefined();
95
+ }
96
+ }
97
+ });
98
+
99
+ test("AVATAR_FOREGROUND_TOKEN names the token the label resolves to", () => {
100
+ // A glyph fallback and a text fallback must be the same shade.
101
+ for (const variant of AVATAR_VARIANTS) {
102
+ for (const color of AVATAR_COLORS) {
103
+ expect(AVATAR_FOREGROUND_TOKEN[variant][color]).toBe(
104
+ colorToken(avatarVariants({ variant, color }).fallbackLabel()) ?? ""
105
+ );
106
+ }
107
+ }
108
+ });
109
+
110
+ test("every token the matrix names is declared in both themes", () => {
111
+ for (const variant of AVATAR_VARIANTS) {
112
+ for (const color of AVATAR_COLORS) {
113
+ const slots = avatarVariants({ variant, color });
114
+ for (const token of [fillToken(slots.face()), colorToken(slots.fallbackLabel())]) {
115
+ expect(LIGHT.has(token ?? "")).toBe(true);
116
+ expect(DARK.has(token ?? "")).toBe(true);
117
+ }
118
+ }
119
+ }
120
+ });
121
+ });
122
+
123
+ describe("structure", () => {
124
+ test("the face clips and the root does not", () => {
125
+ // A badge pinned to the corner hangs outside the circle; clipping the root
126
+ // would cut it in half.
127
+ const slots = avatarVariants();
128
+ expect(slots.face()).toContain("overflow-hidden");
129
+ expect(slots.face()).toContain("rounded-full");
130
+ expect(slots.root()).not.toContain("overflow-hidden");
131
+ });
132
+
133
+ test("the root holds a fixed edge and never overrides its parent's alignment", () => {
134
+ // `self-start` pinned every avatar in an `items-end` or `items-center` row
135
+ // to its top. A fixed size is what keeps a column from stretching it.
136
+ for (const size of AVATAR_SIZES) {
137
+ const slots = avatarVariants({ size });
138
+ expect(slots.root()).not.toMatch(/\bself-/);
139
+ expect(slots.face()).toContain("size-full");
140
+ }
141
+ expect(avatarVariants().group()).not.toMatch(/\bself-/);
142
+ });
143
+
144
+ test("no view slot carries a text colour", () => {
145
+ // A React Native View does not cascade colour to a Text descendant.
146
+ const slots = avatarVariants();
147
+ for (const slot of [slots.root(), slots.face(), slots.badge(), slots.group(), slots.groupItem()]) {
148
+ expect(colorToken(slot)).toBeUndefined();
149
+ }
150
+ });
151
+
152
+ test("a disabled avatar fades", () => {
153
+ expect(avatarVariants({ isDisabled: true }).root()).toContain("opacity-50");
154
+ expect(avatarVariants({ isDisabled: false }).root()).not.toContain("opacity-50");
155
+ });
156
+
157
+ test("a group ring is the page background, so it reads on any surface", () => {
158
+ // The overflow tile is wrapped in the same item, so it wears the same ring
159
+ // and lines up with the faces rather than sitting four points smaller.
160
+ expect(avatarVariants().groupItem()).toContain("border-background");
161
+ expect(avatarVariants().overflow()).not.toContain("border-background");
162
+ });
163
+
164
+ test("every badge placement pins to a different corner", () => {
165
+ const classes = AVATAR_BADGE_PLACEMENTS.map((placement) => avatarVariants({ placement }).badge());
166
+ expect(new Set(classes).size).toBe(AVATAR_BADGE_PLACEMENTS.length);
167
+ for (const cls of classes) expect(cls).toContain("absolute");
168
+ });
169
+
170
+ test("the presence dot grows with the face", () => {
171
+ const dots = AVATAR_SIZES.map((size) =>
172
+ Number(
173
+ avatarVariants({ size })
174
+ .dot()
175
+ .match(/\bsize-([\d.]+)\b/)?.[1]
176
+ )
177
+ );
178
+ for (const dot of dots) expect(Number.isFinite(dot)).toBe(true);
179
+ expect([...dots].sort((a, b) => a - b)).toEqual(dots);
180
+ });
181
+ });
182
+
183
+ describe("the neutral edge", () => {
184
+ test("a default-colour fallback carries a hairline border in both variants", () => {
185
+ // Its fill sits a percent or two from the page in light, so without an
186
+ // edge a neutral face vanishes into the background.
187
+ for (const variant of AVATAR_VARIANTS) {
188
+ expect(avatarVariants({ variant, color: "default", hasImage: false }).face()).toContain("border-border");
189
+ }
190
+ });
191
+
192
+ test("the border goes once a photo is on top", () => {
193
+ for (const variant of AVATAR_VARIANTS) {
194
+ expect(avatarVariants({ variant, color: "default", hasImage: true }).face()).not.toMatch(/\bborder\b/);
195
+ }
196
+ });
197
+
198
+ test("a coloured fallback needs no edge", () => {
199
+ for (const color of AVATAR_COLORS.filter((value) => value !== "default")) {
200
+ expect(avatarVariants({ color, hasImage: false }).face()).not.toMatch(/\bborder\b/);
201
+ }
202
+ });
203
+
204
+ test("the overflow tile carries it too", () => {
205
+ expect(avatarVariants().overflow()).toContain("border-border");
206
+ });
207
+ });
208
+
209
+ describe("resolveAvatarSize", () => {
210
+ test("the avatar's own size wins", () => {
211
+ expect(resolveAvatarSize({ size: "sm", groupSize: "xl" })).toBe("sm");
212
+ });
213
+
214
+ test("then the enclosing group's", () => {
215
+ expect(resolveAvatarSize({ groupSize: "lg" })).toBe("lg");
216
+ });
217
+
218
+ test("then md", () => {
219
+ expect(resolveAvatarSize({})).toBe("md");
220
+ });
221
+ });
222
+
223
+ describe("resolveAvatarOverlap", () => {
224
+ test("defaults to a third of the face, rounded", () => {
225
+ for (const size of AVATAR_SIZES) {
226
+ expect(resolveAvatarOverlap({ size })).toBe(Math.round(AVATAR_SIZE_POINTS[size] / 3));
227
+ }
228
+ });
229
+
230
+ test("an explicit overlap wins, and 0 closes the stack into a row", () => {
231
+ expect(resolveAvatarOverlap({ size: "md", overlap: 6 })).toBe(6);
232
+ expect(resolveAvatarOverlap({ size: "md", overlap: 0 })).toBe(0);
233
+ });
234
+
235
+ test("never slides a face further than its own edge, nor backwards", () => {
236
+ expect(resolveAvatarOverlap({ size: "md", overlap: 999 })).toBe(AVATAR_SIZE_POINTS.md);
237
+ expect(resolveAvatarOverlap({ size: "md", overlap: -4 })).toBe(0);
238
+ expect(resolveAvatarOverlap({ size: "md", overlap: Number.NaN })).toBe(Math.round(AVATAR_SIZE_POINTS.md / 3));
239
+ });
240
+ });
241
+
242
+ describe("resolveAvatarInteractive", () => {
243
+ test("is inert with no handler", () => {
244
+ expect(resolveAvatarInteractive({})).toBe(false);
245
+ });
246
+
247
+ test("becomes pressable with either handler", () => {
248
+ expect(resolveAvatarInteractive({ onPress: () => {} })).toBe(true);
249
+ expect(resolveAvatarInteractive({ onLongPress: () => {} })).toBe(true);
250
+ });
251
+ });
@@ -0,0 +1,270 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+
4
+ /** How the fallback surface is painted. Orthogonal to {@link AVATAR_COLORS}. */
5
+ export const AVATAR_VARIANTS = ["soft", "solid"] as const;
6
+
7
+ /** What the fallback surface means. The same six a `Badge` takes, so the two pair. */
8
+ export const AVATAR_COLORS = ["default", "primary", "success", "warning", "destructive", "info"] as const;
9
+
10
+ export const AVATAR_SIZES = ["sm", "md", "lg", "xl"] as const;
11
+
12
+ /** Where an `Avatar.Badge` is pinned. */
13
+ export const AVATAR_BADGE_PLACEMENTS = ["top-right", "bottom-right"] as const;
14
+
15
+ export type AvatarVariant = (typeof AVATAR_VARIANTS)[number];
16
+ export type AvatarColor = (typeof AVATAR_COLORS)[number];
17
+ export type AvatarSize = (typeof AVATAR_SIZES)[number];
18
+ export type AvatarBadgePlacement = (typeof AVATAR_BADGE_PLACEMENTS)[number];
19
+
20
+ /**
21
+ * A face's edge in points, at each size.
22
+ *
23
+ * The one place a size is a number rather than a class, and it exists because
24
+ * `Avatar.Group` has to compute an overlap: a negative margin set at runtime
25
+ * cannot be a class (Tailwind's scanner is static). A test pins every entry to
26
+ * the `size-*` class the `face` slot resolves to, so the two cannot drift.
27
+ */
28
+ export const AVATAR_SIZE_POINTS: Record<AvatarSize, number> = {
29
+ sm: 32,
30
+ md: 40,
31
+ lg: 48,
32
+ xl: 64,
33
+ };
34
+
35
+ /**
36
+ * Theme token that gives the fallback glyph its colour.
37
+ *
38
+ * The avatar's counterpart to `BADGE_FOREGROUND_TOKEN`: a colour that has to
39
+ * reach an SVG paint prop cannot be a class. Every entry names the token the
40
+ * `fallbackLabel` slot resolves to, so initials and the glyph that stands in for
41
+ * them are the same shade — a test asserts the pair.
42
+ */
43
+ export const AVATAR_FOREGROUND_TOKEN: Record<AvatarVariant, Record<AvatarColor, string>> = {
44
+ soft: {
45
+ default: "muted-foreground",
46
+ primary: "tertiary-foreground",
47
+ success: "success-soft-foreground",
48
+ warning: "warning-soft-foreground",
49
+ destructive: "destructive-soft-foreground",
50
+ info: "info-soft-foreground",
51
+ },
52
+ solid: {
53
+ default: "secondary-foreground",
54
+ primary: "primary-foreground",
55
+ success: "success-foreground",
56
+ warning: "warning-foreground",
57
+ destructive: "destructive-foreground",
58
+ info: "info-foreground",
59
+ },
60
+ };
61
+
62
+ /**
63
+ * Styling for every part of an avatar, and of a group of them.
64
+ *
65
+ * **The root carries the edge, not `self-start`.** A fixed width and height is
66
+ * something Yoga's `stretch` never overrides, so an avatar in a column keeps its
67
+ * size without the escape hatch `Badge` needs — and, unlike `self-start`, it
68
+ * leaves the parent's own alignment alone. `self-start` pinned every avatar in
69
+ * an `items-end` or `items-center` row to the top of it: mixed sizes lost their
70
+ * baseline and a list row's avatar sat above its text. The face fills the root.
71
+ *
72
+ * **Two boxes, not one.** `root` is unclipped and `face` is the clipped circle
73
+ * inside it. An `Avatar.Badge` hangs over the circle's edge, and a single
74
+ * clipped box would cut it in half; a test asserts `overflow-hidden` is on the
75
+ * face and never on the root.
76
+ *
77
+ * **The fallback is always painted; the image goes on top.** So the fill and
78
+ * the initials show while the image loads and after it fails, and a slow or dead
79
+ * URL never leaves an empty circle.
80
+ *
81
+ * **A size is an edge, and it is a fixed one** — unlike a badge, whose size is
82
+ * padding. A face lines up against other faces in a group and against rows in a
83
+ * list, and must not grow with OS font scaling; the initials are one or two
84
+ * glyphs and are sized to fit the smallest step. {@link AVATAR_SIZE_POINTS} is
85
+ * the same scale as a number, for the group's overlap.
86
+ *
87
+ * **The ring in a group is the page background** — `border-background` on the
88
+ * wrapper around each face, never on the face itself, so a face in a group is the
89
+ * same edge as one outside it and the stack reads as separate people on any
90
+ * surface. The overflow tile sits in the same wrapper for the same reason.
91
+ *
92
+ * **A neutral face has an edge.** `muted` and `secondary` sit a percent or two
93
+ * from the page in light, so a `default` fallback and the `+N` tile draw a
94
+ * hairline `border-border` — inside the box, so no size moves. It goes while a
95
+ * photo is mounted (`hasImage`), because a photo carries its own edge and a
96
+ * grey line around a face reads as a frame.
97
+ *
98
+ * The neutral end of the colour matrix reuses the fills `Badge` does — `muted`
99
+ * and `tertiary` for `soft`, `secondary` for a `solid` default — so an avatar
100
+ * beside a badge of the same colour reads as one family.
101
+ *
102
+ * No view slot holds a `text-*` utility: a React Native `View` does not cascade
103
+ * colour to a `Text` descendant.
104
+ *
105
+ * Free of React Native imports so it stays unit-testable. See AGENTS.md.
106
+ */
107
+ export const avatarVariants = tv({
108
+ slots: {
109
+ root: "relative",
110
+ face: "size-full items-center justify-center overflow-hidden rounded-full",
111
+ image: "absolute inset-0",
112
+ fallbackLabel: "text-center font-medium",
113
+ /** Edge length the fallback glyph inherits. */
114
+ icon: "",
115
+ badge: "absolute items-center justify-center",
116
+ dot: "rounded-full border-2 border-background",
117
+ group: "flex-row items-center",
118
+ groupItem: "rounded-full border-2 border-background",
119
+ overflow: "items-center justify-center overflow-hidden rounded-full border border-border bg-muted",
120
+ overflowLabel: "text-center font-medium text-muted-foreground",
121
+ },
122
+ variants: {
123
+ variant: {
124
+ soft: {},
125
+ solid: {},
126
+ },
127
+ // `color` alone paints only the presence dot; the face needs `variant`
128
+ // too, so its twelve cells live in `compoundVariants`.
129
+ color: {
130
+ default: { dot: "bg-muted-foreground" },
131
+ primary: { dot: "bg-primary" },
132
+ success: { dot: "bg-success" },
133
+ warning: { dot: "bg-warning" },
134
+ destructive: { dot: "bg-destructive" },
135
+ info: { dot: "bg-info" },
136
+ },
137
+ size: {
138
+ sm: {
139
+ root: "size-8",
140
+ fallbackLabel: "text-xs",
141
+ icon: "size-icon-sm",
142
+ dot: "size-2.5",
143
+ overflow: "size-8",
144
+ overflowLabel: "text-xs",
145
+ },
146
+ md: {
147
+ root: "size-10",
148
+ fallbackLabel: "text-sm",
149
+ icon: "size-icon-lg",
150
+ dot: "size-3",
151
+ overflow: "size-10",
152
+ overflowLabel: "text-sm",
153
+ },
154
+ lg: {
155
+ root: "size-12",
156
+ fallbackLabel: "text-base",
157
+ icon: "size-icon-xl",
158
+ dot: "size-3.5",
159
+ overflow: "size-12",
160
+ overflowLabel: "text-base",
161
+ },
162
+ xl: {
163
+ root: "size-16",
164
+ fallbackLabel: "text-xl",
165
+ icon: "size-icon-2xl",
166
+ dot: "size-4",
167
+ overflow: "size-16",
168
+ overflowLabel: "text-xl",
169
+ },
170
+ },
171
+ placement: {
172
+ "top-right": { badge: "-top-1 -right-1" },
173
+ "bottom-right": { badge: "-bottom-0.5 -right-0.5" },
174
+ },
175
+ // Whether a photo is mounted over the fallback. Read only by the neutral
176
+ // edge below — a photo carries its own edge.
177
+ hasImage: { true: {}, false: {} },
178
+ // The empty `false` branch is load-bearing typing, not a placeholder.
179
+ // See the note in button.variants.ts.
180
+ isDisabled: { true: { root: "opacity-50" }, false: {} },
181
+ },
182
+ compoundVariants: [
183
+ { variant: "soft", color: "default", class: { face: "bg-muted", fallbackLabel: "text-muted-foreground" } },
184
+ { variant: "soft", color: "primary", class: { face: "bg-tertiary", fallbackLabel: "text-tertiary-foreground" } },
185
+ {
186
+ variant: "soft",
187
+ color: "success",
188
+ class: { face: "bg-success-soft", fallbackLabel: "text-success-soft-foreground" },
189
+ },
190
+ {
191
+ variant: "soft",
192
+ color: "warning",
193
+ class: { face: "bg-warning-soft", fallbackLabel: "text-warning-soft-foreground" },
194
+ },
195
+ {
196
+ variant: "soft",
197
+ color: "destructive",
198
+ class: { face: "bg-destructive-soft", fallbackLabel: "text-destructive-soft-foreground" },
199
+ },
200
+ { variant: "soft", color: "info", class: { face: "bg-info-soft", fallbackLabel: "text-info-soft-foreground" } },
201
+
202
+ {
203
+ variant: "solid",
204
+ color: "default",
205
+ class: { face: "bg-secondary", fallbackLabel: "text-secondary-foreground" },
206
+ },
207
+ { variant: "solid", color: "primary", class: { face: "bg-primary", fallbackLabel: "text-primary-foreground" } },
208
+ { variant: "solid", color: "success", class: { face: "bg-success", fallbackLabel: "text-success-foreground" } },
209
+ { variant: "solid", color: "warning", class: { face: "bg-warning", fallbackLabel: "text-warning-foreground" } },
210
+ {
211
+ variant: "solid",
212
+ color: "destructive",
213
+ class: { face: "bg-destructive", fallbackLabel: "text-destructive-foreground" },
214
+ },
215
+ { variant: "solid", color: "info", class: { face: "bg-info", fallbackLabel: "text-info-foreground" } },
216
+
217
+ { color: "default", hasImage: false, class: { face: "border border-border" } },
218
+ ],
219
+ defaultVariants: {
220
+ variant: "soft",
221
+ color: "default",
222
+ size: "md",
223
+ placement: "top-right",
224
+ hasImage: false,
225
+ isDisabled: false,
226
+ },
227
+ });
228
+
229
+ /**
230
+ * An avatar's size: its own, else its group's, else `md`.
231
+ *
232
+ * A group sets the size for every face in it, and a child that states its own
233
+ * still wins — the order a caller reading the JSX would expect.
234
+ */
235
+ export function resolveAvatarSize({ size, groupSize }: { size?: AvatarSize; groupSize?: AvatarSize }): AvatarSize {
236
+ return size ?? groupSize ?? "md";
237
+ }
238
+
239
+ /**
240
+ * Points each face in a group slides under the one before it.
241
+ *
242
+ * Defaults to a third of the face's edge, which leaves enough of each face to
243
+ * recognise. An explicit value wins — `0` closes the stack into a plain row —
244
+ * and is clamped between nothing and a whole face, so a typo cannot stack every
245
+ * face on the first or push them apart.
246
+ */
247
+ export function resolveAvatarOverlap({ size, overlap }: { size: AvatarSize; overlap?: number }): number {
248
+ const edge = AVATAR_SIZE_POINTS[size];
249
+ if (overlap === undefined || Number.isNaN(overlap)) return Math.round(edge / 3);
250
+ return Math.min(edge, Math.max(0, overlap));
251
+ }
252
+
253
+ /**
254
+ * Whether the root should render as a `Pressable` rather than a plain `View`.
255
+ *
256
+ * The rule `Badge` follows, for the reason it does: an avatar is content until a
257
+ * caller gives it something to do, and a list of fifty should not mount fifty
258
+ * gesture detectors or announce fifty buttons with no action.
259
+ */
260
+ export function resolveAvatarInteractive({
261
+ onPress,
262
+ onLongPress,
263
+ }: {
264
+ onPress?: () => void;
265
+ onLongPress?: () => void;
266
+ }): boolean {
267
+ return onPress !== undefined || onLongPress !== undefined;
268
+ }
269
+
270
+ export type AvatarVariantProps = VariantProps<typeof avatarVariants>;
@@ -0,0 +1,39 @@
1
+ export { Avatar, type AvatarProps } from "./avatar";
2
+ export {
3
+ type AvatarContextValue,
4
+ type AvatarGroupContextValue,
5
+ AvatarProvider,
6
+ useAvatar,
7
+ useAvatarContext,
8
+ useAvatarGroupContext,
9
+ } from "./avatar.context";
10
+ export {
11
+ AVATAR_OVERFLOW_DISPLAY_MAX,
12
+ type AvatarSource,
13
+ type AvatarSourceEntry,
14
+ resolveAvatarAccessibilityLabel,
15
+ resolveAvatarGroup,
16
+ resolveAvatarInitials,
17
+ resolveAvatarOverflowLabel,
18
+ resolveAvatarShowsImage,
19
+ resolveAvatarSourceKey,
20
+ } from "./avatar.utils";
21
+ export {
22
+ AVATAR_BADGE_PLACEMENTS,
23
+ AVATAR_COLORS,
24
+ AVATAR_FOREGROUND_TOKEN,
25
+ AVATAR_SIZE_POINTS,
26
+ AVATAR_SIZES,
27
+ AVATAR_VARIANTS,
28
+ type AvatarBadgePlacement,
29
+ type AvatarColor,
30
+ type AvatarSize,
31
+ type AvatarVariant,
32
+ type AvatarVariantProps,
33
+ avatarVariants,
34
+ resolveAvatarInteractive,
35
+ resolveAvatarOverlap,
36
+ resolveAvatarSize,
37
+ } from "./avatar.variants";
38
+ export type { AvatarBadgeProps } from "./avatar-badge";
39
+ export type { AvatarGroupProps } from "./avatar-group";
@@ -46,6 +46,11 @@ A compact label for status, category or count. Compound root plus `Badge.Label`,
46
46
  with `fade` rather than the root's `scale` — a spring on a glyph that small
47
47
  reads as a jitter. Reach for the part by hand only to place it somewhere other
48
48
  than last.
49
+ - **`closeTestID` reaches the composed close control.** The root's own `testID`
50
+ names the badge; the close part is a separate pressable with no id of its own
51
+ unless the root forwards one, so a flow or test could only find it by its label.
52
+ `closeTestID` is named like `closeAccessibilityLabel`, the other prop the root
53
+ passes through to it.
49
54
  - **A size is padding, never a height.** `Text` respects OS font scaling, so a
50
55
  fixed height clips the label at a large accessibility step instead of growing
51
56
  with it — and unlike `h-button-*` or `h-navbar-row`, a badge lines up against
@@ -81,3 +86,8 @@ A compact label for status, category or count. Compound root plus `Badge.Label`,
81
86
  - **String children** are wrapped in a `Badge.Label` automatically, consecutive
82
87
  strings collapsing into one — the same rule, and the same reason, as
83
88
  [`Button`](../button/AGENTS.md).
89
+
90
+ - **`material="etched"` squares the capsule and tints.** The root and the close button take
91
+ `rounded-sm`; `soft` status fills become `bg-<status>/8` in light and `/16` in dark, written out
92
+ per colour because Tailwind's scanner cannot see a built class. The material rides the badge
93
+ context so the close button follows the root.
@@ -30,13 +30,13 @@ export function BadgeCloseButton({
30
30
  feedback = "fade",
31
31
  ...props
32
32
  }: BadgeCloseButtonProps): ReactElement {
33
- const { size, isDisabled } = useBadgePart("Badge.CloseButton");
33
+ const { size, isDisabled, material } = useBadgePart("Badge.CloseButton");
34
34
 
35
35
  return (
36
36
  <Pressable
37
37
  accessibilityLabel={accessibilityLabel}
38
38
  accessibilityRole="button"
39
- className={badgeVariants({ size }).closeButton({ className })}
39
+ className={badgeVariants({ material, size }).closeButton({ className })}
40
40
  disabled={isDisabled}
41
41
  feedback={feedback}
42
42
  {...props}
@@ -1,5 +1,5 @@
1
1
  import { createContext, type ReactElement, type ReactNode, use } from "react";
2
- import type { BadgeColor, BadgeSize, BadgeVariant } from "./badge.variants";
2
+ import type { BadgeColor, BadgeMaterial, BadgeSize, BadgeVariant } from "./badge.variants";
3
3
 
4
4
  export type BadgeContextValue = {
5
5
  /** Size of the badge. */
@@ -10,6 +10,7 @@ export type BadgeContextValue = {
10
10
  color: BadgeColor;
11
11
  /** Whether the badge is disabled. */
12
12
  isDisabled: boolean;
13
+ material: BadgeMaterial;
13
14
  };
14
15
 
15
16
  const BadgeContext = createContext<BadgeContextValue | null>(null);
@@ -7,6 +7,7 @@ import { type BadgeContextValue, BadgeProvider } from "./badge.context";
7
7
  import {
8
8
  BADGE_FOREGROUND_TOKEN,
9
9
  type BadgeColor,
10
+ type BadgeMaterial,
10
11
  type BadgeSize,
11
12
  type BadgeVariant,
12
13
  badgeVariants,
@@ -23,21 +24,27 @@ export type BadgeProps = Omit<PressableProps, "asChild" | "busy" | "children" |
23
24
  /** What the surface means. */
24
25
  color?: BadgeColor;
25
26
  size?: BadgeSize;
27
+ /** `flat` by default, a capsule. `etched` squares the corner and tints soft status fills as an alpha. */
28
+ material?: BadgeMaterial;
26
29
  isDisabled?: boolean;
27
30
  /** Composes a trailing dismiss control in. Its press never reaches `onPress`. */
28
31
  onClose?: () => void;
29
32
  /** Name a screen reader gives the dismiss control. Defaults to `Remove`. */
30
33
  closeAccessibilityLabel?: string;
34
+ /** `testID` for the dismiss control, so a test or automation can press it. */
35
+ closeTestID?: string;
31
36
  children?: ReactNode;
32
37
  };
33
38
 
34
39
  function BadgeRoot({
35
40
  variant = "solid",
36
41
  color = "default",
42
+ material = "flat",
37
43
  size = "md",
38
44
  isDisabled = false,
39
45
  onClose,
40
46
  closeAccessibilityLabel,
47
+ closeTestID,
41
48
  onPress,
42
49
  onLongPress,
43
50
  haptic,
@@ -49,11 +56,11 @@ function BadgeRoot({
49
56
  ...props
50
57
  }: BadgeProps): ReactElement {
51
58
  const context = useMemo<BadgeContextValue>(
52
- () => ({ variant, color, size, isDisabled }),
53
- [variant, color, size, isDisabled]
59
+ () => ({ variant, color, size, isDisabled, material }),
60
+ [variant, color, size, isDisabled, material]
54
61
  );
55
62
 
56
- const slots = badgeVariants({ color, isDisabled, size, variant });
63
+ const slots = badgeVariants({ color, isDisabled, material, size, variant });
57
64
 
58
65
  // Icons composed into the badge adopt these unless told otherwise.
59
66
  const iconClassName = slots.icon();
@@ -70,7 +77,9 @@ function BadgeRoot({
70
77
  <IconDefaultsProvider value={iconDefaults}>
71
78
  <TextClassProvider value={slots.label()}>
72
79
  {content}
73
- {onClose ? <BadgeCloseButton accessibilityLabel={closeAccessibilityLabel} onPress={onClose} /> : null}
80
+ {onClose ? (
81
+ <BadgeCloseButton accessibilityLabel={closeAccessibilityLabel} onPress={onClose} testID={closeTestID} />
82
+ ) : null}
74
83
  </TextClassProvider>
75
84
  </IconDefaultsProvider>
76
85
  );