@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,347 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+ import {
4
+ PROGRESS_COLORS,
5
+ PROGRESS_DEFAULT_COLOR,
6
+ PROGRESS_DEFAULT_SIZE,
7
+ PROGRESS_MAX_VALUE,
8
+ PROGRESS_MIN_VALUE,
9
+ PROGRESS_SIZES,
10
+ } from "../progress/progress.variants";
11
+
12
+ /**
13
+ * What a meter's colour can say. The progress bar's set, re-exported rather than
14
+ * restated, because a meter is that bar with a judgement about the reading — the
15
+ * two have to name a colour with the same word.
16
+ */
17
+ export const METER_COLORS = PROGRESS_COLORS;
18
+
19
+ export const METER_SIZES = PROGRESS_SIZES;
20
+
21
+ export type MeterColor = (typeof METER_COLORS)[number];
22
+ export type MeterSize = (typeof METER_SIZES)[number];
23
+
24
+ export const METER_DEFAULT_COLOR: MeterColor = PROGRESS_DEFAULT_COLOR;
25
+ export const METER_DEFAULT_SIZE: MeterSize = PROGRESS_DEFAULT_SIZE;
26
+
27
+ /** The scale a meter covers when the caller names none, or names one that is not a finite number. */
28
+ export const METER_MIN_VALUE = PROGRESS_MIN_VALUE;
29
+ export const METER_MAX_VALUE = PROGRESS_MAX_VALUE;
30
+
31
+ /** The most blocks a segmented meter draws. Past this a block is thinner than its own gap. */
32
+ export const METER_MAX_SEGMENTS = 100;
33
+
34
+ /** How long a block takes to light or go dark. Instant under the system's reduce-motion setting. */
35
+ export const METER_SEGMENT_FADE_MS = 180;
36
+
37
+ /**
38
+ * Where a reading falls against `low`, `high` and `optimum`.
39
+ *
40
+ * `optimum` is the good region, `suboptimum` the one next to it, `critical` the
41
+ * far side of that — the three regions a gauge with a preferred end has.
42
+ */
43
+ export const METER_REGIONS = ["optimum", "suboptimum", "critical"] as const;
44
+
45
+ export type MeterRegion = (typeof METER_REGIONS)[number];
46
+
47
+ /** What each region paints the fill. Fixed, so a region reads the same in every meter in an app. */
48
+ export const METER_REGION_COLORS: Record<MeterRegion, MeterColor> = {
49
+ optimum: "success",
50
+ suboptimum: "warning",
51
+ critical: "destructive",
52
+ };
53
+
54
+ /** A point on the scale where the colour changes. `from` is in the value's own units, not a percentage. */
55
+ export type MeterThreshold = { from: number; color: MeterColor };
56
+
57
+ /**
58
+ * How a meter decides its colour.
59
+ *
60
+ * - `plain` — the `color` prop, whatever the reading.
61
+ * - `regions` — `low`, `high` and `optimum` split the scale into good, worse and
62
+ * worst, painted {@link METER_REGION_COLORS}.
63
+ * - `thresholds` — the highest `from` the reading has reached names the colour.
64
+ */
65
+ export type MeterScale =
66
+ | { kind: "plain" }
67
+ | { kind: "regions"; low: number | undefined; high: number | undefined; optimum: number | undefined }
68
+ | { kind: "thresholds"; thresholds: readonly MeterThreshold[] };
69
+
70
+ /**
71
+ * Styling for a segmented meter's blocks.
72
+ *
73
+ * The continuous bar is `Progress.Track` and `Progress.Fill` as they stand, so
74
+ * only the blocks need slots of their own. **An unlit block is the progress
75
+ * track's groove**, `bg-secondary`, and a lit one the progress fill's colour, so
76
+ * a segmented meter and a continuous one in one card read as one instrument;
77
+ * the tests pin both against `progressVariants`.
78
+ *
79
+ * **The lit colour is a layer over the block, not the block's own background.**
80
+ * It fades its opacity in and out, which a background colour cannot do on the UI
81
+ * thread without interpolating two token values in JavaScript.
82
+ *
83
+ * Free of React Native imports so it stays unit-testable. See AGENTS.md.
84
+ */
85
+ export const meterVariants = tv({
86
+ slots: {
87
+ /** The row of blocks. */
88
+ segments: "w-full flex-row gap-1",
89
+ /** One block: an unlit groove clipping its lit layer. */
90
+ segment: "relative flex-1 overflow-hidden rounded-full bg-secondary",
91
+ /** The lit layer. Its opacity is animated, never a class. */
92
+ segmentFill: "absolute inset-0 rounded-full",
93
+ },
94
+ variants: {
95
+ size: {
96
+ sm: { segment: "h-1" },
97
+ md: { segment: "h-2" },
98
+ lg: { segment: "h-3", segments: "gap-1.5" },
99
+ },
100
+ color: {
101
+ default: { segmentFill: "bg-foreground" },
102
+ primary: { segmentFill: "bg-primary" },
103
+ success: { segmentFill: "bg-success" },
104
+ warning: { segmentFill: "bg-warning" },
105
+ destructive: { segmentFill: "bg-destructive" },
106
+ info: { segmentFill: "bg-info" },
107
+ },
108
+ },
109
+ defaultVariants: {
110
+ color: METER_DEFAULT_COLOR,
111
+ size: METER_DEFAULT_SIZE,
112
+ },
113
+ });
114
+
115
+ /**
116
+ * A scale that is finite and ordered.
117
+ *
118
+ * A bound that is not a finite number takes the documented default, and an
119
+ * inverted or empty scale collapses at its floor — so it reads as empty rather
120
+ * than handing a negative span to the fill or `NaN` to the accessibility value.
121
+ */
122
+ export function meterRange(minValue: number, maxValue: number): { minValue: number; maxValue: number } {
123
+ const min = Number.isFinite(minValue) ? minValue : METER_MIN_VALUE;
124
+ const max = Number.isFinite(maxValue) ? maxValue : METER_MAX_VALUE;
125
+ return { maxValue: max < min ? min : max, minValue: min };
126
+ }
127
+
128
+ /**
129
+ * The reading, on the scale.
130
+ *
131
+ * Clamped to its ends. `NaN` reads at the floor; an infinity is a direction, so
132
+ * it reads at the end it points to.
133
+ */
134
+ export function meterValue(value: number, minValue: number, maxValue: number): number {
135
+ if (Number.isNaN(value)) return minValue;
136
+ return value < minValue ? minValue : value > maxValue ? maxValue : value;
137
+ }
138
+
139
+ function clamp(value: number, min: number, max: number): number {
140
+ return value < min ? min : value > max ? max : value;
141
+ }
142
+
143
+ /**
144
+ * Which region a reading falls in.
145
+ *
146
+ * `low` and `high` default to the scale's ends and `optimum` to its middle; each
147
+ * is clamped into the scale, and `high` is never below `low`, so a boundary
148
+ * written the wrong way round is pulled into order rather than inverting every
149
+ * region.
150
+ *
151
+ * - **`optimum` below `low`** — the bottom is good: a disk. Below `low` is
152
+ * `optimum`, up to `high` is `suboptimum`, above it `critical`.
153
+ * - **`optimum` above `high`** — the top is good: a battery. The same, mirrored.
154
+ * - **`optimum` between them** — the band is good and either side is
155
+ * `suboptimum`: a room's temperature.
156
+ */
157
+ export function resolveMeterRegion({
158
+ value,
159
+ minValue,
160
+ maxValue,
161
+ low,
162
+ high,
163
+ optimum,
164
+ }: {
165
+ value: number;
166
+ minValue: number;
167
+ maxValue: number;
168
+ low?: number;
169
+ high?: number;
170
+ optimum?: number;
171
+ }): MeterRegion {
172
+ const reading = meterValue(value, minValue, maxValue);
173
+ const lowBound = clamp(low ?? minValue, minValue, maxValue);
174
+ const highBound = clamp(high ?? maxValue, lowBound, maxValue);
175
+ const best = clamp(optimum ?? (minValue + maxValue) / 2, minValue, maxValue);
176
+
177
+ if (best < lowBound) {
178
+ if (reading < lowBound) return "optimum";
179
+ return reading <= highBound ? "suboptimum" : "critical";
180
+ }
181
+ if (best > highBound) {
182
+ if (reading > highBound) return "optimum";
183
+ return reading >= lowBound ? "suboptimum" : "critical";
184
+ }
185
+ return reading >= lowBound && reading <= highBound ? "optimum" : "suboptimum";
186
+ }
187
+
188
+ /**
189
+ * The colour of the highest threshold the reading has reached, or `undefined`
190
+ * below all of them.
191
+ *
192
+ * The order the thresholds are listed in does not matter, and one whose `from`
193
+ * is not a finite number is ignored.
194
+ */
195
+ export function thresholdColor(value: number, thresholds: readonly MeterThreshold[]): MeterColor | undefined {
196
+ let reached: MeterThreshold | undefined;
197
+ for (const threshold of thresholds) {
198
+ if (!Number.isFinite(threshold.from) || threshold.from > value) continue;
199
+ if (reached === undefined || threshold.from >= reached.from) reached = threshold;
200
+ }
201
+ return reached?.color;
202
+ }
203
+
204
+ /** What a meter was given to judge its reading by. */
205
+ export type MeterScaleInput = {
206
+ low?: number;
207
+ high?: number;
208
+ optimum?: number;
209
+ thresholds?: readonly MeterThreshold[];
210
+ };
211
+
212
+ /** Settles which way a meter decides its colour — see {@link MeterScale}. */
213
+ export function resolveMeterScale({ low, high, optimum, thresholds }: MeterScaleInput): MeterScale {
214
+ if (thresholds !== undefined) return { kind: "thresholds", thresholds };
215
+ if (low !== undefined || high !== undefined || optimum !== undefined) {
216
+ return { high, kind: "regions", low, optimum };
217
+ }
218
+ return { kind: "plain" };
219
+ }
220
+
221
+ /**
222
+ * The colour the fill is painted, and the region it was judged to be in.
223
+ *
224
+ * `region` is `null` unless the scale is `regions`; it is what a readout or a
225
+ * `valueLabel` reads to put the judgement into words, because colour alone is
226
+ * not an answer for anyone who cannot see it.
227
+ */
228
+ export function resolveMeterColor({
229
+ value,
230
+ minValue,
231
+ maxValue,
232
+ color,
233
+ scale,
234
+ }: {
235
+ value: number;
236
+ minValue: number;
237
+ maxValue: number;
238
+ color: MeterColor;
239
+ scale: MeterScale;
240
+ }): { color: MeterColor; region: MeterRegion | null } {
241
+ const reading = meterValue(value, minValue, maxValue);
242
+ switch (scale.kind) {
243
+ case "plain":
244
+ return { color, region: null };
245
+ case "thresholds":
246
+ return { color: thresholdColor(reading, scale.thresholds) ?? color, region: null };
247
+ case "regions": {
248
+ const region = resolveMeterRegion({ ...scale, maxValue, minValue, value: reading });
249
+ return { color: METER_REGION_COLORS[region], region };
250
+ }
251
+ }
252
+ }
253
+
254
+ /**
255
+ * How many blocks to draw, or `null` for the continuous bar.
256
+ *
257
+ * A fractional count rounds down and a count above {@link METER_MAX_SEGMENTS}
258
+ * clamps to it. A count under one, or one that is not a finite number, falls
259
+ * back to the bar rather than drawing nothing.
260
+ */
261
+ export function meterSegmentCount(segments: number | undefined): number | null {
262
+ if (segments === undefined || !Number.isFinite(segments)) return null;
263
+ const count = Math.floor(segments);
264
+ if (count < 1) return null;
265
+ return count > METER_MAX_SEGMENTS ? METER_MAX_SEGMENTS : count;
266
+ }
267
+
268
+ /** Absorbs floating-point error, so `0.7 × 10` lights seven blocks rather than six. */
269
+ const SEGMENT_EPSILON = 1e-9;
270
+
271
+ /**
272
+ * How many blocks a reading lights.
273
+ *
274
+ * Whole blocks only — a block is all or nothing, which is the point of drawing
275
+ * blocks at all. **Any reading above the floor lights at least one**: rounding
276
+ * down would leave the first quarter of a four-block meter dark, and "a little"
277
+ * looking like "none" is the one reading a meter cannot afford to get wrong.
278
+ */
279
+ export function litSegments({ ratio, count }: { ratio: number; count: number }): number {
280
+ const clamped = clamp(ratio, 0, 1);
281
+ if (clamped <= 0) return 0;
282
+ const lit = Math.floor(clamped * count + SEGMENT_EPSILON);
283
+ return lit < 1 ? 1 : lit > count ? count : lit;
284
+ }
285
+
286
+ /**
287
+ * A settled reading, before any `valueLabel` — what a `valueLabel` function is
288
+ * handed to word the reading, and its judgement, itself.
289
+ */
290
+ export type MeterReading = {
291
+ /** The reading, clamped to the scale. */
292
+ value: number;
293
+ minValue: number;
294
+ maxValue: number;
295
+ /** Where the reading sits on the scale, 0–1. */
296
+ ratio: number;
297
+ /** The reading, formatted by `formatOptions`. */
298
+ formatted: string;
299
+ /** The colour the reading was judged to be. */
300
+ color: MeterColor;
301
+ /** The region the reading falls in, when judged by `low`, `high` and `optimum`. */
302
+ region: MeterRegion | null;
303
+ };
304
+
305
+ /** Words for the reading: fixed, or computed from it. */
306
+ export type MeterValueLabel = string | ((reading: MeterReading) => string);
307
+
308
+ /**
309
+ * The text that replaces the formatted reading, on screen and to assistive
310
+ * technology — or `undefined` to keep the formatted reading.
311
+ *
312
+ * **A function is how a judgement reaches a screen reader.** `Meter.Output` is
313
+ * hidden from assistive technology because the root already speaks the value,
314
+ * so words a readout function draws — "Filling up" — are seen and never heard.
315
+ * The label is spoken, so computing it from the region says the judgement to
316
+ * both at once.
317
+ */
318
+ export function resolveMeterValueLabel(
319
+ valueLabel: MeterValueLabel | undefined,
320
+ reading: MeterReading
321
+ ): string | undefined {
322
+ return typeof valueLabel === "function" ? valueLabel(reading) : valueLabel;
323
+ }
324
+
325
+ /** What a meter was given at its own call site. */
326
+ export type MeterOwnAxes = {
327
+ color?: MeterColor;
328
+ size?: MeterSize;
329
+ };
330
+
331
+ /** Every axis settled. */
332
+ export type MeterAxes = Required<MeterOwnAxes>;
333
+
334
+ /**
335
+ * Settles a meter's axes.
336
+ *
337
+ * One rung, deliberately: a meter reports a reading and accepts no input, so an
338
+ * enclosing `Field` has no disabled or invalid state to hand it.
339
+ */
340
+ export function resolveMeterAxes(own: MeterOwnAxes): MeterAxes {
341
+ return {
342
+ color: own.color ?? METER_DEFAULT_COLOR,
343
+ size: own.size ?? METER_DEFAULT_SIZE,
344
+ };
345
+ }
346
+
347
+ export type MeterVariantProps = VariantProps<typeof meterVariants>;
@@ -0,0 +1,89 @@
1
+ # Overlay
2
+
3
+ The layer under every overlay in this library — Dialog, Drawer, Popover, Tooltip, Toast,
4
+ Feedback. Not a component anyone renders on its own: the provider an app mounts once, and the
5
+ parts and hooks an overlay is drawn with.
6
+
7
+ `import { Overlay, OverlayProvider, useOverlayBackHandler, useOverlayPresence } from "@delacour/react-native-ui/overlay";`
8
+
9
+ `react-native-teleport` is an **optional peer**, as it is for `BottomSheet`: an app that never
10
+ imports an overlay never resolves it. The consequence is the same one thing to do — **mount
11
+ `OverlayProvider` once**, inside `DelacourProvider`, around `BottomSheetProvider` and the navigator.
12
+
13
+ ## Files
14
+
15
+ | File | What it holds |
16
+ | --- | --- |
17
+ | `index.ts` | → `@delacour/react-native-ui/overlay` |
18
+ | `overlay.tsx` | `Overlay` — the root is the provider: teleport's `PortalProvider` (skipped when one is already above), the registry and the touch-start fan-out; the `Object.assign` names `Portal` and `Scrim`. `OverlayProvider` is the same component, under the name a root layout reads best with |
19
+ | `overlay-portal.tsx` | `Overlay.Portal` — registers while mounted, teleports to `"root"` in a `box-none` absolute fill carrying the registry's `zIndex`; inline without a provider |
20
+ | `overlay-scrim.tsx` | `Overlay.Scrim` — `bg-overlay`, opacity follows presence, hidden from assistive technology |
21
+ | `overlay.context.tsx` | **Leaf.** `OverlayContext`, `TeleportProvidedContext` and their hooks. `bottom-sheet/` imports it |
22
+ | `overlay-registry.ts` | **Pure.** The z-order reducer, `zIndexOfOverlay`, `topOverlay`, `isTopOverlay`, `OVERLAY_LAYER_BASE` |
23
+ | `overlay-presence.ts` | **Pure.** `reducePresence` — `closed → entering → open → exiting → closed` |
24
+ | `overlay.variants.ts` | `overlayVariants` (`scrim`), `OVERLAY_MOTION`, `OVERLAY_SCRIM_TOKEN` |
25
+ | `use-overlay-presence.ts` | `useOverlayPresence` — keeps an overlay mounted through its exit, drives `progress` 0 → 1 |
26
+ | `use-overlay-back-handler.ts` | `useOverlayBackHandler` — Android back, only while the overlay is the top |
27
+ | `*.test.ts` | The registry, the presence machine, the scrim token and the motion constants |
28
+
29
+ ## Design
30
+
31
+ - **There is exactly one teleport `PortalProvider` in an app, and this is why the folder
32
+ exists.** Teleport registers its hosts natively by name, and the bottom-sheet engine's
33
+ provider already mounts one with a `"root"` host. Six overlays each mounting their own would
34
+ mean seven hosts called `"root"`. So `OverlayProvider` and this library's `BottomSheetProvider`
35
+ each mount teleport's provider only when `TeleportProvidedContext` says none is above, and set
36
+ it when they do — either mount order works, and the engine's provider learned
37
+ `hasPortalProvider` to make the skip possible. Everything draws into one `"root"` host.
38
+ - **One host means one z-order, so the layers are bands.** Sheets are stamped from 1 by the
39
+ engine's registry; overlays take `modal` 1000+, `anchored` 2000+, `toast` 3000+. Every overlay
40
+ draws over every sheet, a popover over the dialog that opened it, a toast over both. Within a
41
+ band the later open is on top, and a re-open brings an overlay forward. The number is base plus
42
+ **rank** in the band, never the raw open counter, so a long session cannot walk a dialog into
43
+ the popover band — a test opens five thousand to prove it. The cost: a sheet opened *from* a
44
+ dialog draws under it. Do not open a sheet from a dialog.
45
+ - **The registry entry lives exactly as long as `Overlay.Portal` is mounted.** Mount the portal
46
+ only while `useOverlayPresence().isPresent`, and the registry, the z-order and the back button
47
+ all follow presence with nothing else to keep in sync.
48
+ - **Back closes only the top overlay.** `useOverlayBackHandler` takes the portal's `id` and
49
+ subscribes only while that id is `topOverlay` among `modal` and `anchored` — a toast never takes
50
+ the back button. React Native calls the most recent listener first, and an overlay over a sheet
51
+ subscribed after it, so the overlay answers first and returns `true`.
52
+ - **Presence is a reducer so an interrupted animation is boring.** A re-open mid-exit goes back
53
+ to `entering` from wherever `progress` is; a close mid-entrance goes to `exiting`. The finish
54
+ callback reaches React only when the timing ran to the end, and a stale finish (`entered` while
55
+ `exiting`) is a no-op in the reducer, so the hook keeps no generation counter.
56
+ - **Overlay motion is behaviour, not decoration.** `isMotionCalm` does not still it — an overlay
57
+ that did not appear would change what the screen says — but every animation is finite, so an E2E
58
+ runner's settle wait ends anyway. Under reduce motion the fade still runs (opacity is not
59
+ motion) and `isReduced` tells the component to drop its transforms. Exit (160 ms) is quicker
60
+ than entrance (220 ms): a dismissed overlay should get out of the way.
61
+ - **The scrim is `bg-overlay` at opacity 1**, the token carrying its own alpha, for
62
+ `BottomSheet.Overlay`'s reason. It is React Native's `Pressable`, not this library's: it wants
63
+ no feedback or haptic, and a Gesture Handler tap there would race a drawer's pan for the touch.
64
+ It always takes the touch, even with no `onDismiss`, because the app under a non-dismissible
65
+ dialog must not be pressable. It is hidden from assistive technology; a screen reader user
66
+ dismisses through the panel's `onAccessibilityEscape` or the back button.
67
+ - **The portal's wrapper is `box-none`.** An overlay with no scrim — a toast, a tooltip — leaves
68
+ the app under it interactive.
69
+ - **The provider hears every touch that starts beneath it, and claims none.** Its children sit in a
70
+ `flex: 1` `View` whose `onTouchStart` fans out to `subscribeTouchStart` listeners. `onTouchStart`
71
+ is a bubbling touch event, not a responder negotiation, so it never stops the touch reaching its
72
+ target or a Gesture Handler gesture; and React Native bubbles it through the React tree rather
73
+ than the native one, so a touch inside a teleported overlay arrives too. It exists for Tooltip,
74
+ whose outside tap must close it *and* still press what it landed on — a full-screen catcher
75
+ under the panel, Popover's way, would swallow that tap, and nothing but a common ancestor of the
76
+ whole app can hear a touch on a view the tooltip knows nothing about.
77
+ - **No provider, no teleport.** Without `OverlayProvider` a portal renders inline, in an absolute
78
+ fill of the nearest positioned ancestor, and warns once in development; it may be clipped. That
79
+ is a working fallback for a `delacour add` copy in an app that has not mounted the provider yet,
80
+ not a supported layout.
81
+ - **`DelacourProvider` cannot mount this** — the peer decision in
82
+ [DelacourProvider](../provider/AGENTS.md): teleport is optional. `apps/playground/src/app/_layout.tsx`
83
+ is the reference mount.
84
+
85
+ ## Testing
86
+
87
+ `bun test` reaches the registry, the presence machine, the scrim token and the motion constants.
88
+ The provider, the portal and the hooks are verified on a simulator through every overlay's
89
+ gallery — the foundation has no gallery of its own, the way `DelacourProvider` has none.
@@ -0,0 +1,27 @@
1
+ export { Overlay, OverlayProvider, type OverlayProviderProps } from "./overlay";
2
+ export {
3
+ OverlayContext,
4
+ type OverlayContextValue,
5
+ TeleportProvidedContext,
6
+ useIsTeleportProvided,
7
+ useOptionalOverlay,
8
+ } from "./overlay.context";
9
+ export { OVERLAY_MOTION, OVERLAY_SCRIM_TOKEN, type OverlayVariantProps, overlayVariants } from "./overlay.variants";
10
+ export type { OverlayPortalProps } from "./overlay-portal";
11
+ export { isPresent, type PresenceEvent, type PresencePhase, presenceTarget, reducePresence } from "./overlay-presence";
12
+ export {
13
+ INITIAL_OVERLAY_REGISTRY,
14
+ isTopOverlay,
15
+ OVERLAY_LAYER_BASE,
16
+ OVERLAY_LAYERS,
17
+ type OverlayLayer,
18
+ type OverlayRegistryAction,
19
+ type OverlayRegistryEntry,
20
+ type OverlayRegistryState,
21
+ reduceOverlayRegistry,
22
+ topOverlay,
23
+ zIndexOfOverlay,
24
+ } from "./overlay-registry";
25
+ export type { OverlayScrimProps } from "./overlay-scrim";
26
+ export { type UseOverlayBackHandlerOptions, useOverlayBackHandler } from "./use-overlay-back-handler";
27
+ export { type OverlayPresence, type UseOverlayPresenceOptions, useOverlayPresence } from "./use-overlay-presence";
@@ -0,0 +1,84 @@
1
+ import { type ReactElement, type ReactNode, useEffect, useId } from "react";
2
+ import { StyleSheet, View } from "react-native";
3
+ import { Portal } from "react-native-teleport";
4
+ import { useOptionalOverlay } from "./overlay.context";
5
+ import { type OverlayLayer, zIndexOfOverlay } from "./overlay-registry";
6
+
7
+ export type OverlayPortalProps = {
8
+ /** Which band of the z-order this overlay draws in. */
9
+ layer: OverlayLayer;
10
+ children?: ReactNode;
11
+ /**
12
+ * The overlay's registry id. Pass the same id to `useOverlayBackHandler` so
13
+ * the back button knows whether this overlay is the top. Defaults to a
14
+ * `useId()` of the portal's own.
15
+ */
16
+ id?: string;
17
+ /** Render where it is written instead of teleporting. Default false. */
18
+ isInline?: boolean;
19
+ /** The teleport host. Default `"root"` — the one `OverlayProvider`'s teleport provider fills the app with. */
20
+ hostName?: string;
21
+ };
22
+
23
+ let hasWarnedMissingProvider = false;
24
+
25
+ /**
26
+ * Draws its children over the whole app, in its layer of the z-order.
27
+ *
28
+ * Teleport moves the native view and leaves the React tree where it was, so
29
+ * every context provided around the overlay's trigger — a theme, a form, a
30
+ * query client — still reaches what the portal draws.
31
+ *
32
+ * The overlay is in the registry for exactly as long as this is mounted, so
33
+ * mount it only while the overlay is present (`useOverlayPresence`). Its
34
+ * wrapper is an absolute fill and `box-none`: it takes no touch of its own, so
35
+ * an overlay with no scrim — a toast, a tooltip — leaves the app under it
36
+ * interactive.
37
+ *
38
+ * Without an `OverlayProvider` above, or with `isInline`, the children render
39
+ * where they are written, in an absolute fill of the nearest positioned
40
+ * ancestor — and the missing provider warns once in development.
41
+ */
42
+ export function OverlayPortal({
43
+ layer,
44
+ children,
45
+ id,
46
+ isInline = false,
47
+ hostName = "root",
48
+ }: OverlayPortalProps): ReactElement {
49
+ const ownId = useId();
50
+ const overlayId = id ?? ownId;
51
+ const registry = useOptionalOverlay();
52
+
53
+ const present = registry?.present;
54
+ const dismissed = registry?.dismissed;
55
+ useEffect(() => {
56
+ if (present === undefined || dismissed === undefined) return;
57
+ present(overlayId, layer);
58
+ return () => dismissed(overlayId);
59
+ }, [present, dismissed, overlayId, layer]);
60
+
61
+ useEffect(() => {
62
+ if (!__DEV__ || registry !== null || isInline || hasWarnedMissingProvider) return;
63
+ hasWarnedMissingProvider = true;
64
+ console.warn(
65
+ "Overlay.Portal: an overlay rendered with no <OverlayProvider> above it, so it draws inline and may be clipped. Mount OverlayProvider once at the app root."
66
+ );
67
+ }, [registry, isInline]);
68
+
69
+ const zIndex = registry === null ? 0 : zIndexOfOverlay(registry.state, overlayId);
70
+ const frame = (
71
+ <View pointerEvents="box-none" style={[StyleSheet.absoluteFill, { zIndex }]}>
72
+ {children}
73
+ </View>
74
+ );
75
+
76
+ if (isInline || registry === null) return frame;
77
+
78
+ return (
79
+ <Portal hostName={hostName} style={[StyleSheet.absoluteFill, { zIndex }]}>
80
+ {frame}
81
+ </Portal>
82
+ );
83
+ }
84
+ OverlayPortal.displayName = "DelacourUI.Overlay.Portal";
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { isPresent, type PresencePhase, presenceTarget, reducePresence } from "./overlay-presence";
3
+
4
+ describe("reducePresence", () => {
5
+ const cases: [PresencePhase, Parameters<typeof reducePresence>[1]["type"], PresencePhase][] = [
6
+ ["closed", "open", "entering"],
7
+ ["closed", "close", "closed"],
8
+ ["closed", "entered", "closed"],
9
+ ["closed", "exited", "closed"],
10
+ ["entering", "entered", "open"],
11
+ ["entering", "close", "exiting"],
12
+ ["entering", "open", "entering"],
13
+ ["entering", "exited", "entering"],
14
+ ["open", "close", "exiting"],
15
+ ["open", "open", "open"],
16
+ ["open", "entered", "open"],
17
+ ["exiting", "exited", "closed"],
18
+ ["exiting", "open", "entering"],
19
+ ["exiting", "close", "exiting"],
20
+ ["exiting", "entered", "exiting"],
21
+ ];
22
+
23
+ for (const [from, event, to] of cases) {
24
+ test(`${from} + ${event} → ${to}`, () => {
25
+ expect(reducePresence(from, { type: event })).toBe(to);
26
+ });
27
+ }
28
+ });
29
+
30
+ describe("isPresent", () => {
31
+ test("is false only when closed", () => {
32
+ expect(isPresent("closed")).toBe(false);
33
+ expect(isPresent("entering")).toBe(true);
34
+ expect(isPresent("open")).toBe(true);
35
+ expect(isPresent("exiting")).toBe(true);
36
+ });
37
+ });
38
+
39
+ describe("presenceTarget", () => {
40
+ test("animates toward 1 while entering or open, 0 otherwise", () => {
41
+ expect(presenceTarget("entering")).toBe(1);
42
+ expect(presenceTarget("open")).toBe(1);
43
+ expect(presenceTarget("exiting")).toBe(0);
44
+ expect(presenceTarget("closed")).toBe(0);
45
+ });
46
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Where an overlay is in its life: absent, arriving, shown, or leaving.
3
+ *
4
+ * `exiting` is the whole reason this exists — an overlay that unmounted the
5
+ * moment it closed would have no exit animation to play.
6
+ */
7
+ export type PresencePhase = "closed" | "entering" | "open" | "exiting";
8
+
9
+ export type PresenceEvent =
10
+ /** The owner asked for it to show. */
11
+ | { type: "open" }
12
+ /** The owner asked for it to hide. */
13
+ | { type: "close" }
14
+ /** The entrance animation finished. */
15
+ | { type: "entered" }
16
+ /** The exit animation finished. */
17
+ | { type: "exited" };
18
+
19
+ /**
20
+ * The presence machine as a pure reducer.
21
+ *
22
+ * A re-open during the exit reverses into `entering` from wherever the
23
+ * animation is, and a close during the entrance reverses into `exiting`. A
24
+ * finish event from an animation that was interrupted — `entered` while
25
+ * already `exiting` — is stale and changes nothing, so the hook needs no
26
+ * generation counter to drop it.
27
+ */
28
+ export function reducePresence(phase: PresencePhase, event: PresenceEvent): PresencePhase {
29
+ switch (event.type) {
30
+ case "open":
31
+ return phase === "closed" || phase === "exiting" ? "entering" : phase;
32
+ case "close":
33
+ return phase === "entering" || phase === "open" ? "exiting" : phase;
34
+ case "entered":
35
+ return phase === "entering" ? "open" : phase;
36
+ case "exited":
37
+ return phase === "exiting" ? "closed" : phase;
38
+ }
39
+ }
40
+
41
+ /** Whether the overlay is mounted: every phase but `closed`. */
42
+ export function isPresent(phase: PresencePhase): boolean {
43
+ return phase !== "closed";
44
+ }
45
+
46
+ /** The progress value the phase animates toward — 1 shown, 0 hidden. */
47
+ export function presenceTarget(phase: PresencePhase): 0 | 1 {
48
+ return phase === "entering" || phase === "open" ? 1 : 0;
49
+ }