@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
@@ -41,6 +41,11 @@ export type ButtonSpinnerPlacement = (typeof BUTTON_SPINNER_PLACEMENTS)[number];
41
41
  export type ButtonGroupOrientation = (typeof BUTTON_GROUP_ORIENTATIONS)[number];
42
42
  export type ButtonGroupPosition = (typeof BUTTON_GROUP_POSITIONS)[number];
43
43
 
44
+ /** How a group shapes its run. Omitted, a group joins — that is what a group is. */
45
+ export type ButtonGroupShapeOptions = {
46
+ isAttached?: boolean;
47
+ };
48
+
44
49
  /** A member's place in its group, or `none` for a button standing on its own. */
45
50
  export type ButtonGroupSlotPosition = ButtonGroupPosition | "none";
46
51
 
@@ -135,8 +140,9 @@ export const buttonVariants = tv({
135
140
  /**
136
141
  * A `Button.Group`'s box.
137
142
  *
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
143
+ * Holds no `gap` while attached and no `overflow-hidden` ever, and both
144
+ * absences are load-bearing. A gap is the seam a joined run exists to
145
+ * close — only `isAttached: false` adds one, where there is no seam. A clip
140
146
  * would square off the very corners the position compounds just rounded,
141
147
  * and would have to restate the group's own corner to avoid it — while the
142
148
  * members already square themselves, which is the whole point.
@@ -192,6 +198,21 @@ export const buttonVariants = tv({
192
198
  horizontal: { group: "flex-row self-start" },
193
199
  vertical: { group: "flex-col" },
194
200
  },
201
+ // Declared after `orientation` on purpose: `tv` merges in declaration
202
+ // order, so `self-stretch` here replaces the horizontal run's `self-start`
203
+ // rather than losing to it.
204
+ /** A `Button.Group` that spans its parent. The members split it — see `isStretched`. */
205
+ isFullWidth: { true: { group: "w-full self-stretch" }, false: {} },
206
+ /** A `Button.Group` whose members join into one shape. Detached, they stand apart by a gap. */
207
+ isAttached: { true: {}, false: { group: "gap-2" } },
208
+ /**
209
+ * A member of a full-width horizontal run, taking an equal share of it.
210
+ *
211
+ * `basis-0` is the half that matters. With `flex-1` alone Yoga shares out
212
+ * only the space left over after each member's content width, so the
213
+ * widths still follow the labels; from a zero basis every share is equal.
214
+ */
215
+ isStretched: { true: { root: "flex-1 basis-0" }, false: {} },
195
216
  // `middle` is the one position whose corner needs neither the size nor the
196
217
  // orientation, so it lands here rather than in six compound cells that
197
218
  // would all say the same word.
@@ -304,6 +325,9 @@ export const buttonVariants = tv({
304
325
  orientation: "horizontal",
305
326
  groupPosition: "none",
306
327
  isSeamed: false,
328
+ isFullWidth: false,
329
+ isAttached: true,
330
+ isStretched: false,
307
331
  isDisabled: false,
308
332
  isLoading: false,
309
333
  isDimmedWhileLoading: false,
@@ -413,9 +437,18 @@ export function resolveGroupedButtonSize(own: ButtonSize | undefined, group: But
413
437
  * trade {@link resolveSpinnerSwapIndex} already makes. React Native has no
414
438
  * sibling selector, so this is the only place a member's corner can be decided.
415
439
  *
440
+ * A detached run (`isAttached: false`) places every member `only` — the lone
441
+ * position, which draws the lone button's corner — rather than minting a fourth
442
+ * sentinel: a spaced member *is* a button on its own that happens to share props.
443
+ *
416
444
  * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
417
445
  */
418
- export function resolveGroupPositions(isMember: readonly boolean[]): (ButtonGroupPosition | null)[] {
446
+ export function resolveGroupPositions(
447
+ isMember: readonly boolean[],
448
+ { isAttached = true }: ButtonGroupShapeOptions = {}
449
+ ): (ButtonGroupPosition | null)[] {
450
+ if (!isAttached) return isMember.map((member) => (member ? "only" : null));
451
+
419
452
  const count = isMember.reduce((total, member) => (member ? total + 1 : total), 0);
420
453
  let seen = 0;
421
454
 
@@ -445,9 +478,16 @@ export function resolveGroupPositions(isMember: readonly boolean[]): (ButtonGrou
445
478
  * `isMember[-1]` is `undefined`, so the first child falls out as `false` without
446
479
  * a case of its own.
447
480
  *
481
+ * A detached run has no shared edge, so nothing overlaps: a point of overlap
482
+ * there would only eat into the gap.
483
+ *
448
484
  * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
449
485
  */
450
- export function resolveGroupSeams(isMember: readonly boolean[]): boolean[] {
486
+ export function resolveGroupSeams(
487
+ isMember: readonly boolean[],
488
+ { isAttached = true }: ButtonGroupShapeOptions = {}
489
+ ): boolean[] {
490
+ if (!isAttached) return isMember.map(() => false);
451
491
  return isMember.map((member, index) => member && isMember[index - 1] === true);
452
492
  }
453
493
 
@@ -463,6 +503,10 @@ export function resolveGroupSeams(isMember: readonly boolean[]): boolean[] {
463
503
  * while its neighbours hold still, so the seam the group exists to close tears
464
504
  * open for the length of the press. `fade` moves no geometry.
465
505
  *
506
+ * Only while the group is *attached*. A detached run has no seam to tear, so
507
+ * its members fall back to the lone button's `scale`. `isAttached` defaults to
508
+ * `true`, because joining is what a group does when it says nothing.
509
+ *
466
510
  * A caller still gets `scale` by asking for it, on one button or on a whole
467
511
  * group. `pressedScale` beats both, because `resolvePressedState` lets an
468
512
  * explicit value win on the axis it names — see `pressable.variants.ts`.
@@ -472,9 +516,30 @@ export function resolveGroupSeams(isMember: readonly boolean[]): boolean[] {
472
516
  export function resolveButtonFeedback(
473
517
  own: PressableFeedback | undefined,
474
518
  group: PressableFeedback | undefined,
475
- isGrouped: boolean
519
+ isGrouped: boolean,
520
+ isAttached = true
476
521
  ): PressableFeedback {
477
- return own ?? group ?? (isGrouped ? BUTTON_GROUP_FEEDBACK : BUTTON_FEEDBACK);
522
+ return own ?? group ?? (isGrouped && isAttached ? BUTTON_GROUP_FEEDBACK : BUTTON_FEEDBACK);
523
+ }
524
+
525
+ /**
526
+ * Whether a group's members take an equal share of its width.
527
+ *
528
+ * Horizontal only. A vertical run already stretches its members on the cross
529
+ * axis, and `basis-0` on the main axis of a column with no definite height
530
+ * would collapse every member to nothing — so `isFullWidth` on a vertical group
531
+ * widens the group and does nothing further.
532
+ *
533
+ * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
534
+ */
535
+ export function resolveGroupMemberStretch({
536
+ isFullWidth,
537
+ orientation,
538
+ }: {
539
+ isFullWidth: boolean;
540
+ orientation: ButtonGroupOrientation;
541
+ }): boolean {
542
+ return isFullWidth && orientation === "horizontal";
478
543
  }
479
544
 
480
545
  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`.
@@ -0,0 +1,116 @@
1
+ import { type ReactElement, useCallback } from "react";
2
+ import { type AccessibilityActionEvent, View, type ViewProps } from "react-native";
3
+ import { IconChevronDownSmall } from "../../icons/central";
4
+ import { Icon } from "../icon";
5
+ import { Pressable } from "../pressable";
6
+ import { Text } from "../text";
7
+ import { useCalendarPart } from "./calendar.context";
8
+ import { formatMonthYear, formatYear } from "./calendar.date";
9
+ import { calendarVariants, resolvePartTestID } from "./calendar.variants";
10
+
11
+ export type CalendarCaptionProps = Omit<ViewProps, "children"> & {
12
+ className?: string;
13
+ };
14
+
15
+ const PAGE_ACTIONS = [
16
+ { name: "decrement", label: "Previous" },
17
+ { name: "increment", label: "Next" },
18
+ ];
19
+
20
+ /**
21
+ * The month and year above the grid.
22
+ *
23
+ * With `captionLayout="picker"` it is a button: in the grid it opens the months view, in the
24
+ * months view the years view, and in the years view it returns to the grid. With `"label"` it is
25
+ * text.
26
+ *
27
+ * **It is where a screen-reader user pages.** A grid of buttons cannot itself be an adjustable
28
+ * element — iOS and Android both fold an accessible container's children into it, and the days
29
+ * would become unreachable — so the caption carries the increment and decrement actions instead:
30
+ * swipe up or down on "October 2026" to move a month. As a plain label it takes the `adjustable`
31
+ * role outright; as a button the two actions sit beside its activation.
32
+ */
33
+ export function CalendarCaption({ className, ...props }: CalendarCaptionProps): ReactElement {
34
+ const {
35
+ canGoNext,
36
+ canGoPrev,
37
+ captionLayout,
38
+ goNext,
39
+ goPrev,
40
+ isDisabled,
41
+ locale,
42
+ setView,
43
+ size,
44
+ testID,
45
+ variant,
46
+ view,
47
+ visibleMonth,
48
+ yearSpan,
49
+ } = useCalendarPart("Calendar.Caption");
50
+ const slots = calendarVariants({ size, variant });
51
+
52
+ const text =
53
+ view === "days"
54
+ ? formatMonthYear(visibleMonth, locale)
55
+ : view === "months"
56
+ ? formatYear(visibleMonth.year, locale)
57
+ : `${formatYear(yearSpan.from, locale)} – ${formatYear(yearSpan.to, locale)}`;
58
+
59
+ const handleAction = useCallback(
60
+ (event: AccessibilityActionEvent) => {
61
+ if (event.nativeEvent.actionName === "increment" && canGoNext) goNext();
62
+ if (event.nativeEvent.actionName === "decrement" && canGoPrev) goPrev();
63
+ },
64
+ [canGoNext, canGoPrev, goNext, goPrev]
65
+ );
66
+
67
+ const handlePress = useCallback(() => {
68
+ setView(view === "days" ? "months" : view === "months" ? "years" : "days");
69
+ }, [setView, view]);
70
+
71
+ const label = <Text className={slots.captionText()}>{text}</Text>;
72
+
73
+ if (captionLayout === "label") {
74
+ return (
75
+ <View
76
+ accessibilityActions={PAGE_ACTIONS}
77
+ accessibilityLabel={text}
78
+ accessibilityRole="adjustable"
79
+ accessible
80
+ className={slots.caption({ className })}
81
+ onAccessibilityAction={handleAction}
82
+ testID={resolvePartTestID(testID, "caption")}
83
+ {...props}
84
+ >
85
+ {label}
86
+ </View>
87
+ );
88
+ }
89
+
90
+ const hint =
91
+ view === "days"
92
+ ? "Opens the month and year picker"
93
+ : view === "months"
94
+ ? "Opens the year list"
95
+ : "Returns to the month";
96
+
97
+ return (
98
+ <Pressable
99
+ accessibilityActions={PAGE_ACTIONS}
100
+ accessibilityHint={hint}
101
+ accessibilityLabel={text}
102
+ className={slots.caption({ className })}
103
+ disabled={isDisabled}
104
+ feedback="fade"
105
+ haptic="selection"
106
+ onAccessibilityAction={handleAction}
107
+ onPress={handlePress}
108
+ testID={resolvePartTestID(testID, "caption")}
109
+ {...props}
110
+ >
111
+ {label}
112
+ <Icon color="muted-foreground" icon={IconChevronDownSmall} size="sm" />
113
+ </Pressable>
114
+ );
115
+ }
116
+ CalendarCaption.displayName = "DelacourUI.Calendar.Caption";
@@ -0,0 +1,103 @@
1
+ import { type ReactElement, type ReactNode, useCallback } from "react";
2
+ import { View } from "react-native";
3
+ import { Pressable } from "../pressable";
4
+ import { Text } from "../text";
5
+ import { useCalendarPart } from "./calendar.context";
6
+ import { type CalendarDate, formatDate, isSameMonth, serialiseDate } from "./calendar.date";
7
+ import { type CalendarDayState, calendarVariants, resolveDayState, resolvePartTestID } from "./calendar.variants";
8
+
9
+ /** What a function child of `Calendar.Day` is handed. */
10
+ export type CalendarDayRenderProps = CalendarDayState & {
11
+ date: CalendarDate;
12
+ /** The class the default number is drawn with — size, weight and colour for this state. */
13
+ labelClassName: string;
14
+ };
15
+
16
+ export type CalendarDayProps = {
17
+ /** The day this cell draws. */
18
+ date: CalendarDate;
19
+ /**
20
+ * Custom content inside the circle, in place of the number. A function is handed the day's
21
+ * state, so a price under the number can take the selected colour with it.
22
+ */
23
+ children?: ReactNode | ((props: CalendarDayRenderProps) => ReactNode);
24
+ className?: string;
25
+ };
26
+
27
+ /** The state words a screen reader adds after the date. */
28
+ function describeState(state: CalendarDayState): string | undefined {
29
+ const parts: string[] = [];
30
+ if (state.isToday) parts.push("Today");
31
+ if (state.rangeRole === "start") parts.push("Range start");
32
+ if (state.rangeRole === "end") parts.push("Range end");
33
+ return parts.length > 0 ? parts.join(", ") : undefined;
34
+ }
35
+
36
+ /**
37
+ * One day: its column share, the range band behind it, and the circle with the number.
38
+ *
39
+ * **The whole cell is the target**, not the circle — a column on a phone is wider than the
40
+ * circle is tall, and the gap between two circles belongs to whichever day it sits beside.
41
+ *
42
+ * The band is drawn for a range's start, middle and end and never for a lone day, which is a
43
+ * circle with nothing to join. An outside day with `showOutsideDays` off is an empty cell of the
44
+ * same height, so the grid's rows never move.
45
+ *
46
+ * Announced as a button with the full date — "Monday 5 October 2026" — plus today and the range
47
+ * end it is, and `selected` and `disabled` as accessibility state.
48
+ */
49
+ export function CalendarDay({ date, children, className }: CalendarDayProps): ReactElement {
50
+ const calendar = useCalendarPart("Calendar.Day");
51
+ const state = resolveDayState({
52
+ cell: { date, isOutside: !isSameMonth(date, calendar.visibleMonth), weekIndex: 0, dayIndex: 0 },
53
+ isCalendarDisabled: calendar.isDisabled,
54
+ isDisabled: calendar.isDateDisabled,
55
+ isReadOnly: calendar.isReadOnly,
56
+ selectOutsideDays: calendar.selectOutsideDays,
57
+ selection: calendar.selection,
58
+ showOutsideDays: calendar.showOutsideDays,
59
+ today: calendar.today,
60
+ });
61
+ const slots = calendarVariants({
62
+ isDisabled: state.isDisabled,
63
+ isInvalid: calendar.isInvalid,
64
+ isOutside: state.isOutside,
65
+ isToday: state.isToday,
66
+ rangeRole: state.rangeRole ?? "none",
67
+ size: calendar.size,
68
+ tone: state.tone,
69
+ variant: calendar.variant,
70
+ });
71
+
72
+ const { select } = calendar;
73
+ const handlePress = useCallback(() => select(date), [date, select]);
74
+
75
+ if (state.isHidden) {
76
+ return <View className={slots.cell({ className })} importantForAccessibility="no-hide-descendants" />;
77
+ }
78
+
79
+ const labelClassName = slots.dayLabel();
80
+ const content =
81
+ typeof children === "function"
82
+ ? children({ ...state, date, labelClassName })
83
+ : (children ?? <Text className={labelClassName}>{date.day}</Text>);
84
+ const showsBand = state.rangeRole !== null && state.rangeRole !== "only";
85
+
86
+ return (
87
+ <Pressable
88
+ accessibilityHint={describeState(state)}
89
+ accessibilityLabel={formatDate(date, { locale: calendar.locale, style: "full" })}
90
+ accessibilityState={{ selected: state.isSelected }}
91
+ className={slots.cell({ className })}
92
+ disabled={!state.isPressable}
93
+ feedback="fade"
94
+ haptic="selection"
95
+ onPress={handlePress}
96
+ testID={resolvePartTestID(calendar.testID, `day-${serialiseDate(date)}`)}
97
+ >
98
+ {showsBand ? <View className={slots.band()} /> : null}
99
+ <View className={slots.dayBase()}>{content}</View>
100
+ </Pressable>
101
+ );
102
+ }
103
+ CalendarDay.displayName = "DelacourUI.Calendar.Day";