@delacour/react-native-ui 0.1.0-alpha.4 → 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 (481) hide show
  1. package/README.md +10 -6
  2. package/package.json +48 -4
  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 -231
  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 -208
  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 -122
  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 -62
  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 -87
  55. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
  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 +8 -0
  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/selection-mode/AGENTS.md +155 -0
  344. package/src/components/selection-mode/index.ts +43 -0
  345. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  346. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  347. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  348. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  349. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  350. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  351. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  352. package/src/components/selection-mode/selection-mode.tsx +207 -0
  353. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  354. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  355. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  356. package/src/components/skeleton/AGENTS.md +120 -0
  357. package/src/components/skeleton/index.ts +15 -0
  358. package/src/components/skeleton/skeleton-clock.ts +48 -0
  359. package/src/components/skeleton/skeleton-group.tsx +64 -0
  360. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  361. package/src/components/skeleton/skeleton.context.tsx +45 -0
  362. package/src/components/skeleton/skeleton.tsx +227 -0
  363. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  364. package/src/components/skeleton/skeleton.variants.ts +222 -0
  365. package/src/components/slide-button/AGENTS.md +122 -0
  366. package/src/components/slide-button/index.ts +43 -0
  367. package/src/components/slide-button/slide-button-label.tsx +70 -0
  368. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  369. package/src/components/slide-button/slide-button.context.tsx +76 -0
  370. package/src/components/slide-button/slide-button.tsx +457 -0
  371. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  372. package/src/components/slide-button/slide-button.variants.ts +324 -0
  373. package/src/components/slider/index.ts +2 -0
  374. package/src/components/slider/slider-thumb.tsx +33 -1
  375. package/src/components/slider/slider.variants.test.ts +52 -0
  376. package/src/components/slider/slider.variants.ts +56 -2
  377. package/src/components/stack-card/AGENTS.md +169 -0
  378. package/src/components/stack-card/index.ts +37 -0
  379. package/src/components/stack-card/stack-card-action.tsx +66 -0
  380. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  381. package/src/components/stack-card/stack-card-card.tsx +71 -0
  382. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  383. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  384. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  385. package/src/components/stack-card/stack-card.context.tsx +117 -0
  386. package/src/components/stack-card/stack-card.tsx +557 -0
  387. package/src/components/stack-card/stack-card.types.ts +26 -0
  388. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  389. package/src/components/stack-card/stack-card.variants.ts +360 -0
  390. package/src/components/steps/AGENTS.md +153 -0
  391. package/src/components/steps/index.ts +45 -0
  392. package/src/components/steps/steps-connector.tsx +31 -0
  393. package/src/components/steps/steps-description.tsx +26 -0
  394. package/src/components/steps/steps-indicator.tsx +106 -0
  395. package/src/components/steps/steps-item.tsx +225 -0
  396. package/src/components/steps/steps-panel.tsx +41 -0
  397. package/src/components/steps/steps-title.tsx +32 -0
  398. package/src/components/steps/steps-track.tsx +59 -0
  399. package/src/components/steps/steps.context.tsx +123 -0
  400. package/src/components/steps/steps.tsx +166 -0
  401. package/src/components/steps/steps.variants.test.ts +388 -0
  402. package/src/components/steps/steps.variants.ts +468 -0
  403. package/src/components/surface/AGENTS.md +72 -0
  404. package/src/components/surface/index.ts +15 -0
  405. package/src/components/surface/surface.context.tsx +54 -0
  406. package/src/components/surface/surface.tsx +70 -0
  407. package/src/components/surface/surface.variants.test.ts +180 -0
  408. package/src/components/surface/surface.variants.ts +133 -0
  409. package/src/components/swipe/AGENTS.md +140 -0
  410. package/src/components/swipe/index.ts +31 -0
  411. package/src/components/swipe/swipe-action.tsx +99 -0
  412. package/src/components/swipe/swipe-end.tsx +14 -0
  413. package/src/components/swipe/swipe-group.tsx +55 -0
  414. package/src/components/swipe/swipe-panel.tsx +55 -0
  415. package/src/components/swipe/swipe-start.tsx +15 -0
  416. package/src/components/swipe/swipe.context.tsx +135 -0
  417. package/src/components/swipe/swipe.tsx +393 -0
  418. package/src/components/swipe/swipe.types.ts +7 -0
  419. package/src/components/swipe/swipe.variants.test.ts +337 -0
  420. package/src/components/swipe/swipe.variants.ts +290 -0
  421. package/src/components/switch/switch.variants.ts +2 -2
  422. package/src/components/tabs/AGENTS.md +4 -3
  423. package/src/components/tabs/tabs.variants.test.ts +1 -1
  424. package/src/components/tabs/tabs.variants.ts +4 -6
  425. package/src/components/textarea/AGENTS.md +86 -0
  426. package/src/components/textarea/index.ts +16 -0
  427. package/src/components/textarea/textarea.tsx +157 -0
  428. package/src/components/textarea/textarea.variants.test.ts +219 -0
  429. package/src/components/textarea/textarea.variants.ts +171 -0
  430. package/src/components/toast/AGENTS.md +140 -0
  431. package/src/components/toast/index.ts +74 -0
  432. package/src/components/toast/toast-action.tsx +59 -0
  433. package/src/components/toast/toast-api.ts +32 -0
  434. package/src/components/toast/toast-close.tsx +44 -0
  435. package/src/components/toast/toast-content.tsx +11 -0
  436. package/src/components/toast/toast-description.tsx +11 -0
  437. package/src/components/toast/toast-indicator.tsx +34 -0
  438. package/src/components/toast/toast-item.tsx +308 -0
  439. package/src/components/toast/toast-title.tsx +13 -0
  440. package/src/components/toast/toast-viewport.tsx +135 -0
  441. package/src/components/toast/toast.context.tsx +69 -0
  442. package/src/components/toast/toast.store.test.ts +259 -0
  443. package/src/components/toast/toast.store.ts +338 -0
  444. package/src/components/toast/toast.timer.test.ts +64 -0
  445. package/src/components/toast/toast.timer.ts +51 -0
  446. package/src/components/toast/toast.tsx +132 -0
  447. package/src/components/toast/toast.variants.test.ts +393 -0
  448. package/src/components/toast/toast.variants.ts +304 -0
  449. package/src/components/toggle-button/AGENTS.md +106 -0
  450. package/src/components/toggle-button/index.ts +30 -0
  451. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  452. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  453. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  454. package/src/components/toggle-button/toggle-button.tsx +172 -0
  455. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  456. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  457. package/src/components/tooltip/AGENTS.md +133 -0
  458. package/src/components/tooltip/index.ts +28 -0
  459. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  460. package/src/components/tooltip/tooltip-content.tsx +179 -0
  461. package/src/components/tooltip/tooltip-description.tsx +19 -0
  462. package/src/components/tooltip/tooltip-text.tsx +21 -0
  463. package/src/components/tooltip/tooltip-title.tsx +20 -0
  464. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  465. package/src/components/tooltip/tooltip.context.tsx +68 -0
  466. package/src/components/tooltip/tooltip.tsx +213 -0
  467. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  468. package/src/components/tooltip/tooltip.variants.ts +177 -0
  469. package/src/hooks/use-calm-motion.tsx +31 -0
  470. package/src/lib/calm-motion.test.ts +17 -0
  471. package/src/lib/calm-motion.ts +23 -0
  472. package/src/lib/cn.test.ts +14 -0
  473. package/src/lib/cn.ts +2 -2
  474. package/src/no-gorhom.test.ts +54 -0
  475. package/src/styles/geometry.test.ts +1 -0
  476. package/src/styles/theme.css +4 -0
  477. package/src/styles/tokens.css +11 -0
  478. package/src/styles/tokens.test.ts +22 -0
  479. package/src/styles/tokens.ts +20 -0
  480. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  481. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -0,0 +1,584 @@
1
+ /**
2
+ * Calendar dates without time zones, and everything a month grid needs to do with them.
3
+ *
4
+ * A {@link CalendarDate} is three numbers — a year, a month and a day — and nothing else. There is
5
+ * no instant behind it, so there is no time zone to shift it: the 5th is the 5th wherever the
6
+ * phone is. Every comparison here works on the three fields, and the only place a `Date` appears
7
+ * is at the two edges — {@link toDate}/{@link fromDate}, which read and write **local** fields,
8
+ * and the formatters, which hand `Intl` a UTC instant and ask it to format in UTC so the day it
9
+ * prints is the day it was given.
10
+ *
11
+ * This is a **leaf**: no React, no React Native, nothing but the language. `date-picker` and
12
+ * `date-time-picker` import it as `../calendar/calendar.date`, never `../calendar`, so a picker
13
+ * can do date maths without pulling the calendar's components in (AGENTS.md rule 3).
14
+ */
15
+
16
+ /** A day on the Gregorian calendar. `month` is 1–12 and `day` 1–31, as a person writes them. */
17
+ export type CalendarDate = { year: number; month: number; day: number };
18
+
19
+ /** A day of the week, `0` for Sunday through `6` for Saturday — the numbering `Date#getDay` uses. */
20
+ export type Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6;
21
+
22
+ /** A range that is empty, started, or complete. An end is never set without a start. */
23
+ export type CalendarDateRange = { start: CalendarDate | null; end: CalendarDate | null };
24
+
25
+ /** One rule that disables days. A list of them disables any day one matches. */
26
+ export type DateMatcher =
27
+ /** Exactly this day. */
28
+ | { type: "date"; date: CalendarDate }
29
+ /** Every day from `from` to `to`, both inclusive. A `null` end is open. */
30
+ | { type: "span"; from: CalendarDate | null; to: CalendarDate | null }
31
+ /** Every day falling on one of these weekdays. */
32
+ | { type: "weekday"; days: readonly Weekday[] }
33
+ /** Every day the test accepts. */
34
+ | { type: "predicate"; test: (date: CalendarDate) => boolean };
35
+
36
+ /** One square of a month grid. */
37
+ export type CalendarCell = {
38
+ date: CalendarDate;
39
+ /** The day belongs to the month before or after the one the grid shows. */
40
+ isOutside: boolean;
41
+ /** The row, 0–5. */
42
+ weekIndex: number;
43
+ /** The column, 0–6, counted from the week's first day rather than from Sunday. */
44
+ dayIndex: number;
45
+ };
46
+
47
+ /** How long a formatted date is: `5/10/2026`, `5 Oct 2026`, `5 October 2026` or `Monday 5 October 2026`. */
48
+ export type CalendarDateStyle = "short" | "medium" | "long" | "full";
49
+
50
+ export type CalendarDateFormatOptions = {
51
+ /** A BCP 47 tag. Omitted, the device's own. An unreadable one falls back to `en`. */
52
+ locale?: string;
53
+ style: CalendarDateStyle;
54
+ };
55
+
56
+ /** What a selection holds, by mode — the value a `Calendar` keeps and reports. */
57
+ export type CalendarSelectionState =
58
+ | { mode: "single"; value: CalendarDate | null }
59
+ | { mode: "multiple"; value: readonly CalendarDate[] }
60
+ | { mode: "range"; value: CalendarDateRange };
61
+
62
+ /** How many rows every month grid has, so paging never changes its height. */
63
+ export const CALENDAR_WEEKS = 6;
64
+
65
+ /** The weekday a week starts on when nothing better is known: Monday, the ISO 8601 week. */
66
+ export const DEFAULT_WEEK_START: Weekday = 1;
67
+
68
+ /** How far the year view reaches either side of the visible year when nothing bounds it. */
69
+ export const CALENDAR_YEAR_REACH = 100;
70
+
71
+ // ── Validity and comparison ─────────────────────────────────────────────────
72
+
73
+ /** Whether a year has a 29 February — every fourth, except centuries not divisible by 400. */
74
+ export function isLeapYear(year: number): boolean {
75
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
76
+ }
77
+
78
+ /** How many days a month has. `month` is 1–12. */
79
+ export function daysInMonth(year: number, month: number): number {
80
+ if (month === 2) return isLeapYear(year) ? 29 : 28;
81
+ return month === 4 || month === 6 || month === 9 || month === 11 ? 30 : 31;
82
+ }
83
+
84
+ /** Whether three numbers name a day that exists — 29 February only in a leap year. */
85
+ export function isValidDate(date: CalendarDate): boolean {
86
+ const { year, month, day } = date;
87
+ if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return false;
88
+ if (month < 1 || month > 12) return false;
89
+ return day >= 1 && day <= daysInMonth(year, month);
90
+ }
91
+
92
+ /** Negative when `a` is earlier, positive when later, zero on the same day. */
93
+ export function compareDates(a: CalendarDate, b: CalendarDate): number {
94
+ return a.year - b.year || a.month - b.month || a.day - b.day;
95
+ }
96
+
97
+ /** {@link compareDates} for the month alone — the day is ignored. */
98
+ export function compareMonths(a: CalendarDate, b: CalendarDate): number {
99
+ return a.year - b.year || a.month - b.month;
100
+ }
101
+
102
+ /** Whether two dates are the same day. A `null` is never the same as anything. */
103
+ export function isSameDay(a: CalendarDate | null | undefined, b: CalendarDate | null | undefined): boolean {
104
+ if (!a || !b) return false;
105
+ return a.year === b.year && a.month === b.month && a.day === b.day;
106
+ }
107
+
108
+ /** Whether two dates fall in the same month of the same year. */
109
+ export function isSameMonth(a: CalendarDate, b: CalendarDate): boolean {
110
+ return a.year === b.year && a.month === b.month;
111
+ }
112
+
113
+ /** Whether `date` lies between `a` and `b`, both inclusive, whichever of them is earlier. */
114
+ export function isBetween(date: CalendarDate, a: CalendarDate, b: CalendarDate): boolean {
115
+ const [low, high] = compareDates(a, b) <= 0 ? [a, b] : [b, a];
116
+ return compareDates(date, low) >= 0 && compareDates(date, high) <= 0;
117
+ }
118
+
119
+ // ── Arithmetic ──────────────────────────────────────────────────────────────
120
+
121
+ /** Month offsets for Sakamoto's weekday method. */
122
+ const SAKAMOTO = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4] as const;
123
+
124
+ /**
125
+ * The day of the week, `0` for Sunday — Sakamoto's method, so no `Date` and no time zone.
126
+ *
127
+ * January and February count as the end of the previous year, which is what puts the leap day at
128
+ * the end of the cycle where it cannot disturb the months after it.
129
+ */
130
+ export function weekdayOf(date: CalendarDate): Weekday {
131
+ const year = date.month < 3 ? date.year - 1 : date.year;
132
+ const offset = SAKAMOTO[date.month - 1] ?? 0;
133
+ const raw = year + Math.floor(year / 4) - Math.floor(year / 100) + Math.floor(year / 400) + offset + date.day;
134
+ return (((raw % 7) + 7) % 7) as Weekday;
135
+ }
136
+
137
+ /**
138
+ * Days since 1970-01-01 — the proleptic Gregorian day number.
139
+ *
140
+ * The civil-from-days pair is what lets {@link addDays} cross any number of month, year and leap
141
+ * boundaries in constant time rather than walking them one at a time.
142
+ */
143
+ function toDayNumber({ year, month, day }: CalendarDate): number {
144
+ const y = month <= 2 ? year - 1 : year;
145
+ const era = Math.floor(y / 400);
146
+ const yearOfEra = y - era * 400;
147
+ const monthFromMarch = month > 2 ? month - 3 : month + 9;
148
+ const dayOfYear = Math.floor((153 * monthFromMarch + 2) / 5) + day - 1;
149
+ const dayOfEra = yearOfEra * 365 + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100) + dayOfYear;
150
+ return era * 146097 + dayOfEra - 719468;
151
+ }
152
+
153
+ /** The inverse of {@link toDayNumber}. */
154
+ function fromDayNumber(dayNumber: number): CalendarDate {
155
+ const shifted = dayNumber + 719468;
156
+ const era = Math.floor(shifted / 146097);
157
+ const dayOfEra = shifted - era * 146097;
158
+ const yearOfEra = Math.floor(
159
+ (dayOfEra - Math.floor(dayOfEra / 1460) + Math.floor(dayOfEra / 36524) - Math.floor(dayOfEra / 146096)) / 365
160
+ );
161
+ const dayOfYear = dayOfEra - (365 * yearOfEra + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100));
162
+ const monthFromMarch = Math.floor((5 * dayOfYear + 2) / 153);
163
+ const day = dayOfYear - Math.floor((153 * monthFromMarch + 2) / 5) + 1;
164
+ const month = monthFromMarch < 10 ? monthFromMarch + 3 : monthFromMarch - 9;
165
+ const year = yearOfEra + era * 400 + (month <= 2 ? 1 : 0);
166
+ return { year, month, day };
167
+ }
168
+
169
+ /** The day `n` days after `date`. A negative `n` goes back. */
170
+ export function addDays(date: CalendarDate, n: number): CalendarDate {
171
+ return fromDayNumber(toDayNumber(date) + n);
172
+ }
173
+
174
+ /**
175
+ * The same day `n` months later, clamped to the month it lands in.
176
+ *
177
+ * 31 January plus one month is the last day of February, not 3 March: a person paging forward a
178
+ * month expects to land in the next month, and an overflow would skip one.
179
+ */
180
+ export function addMonths(date: CalendarDate, n: number): CalendarDate {
181
+ const index = date.year * 12 + (date.month - 1) + n;
182
+ const year = Math.floor(index / 12);
183
+ const month = index - year * 12 + 1;
184
+ return { year, month, day: Math.min(date.day, daysInMonth(year, month)) };
185
+ }
186
+
187
+ /** The same day `n` years later. 29 February lands on the 28th in a common year. */
188
+ export function addYears(date: CalendarDate, n: number): CalendarDate {
189
+ return addMonths(date, n * 12);
190
+ }
191
+
192
+ /** The first of the month `date` falls in. */
193
+ export function startOfMonth(date: CalendarDate): CalendarDate {
194
+ return { year: date.year, month: date.month, day: 1 };
195
+ }
196
+
197
+ /** `date` held inside `min` and `max`, either of which may be absent. */
198
+ export function clampDate(
199
+ date: CalendarDate,
200
+ min: CalendarDate | null | undefined,
201
+ max: CalendarDate | null | undefined
202
+ ): CalendarDate {
203
+ if (min && compareDates(date, min) < 0) return min;
204
+ if (max && compareDates(date, max) > 0) return max;
205
+ return date;
206
+ }
207
+
208
+ // ── The grid ────────────────────────────────────────────────────────────────
209
+
210
+ /**
211
+ * The six weeks a month is drawn as, starting on `weekStartsOn`.
212
+ *
213
+ * **Always six rows.** The first row starts on the week containing the 1st, so a month beginning
214
+ * on the first column has no leading week — its last two rows are both next month's. A fixed
215
+ * count is what keeps the calendar's height, and every column's position, still while paging:
216
+ * a February that fits in four rows would otherwise shrink the surface under the finger.
217
+ */
218
+ export function monthGrid(month: CalendarDate, weekStartsOn: Weekday): CalendarCell[][] {
219
+ const first = startOfMonth(month);
220
+ const lead = (weekdayOf(first) - weekStartsOn + 7) % 7;
221
+ const origin = toDayNumber(first) - lead;
222
+ const weeks: CalendarCell[][] = [];
223
+
224
+ for (let weekIndex = 0; weekIndex < CALENDAR_WEEKS; weekIndex += 1) {
225
+ const week: CalendarCell[] = [];
226
+ for (let dayIndex = 0; dayIndex < 7; dayIndex += 1) {
227
+ const date = fromDayNumber(origin + weekIndex * 7 + dayIndex);
228
+ week.push({ date, isOutside: !isSameMonth(date, first), weekIndex, dayIndex });
229
+ }
230
+ weeks.push(week);
231
+ }
232
+
233
+ return weeks;
234
+ }
235
+
236
+ /**
237
+ * The years the year view lists: the bounding months' years, or a century either side.
238
+ *
239
+ * A century rather than a decade because the commonest unbounded use is a birth date, and a
240
+ * person born sixty years ago should not have to page to find their year.
241
+ */
242
+ export function resolveYearSpan({
243
+ visible,
244
+ startMonth,
245
+ endMonth,
246
+ }: {
247
+ visible: CalendarDate;
248
+ startMonth?: CalendarDate | null;
249
+ endMonth?: CalendarDate | null;
250
+ }): { from: number; to: number } {
251
+ return {
252
+ from: startMonth?.year ?? visible.year - CALENDAR_YEAR_REACH,
253
+ to: endMonth?.year ?? visible.year + CALENDAR_YEAR_REACH,
254
+ };
255
+ }
256
+
257
+ // ── Native Date, at the edges ───────────────────────────────────────────────
258
+
259
+ /** A `Date` at local midnight on `date` — local fields, so the day is the one the phone shows. */
260
+ export function toDate(date: CalendarDate): Date {
261
+ const native = new Date(2000, 0, 1);
262
+ native.setFullYear(date.year, date.month - 1, date.day);
263
+ return native;
264
+ }
265
+
266
+ /** The local day a `Date` falls on. The time is dropped, never rounded. */
267
+ export function fromDate(date: Date): CalendarDate {
268
+ return { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() };
269
+ }
270
+
271
+ /** Today, on the phone's own calendar. */
272
+ export function today(): CalendarDate {
273
+ return fromDate(new Date());
274
+ }
275
+
276
+ // ── Disabled days ───────────────────────────────────────────────────────────
277
+
278
+ /** Whether any of `matchers` disables `date`. */
279
+ export function matchesDisabled(date: CalendarDate, matchers: readonly DateMatcher[] | undefined): boolean {
280
+ if (!matchers) return false;
281
+ for (const matcher of matchers) {
282
+ switch (matcher.type) {
283
+ case "date":
284
+ if (isSameDay(date, matcher.date)) return true;
285
+ break;
286
+ case "span":
287
+ if (
288
+ (!matcher.from || compareDates(date, matcher.from) >= 0) &&
289
+ (!matcher.to || compareDates(date, matcher.to) <= 0)
290
+ )
291
+ return true;
292
+ break;
293
+ case "weekday":
294
+ if (matcher.days.includes(weekdayOf(date))) return true;
295
+ break;
296
+ case "predicate":
297
+ if (matcher.test(date)) return true;
298
+ break;
299
+ }
300
+ }
301
+ return false;
302
+ }
303
+
304
+ export type CalendarDateBounds = {
305
+ minDate?: CalendarDate | null;
306
+ maxDate?: CalendarDate | null;
307
+ disabled?: readonly DateMatcher[];
308
+ };
309
+
310
+ /** Whether a day cannot be picked: before `minDate`, after `maxDate`, or matched. Both bounds inclusive. */
311
+ export function isDateDisabled(date: CalendarDate, { minDate, maxDate, disabled }: CalendarDateBounds): boolean {
312
+ if (minDate && compareDates(date, minDate) < 0) return true;
313
+ if (maxDate && compareDates(date, maxDate) > 0) return true;
314
+ return matchesDisabled(date, disabled);
315
+ }
316
+
317
+ // ── Selection ───────────────────────────────────────────────────────────────
318
+
319
+ /** A range from two days in either order. */
320
+ export function normaliseRange(a: CalendarDate, b: CalendarDate): CalendarDateRange {
321
+ return compareDates(a, b) <= 0 ? { start: a, end: b } : { start: b, end: a };
322
+ }
323
+
324
+ /**
325
+ * The range after a tap on `date`.
326
+ *
327
+ * Empty or complete, a tap starts a new range there. Started, a second tap completes it in
328
+ * either direction — the earlier of the two days is always the start — and the same day twice is
329
+ * a one-day range. Booking a stay backwards, check-out first, is as natural as forwards.
330
+ */
331
+ export function applyRangeTap(range: CalendarDateRange, date: CalendarDate): CalendarDateRange {
332
+ if (!range.start || range.end) return { start: date, end: null };
333
+ return normaliseRange(range.start, date);
334
+ }
335
+
336
+ /** Whether any day from `a` to `b`, both inclusive, is disabled. */
337
+ export function rangeContainsDisabled(
338
+ a: CalendarDate,
339
+ b: CalendarDate,
340
+ isDisabled: (date: CalendarDate) => boolean
341
+ ): boolean {
342
+ const { start, end } = normaliseRange(a, b);
343
+ if (!start || !end) return false;
344
+ const last = toDayNumber(end);
345
+ for (let day = toDayNumber(start); day <= last; day += 1) {
346
+ if (isDisabled(fromDayNumber(day))) return true;
347
+ }
348
+ return false;
349
+ }
350
+
351
+ /** The single selection after a tap: the tapped day, or nothing when it was already selected. */
352
+ export function applySingleTap(selected: CalendarDate | null, date: CalendarDate): CalendarDate | null {
353
+ return isSameDay(selected, date) ? null : date;
354
+ }
355
+
356
+ /** The list with `date` added, or removed when already present — sorted, without duplicates. */
357
+ export function toggleMultiple(list: readonly CalendarDate[], date: CalendarDate): CalendarDate[] {
358
+ const present = list.some((item) => isSameDay(item, date));
359
+ const next = present ? list.filter((item) => !isSameDay(item, date)) : [...list, date];
360
+ const sorted = [...next].sort(compareDates);
361
+ return sorted.filter((item, index) => index === 0 || !isSameDay(item, sorted[index - 1]));
362
+ }
363
+
364
+ /**
365
+ * The selection after a tap on `date`, in whichever mode it is.
366
+ *
367
+ * A range that would complete across a disabled day restarts at the tap instead, so a booking can
368
+ * never span a day that cannot be booked.
369
+ */
370
+ export function nextSelection(
371
+ state: CalendarSelectionState,
372
+ date: CalendarDate,
373
+ isDisabled: (date: CalendarDate) => boolean
374
+ ): CalendarSelectionState {
375
+ switch (state.mode) {
376
+ case "single":
377
+ return { mode: "single", value: applySingleTap(state.value, date) };
378
+ case "multiple":
379
+ return { mode: "multiple", value: toggleMultiple(state.value, date) };
380
+ case "range": {
381
+ const next = applyRangeTap(state.value, date);
382
+ if (next.start && next.end && rangeContainsDisabled(next.start, next.end, isDisabled)) {
383
+ return { mode: "range", value: { start: date, end: null } };
384
+ }
385
+ return { mode: "range", value: next };
386
+ }
387
+ }
388
+ }
389
+
390
+ // ── Locale ──────────────────────────────────────────────────────────────────
391
+
392
+ /** Regions whose week starts on Sunday, from CLDR's week data. */
393
+ const SUNDAY_REGIONS = new Set(
394
+ "AG AS BD BR BS BT BW BZ CA CO DM DO ET GT GU HK HN ID IL IN JM JP KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT PY SA SG SV TH TT TW UM US VE VI WS YE ZA ZW".split(
395
+ " "
396
+ )
397
+ );
398
+
399
+ /** Regions whose week starts on Saturday, from CLDR's week data. */
400
+ const SATURDAY_REGIONS = new Set("AE AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY".split(" "));
401
+
402
+ /** Regions whose week starts on Friday, from CLDR's week data. */
403
+ const FRIDAY_REGIONS = new Set(["MV"]);
404
+
405
+ /** The region a bare language most likely means, for a tag with no region and no `Intl.Locale`. */
406
+ const LIKELY_REGION: Record<string, string> = {
407
+ ar: "EG",
408
+ en: "US",
409
+ fa: "IR",
410
+ he: "IL",
411
+ hi: "IN",
412
+ ja: "JP",
413
+ ko: "KR",
414
+ pt: "BR",
415
+ };
416
+
417
+ /** The first day of the week in a region, from the table — Monday when the region is unknown. */
418
+ export function weekStartFromRegion(region: string | undefined): Weekday {
419
+ if (!region) return DEFAULT_WEEK_START;
420
+ const upper = region.toUpperCase();
421
+ if (SUNDAY_REGIONS.has(upper)) return 0;
422
+ if (SATURDAY_REGIONS.has(upper)) return 6;
423
+ if (FRIDAY_REGIONS.has(upper)) return 5;
424
+ return DEFAULT_WEEK_START;
425
+ }
426
+
427
+ /** `Intl.Locale` as far as the week goes — the method is newer than the lib types. */
428
+ type LocaleWithWeekInfo = Intl.Locale & {
429
+ getWeekInfo?: () => { firstDay: number };
430
+ weekInfo?: { firstDay: number };
431
+ };
432
+
433
+ /**
434
+ * The first day of the week in a locale.
435
+ *
436
+ * `Intl.Locale#getWeekInfo` (or the older `weekInfo` getter) answers when the engine has it; the
437
+ * CLDR table answers when it does not, keyed on the tag's region or the region its language most
438
+ * likely means. Hermes lacks the method on some builds, which is why the table ships regardless.
439
+ * A tag nothing can read is Monday.
440
+ */
441
+ export function resolveWeekStart(locale: string | undefined): Weekday {
442
+ try {
443
+ const tag = new Intl.Locale(locale ?? new Intl.DateTimeFormat().resolvedOptions().locale) as LocaleWithWeekInfo;
444
+ const info = typeof tag.getWeekInfo === "function" ? tag.getWeekInfo() : tag.weekInfo;
445
+ if (info && Number.isInteger(info.firstDay)) return (info.firstDay % 7) as Weekday;
446
+ const region = tag.region ?? (typeof tag.maximize === "function" ? tag.maximize().region : undefined);
447
+ return weekStartFromRegion(region ?? LIKELY_REGION[tag.language]);
448
+ } catch {
449
+ const match = locale?.match(/^([a-z]{2,3})(?:[-_][A-Za-z]{4})?(?:[-_]([A-Za-z]{2}))?(?:[-_]|$)/);
450
+ if (!match) return DEFAULT_WEEK_START;
451
+ return weekStartFromRegion(match[2] ?? LIKELY_REGION[match[1] ?? ""]);
452
+ }
453
+ }
454
+
455
+ // ── Formatting ──────────────────────────────────────────────────────────────
456
+
457
+ /** The fallback locale for a tag `Intl` cannot read. */
458
+ const FALLBACK_LOCALE = "en";
459
+
460
+ const FORMAT_OPTIONS: Record<CalendarDateStyle, Intl.DateTimeFormatOptions> = {
461
+ short: { day: "numeric", month: "numeric", year: "numeric" },
462
+ medium: { day: "numeric", month: "short", year: "numeric" },
463
+ long: { day: "numeric", month: "long", year: "numeric" },
464
+ full: { weekday: "long", day: "numeric", month: "long", year: "numeric" },
465
+ };
466
+
467
+ /**
468
+ * Built formatters, keyed on locale and options.
469
+ *
470
+ * A month grid formats forty-two accessibility labels a render, and constructing an
471
+ * `Intl.DateTimeFormat` is the expensive half of formatting — so each one is built once.
472
+ */
473
+ const formatterCache = new Map<string, Intl.DateTimeFormat>();
474
+
475
+ /**
476
+ * A formatter that prints a {@link CalendarDate} as the day it is, in any time zone.
477
+ *
478
+ * The date is handed over as a UTC instant and formatted in UTC, so no offset can move it across
479
+ * midnight. A locale `Intl` rejects falls back to `en` rather than throwing.
480
+ */
481
+ function formatter(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {
482
+ const key = `${locale ?? ""}|${JSON.stringify(options)}`;
483
+ const cached = formatterCache.get(key);
484
+ if (cached) return cached;
485
+
486
+ let built: Intl.DateTimeFormat;
487
+ try {
488
+ built = new Intl.DateTimeFormat(locale, { ...options, timeZone: "UTC" });
489
+ } catch {
490
+ built = new Intl.DateTimeFormat(FALLBACK_LOCALE, { ...options, timeZone: "UTC" });
491
+ }
492
+ formatterCache.set(key, built);
493
+ return built;
494
+ }
495
+
496
+ /** A UTC instant at midnight on `date`, for a UTC formatter. Years below 100 are not shifted to 19xx. */
497
+ function toUtcInstant(date: CalendarDate): Date {
498
+ const native = new Date(0);
499
+ native.setUTCFullYear(date.year, date.month - 1, date.day);
500
+ return native;
501
+ }
502
+
503
+ /** A date as a person reads it, in the style and locale asked for. */
504
+ export function formatDate(date: CalendarDate, { locale, style }: CalendarDateFormatOptions): string {
505
+ return formatter(locale, FORMAT_OPTIONS[style]).format(toUtcInstant(date));
506
+ }
507
+
508
+ /** The month and year a grid shows: "October 2026". */
509
+ export function formatMonthYear(date: CalendarDate, locale?: string): string {
510
+ return formatter(locale, { month: "long", year: "numeric" }).format(toUtcInstant(date));
511
+ }
512
+
513
+ /** A year as the locale writes it. */
514
+ export function formatYear(year: number, locale?: string): string {
515
+ return formatter(locale, { year: "numeric" }).format(toUtcInstant({ year, month: 1, day: 1 }));
516
+ }
517
+
518
+ /** `Intl.DateTimeFormat` as far as ranges go — `formatRange` is missing from some engines. */
519
+ type RangeFormatter = Intl.DateTimeFormat & { formatRange?: (start: Date, end: Date) => string };
520
+
521
+ /**
522
+ * A range as a person reads it: empty, "5 Oct 2026 –", or the locale's own range form.
523
+ *
524
+ * `formatRange` collapses the parts two ends share ("5–9 Oct 2026") where the engine has it; an
525
+ * en dash between two full dates stands in where it does not.
526
+ */
527
+ export function formatDateRange(range: CalendarDateRange, options: CalendarDateFormatOptions): string {
528
+ if (!range.start) return "";
529
+ const start = formatDate(range.start, options);
530
+ if (!range.end) return `${start} –`;
531
+ if (isSameDay(range.start, range.end)) return start;
532
+
533
+ const built = formatter(options.locale, FORMAT_OPTIONS[options.style]) as RangeFormatter;
534
+ if (typeof built.formatRange === "function") {
535
+ try {
536
+ return built.formatRange(toUtcInstant(range.start), toUtcInstant(range.end));
537
+ } catch {
538
+ // Fall through to the plain form below.
539
+ }
540
+ }
541
+ return `${start} – ${formatDate(range.end, options)}`;
542
+ }
543
+
544
+ /** A known Sunday — 1 January 2023 — that the weekday labels are counted from. */
545
+ const REFERENCE_SUNDAY: CalendarDate = { year: 2023, month: 1, day: 1 };
546
+
547
+ /** The seven weekday names in grid order, starting on `weekStartsOn`. */
548
+ export function weekdayLabels(
549
+ locale: string | undefined,
550
+ weekStartsOn: Weekday,
551
+ width: "narrow" | "short" | "long"
552
+ ): string[] {
553
+ const built = formatter(locale, { weekday: width });
554
+ return Array.from({ length: 7 }, (_, index) =>
555
+ built.format(toUtcInstant(addDays(REFERENCE_SUNDAY, (weekStartsOn + index) % 7)))
556
+ );
557
+ }
558
+
559
+ /** The twelve month names, January first. */
560
+ export function monthNames(locale?: string, width: "narrow" | "short" | "long" = "long"): string[] {
561
+ const built = formatter(locale, { month: width });
562
+ return Array.from({ length: 12 }, (_, index) => built.format(toUtcInstant({ year: 2001, month: index + 1, day: 1 })));
563
+ }
564
+
565
+ // ── ISO 8601 ────────────────────────────────────────────────────────────────
566
+
567
+ /** `YYYY-MM-DD`, zero-padded — the form a form library or an API stores. */
568
+ export function serialiseDate(date: CalendarDate): string {
569
+ const pad = (value: number, length: number) => String(value).padStart(length, "0");
570
+ return `${pad(date.year, 4)}-${pad(date.month, 2)}-${pad(date.day, 2)}`;
571
+ }
572
+
573
+ /**
574
+ * A date from `YYYY-MM-DD`, or `null`.
575
+ *
576
+ * Strict: exactly that shape, and a day that exists. A timestamp is refused rather than truncated,
577
+ * because which day an instant falls on depends on a time zone this type deliberately has none of.
578
+ */
579
+ export function parseDate(value: string): CalendarDate | null {
580
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
581
+ if (!match) return null;
582
+ const date = { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) };
583
+ return isValidDate(date) ? date : null;
584
+ }