@delacour/react-native-ui 0.1.0-alpha.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (482) hide show
  1. package/README.md +11 -7
  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 +5 -0
  34. package/src/components/badge/badge.tsx +6 -1
  35. package/src/components/bottom-sheet/AGENTS.md +186 -221
  36. package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
  37. package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
  38. package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -207
  39. package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
  40. package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
  41. package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
  42. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -120
  43. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
  44. package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
  45. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
  46. package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
  47. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -52
  48. package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
  49. package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
  50. package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
  51. package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
  52. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
  53. package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
  54. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -70
  55. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -105
  56. package/src/components/bottom-sheet/index.ts +39 -17
  57. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
  58. package/src/components/button/AGENTS.md +45 -11
  59. package/src/components/button/button-group.tsx +44 -8
  60. package/src/components/button/button.context.tsx +10 -0
  61. package/src/components/button/button.tsx +2 -1
  62. package/src/components/button/button.variants.test.ts +154 -0
  63. package/src/components/button/button.variants.ts +71 -6
  64. package/src/components/button/index.ts +2 -0
  65. package/src/components/calendar/AGENTS.md +142 -0
  66. package/src/components/calendar/calendar-caption.tsx +116 -0
  67. package/src/components/calendar/calendar-day.tsx +103 -0
  68. package/src/components/calendar/calendar-grid.tsx +183 -0
  69. package/src/components/calendar/calendar-header.tsx +34 -0
  70. package/src/components/calendar/calendar-nav.tsx +51 -0
  71. package/src/components/calendar/calendar-picker.tsx +128 -0
  72. package/src/components/calendar/calendar-weekdays.tsx +48 -0
  73. package/src/components/calendar/calendar.context.tsx +108 -0
  74. package/src/components/calendar/calendar.date.test.ts +456 -0
  75. package/src/components/calendar/calendar.date.ts +584 -0
  76. package/src/components/calendar/calendar.tsx +389 -0
  77. package/src/components/calendar/calendar.types.ts +87 -0
  78. package/src/components/calendar/calendar.variants.test.ts +363 -0
  79. package/src/components/calendar/calendar.variants.ts +426 -0
  80. package/src/components/calendar/index.ts +96 -0
  81. package/src/components/card/AGENTS.md +88 -0
  82. package/src/components/card/card-action.tsx +16 -0
  83. package/src/components/card/card-content.tsx +12 -0
  84. package/src/components/card/card-description.tsx +12 -0
  85. package/src/components/card/card-footer.tsx +30 -0
  86. package/src/components/card/card-header.tsx +45 -0
  87. package/src/components/card/card-title.tsx +25 -0
  88. package/src/components/card/card.context.tsx +64 -0
  89. package/src/components/card/card.tsx +94 -0
  90. package/src/components/card/card.types.ts +13 -0
  91. package/src/components/card/card.variants.test.ts +240 -0
  92. package/src/components/card/card.variants.ts +177 -0
  93. package/src/components/card/index.ts +15 -0
  94. package/src/components/chart/AGENTS.md +9 -1
  95. package/src/components/chart/chart.tsx +8 -1
  96. package/src/components/chip/AGENTS.md +113 -0
  97. package/src/components/chip/chip-close-button.tsx +48 -0
  98. package/src/components/chip/chip-end-content.tsx +15 -0
  99. package/src/components/chip/chip-label.tsx +21 -0
  100. package/src/components/chip/chip-start-content.tsx +16 -0
  101. package/src/components/chip/chip.context.tsx +66 -0
  102. package/src/components/chip/chip.tsx +268 -0
  103. package/src/components/chip/chip.types.ts +10 -0
  104. package/src/components/chip/chip.variants.test.ts +335 -0
  105. package/src/components/chip/chip.variants.ts +321 -0
  106. package/src/components/chip/index.ts +24 -0
  107. package/src/components/collapsible/AGENTS.md +97 -0
  108. package/src/components/collapsible/collapsible-content.tsx +97 -0
  109. package/src/components/collapsible/collapsible-description.tsx +17 -0
  110. package/src/components/collapsible/collapsible-indicator.tsx +92 -0
  111. package/src/components/collapsible/collapsible-title.tsx +17 -0
  112. package/src/components/collapsible/collapsible-trigger.tsx +133 -0
  113. package/src/components/collapsible/collapsible.context.tsx +91 -0
  114. package/src/components/collapsible/collapsible.tsx +167 -0
  115. package/src/components/collapsible/collapsible.types.ts +9 -0
  116. package/src/components/collapsible/collapsible.variants.test.ts +338 -0
  117. package/src/components/collapsible/collapsible.variants.ts +228 -0
  118. package/src/components/collapsible/index.ts +32 -0
  119. package/src/components/context-menu/AGENTS.md +103 -0
  120. package/src/components/context-menu/context-menu-content.tsx +64 -0
  121. package/src/components/context-menu/context-menu-item.tsx +22 -0
  122. package/src/components/context-menu/context-menu-preview.tsx +60 -0
  123. package/src/components/context-menu/context-menu-trigger.tsx +160 -0
  124. package/src/components/context-menu/context-menu.context.tsx +91 -0
  125. package/src/components/context-menu/context-menu.tsx +90 -0
  126. package/src/components/context-menu/context-menu.variants.test.ts +266 -0
  127. package/src/components/context-menu/context-menu.variants.ts +147 -0
  128. package/src/components/context-menu/index.ts +18 -0
  129. package/src/components/dialog/AGENTS.md +97 -0
  130. package/src/components/dialog/dialog-body.tsx +21 -0
  131. package/src/components/dialog/dialog-close.tsx +82 -0
  132. package/src/components/dialog/dialog-content.tsx +153 -0
  133. package/src/components/dialog/dialog-description.tsx +23 -0
  134. package/src/components/dialog/dialog-footer.tsx +32 -0
  135. package/src/components/dialog/dialog-header.tsx +20 -0
  136. package/src/components/dialog/dialog-title.tsx +42 -0
  137. package/src/components/dialog/dialog-trigger.tsx +54 -0
  138. package/src/components/dialog/dialog.context.tsx +90 -0
  139. package/src/components/dialog/dialog.tsx +119 -0
  140. package/src/components/dialog/dialog.variants.test.ts +224 -0
  141. package/src/components/dialog/dialog.variants.ts +173 -0
  142. package/src/components/dialog/index.ts +30 -0
  143. package/src/components/drawer/AGENTS.md +123 -0
  144. package/src/components/drawer/drawer-body.tsx +46 -0
  145. package/src/components/drawer/drawer-close.tsx +76 -0
  146. package/src/components/drawer/drawer-content.tsx +186 -0
  147. package/src/components/drawer/drawer-description.tsx +23 -0
  148. package/src/components/drawer/drawer-footer.tsx +21 -0
  149. package/src/components/drawer/drawer-header.tsx +42 -0
  150. package/src/components/drawer/drawer-title.tsx +35 -0
  151. package/src/components/drawer/drawer-trigger.tsx +54 -0
  152. package/src/components/drawer/drawer.context.tsx +114 -0
  153. package/src/components/drawer/drawer.tsx +119 -0
  154. package/src/components/drawer/drawer.variants.test.ts +392 -0
  155. package/src/components/drawer/drawer.variants.ts +244 -0
  156. package/src/components/drawer/index.ts +41 -0
  157. package/src/components/drawer/use-drawer-pan.ts +108 -0
  158. package/src/components/empty-state/AGENTS.md +90 -0
  159. package/src/components/empty-state/empty-state-content.tsx +17 -0
  160. package/src/components/empty-state/empty-state-description.tsx +12 -0
  161. package/src/components/empty-state/empty-state-header.tsx +17 -0
  162. package/src/components/empty-state/empty-state-media.tsx +56 -0
  163. package/src/components/empty-state/empty-state-title.tsx +18 -0
  164. package/src/components/empty-state/empty-state.context.tsx +65 -0
  165. package/src/components/empty-state/empty-state.tsx +84 -0
  166. package/src/components/empty-state/empty-state.types.ts +14 -0
  167. package/src/components/empty-state/empty-state.variants.test.ts +245 -0
  168. package/src/components/empty-state/empty-state.variants.ts +132 -0
  169. package/src/components/empty-state/index.ts +20 -0
  170. package/src/components/fab/AGENTS.md +119 -0
  171. package/src/components/fab/fab-action.tsx +118 -0
  172. package/src/components/fab/fab-group.tsx +214 -0
  173. package/src/components/fab/fab-label.tsx +18 -0
  174. package/src/components/fab/fab.context.tsx +125 -0
  175. package/src/components/fab/fab.tsx +156 -0
  176. package/src/components/fab/fab.types.ts +17 -0
  177. package/src/components/fab/fab.variants.test.ts +274 -0
  178. package/src/components/fab/fab.variants.ts +234 -0
  179. package/src/components/fab/index.ts +37 -0
  180. package/src/components/feedback/AGENTS.md +95 -0
  181. package/src/components/feedback/feedback-action.tsx +16 -0
  182. package/src/components/feedback/feedback-cancel.tsx +34 -0
  183. package/src/components/feedback/feedback-close.tsx +21 -0
  184. package/src/components/feedback/feedback-content.tsx +24 -0
  185. package/src/components/feedback/feedback-field.tsx +70 -0
  186. package/src/components/feedback/feedback-footer.tsx +21 -0
  187. package/src/components/feedback/feedback-panel.tsx +54 -0
  188. package/src/components/feedback/feedback-submit.tsx +74 -0
  189. package/src/components/feedback/feedback-title.tsx +34 -0
  190. package/src/components/feedback/feedback-trigger.tsx +20 -0
  191. package/src/components/feedback/feedback.context.tsx +73 -0
  192. package/src/components/feedback/feedback.tsx +135 -0
  193. package/src/components/feedback/feedback.variants.test.ts +185 -0
  194. package/src/components/feedback/feedback.variants.ts +128 -0
  195. package/src/components/feedback/index.ts +24 -0
  196. package/src/components/field/AGENTS.md +19 -0
  197. package/src/components/field/field-label.tsx +19 -5
  198. package/src/components/field/field.context.tsx +17 -0
  199. package/src/components/field/field.tsx +15 -2
  200. package/src/components/field/field.variants.test.ts +24 -0
  201. package/src/components/field/field.variants.ts +20 -0
  202. package/src/components/field/index.ts +1 -0
  203. package/src/components/item/AGENTS.md +92 -0
  204. package/src/components/item/index.ts +25 -0
  205. package/src/components/item/item-actions.tsx +28 -0
  206. package/src/components/item/item-content.tsx +15 -0
  207. package/src/components/item/item-description.tsx +12 -0
  208. package/src/components/item/item-footer.tsx +13 -0
  209. package/src/components/item/item-group.tsx +32 -0
  210. package/src/components/item/item-header.tsx +13 -0
  211. package/src/components/item/item-media.tsx +37 -0
  212. package/src/components/item/item-separator.tsx +13 -0
  213. package/src/components/item/item-title.tsx +17 -0
  214. package/src/components/item/item.context.tsx +66 -0
  215. package/src/components/item/item.tsx +218 -0
  216. package/src/components/item/item.types.ts +12 -0
  217. package/src/components/item/item.variants.test.ts +263 -0
  218. package/src/components/item/item.variants.ts +200 -0
  219. package/src/components/kpi/AGENTS.md +125 -0
  220. package/src/components/kpi/index.ts +45 -0
  221. package/src/components/kpi/kpi-action.tsx +12 -0
  222. package/src/components/kpi/kpi-content.tsx +35 -0
  223. package/src/components/kpi/kpi-footer.tsx +52 -0
  224. package/src/components/kpi/kpi-group.tsx +70 -0
  225. package/src/components/kpi/kpi-header.tsx +25 -0
  226. package/src/components/kpi/kpi-icon.tsx +28 -0
  227. package/src/components/kpi/kpi-sparkline.tsx +145 -0
  228. package/src/components/kpi/kpi-stat.tsx +20 -0
  229. package/src/components/kpi/kpi-title.tsx +19 -0
  230. package/src/components/kpi/kpi-trend.tsx +105 -0
  231. package/src/components/kpi/kpi-value.tsx +32 -0
  232. package/src/components/kpi/kpi.context.tsx +111 -0
  233. package/src/components/kpi/kpi.tsx +154 -0
  234. package/src/components/kpi/kpi.types.ts +9 -0
  235. package/src/components/kpi/kpi.variants.test.ts +428 -0
  236. package/src/components/kpi/kpi.variants.ts +347 -0
  237. package/src/components/label/AGENTS.md +89 -0
  238. package/src/components/label/index.ts +10 -0
  239. package/src/components/label/label.tsx +97 -0
  240. package/src/components/label/label.variants.test.ts +176 -0
  241. package/src/components/label/label.variants.ts +123 -0
  242. package/src/components/menu/AGENTS.md +176 -0
  243. package/src/components/menu/index.ts +44 -0
  244. package/src/components/menu/menu-background.tsx +17 -0
  245. package/src/components/menu/menu-checkbox-item.tsx +57 -0
  246. package/src/components/menu/menu-content.tsx +303 -0
  247. package/src/components/menu/menu-item.tsx +162 -0
  248. package/src/components/menu/menu-label.tsx +14 -0
  249. package/src/components/menu/menu-radio-group.tsx +50 -0
  250. package/src/components/menu/menu-radio-item.tsx +57 -0
  251. package/src/components/menu/menu-separator.tsx +11 -0
  252. package/src/components/menu/menu-sub-content.tsx +72 -0
  253. package/src/components/menu/menu-sub-trigger.tsx +50 -0
  254. package/src/components/menu/menu-sub.tsx +75 -0
  255. package/src/components/menu/menu-trigger.tsx +57 -0
  256. package/src/components/menu/menu.context.tsx +214 -0
  257. package/src/components/menu/menu.tsx +88 -0
  258. package/src/components/menu/menu.types.ts +33 -0
  259. package/src/components/menu/menu.variants.test.ts +400 -0
  260. package/src/components/menu/menu.variants.ts +353 -0
  261. package/src/components/meter/AGENTS.md +135 -0
  262. package/src/components/meter/index.ts +43 -0
  263. package/src/components/meter/meter-fill.tsx +19 -0
  264. package/src/components/meter/meter-header.tsx +17 -0
  265. package/src/components/meter/meter-label.tsx +18 -0
  266. package/src/components/meter/meter-output.tsx +42 -0
  267. package/src/components/meter/meter-segments.tsx +83 -0
  268. package/src/components/meter/meter-track.tsx +16 -0
  269. package/src/components/meter/meter.context.tsx +62 -0
  270. package/src/components/meter/meter.tsx +286 -0
  271. package/src/components/meter/meter.types.ts +30 -0
  272. package/src/components/meter/meter.variants.test.ts +430 -0
  273. package/src/components/meter/meter.variants.ts +347 -0
  274. package/src/components/overlay/AGENTS.md +89 -0
  275. package/src/components/overlay/index.ts +27 -0
  276. package/src/components/overlay/overlay-portal.tsx +84 -0
  277. package/src/components/overlay/overlay-presence.test.ts +46 -0
  278. package/src/components/overlay/overlay-presence.ts +49 -0
  279. package/src/components/overlay/overlay-registry.test.ts +96 -0
  280. package/src/components/overlay/overlay-registry.ts +99 -0
  281. package/src/components/overlay/overlay-scrim.tsx +49 -0
  282. package/src/components/overlay/overlay.context.tsx +48 -0
  283. package/src/components/overlay/overlay.tsx +118 -0
  284. package/src/components/overlay/overlay.variants.test.ts +37 -0
  285. package/src/components/overlay/overlay.variants.ts +33 -0
  286. package/src/components/overlay/use-overlay-back-handler.ts +34 -0
  287. package/src/components/overlay/use-overlay-presence.ts +87 -0
  288. package/src/components/popover/AGENTS.md +138 -0
  289. package/src/components/popover/index.ts +48 -0
  290. package/src/components/popover/popover-anchor.tsx +49 -0
  291. package/src/components/popover/popover-arrow.tsx +89 -0
  292. package/src/components/popover/popover-close.tsx +73 -0
  293. package/src/components/popover/popover-content.tsx +235 -0
  294. package/src/components/popover/popover-description.tsx +18 -0
  295. package/src/components/popover/popover-title.tsx +33 -0
  296. package/src/components/popover/popover-trigger.tsx +56 -0
  297. package/src/components/popover/popover.context.tsx +71 -0
  298. package/src/components/popover/popover.position.test.ts +354 -0
  299. package/src/components/popover/popover.position.ts +286 -0
  300. package/src/components/popover/popover.tsx +123 -0
  301. package/src/components/popover/popover.variants.test.ts +135 -0
  302. package/src/components/popover/popover.variants.ts +85 -0
  303. package/src/components/popover/use-anchor-measure.ts +76 -0
  304. package/src/components/popover/use-anchored-content.ts +219 -0
  305. package/src/components/pressable/AGENTS.md +11 -1
  306. package/src/components/pressable/pressable.tsx +7 -2
  307. package/src/components/pressable/pressable.variants.test.ts +29 -0
  308. package/src/components/pressable/pressable.variants.ts +17 -0
  309. package/src/components/progress/AGENTS.md +120 -0
  310. package/src/components/progress/index.ts +40 -0
  311. package/src/components/progress/progress-fill.tsx +152 -0
  312. package/src/components/progress/progress-header.tsx +19 -0
  313. package/src/components/progress/progress-label.tsx +27 -0
  314. package/src/components/progress/progress-output.tsx +47 -0
  315. package/src/components/progress/progress-track.tsx +38 -0
  316. package/src/components/progress/progress.context.tsx +79 -0
  317. package/src/components/progress/progress.tsx +171 -0
  318. package/src/components/progress/progress.types.ts +23 -0
  319. package/src/components/progress/progress.variants.test.ts +335 -0
  320. package/src/components/progress/progress.variants.ts +284 -0
  321. package/src/components/progress-button/AGENTS.md +116 -0
  322. package/src/components/progress-button/index.ts +36 -0
  323. package/src/components/progress-button/progress-button-done.tsx +33 -0
  324. package/src/components/progress-button/progress-button-label.tsx +27 -0
  325. package/src/components/progress-button/progress-button.context.tsx +93 -0
  326. package/src/components/progress-button/progress-button.tsx +414 -0
  327. package/src/components/progress-button/progress-button.variants.test.ts +314 -0
  328. package/src/components/progress-button/progress-button.variants.ts +236 -0
  329. package/src/components/provider/AGENTS.md +38 -25
  330. package/src/components/provider/provider.tsx +38 -11
  331. package/src/components/radio/AGENTS.md +8 -10
  332. package/src/components/radio/radio.variants.test.ts +5 -6
  333. package/src/components/radio/radio.variants.ts +6 -7
  334. package/src/components/rating/AGENTS.md +143 -0
  335. package/src/components/rating/index.ts +43 -0
  336. package/src/components/rating/rating-output.tsx +39 -0
  337. package/src/components/rating/rating-star.tsx +70 -0
  338. package/src/components/rating/rating-stars.tsx +219 -0
  339. package/src/components/rating/rating.context.tsx +79 -0
  340. package/src/components/rating/rating.tsx +163 -0
  341. package/src/components/rating/rating.variants.test.ts +347 -0
  342. package/src/components/rating/rating.variants.ts +354 -0
  343. package/src/components/screen/screen-navbar-back-button.tsx +1 -1
  344. package/src/components/selection-mode/AGENTS.md +155 -0
  345. package/src/components/selection-mode/index.ts +43 -0
  346. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  347. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  348. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  349. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  350. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  351. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  352. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  353. package/src/components/selection-mode/selection-mode.tsx +207 -0
  354. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  355. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  356. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  357. package/src/components/skeleton/AGENTS.md +120 -0
  358. package/src/components/skeleton/index.ts +15 -0
  359. package/src/components/skeleton/skeleton-clock.ts +48 -0
  360. package/src/components/skeleton/skeleton-group.tsx +64 -0
  361. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  362. package/src/components/skeleton/skeleton.context.tsx +45 -0
  363. package/src/components/skeleton/skeleton.tsx +227 -0
  364. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  365. package/src/components/skeleton/skeleton.variants.ts +222 -0
  366. package/src/components/slide-button/AGENTS.md +122 -0
  367. package/src/components/slide-button/index.ts +43 -0
  368. package/src/components/slide-button/slide-button-label.tsx +70 -0
  369. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  370. package/src/components/slide-button/slide-button.context.tsx +76 -0
  371. package/src/components/slide-button/slide-button.tsx +457 -0
  372. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  373. package/src/components/slide-button/slide-button.variants.ts +324 -0
  374. package/src/components/slider/index.ts +2 -0
  375. package/src/components/slider/slider-thumb.tsx +33 -1
  376. package/src/components/slider/slider.variants.test.ts +52 -0
  377. package/src/components/slider/slider.variants.ts +56 -2
  378. package/src/components/stack-card/AGENTS.md +169 -0
  379. package/src/components/stack-card/index.ts +37 -0
  380. package/src/components/stack-card/stack-card-action.tsx +66 -0
  381. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  382. package/src/components/stack-card/stack-card-card.tsx +71 -0
  383. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  384. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  385. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  386. package/src/components/stack-card/stack-card.context.tsx +117 -0
  387. package/src/components/stack-card/stack-card.tsx +557 -0
  388. package/src/components/stack-card/stack-card.types.ts +26 -0
  389. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  390. package/src/components/stack-card/stack-card.variants.ts +360 -0
  391. package/src/components/steps/AGENTS.md +153 -0
  392. package/src/components/steps/index.ts +45 -0
  393. package/src/components/steps/steps-connector.tsx +31 -0
  394. package/src/components/steps/steps-description.tsx +26 -0
  395. package/src/components/steps/steps-indicator.tsx +106 -0
  396. package/src/components/steps/steps-item.tsx +225 -0
  397. package/src/components/steps/steps-panel.tsx +41 -0
  398. package/src/components/steps/steps-title.tsx +32 -0
  399. package/src/components/steps/steps-track.tsx +59 -0
  400. package/src/components/steps/steps.context.tsx +123 -0
  401. package/src/components/steps/steps.tsx +166 -0
  402. package/src/components/steps/steps.variants.test.ts +388 -0
  403. package/src/components/steps/steps.variants.ts +468 -0
  404. package/src/components/surface/AGENTS.md +72 -0
  405. package/src/components/surface/index.ts +15 -0
  406. package/src/components/surface/surface.context.tsx +54 -0
  407. package/src/components/surface/surface.tsx +70 -0
  408. package/src/components/surface/surface.variants.test.ts +180 -0
  409. package/src/components/surface/surface.variants.ts +133 -0
  410. package/src/components/swipe/AGENTS.md +140 -0
  411. package/src/components/swipe/index.ts +31 -0
  412. package/src/components/swipe/swipe-action.tsx +99 -0
  413. package/src/components/swipe/swipe-end.tsx +14 -0
  414. package/src/components/swipe/swipe-group.tsx +55 -0
  415. package/src/components/swipe/swipe-panel.tsx +55 -0
  416. package/src/components/swipe/swipe-start.tsx +15 -0
  417. package/src/components/swipe/swipe.context.tsx +135 -0
  418. package/src/components/swipe/swipe.tsx +393 -0
  419. package/src/components/swipe/swipe.types.ts +7 -0
  420. package/src/components/swipe/swipe.variants.test.ts +337 -0
  421. package/src/components/swipe/swipe.variants.ts +290 -0
  422. package/src/components/switch/switch.variants.ts +2 -2
  423. package/src/components/tabs/AGENTS.md +4 -3
  424. package/src/components/tabs/tabs.variants.test.ts +1 -1
  425. package/src/components/tabs/tabs.variants.ts +4 -6
  426. package/src/components/textarea/AGENTS.md +86 -0
  427. package/src/components/textarea/index.ts +16 -0
  428. package/src/components/textarea/textarea.tsx +157 -0
  429. package/src/components/textarea/textarea.variants.test.ts +219 -0
  430. package/src/components/textarea/textarea.variants.ts +171 -0
  431. package/src/components/toast/AGENTS.md +140 -0
  432. package/src/components/toast/index.ts +74 -0
  433. package/src/components/toast/toast-action.tsx +59 -0
  434. package/src/components/toast/toast-api.ts +32 -0
  435. package/src/components/toast/toast-close.tsx +44 -0
  436. package/src/components/toast/toast-content.tsx +11 -0
  437. package/src/components/toast/toast-description.tsx +11 -0
  438. package/src/components/toast/toast-indicator.tsx +34 -0
  439. package/src/components/toast/toast-item.tsx +308 -0
  440. package/src/components/toast/toast-title.tsx +13 -0
  441. package/src/components/toast/toast-viewport.tsx +135 -0
  442. package/src/components/toast/toast.context.tsx +69 -0
  443. package/src/components/toast/toast.store.test.ts +259 -0
  444. package/src/components/toast/toast.store.ts +338 -0
  445. package/src/components/toast/toast.timer.test.ts +64 -0
  446. package/src/components/toast/toast.timer.ts +51 -0
  447. package/src/components/toast/toast.tsx +132 -0
  448. package/src/components/toast/toast.variants.test.ts +393 -0
  449. package/src/components/toast/toast.variants.ts +304 -0
  450. package/src/components/toggle-button/AGENTS.md +106 -0
  451. package/src/components/toggle-button/index.ts +30 -0
  452. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  453. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  454. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  455. package/src/components/toggle-button/toggle-button.tsx +172 -0
  456. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  457. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  458. package/src/components/tooltip/AGENTS.md +133 -0
  459. package/src/components/tooltip/index.ts +28 -0
  460. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  461. package/src/components/tooltip/tooltip-content.tsx +179 -0
  462. package/src/components/tooltip/tooltip-description.tsx +19 -0
  463. package/src/components/tooltip/tooltip-text.tsx +21 -0
  464. package/src/components/tooltip/tooltip-title.tsx +20 -0
  465. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  466. package/src/components/tooltip/tooltip.context.tsx +68 -0
  467. package/src/components/tooltip/tooltip.tsx +213 -0
  468. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  469. package/src/components/tooltip/tooltip.variants.ts +177 -0
  470. package/src/hooks/use-calm-motion.tsx +31 -0
  471. package/src/lib/calm-motion.test.ts +17 -0
  472. package/src/lib/calm-motion.ts +23 -0
  473. package/src/lib/cn.test.ts +14 -0
  474. package/src/lib/cn.ts +2 -2
  475. package/src/no-gorhom.test.ts +54 -0
  476. package/src/styles/geometry.test.ts +1 -0
  477. package/src/styles/theme.css +4 -0
  478. package/src/styles/tokens.css +11 -0
  479. package/src/styles/tokens.test.ts +22 -0
  480. package/src/styles/tokens.ts +20 -0
  481. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  482. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -1,40 +1,62 @@
1
- export { BottomSheet, type BottomSheetProps } from "./bottom-sheet";
2
1
  export {
3
- type BottomSheetContainerContextValue,
4
- BottomSheetContainerProvider,
2
+ type BottomSheetAnimatedValue,
5
3
  type BottomSheetContextValue,
6
- type BottomSheetHandle,
7
- type BottomSheetPortalContextValue,
8
- BottomSheetPortalProvider,
9
- BottomSheetProvider,
4
+ type BottomSheetHostProps,
5
+ type BottomSheetPortalProps,
6
+ type BottomSheetRef,
7
+ type BottomSheetRegistryValue,
8
+ type BottomSheetTextInputHandlers,
9
+ defineSheetMachine,
10
+ type SheetEvent,
11
+ type SheetMachine,
12
+ type SheetMachineConfig,
13
+ type SheetMachineSnapshot,
14
+ type SheetStepController,
15
+ type SheetStepTransition,
10
16
  useBottomSheet,
11
- useBottomSheetContainerContext,
12
- useBottomSheetContext,
13
- useBottomSheetPortalContext,
14
- } from "./bottom-sheet.context";
17
+ useBottomSheetAnimated,
18
+ useBottomSheetRegistry,
19
+ useBottomSheetTextInput,
20
+ useOptionalBottomSheet,
21
+ useOptionalBottomSheet as useBottomSheetContext,
22
+ useSheetMachine,
23
+ useSheetStep,
24
+ } from "@delacour/react-native-bottom-sheet";
25
+ export { resolveSheetBottomInset, resolveSheetScrollEndPadding } from "@delacour/react-native-bottom-sheet/core";
26
+ export { BottomSheet, type BottomSheetProps } from "./bottom-sheet";
15
27
  export {
16
28
  BOTTOM_SHEET_BACKDROP_INDICES,
17
29
  BOTTOM_SHEET_CLOSE_HIT_SLOP,
18
30
  BOTTOM_SHEET_FOOTER_GAP,
19
31
  BOTTOM_SHEET_FOOTER_PADDING,
20
- BOTTOM_SHEET_KEYBOARD_DEFAULTS,
21
32
  BOTTOM_SHEET_OVERLAY_OPACITY,
22
33
  BOTTOM_SHEET_OVERLAY_TOKEN,
23
- type BottomSheetFooterFlag,
24
- type BottomSheetFooterPlacement,
25
34
  type BottomSheetVariantProps,
26
35
  bottomSheetVariants,
27
- resolveFooterPlacement,
28
- resolveSheetBottomInset,
29
36
  } from "./bottom-sheet.variants";
37
+ export type { BottomSheetBackgroundProps } from "./bottom-sheet-background";
30
38
  export type { BottomSheetCloseProps } from "./bottom-sheet-close";
31
39
  export type { BottomSheetContainerProps } from "./bottom-sheet-container";
32
40
  export type { BottomSheetContentProps } from "./bottom-sheet-content";
33
41
  export type { BottomSheetDescriptionProps } from "./bottom-sheet-description";
42
+ export type { BottomSheetFlatListProps } from "./bottom-sheet-flat-list";
34
43
  export type { BottomSheetFooterProps } from "./bottom-sheet-footer";
44
+ export type { BottomSheetHandleProps } from "./bottom-sheet-handle";
45
+ export type { BottomSheetLegendListProps } from "./bottom-sheet-legend-list";
35
46
  export type { BottomSheetOverlayProps } from "./bottom-sheet-overlay";
36
- export type { BottomSheetPortalProps } from "./bottom-sheet-portal";
47
+ export { BottomSheetProvider, type BottomSheetProviderProps } from "./bottom-sheet-provider";
37
48
  export type { BottomSheetScrollViewProps } from "./bottom-sheet-scroll-view";
49
+ export type { BottomSheetSectionListProps } from "./bottom-sheet-section-list";
50
+ export type { BottomSheetStepProps, BottomSheetStepsProps } from "./bottom-sheet-steps";
51
+ export type { BottomSheetTextInputProps } from "./bottom-sheet-text-input";
38
52
  export type { BottomSheetTitleProps } from "./bottom-sheet-title";
39
53
  export type { BottomSheetTriggerProps } from "./bottom-sheet-trigger";
40
54
  export { type BottomSheetInputHandlers, useBottomSheetInput } from "./use-bottom-sheet-input";
55
+
56
+ /**
57
+ * The imperative handle a `BottomSheet` ref exposes.
58
+ *
59
+ * @deprecated The engine's name is `BottomSheetRef`; this alias stays for the
60
+ * callers that typed the old name and goes with the next major.
61
+ */
62
+ export type BottomSheetHandle = import("@delacour/react-native-bottom-sheet").BottomSheetRef;
@@ -1,125 +1,26 @@
1
- import { useBottomSheetInternal } from "@gorhom/bottom-sheet";
2
- import { type RefCallback, useCallback, useRef } from "react";
3
- import { type BlurEvent, type FocusEvent, findNodeHandle, TextInput } from "react-native";
1
+ import { type BottomSheetTextInputHandlers, useBottomSheetTextInput } from "@delacour/react-native-bottom-sheet";
4
2
 
5
- /**
6
- * `findNodeHandle` for a value React Native's own types do not admit.
7
- *
8
- * `TextInput.State.currentlyFocusedInput()` returns a Fabric host instance, and
9
- * the shipped signature still describes the pre-Fabric parameter — the runtime
10
- * accepts it, which is what gorhom's own `BottomSheetTextInput` relies on. One
11
- * cast, in one place, rather than at each call.
12
- */
13
- function nodeHandleOf(instance: unknown): number | null {
14
- return findNodeHandle(instance as Parameters<typeof findNodeHandle>[0]);
15
- }
16
-
17
- export type BottomSheetInputHandlers = {
18
- onFocus: (event: FocusEvent) => void;
19
- onBlur: (event: BlurEvent) => void;
20
- ref: RefCallback<TextInput | null>;
21
- };
3
+ export type BottomSheetInputHandlers = BottomSheetTextInputHandlers;
22
4
 
23
5
  /**
24
- * Makes this package's `Input` behave like a text field the sheet owns.
6
+ * Registers a text field as the sheet's own, so the keyboard it raises is the
7
+ * sheet's to lift for.
25
8
  *
26
- * Spread the result onto any `TextInput` inside a `BottomSheet`:
9
+ * The engine's `useBottomSheetTextInput()` under the name this library shipped
10
+ * it as. Spread the result onto any `Input` or `TextInput`:
27
11
  *
28
12
  * ```tsx
29
- * <Input {...useBottomSheetInput()} placeholder="Name" />
13
+ * const sheetInput = useBottomSheetInput();
14
+ * <Input {...sheetInput} placeholder="Name" />
30
15
  * ```
31
16
  *
32
- * A sheet only knows to grow for the keyboard when it knows the focused field is
33
- * one of its own. gorhom ships `BottomSheetTextInput` to register that, but
34
- * reaching for it here would mean this library's `Input` — its variants, its
35
- * `Input.Group`, its invalid and disabled cascade — could not be used inside a
36
- * sheet at all. So the registration is exposed as handlers instead, and `Input`
37
- * keeps knowing nothing about `@gorhom/bottom-sheet`.
38
- *
39
- * Three props, and all three are load-bearing:
17
+ * All three handlers matter: `ref` puts the field's native node in the sheet's
18
+ * registry, `onFocus` claims the keyboard, and `onBlur` gives it up — unless
19
+ * focus moved to another registered field in the same sheet, which is what
20
+ * keeps a tap from one field to the next from reading as a close and a reopen.
21
+ * Outside a sheet the handlers are inert, so a form shared between a screen
22
+ * and a sheet needs no branch.
40
23
  *
41
- * - `ref` registers the field's native node with the sheet. Without it, moving
42
- * focus from one field in the sheet to the next reads as the keyboard closing
43
- * and reopening, and the sheet resizes twice on the way.
44
- * - `onFocus` tells the sheet which field to keep clear of the keyboard.
45
- * - `onBlur` gives that up again, unless focus went to another field in the same
46
- * sheet.
47
- *
48
- * Called outside a sheet it returns inert handlers rather than throwing, so a
49
- * form component shared between a screen and a sheet needs no branch.
50
- *
51
- * It covers the keyboard half only. A *drag* across a field can still be claimed
52
- * by the sheet's content pan, because gorhom's input is built on Gesture
53
- * Handler's `TextInput` and this one is not; if that ever bites, the fix is
54
- * `enableContentPanningGesture={false}` on the `BottomSheet.Container`. The
55
- * *tap* half — a scrim that used to swallow the press meant for the field — is
56
- * solved in `BottomSheet.Overlay`, not here.
57
- *
58
- * @example
59
- * <Field>
60
- * <Field.Label>Name</Field.Label>
61
- * <Input {...useBottomSheetInput()} onChangeText={setName} value={name} />
62
- * </Field>
24
+ * `BottomSheet.TextInput` is this over `Input`, for the plain case.
63
25
  */
64
- export function useBottomSheetInput(): BottomSheetInputHandlers {
65
- const internal = useBottomSheetInternal(true);
66
- // The node this field last registered, so the ref can withdraw exactly it.
67
- // A JS ref rather than a shared value: nothing renders differently for it.
68
- const registeredNodeRef = useRef<number | null>(null);
69
-
70
- const ref = useCallback<RefCallback<TextInput | null>>(
71
- (instance) => {
72
- const nodes = internal?.textInputNodesRef.current;
73
- const previous = registeredNodeRef.current;
74
-
75
- if (previous !== null) {
76
- nodes?.delete(previous);
77
- registeredNodeRef.current = null;
78
- if (internal !== null && internal.animatedKeyboardState.get().target === previous) {
79
- internal.animatedKeyboardState.set((state) => ({ ...state, target: undefined }));
80
- }
81
- }
82
-
83
- if (instance === null || nodes === undefined) return;
84
-
85
- const node = nodeHandleOf(instance);
86
- if (node === null) return;
87
-
88
- nodes.add(node);
89
- registeredNodeRef.current = node;
90
- },
91
- [internal]
92
- );
93
-
94
- const onFocus = useCallback(
95
- (event: FocusEvent) => {
96
- if (internal === null) return;
97
- const target = event.nativeEvent.target;
98
- internal.animatedKeyboardState.set((state) => ({ ...state, target }));
99
- },
100
- [internal]
101
- );
102
-
103
- const onBlur = useCallback(
104
- (event: BlurEvent) => {
105
- if (internal === null) return;
106
-
107
- const target = event.nativeEvent.target;
108
- const isThisFieldsTarget = internal.animatedKeyboardState.get().target === target;
109
- if (!isThisFieldsTarget) return;
110
-
111
- // React Native's own focus registry, not the sheet's: focus may already
112
- // have moved to the next field in the same sheet, and giving the target
113
- // up there would close and reopen the sheet's keyboard allowance between
114
- // two taps that never dismissed the keyboard.
115
- const focused = TextInput.State.currentlyFocusedInput();
116
- const focusedNode = focused === null ? null : nodeHandleOf(focused);
117
- if (focusedNode !== null && internal.textInputNodesRef.current.has(focusedNode)) return;
118
-
119
- internal.animatedKeyboardState.set((state) => ({ ...state, target: undefined }));
120
- },
121
- [internal]
122
- );
123
-
124
- return { onBlur, onFocus, ref };
125
- }
26
+ export const useBottomSheetInput: () => BottomSheetInputHandlers = useBottomSheetTextInput;
@@ -194,20 +194,54 @@ for a chunk that says something rather than doing something. An
194
194
  - **The group paints nothing.** No background, no border, no disabled fade. A
195
195
  disabled group publishes `isDisabled` and each member fades itself; a group
196
196
  fading as well would compound the two down to a quarter opacity. It carries no
197
- `gap` either — a gap is the seam this component exists to close — and no
198
- `overflow-hidden`, which would square off the very corners the position
197
+ `gap` while attached — a gap is the seam this component exists to close — and
198
+ no `overflow-hidden`, which would square off the very corners the position
199
199
  compounds just rounded.
200
200
  - **No `accessibilityRole` on the group.** Announcing a control with no action
201
201
  in front of every member helps nobody; the members are already buttons.
202
- - **Horizontal groups are content-width** (`self-start`), because that is what a
203
- segmented control is. For a run that fills its parent, put `className="w-full"`
204
- on the group and `className="flex-1"` on each member — **and make sure the
205
- parent has a definite width**. Yoga resolves a percentage against the parent's
206
- content box, so `w-full` inside a container that is itself content-sized
207
- resolves to nothing and falls back to the content width: the group collapses
208
- onto its buttons and a `flex-1` field between them is squeezed to a few points.
209
- It looks like the group ignored `w-full`. It did not; there was no width to
210
- take a percentage of.
202
+ - **`isAttached={false}` shares the props and drops the shape.** A toolbar of
203
+ `Find · Export · Share` wants one `variant` and one `size` without becoming a
204
+ segmented control. Detached, `resolveGroupPositions` places every member
205
+ `only` — the lone member's existing position, which draws the lone button's
206
+ `rounded-button-*` — rather than minting a fourth sentinel, and
207
+ `resolveGroupSeams` returns no seams, so nothing overlaps into the gap. The gap
208
+ is `gap-2` on either axis, a `tv` variant on the `group` slot and never inline.
209
+ `Button.Group.Separator` still renders, as a rule between spaced members.
210
+ - **A detached member presses with `scale` again.** The fade exists only because
211
+ a scaling member tears its seam, and a detached run has none.
212
+ `resolveButtonFeedback` takes `isAttached` as a fourth argument, defaulting to
213
+ `true`, rather than turning into an options object: its positional signature
214
+ is public, `Input` and the existing tests call it, and the new rung is the only
215
+ thing that changed.
216
+ - **`isFullWidth` splits the parent equally, not by content.** A
217
+ `Monthly · Annual · One-off` picker sized to its labels moves its dividers when
218
+ the copy changes, and a wider segment reads as more important. The group trades
219
+ `self-start` for `w-full self-stretch` — declared after `orientation` in
220
+ `buttonVariants`, so tailwind-merge drops `self-start` rather than keeping it —
221
+ and every member of a horizontal run gets `flex-1 basis-0`. **`basis-0` is the
222
+ part that matters**: with `flex-1` alone Yoga shares out only the space left
223
+ after each member's content width, and the widths still follow the labels.
224
+ - **A member learns it stretches from context, as `isStretched`.**
225
+ `resolveGroupMemberStretch` decides it once per group, it rides on the
226
+ per-member context beside `position` and `isSeamed`, and `Button` feeds it to
227
+ an `isStretched` variant on its own `tv`. No `cloneElement`, for the reason
228
+ above. A caller's `className="flex-none"` on one member still wins — the
229
+ variant is merged before the caller's class. `Button.Group.Text` and the
230
+ separator do **not** stretch: a caption and a rule keep their content width
231
+ and the buttons share what is left. `Input` does not read it yet; a caller
232
+ sizes a joined field with its own `className`.
233
+ - **`isFullWidth` on a vertical group widens the group and does nothing else.**
234
+ Its members already stretch on the cross axis, and `basis-0` on the main axis
235
+ of a column with no definite height would collapse every member to nothing —
236
+ so `isStretched` is `false` there by construction.
237
+ - **Horizontal groups are content-width by default** (`self-start`), because
238
+ that is what a segmented control is. `isFullWidth` fills the parent — **and the
239
+ parent must have a definite width**. Yoga resolves a percentage against the
240
+ parent's content box, so `w-full` inside a container that is itself
241
+ content-sized resolves to nothing and falls back to the content width: the
242
+ group collapses onto its buttons and a `flex-1` field between them is squeezed
243
+ to a few points. It looks like the group ignored `isFullWidth`. It did not;
244
+ there was no width to take a percentage of.
211
245
  - **`Button.Group.Text` draws the button's own chrome**, not chrome of its own,
212
246
  which is what keeps its height, padding and corner identical to the buttons
213
247
  beside it — those come off one axis, and restating any of them here is how a
@@ -13,6 +13,7 @@ import {
13
13
  type ButtonSize,
14
14
  type ButtonVariant,
15
15
  buttonVariants,
16
+ resolveGroupMemberStretch,
16
17
  resolveGroupPositions,
17
18
  resolveGroupSeams,
18
19
  } from "./button.variants";
@@ -30,6 +31,18 @@ export type ButtonGroupProps = ViewProps & {
30
31
  isDisabled?: boolean;
31
32
  /** Press treatment for every member. Unset, a joined member fades rather than scaling. */
32
33
  feedback?: PressableFeedback;
34
+ /**
35
+ * Join the members into one shape. On by default, because that is what a group is. Off keeps the
36
+ * inherited `variant` / `size` / `isDisabled` / `feedback` and drops the joined shape: every
37
+ * member keeps its own corner, no seam overlap, and the run takes a `gap`.
38
+ */
39
+ isAttached?: boolean;
40
+ /**
41
+ * Span the parent and split it equally between members. Horizontal only — a vertical group
42
+ * already stretches its members, so there it only widens the group. The parent needs a
43
+ * definite width.
44
+ */
45
+ isFullWidth?: boolean;
33
46
  className?: string;
34
47
  children?: ReactNode;
35
48
  };
@@ -40,13 +53,15 @@ function ButtonGroupRoot({
40
53
  variant,
41
54
  isDisabled,
42
55
  feedback,
56
+ isAttached = true,
57
+ isFullWidth = false,
43
58
  className,
44
59
  children,
45
60
  ...props
46
61
  }: ButtonGroupProps): ReactElement {
47
62
  const group = useMemo<ButtonGroupContextValue>(
48
- () => ({ feedback, isDisabled, orientation, size, variant }),
49
- [feedback, isDisabled, orientation, size, variant]
63
+ () => ({ feedback, isAttached, isDisabled, isFullWidth, orientation, size, variant }),
64
+ [feedback, isAttached, isDisabled, isFullWidth, orientation, size, variant]
50
65
  );
51
66
 
52
67
  const items = useMemo(() => Children.toArray(children), [children]);
@@ -56,17 +71,18 @@ function ButtonGroupRoot({
56
71
  // the members' order actually change.
57
72
  const values = useMemo(() => {
58
73
  const isMember = items.map(isGroupMember);
59
- const positions = resolveGroupPositions(isMember);
60
- const seams = resolveGroupSeams(isMember);
74
+ const positions = resolveGroupPositions(isMember, group);
75
+ const seams = resolveGroupSeams(isMember, group);
76
+ const isStretched = resolveGroupMemberStretch(group);
61
77
 
62
78
  return positions.map<ButtonGroupItemContextValue | null>((position, index) =>
63
- position === null ? null : { ...group, isSeamed: seams[index] === true, position }
79
+ position === null ? null : { ...group, isSeamed: seams[index] === true, isStretched, position }
64
80
  );
65
81
  }, [items, group]);
66
82
 
67
83
  return (
68
84
  <ButtonGroupProvider value={group}>
69
- <View className={buttonVariants({ orientation }).group({ className })} {...props}>
85
+ <View className={buttonVariants({ isAttached, isFullWidth, orientation }).group({ className })} {...props}>
70
86
  {items.map((child, index) => {
71
87
  const value = values[index];
72
88
  if (!value) return child;
@@ -130,8 +146,14 @@ function keyOf(child: ReactNode, index: number): string {
130
146
  * The group paints nothing itself — no background, no border, no disabled fade.
131
147
  * A fade here would compound with the members' own down to a quarter opacity.
132
148
  *
133
- * Horizontal groups are content-width. For a run that fills its parent, put
134
- * `className="w-full"` on the group and `className="flex-1"` on each member.
149
+ * Horizontal groups are content-width. `isFullWidth` spans the parent and
150
+ * splits it equally between members — `flex-1 basis-0` on each, published as
151
+ * `isStretched` on the member's context — whatever their labels say. The parent
152
+ * needs a definite width for there to be anything to split.
153
+ *
154
+ * `isAttached={false}` keeps the shared axes and drops the shape: every member
155
+ * draws its own corner, nothing overlaps, the run takes a gap, and an unset
156
+ * `feedback` goes back to the lone button's `scale`.
135
157
  *
136
158
  * @example
137
159
  * <Button.Group variant="outline">
@@ -141,6 +163,20 @@ function keyOf(child: ReactNode, index: number): string {
141
163
  * </Button.Group>
142
164
  *
143
165
  * @example
166
+ * <Button.Group isAttached={false} size="sm" variant="outline">
167
+ * <Button onPress={find}>Find</Button>
168
+ * <Button onPress={exportFile}>Export</Button>
169
+ * <Button onPress={share}>Share</Button>
170
+ * </Button.Group>
171
+ *
172
+ * @example
173
+ * <Button.Group isFullWidth variant="outline">
174
+ * <Button onPress={monthly}>Monthly</Button>
175
+ * <Button onPress={annual}>Annual</Button>
176
+ * <Button onPress={once}>One-off</Button>
177
+ * </Button.Group>
178
+ *
179
+ * @example
144
180
  * <Button.Group>
145
181
  * <Button onPress={save}>Save</Button>
146
182
  * <Button.Group.Separator />
@@ -76,6 +76,10 @@ export type ButtonGroupContextValue = {
76
76
  isDisabled?: boolean;
77
77
  /** Press treatment for members. Unset falls back to `fade` — see `resolveButtonFeedback`. */
78
78
  feedback?: PressableFeedback;
79
+ /** Members join into one shape. False, each keeps its own corner and the run takes a gap. */
80
+ isAttached: boolean;
81
+ /** The group spans its parent. Horizontal members then split it — see `isStretched`. */
82
+ isFullWidth: boolean;
79
83
  };
80
84
 
81
85
  export type ButtonGroupItemContextValue = ButtonGroupContextValue & {
@@ -83,6 +87,12 @@ export type ButtonGroupItemContextValue = ButtonGroupContextValue & {
83
87
  position: ButtonGroupPosition;
84
88
  /** Overlap the member before it, so the shared edge is drawn once. */
85
89
  isSeamed: boolean;
90
+ /**
91
+ * Take an equal share of a full-width horizontal run — `flex-1 basis-0`.
92
+ * A custom member that should split the run reads this; one that should
93
+ * keep its content width ignores it, as `Button.Group.Text` does.
94
+ */
95
+ isStretched: boolean;
86
96
  };
87
97
 
88
98
  const ButtonGroupContext = createContext<ButtonGroupContextValue | null>(null);
@@ -74,6 +74,7 @@ function ButtonRoot({
74
74
  isDisabled: resolvedIsDisabled,
75
75
  isLoading,
76
76
  isSeamed: item?.isSeamed ?? false,
77
+ isStretched: item?.isStretched ?? false,
77
78
  orientation: item?.orientation ?? "horizontal",
78
79
  size: resolvedSize,
79
80
  variant: resolvedVariant,
@@ -96,7 +97,7 @@ function ButtonRoot({
96
97
  busy={isLoading}
97
98
  className={slots.root({ className })}
98
99
  disabled={resolvedIsDisabled}
99
- feedback={resolveButtonFeedback(feedback, item?.feedback, item !== null)}
100
+ feedback={resolveButtonFeedback(feedback, item?.feedback, item !== null, item?.isAttached)}
100
101
  {...props}
101
102
  >
102
103
  <IconDefaultsProvider value={iconDefaults}>
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { cn } from "../../lib/cn";
2
3
  import { BUTTON_RADIUS_TOKENS, ICON_SIZE_TOKENS } from "../../styles/tokens";
3
4
  import {
4
5
  BUTTON_FOREGROUND_TOKEN,
@@ -16,6 +17,7 @@ import {
16
17
  resolveButtonFeedback,
17
18
  resolveButtonLayout,
18
19
  resolveGroupedButtonSize,
20
+ resolveGroupMemberStretch,
19
21
  resolveGroupPositions,
20
22
  resolveGroupSeams,
21
23
  resolveSpinnerSwapIndex,
@@ -809,3 +811,155 @@ describe("resolveGroupedButtonSize", () => {
809
811
  }
810
812
  });
811
813
  });
814
+
815
+ describe("a detached group", () => {
816
+ test("places every member on its own", () => {
817
+ // `only` is the lone member's position and draws the lone button's corner,
818
+ // so a spaced run is a row of whole buttons rather than a joined shape with
819
+ // gaps punched in it.
820
+ expect(resolveGroupPositions([true, true, true], { isAttached: false })).toEqual(["only", "only", "only"]);
821
+ expect(resolveGroupPositions([true, false, true], { isAttached: false })).toEqual(["only", null, "only"]);
822
+ });
823
+
824
+ test("still skips a separator", () => {
825
+ expect(resolveGroupPositions([false, true, false], { isAttached: false })).toEqual([null, "only", null]);
826
+ });
827
+
828
+ test("joins by default", () => {
829
+ expect(resolveGroupPositions([true, true], { isAttached: true })).toEqual(["first", "last"]);
830
+ expect(resolveGroupPositions([true, true], {})).toEqual(["first", "last"]);
831
+ });
832
+
833
+ test("overlaps nothing", () => {
834
+ // There is no shared edge to draw once, and a point of overlap would eat
835
+ // into the gap instead.
836
+ for (const shape of SHAPES) {
837
+ expect(resolveGroupSeams(shape, { isAttached: false }).every((seamed) => !seamed)).toBe(true);
838
+ }
839
+ expect(resolveGroupSeams([true, true, true], { isAttached: true })).toEqual([false, true, true]);
840
+ });
841
+
842
+ test("draws each member's own corner", () => {
843
+ for (const size of BUTTON_SIZES) {
844
+ const step = size.startsWith("icon-") ? size.slice("icon-".length) : size;
845
+ const cls = buttonVariants({ groupPosition: "only", size }).root();
846
+ expect(cls).toContain(`rounded-button-${step}`);
847
+ expect(cls).not.toMatch(/\brounded-[sebt]-/);
848
+ }
849
+ });
850
+
851
+ test("spaces its members with a gap on either axis", () => {
852
+ for (const orientation of BUTTON_GROUP_ORIENTATIONS) {
853
+ expect(buttonVariants({ isAttached: false, orientation }).group()).toMatch(/\bgap-2\b/);
854
+ }
855
+ });
856
+
857
+ test("an attached group still holds no gap", () => {
858
+ for (const orientation of BUTTON_GROUP_ORIENTATIONS) {
859
+ expect(buttonVariants({ isAttached: true, orientation }).group()).not.toMatch(/\bgap-/);
860
+ }
861
+ });
862
+ });
863
+
864
+ describe("resolveButtonFeedback in a detached group", () => {
865
+ test("falls back to the lone button's scale", () => {
866
+ // Tearing a seam is impossible with no seam, so the group rung is dropped.
867
+ expect(resolveButtonFeedback(undefined, undefined, true, false)).toBe("scale");
868
+ });
869
+
870
+ test("still fades when attached", () => {
871
+ expect(resolveButtonFeedback(undefined, undefined, true, true)).toBe("fade");
872
+ });
873
+
874
+ test("defaults to attached, which is what a group is", () => {
875
+ expect(resolveButtonFeedback(undefined, undefined, true)).toBe("fade");
876
+ });
877
+
878
+ test("honours an explicit `none` either way", () => {
879
+ for (const isAttached of [true, false]) {
880
+ expect(resolveButtonFeedback("none", undefined, true, isAttached)).toBe("none");
881
+ expect(resolveButtonFeedback(undefined, "none", true, isAttached)).toBe("none");
882
+ }
883
+ });
884
+
885
+ test("still lets the group or the member name a treatment", () => {
886
+ expect(resolveButtonFeedback(undefined, "fade", true, false)).toBe("fade");
887
+ expect(resolveButtonFeedback("scale-fade", "fade", true, false)).toBe("scale-fade");
888
+ });
889
+ });
890
+
891
+ describe("a full-width group", () => {
892
+ test("spans its parent instead of hugging its content", () => {
893
+ const cls = buttonVariants({ isFullWidth: true, orientation: "horizontal" }).group();
894
+ expect(cls).toMatch(/\bw-full\b/);
895
+ expect(cls).toMatch(/\bself-stretch\b/);
896
+ expect(cls).not.toMatch(/\bself-start\b/);
897
+ });
898
+
899
+ test("fills the width when vertical too", () => {
900
+ const cls = buttonVariants({ isFullWidth: true, orientation: "vertical" }).group();
901
+ expect(cls).toMatch(/\bw-full\b/);
902
+ expect(cls).toMatch(/\bself-stretch\b/);
903
+ });
904
+
905
+ test("hugs its content by default", () => {
906
+ const cls = buttonVariants({ orientation: "horizontal" }).group();
907
+ expect(cls).toMatch(/\bself-start\b/);
908
+ expect(cls).not.toMatch(/\bw-full\b/);
909
+ });
910
+
911
+ test("keeps a gap when it is also detached", () => {
912
+ const cls = buttonVariants({ isAttached: false, isFullWidth: true }).group();
913
+ expect(cls).toMatch(/\bgap-2\b/);
914
+ expect(cls).toMatch(/\bw-full\b/);
915
+ });
916
+ });
917
+
918
+ describe("a stretched member", () => {
919
+ test("splits the run equally rather than by content", () => {
920
+ // `basis-0` is the half that matters: with `flex-1` alone Yoga shares out
921
+ // only the space left over, and the widths still follow the labels.
922
+ const cls = buttonVariants({ isStretched: true }).root();
923
+ expect(cls).toMatch(/\bflex-1\b/);
924
+ expect(cls).toMatch(/\bbasis-0\b/);
925
+ });
926
+
927
+ test("does not stretch by default", () => {
928
+ const cls = buttonVariants().root();
929
+ expect(cls).not.toMatch(/\bflex-1\b/);
930
+ expect(cls).not.toMatch(/\bbasis-0\b/);
931
+ });
932
+
933
+ test("yields to a caller's flex", () => {
934
+ const slot = buttonVariants({ isStretched: true }).root({ className: "flex-none" });
935
+ expect(slot).toMatch(/\bflex-none\b/);
936
+ expect(slot).not.toMatch(/\bflex-1\b/);
937
+ const merged = cn(buttonVariants({ isStretched: true }).root(), "flex-none");
938
+ expect(merged).toMatch(/\bflex-none\b/);
939
+ expect(merged).not.toMatch(/\bflex-1\b/);
940
+ });
941
+
942
+ test("keeps its own corner and height", () => {
943
+ const cls = buttonVariants({ groupPosition: "first", isStretched: true, size: "md" }).root();
944
+ expect(cls).toContain("h-button-md");
945
+ expect(cls).toContain("rounded-s-button-md");
946
+ });
947
+ });
948
+
949
+ describe("resolveGroupMemberStretch", () => {
950
+ test("stretches members of a full-width horizontal run", () => {
951
+ expect(resolveGroupMemberStretch({ isFullWidth: true, orientation: "horizontal" })).toBe(true);
952
+ });
953
+
954
+ test("leaves a vertical run to the cross axis it already stretches on", () => {
955
+ // `basis-0` on the main axis of a column with no definite height would
956
+ // collapse every member to nothing.
957
+ expect(resolveGroupMemberStretch({ isFullWidth: true, orientation: "vertical" })).toBe(false);
958
+ });
959
+
960
+ test("stretches nothing by default", () => {
961
+ for (const orientation of BUTTON_GROUP_ORIENTATIONS) {
962
+ expect(resolveGroupMemberStretch({ isFullWidth: false, orientation })).toBe(false);
963
+ }
964
+ });
965
+ });