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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (493) hide show
  1. package/README.md +10 -6
  2. package/package.json +49 -5
  3. package/src/components/accordion/AGENTS.md +21 -9
  4. package/src/components/accordion/accordion-content.tsx +7 -4
  5. package/src/components/accordion/accordion-item.tsx +15 -13
  6. package/src/components/accordion/accordion.context.tsx +11 -5
  7. package/src/components/accordion/accordion.variants.test.ts +64 -3
  8. package/src/components/accordion/accordion.variants.ts +33 -9
  9. package/src/components/alert/AGENTS.md +102 -0
  10. package/src/components/alert/alert-action.tsx +17 -0
  11. package/src/components/alert/alert-close-button.tsx +50 -0
  12. package/src/components/alert/alert-content.tsx +15 -0
  13. package/src/components/alert/alert-description.tsx +16 -0
  14. package/src/components/alert/alert-glyphs.ts +25 -0
  15. package/src/components/alert/alert-indicator.tsx +40 -0
  16. package/src/components/alert/alert-title.tsx +18 -0
  17. package/src/components/alert/alert.context.tsx +69 -0
  18. package/src/components/alert/alert.tsx +160 -0
  19. package/src/components/alert/alert.types.ts +10 -0
  20. package/src/components/alert/alert.variants.test.ts +218 -0
  21. package/src/components/alert/alert.variants.ts +180 -0
  22. package/src/components/alert/index.ts +23 -0
  23. package/src/components/avatar/AGENTS.md +106 -0
  24. package/src/components/avatar/avatar-badge.tsx +51 -0
  25. package/src/components/avatar/avatar-group.tsx +139 -0
  26. package/src/components/avatar/avatar.context.tsx +93 -0
  27. package/src/components/avatar/avatar.tsx +201 -0
  28. package/src/components/avatar/avatar.utils.test.ts +174 -0
  29. package/src/components/avatar/avatar.utils.ts +141 -0
  30. package/src/components/avatar/avatar.variants.test.ts +251 -0
  31. package/src/components/avatar/avatar.variants.ts +270 -0
  32. package/src/components/avatar/index.ts +39 -0
  33. package/src/components/badge/AGENTS.md +10 -0
  34. package/src/components/badge/badge-close-button.tsx +2 -2
  35. package/src/components/badge/badge.context.tsx +2 -1
  36. package/src/components/badge/badge.tsx +13 -4
  37. package/src/components/badge/badge.variants.test.ts +26 -0
  38. package/src/components/badge/badge.variants.ts +21 -0
  39. package/src/components/bottom-sheet/AGENTS.md +186 -231
  40. package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
  41. package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
  42. package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -208
  43. package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
  44. package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
  45. package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
  46. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -122
  47. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
  48. package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
  49. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
  50. package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
  51. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -62
  52. package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
  53. package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
  54. package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
  55. package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
  56. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
  57. package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
  58. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -87
  59. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
  60. package/src/components/bottom-sheet/index.ts +39 -17
  61. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
  62. package/src/components/button/AGENTS.md +51 -11
  63. package/src/components/button/button-group.tsx +44 -8
  64. package/src/components/button/button.context.tsx +10 -0
  65. package/src/components/button/button.tsx +7 -1
  66. package/src/components/button/button.variants.test.ts +190 -0
  67. package/src/components/button/button.variants.ts +87 -6
  68. package/src/components/button/index.ts +2 -0
  69. package/src/components/calendar/AGENTS.md +142 -0
  70. package/src/components/calendar/calendar-caption.tsx +116 -0
  71. package/src/components/calendar/calendar-day.tsx +103 -0
  72. package/src/components/calendar/calendar-grid.tsx +183 -0
  73. package/src/components/calendar/calendar-header.tsx +34 -0
  74. package/src/components/calendar/calendar-nav.tsx +51 -0
  75. package/src/components/calendar/calendar-picker.tsx +128 -0
  76. package/src/components/calendar/calendar-weekdays.tsx +48 -0
  77. package/src/components/calendar/calendar.context.tsx +108 -0
  78. package/src/components/calendar/calendar.date.test.ts +456 -0
  79. package/src/components/calendar/calendar.date.ts +584 -0
  80. package/src/components/calendar/calendar.tsx +389 -0
  81. package/src/components/calendar/calendar.types.ts +87 -0
  82. package/src/components/calendar/calendar.variants.test.ts +363 -0
  83. package/src/components/calendar/calendar.variants.ts +426 -0
  84. package/src/components/calendar/index.ts +96 -0
  85. package/src/components/card/AGENTS.md +92 -0
  86. package/src/components/card/card-action.tsx +16 -0
  87. package/src/components/card/card-content.tsx +12 -0
  88. package/src/components/card/card-description.tsx +12 -0
  89. package/src/components/card/card-footer.tsx +30 -0
  90. package/src/components/card/card-header.tsx +45 -0
  91. package/src/components/card/card-title.tsx +25 -0
  92. package/src/components/card/card.context.tsx +64 -0
  93. package/src/components/card/card.tsx +100 -0
  94. package/src/components/card/card.types.ts +13 -0
  95. package/src/components/card/card.variants.test.ts +240 -0
  96. package/src/components/card/card.variants.ts +177 -0
  97. package/src/components/card/index.ts +15 -0
  98. package/src/components/chart/AGENTS.md +8 -0
  99. package/src/components/chart/chart.tsx +8 -1
  100. package/src/components/chip/AGENTS.md +113 -0
  101. package/src/components/chip/chip-close-button.tsx +48 -0
  102. package/src/components/chip/chip-end-content.tsx +15 -0
  103. package/src/components/chip/chip-label.tsx +21 -0
  104. package/src/components/chip/chip-start-content.tsx +16 -0
  105. package/src/components/chip/chip.context.tsx +66 -0
  106. package/src/components/chip/chip.tsx +268 -0
  107. package/src/components/chip/chip.types.ts +10 -0
  108. package/src/components/chip/chip.variants.test.ts +335 -0
  109. package/src/components/chip/chip.variants.ts +321 -0
  110. package/src/components/chip/index.ts +24 -0
  111. package/src/components/collapsible/AGENTS.md +97 -0
  112. package/src/components/collapsible/collapsible-content.tsx +97 -0
  113. package/src/components/collapsible/collapsible-description.tsx +17 -0
  114. package/src/components/collapsible/collapsible-indicator.tsx +92 -0
  115. package/src/components/collapsible/collapsible-title.tsx +17 -0
  116. package/src/components/collapsible/collapsible-trigger.tsx +133 -0
  117. package/src/components/collapsible/collapsible.context.tsx +91 -0
  118. package/src/components/collapsible/collapsible.tsx +167 -0
  119. package/src/components/collapsible/collapsible.types.ts +9 -0
  120. package/src/components/collapsible/collapsible.variants.test.ts +338 -0
  121. package/src/components/collapsible/collapsible.variants.ts +228 -0
  122. package/src/components/collapsible/index.ts +32 -0
  123. package/src/components/context-menu/AGENTS.md +103 -0
  124. package/src/components/context-menu/context-menu-content.tsx +64 -0
  125. package/src/components/context-menu/context-menu-item.tsx +22 -0
  126. package/src/components/context-menu/context-menu-preview.tsx +60 -0
  127. package/src/components/context-menu/context-menu-trigger.tsx +160 -0
  128. package/src/components/context-menu/context-menu.context.tsx +91 -0
  129. package/src/components/context-menu/context-menu.tsx +90 -0
  130. package/src/components/context-menu/context-menu.variants.test.ts +266 -0
  131. package/src/components/context-menu/context-menu.variants.ts +147 -0
  132. package/src/components/context-menu/index.ts +18 -0
  133. package/src/components/dialog/AGENTS.md +97 -0
  134. package/src/components/dialog/dialog-body.tsx +21 -0
  135. package/src/components/dialog/dialog-close.tsx +82 -0
  136. package/src/components/dialog/dialog-content.tsx +153 -0
  137. package/src/components/dialog/dialog-description.tsx +23 -0
  138. package/src/components/dialog/dialog-footer.tsx +32 -0
  139. package/src/components/dialog/dialog-header.tsx +20 -0
  140. package/src/components/dialog/dialog-title.tsx +42 -0
  141. package/src/components/dialog/dialog-trigger.tsx +54 -0
  142. package/src/components/dialog/dialog.context.tsx +90 -0
  143. package/src/components/dialog/dialog.tsx +119 -0
  144. package/src/components/dialog/dialog.variants.test.ts +224 -0
  145. package/src/components/dialog/dialog.variants.ts +173 -0
  146. package/src/components/dialog/index.ts +30 -0
  147. package/src/components/drawer/AGENTS.md +123 -0
  148. package/src/components/drawer/drawer-body.tsx +46 -0
  149. package/src/components/drawer/drawer-close.tsx +76 -0
  150. package/src/components/drawer/drawer-content.tsx +186 -0
  151. package/src/components/drawer/drawer-description.tsx +23 -0
  152. package/src/components/drawer/drawer-footer.tsx +21 -0
  153. package/src/components/drawer/drawer-header.tsx +42 -0
  154. package/src/components/drawer/drawer-title.tsx +35 -0
  155. package/src/components/drawer/drawer-trigger.tsx +54 -0
  156. package/src/components/drawer/drawer.context.tsx +114 -0
  157. package/src/components/drawer/drawer.tsx +119 -0
  158. package/src/components/drawer/drawer.variants.test.ts +392 -0
  159. package/src/components/drawer/drawer.variants.ts +244 -0
  160. package/src/components/drawer/index.ts +41 -0
  161. package/src/components/drawer/use-drawer-pan.ts +108 -0
  162. package/src/components/empty-state/AGENTS.md +90 -0
  163. package/src/components/empty-state/empty-state-content.tsx +17 -0
  164. package/src/components/empty-state/empty-state-description.tsx +12 -0
  165. package/src/components/empty-state/empty-state-header.tsx +17 -0
  166. package/src/components/empty-state/empty-state-media.tsx +56 -0
  167. package/src/components/empty-state/empty-state-title.tsx +18 -0
  168. package/src/components/empty-state/empty-state.context.tsx +65 -0
  169. package/src/components/empty-state/empty-state.tsx +84 -0
  170. package/src/components/empty-state/empty-state.types.ts +14 -0
  171. package/src/components/empty-state/empty-state.variants.test.ts +245 -0
  172. package/src/components/empty-state/empty-state.variants.ts +132 -0
  173. package/src/components/empty-state/index.ts +20 -0
  174. package/src/components/fab/AGENTS.md +119 -0
  175. package/src/components/fab/fab-action.tsx +118 -0
  176. package/src/components/fab/fab-group.tsx +214 -0
  177. package/src/components/fab/fab-label.tsx +18 -0
  178. package/src/components/fab/fab.context.tsx +125 -0
  179. package/src/components/fab/fab.tsx +156 -0
  180. package/src/components/fab/fab.types.ts +17 -0
  181. package/src/components/fab/fab.variants.test.ts +274 -0
  182. package/src/components/fab/fab.variants.ts +234 -0
  183. package/src/components/fab/index.ts +37 -0
  184. package/src/components/feedback/AGENTS.md +95 -0
  185. package/src/components/feedback/feedback-action.tsx +16 -0
  186. package/src/components/feedback/feedback-cancel.tsx +34 -0
  187. package/src/components/feedback/feedback-close.tsx +21 -0
  188. package/src/components/feedback/feedback-content.tsx +24 -0
  189. package/src/components/feedback/feedback-field.tsx +70 -0
  190. package/src/components/feedback/feedback-footer.tsx +21 -0
  191. package/src/components/feedback/feedback-panel.tsx +54 -0
  192. package/src/components/feedback/feedback-submit.tsx +74 -0
  193. package/src/components/feedback/feedback-title.tsx +34 -0
  194. package/src/components/feedback/feedback-trigger.tsx +20 -0
  195. package/src/components/feedback/feedback.context.tsx +73 -0
  196. package/src/components/feedback/feedback.tsx +135 -0
  197. package/src/components/feedback/feedback.variants.test.ts +185 -0
  198. package/src/components/feedback/feedback.variants.ts +128 -0
  199. package/src/components/feedback/index.ts +24 -0
  200. package/src/components/field/AGENTS.md +19 -0
  201. package/src/components/field/field-label.tsx +19 -5
  202. package/src/components/field/field.context.tsx +17 -0
  203. package/src/components/field/field.tsx +15 -2
  204. package/src/components/field/field.variants.test.ts +24 -0
  205. package/src/components/field/field.variants.ts +20 -0
  206. package/src/components/field/index.ts +1 -0
  207. package/src/components/input/AGENTS.md +5 -0
  208. package/src/components/input/input.variants.test.ts +32 -0
  209. package/src/components/input/input.variants.ts +13 -1
  210. package/src/components/item/AGENTS.md +92 -0
  211. package/src/components/item/index.ts +25 -0
  212. package/src/components/item/item-actions.tsx +28 -0
  213. package/src/components/item/item-content.tsx +15 -0
  214. package/src/components/item/item-description.tsx +12 -0
  215. package/src/components/item/item-footer.tsx +13 -0
  216. package/src/components/item/item-group.tsx +32 -0
  217. package/src/components/item/item-header.tsx +13 -0
  218. package/src/components/item/item-media.tsx +37 -0
  219. package/src/components/item/item-separator.tsx +13 -0
  220. package/src/components/item/item-title.tsx +17 -0
  221. package/src/components/item/item.context.tsx +66 -0
  222. package/src/components/item/item.tsx +218 -0
  223. package/src/components/item/item.types.ts +12 -0
  224. package/src/components/item/item.variants.test.ts +263 -0
  225. package/src/components/item/item.variants.ts +200 -0
  226. package/src/components/kpi/AGENTS.md +125 -0
  227. package/src/components/kpi/index.ts +45 -0
  228. package/src/components/kpi/kpi-action.tsx +12 -0
  229. package/src/components/kpi/kpi-content.tsx +35 -0
  230. package/src/components/kpi/kpi-footer.tsx +52 -0
  231. package/src/components/kpi/kpi-group.tsx +70 -0
  232. package/src/components/kpi/kpi-header.tsx +25 -0
  233. package/src/components/kpi/kpi-icon.tsx +28 -0
  234. package/src/components/kpi/kpi-sparkline.tsx +145 -0
  235. package/src/components/kpi/kpi-stat.tsx +20 -0
  236. package/src/components/kpi/kpi-title.tsx +19 -0
  237. package/src/components/kpi/kpi-trend.tsx +105 -0
  238. package/src/components/kpi/kpi-value.tsx +32 -0
  239. package/src/components/kpi/kpi.context.tsx +111 -0
  240. package/src/components/kpi/kpi.tsx +154 -0
  241. package/src/components/kpi/kpi.types.ts +9 -0
  242. package/src/components/kpi/kpi.variants.test.ts +428 -0
  243. package/src/components/kpi/kpi.variants.ts +347 -0
  244. package/src/components/label/AGENTS.md +89 -0
  245. package/src/components/label/index.ts +10 -0
  246. package/src/components/label/label.tsx +97 -0
  247. package/src/components/label/label.variants.test.ts +176 -0
  248. package/src/components/label/label.variants.ts +123 -0
  249. package/src/components/menu/AGENTS.md +176 -0
  250. package/src/components/menu/index.ts +44 -0
  251. package/src/components/menu/menu-background.tsx +17 -0
  252. package/src/components/menu/menu-checkbox-item.tsx +57 -0
  253. package/src/components/menu/menu-content.tsx +303 -0
  254. package/src/components/menu/menu-item.tsx +162 -0
  255. package/src/components/menu/menu-label.tsx +14 -0
  256. package/src/components/menu/menu-radio-group.tsx +50 -0
  257. package/src/components/menu/menu-radio-item.tsx +57 -0
  258. package/src/components/menu/menu-separator.tsx +11 -0
  259. package/src/components/menu/menu-sub-content.tsx +72 -0
  260. package/src/components/menu/menu-sub-trigger.tsx +50 -0
  261. package/src/components/menu/menu-sub.tsx +75 -0
  262. package/src/components/menu/menu-trigger.tsx +57 -0
  263. package/src/components/menu/menu.context.tsx +214 -0
  264. package/src/components/menu/menu.tsx +88 -0
  265. package/src/components/menu/menu.types.ts +33 -0
  266. package/src/components/menu/menu.variants.test.ts +400 -0
  267. package/src/components/menu/menu.variants.ts +353 -0
  268. package/src/components/meter/AGENTS.md +135 -0
  269. package/src/components/meter/index.ts +43 -0
  270. package/src/components/meter/meter-fill.tsx +19 -0
  271. package/src/components/meter/meter-header.tsx +17 -0
  272. package/src/components/meter/meter-label.tsx +18 -0
  273. package/src/components/meter/meter-output.tsx +42 -0
  274. package/src/components/meter/meter-segments.tsx +83 -0
  275. package/src/components/meter/meter-track.tsx +16 -0
  276. package/src/components/meter/meter.context.tsx +62 -0
  277. package/src/components/meter/meter.tsx +286 -0
  278. package/src/components/meter/meter.types.ts +30 -0
  279. package/src/components/meter/meter.variants.test.ts +430 -0
  280. package/src/components/meter/meter.variants.ts +347 -0
  281. package/src/components/overlay/AGENTS.md +89 -0
  282. package/src/components/overlay/index.ts +27 -0
  283. package/src/components/overlay/overlay-portal.tsx +84 -0
  284. package/src/components/overlay/overlay-presence.test.ts +46 -0
  285. package/src/components/overlay/overlay-presence.ts +49 -0
  286. package/src/components/overlay/overlay-registry.test.ts +96 -0
  287. package/src/components/overlay/overlay-registry.ts +99 -0
  288. package/src/components/overlay/overlay-scrim.tsx +49 -0
  289. package/src/components/overlay/overlay.context.tsx +48 -0
  290. package/src/components/overlay/overlay.tsx +118 -0
  291. package/src/components/overlay/overlay.variants.test.ts +37 -0
  292. package/src/components/overlay/overlay.variants.ts +33 -0
  293. package/src/components/overlay/use-overlay-back-handler.ts +34 -0
  294. package/src/components/overlay/use-overlay-presence.ts +87 -0
  295. package/src/components/popover/AGENTS.md +138 -0
  296. package/src/components/popover/index.ts +48 -0
  297. package/src/components/popover/popover-anchor.tsx +49 -0
  298. package/src/components/popover/popover-arrow.tsx +89 -0
  299. package/src/components/popover/popover-close.tsx +73 -0
  300. package/src/components/popover/popover-content.tsx +235 -0
  301. package/src/components/popover/popover-description.tsx +18 -0
  302. package/src/components/popover/popover-title.tsx +33 -0
  303. package/src/components/popover/popover-trigger.tsx +56 -0
  304. package/src/components/popover/popover.context.tsx +71 -0
  305. package/src/components/popover/popover.position.test.ts +354 -0
  306. package/src/components/popover/popover.position.ts +286 -0
  307. package/src/components/popover/popover.tsx +123 -0
  308. package/src/components/popover/popover.variants.test.ts +135 -0
  309. package/src/components/popover/popover.variants.ts +85 -0
  310. package/src/components/popover/use-anchor-measure.ts +76 -0
  311. package/src/components/popover/use-anchored-content.ts +219 -0
  312. package/src/components/pressable/AGENTS.md +11 -1
  313. package/src/components/pressable/pressable.tsx +7 -2
  314. package/src/components/pressable/pressable.variants.test.ts +29 -0
  315. package/src/components/pressable/pressable.variants.ts +17 -0
  316. package/src/components/progress/AGENTS.md +120 -0
  317. package/src/components/progress/index.ts +40 -0
  318. package/src/components/progress/progress-fill.tsx +152 -0
  319. package/src/components/progress/progress-header.tsx +19 -0
  320. package/src/components/progress/progress-label.tsx +27 -0
  321. package/src/components/progress/progress-output.tsx +47 -0
  322. package/src/components/progress/progress-track.tsx +38 -0
  323. package/src/components/progress/progress.context.tsx +79 -0
  324. package/src/components/progress/progress.tsx +171 -0
  325. package/src/components/progress/progress.types.ts +23 -0
  326. package/src/components/progress/progress.variants.test.ts +335 -0
  327. package/src/components/progress/progress.variants.ts +284 -0
  328. package/src/components/progress-button/AGENTS.md +116 -0
  329. package/src/components/progress-button/index.ts +36 -0
  330. package/src/components/progress-button/progress-button-done.tsx +33 -0
  331. package/src/components/progress-button/progress-button-label.tsx +27 -0
  332. package/src/components/progress-button/progress-button.context.tsx +93 -0
  333. package/src/components/progress-button/progress-button.tsx +414 -0
  334. package/src/components/progress-button/progress-button.variants.test.ts +314 -0
  335. package/src/components/progress-button/progress-button.variants.ts +236 -0
  336. package/src/components/provider/AGENTS.md +38 -25
  337. package/src/components/provider/provider.tsx +38 -11
  338. package/src/components/radio/AGENTS.md +8 -10
  339. package/src/components/radio/radio.variants.test.ts +5 -6
  340. package/src/components/radio/radio.variants.ts +6 -7
  341. package/src/components/rating/AGENTS.md +143 -0
  342. package/src/components/rating/index.ts +43 -0
  343. package/src/components/rating/rating-output.tsx +39 -0
  344. package/src/components/rating/rating-star.tsx +70 -0
  345. package/src/components/rating/rating-stars.tsx +219 -0
  346. package/src/components/rating/rating.context.tsx +79 -0
  347. package/src/components/rating/rating.tsx +163 -0
  348. package/src/components/rating/rating.variants.test.ts +347 -0
  349. package/src/components/rating/rating.variants.ts +354 -0
  350. package/src/components/selection-mode/AGENTS.md +155 -0
  351. package/src/components/selection-mode/index.ts +43 -0
  352. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  353. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  354. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  355. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  356. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  357. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  358. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  359. package/src/components/selection-mode/selection-mode.tsx +207 -0
  360. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  361. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  362. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  363. package/src/components/skeleton/AGENTS.md +120 -0
  364. package/src/components/skeleton/index.ts +15 -0
  365. package/src/components/skeleton/skeleton-clock.ts +48 -0
  366. package/src/components/skeleton/skeleton-group.tsx +64 -0
  367. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  368. package/src/components/skeleton/skeleton.context.tsx +45 -0
  369. package/src/components/skeleton/skeleton.tsx +227 -0
  370. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  371. package/src/components/skeleton/skeleton.variants.ts +222 -0
  372. package/src/components/slide-button/AGENTS.md +122 -0
  373. package/src/components/slide-button/index.ts +43 -0
  374. package/src/components/slide-button/slide-button-label.tsx +70 -0
  375. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  376. package/src/components/slide-button/slide-button.context.tsx +76 -0
  377. package/src/components/slide-button/slide-button.tsx +457 -0
  378. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  379. package/src/components/slide-button/slide-button.variants.ts +324 -0
  380. package/src/components/slider/index.ts +2 -0
  381. package/src/components/slider/slider-thumb.tsx +33 -1
  382. package/src/components/slider/slider.variants.test.ts +52 -0
  383. package/src/components/slider/slider.variants.ts +56 -2
  384. package/src/components/stack-card/AGENTS.md +169 -0
  385. package/src/components/stack-card/index.ts +37 -0
  386. package/src/components/stack-card/stack-card-action.tsx +66 -0
  387. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  388. package/src/components/stack-card/stack-card-card.tsx +71 -0
  389. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  390. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  391. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  392. package/src/components/stack-card/stack-card.context.tsx +117 -0
  393. package/src/components/stack-card/stack-card.tsx +557 -0
  394. package/src/components/stack-card/stack-card.types.ts +26 -0
  395. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  396. package/src/components/stack-card/stack-card.variants.ts +360 -0
  397. package/src/components/steps/AGENTS.md +153 -0
  398. package/src/components/steps/index.ts +45 -0
  399. package/src/components/steps/steps-connector.tsx +31 -0
  400. package/src/components/steps/steps-description.tsx +26 -0
  401. package/src/components/steps/steps-indicator.tsx +106 -0
  402. package/src/components/steps/steps-item.tsx +225 -0
  403. package/src/components/steps/steps-panel.tsx +41 -0
  404. package/src/components/steps/steps-title.tsx +32 -0
  405. package/src/components/steps/steps-track.tsx +59 -0
  406. package/src/components/steps/steps.context.tsx +123 -0
  407. package/src/components/steps/steps.tsx +166 -0
  408. package/src/components/steps/steps.variants.test.ts +388 -0
  409. package/src/components/steps/steps.variants.ts +468 -0
  410. package/src/components/surface/AGENTS.md +82 -0
  411. package/src/components/surface/index.ts +15 -0
  412. package/src/components/surface/surface.context.tsx +56 -0
  413. package/src/components/surface/surface.tsx +87 -0
  414. package/src/components/surface/surface.variants.test.ts +227 -0
  415. package/src/components/surface/surface.variants.ts +165 -0
  416. package/src/components/swipe/AGENTS.md +140 -0
  417. package/src/components/swipe/index.ts +31 -0
  418. package/src/components/swipe/swipe-action.tsx +99 -0
  419. package/src/components/swipe/swipe-end.tsx +14 -0
  420. package/src/components/swipe/swipe-group.tsx +55 -0
  421. package/src/components/swipe/swipe-panel.tsx +55 -0
  422. package/src/components/swipe/swipe-start.tsx +15 -0
  423. package/src/components/swipe/swipe.context.tsx +135 -0
  424. package/src/components/swipe/swipe.tsx +393 -0
  425. package/src/components/swipe/swipe.types.ts +7 -0
  426. package/src/components/swipe/swipe.variants.test.ts +337 -0
  427. package/src/components/swipe/swipe.variants.ts +290 -0
  428. package/src/components/switch/switch.variants.ts +2 -2
  429. package/src/components/tabs/AGENTS.md +4 -3
  430. package/src/components/tabs/tabs.variants.test.ts +1 -1
  431. package/src/components/tabs/tabs.variants.ts +4 -6
  432. package/src/components/text/AGENTS.md +8 -2
  433. package/src/components/text/text.tsx +7 -0
  434. package/src/components/text/text.variants.test.ts +24 -1
  435. package/src/components/text/text.variants.ts +5 -0
  436. package/src/components/textarea/AGENTS.md +89 -0
  437. package/src/components/textarea/index.ts +16 -0
  438. package/src/components/textarea/textarea.tsx +157 -0
  439. package/src/components/textarea/textarea.variants.test.ts +219 -0
  440. package/src/components/textarea/textarea.variants.ts +171 -0
  441. package/src/components/toast/AGENTS.md +140 -0
  442. package/src/components/toast/index.ts +74 -0
  443. package/src/components/toast/toast-action.tsx +59 -0
  444. package/src/components/toast/toast-api.ts +32 -0
  445. package/src/components/toast/toast-close.tsx +44 -0
  446. package/src/components/toast/toast-content.tsx +11 -0
  447. package/src/components/toast/toast-description.tsx +11 -0
  448. package/src/components/toast/toast-indicator.tsx +34 -0
  449. package/src/components/toast/toast-item.tsx +308 -0
  450. package/src/components/toast/toast-title.tsx +13 -0
  451. package/src/components/toast/toast-viewport.tsx +135 -0
  452. package/src/components/toast/toast.context.tsx +69 -0
  453. package/src/components/toast/toast.store.test.ts +259 -0
  454. package/src/components/toast/toast.store.ts +338 -0
  455. package/src/components/toast/toast.timer.test.ts +64 -0
  456. package/src/components/toast/toast.timer.ts +51 -0
  457. package/src/components/toast/toast.tsx +132 -0
  458. package/src/components/toast/toast.variants.test.ts +393 -0
  459. package/src/components/toast/toast.variants.ts +304 -0
  460. package/src/components/toggle-button/AGENTS.md +106 -0
  461. package/src/components/toggle-button/index.ts +30 -0
  462. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  463. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  464. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  465. package/src/components/toggle-button/toggle-button.tsx +172 -0
  466. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  467. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  468. package/src/components/tooltip/AGENTS.md +133 -0
  469. package/src/components/tooltip/index.ts +28 -0
  470. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  471. package/src/components/tooltip/tooltip-content.tsx +179 -0
  472. package/src/components/tooltip/tooltip-description.tsx +19 -0
  473. package/src/components/tooltip/tooltip-text.tsx +21 -0
  474. package/src/components/tooltip/tooltip-title.tsx +20 -0
  475. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  476. package/src/components/tooltip/tooltip.context.tsx +68 -0
  477. package/src/components/tooltip/tooltip.tsx +213 -0
  478. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  479. package/src/components/tooltip/tooltip.variants.ts +177 -0
  480. package/src/hooks/use-calm-motion.tsx +31 -0
  481. package/src/lib/calm-motion.test.ts +17 -0
  482. package/src/lib/calm-motion.ts +23 -0
  483. package/src/lib/cn.parity.test.ts +62 -0
  484. package/src/lib/cn.test.ts +14 -0
  485. package/src/lib/cn.ts +11 -6
  486. package/src/no-gorhom.test.ts +54 -0
  487. package/src/styles/geometry.test.ts +1 -0
  488. package/src/styles/theme.css +4 -0
  489. package/src/styles/tokens.css +44 -0
  490. package/src/styles/tokens.test.ts +56 -0
  491. package/src/styles/tokens.ts +29 -1
  492. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  493. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -0,0 +1,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";
@@ -0,0 +1,183 @@
1
+ import { type ReactElement, type ReactNode, useCallback, useLayoutEffect, useMemo, useRef } from "react";
2
+ import { type LayoutChangeEvent, View, type ViewProps } from "react-native";
3
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
+ import Animated, {
5
+ Easing,
6
+ useAnimatedStyle,
7
+ useSharedValue,
8
+ withSequence,
9
+ withSpring,
10
+ withTiming,
11
+ } from "react-native-reanimated";
12
+ import { scheduleOnRN } from "react-native-worklets";
13
+ import { useCalmMotion } from "../../hooks/use-calm-motion";
14
+ import { playHaptic } from "../pressable";
15
+ import { useCalendarPart } from "./calendar.context";
16
+ import { type CalendarCell, formatMonthYear, monthGrid, serialiseDate } from "./calendar.date";
17
+ import { calendarVariants, resolvePageDirection } from "./calendar.variants";
18
+ import { CalendarDay } from "./calendar-day";
19
+ import { CalendarPicker } from "./calendar-picker";
20
+
21
+ export type CalendarGridProps = Omit<ViewProps, "children"> & {
22
+ className?: string;
23
+ /** Draws one cell. Omitted, a `Calendar.Day` for its date. */
24
+ renderDay?: (cell: CalendarCell) => ReactNode;
25
+ };
26
+
27
+ /** How far a sideways drag travels before the grid claims it, and a vertical one before it gives up. */
28
+ const PAN_ACTIVATE_X = 12;
29
+ const PAN_FAIL_Y = 12;
30
+
31
+ /** How far a drag past a bound still follows the finger, as a fraction — enough to feel the edge. */
32
+ const BOUND_RESISTANCE = 0.2;
33
+
34
+ const SLIDE = { duration: 200, easing: Easing.out(Easing.cubic) } as const;
35
+ const SETTLE_SPRING = { damping: 20, stiffness: 260 } as const;
36
+
37
+ /**
38
+ * The six weeks of the visible month, and the surface a swipe pages on.
39
+ *
40
+ * **Paging is a slide out and a slide in.** A released swipe slides the old month off the side it
41
+ * was dragged towards, the month changes, and the new one slides in from the other side — the
42
+ * same entrance the arrows give it. Both off-screen positions are clipped, so the frame where the
43
+ * month swaps is never seen. With calm motion on, both slides are skipped and the page simply
44
+ * swaps; the page change itself is behaviour, not decoration, and always happens.
45
+ *
46
+ * **A drag towards a bound resists** rather than moving freely, then settles back with a light
47
+ * impact haptic — the edge is felt instead of the month silently refusing to turn.
48
+ *
49
+ * **The grid claims a sideways drag and gives up a vertical one**, so it lives inside a scrolling
50
+ * screen or a sheet without stealing the scroll.
51
+ *
52
+ * It also hosts `Calendar.Picker` over itself while a jump view is open, and hides its weeks
53
+ * rather than unmounting them, so the calendar's height never moves.
54
+ */
55
+ export function CalendarGrid({ className, renderDay, ...props }: CalendarGridProps): ReactElement {
56
+ const calendar = useCalendarPart("Calendar.Grid");
57
+ const {
58
+ canGoNext,
59
+ canGoPrev,
60
+ goNext,
61
+ goPrev,
62
+ isDisabled,
63
+ label,
64
+ locale,
65
+ pageDirection,
66
+ size,
67
+ swipeToPage,
68
+ variant,
69
+ view,
70
+ } = calendar;
71
+ const { visibleMonth, weekStartsOn } = calendar;
72
+ const slots = calendarVariants({ size, variant });
73
+ const isCalm = useCalmMotion();
74
+
75
+ const width = useSharedValue(0);
76
+ const translateX = useSharedValue(0);
77
+
78
+ const handleLayout = useCallback(
79
+ (event: LayoutChangeEvent) => {
80
+ width.value = event.nativeEvent.layout.width;
81
+ },
82
+ [width]
83
+ );
84
+
85
+ const page = useCallback((direction: -1 | 1) => (direction === 1 ? goNext() : goPrev()), [goNext, goPrev]);
86
+
87
+ const pan = useMemo(
88
+ () =>
89
+ Gesture.Pan()
90
+ .enabled(swipeToPage && view === "days" && !isDisabled)
91
+ .activeOffsetX([-PAN_ACTIVATE_X, PAN_ACTIVATE_X])
92
+ .failOffsetY([-PAN_FAIL_Y, PAN_FAIL_Y])
93
+ .onUpdate((event) => {
94
+ "worklet";
95
+ const blocked = (event.translationX > 0 && !canGoPrev) || (event.translationX < 0 && !canGoNext);
96
+ translateX.value = blocked ? event.translationX * BOUND_RESISTANCE : event.translationX;
97
+ })
98
+ .onEnd((event) => {
99
+ "worklet";
100
+ const direction = resolvePageDirection({
101
+ canGoNext,
102
+ canGoPrev,
103
+ translationX: event.translationX,
104
+ velocityX: event.velocityX,
105
+ width: width.value,
106
+ });
107
+
108
+ if (direction === 0) {
109
+ const blocked = (event.translationX > 0 && !canGoPrev) || (event.translationX < 0 && !canGoNext);
110
+ if (blocked) playHaptic("light");
111
+ translateX.value = withSpring(0, SETTLE_SPRING);
112
+ return;
113
+ }
114
+
115
+ playHaptic("selection");
116
+ if (isCalm) {
117
+ translateX.value = 0;
118
+ scheduleOnRN(page, direction);
119
+ return;
120
+ }
121
+ translateX.value = withTiming(-direction * width.value, SLIDE, (finished) => {
122
+ "worklet";
123
+ if (finished) scheduleOnRN(page, direction);
124
+ });
125
+ })
126
+ .onFinalize((_event, success) => {
127
+ "worklet";
128
+ if (!success) translateX.value = withSpring(0, SETTLE_SPRING);
129
+ }),
130
+ [canGoNext, canGoPrev, isCalm, isDisabled, page, swipeToPage, translateX, view, width]
131
+ );
132
+
133
+ // The new month slides in from the side the old one left by. Skipped on the first render — a
134
+ // calendar does not animate onto the screen — and for a jump, which has no side.
135
+ const monthKey = serialiseDate(visibleMonth);
136
+ const previousKey = useRef(monthKey);
137
+ useLayoutEffect(() => {
138
+ if (previousKey.current === monthKey) return;
139
+ previousKey.current = monthKey;
140
+ if (isCalm || pageDirection === 0 || width.value <= 0) {
141
+ translateX.value = 0;
142
+ return;
143
+ }
144
+ translateX.value = withSequence(withTiming(pageDirection * width.value, { duration: 0 }), withTiming(0, SLIDE));
145
+ }, [isCalm, monthKey, pageDirection, translateX, width]);
146
+
147
+ const slideStyle = useAnimatedStyle(() => ({ transform: [{ translateX: translateX.value }] }));
148
+
149
+ const weeks = useMemo(() => monthGrid(visibleMonth, weekStartsOn), [visibleMonth, weekStartsOn]);
150
+ const isPicking = view !== "days";
151
+
152
+ return (
153
+ <View className={slots.grid({ className })} onLayout={handleLayout} {...props}>
154
+ <GestureDetector gesture={pan}>
155
+ <Animated.View
156
+ accessibilityElementsHidden={isPicking}
157
+ accessibilityLabel={label ?? formatMonthYear(visibleMonth, locale)}
158
+ role="grid"
159
+ className={isPicking ? "opacity-0" : undefined}
160
+ importantForAccessibility={isPicking ? "no-hide-descendants" : "auto"}
161
+ pointerEvents={isPicking ? "none" : "auto"}
162
+ style={slideStyle}
163
+ >
164
+ {weeks.map((week) => (
165
+ <View className={slots.week()} key={serialiseDate(week[0]?.date ?? visibleMonth)}>
166
+ {week.map((cell) =>
167
+ renderDay ? (
168
+ <View className="flex-1" key={serialiseDate(cell.date)}>
169
+ {renderDay(cell)}
170
+ </View>
171
+ ) : (
172
+ <CalendarDay date={cell.date} key={serialiseDate(cell.date)} />
173
+ )
174
+ )}
175
+ </View>
176
+ ))}
177
+ </Animated.View>
178
+ </GestureDetector>
179
+ {isPicking ? <CalendarPicker /> : null}
180
+ </View>
181
+ );
182
+ }
183
+ CalendarGrid.displayName = "DelacourUI.Calendar.Grid";
@@ -0,0 +1,34 @@
1
+ import type { ReactElement } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { useCalendarPart } from "./calendar.context";
4
+ import { calendarVariants } from "./calendar.variants";
5
+ import { CalendarCaption } from "./calendar-caption";
6
+ import { CalendarNav } from "./calendar-nav";
7
+
8
+ export type CalendarHeaderProps = ViewProps & {
9
+ className?: string;
10
+ };
11
+
12
+ /**
13
+ * The row above the grid: the previous arrow, the caption and the next arrow.
14
+ *
15
+ * Omit `children` for that default; pass them to rearrange it — both arrows on one side, say, or
16
+ * a caption with a "Today" button beside it.
17
+ */
18
+ export function CalendarHeader({ className, children, ...props }: CalendarHeaderProps): ReactElement {
19
+ const { size, variant } = useCalendarPart("Calendar.Header");
20
+ const slots = calendarVariants({ size, variant });
21
+
22
+ return (
23
+ <View className={slots.header({ className })} {...props}>
24
+ {children ?? (
25
+ <>
26
+ <CalendarNav direction="prev" />
27
+ <CalendarCaption />
28
+ <CalendarNav direction="next" />
29
+ </>
30
+ )}
31
+ </View>
32
+ );
33
+ }
34
+ CalendarHeader.displayName = "DelacourUI.Calendar.Header";
@@ -0,0 +1,51 @@
1
+ import type { ReactElement } from "react";
2
+ import { IconChevronLeft, IconChevronRight } from "../../icons/central";
3
+ import { Button, type ButtonProps } from "../button";
4
+ import { Icon } from "../icon";
5
+ import { useCalendarPart } from "./calendar.context";
6
+ import { resolvePartTestID } from "./calendar.variants";
7
+
8
+ export type CalendarNavProps = Omit<ButtonProps, "children" | "onPress"> & {
9
+ /** Which way the arrow pages. */
10
+ direction: "prev" | "next";
11
+ /** The arrow's accessible name. Defaults to "Previous month" / "Next month" — or year, in the months view. */
12
+ label?: string;
13
+ };
14
+
15
+ /**
16
+ * One paging arrow: a ghost icon button that disables itself at the bound it points at.
17
+ *
18
+ * In the month grid it pages a month; in the months view, a year; in the years view, where the
19
+ * list scrolls instead, it is disabled. A disabled calendar disables it too — a read-only one
20
+ * does not, because looking at another month changes nothing.
21
+ */
22
+ export function CalendarNav({ direction, label, isDisabled, ...props }: CalendarNavProps): ReactElement {
23
+ const {
24
+ canGoNext,
25
+ canGoPrev,
26
+ goNext,
27
+ goPrev,
28
+ isDisabled: calendarDisabled,
29
+ testID,
30
+ view,
31
+ } = useCalendarPart("Calendar.Nav");
32
+ const isPrev = direction === "prev";
33
+ const canGo = (isPrev ? canGoPrev : canGoNext) && view !== "years";
34
+ const unit = view === "months" ? "year" : "month";
35
+
36
+ return (
37
+ <Button
38
+ accessibilityLabel={label ?? `${isPrev ? "Previous" : "Next"} ${unit}`}
39
+ haptic="selection"
40
+ isDisabled={isDisabled ?? (calendarDisabled || !canGo)}
41
+ onPress={isPrev ? goPrev : goNext}
42
+ size="icon-sm"
43
+ testID={resolvePartTestID(testID, direction)}
44
+ variant="ghost"
45
+ {...props}
46
+ >
47
+ <Icon icon={isPrev ? IconChevronLeft : IconChevronRight} />
48
+ </Button>
49
+ );
50
+ }
51
+ CalendarNav.displayName = "DelacourUI.Calendar.Nav";
@@ -0,0 +1,128 @@
1
+ import { type ReactElement, useCallback, useMemo, useRef } from "react";
2
+ import { type LayoutChangeEvent, ScrollView, View, type ViewProps } from "react-native";
3
+ import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
4
+ import { useCalmMotion } from "../../hooks/use-calm-motion";
5
+ import { cn } from "../../lib/cn";
6
+ import { Pressable } from "../pressable";
7
+ import { Text } from "../text";
8
+ import { useCalendarPart } from "./calendar.context";
9
+ import { compareMonths, formatYear, monthNames } from "./calendar.date";
10
+ import { calendarVariants, isMonthInBounds, resolvePartTestID } from "./calendar.variants";
11
+
12
+ export type CalendarPickerProps = Omit<ViewProps, "children"> & {
13
+ className?: string;
14
+ };
15
+
16
+ const CROSSFADE_MS = 150;
17
+
18
+ /**
19
+ * The months and years jump views, laid over the grid while one is open.
20
+ *
21
+ * **Months** is twelve names in a three-column grid: a tap shows that month and returns to the
22
+ * days. **Years** is a scrolling three-column list over the calendar's year span, opened already
23
+ * scrolled to the visible year: a tap returns to the months view in that year, so a birth date
24
+ * is three taps — caption, caption, year — then a month and a day.
25
+ *
26
+ * A month outside the paging bounds is disabled. The view crossfades with the grid it covers,
27
+ * and with calm motion on it simply appears.
28
+ *
29
+ * Rendered by `Calendar.Grid`, which sizes it — it fills the grid's own box, so opening it never
30
+ * changes the calendar's height. Exported for a custom grid that wants to host it itself.
31
+ */
32
+ export function CalendarPicker({ className, ...props }: CalendarPickerProps): ReactElement {
33
+ const calendar = useCalendarPart("Calendar.Picker");
34
+ const { goToMonth, isDisabled, locale, monthBounds, setView, size, variant, view, visibleMonth, yearSpan } = calendar;
35
+ const isCalm = useCalmMotion();
36
+ const slots = calendarVariants({ size, variant });
37
+
38
+ const names = useMemo(() => monthNames(locale, "short"), [locale]);
39
+ const years = useMemo(() => {
40
+ const list: number[] = [];
41
+ for (let year = yearSpan.from; year <= yearSpan.to; year += 1) list.push(year);
42
+ return list;
43
+ }, [yearSpan.from, yearSpan.to]);
44
+
45
+ const scroller = useRef<ScrollView>(null);
46
+ const hasScrolled = useRef(false);
47
+ const handleCurrentLayout = useCallback((event: LayoutChangeEvent) => {
48
+ if (hasScrolled.current) return;
49
+ hasScrolled.current = true;
50
+ scroller.current?.scrollTo({ animated: false, y: event.nativeEvent.layout.y });
51
+ }, []);
52
+
53
+ const pickMonth = useCallback(
54
+ (month: number) => {
55
+ goToMonth({ day: 1, month, year: visibleMonth.year });
56
+ setView("days");
57
+ },
58
+ [goToMonth, setView, visibleMonth.year]
59
+ );
60
+
61
+ const pickYear = useCallback(
62
+ (year: number) => {
63
+ goToMonth({ day: 1, month: visibleMonth.month, year });
64
+ setView("months");
65
+ },
66
+ [goToMonth, setView, visibleMonth.month]
67
+ );
68
+
69
+ const fade = isCalm ? undefined : FadeIn.duration(CROSSFADE_MS);
70
+ const fadeOut = isCalm ? undefined : FadeOut.duration(CROSSFADE_MS);
71
+
72
+ return (
73
+ <Animated.View className={cn("absolute inset-0", className)} entering={fade} exiting={fadeOut} {...props}>
74
+ {view === "months" ? (
75
+ <View accessibilityRole="list" className={slots.pickerGrid()}>
76
+ {names.map((name, index) => {
77
+ const month = index + 1;
78
+ const isCurrent = month === visibleMonth.month;
79
+ const inBounds = isMonthInBounds({ day: 1, month, year: visibleMonth.year }, monthBounds);
80
+ const item = calendarVariants({ size, tone: isCurrent ? "selected" : "plain", variant });
81
+ return (
82
+ <Pressable
83
+ accessibilityState={{ selected: isCurrent }}
84
+ className={item.pickerItem({ className: inBounds ? undefined : "opacity-40" })}
85
+ disabled={isDisabled || !inBounds}
86
+ feedback="fade"
87
+ haptic="selection"
88
+ key={name}
89
+ onPress={() => pickMonth(month)}
90
+ testID={resolvePartTestID(calendar.testID, `month-${month}`)}
91
+ >
92
+ <Text className={item.pickerItemLabel()}>{name}</Text>
93
+ </Pressable>
94
+ );
95
+ })}
96
+ </View>
97
+ ) : (
98
+ <ScrollView ref={scroller} showsVerticalScrollIndicator={false}>
99
+ <View accessibilityRole="list" className={slots.pickerGrid()}>
100
+ {years.map((year) => {
101
+ const isCurrent = year === visibleMonth.year;
102
+ const item = calendarVariants({ size, tone: isCurrent ? "selected" : "plain", variant });
103
+ const inBounds =
104
+ (!monthBounds.first || compareMonths({ day: 1, month: 12, year }, monthBounds.first) >= 0) &&
105
+ (!monthBounds.last || compareMonths({ day: 1, month: 1, year }, monthBounds.last) <= 0);
106
+ return (
107
+ <Pressable
108
+ accessibilityState={{ selected: isCurrent }}
109
+ className={item.pickerItem({ className: inBounds ? undefined : "opacity-40" })}
110
+ disabled={isDisabled || !inBounds}
111
+ feedback="fade"
112
+ haptic="selection"
113
+ key={year}
114
+ onLayout={isCurrent ? handleCurrentLayout : undefined}
115
+ onPress={() => pickYear(year)}
116
+ testID={resolvePartTestID(calendar.testID, `year-${year}`)}
117
+ >
118
+ <Text className={item.pickerItemLabel()}>{formatYear(year, locale)}</Text>
119
+ </Pressable>
120
+ );
121
+ })}
122
+ </View>
123
+ </ScrollView>
124
+ )}
125
+ </Animated.View>
126
+ );
127
+ }
128
+ CalendarPicker.displayName = "DelacourUI.Calendar.Picker";
@@ -0,0 +1,48 @@
1
+ import { type ReactElement, useMemo } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { Text } from "../text";
4
+ import { useCalendarPart } from "./calendar.context";
5
+ import { weekdayLabels } from "./calendar.date";
6
+ import { calendarVariants } from "./calendar.variants";
7
+
8
+ export type CalendarWeekdaysProps = Omit<ViewProps, "children"> & {
9
+ className?: string;
10
+ /** How the names are drawn. The accessible name is always the full one. */
11
+ format?: "narrow" | "short";
12
+ };
13
+
14
+ /**
15
+ * The row of weekday names, in the grid's column order and the calendar's locale.
16
+ *
17
+ * Each column says its short name and is announced by its full one, so a screen reader reads
18
+ * "Monday" rather than "Mon" — or "M", which is two different days. Hidden, not removed, while a
19
+ * jump view is open, so the calendar's height holds still.
20
+ */
21
+ export function CalendarWeekdays({ className, format = "short", ...props }: CalendarWeekdaysProps): ReactElement {
22
+ const { locale, size, variant, view, weekStartsOn } = useCalendarPart("Calendar.Weekdays");
23
+ const slots = calendarVariants({ size, variant });
24
+ const isHidden = view !== "days";
25
+
26
+ const names = useMemo(
27
+ () => ({ shown: weekdayLabels(locale, weekStartsOn, format), spoken: weekdayLabels(locale, weekStartsOn, "long") }),
28
+ [format, locale, weekStartsOn]
29
+ );
30
+
31
+ return (
32
+ <View
33
+ accessibilityElementsHidden={isHidden}
34
+ className={slots.weekdays({ className: [isHidden ? "opacity-0" : undefined, className] })}
35
+ importantForAccessibility={isHidden ? "no-hide-descendants" : "auto"}
36
+ {...props}
37
+ >
38
+ {names.shown.map((name, index) => (
39
+ <View className={slots.weekday()} key={names.spoken[index]}>
40
+ <Text accessibilityLabel={names.spoken[index]} className={slots.weekdayLabel()}>
41
+ {name}
42
+ </Text>
43
+ </View>
44
+ ))}
45
+ </View>
46
+ );
47
+ }
48
+ CalendarWeekdays.displayName = "DelacourUI.Calendar.Weekdays";