@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
@@ -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);
@@ -7,6 +7,7 @@ import { type ButtonContextValue, ButtonProvider, useButtonGroupItemContext } fr
7
7
  import {
8
8
  BUTTON_FOREGROUND_TOKEN,
9
9
  type ButtonLayout,
10
+ type ButtonMaterial,
10
11
  type ButtonSize,
11
12
  type ButtonSpinnerPlacement,
12
13
  type ButtonVariant,
@@ -23,6 +24,8 @@ import { ButtonStartContent } from "./button-start-content";
23
24
 
24
25
  export type ButtonProps = Omit<PressableProps, "busy" | "children" | "disabled" | "pressedOpacity" | "pressedScale"> & {
25
26
  variant?: ButtonVariant;
27
+ /** `flat` by default; `etched` sets a highlight into the fill's edge. */
28
+ material?: ButtonMaterial;
26
29
  /** Height, label step, icon step, corner and padding on one axis. `icon-*` is a square. */
27
30
  size?: ButtonSize;
28
31
  isDisabled?: boolean;
@@ -37,6 +40,7 @@ export type ButtonProps = Omit<PressableProps, "busy" | "children" | "disabled"
37
40
 
38
41
  function ButtonRoot({
39
42
  variant,
43
+ material = "flat",
40
44
  size,
41
45
  isDisabled,
42
46
  isLoading = false,
@@ -74,6 +78,8 @@ function ButtonRoot({
74
78
  isDisabled: resolvedIsDisabled,
75
79
  isLoading,
76
80
  isSeamed: item?.isSeamed ?? false,
81
+ isStretched: item?.isStretched ?? false,
82
+ material,
77
83
  orientation: item?.orientation ?? "horizontal",
78
84
  size: resolvedSize,
79
85
  variant: resolvedVariant,
@@ -96,7 +102,7 @@ function ButtonRoot({
96
102
  busy={isLoading}
97
103
  className={slots.root({ className })}
98
104
  disabled={resolvedIsDisabled}
99
- feedback={resolveButtonFeedback(feedback, item?.feedback, item !== null)}
105
+ feedback={resolveButtonFeedback(feedback, item?.feedback, item !== null, item?.isAttached)}
100
106
  {...props}
101
107
  >
102
108
  <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,
@@ -7,6 +8,7 @@ import {
7
8
  BUTTON_GROUP_SEPARATOR_ORIENTATION,
8
9
  BUTTON_ICON_SIZES,
9
10
  BUTTON_LABEL_SIZES,
11
+ BUTTON_MATERIALS,
10
12
  BUTTON_SIZES,
11
13
  BUTTON_SPINNER_PLACEMENTS,
12
14
  BUTTON_VARIANTS,
@@ -16,6 +18,7 @@ import {
16
18
  resolveButtonFeedback,
17
19
  resolveButtonLayout,
18
20
  resolveGroupedButtonSize,
21
+ resolveGroupMemberStretch,
19
22
  resolveGroupPositions,
20
23
  resolveGroupSeams,
21
24
  resolveSpinnerSwapIndex,
@@ -809,3 +812,190 @@ describe("resolveGroupedButtonSize", () => {
809
812
  }
810
813
  });
811
814
  });
815
+
816
+ describe("the etched button material", () => {
817
+ test("flat is the default and adds no shadow", () => {
818
+ for (const variant of BUTTON_VARIANTS) {
819
+ expect(buttonVariants({ variant }).root()).toBe(buttonVariants({ variant, material: "flat" }).root());
820
+ expect(buttonVariants({ variant }).root()).not.toMatch(/shadow/);
821
+ }
822
+ });
823
+
824
+ test("etched primary carries the white inset highlight", () => {
825
+ const cls = buttonVariants({ material: "etched", variant: "primary" }).root();
826
+ expect(cls).toContain("shadow-etched-primary");
827
+ expect(cls).not.toContain("shadow-etched ");
828
+ });
829
+
830
+ test("etched filled and outlined buttons carry the light and dark edge", () => {
831
+ for (const variant of ["secondary", "tertiary", "outline"] as const) {
832
+ const cls = buttonVariants({ material: "etched", variant }).root();
833
+ expect(cls).toContain("shadow-etched");
834
+ expect(cls).toContain("dark:shadow-etched-dark");
835
+ }
836
+ });
837
+
838
+ // A ghost has no surface to lift, and a destructive fill is a signal colour
839
+ // whose highlight would read as a second state.
840
+ test("ghost and destructive variants stay flat", () => {
841
+ for (const variant of ["ghost", "destructive", "destructive-soft"] as const) {
842
+ expect(buttonVariants({ material: "etched", variant }).root()).not.toMatch(/shadow/);
843
+ }
844
+ });
845
+
846
+ test("lists every material", () => {
847
+ expect(BUTTON_MATERIALS).toEqual(["flat", "etched"]);
848
+ });
849
+ });
850
+
851
+ describe("a detached group", () => {
852
+ test("places every member on its own", () => {
853
+ // `only` is the lone member's position and draws the lone button's corner,
854
+ // so a spaced run is a row of whole buttons rather than a joined shape with
855
+ // gaps punched in it.
856
+ expect(resolveGroupPositions([true, true, true], { isAttached: false })).toEqual(["only", "only", "only"]);
857
+ expect(resolveGroupPositions([true, false, true], { isAttached: false })).toEqual(["only", null, "only"]);
858
+ });
859
+
860
+ test("still skips a separator", () => {
861
+ expect(resolveGroupPositions([false, true, false], { isAttached: false })).toEqual([null, "only", null]);
862
+ });
863
+
864
+ test("joins by default", () => {
865
+ expect(resolveGroupPositions([true, true], { isAttached: true })).toEqual(["first", "last"]);
866
+ expect(resolveGroupPositions([true, true], {})).toEqual(["first", "last"]);
867
+ });
868
+
869
+ test("overlaps nothing", () => {
870
+ // There is no shared edge to draw once, and a point of overlap would eat
871
+ // into the gap instead.
872
+ for (const shape of SHAPES) {
873
+ expect(resolveGroupSeams(shape, { isAttached: false }).every((seamed) => !seamed)).toBe(true);
874
+ }
875
+ expect(resolveGroupSeams([true, true, true], { isAttached: true })).toEqual([false, true, true]);
876
+ });
877
+
878
+ test("draws each member's own corner", () => {
879
+ for (const size of BUTTON_SIZES) {
880
+ const step = size.startsWith("icon-") ? size.slice("icon-".length) : size;
881
+ const cls = buttonVariants({ groupPosition: "only", size }).root();
882
+ expect(cls).toContain(`rounded-button-${step}`);
883
+ expect(cls).not.toMatch(/\brounded-[sebt]-/);
884
+ }
885
+ });
886
+
887
+ test("spaces its members with a gap on either axis", () => {
888
+ for (const orientation of BUTTON_GROUP_ORIENTATIONS) {
889
+ expect(buttonVariants({ isAttached: false, orientation }).group()).toMatch(/\bgap-2\b/);
890
+ }
891
+ });
892
+
893
+ test("an attached group still holds no gap", () => {
894
+ for (const orientation of BUTTON_GROUP_ORIENTATIONS) {
895
+ expect(buttonVariants({ isAttached: true, orientation }).group()).not.toMatch(/\bgap-/);
896
+ }
897
+ });
898
+ });
899
+
900
+ describe("resolveButtonFeedback in a detached group", () => {
901
+ test("falls back to the lone button's scale", () => {
902
+ // Tearing a seam is impossible with no seam, so the group rung is dropped.
903
+ expect(resolveButtonFeedback(undefined, undefined, true, false)).toBe("scale");
904
+ });
905
+
906
+ test("still fades when attached", () => {
907
+ expect(resolveButtonFeedback(undefined, undefined, true, true)).toBe("fade");
908
+ });
909
+
910
+ test("defaults to attached, which is what a group is", () => {
911
+ expect(resolveButtonFeedback(undefined, undefined, true)).toBe("fade");
912
+ });
913
+
914
+ test("honours an explicit `none` either way", () => {
915
+ for (const isAttached of [true, false]) {
916
+ expect(resolveButtonFeedback("none", undefined, true, isAttached)).toBe("none");
917
+ expect(resolveButtonFeedback(undefined, "none", true, isAttached)).toBe("none");
918
+ }
919
+ });
920
+
921
+ test("still lets the group or the member name a treatment", () => {
922
+ expect(resolveButtonFeedback(undefined, "fade", true, false)).toBe("fade");
923
+ expect(resolveButtonFeedback("scale-fade", "fade", true, false)).toBe("scale-fade");
924
+ });
925
+ });
926
+
927
+ describe("a full-width group", () => {
928
+ test("spans its parent instead of hugging its content", () => {
929
+ const cls = buttonVariants({ isFullWidth: true, orientation: "horizontal" }).group();
930
+ expect(cls).toMatch(/\bw-full\b/);
931
+ expect(cls).toMatch(/\bself-stretch\b/);
932
+ expect(cls).not.toMatch(/\bself-start\b/);
933
+ });
934
+
935
+ test("fills the width when vertical too", () => {
936
+ const cls = buttonVariants({ isFullWidth: true, orientation: "vertical" }).group();
937
+ expect(cls).toMatch(/\bw-full\b/);
938
+ expect(cls).toMatch(/\bself-stretch\b/);
939
+ });
940
+
941
+ test("hugs its content by default", () => {
942
+ const cls = buttonVariants({ orientation: "horizontal" }).group();
943
+ expect(cls).toMatch(/\bself-start\b/);
944
+ expect(cls).not.toMatch(/\bw-full\b/);
945
+ });
946
+
947
+ test("keeps a gap when it is also detached", () => {
948
+ const cls = buttonVariants({ isAttached: false, isFullWidth: true }).group();
949
+ expect(cls).toMatch(/\bgap-2\b/);
950
+ expect(cls).toMatch(/\bw-full\b/);
951
+ });
952
+ });
953
+
954
+ describe("a stretched member", () => {
955
+ test("splits the run equally rather than by content", () => {
956
+ // `basis-0` is the half that matters: with `flex-1` alone Yoga shares out
957
+ // only the space left over, and the widths still follow the labels.
958
+ const cls = buttonVariants({ isStretched: true }).root();
959
+ expect(cls).toMatch(/\bflex-1\b/);
960
+ expect(cls).toMatch(/\bbasis-0\b/);
961
+ });
962
+
963
+ test("does not stretch by default", () => {
964
+ const cls = buttonVariants().root();
965
+ expect(cls).not.toMatch(/\bflex-1\b/);
966
+ expect(cls).not.toMatch(/\bbasis-0\b/);
967
+ });
968
+
969
+ test("yields to a caller's flex", () => {
970
+ const slot = buttonVariants({ isStretched: true }).root({ className: "flex-none" });
971
+ expect(slot).toMatch(/\bflex-none\b/);
972
+ expect(slot).not.toMatch(/\bflex-1\b/);
973
+ const merged = cn(buttonVariants({ isStretched: true }).root(), "flex-none");
974
+ expect(merged).toMatch(/\bflex-none\b/);
975
+ expect(merged).not.toMatch(/\bflex-1\b/);
976
+ });
977
+
978
+ test("keeps its own corner and height", () => {
979
+ const cls = buttonVariants({ groupPosition: "first", isStretched: true, size: "md" }).root();
980
+ expect(cls).toContain("h-button-md");
981
+ expect(cls).toContain("rounded-s-button-md");
982
+ });
983
+ });
984
+
985
+ describe("resolveGroupMemberStretch", () => {
986
+ test("stretches members of a full-width horizontal run", () => {
987
+ expect(resolveGroupMemberStretch({ isFullWidth: true, orientation: "horizontal" })).toBe(true);
988
+ });
989
+
990
+ test("leaves a vertical run to the cross axis it already stretches on", () => {
991
+ // `basis-0` on the main axis of a column with no definite height would
992
+ // collapse every member to nothing.
993
+ expect(resolveGroupMemberStretch({ isFullWidth: true, orientation: "vertical" })).toBe(false);
994
+ });
995
+
996
+ test("stretches nothing by default", () => {
997
+ for (const orientation of BUTTON_GROUP_ORIENTATIONS) {
998
+ expect(resolveGroupMemberStretch({ isFullWidth: false, orientation })).toBe(false);
999
+ }
1000
+ });
1001
+ });
@@ -13,6 +13,14 @@ export const BUTTON_VARIANTS = [
13
13
  ] as const;
14
14
 
15
15
  /** Sizes that hold a label. Horizontal padding; the width comes from the content. */
16
+ /**
17
+ * `flat` is the library default. `etched` sets the highlight into the fill's
18
+ * edge — a white inset top edge on `primary`, the light/dark edge on the quiet
19
+ * fills. Ghost and destructive buttons stay flat: a ghost has no surface to
20
+ * lift, and a signal colour does not take a second highlight.
21
+ */
22
+ export const BUTTON_MATERIALS = ["flat", "etched"] as const;
23
+
16
24
  export const BUTTON_LABEL_SIZES = ["sm", "md", "lg"] as const;
17
25
 
18
26
  /** Sizes with a square footprint, for a button whose only content is an icon. */
@@ -33,6 +41,7 @@ export const BUTTON_GROUP_ORIENTATIONS = ["horizontal", "vertical"] as const;
33
41
 
34
42
  export const BUTTON_GROUP_POSITIONS = ["first", "middle", "last", "only"] as const;
35
43
 
44
+ export type ButtonMaterial = (typeof BUTTON_MATERIALS)[number];
36
45
  export type ButtonVariant = (typeof BUTTON_VARIANTS)[number];
37
46
  export type ButtonLabelSize = (typeof BUTTON_LABEL_SIZES)[number];
38
47
  export type ButtonIconSize = (typeof BUTTON_ICON_SIZES)[number];
@@ -41,6 +50,11 @@ export type ButtonSpinnerPlacement = (typeof BUTTON_SPINNER_PLACEMENTS)[number];
41
50
  export type ButtonGroupOrientation = (typeof BUTTON_GROUP_ORIENTATIONS)[number];
42
51
  export type ButtonGroupPosition = (typeof BUTTON_GROUP_POSITIONS)[number];
43
52
 
53
+ /** How a group shapes its run. Omitted, a group joins — that is what a group is. */
54
+ export type ButtonGroupShapeOptions = {
55
+ isAttached?: boolean;
56
+ };
57
+
44
58
  /** A member's place in its group, or `none` for a button standing on its own. */
45
59
  export type ButtonGroupSlotPosition = ButtonGroupPosition | "none";
46
60
 
@@ -135,8 +149,9 @@ export const buttonVariants = tv({
135
149
  /**
136
150
  * A `Button.Group`'s box.
137
151
  *
138
- * Holds no `gap` and no `overflow-hidden`, and both absences are
139
- * load-bearing. A gap is the seam this component exists to close. A clip
152
+ * Holds no `gap` while attached and no `overflow-hidden` ever, and both
153
+ * absences are load-bearing. A gap is the seam a joined run exists to
154
+ * close — only `isAttached: false` adds one, where there is no seam. A clip
140
155
  * would square off the very corners the position compounds just rounded,
141
156
  * and would have to restate the group's own corner to avoid it — while the
142
157
  * members already square themselves, which is the whole point.
@@ -192,10 +207,26 @@ export const buttonVariants = tv({
192
207
  horizontal: { group: "flex-row self-start" },
193
208
  vertical: { group: "flex-col" },
194
209
  },
210
+ // Declared after `orientation` on purpose: `tv` merges in declaration
211
+ // order, so `self-stretch` here replaces the horizontal run's `self-start`
212
+ // rather than losing to it.
213
+ /** A `Button.Group` that spans its parent. The members split it — see `isStretched`. */
214
+ isFullWidth: { true: { group: "w-full self-stretch" }, false: {} },
215
+ /** A `Button.Group` whose members join into one shape. Detached, they stand apart by a gap. */
216
+ isAttached: { true: {}, false: { group: "gap-2" } },
217
+ /**
218
+ * A member of a full-width horizontal run, taking an equal share of it.
219
+ *
220
+ * `basis-0` is the half that matters. With `flex-1` alone Yoga shares out
221
+ * only the space left over after each member's content width, so the
222
+ * widths still follow the labels; from a zero basis every share is equal.
223
+ */
224
+ isStretched: { true: { root: "flex-1 basis-0" }, false: {} },
195
225
  // `middle` is the one position whose corner needs neither the size nor the
196
226
  // orientation, so it lands here rather than in six compound cells that
197
227
  // would all say the same word.
198
228
  groupPosition: { none: {}, first: {}, middle: { root: "rounded-none" }, last: {}, only: {} },
229
+ material: { flat: {}, etched: {} },
199
230
  // The empty `false` branches are load-bearing typing, not placeholders.
200
231
  // `tv` derives the prop type from the declared keys, so a map with only
201
232
  // `true` types the prop as `true` rather than `boolean` and rejects
@@ -208,6 +239,12 @@ export const buttonVariants = tv({
208
239
  isDimmedWhileLoading: { true: {}, false: {} },
209
240
  },
210
241
  compoundVariants: [
242
+ { material: "etched", variant: "primary", class: { root: "shadow-etched-primary" } },
243
+ {
244
+ material: "etched",
245
+ variant: ["secondary", "tertiary", "outline"],
246
+ class: { root: "shadow-etched dark:shadow-etched-dark" },
247
+ },
211
248
  // Loading is not a disabled state. The button keeps full contrast — the
212
249
  // spinner already says the press landed — unless the caller opts in.
213
250
  { isLoading: true, isDimmedWhileLoading: true, class: { root: "opacity-50" } },
@@ -304,6 +341,9 @@ export const buttonVariants = tv({
304
341
  orientation: "horizontal",
305
342
  groupPosition: "none",
306
343
  isSeamed: false,
344
+ isFullWidth: false,
345
+ isAttached: true,
346
+ isStretched: false,
307
347
  isDisabled: false,
308
348
  isLoading: false,
309
349
  isDimmedWhileLoading: false,
@@ -413,9 +453,18 @@ export function resolveGroupedButtonSize(own: ButtonSize | undefined, group: But
413
453
  * trade {@link resolveSpinnerSwapIndex} already makes. React Native has no
414
454
  * sibling selector, so this is the only place a member's corner can be decided.
415
455
  *
456
+ * A detached run (`isAttached: false`) places every member `only` — the lone
457
+ * position, which draws the lone button's corner — rather than minting a fourth
458
+ * sentinel: a spaced member *is* a button on its own that happens to share props.
459
+ *
416
460
  * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
417
461
  */
418
- export function resolveGroupPositions(isMember: readonly boolean[]): (ButtonGroupPosition | null)[] {
462
+ export function resolveGroupPositions(
463
+ isMember: readonly boolean[],
464
+ { isAttached = true }: ButtonGroupShapeOptions = {}
465
+ ): (ButtonGroupPosition | null)[] {
466
+ if (!isAttached) return isMember.map((member) => (member ? "only" : null));
467
+
419
468
  const count = isMember.reduce((total, member) => (member ? total + 1 : total), 0);
420
469
  let seen = 0;
421
470
 
@@ -445,9 +494,16 @@ export function resolveGroupPositions(isMember: readonly boolean[]): (ButtonGrou
445
494
  * `isMember[-1]` is `undefined`, so the first child falls out as `false` without
446
495
  * a case of its own.
447
496
  *
497
+ * A detached run has no shared edge, so nothing overlaps: a point of overlap
498
+ * there would only eat into the gap.
499
+ *
448
500
  * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
449
501
  */
450
- export function resolveGroupSeams(isMember: readonly boolean[]): boolean[] {
502
+ export function resolveGroupSeams(
503
+ isMember: readonly boolean[],
504
+ { isAttached = true }: ButtonGroupShapeOptions = {}
505
+ ): boolean[] {
506
+ if (!isAttached) return isMember.map(() => false);
451
507
  return isMember.map((member, index) => member && isMember[index - 1] === true);
452
508
  }
453
509
 
@@ -463,6 +519,10 @@ export function resolveGroupSeams(isMember: readonly boolean[]): boolean[] {
463
519
  * while its neighbours hold still, so the seam the group exists to close tears
464
520
  * open for the length of the press. `fade` moves no geometry.
465
521
  *
522
+ * Only while the group is *attached*. A detached run has no seam to tear, so
523
+ * its members fall back to the lone button's `scale`. `isAttached` defaults to
524
+ * `true`, because joining is what a group does when it says nothing.
525
+ *
466
526
  * A caller still gets `scale` by asking for it, on one button or on a whole
467
527
  * group. `pressedScale` beats both, because `resolvePressedState` lets an
468
528
  * explicit value win on the axis it names — see `pressable.variants.ts`.
@@ -472,9 +532,30 @@ export function resolveGroupSeams(isMember: readonly boolean[]): boolean[] {
472
532
  export function resolveButtonFeedback(
473
533
  own: PressableFeedback | undefined,
474
534
  group: PressableFeedback | undefined,
475
- isGrouped: boolean
535
+ isGrouped: boolean,
536
+ isAttached = true
476
537
  ): PressableFeedback {
477
- return own ?? group ?? (isGrouped ? BUTTON_GROUP_FEEDBACK : BUTTON_FEEDBACK);
538
+ return own ?? group ?? (isGrouped && isAttached ? BUTTON_GROUP_FEEDBACK : BUTTON_FEEDBACK);
539
+ }
540
+
541
+ /**
542
+ * Whether a group's members take an equal share of its width.
543
+ *
544
+ * Horizontal only. A vertical run already stretches its members on the cross
545
+ * axis, and `basis-0` on the main axis of a column with no definite height
546
+ * would collapse every member to nothing — so `isFullWidth` on a vertical group
547
+ * widens the group and does nothing further.
548
+ *
549
+ * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
550
+ */
551
+ export function resolveGroupMemberStretch({
552
+ isFullWidth,
553
+ orientation,
554
+ }: {
555
+ isFullWidth: boolean;
556
+ orientation: ButtonGroupOrientation;
557
+ }): boolean {
558
+ return isFullWidth && orientation === "horizontal";
478
559
  }
479
560
 
480
561
  export type ButtonVariantProps = VariantProps<typeof buttonVariants>;
@@ -28,6 +28,7 @@ export {
28
28
  BUTTON_VARIANTS,
29
29
  type ButtonGroupOrientation,
30
30
  type ButtonGroupPosition,
31
+ type ButtonGroupShapeOptions,
31
32
  type ButtonGroupSlotPosition,
32
33
  type ButtonIconSize,
33
34
  type ButtonLabelSize,
@@ -41,6 +42,7 @@ export {
41
42
  resolveButtonLayout,
42
43
  resolveButtonSizeStep,
43
44
  resolveGroupedButtonSize,
45
+ resolveGroupMemberStretch,
44
46
  resolveGroupPositions,
45
47
  resolveGroupSeams,
46
48
  } from "./button.variants";
@@ -0,0 +1,142 @@
1
+ # Calendar
2
+
3
+ An always-visible month grid for picking one day, several days or a range. Compound root plus
4
+ `Calendar.Header`, `Calendar.Nav`, `Calendar.Caption`, `Calendar.Weekdays`, `Calendar.Grid`,
5
+ `Calendar.Day` and `Calendar.Picker`. Used inline on a screen, and as the body of the date pickers'
6
+ sheets.
7
+
8
+ `import { Calendar } from "@delacour/react-native-ui/calendar";`
9
+
10
+ ## Files
11
+
12
+ | File | What it holds |
13
+ | --- | --- |
14
+ | `index.ts` | → `@delacour/react-native-ui/calendar`; also re-exports every date helper and type |
15
+ | `calendar.tsx` | Root: selection and month state, bounds, the context value, the `Object.assign` surface |
16
+ | `calendar-header.tsx` | `Calendar.Header` — prev arrow, caption, next arrow by default |
17
+ | `calendar-nav.tsx` | `Calendar.Nav` — a ghost `Button` that disables itself at a bound |
18
+ | `calendar-caption.tsx` | `Calendar.Caption` — label, or the button that cycles days → months → years |
19
+ | `calendar-weekdays.tsx` | `Calendar.Weekdays` — short names shown, full names spoken |
20
+ | `calendar-grid.tsx` | `Calendar.Grid` — six weeks, the paging pan and slide, hosts the picker |
21
+ | `calendar-day.tsx` | `Calendar.Day` — one cell: band, circle, number or custom content |
22
+ | `calendar-picker.tsx` | `Calendar.Picker` — the months and years jump views |
23
+ | `calendar.context.tsx` | `CalendarContext`, `useCalendar()`, `useCalendarContext()`, `useCalendarPart()` |
24
+ | `calendar.types.ts` | `CalendarSelection` (the mode union), `CalendarProps`, `CalendarCaptionLayout` |
25
+ | `calendar.date.ts` | **Shared leaf.** `CalendarDate` maths, matchers, grid, selection rules, `Intl` formatters. No React Native |
26
+ | `calendar.date.test.ts` | |
27
+ | `calendar.variants.ts` | Slotted `tv()`, the day-state, range-role, bounds, axes and page-direction resolvers |
28
+ | `calendar.variants.test.ts` | |
29
+
30
+ ## The date leaf
31
+
32
+ - **A day is `{ year, month, day }` and nothing else.** No `Date`, no time zone, no UTC
33
+ round-trip — a `Date` built at midnight in Auckland is the previous day in UTC, and that is the
34
+ bug every calendar built on `Date` ships once. `toDate`/`fromDate` cross over by **local**
35
+ fields, at the edge only.
36
+ - **`weekdayOf` is Sakamoto's method**, not `new Date().getDay()`, so the leaf never touches the
37
+ device clock or zone. A test checks it against the platform for every day of a leap year.
38
+ - **`calendar.date.ts` is the only module another folder may import from here.** `date-picker`
39
+ and `date-time-picker` reach `../calendar/calendar.date` — never `../calendar`, which would pull
40
+ the whole compound in (package rule 3).
41
+ - **`resolveWeekStart` prefers `Intl.Locale#getWeekInfo`/`weekInfo`** and falls back to a region
42
+ table, because Hermes support for either is not guaranteed. Unknown is Monday (ISO 8601).
43
+ - **Formatters never throw.** An unknown locale falls back to `en`; a formatter that throws inside
44
+ render takes the screen down for a typo.
45
+ - **`parseDate` is strict** — `YYYY-MM-DD` and a real day, or `null`. A lenient parser turns
46
+ `2026-02-31` into 3 March, which is a different booking.
47
+
48
+ ## Selection
49
+
50
+ - **`mode` is the discriminant** of `CalendarSelection`, so `selected` and `onSelect` are typed for
51
+ the mode named. Internally one `CalendarSelectionState` union flows through
52
+ `useControllableState`, and `emitSelection` narrows back to the caller's shape.
53
+ - **Single:** a tap on the selected day clears it. **Multiple:** toggles; always sorted, deduped.
54
+ - **Range** (`applyRangeTap`): empty → start; start + any day → complete, in either direction,
55
+ with the earlier day as `start` (`normaliseRange`); complete + tap → a new start. Backwards
56
+ completes rather than moving the start, because tapping check-out first is as common as
57
+ check-in first and a tap that silently discards the first one reads as a bug. A range that would contain a disabled day
58
+ (`rangeContainsDisabled`) restarts at the tapped day instead of completing — a stay that spans a
59
+ closed night is not bookable, and silently accepting it is worse than making the user tap again.
60
+ - **Disabled vs read-only.** `isDisabled` makes everything inert and fades the root. `isReadOnly`
61
+ ignores day taps and leaves paging working — looking at another month changes nothing.
62
+
63
+ ## Layout
64
+
65
+ - **Always six weeks** (`CALENDAR_WEEKS`). A month that fits in four or five rows still draws six,
66
+ so height and column positions never shift while paging. An outside day with
67
+ `showOutsideDays={false}` is an empty cell of the same height, never a missing one.
68
+ - **The whole cell is the target**, not the circle. A column on a phone is wider than the circle
69
+ is tall, and the gap belongs to whichever day it sits beside.
70
+ - **The cell is on the input scale** (`h-input-*`, `size-input-*`, `text-input-*`): sm 36, md 44,
71
+ lg 52. `md` and `lg` meet 44pt.
72
+ - **The jump view sits over the grid** (`absolute inset-0`) while the weeks are hidden with
73
+ `opacity-0`, not unmounted, so opening it never changes the calendar's height. The weekday row
74
+ hides the same way.
75
+
76
+ ## Drawing
77
+
78
+ - **A day is three layers:** `band` (absolute, edge to edge, behind), `dayBase` (the circle) and
79
+ the label. The band spans the full cell so adjacent days join into one strip.
80
+ - **The band ends at the centre of each end cell, and the circle is the cap.** Start runs from its
81
+ cell's centre to the right edge, end from the left edge to its centre, middle spans the cell; none
82
+ is rounded. A cell is wider than its circle, so the first version — a band rounded across the
83
+ whole end cell — showed a crescent of band beside the circle at both ends. A lone `only` day
84
+ draws no band. At a week's edge the strip simply stops and resumes on the next row.
85
+ - **Today is a ring (`border-ring`) and `text-primary`, never a fill**, so it cannot be mistaken
86
+ for a selection.
87
+ - **Variant:** `primary` fills `bg-primary`, band `bg-primary/15`; `secondary` fills
88
+ `bg-primary/25` under `text-foreground`, band `bg-primary/10`. The primary band is a tint of the selection colour, not
89
+ `bg-accent`: light `--accent` is 0.97 on a 0.985 page, so the band all but vanished in light
90
+ while reading fine in dark. A tint of `primary` reads in both themes and joins the end caps
91
+ as one shape. `secondary` is tinted from `primary` for the same reason — `bg-secondary`
92
+ and `bg-muted` are 0.97 on the same 0.985 page, and the recorded preview showed a soft
93
+ selection with no visible circle at all. `isInvalid` overrides both with `bg-destructive` / `bg-destructive-soft`.
94
+ - **Colour is on the text slots only** (rule 1): `dayLabel`, `weekdayLabel`, `captionText`,
95
+ `pickerItemLabel`. A test holds every `View` slot to carrying no `text-*` colour.
96
+ - **Custom day content** gets `labelClassName` in its render props, so a price under the number
97
+ takes the selected colour with it rather than reimplementing the tone table.
98
+
99
+ ## Paging
100
+
101
+ - **A Gesture Handler pan on `Calendar.Grid`**, `activeOffsetX` ±12 and `failOffsetY` ±12: it
102
+ claims a sideways drag and gives up a vertical one, so the calendar lives inside a `ScrollView`
103
+ or a sheet without stealing its scroll.
104
+ - **`resolvePageDirection` is pure and tested.** A quarter of the width, or a release faster than
105
+ 500 pt/s, pages; a flick against the drag settles back; it never pages past a bound or on an
106
+ unmeasured grid.
107
+ - **Slide out, swap, slide in.** The old month slides off, the month changes on the JS thread, and
108
+ the new one slides in from the opposite side — the same entrance the arrows give. The grid
109
+ clips (`overflow-hidden`) so the swap frame is never seen.
110
+ - **Calm motion skips both slides.** The page change is behaviour and always happens.
111
+ - **A drag toward a bound resists** (20% travel) and settles back with a `light` haptic — the edge
112
+ is felt rather than the month silently refusing to turn.
113
+ - **Tapping an outside day** (with `selectOutsideDays`) selects it and pages to its month.
114
+
115
+ ## Accessibility
116
+
117
+ - Each day is a button labelled with the full date (`formatDate(…, { style: "full" })`), hinted
118
+ "Today" / "Range start" / "Range end", with `selected` and `disabled` state.
119
+ - The grid is `role="grid"`, named by an enclosing `Field.Label` or else the month.
120
+ - **Paging for screen readers lives on the caption**, as increment/decrement actions. An
121
+ adjustable grid would fold its child buttons into itself on both platforms and make the days
122
+ unreachable.
123
+ - A month change is announced with `AccessibilityInfo.announceForAccessibility` — on change only,
124
+ never on mount, because the caption already says it.
125
+ - Today is re-read on `AppState` → `active`, so a calendar left open overnight marks the right day.
126
+
127
+ - **Parts derive their `testID` from the root's** (`resolvePartTestID`): `testID="booking"`
128
+ names the days `booking-day-2026-10-05`, the arrows `booking-prev`/`booking-next`, the caption
129
+ `booking-caption` and the jump-view items `booking-month-10`/`booking-year-2026`. The default
130
+ anatomy renders forty-odd parts no caller writes, and an end-to-end test or a recorded preview
131
+ flow has to reach one particular day all the same. No root `testID`, no part ids.
132
+
133
+ ## Field
134
+
135
+ `resolveCalendarAxes` is the nearest-wins chain: the calendar's own `isDisabled` / `isInvalid` /
136
+ `isReadOnly` / `size` / `variant`, else the `Field`'s. The field's label names the grid. It does
137
+ not `registerPress` — there is no single focus target for a row tap to reach.
138
+
139
+ ## Not here
140
+
141
+ Non-Gregorian calendars, side-by-side months, week numbers, drag-to-select ranges, time. The
142
+ anchored presentation belongs to `DatePicker`.