@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,228 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+ import type { IconSize } from "../icon/icon.variants";
4
+
5
+ /**
6
+ * How the surface is painted.
7
+ *
8
+ * `ListGroup`'s and `Accordion`'s set, because a collapsible is the same kind of
9
+ * surface and the three sit beside each other on a screen. A test pins the set
10
+ * against `Accordion`'s.
11
+ */
12
+ export const COLLAPSIBLE_VARIANTS = ["default", "secondary", "tertiary", "transparent"] as const;
13
+
14
+ export const COLLAPSIBLE_SIZES = ["sm", "md", "lg"] as const;
15
+
16
+ export type CollapsibleVariant = (typeof COLLAPSIBLE_VARIANTS)[number];
17
+ export type CollapsibleSize = (typeof COLLAPSIBLE_SIZES)[number];
18
+
19
+ /** The axes a collapsible falls back to when nothing names one. Pinned against `defaultVariants`. */
20
+ export const COLLAPSIBLE_DEFAULT_VARIANT: CollapsibleVariant = "default";
21
+ export const COLLAPSIBLE_DEFAULT_SIZE: CollapsibleSize = "md";
22
+
23
+ /**
24
+ * The step every glyph in a collapsible is drawn at — the indicator, or an icon a
25
+ * caller composes into the trigger. One step, so a chevron and a leading glyph on
26
+ * the same row are the same mark. The `glyph` slot writes the class out, because
27
+ * Tailwind's scanner is static, and a test pins the two together.
28
+ */
29
+ export const COLLAPSIBLE_GLYPH_STEP: Record<CollapsibleSize, IconSize> = { sm: "xs", md: "sm", lg: "md" };
30
+
31
+ /** Theme token an `Icon` composed into the trigger inherits. */
32
+ export const COLLAPSIBLE_FOREGROUND_TOKEN = "foreground";
33
+
34
+ /** Theme token the indicator inherits — a step quieter than the title, and a test pins that the two differ. */
35
+ export const COLLAPSIBLE_INDICATOR_TOKEN = "muted-foreground";
36
+
37
+ /**
38
+ * The one spring a collapsible runs.
39
+ *
40
+ * **The panel's height, its opacity and the indicator's rotation all read it**,
41
+ * off a single `progress` shared value, so they cannot drift out of step by a
42
+ * frame. **Critically damped**, because an overshoot in *height* draws the panel
43
+ * taller than its content measured and flashes the surface behind it at the end
44
+ * of every expand.
45
+ *
46
+ * Restated rather than imported from `Accordion`, so `delacour add collapsible`
47
+ * copies one folder and not two. A test pins the two equal, so the two
48
+ * disclosures cannot drift apart in feel.
49
+ */
50
+ export const COLLAPSIBLE_SPRING = { damping: 26, mass: 0.4, stiffness: 400 } as const;
51
+
52
+ /** Where the indicator points at each end of the travel, in degrees — a half turn. */
53
+ export const COLLAPSIBLE_INDICATOR_ROTATION = { collapsed: 0, expanded: 180 } as const;
54
+
55
+ /**
56
+ * What the measured height holds before the panel has ever reported its layout.
57
+ *
58
+ * Negative rather than zero: a panel that measured `0` is a real answer and still
59
+ * opens. Only a value no layout can produce can mean *unmeasured*, so the height
60
+ * style floors it. Whether the panel *has* measured is never decided by comparing
61
+ * against this on the JS thread — see {@link collapsibleTravelTarget}.
62
+ */
63
+ export const COLLAPSIBLE_UNMEASURED = -1;
64
+
65
+ /**
66
+ * Where the root's `progress` should spring to, or `null` to wait — only an open,
67
+ * and only before the panel's first measurement.
68
+ *
69
+ * `isMeasured` is React state, never a JS-thread read of the measured height: the
70
+ * panel's write is queued onto the UI runtime, and a read straight after it can
71
+ * still see {@link COLLAPSIBLE_UNMEASURED} — a Release build does, every time, and
72
+ * the panel stays shut. `accordionTravelTarget`, restated.
73
+ */
74
+ export function collapsibleTravelTarget({
75
+ isOpen,
76
+ isMeasured,
77
+ }: {
78
+ isOpen: boolean;
79
+ isMeasured: boolean;
80
+ }): 0 | 1 | null {
81
+ if (!isOpen) return 0;
82
+ return isMeasured ? 1 : null;
83
+ }
84
+
85
+ /**
86
+ * The window of the travel the panel's opacity ramps across — ahead of the
87
+ * height, so the content is legible for most of an expand rather than half
88
+ * transparent at its midpoint.
89
+ */
90
+ export const COLLAPSIBLE_CONTENT_FADE = { start: 0.1, end: 0.6 } as const;
91
+
92
+ /**
93
+ * Styling for every part of a collapsible.
94
+ *
95
+ * One slotted `tv()`, because the parts cannot import the root without closing a
96
+ * cycle (AGENTS.md rule 3) yet all of them read the same `size`.
97
+ *
98
+ * **The slots are `Accordion`'s, class for class**, and a test asserts it: a
99
+ * collapsible is one accordion item without the group, and the two on one screen
100
+ * have to read as the same control. What differs is where the state lives, not
101
+ * how it looks.
102
+ *
103
+ * **The disabled fade lands on `root`**, never on `trigger`. The trigger is a
104
+ * `Pressable`, whose `Animated.View` writes `opacity` every frame, so a class on
105
+ * that node is overwritten silently. A collapsible has no item layer between the
106
+ * two, so the root wears it.
107
+ *
108
+ * **`overflow-hidden` on `root`** keeps an expanding panel inside the rounded
109
+ * corner; **on `content`** it is the disclosure itself, since that slot's height
110
+ * is an animated style running below its content's.
111
+ *
112
+ * **`contentInner` is out of flow**, so its height is purely its content's and can
113
+ * never be fed back the clip's own animated one. `left-0 right-0` is the other
114
+ * half: an absolute child is content-width without it, and a paragraph would
115
+ * measure as a single unwrapped line.
116
+ *
117
+ * No slot worn by a `View` carries `text-*` (rule 1). Free of React Native
118
+ * imports so it stays reachable from `bun test`.
119
+ */
120
+ export const collapsibleVariants = tv({
121
+ slots: {
122
+ /** The clipped, rounded surface. Wears the disabled fade. */
123
+ root: "overflow-hidden border border-transparent",
124
+ /** The row that opens the panel. A `Pressable`, so it carries no opacity of its own. */
125
+ trigger: "w-full flex-row items-center",
126
+ /** The trigger's text column, taking whatever width the indicator leaves. */
127
+ triggerContent: "min-w-0 flex-1 justify-center gap-0.5",
128
+ /** The box the indicator turns inside. Its rotation is an animated style. */
129
+ indicator: "items-center justify-center",
130
+ /** Edge length any glyph in a collapsible inherits. */
131
+ glyph: "",
132
+ /** The trigger's primary line. Carries its own colour — a `View` cannot cascade one. */
133
+ title: "font-medium text-foreground",
134
+ /** The trigger's secondary line, a step down in scale and on the muted token. */
135
+ description: "text-muted-foreground",
136
+ /** The clip. Its height is an animated style, never a class. */
137
+ content: "w-full overflow-hidden",
138
+ /** The measured layer, out of flow so no clip height can squash what it reports. */
139
+ contentInner: "absolute top-0 right-0 left-0",
140
+ },
141
+ variants: {
142
+ variant: {
143
+ default: { root: "border-border bg-card" },
144
+ secondary: { root: "bg-secondary" },
145
+ tertiary: { root: "bg-tertiary" },
146
+ transparent: { root: "bg-transparent" },
147
+ },
148
+ size: {
149
+ sm: {
150
+ root: "rounded-md",
151
+ trigger: "min-h-12 gap-2.5 px-3 py-3",
152
+ contentInner: "px-3 pb-3",
153
+ glyph: "size-icon-xs",
154
+ title: "text-sm",
155
+ description: "text-xs",
156
+ },
157
+ md: {
158
+ root: "rounded-md",
159
+ trigger: "min-h-14 gap-3 px-4 py-3.5",
160
+ contentInner: "px-4 pb-4",
161
+ glyph: "size-icon-sm",
162
+ title: "text-base",
163
+ description: "text-sm",
164
+ },
165
+ lg: {
166
+ root: "rounded-md",
167
+ trigger: "min-h-16 gap-3.5 px-5 py-4",
168
+ contentInner: "px-5 pb-5",
169
+ glyph: "size-icon-md",
170
+ title: "text-lg",
171
+ description: "text-base",
172
+ },
173
+ },
174
+ // The empty `false` branch is load-bearing typing: a map with only `true`
175
+ // types the prop as `true` rather than `boolean`.
176
+ isDisabled: { true: { root: "opacity-50" }, false: {} },
177
+ },
178
+ defaultVariants: {
179
+ variant: COLLAPSIBLE_DEFAULT_VARIANT,
180
+ size: COLLAPSIBLE_DEFAULT_SIZE,
181
+ isDisabled: false,
182
+ },
183
+ });
184
+
185
+ export type CollapsibleVariantProps = VariantProps<typeof collapsibleVariants>;
186
+
187
+ /** The state a collapsible settles at rest. */
188
+ export type CollapsibleState = { isOpen: boolean; isDisabled: boolean };
189
+
190
+ /**
191
+ * The whole state transition a tap on the trigger makes.
192
+ *
193
+ * A disabled collapsible refuses the tap **in both directions** — it does not
194
+ * close a section that is already open. A disabled control is one that cannot be
195
+ * used, not one that undoes itself. The trigger is also a disabled `Pressable`,
196
+ * so this is the second line rather than the first; it is what keeps a tap that
197
+ * slips through the gesture layer, or a caller's own `toggle()`, honest.
198
+ */
199
+ export function toggleCollapsibleOpen({ isOpen, isDisabled }: CollapsibleState): boolean {
200
+ return isDisabled ? isOpen : !isOpen;
201
+ }
202
+
203
+ /** What the trigger and the panel tell assistive technology. */
204
+ export type CollapsibleAccessibility = {
205
+ trigger: { expanded: boolean; disabled: boolean };
206
+ content: {
207
+ accessibilityElementsHidden: boolean;
208
+ importantForAccessibility: "auto" | "no-hide-descendants";
209
+ };
210
+ };
211
+
212
+ /**
213
+ * Settles what the trigger and the panel announce, from the settled state.
214
+ *
215
+ * **A closed panel is taken out of the tree**, not merely clipped to zero. The
216
+ * panel stays mounted after its first expand, and mounted content is content a
217
+ * screen reader reads — so a closed one has to say it is not there. Disabling a
218
+ * section does not hide it: an open, disabled panel is still on screen.
219
+ */
220
+ export function resolveCollapsibleAccessibility({ isOpen, isDisabled }: CollapsibleState): CollapsibleAccessibility {
221
+ return {
222
+ trigger: { disabled: isDisabled, expanded: isOpen },
223
+ content: {
224
+ accessibilityElementsHidden: !isOpen,
225
+ importantForAccessibility: isOpen ? "auto" : "no-hide-descendants",
226
+ },
227
+ };
228
+ }
@@ -0,0 +1,32 @@
1
+ export { Collapsible, type CollapsibleProps } from "./collapsible";
2
+ export {
3
+ type CollapsibleContextValue,
4
+ CollapsibleProvider,
5
+ useCollapsible,
6
+ useCollapsibleContext,
7
+ } from "./collapsible.context";
8
+ export type { CollapsibleTextProps } from "./collapsible.types";
9
+ export {
10
+ COLLAPSIBLE_CONTENT_FADE,
11
+ COLLAPSIBLE_DEFAULT_SIZE,
12
+ COLLAPSIBLE_DEFAULT_VARIANT,
13
+ COLLAPSIBLE_FOREGROUND_TOKEN,
14
+ COLLAPSIBLE_GLYPH_STEP,
15
+ COLLAPSIBLE_INDICATOR_ROTATION,
16
+ COLLAPSIBLE_INDICATOR_TOKEN,
17
+ COLLAPSIBLE_SIZES,
18
+ COLLAPSIBLE_SPRING,
19
+ COLLAPSIBLE_UNMEASURED,
20
+ COLLAPSIBLE_VARIANTS,
21
+ type CollapsibleAccessibility,
22
+ type CollapsibleSize,
23
+ type CollapsibleState,
24
+ type CollapsibleVariant,
25
+ type CollapsibleVariantProps,
26
+ collapsibleVariants,
27
+ resolveCollapsibleAccessibility,
28
+ toggleCollapsibleOpen,
29
+ } from "./collapsible.variants";
30
+ export type { CollapsibleContentProps } from "./collapsible-content";
31
+ export type { CollapsibleIndicatorProps, CollapsibleIndicatorState } from "./collapsible-indicator";
32
+ export type { CollapsibleTriggerProps } from "./collapsible-trigger";
@@ -0,0 +1,103 @@
1
+ # ContextMenu
2
+
3
+ The actions that belong to a piece of **content** — a message bubble, a card, a
4
+ row — reached by holding the content itself. A [Menu](../menu/AGENTS.md) hangs
5
+ off a control that exists to be opened; a context menu has no such control,
6
+ because the target is the content. Compound root plus `ContextMenu.Trigger`,
7
+ `ContextMenu.Content`, `ContextMenu.Preview` and `ContextMenu.Item`, with Menu's
8
+ `Label`, `Separator`, `CheckboxItem`, `RadioGroup`, `RadioItem`, `Sub`,
9
+ `SubTrigger`, `SubContent` and `Background` re-exported outright.
10
+
11
+ `import { ContextMenu } from "@delacour/react-native-ui/context-menu";`
12
+
13
+ ## Files
14
+
15
+ | File | What it holds |
16
+ | --- | --- |
17
+ | `index.ts` | → `@delacour/react-native-ui/context-menu` |
18
+ | `context-menu.tsx` | Root — Menu's `useMenuRootValue` into `MenuRootProvider`, plus this folder's provider; the `Object.assign` |
19
+ | `context-menu-trigger.tsx` | `ContextMenu.Trigger` — the wrapper, the hold-vs-tap gesture, the measurement, the accessibility actions |
20
+ | `context-menu-content.tsx` | `ContextMenu.Content` — `Menu.Content` with the context defaults; lifts a `Preview` out by type |
21
+ | `context-menu-preview.tsx` | `ContextMenu.Preview` — the lifted copy at the trigger's window rect |
22
+ | `context-menu-item.tsx` | `ContextMenu.Item` — `Menu.Item` with a trailing icon and a taller row |
23
+ | `context-menu.context.tsx` | The invocation, the registered trigger children, `useContextMenuTarget` |
24
+ | `context-menu.variants.ts` | The slotted `tv()`, the constants and the pure resolvers — no RN imports |
25
+ | `context-menu.variants.test.ts` | Anchor, hold, accessibility, the content defaults against Menu's resolvers, tokens |
26
+
27
+ ## Design
28
+
29
+ - **The rows and the panel are Menu's, imported from its leaves.** `Menu.Content`,
30
+ `Menu.Item` and the other rows come from `../menu/<leaf>`, never from `../menu`
31
+ — the folder index would pull Menu's root and could close a cycle (rule 3).
32
+ A restyled copy would be a second press fill and a second placement that drift.
33
+ - **One recogniser decides up front.** `Gesture.Exclusive(longPress, tap)`, with
34
+ `LongPress().minDuration(delay).maxDistance(slop)`: the tap waits for the hold
35
+ to fail, so a hold that opens the menu never also fires `onPress`. That is why
36
+ `onPress` is the trigger's prop — a pressable inside it would race the hold
37
+ instead of being arbitrated against it. Without `onPress` the tap is not
38
+ installed at all, so an inner control keeps its own taps.
39
+ - **A scroll that starts on the trigger still scrolls.** The hold fails once the
40
+ finger drifts past `slop` (12pt), and the scroller takes the touch. The tap's
41
+ `maxDuration` is at least the hold's delay, so a long `delay` never leaves a
42
+ dead window where neither fires.
43
+ - **The delay has a 150ms floor.** `resolveHoldConfig` clamps it: anything
44
+ shorter is a tap, and a menu that opens on a tap swallows `onPress`.
45
+ - **The content is not cloned or altered**, and need not be pressable. The
46
+ wrapper is a plain `View` that lays out like one — it does not shrink to its
47
+ child — with `collapsable={false}` so Android keeps a node to measure.
48
+ - **`point` anchors at the finger, `target` at the wrapper.** The hold's
49
+ `absoluteX/absoluteY` become a zero-size rect, which Menu's
50
+ `resolveMenuPlacement` handles with no special case; `target` is the wrapper's
51
+ `measureInWindow`. Both go to Menu through its reuse contract: `open(rect)`
52
+ on the shared `MenuRootProvider`, and `anchor` on `Menu.Content`.
53
+ - **A preview, or a screen reader, forces the target.** With a preview the panel
54
+ must open beside the lifted copy, never across it; a screen-reader open has no
55
+ pointer coordinate. `resolveContextAnchor` is the one place this is decided.
56
+ - **A stale invocation is detected by identity.** The trigger records the rect it
57
+ handed to `open()`; Menu keeps that object as its anchor. A parent flipping
58
+ `isOpen` makes Menu measure the trigger itself, the identities differ, and
59
+ Content and Preview fall back to Menu's measured rect rather than last visit's.
60
+ - **The preview is drawn through Menu's `backdrop`.** A `Modal` covers everything,
61
+ so a copy above the scrim has to live inside it. `ContextMenu.Preview` is lifted
62
+ out of the rows by type and handed to `Menu.Content`'s `backdrop`: over the
63
+ scrim, behind the panel, fading with both, taking no touches. That prop is the
64
+ one change this component needed from Menu.
65
+ - **The preview is a second instance.** It renders the trigger's children again
66
+ (registered in context from an effect), so their local state starts fresh;
67
+ pass `ContextMenu.Preview` children of its own when that matters. It grows
68
+ 1 → 1.03 over Menu's 160ms enter and casts `shadow-lg`. The 8pt offset clears
69
+ that growth for any target under ~530pt tall, and a test pins the arithmetic.
70
+ Under reduced motion it does not grow — it still appears.
71
+ - **The scrim is on by default.** `hasScrim` is Menu's; ContextMenu defaults it
72
+ to `true` — the content behind is not the menu's control, and the dim says
73
+ where the action now is. A tap on it (or the preview) closes; Android back
74
+ closes through the `Modal`.
75
+ - **Defaults differ from Menu's**: below, start-aligned, 8pt offset, 280 wide at
76
+ least — a point anchor has no width to inherit, and Menu's 200 floor is too
77
+ narrow for a trailing icon and a label. `CONTEXT_MENU_CONTENT_DEFAULTS`.
78
+ - **Rows are taller and icons trail.** `ContextMenu.Item` is `Menu.Item` with
79
+ `iconPlacement="trailing"` and `min-h-12` merged over the row's `min-h-11`.
80
+ The other row kinds are re-exported unchanged.
81
+ - **The haptic fires at hold acceptance**, in the long-press `onStart` on the UI
82
+ thread, before the open crosses to JS. The root's `haptic` is Menu's: played
83
+ when a row is chosen. Both are off by default.
84
+
85
+ ## Accessibility
86
+
87
+ - The trigger is **one** element (`accessible`): `accessibilityRole="button"` with
88
+ `onPress`, `"none"` without, `accessibilityState={{ disabled, expanded }}`.
89
+ - `accessibilityActions`: `activate` and `longpress` labelled "Show menu".
90
+ `activate` runs `onPress`, or opens when there is none; `longpress` opens
91
+ against the target rect. `resolveContextMenuAccessibility` holds the matrix.
92
+ - Disabled: announced, and neither the gesture nor either action does anything.
93
+ - The panel is Menu's: `menu` / `menuitem` roles and modal containment. The
94
+ preview is hidden from the accessibility tree.
95
+
96
+ ## Out of scope
97
+
98
+ - **`presentation="bottom-sheet"`** — the same reason as Menu: it would force the
99
+ optional sheet engine on every consumer.
100
+ - **Hardware keyboard** — Shift+F10 and the Context Menu key.
101
+ - **Gesture Handler's absolute coordinates** are relative to the root view; in an
102
+ app whose root is not the window (a modal screen with an offset), `point` mode
103
+ can land a few points off. Use `anchor="target"` there.
@@ -0,0 +1,64 @@
1
+ import { Children, isValidElement, type ReactElement, type ReactNode, useMemo } from "react";
2
+ import { MenuContent, type MenuContentProps } from "../menu/menu-content";
3
+ import { useContextMenuTarget } from "./context-menu.context";
4
+ import { CONTEXT_MENU_CONTENT_DEFAULTS } from "./context-menu.variants";
5
+ import { ContextMenuPreview } from "./context-menu-preview";
6
+
7
+ export type ContextMenuContentProps = Omit<MenuContentProps, "hasScrim" | "anchor" | "backdrop"> & {
8
+ /** Tint the screen behind the panel. Default `true`. */
9
+ hasScrim?: boolean;
10
+ };
11
+
12
+ /**
13
+ * The panel: `Menu.Content` itself, with the defaults a held menu wants —
14
+ * below, start-aligned, 8pt from the anchor, at least 280 wide, over a scrim.
15
+ *
16
+ * The anchor is this visit's: a zero-size point at the finger, or the trigger's
17
+ * rect — forced to the rect when a `ContextMenu.Preview` is among the children.
18
+ * The preview is lifted out by type and drawn through Menu's `backdrop`, over
19
+ * the scrim and behind the panel.
20
+ */
21
+ export function ContextMenuContent({
22
+ children,
23
+ placement = CONTEXT_MENU_CONTENT_DEFAULTS.placement,
24
+ align = CONTEXT_MENU_CONTENT_DEFAULTS.align,
25
+ offset = CONTEXT_MENU_CONTENT_DEFAULTS.offset,
26
+ minWidth = CONTEXT_MENU_CONTENT_DEFAULTS.minWidth,
27
+ hasScrim = CONTEXT_MENU_CONTENT_DEFAULTS.hasScrim,
28
+ ...props
29
+ }: ContextMenuContentProps): ReactElement {
30
+ const { preview, rows } = useMemo(() => splitPreview(children), [children]);
31
+ const { anchor } = useContextMenuTarget("ContextMenu.Content", preview !== null);
32
+
33
+ return (
34
+ <MenuContent
35
+ align={align}
36
+ anchor={anchor ?? undefined}
37
+ backdrop={preview}
38
+ hasScrim={hasScrim}
39
+ minWidth={minWidth}
40
+ offset={offset}
41
+ placement={placement}
42
+ {...props}
43
+ >
44
+ {rows}
45
+ </MenuContent>
46
+ );
47
+ }
48
+ ContextMenuContent.displayName = "DelacourUI.ContextMenu.Content";
49
+
50
+ /** Lifts a `ContextMenu.Preview` out of the rows, so it is drawn behind the panel instead of in it. */
51
+ function splitPreview(children: ReactNode): { preview: ReactElement | null; rows: ReactNode[] } {
52
+ let preview: ReactElement | null = null;
53
+ const rows: ReactNode[] = [];
54
+
55
+ for (const child of Children.toArray(children)) {
56
+ if (isValidElement(child) && child.type === ContextMenuPreview) {
57
+ preview = child;
58
+ continue;
59
+ }
60
+ rows.push(child);
61
+ }
62
+
63
+ return { preview, rows };
64
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReactElement } from "react";
2
+ import type { MenuItemProps } from "../menu/menu.types";
3
+ import { MenuItem } from "../menu/menu-item";
4
+ import { contextMenuVariants } from "./context-menu.variants";
5
+
6
+ export type ContextMenuItemProps = MenuItemProps;
7
+
8
+ /**
9
+ * A verb: `Menu.Item` itself, with its icon at the trailing edge by default
10
+ * and a taller row. The same row, so the two menus cannot drift.
11
+ *
12
+ * @example
13
+ * <ContextMenu.Item icon={IconShare} onSelect={share}>Share</ContextMenu.Item>
14
+ */
15
+ export function ContextMenuItem({
16
+ className,
17
+ iconPlacement = "trailing",
18
+ ...props
19
+ }: ContextMenuItemProps): ReactElement {
20
+ return <MenuItem className={contextMenuVariants().item({ className })} iconPlacement={iconPlacement} {...props} />;
21
+ }
22
+ ContextMenuItem.displayName = "DelacourUI.ContextMenu.Item";
@@ -0,0 +1,60 @@
1
+ import { type ReactElement, type ReactNode, useEffect } from "react";
2
+ import type { ViewProps } from "react-native";
3
+ import Animated, { useAnimatedStyle, useReducedMotion, useSharedValue, withTiming } from "react-native-reanimated";
4
+ import { useContextMenuPart, useContextMenuTarget } from "./context-menu.context";
5
+ import { CONTEXT_MENU_PREVIEW_MS, CONTEXT_MENU_PREVIEW_SCALE, contextMenuVariants } from "./context-menu.variants";
6
+
7
+ export type ContextMenuPreviewProps = Omit<ViewProps, "children"> & {
8
+ /** What to lift. Default: the trigger's own children, rendered again. */
9
+ children?: ReactNode;
10
+ className?: string;
11
+ };
12
+
13
+ /**
14
+ * The held content, lifted over the scrim while the menu is open.
15
+ *
16
+ * Placed among `ContextMenu.Content`'s rows, and lifted out of them by type: it
17
+ * is drawn through Menu's `backdrop`, absolutely at the trigger's measured
18
+ * window rect, grows 1 → 1.03 and casts a shadow. It takes no touches — a tap
19
+ * on it is an outside tap.
20
+ *
21
+ * It renders the trigger's children **again**, as a second instance, so any
22
+ * local state inside them starts fresh. Pass children of your own when that
23
+ * matters. With a preview present, the panel anchors to the trigger's rect
24
+ * rather than the press point, so it opens beside the copy, never across it.
25
+ *
26
+ * Under reduced motion it does not grow; it still appears, with the scrim.
27
+ */
28
+ export function ContextMenuPreview({
29
+ children,
30
+ className,
31
+ style,
32
+ ...props
33
+ }: ContextMenuPreviewProps): ReactElement | null {
34
+ const { content } = useContextMenuPart("ContextMenu.Preview");
35
+ const { targetRect } = useContextMenuTarget("ContextMenu.Preview", true);
36
+ const isReducedMotion = useReducedMotion();
37
+
38
+ const lift = useSharedValue(1);
39
+ useEffect(() => {
40
+ if (isReducedMotion) return;
41
+ lift.value = withTiming(CONTEXT_MENU_PREVIEW_SCALE, { duration: CONTEXT_MENU_PREVIEW_MS });
42
+ }, [isReducedMotion, lift]);
43
+ const liftStyle = useAnimatedStyle(() => ({ transform: [{ scale: lift.value }] }));
44
+
45
+ if (!targetRect) return null;
46
+
47
+ const frame = { height: targetRect.height, left: targetRect.x, top: targetRect.y, width: targetRect.width };
48
+
49
+ return (
50
+ <Animated.View
51
+ className={contextMenuVariants().preview({ className })}
52
+ pointerEvents="none"
53
+ style={[frame, liftStyle, style]}
54
+ {...props}
55
+ >
56
+ {children ?? content}
57
+ </Animated.View>
58
+ );
59
+ }
60
+ ContextMenuPreview.displayName = "DelacourUI.ContextMenu.Preview";
@@ -0,0 +1,160 @@
1
+ import { type ReactElement, type ReactNode, useCallback, useEffect, useMemo, useRef } from "react";
2
+ import { type AccessibilityActionEvent, View, type ViewProps } from "react-native";
3
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
+ import { scheduleOnRN } from "react-native-worklets";
5
+ import { type MenuMeasurable, useMenuPart } from "../menu/menu.context";
6
+ import { type HapticFeedback, playHaptic } from "../pressable/pressable";
7
+ import { useContextMenuPart } from "./context-menu.context";
8
+ import {
9
+ CONTEXT_MENU_ACCESSIBILITY_ACTIONS,
10
+ type ContextMenuAnchor,
11
+ type ContextMenuInvoker,
12
+ type ContextMenuPoint,
13
+ contextMenuVariants,
14
+ resolveContextAnchor,
15
+ resolveContextMenuAccessibility,
16
+ resolveHoldConfig,
17
+ } from "./context-menu.variants";
18
+
19
+ export type ContextMenuTriggerProps = Omit<ViewProps, "children"> & {
20
+ /** The content that is held. Not cloned or altered, and it need not be pressable. */
21
+ children: ReactNode;
22
+ /** Lays out like a `View`; the wrapper does not shrink to its child. */
23
+ className?: string;
24
+ /** Open at the finger (`point`, default) or against the whole trigger (`target`). */
25
+ anchor?: ContextMenuAnchor;
26
+ /** How long the hold lasts before it opens, in ms. Default 350, at least 150. */
27
+ delay?: number;
28
+ /** How far the finger may drift during the hold, in pt. Default 12. */
29
+ slop?: number;
30
+ /** A short press. Arbitrated against the hold, so the two never both fire. */
31
+ onPress?: () => void;
32
+ /** Played the moment the hold is accepted. Off by default. */
33
+ haptic?: false | HapticFeedback;
34
+ /** Neither the hold nor `onPress` fires, and the trigger announces disabled. */
35
+ isDisabled?: boolean;
36
+ };
37
+
38
+ /**
39
+ * The content that is held: a message bubble, a card, a row.
40
+ *
41
+ * **One recogniser decides up front.** `Gesture.Exclusive(longPress, tap)`: the
42
+ * tap waits for the hold to fail, so a hold that opens the menu never also
43
+ * counts as a tap. That is why `onPress` belongs here, on the trigger, and not
44
+ * on a pressable inside it — an inner press would race the hold instead.
45
+ *
46
+ * **A scroll that starts here still scrolls.** The hold fails once the finger
47
+ * drifts past `slop`, and the scroller takes the touch.
48
+ *
49
+ * The haptic plays on the UI thread in the hold's `onStart`; the open crosses
50
+ * to JS, measures the wrapper with `measureInWindow`, and anchors the panel at
51
+ * the press point or against that rect.
52
+ *
53
+ * For a screen reader it is one element with two actions: `activate` runs
54
+ * `onPress` (or opens, without one) and `longpress`, "Show menu", opens against
55
+ * the trigger — there is no pointer coordinate to open at.
56
+ */
57
+ export function ContextMenuTrigger({
58
+ children,
59
+ className,
60
+ anchor = "point",
61
+ delay,
62
+ slop,
63
+ onPress,
64
+ haptic = false,
65
+ isDisabled = false,
66
+ ...props
67
+ }: ContextMenuTriggerProps): ReactElement {
68
+ const { isOpen, open, triggerRef } = useMenuPart("ContextMenu.Trigger");
69
+ const { invoke, registerContent } = useContextMenuPart("ContextMenu.Trigger");
70
+ const hold = resolveHoldConfig({ delay, slop });
71
+ const hasPress = onPress !== undefined;
72
+ const a11y = resolveContextMenuAccessibility({ hasPress, isDisabled, isOpen });
73
+
74
+ useEffect(() => {
75
+ registerContent(children);
76
+ }, [children, registerContent]);
77
+
78
+ const nodeRef = useRef<View | null>(null);
79
+ const setNode = useCallback(
80
+ (node: View | null) => {
81
+ nodeRef.current = node;
82
+ triggerRef.current = node as MenuMeasurable | null;
83
+ },
84
+ [triggerRef]
85
+ );
86
+
87
+ const openFrom = useCallback(
88
+ (invokedBy: ContextMenuInvoker, point: ContextMenuPoint | null) => {
89
+ const node = nodeRef.current;
90
+ if (!node) return;
91
+ node.measureInWindow((x, y, width, height) => {
92
+ const targetRect = { height, width, x, y };
93
+ const opened = resolveContextAnchor({ hasPreview: false, invokedBy, mode: anchor, point, targetRect });
94
+ invoke({ invokedBy, mode: anchor, opened, point, targetRect });
95
+ open(opened);
96
+ });
97
+ },
98
+ [anchor, invoke, open]
99
+ );
100
+
101
+ const openAtPoint = useCallback((x: number, y: number) => openFrom("pointer", { x, y }), [openFrom]);
102
+
103
+ const pressRef = useRef(onPress);
104
+ pressRef.current = onPress;
105
+ const handlePress = useCallback(() => pressRef.current?.(), []);
106
+
107
+ const gesture = useMemo(() => {
108
+ const longPress = Gesture.LongPress()
109
+ .enabled(!isDisabled)
110
+ .minDuration(hold.delay)
111
+ .maxDistance(hold.slop)
112
+ .onStart((event) => {
113
+ "worklet";
114
+ if (haptic) playHaptic(haptic);
115
+ scheduleOnRN(openAtPoint, event.absoluteX, event.absoluteY);
116
+ });
117
+
118
+ if (!hasPress) return longPress;
119
+
120
+ const tap = Gesture.Tap()
121
+ .enabled(!isDisabled)
122
+ .maxDistance(hold.slop)
123
+ .maxDuration(Math.max(500, hold.delay))
124
+ .shouldCancelWhenOutside(true)
125
+ .onEnd((_event, isSuccess) => {
126
+ "worklet";
127
+ if (isSuccess) scheduleOnRN(handlePress);
128
+ });
129
+
130
+ return Gesture.Exclusive(longPress, tap);
131
+ }, [handlePress, hasPress, haptic, hold.delay, hold.slop, isDisabled, openAtPoint]);
132
+
133
+ const handleAccessibilityAction = useCallback(
134
+ (event: AccessibilityActionEvent) => {
135
+ const outcome = a11y.onAction(event.nativeEvent.actionName);
136
+ if (outcome === "press") handlePress();
137
+ else if (outcome === "open") openFrom("accessibility", null);
138
+ },
139
+ [a11y, handlePress, openFrom]
140
+ );
141
+
142
+ return (
143
+ <GestureDetector gesture={gesture}>
144
+ <View
145
+ accessibilityActions={CONTEXT_MENU_ACCESSIBILITY_ACTIONS}
146
+ accessibilityRole={a11y.role}
147
+ accessibilityState={a11y.state}
148
+ accessible
149
+ className={contextMenuVariants().trigger({ className })}
150
+ collapsable={false}
151
+ onAccessibilityAction={handleAccessibilityAction}
152
+ ref={setNode}
153
+ {...props}
154
+ >
155
+ {children}
156
+ </View>
157
+ </GestureDetector>
158
+ );
159
+ }
160
+ ContextMenuTrigger.displayName = "DelacourUI.ContextMenu.Trigger";