@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,180 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+ import type { SurfacePadding, SurfaceVariant } from "../surface/surface.variants";
4
+
5
+ /** What the alert says about the thing it describes. */
6
+ export const ALERT_STATUSES = ["default", "info", "success", "warning", "destructive"] as const;
7
+
8
+ /** The statuses that carry a colour — every one but `default`. */
9
+ export const ALERT_TINTED_STATUSES = ["info", "success", "warning", "destructive"] as const;
10
+
11
+ /**
12
+ * How the surface is painted. `soft` washes it in the status's soft fill;
13
+ * `surface` keeps the neutral fill the surface ladder gives it and colours only
14
+ * the indicator and the title.
15
+ */
16
+ export const ALERT_VARIANTS = ["soft", "surface"] as const;
17
+
18
+ export const ALERT_SIZES = ["sm", "md", "lg"] as const;
19
+
20
+ export type AlertStatus = (typeof ALERT_STATUSES)[number];
21
+ export type AlertVariant = (typeof ALERT_VARIANTS)[number];
22
+ export type AlertSize = (typeof ALERT_SIZES)[number];
23
+
24
+ /**
25
+ * Theme token for the indicator's glyph and the title's colour.
26
+ *
27
+ * One token per status, not per status and variant: `X-soft-foreground` is
28
+ * tuned to read on `X-soft` and on the neutral fills alike, so the same shade
29
+ * holds on both variants. A test pins each entry to the class the `title` slot
30
+ * emits, so the glyph and the words beside it cannot drift apart.
31
+ */
32
+ export const ALERT_FOREGROUND_TOKEN = {
33
+ default: "foreground",
34
+ info: "info-soft-foreground",
35
+ success: "success-soft-foreground",
36
+ warning: "warning-soft-foreground",
37
+ destructive: "destructive-soft-foreground",
38
+ } as const satisfies Record<AlertStatus, string>;
39
+
40
+ /** The surface's padding step for each alert size. */
41
+ export const ALERT_SURFACE_PADDING = {
42
+ sm: "sm",
43
+ md: "md",
44
+ lg: "lg",
45
+ } as const satisfies Record<AlertSize, SurfacePadding>;
46
+
47
+ /**
48
+ * Styling for every part of an alert.
49
+ *
50
+ * The root is laid over a `Surface`, which owns the corner, the padding and —
51
+ * unless the alert is tinted — the fill. This `tv()` adds only what an alert
52
+ * has and a surface does not: the row, the tint and the text treatment.
53
+ *
54
+ * A tinted alert (`soft`, with a status) swaps the card's fill for the status's
55
+ * soft fill and clears the card's hairline. A grey hairline around a red wash
56
+ * reads as two components stacked; the wash is its own edge. Everything else
57
+ * names no background, so the surface ladder picks one and an alert inside a
58
+ * card steps off the card rather than vanishing into it.
59
+ *
60
+ * `items-start`, not `items-center`: a description that wraps to four lines
61
+ * must not drag the glyph down to its middle. The indicator instead carries the
62
+ * title's line height as its own height and centres the glyph in it, which
63
+ * puts the glyph level with the first line at every size.
64
+ *
65
+ * No slot carries a fixed height apart from that indicator — `Text` respects OS
66
+ * font scaling, and a fixed box would clip a wrapped title.
67
+ *
68
+ * The root holds no `text-*` (rule 1). The title takes its status's colour,
69
+ * the description is always muted, so a long explanation never shouts.
70
+ *
71
+ * Free of React Native imports so it stays unit-testable — `bun test` cannot
72
+ * parse React Native's Flow-typed source. See AGENTS.md.
73
+ */
74
+ export const alertVariants = tv({
75
+ slots: {
76
+ root: "flex-row items-start",
77
+ indicator: "items-center justify-center",
78
+ content: "min-w-0 flex-1 gap-1",
79
+ title: "font-semibold",
80
+ description: "text-muted-foreground",
81
+ action: "mt-2 flex-row flex-wrap items-center gap-2",
82
+ closeButton: "items-center justify-center rounded-full",
83
+ /** Edge length an `Icon` composed into the alert inherits. */
84
+ icon: "",
85
+ },
86
+ variants: {
87
+ variant: {
88
+ soft: {},
89
+ surface: {},
90
+ },
91
+ // Written out rather than built from `ALERT_FOREGROUND_TOKEN`: Tailwind's
92
+ // scanner is static, and a class assembled at runtime is never compiled.
93
+ status: {
94
+ default: { title: "text-foreground" },
95
+ info: { title: "text-info-soft-foreground" },
96
+ success: { title: "text-success-soft-foreground" },
97
+ warning: { title: "text-warning-soft-foreground" },
98
+ destructive: { title: "text-destructive-soft-foreground" },
99
+ },
100
+ size: {
101
+ sm: {
102
+ root: "gap-2",
103
+ indicator: "h-5",
104
+ title: "text-sm",
105
+ description: "text-xs",
106
+ closeButton: "size-5",
107
+ icon: "size-icon-sm",
108
+ },
109
+ md: {
110
+ root: "gap-3",
111
+ indicator: "h-6",
112
+ title: "text-base",
113
+ description: "text-sm",
114
+ closeButton: "size-6",
115
+ icon: "size-icon-md",
116
+ },
117
+ lg: {
118
+ root: "gap-3.5",
119
+ indicator: "h-7",
120
+ title: "text-lg",
121
+ description: "text-base",
122
+ closeButton: "size-7",
123
+ icon: "size-icon-lg",
124
+ },
125
+ },
126
+ },
127
+ compoundVariants: [
128
+ { variant: "soft", status: "info", class: { root: "border-transparent bg-info-soft" } },
129
+ { variant: "soft", status: "success", class: { root: "border-transparent bg-success-soft" } },
130
+ { variant: "soft", status: "warning", class: { root: "border-transparent bg-warning-soft" } },
131
+ { variant: "soft", status: "destructive", class: { root: "border-transparent bg-destructive-soft" } },
132
+ ],
133
+ defaultVariants: {
134
+ variant: "soft",
135
+ status: "default",
136
+ size: "md",
137
+ },
138
+ });
139
+
140
+ export type AlertVariantProps = VariantProps<typeof alertVariants>;
141
+
142
+ /**
143
+ * Whether the alert paints its status's soft fill over the surface.
144
+ *
145
+ * Only a `soft` alert with a status is: a neutral alert has no tint to paint,
146
+ * and a `surface` alert keeps the ladder's fill on purpose.
147
+ */
148
+ export function resolveAlertTinted({ status, variant }: { status: AlertStatus; variant: AlertVariant }): boolean {
149
+ return variant === "soft" && status !== "default";
150
+ }
151
+
152
+ /**
153
+ * The `variant` the alert hands its `Surface`.
154
+ *
155
+ * A tinted alert pins it to `default`: the tint replaces the fill, and what
156
+ * nests inside then steps from the card like anything else on one. Every other
157
+ * alert leaves it `undefined`, so the surface reads the plane it sits on and
158
+ * steps to the next fill — an alert inside a card is told apart from the card
159
+ * with nothing said at the call site.
160
+ */
161
+ export function resolveAlertSurfaceVariant({
162
+ status,
163
+ variant,
164
+ }: {
165
+ status: AlertStatus;
166
+ variant: AlertVariant;
167
+ }): SurfaceVariant | undefined {
168
+ return resolveAlertTinted({ status, variant }) ? "default" : undefined;
169
+ }
170
+
171
+ /**
172
+ * How urgently Android's TalkBack announces a change inside the alert.
173
+ *
174
+ * A warning or a failure interrupts whatever is being read; anything else waits
175
+ * its turn. iOS has no live regions — the root's `alert` role is what VoiceOver
176
+ * reads there.
177
+ */
178
+ export function resolveAlertLiveRegion(status: AlertStatus): "polite" | "assertive" {
179
+ return status === "destructive" || status === "warning" ? "assertive" : "polite";
180
+ }
@@ -0,0 +1,23 @@
1
+ export { Alert, type AlertProps } from "./alert";
2
+ export { type AlertContextValue, AlertProvider, useAlert, useAlertContext } from "./alert.context";
3
+ export type { AlertSlotProps } from "./alert.types";
4
+ export {
5
+ ALERT_FOREGROUND_TOKEN,
6
+ ALERT_SIZES,
7
+ ALERT_STATUSES,
8
+ ALERT_SURFACE_PADDING,
9
+ ALERT_TINTED_STATUSES,
10
+ ALERT_VARIANTS,
11
+ type AlertSize,
12
+ type AlertStatus,
13
+ type AlertVariant,
14
+ type AlertVariantProps,
15
+ alertVariants,
16
+ resolveAlertLiveRegion,
17
+ resolveAlertSurfaceVariant,
18
+ resolveAlertTinted,
19
+ } from "./alert.variants";
20
+ export type { AlertCloseButtonProps } from "./alert-close-button";
21
+ export type { AlertDescriptionProps } from "./alert-description";
22
+ export type { AlertIndicatorProps } from "./alert-indicator";
23
+ export type { AlertTitleProps } from "./alert-title";
@@ -0,0 +1,106 @@
1
+ # Avatar
2
+
3
+ A person, as a picture — with an initials fallback, an overlay slot for a count
4
+ or a presence dot, and a group that stacks several faces and counts the rest.
5
+ Compound root plus `Avatar.Badge` and `Avatar.Group`.
6
+
7
+ `import { Avatar } from "@delacour/react-native-ui/avatar";`
8
+
9
+ ## Files
10
+
11
+ | File | What it holds |
12
+ | --- | --- |
13
+ | `index.ts` | → `@delacour/react-native-ui/avatar` |
14
+ | `avatar.tsx` | Root + the `Object.assign` compound surface |
15
+ | `avatar-badge.tsx` | `Avatar.Badge` — the corner overlay, a presence dot when empty |
16
+ | `avatar-group.tsx` | `Avatar.Group`, plus its internal `Item` and `Overflow` leaves |
17
+ | `avatar.context.tsx` | `AvatarProvider`, `AvatarGroupProvider`, `useAvatar()`, `useAvatarContext()`, `useAvatarGroupContext()`, `useAvatarPart()` |
18
+ | `avatar.variants.ts` | Pure `tv()` slots, `AVATAR_SIZE_POINTS`, `resolveAvatarSize`, `resolveAvatarOverlap`, `resolveAvatarInteractive` |
19
+ | `avatar.variants.test.ts` | |
20
+ | `avatar.utils.ts` | Pure non-styling logic — initials, the image-retry key, group overflow maths, the spoken label |
21
+ | `avatar.utils.test.ts` | |
22
+
23
+ ## Design
24
+
25
+ - **The fallback is always painted; the image goes on top.** The face renders
26
+ its initials (or a person glyph) first and mounts the `Image` absolutely over
27
+ them. A picture that is still loading, or that never loads, leaves the fallback
28
+ showing rather than an empty circle — there is no loading state to manage
29
+ because there is nothing to swap.
30
+ - **A failure is remembered by key, not by object.** `resolveAvatarSourceKey`
31
+ turns the source into a string — URI plus sorted headers, or the asset's module
32
+ id — and `onError` stores the key that failed. Keyed by object identity, an
33
+ inline `source={{ uri }}` is new every render and a dead URL would retry
34
+ forever; keyed by URI alone, a token refresh in the headers would never retry.
35
+ A consumer's `imageProps.onError` fires after the fallback has taken over.
36
+ - **The root carries the edge; there is no `self-start`.** A fixed width and
37
+ height is something a column's `stretch` never overrides, so the avatar keeps
38
+ its size without the `self-start` a [`Badge`](../badge/AGENTS.md) needs — and
39
+ it leaves the parent's alignment alone. With `self-start`, every avatar in an
40
+ `items-end` or `items-center` row was pinned to the row's top: mixed sizes lost
41
+ their baseline and a list row's avatar sat above its title. Found on the
42
+ simulator; a test now forbids any `self-*` on the root or the group.
43
+ - **Two boxes, not one.** `root` is unclipped and holds the overlays; `face` is
44
+ the clipped circle. An `Avatar.Badge` hangs over the circle's edge, and a
45
+ single clipped box would cut it in half. A test asserts `overflow-hidden` is on
46
+ `face` and never on `root`.
47
+ - **Initials are the first letter of the first and last word.** `Anahera Rose
48
+ Tui` is `AT`, the way a person signs, and `Mihi` is `M`.
49
+ `resolveAvatarInitials` walks code points rather than UTF-16 units, so a letter
50
+ outside the basic plane is never split into a replacement box, and skips
51
+ leading punctuation so `"(Aria)"` still gives `A`. `fallback` overrides the
52
+ drawn text only; `name` is still what a screen reader reads.
53
+ - **Two axes paint the fallback, on `Badge`'s six colours.** `variant` is `soft`
54
+ or `solid`, `color` is `default` … `info`, and the twelve cells live in
55
+ `compoundVariants` with the same fills `Badge` uses, so an avatar and a badge
56
+ of one colour read as one family. `AVATAR_FOREGROUND_TOKEN` gives the person
57
+ glyph the same shade as the initials; a test pins each entry to the token the
58
+ `fallbackLabel` slot resolves to, and checks every token exists in both themes.
59
+ - **A neutral face has a hairline edge; a photo does not.** `muted` and
60
+ `secondary` sit a percent or two from the page in light — on the simulator a
61
+ `default` fallback and the `+N` tile all but vanished. They draw
62
+ `border-border` inside the box, so nothing moves, and the edge goes while a
63
+ photo is mounted (`hasImage`), since a grey line around a face reads as a
64
+ frame. The coloured fallbacks have fill enough without one.
65
+ - **A size is a fixed edge, unlike a badge.** `sm`/`md`/`lg`/`xl` are
66
+ 32/40/48/64 points. Faces line up against each other and against list rows, and
67
+ a circle that grew with OS font scaling would break the stack; the initials
68
+ therefore set `allowFontScaling={false}` and are sized to fit the smallest
69
+ step. `AVATAR_SIZE_POINTS` restates the scale as numbers because the group's
70
+ overlap is a runtime margin and cannot be a class; a test pins the two
71
+ together.
72
+ - **An avatar is content until it is given something to do** — the rule
73
+ [`Badge`](../badge/AGENTS.md) follows. With no `onPress` or `onLongPress` it
74
+ is a plain `View` announced as an image (or not announced at all when it has
75
+ no name); supply either and it becomes a [`Pressable`](../pressable/AGENTS.md)
76
+ announced as a button. `resolveAvatarInteractive` is that decision.
77
+ - **The avatar is one element to assistive technology.** The root is
78
+ `accessible`, so an `Avatar.Badge` inside it is never focused on its own. A
79
+ presence dot has no words; put the status into the avatar's
80
+ `accessibilityLabel` (`"Aria Whitlock, online"`).
81
+ - **`Avatar.Badge` is a pin, and a dot only when empty.** Given children — a
82
+ `Badge` with a count, an `Icon` — it positions them and adds nothing else, so a
83
+ count keeps the look it has everywhere else. Empty, it draws a dot sized to
84
+ the face and ringed in `border-background`, which separates it from the photo
85
+ underneath. `placement` is `top-right` or `bottom-right`.
86
+ - **The group's ring belongs to the slot, not the face.** Each child is wrapped
87
+ in an item with `border-2 border-background`, so a face in a group keeps the
88
+ same edge it has alone, and the stack reads as separate people on any surface.
89
+ The `+N` tile sits in the same wrapper, so it lines up with the faces.
90
+ - **The first face is on top, and the list still reads first to last.**
91
+ `zIndex` counts down across the items instead of the children being reversed,
92
+ and the overlap is a negative `marginStart`, so the order in the tree — and
93
+ therefore the order a screen reader walks — is the written order in both
94
+ writing directions. The group is `role="list"` and each item `listitem`.
95
+ - **`max` caps the faces, not the row; `total` counts people never passed.**
96
+ Five children at `max={3}` is three faces and `+2`; three children with
97
+ `total={40}` is `+37`, without forty elements in the tree.
98
+ `resolveAvatarGroup` clamps a negative or fractional `max` and ignores a
99
+ `total` below the child count rather than drawing `+-2`.
100
+ - **The overflow tile draws at most `99+` and reads the real number.** Three
101
+ digits do not fit a small circle; a screen reader has room for `1234 more`.
102
+ `onOverflowPress` turns the tile into a button — to open the full list.
103
+ - **The overlap defaults to a third of the edge and is clamped.**
104
+ `resolveAvatarOverlap` never slides a face further than a whole face nor
105
+ backwards, and `overlap={0}` closes the stack into a plain row. A child's own
106
+ `size` still beats the group's, but the overlap is computed from the group's.
@@ -0,0 +1,51 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { cn } from "../../lib/cn";
4
+ import { useAvatarPart } from "./avatar.context";
5
+ import { type AvatarBadgePlacement, type AvatarColor, avatarVariants } from "./avatar.variants";
6
+
7
+ export type AvatarBadgeProps = ViewProps & {
8
+ /** Which corner the overlay is pinned to. Default `top-right`. */
9
+ placement?: AvatarBadgePlacement;
10
+ /** The presence dot's colour, when there are no children. Default `success`. */
11
+ color?: AvatarColor;
12
+ className?: string;
13
+ /** What to pin — a `Badge` with a count, an `Icon`. Leave empty for a presence dot. */
14
+ children?: ReactNode;
15
+ };
16
+
17
+ /**
18
+ * An overlay pinned to a corner of the avatar — an unread count, a presence dot.
19
+ *
20
+ * **Empty, it is a presence dot** sized to the face it sits on and ringed in the
21
+ * page background, so it reads as separate from the photo underneath. Given
22
+ * children, it is only the pin: a `Badge` or an `Icon` dropped in keeps its own
23
+ * look, which is what lets a count and a dot share one part.
24
+ *
25
+ * It sits in the avatar's unclipped root, never inside the circle, so it can
26
+ * hang over the edge without being cut in half.
27
+ *
28
+ * A dot says nothing to a screen reader on its own: the avatar is one element to
29
+ * assistive technology, so put the status into its `accessibilityLabel`.
30
+ */
31
+ export function AvatarBadge({
32
+ placement = "top-right",
33
+ color = "success",
34
+ className,
35
+ children,
36
+ ...props
37
+ }: AvatarBadgeProps): ReactElement {
38
+ const { size } = useAvatarPart("Avatar.Badge");
39
+ const slots = avatarVariants({ size, placement, color });
40
+
41
+ if (children === undefined || children === null || children === false) {
42
+ return <View className={cn(slots.badge(), slots.dot(), className)} pointerEvents="none" {...props} />;
43
+ }
44
+
45
+ return (
46
+ <View className={slots.badge({ className })} pointerEvents="box-none" {...props}>
47
+ {children}
48
+ </View>
49
+ );
50
+ }
51
+ AvatarBadge.displayName = "DelacourUI.Avatar.Badge";
@@ -0,0 +1,139 @@
1
+ import { Children, isValidElement, type ReactElement, type ReactNode, useMemo } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { Pressable } from "../pressable";
4
+ import { Text } from "../text";
5
+ import { type AvatarGroupContextValue, AvatarGroupProvider } from "./avatar.context";
6
+ import { resolveAvatarGroup, resolveAvatarOverflowLabel } from "./avatar.utils";
7
+ import { type AvatarSize, avatarVariants, resolveAvatarOverlap } from "./avatar.variants";
8
+
9
+ export type AvatarGroupProps = ViewProps & {
10
+ /** Size for every avatar in the stack. A child's own `size` still wins. Default `md`. */
11
+ size?: AvatarSize;
12
+ /** How many faces to show; the rest are counted into a trailing `+N`. Caps the faces, not the row. */
13
+ max?: number;
14
+ /** How many people there are, when the children are only the first few of them. */
15
+ total?: number;
16
+ /** Points each face slides under the one before it. Default a third of the size; `0` makes a plain row. */
17
+ overlap?: number;
18
+ /** Makes the `+N` tile a button — to open the full list, say. */
19
+ onOverflowPress?: () => void;
20
+ className?: string;
21
+ children?: ReactNode;
22
+ };
23
+
24
+ type AvatarGroupItemProps = {
25
+ /** Position in the stack, first face first. */
26
+ index: number;
27
+ /** How many items the stack holds, overflow tile included. */
28
+ count: number;
29
+ /** The overlap, in points. */
30
+ shift: number;
31
+ children: ReactNode;
32
+ };
33
+
34
+ /**
35
+ * One slot in the stack: the ring, the overlap and the stacking order.
36
+ *
37
+ * The ring is here rather than on the face so a face in a group is the same edge
38
+ * as one outside it. `zIndex` counts down so the first face sits on top without
39
+ * reversing the order a screen reader walks — the list is still read first to
40
+ * last, in both writing directions, because the overlap is a `marginStart`.
41
+ */
42
+ function AvatarGroupItem({ index, count, shift, children }: AvatarGroupItemProps): ReactElement {
43
+ return (
44
+ <View
45
+ className={avatarVariants().groupItem()}
46
+ role="listitem"
47
+ style={{ marginStart: index === 0 ? 0 : -shift, zIndex: count - index }}
48
+ >
49
+ {children}
50
+ </View>
51
+ );
52
+ }
53
+ AvatarGroupItem.displayName = "DelacourUI.Avatar.Group.Item";
54
+
55
+ type AvatarGroupOverflowProps = {
56
+ size: AvatarSize;
57
+ overflow: number;
58
+ onPress?: () => void;
59
+ };
60
+
61
+ /**
62
+ * The trailing `+N` tile, the same edge as a face.
63
+ *
64
+ * It draws at most `99+` and reads the real number, because three digits do not
65
+ * fit a small circle and a screen reader has room for all of them.
66
+ */
67
+ function AvatarGroupOverflow({ size, overflow, onPress }: AvatarGroupOverflowProps): ReactElement {
68
+ const slots = avatarVariants({ size });
69
+ const { text, accessibilityLabel } = resolveAvatarOverflowLabel(overflow);
70
+ const label = (
71
+ <Text allowFontScaling={false} className={slots.overflowLabel()} numberOfLines={1}>
72
+ {text}
73
+ </Text>
74
+ );
75
+
76
+ if (onPress) {
77
+ return (
78
+ <Pressable
79
+ accessibilityLabel={accessibilityLabel}
80
+ accessibilityRole="button"
81
+ className={slots.overflow()}
82
+ feedback="scale"
83
+ onPress={onPress}
84
+ >
85
+ {label}
86
+ </Pressable>
87
+ );
88
+ }
89
+
90
+ return (
91
+ <View accessibilityLabel={accessibilityLabel} accessible className={slots.overflow()}>
92
+ {label}
93
+ </View>
94
+ );
95
+ }
96
+ AvatarGroupOverflow.displayName = "DelacourUI.Avatar.Group.Overflow";
97
+
98
+ /**
99
+ * A row of avatars, each overlapping the one after it, with the people who did
100
+ * not fit counted at the end.
101
+ *
102
+ * Children are kept in the order they were written. `max` caps the faces and
103
+ * `total` says how many people there really are, so three children out of forty
104
+ * reads `+37` without forty elements in the tree.
105
+ */
106
+ export function AvatarGroup({
107
+ size,
108
+ max,
109
+ total,
110
+ overlap,
111
+ onOverflowPress,
112
+ className,
113
+ children,
114
+ ...props
115
+ }: AvatarGroupProps): ReactElement {
116
+ const items = Children.toArray(children).filter(isValidElement);
117
+ const { visible, overflow } = resolveAvatarGroup({ count: items.length, max, total });
118
+ const shift = resolveAvatarOverlap({ size: size ?? "md", overlap });
119
+ const count = visible + (overflow > 0 ? 1 : 0);
120
+ const context = useMemo<AvatarGroupContextValue>(() => ({ size }), [size]);
121
+
122
+ return (
123
+ <AvatarGroupProvider value={context}>
124
+ <View className={avatarVariants().group({ className })} role="list" {...props}>
125
+ {items.slice(0, visible).map((child, index) => (
126
+ <AvatarGroupItem count={count} index={index} key={child.key ?? index} shift={shift}>
127
+ {child}
128
+ </AvatarGroupItem>
129
+ ))}
130
+ {overflow > 0 ? (
131
+ <AvatarGroupItem count={count} index={visible} shift={shift}>
132
+ <AvatarGroupOverflow onPress={onOverflowPress} overflow={overflow} size={size ?? "md"} />
133
+ </AvatarGroupItem>
134
+ ) : null}
135
+ </View>
136
+ </AvatarGroupProvider>
137
+ );
138
+ }
139
+ AvatarGroup.displayName = "DelacourUI.Avatar.Group";
@@ -0,0 +1,93 @@
1
+ import { createContext, type ReactElement, type ReactNode, use } from "react";
2
+ import type { AvatarColor, AvatarSize, AvatarVariant } from "./avatar.variants";
3
+
4
+ export type AvatarContextValue = {
5
+ /** The avatar's resolved size — its own, else its group's, else `md`. */
6
+ size: AvatarSize;
7
+ /** How the fallback surface is painted. */
8
+ variant: AvatarVariant;
9
+ /** What the fallback surface means. */
10
+ color: AvatarColor;
11
+ /** Whether the avatar is disabled. */
12
+ isDisabled: boolean;
13
+ };
14
+
15
+ export type AvatarGroupContextValue = {
16
+ /** The size every avatar in the group takes unless it states its own. */
17
+ size?: AvatarSize;
18
+ };
19
+
20
+ const AvatarContext = createContext<AvatarContextValue | null>(null);
21
+ const AvatarGroupContext = createContext<AvatarGroupContextValue | null>(null);
22
+
23
+ /**
24
+ * Supplies the enclosing avatar's size, paint and state to its subtree.
25
+ *
26
+ * Lives in its own module, importing nothing but `avatar.variants`, so a part
27
+ * can read it without importing `./avatar`. That import would close a cycle, and
28
+ * Metro serves a partially initialised module for a cycle — leaving the context
29
+ * `undefined` at import time and red-boxing the app on a cold start.
30
+ */
31
+ export function AvatarProvider({ value, children }: { value: AvatarContextValue; children: ReactNode }): ReactElement {
32
+ return <AvatarContext value={value}>{children}</AvatarContext>;
33
+ }
34
+ AvatarProvider.displayName = "DelacourUI.Avatar.Provider";
35
+
36
+ /**
37
+ * Supplies a group's size to every avatar inside it.
38
+ *
39
+ * Separate from {@link AvatarProvider} because the two nest the other way round:
40
+ * a group holds avatars, and an avatar must not read a size its group never set
41
+ * as though it had been told one.
42
+ */
43
+ export function AvatarGroupProvider({
44
+ value,
45
+ children,
46
+ }: {
47
+ value: AvatarGroupContextValue;
48
+ children: ReactNode;
49
+ }): ReactElement {
50
+ return <AvatarGroupContext value={value}>{children}</AvatarGroupContext>;
51
+ }
52
+ AvatarGroupProvider.displayName = "DelacourUI.Avatar.Group.Provider";
53
+
54
+ /** The enclosing avatar's context, or null outside an `<Avatar>`. */
55
+ export function useAvatarContext(): AvatarContextValue | null {
56
+ return use(AvatarContext);
57
+ }
58
+
59
+ /** The enclosing group's context, or null outside an `<Avatar.Group>`. */
60
+ export function useAvatarGroupContext(): AvatarGroupContextValue | null {
61
+ return use(AvatarGroupContext);
62
+ }
63
+
64
+ /**
65
+ * Reads the enclosing avatar's size, paint and state.
66
+ *
67
+ * Lets a custom overlay size itself to the face it sits on without the avatar
68
+ * passing props down. Throws outside an `<Avatar>` — use
69
+ * {@link useAvatarContext} where the enclosing avatar is optional.
70
+ */
71
+ export function useAvatar(): AvatarContextValue {
72
+ const context = useAvatarContext();
73
+ if (!context) {
74
+ throw new Error("useAvatar must be called inside an <Avatar>.");
75
+ }
76
+ return context;
77
+ }
78
+
79
+ /**
80
+ * The enclosing avatar's context, for a compound part that cannot work without
81
+ * one.
82
+ *
83
+ * Internal: deliberately not re-exported from `index.ts`. A caller outside the
84
+ * library wants {@link useAvatar}, whose error message names the hook rather
85
+ * than a part.
86
+ */
87
+ export function useAvatarPart(component: string): AvatarContextValue {
88
+ const context = useAvatarContext();
89
+ if (!context) {
90
+ throw new Error(`${component} must be rendered inside an <Avatar>.`);
91
+ }
92
+ return context;
93
+ }