@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,96 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ INITIAL_OVERLAY_REGISTRY,
4
+ isTopOverlay,
5
+ OVERLAY_LAYER_BASE,
6
+ type OverlayRegistryState,
7
+ reduceOverlayRegistry,
8
+ topOverlay,
9
+ zIndexOfOverlay,
10
+ } from "./overlay-registry";
11
+
12
+ function apply(...actions: Parameters<typeof reduceOverlayRegistry>[1][]): OverlayRegistryState {
13
+ return actions.reduce(reduceOverlayRegistry, INITIAL_OVERLAY_REGISTRY);
14
+ }
15
+
16
+ describe("OVERLAY_LAYER_BASE", () => {
17
+ test("orders modal under anchored under toast, all above the sheet engine's 1-up stamps", () => {
18
+ expect(OVERLAY_LAYER_BASE.modal).toBeGreaterThanOrEqual(1000);
19
+ expect(OVERLAY_LAYER_BASE.anchored).toBeGreaterThan(OVERLAY_LAYER_BASE.modal);
20
+ expect(OVERLAY_LAYER_BASE.toast).toBeGreaterThan(OVERLAY_LAYER_BASE.anchored);
21
+ });
22
+ });
23
+
24
+ describe("reduceOverlayRegistry", () => {
25
+ test("an open overlay draws above its layer's base", () => {
26
+ const state = apply({ type: "open", id: "a", layer: "modal" });
27
+ expect(zIndexOfOverlay(state, "a")).toBe(OVERLAY_LAYER_BASE.modal + 1);
28
+ });
29
+
30
+ test("a later open in the same layer draws above the earlier one", () => {
31
+ const state = apply({ type: "open", id: "a", layer: "modal" }, { type: "open", id: "b", layer: "modal" });
32
+ expect(zIndexOfOverlay(state, "b")).toBeGreaterThan(zIndexOfOverlay(state, "a"));
33
+ });
34
+
35
+ test("the layer wins over open order", () => {
36
+ const state = apply({ type: "open", id: "toast", layer: "toast" }, { type: "open", id: "dialog", layer: "modal" });
37
+ expect(zIndexOfOverlay(state, "toast")).toBeGreaterThan(zIndexOfOverlay(state, "dialog"));
38
+ });
39
+
40
+ test("re-opening brings an overlay forward instead of duplicating it", () => {
41
+ const state = apply(
42
+ { type: "open", id: "a", layer: "modal" },
43
+ { type: "open", id: "b", layer: "modal" },
44
+ { type: "open", id: "a", layer: "modal" }
45
+ );
46
+ expect(state.open.filter((entry) => entry.id === "a")).toHaveLength(1);
47
+ expect(zIndexOfOverlay(state, "a")).toBeGreaterThan(zIndexOfOverlay(state, "b"));
48
+ });
49
+
50
+ test("a z never leaves its layer's band, however many opens came before", () => {
51
+ let state = INITIAL_OVERLAY_REGISTRY;
52
+ for (let i = 0; i < 5000; i++) {
53
+ state = reduceOverlayRegistry(state, { type: "open", id: `m${i}`, layer: "modal" });
54
+ state = reduceOverlayRegistry(state, { type: "close", id: `m${i}` });
55
+ }
56
+ state = reduceOverlayRegistry(state, { type: "open", id: "last", layer: "modal" });
57
+ expect(zIndexOfOverlay(state, "last")).toBeLessThan(OVERLAY_LAYER_BASE.anchored);
58
+ });
59
+
60
+ test("closing an overlay that is not open returns the same state", () => {
61
+ const state = apply({ type: "open", id: "a", layer: "modal" });
62
+ expect(reduceOverlayRegistry(state, { type: "close", id: "nope" })).toBe(state);
63
+ });
64
+
65
+ test("a closed overlay has no z", () => {
66
+ const state = apply({ type: "open", id: "a", layer: "modal" }, { type: "close", id: "a" });
67
+ expect(zIndexOfOverlay(state, "a")).toBe(0);
68
+ });
69
+ });
70
+
71
+ describe("topOverlay / isTopOverlay", () => {
72
+ test("is null with nothing open", () => {
73
+ expect(topOverlay(INITIAL_OVERLAY_REGISTRY)).toBeNull();
74
+ });
75
+
76
+ test("an anchored overlay over a dialog is the top", () => {
77
+ const state = apply({ type: "open", id: "dialog", layer: "modal" }, { type: "open", id: "pop", layer: "anchored" });
78
+ expect(topOverlay(state)).toBe("pop");
79
+ expect(isTopOverlay(state, "dialog")).toBe(false);
80
+ });
81
+
82
+ test("a toast never captures the back button", () => {
83
+ const state = apply({ type: "open", id: "dialog", layer: "modal" }, { type: "open", id: "toast", layer: "toast" });
84
+ expect(topOverlay(state)).toBe("dialog");
85
+ expect(isTopOverlay(state, "toast")).toBe(false);
86
+ });
87
+
88
+ test("closing the top hands it to the one beneath", () => {
89
+ const state = apply(
90
+ { type: "open", id: "a", layer: "modal" },
91
+ { type: "open", id: "b", layer: "modal" },
92
+ { type: "close", id: "b" }
93
+ );
94
+ expect(isTopOverlay(state, "a")).toBe(true);
95
+ });
96
+ });
@@ -0,0 +1,99 @@
1
+ /** The band of the z-order an overlay draws in. */
2
+ export type OverlayLayer = "modal" | "anchored" | "toast";
3
+
4
+ export const OVERLAY_LAYERS = ["modal", "anchored", "toast"] as const satisfies readonly OverlayLayer[];
5
+
6
+ /**
7
+ * Where each layer's band starts.
8
+ *
9
+ * Every overlay and every bottom sheet teleports into the same `"root"` host, so
10
+ * they are siblings ordered by `zIndex`. The sheet engine stamps its sheets from
11
+ * 1 upward; starting the first band at 1000 puts every overlay above every sheet.
12
+ * A dialog sits under a popover opened from it, and a toast sits above both.
13
+ */
14
+ export const OVERLAY_LAYER_BASE = {
15
+ modal: 1000,
16
+ anchored: 2000,
17
+ toast: 3000,
18
+ } as const satisfies Record<OverlayLayer, number>;
19
+
20
+ /** The layers whose top overlay answers Android's back button. A toast never does. */
21
+ const BACK_CAPTURING_LAYERS: ReadonlySet<OverlayLayer> = new Set(["modal", "anchored"]);
22
+
23
+ /** One overlay that is presented — open, or animating closed. */
24
+ export type OverlayRegistryEntry = {
25
+ id: string;
26
+ layer: OverlayLayer;
27
+ /** Open order across every layer; later is larger. */
28
+ seq: number;
29
+ };
30
+
31
+ export type OverlayRegistryState = {
32
+ nextSeq: number;
33
+ open: readonly OverlayRegistryEntry[];
34
+ };
35
+
36
+ export const INITIAL_OVERLAY_REGISTRY: OverlayRegistryState = { nextSeq: 1, open: [] };
37
+
38
+ export type OverlayRegistryAction = { type: "open"; id: string; layer: OverlayLayer } | { type: "close"; id: string };
39
+
40
+ /**
41
+ * The registry as a pure reducer, so the z-order is testable without a renderer.
42
+ *
43
+ * An `open` stamps the next sequence number; an overlay already open is
44
+ * re-stamped rather than duplicated, which brings it forward. A `close` of an
45
+ * overlay that is not open returns the same state object, so an unmount racing
46
+ * an exit causes no render.
47
+ */
48
+ export function reduceOverlayRegistry(
49
+ state: OverlayRegistryState,
50
+ action: OverlayRegistryAction
51
+ ): OverlayRegistryState {
52
+ if (action.type === "close") {
53
+ if (!state.open.some((entry) => entry.id === action.id)) return state;
54
+ return { ...state, open: state.open.filter((entry) => entry.id !== action.id) };
55
+ }
56
+
57
+ const others = state.open.filter((entry) => entry.id !== action.id);
58
+ return {
59
+ nextSeq: state.nextSeq + 1,
60
+ open: [...others, { id: action.id, layer: action.layer, seq: state.nextSeq }],
61
+ };
62
+ }
63
+
64
+ /**
65
+ * The overlay's `zIndex` while presented, `0` otherwise.
66
+ *
67
+ * Its layer's base plus its rank among the open overlays of that layer — a rank,
68
+ * not the raw sequence number, so a long session's thousandth dialog still sits
69
+ * below the first popover.
70
+ */
71
+ export function zIndexOfOverlay(state: OverlayRegistryState, id: string): number {
72
+ const entry = state.open.find((candidate) => candidate.id === id);
73
+ if (entry === undefined) return 0;
74
+ let rank = 1;
75
+ for (const other of state.open) {
76
+ if (other.layer === entry.layer && other.seq < entry.seq) rank++;
77
+ }
78
+ return OVERLAY_LAYER_BASE[entry.layer] + rank;
79
+ }
80
+
81
+ /** The back-capturing overlay drawn on top, or `null` when none is presented. */
82
+ export function topOverlay(state: OverlayRegistryState): string | null {
83
+ let top: string | null = null;
84
+ let topZ = 0;
85
+ for (const entry of state.open) {
86
+ if (!BACK_CAPTURING_LAYERS.has(entry.layer)) continue;
87
+ const z = zIndexOfOverlay(state, entry.id);
88
+ if (z > topZ) {
89
+ top = entry.id;
90
+ topZ = z;
91
+ }
92
+ }
93
+ return top;
94
+ }
95
+
96
+ /** Whether `id` is the overlay a back press should close. */
97
+ export function isTopOverlay(state: OverlayRegistryState, id: string): boolean {
98
+ return topOverlay(state) === id;
99
+ }
@@ -0,0 +1,49 @@
1
+ import type { ReactElement } from "react";
2
+ import { Pressable, type PressableProps } from "react-native";
3
+ import Animated, { type SharedValue, useAnimatedStyle } from "react-native-reanimated";
4
+ import { overlayVariants } from "./overlay.variants";
5
+
6
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
7
+
8
+ export type OverlayScrimProps = Omit<PressableProps, "style" | "children" | "onPress"> & {
9
+ /** The 0 → 1 presence progress from `useOverlayPresence`; the scrim's opacity follows it. */
10
+ progress: SharedValue<number>;
11
+ /**
12
+ * Called on a tap. Omit it and the scrim still takes the touch — the app
13
+ * under a non-dismissible overlay must not be pressable — but does nothing.
14
+ */
15
+ onDismiss?: () => void;
16
+ className?: string;
17
+ };
18
+
19
+ /**
20
+ * The dim layer between the app and a modal overlay.
21
+ *
22
+ * `bg-overlay` at opacity 1 — the token carries its own alpha — faded in and
23
+ * out by presence. Written before the panel in the same portal, so view order
24
+ * gives a touch on the panel to the panel.
25
+ *
26
+ * Invisible to assistive technology. A scrim is not a control a screen reader
27
+ * user can find; they dismiss through the panel's `onAccessibilityEscape` and
28
+ * the back button instead.
29
+ *
30
+ * React Native's own `Pressable`, not this library's: it needs no feedback, no
31
+ * haptic and no gesture handler — a gesture-handler tap here would race the
32
+ * panel's own pans for the touch.
33
+ */
34
+ export function OverlayScrim({ progress, onDismiss, className, ...props }: OverlayScrimProps): ReactElement {
35
+ const fade = useAnimatedStyle(() => ({ opacity: progress.value }));
36
+
37
+ return (
38
+ <AnimatedPressable
39
+ accessibilityElementsHidden
40
+ accessible={false}
41
+ className={overlayVariants().scrim({ className })}
42
+ importantForAccessibility="no-hide-descendants"
43
+ onPress={onDismiss}
44
+ style={fade}
45
+ {...props}
46
+ />
47
+ );
48
+ }
49
+ OverlayScrim.displayName = "DelacourUI.Overlay.Scrim";
@@ -0,0 +1,48 @@
1
+ import { createContext, useContext } from "react";
2
+ import type { OverlayLayer, OverlayRegistryState } from "./overlay-registry";
3
+
4
+ /** What `OverlayProvider` shares with every overlay beneath it. */
5
+ export type OverlayContextValue = {
6
+ /** The open set and its z-order. A change re-renders every presented portal. */
7
+ state: OverlayRegistryState;
8
+ /** A portal reports that its overlay is presented, and in which layer. */
9
+ present: (id: string, layer: OverlayLayer) => void;
10
+ /** The overlay's portal unmounted. */
11
+ dismissed: (id: string) => void;
12
+ /**
13
+ * Hear every touch that starts anywhere under the provider — the app, a
14
+ * sheet, another overlay — without taking it. A non-modal overlay closes on
15
+ * an outside tap this way and the tap still reaches what was under it.
16
+ * Returns the unsubscribe.
17
+ */
18
+ subscribeTouchStart: (listener: () => void) => () => void;
19
+ };
20
+
21
+ export const OverlayContext = createContext<OverlayContextValue | null>(null);
22
+ OverlayContext.displayName = "DelacourUI.Overlay.Context";
23
+
24
+ /** The overlay registry, or `null` outside an `OverlayProvider`. */
25
+ export function useOptionalOverlay(): OverlayContextValue | null {
26
+ return useContext(OverlayContext);
27
+ }
28
+
29
+ /**
30
+ * Whether a teleport `PortalProvider` is already mounted above.
31
+ *
32
+ * Teleport registers its hosts natively by name, so two providers would mean
33
+ * two hosts called `"root"`. `OverlayProvider` and this library's
34
+ * `BottomSheetProvider` both set this when they mount one, and both skip
35
+ * their own when it is already set — which is what makes either mount order
36
+ * work.
37
+ *
38
+ * This file is a leaf on purpose: `bottom-sheet/` imports it across folders,
39
+ * and it imports nothing but React and types, so the import can never close a
40
+ * cycle (package rule 3).
41
+ */
42
+ export const TeleportProvidedContext = createContext(false);
43
+ TeleportProvidedContext.displayName = "DelacourUI.Overlay.TeleportProvided";
44
+
45
+ /** True beneath a provider that mounted teleport's `PortalProvider`. */
46
+ export function useIsTeleportProvided(): boolean {
47
+ return useContext(TeleportProvidedContext);
48
+ }
@@ -0,0 +1,118 @@
1
+ import { type ReactElement, type ReactNode, useCallback, useMemo, useRef, useState } from "react";
2
+ import { StyleSheet, View } from "react-native";
3
+ import { PortalProvider } from "react-native-teleport";
4
+ import {
5
+ OverlayContext,
6
+ type OverlayContextValue,
7
+ TeleportProvidedContext,
8
+ useIsTeleportProvided,
9
+ } from "./overlay.context";
10
+ import { OverlayPortal } from "./overlay-portal";
11
+ import {
12
+ INITIAL_OVERLAY_REGISTRY,
13
+ type OverlayLayer,
14
+ type OverlayRegistryState,
15
+ reduceOverlayRegistry,
16
+ } from "./overlay-registry";
17
+ import { OverlayScrim } from "./overlay-scrim";
18
+
19
+ export type OverlayProviderProps = {
20
+ children?: ReactNode;
21
+ };
22
+
23
+ function OverlayRoot({ children }: OverlayProviderProps): ReactElement {
24
+ const isTeleportProvided = useIsTeleportProvided();
25
+ const current = useRef<OverlayRegistryState>(INITIAL_OVERLAY_REGISTRY);
26
+ const [state, setState] = useState<OverlayRegistryState>(INITIAL_OVERLAY_REGISTRY);
27
+
28
+ const present = useCallback((id: string, layer: OverlayLayer) => {
29
+ current.current = reduceOverlayRegistry(current.current, { type: "open", id, layer });
30
+ setState(current.current);
31
+ }, []);
32
+
33
+ const dismissed = useCallback((id: string) => {
34
+ const next = reduceOverlayRegistry(current.current, { type: "close", id });
35
+ if (next === current.current) return;
36
+ current.current = next;
37
+ setState(next);
38
+ }, []);
39
+
40
+ const touchListeners = useRef(new Set<() => void>());
41
+ const subscribeTouchStart = useCallback((listener: () => void) => {
42
+ touchListeners.current.add(listener);
43
+ return () => {
44
+ touchListeners.current.delete(listener);
45
+ };
46
+ }, []);
47
+ const onTouchStart = useCallback(() => {
48
+ for (const listener of [...touchListeners.current]) listener();
49
+ }, []);
50
+
51
+ const value = useMemo<OverlayContextValue>(
52
+ () => ({ state, present, dismissed, subscribeTouchStart }),
53
+ [state, present, dismissed, subscribeTouchStart]
54
+ );
55
+
56
+ // `onTouchStart` is a bubbling touch event, not a responder claim: it fires
57
+ // for a touch on any descendant in the React tree — teleported overlays
58
+ // included, since teleport leaves the fiber where it was — and never stops
59
+ // the touch reaching its target or a gesture handler.
60
+ const registry = (
61
+ <OverlayContext.Provider value={value}>
62
+ <View onTouchStart={onTouchStart} style={styles.fill}>
63
+ {children}
64
+ </View>
65
+ </OverlayContext.Provider>
66
+ );
67
+ if (isTeleportProvided) return registry;
68
+
69
+ return (
70
+ <PortalProvider>
71
+ <TeleportProvidedContext.Provider value>{registry}</TeleportProvidedContext.Provider>
72
+ </PortalProvider>
73
+ );
74
+ }
75
+
76
+ const styles = StyleSheet.create({ fill: { flex: 1 } });
77
+
78
+ /**
79
+ * The layer every overlay in this library draws into. Mount it once, inside
80
+ * `DelacourProvider` and around the navigator — and around
81
+ * `BottomSheetProvider` when the app has sheets.
82
+ *
83
+ * Two things. Teleport's `PortalProvider`, whose `"root"` host fills the app
84
+ * and is where every dialog, drawer, popover, tooltip, toast and sheet draws —
85
+ * skipped when a provider above already mounted one, because two would mean
86
+ * two native hosts with one name. And the overlay registry, which gives every
87
+ * presented overlay a `zIndex` in its layer's band and tells each one whether it
88
+ * is the top, the one Android's back button closes. It also hears every touch
89
+ * that starts beneath it, without claiming one, so a tooltip can close on an
90
+ * outside tap that still lands where it was aimed.
91
+ *
92
+ * `DelacourProvider` cannot mount this, for the reason it cannot mount
93
+ * `BottomSheetProvider`: `react-native-teleport` is an optional peer, and an
94
+ * import there would make every app resolve it.
95
+ *
96
+ * `OverlayProvider` is the same component under the name a root layout reads
97
+ * best with; `Overlay.Portal` and `Overlay.Scrim` are the parts every overlay
98
+ * is drawn with.
99
+ *
100
+ * @example
101
+ * <DelacourProvider>
102
+ * <OverlayProvider>
103
+ * <BottomSheetProvider>
104
+ * <Stack />
105
+ * </BottomSheetProvider>
106
+ * </OverlayProvider>
107
+ * </DelacourProvider>
108
+ */
109
+ export const Overlay = Object.assign(OverlayRoot, {
110
+ /** Teleports its children over the app, in a layer of the z-order. */
111
+ Portal: OverlayPortal,
112
+ /** The dim layer under a modal overlay, faded by presence. */
113
+ Scrim: OverlayScrim,
114
+ displayName: "DelacourUI.Overlay",
115
+ });
116
+
117
+ /** `Overlay` under the name a root layout reads best with. */
118
+ export const OverlayProvider = Overlay;
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { declaredTokens } from "../../styles/theme-tokens.test";
3
+ import { OVERLAY_MOTION, OVERLAY_SCRIM_TOKEN, overlayVariants } from "./overlay.variants";
4
+
5
+ const LIGHT = declaredTokens("light");
6
+ const DARK = declaredTokens("dark");
7
+
8
+ describe("overlayVariants", () => {
9
+ test("the scrim fills its parent in the overlay token", () => {
10
+ const scrim = overlayVariants().scrim();
11
+ expect(scrim).toContain("absolute");
12
+ expect(scrim).toContain("inset-0");
13
+ expect(scrim).toContain(`bg-${OVERLAY_SCRIM_TOKEN}`);
14
+ });
15
+
16
+ test("a caller's className reaches the scrim and wins", () => {
17
+ expect(overlayVariants().scrim({ className: "bg-black/30" })).toContain("bg-black/30");
18
+ expect(overlayVariants().scrim({ className: "bg-black/30" })).not.toContain(`bg-${OVERLAY_SCRIM_TOKEN}`);
19
+ });
20
+
21
+ test("the scrim token exists in both themes", () => {
22
+ expect(LIGHT.has(OVERLAY_SCRIM_TOKEN)).toBe(true);
23
+ expect(DARK.has(OVERLAY_SCRIM_TOKEN)).toBe(true);
24
+ });
25
+ });
26
+
27
+ describe("OVERLAY_MOTION", () => {
28
+ test("is finite, so an E2E runner's settle wait always ends", () => {
29
+ expect(Number.isFinite(OVERLAY_MOTION.enterMs)).toBe(true);
30
+ expect(Number.isFinite(OVERLAY_MOTION.exitMs)).toBe(true);
31
+ });
32
+
33
+ test("leaves faster than it arrives", () => {
34
+ expect(OVERLAY_MOTION.exitMs).toBeLessThan(OVERLAY_MOTION.enterMs);
35
+ expect(OVERLAY_MOTION.exitMs).toBeGreaterThan(0);
36
+ });
37
+ });
@@ -0,0 +1,33 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+
4
+ /**
5
+ * The theme token every overlay's scrim paints with.
6
+ *
7
+ * `--overlay` carries its own alpha, and the two themes carry different ones,
8
+ * so a scrim draws it at opacity 1 and fades only through presence — the same
9
+ * rule `BottomSheet.Overlay` follows.
10
+ */
11
+ export const OVERLAY_SCRIM_TOKEN = "overlay";
12
+
13
+ /**
14
+ * How long an overlay takes to arrive and to leave, in milliseconds.
15
+ *
16
+ * Leaving is quicker than arriving: an overlay the user dismissed should get
17
+ * out of the way. Both are finite on purpose — overlay motion is behaviour, not
18
+ * decoration, so `isMotionCalm` does not still it, and a finite animation is
19
+ * what lets an E2E runner's settle wait end anyway.
20
+ */
21
+ export const OVERLAY_MOTION = {
22
+ enterMs: 220,
23
+ exitMs: 160,
24
+ } as const;
25
+
26
+ /** Styling for the foundation's own parts. */
27
+ export const overlayVariants = tv({
28
+ slots: {
29
+ scrim: "absolute inset-0 bg-overlay",
30
+ },
31
+ });
32
+
33
+ export type OverlayVariantProps = VariantProps<typeof overlayVariants>;
@@ -0,0 +1,34 @@
1
+ import { useEffect } from "react";
2
+ import { BackHandler } from "react-native";
3
+ import { useOptionalOverlay } from "./overlay.context";
4
+ import { isTopOverlay } from "./overlay-registry";
5
+
6
+ export type UseOverlayBackHandlerOptions = {
7
+ /** The id the overlay's `Overlay.Portal` was given. */
8
+ id: string;
9
+ /** Listen only while true — typically `isPresent && isDismissible`. */
10
+ isEnabled: boolean;
11
+ onBack: () => void;
12
+ };
13
+
14
+ /**
15
+ * Closes the overlay on Android's back button, but only while it is the top
16
+ * one: a popover over a dialog closes, the dialog under it stays.
17
+ *
18
+ * Without an `OverlayProvider` every overlay is its own top. React Native calls
19
+ * the most recently added listener first, and an overlay drawn over a bottom
20
+ * sheet subscribed after it, so the overlay answers before the sheet does.
21
+ */
22
+ export function useOverlayBackHandler({ id, isEnabled, onBack }: UseOverlayBackHandlerOptions): void {
23
+ const registry = useOptionalOverlay();
24
+ const isTop = registry === null ? true : isTopOverlay(registry.state, id);
25
+
26
+ useEffect(() => {
27
+ if (!isEnabled || !isTop) return;
28
+ const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
29
+ onBack();
30
+ return true;
31
+ });
32
+ return () => subscription.remove();
33
+ }, [isEnabled, isTop, onBack]);
34
+ }
@@ -0,0 +1,87 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { Easing, type SharedValue, useReducedMotion, useSharedValue, withTiming } from "react-native-reanimated";
3
+ import { scheduleOnRN } from "react-native-worklets";
4
+ import { OVERLAY_MOTION } from "./overlay.variants";
5
+ import { isPresent, type PresenceEvent, type PresencePhase, presenceTarget, reducePresence } from "./overlay-presence";
6
+
7
+ export type UseOverlayPresenceOptions = {
8
+ isOpen: boolean;
9
+ /** Entrance duration, ms. Default `OVERLAY_MOTION.enterMs`. */
10
+ enterMs?: number;
11
+ /** Exit duration, ms. Default `OVERLAY_MOTION.exitMs`. */
12
+ exitMs?: number;
13
+ /** The entrance finished — the moment to move accessibility focus in. */
14
+ onEntered?: () => void;
15
+ /** The exit finished and the overlay is about to unmount. */
16
+ onExited?: () => void;
17
+ };
18
+
19
+ export type OverlayPresence = {
20
+ /** Keep the overlay mounted while true: from the open until the exit animation ends. */
21
+ isPresent: boolean;
22
+ /** 0 hidden → 1 shown, animated on the UI thread. Drive opacity and transforms from it. */
23
+ progress: SharedValue<number>;
24
+ /** Reduce motion is on: drop scales and translates, keep the fade. */
25
+ isReduced: boolean;
26
+ phase: PresencePhase;
27
+ };
28
+
29
+ const ENTER_EASING = Easing.out(Easing.cubic);
30
+ const EXIT_EASING = Easing.in(Easing.cubic);
31
+
32
+ /**
33
+ * Keeps an overlay mounted through its exit animation and drives its
34
+ * `progress` from 0 to 1 and back.
35
+ *
36
+ * `isOpen` is the owner's state; this follows it through `reducePresence`,
37
+ * which is what lets a re-open during the exit reverse smoothly from wherever
38
+ * the animation is. The finish of each animation reaches React through
39
+ * `scheduleOnRN`, and only when the animation ran to the end — an interrupted
40
+ * one reports nothing, and the reducer drops a stale finish anyway.
41
+ *
42
+ * Under reduce motion the fade still runs — an opacity change is not motion —
43
+ * and `isReduced` tells the component to drop its transforms.
44
+ */
45
+ export function useOverlayPresence({
46
+ isOpen,
47
+ enterMs = OVERLAY_MOTION.enterMs,
48
+ exitMs = OVERLAY_MOTION.exitMs,
49
+ onEntered,
50
+ onExited,
51
+ }: UseOverlayPresenceOptions): OverlayPresence {
52
+ const [phase, setPhase] = useState<PresencePhase>(isOpen ? "entering" : "closed");
53
+ const progress = useSharedValue(0);
54
+ const isReduced = useReducedMotion();
55
+
56
+ const send = useCallback((event: PresenceEvent) => {
57
+ setPhase((current) => reducePresence(current, event));
58
+ }, []);
59
+
60
+ const finishEnter = useCallback(() => {
61
+ send({ type: "entered" });
62
+ onEntered?.();
63
+ }, [send, onEntered]);
64
+
65
+ const finishExit = useCallback(() => {
66
+ send({ type: "exited" });
67
+ onExited?.();
68
+ }, [send, onExited]);
69
+
70
+ useEffect(() => {
71
+ send({ type: isOpen ? "open" : "close" });
72
+ }, [isOpen, send]);
73
+
74
+ useEffect(() => {
75
+ if (phase === "entering") {
76
+ progress.value = withTiming(presenceTarget(phase), { duration: enterMs, easing: ENTER_EASING }, (finished) => {
77
+ if (finished) scheduleOnRN(finishEnter);
78
+ });
79
+ } else if (phase === "exiting") {
80
+ progress.value = withTiming(presenceTarget(phase), { duration: exitMs, easing: EXIT_EASING }, (finished) => {
81
+ if (finished) scheduleOnRN(finishExit);
82
+ });
83
+ }
84
+ }, [phase, progress, enterMs, exitMs, finishEnter, finishExit]);
85
+
86
+ return { isPresent: isPresent(phase), progress, isReduced, phase };
87
+ }