@delacour/react-native-ui 0.1.0-alpha.3 → 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 (482) hide show
  1. package/README.md +11 -7
  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 +5 -0
  34. package/src/components/badge/badge.tsx +6 -1
  35. package/src/components/bottom-sheet/AGENTS.md +186 -221
  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 -207
  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 -120
  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 -52
  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 -70
  55. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -105
  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 +9 -1
  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/screen/screen-navbar-back-button.tsx +1 -1
  344. package/src/components/selection-mode/AGENTS.md +155 -0
  345. package/src/components/selection-mode/index.ts +43 -0
  346. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  347. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  348. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  349. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  350. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  351. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  352. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  353. package/src/components/selection-mode/selection-mode.tsx +207 -0
  354. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  355. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  356. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  357. package/src/components/skeleton/AGENTS.md +120 -0
  358. package/src/components/skeleton/index.ts +15 -0
  359. package/src/components/skeleton/skeleton-clock.ts +48 -0
  360. package/src/components/skeleton/skeleton-group.tsx +64 -0
  361. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  362. package/src/components/skeleton/skeleton.context.tsx +45 -0
  363. package/src/components/skeleton/skeleton.tsx +227 -0
  364. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  365. package/src/components/skeleton/skeleton.variants.ts +222 -0
  366. package/src/components/slide-button/AGENTS.md +122 -0
  367. package/src/components/slide-button/index.ts +43 -0
  368. package/src/components/slide-button/slide-button-label.tsx +70 -0
  369. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  370. package/src/components/slide-button/slide-button.context.tsx +76 -0
  371. package/src/components/slide-button/slide-button.tsx +457 -0
  372. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  373. package/src/components/slide-button/slide-button.variants.ts +324 -0
  374. package/src/components/slider/index.ts +2 -0
  375. package/src/components/slider/slider-thumb.tsx +33 -1
  376. package/src/components/slider/slider.variants.test.ts +52 -0
  377. package/src/components/slider/slider.variants.ts +56 -2
  378. package/src/components/stack-card/AGENTS.md +169 -0
  379. package/src/components/stack-card/index.ts +37 -0
  380. package/src/components/stack-card/stack-card-action.tsx +66 -0
  381. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  382. package/src/components/stack-card/stack-card-card.tsx +71 -0
  383. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  384. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  385. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  386. package/src/components/stack-card/stack-card.context.tsx +117 -0
  387. package/src/components/stack-card/stack-card.tsx +557 -0
  388. package/src/components/stack-card/stack-card.types.ts +26 -0
  389. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  390. package/src/components/stack-card/stack-card.variants.ts +360 -0
  391. package/src/components/steps/AGENTS.md +153 -0
  392. package/src/components/steps/index.ts +45 -0
  393. package/src/components/steps/steps-connector.tsx +31 -0
  394. package/src/components/steps/steps-description.tsx +26 -0
  395. package/src/components/steps/steps-indicator.tsx +106 -0
  396. package/src/components/steps/steps-item.tsx +225 -0
  397. package/src/components/steps/steps-panel.tsx +41 -0
  398. package/src/components/steps/steps-title.tsx +32 -0
  399. package/src/components/steps/steps-track.tsx +59 -0
  400. package/src/components/steps/steps.context.tsx +123 -0
  401. package/src/components/steps/steps.tsx +166 -0
  402. package/src/components/steps/steps.variants.test.ts +388 -0
  403. package/src/components/steps/steps.variants.ts +468 -0
  404. package/src/components/surface/AGENTS.md +72 -0
  405. package/src/components/surface/index.ts +15 -0
  406. package/src/components/surface/surface.context.tsx +54 -0
  407. package/src/components/surface/surface.tsx +70 -0
  408. package/src/components/surface/surface.variants.test.ts +180 -0
  409. package/src/components/surface/surface.variants.ts +133 -0
  410. package/src/components/swipe/AGENTS.md +140 -0
  411. package/src/components/swipe/index.ts +31 -0
  412. package/src/components/swipe/swipe-action.tsx +99 -0
  413. package/src/components/swipe/swipe-end.tsx +14 -0
  414. package/src/components/swipe/swipe-group.tsx +55 -0
  415. package/src/components/swipe/swipe-panel.tsx +55 -0
  416. package/src/components/swipe/swipe-start.tsx +15 -0
  417. package/src/components/swipe/swipe.context.tsx +135 -0
  418. package/src/components/swipe/swipe.tsx +393 -0
  419. package/src/components/swipe/swipe.types.ts +7 -0
  420. package/src/components/swipe/swipe.variants.test.ts +337 -0
  421. package/src/components/swipe/swipe.variants.ts +290 -0
  422. package/src/components/switch/switch.variants.ts +2 -2
  423. package/src/components/tabs/AGENTS.md +4 -3
  424. package/src/components/tabs/tabs.variants.test.ts +1 -1
  425. package/src/components/tabs/tabs.variants.ts +4 -6
  426. package/src/components/textarea/AGENTS.md +86 -0
  427. package/src/components/textarea/index.ts +16 -0
  428. package/src/components/textarea/textarea.tsx +157 -0
  429. package/src/components/textarea/textarea.variants.test.ts +219 -0
  430. package/src/components/textarea/textarea.variants.ts +171 -0
  431. package/src/components/toast/AGENTS.md +140 -0
  432. package/src/components/toast/index.ts +74 -0
  433. package/src/components/toast/toast-action.tsx +59 -0
  434. package/src/components/toast/toast-api.ts +32 -0
  435. package/src/components/toast/toast-close.tsx +44 -0
  436. package/src/components/toast/toast-content.tsx +11 -0
  437. package/src/components/toast/toast-description.tsx +11 -0
  438. package/src/components/toast/toast-indicator.tsx +34 -0
  439. package/src/components/toast/toast-item.tsx +308 -0
  440. package/src/components/toast/toast-title.tsx +13 -0
  441. package/src/components/toast/toast-viewport.tsx +135 -0
  442. package/src/components/toast/toast.context.tsx +69 -0
  443. package/src/components/toast/toast.store.test.ts +259 -0
  444. package/src/components/toast/toast.store.ts +338 -0
  445. package/src/components/toast/toast.timer.test.ts +64 -0
  446. package/src/components/toast/toast.timer.ts +51 -0
  447. package/src/components/toast/toast.tsx +132 -0
  448. package/src/components/toast/toast.variants.test.ts +393 -0
  449. package/src/components/toast/toast.variants.ts +304 -0
  450. package/src/components/toggle-button/AGENTS.md +106 -0
  451. package/src/components/toggle-button/index.ts +30 -0
  452. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  453. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  454. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  455. package/src/components/toggle-button/toggle-button.tsx +172 -0
  456. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  457. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  458. package/src/components/tooltip/AGENTS.md +133 -0
  459. package/src/components/tooltip/index.ts +28 -0
  460. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  461. package/src/components/tooltip/tooltip-content.tsx +179 -0
  462. package/src/components/tooltip/tooltip-description.tsx +19 -0
  463. package/src/components/tooltip/tooltip-text.tsx +21 -0
  464. package/src/components/tooltip/tooltip-title.tsx +20 -0
  465. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  466. package/src/components/tooltip/tooltip.context.tsx +68 -0
  467. package/src/components/tooltip/tooltip.tsx +213 -0
  468. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  469. package/src/components/tooltip/tooltip.variants.ts +177 -0
  470. package/src/hooks/use-calm-motion.tsx +31 -0
  471. package/src/lib/calm-motion.test.ts +17 -0
  472. package/src/lib/calm-motion.ts +23 -0
  473. package/src/lib/cn.test.ts +14 -0
  474. package/src/lib/cn.ts +2 -2
  475. package/src/no-gorhom.test.ts +54 -0
  476. package/src/styles/geometry.test.ts +1 -0
  477. package/src/styles/theme.css +4 -0
  478. package/src/styles/tokens.css +11 -0
  479. package/src/styles/tokens.test.ts +22 -0
  480. package/src/styles/tokens.ts +20 -0
  481. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  482. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -0,0 +1,303 @@
1
+ import {
2
+ Children,
3
+ isValidElement,
4
+ type ReactElement,
5
+ type ReactNode,
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useState,
10
+ } from "react";
11
+ import { I18nManager, Modal, ScrollView, useWindowDimensions, View, type ViewProps } from "react-native";
12
+ import { GestureHandlerRootView } from "react-native-gesture-handler";
13
+ import Animated, { useAnimatedStyle, useReducedMotion, useSharedValue, withTiming } from "react-native-reanimated";
14
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
15
+ import { scheduleOnRN } from "react-native-worklets";
16
+ import { Pressable } from "../pressable";
17
+ import { useMenuPart } from "./menu.context";
18
+ import {
19
+ MENU_DEFAULT_OFFSET,
20
+ MENU_EDGE_MARGIN,
21
+ MENU_ENTER_SCALE,
22
+ MENU_MOTION,
23
+ type MenuAlign,
24
+ type MenuAnchorRect,
25
+ type MenuPlacement,
26
+ type MenuPlacementResult,
27
+ menuVariants,
28
+ resolveMenuOrigin,
29
+ resolveMenuPhysicalAlign,
30
+ resolveMenuPlacement,
31
+ resolveMenuWidth,
32
+ } from "./menu.variants";
33
+ import { MenuBackground } from "./menu-background";
34
+
35
+ export type MenuContentProps = Omit<ViewProps, "children"> & {
36
+ children: ReactNode;
37
+ className?: string;
38
+ /** The side it prefers. Default `bottom`; flips when there is no room. */
39
+ placement?: MenuPlacement;
40
+ /** Which edge lines up with the anchor's. Default `start`; swaps under RTL. */
41
+ align?: MenuAlign;
42
+ /** Gap from the anchor, in points. Default 6. */
43
+ offset?: number;
44
+ /** Panel width. Default: the trigger's width, at least 200. */
45
+ width?: number;
46
+ minWidth?: number;
47
+ /** Cap on the panel's height. Default: the room inside the safe area. */
48
+ maxHeight?: number;
49
+ /** Scroll rows past the cap. Default `true`; `false` clips instead. */
50
+ isScrollable?: boolean;
51
+ /** Tint the screen behind the panel. Default `false`. */
52
+ hasScrim?: boolean;
53
+ /** Anchor to this rect instead of the trigger. A zero-size rect is a point. */
54
+ anchor?: MenuAnchorRect;
55
+ /**
56
+ * Drawn inside the `Modal`, over the scrim and behind the panel, fading with
57
+ * them. It takes no touches — a tap on it is an outside tap. ContextMenu's
58
+ * lifted preview is the reason it exists.
59
+ */
60
+ backdrop?: ReactNode;
61
+ };
62
+
63
+ /**
64
+ * The panel: a transparent `Modal` holding an anchored, scrolling list of rows.
65
+ *
66
+ * **The `Modal` is the layer.** It sits above everything, routes Android's back
67
+ * button to `onRequestClose`, and contains a screen reader — with no new
68
+ * dependency. A transparent full-screen pressable behind the panel closes it on
69
+ * an outside tap.
70
+ *
71
+ * **It measures, then places, then shows.** The first frame renders at opacity
72
+ * 0; once the rows report their height, `resolveMenuPlacement` picks the side,
73
+ * and the enter runs from there — so the panel never jumps. The side is then
74
+ * locked for the visit: a submenu expanding later grows the panel away from the
75
+ * anchor (a panel above is positioned by its bottom edge) and scrolls past the
76
+ * cap rather than flipping under the finger.
77
+ *
78
+ * **Enter is 160ms, exit 120ms**: opacity with a 0.96 scale from the anchor's
79
+ * side, approximated with a translate. Under reduced motion, opacity alone. The
80
+ * `Modal` unmounts when the exit finishes.
81
+ *
82
+ * A caller's `Menu.Background` child is lifted out and drawn behind the
83
+ * scroller in place of the default one.
84
+ */
85
+ export function MenuContent({
86
+ children,
87
+ className,
88
+ placement = "bottom",
89
+ align = "start",
90
+ offset = MENU_DEFAULT_OFFSET,
91
+ width,
92
+ minWidth,
93
+ maxHeight: maxHeightProp,
94
+ isScrollable = true,
95
+ hasScrim = false,
96
+ anchor: anchorProp,
97
+ backdrop,
98
+ ...props
99
+ }: MenuContentProps): ReactElement | null {
100
+ const { anchor: contextAnchor, close, isOpen } = useMenuPart("Menu.Content");
101
+ const anchor = anchorProp ?? contextAnchor;
102
+ const window = useWindowDimensions();
103
+ const insets = useSafeAreaInsets();
104
+ const isReducedMotion = useReducedMotion();
105
+ const slots = menuVariants();
106
+
107
+ // Adjusted during render, so the Modal mounts in the commit that opened it.
108
+ const [isPresent, setPresent] = useState(isOpen);
109
+ if (isOpen && !isPresent) setPresent(true);
110
+
111
+ const [measuredHeight, setMeasuredHeight] = useState<number | null>(null);
112
+ const [locked, setLocked] = useState<MenuPlacementResult | null>(null);
113
+
114
+ const { background, rows } = useMemo(() => splitBackground(children), [children]);
115
+
116
+ const resolvedWidth = resolveMenuWidth({
117
+ maxWidth: window.width - insets.left - insets.right - MENU_EDGE_MARGIN * 2,
118
+ minWidth,
119
+ triggerWidth: anchor?.width ?? 0,
120
+ width,
121
+ });
122
+
123
+ const resolve = useCallback(
124
+ (height: number) =>
125
+ anchor
126
+ ? resolveMenuPlacement({
127
+ align,
128
+ anchor,
129
+ content: { height, width: resolvedWidth },
130
+ insets,
131
+ isRTL: I18nManager.isRTL,
132
+ offset,
133
+ placement,
134
+ window: { height: window.height, width: window.width },
135
+ })
136
+ : null,
137
+ [align, anchor, insets, offset, placement, resolvedWidth, window.height, window.width]
138
+ );
139
+
140
+ // Placed once per visit, from the first measurement taken with an anchor.
141
+ useEffect(() => {
142
+ if (!isOpen || locked || measuredHeight === null) return;
143
+ const result = resolve(measuredHeight);
144
+ if (result) setLocked(result);
145
+ }, [isOpen, locked, measuredHeight, resolve]);
146
+
147
+ const handleContentSize = useCallback((_width: number, height: number) => setMeasuredHeight(height), []);
148
+
149
+ const handleExited = useCallback(() => {
150
+ setPresent(false);
151
+ setLocked(null);
152
+ setMeasuredHeight(null);
153
+ }, []);
154
+
155
+ const progress = useSharedValue(0);
156
+
157
+ useEffect(() => {
158
+ if (isOpen) {
159
+ if (!locked) return;
160
+ progress.value = withTiming(1, { duration: MENU_MOTION.enterMs });
161
+ return;
162
+ }
163
+ if (!isPresent) return;
164
+ progress.value = withTiming(0, { duration: MENU_MOTION.exitMs }, (isFinished) => {
165
+ "worklet";
166
+ if (isFinished) scheduleOnRN(handleExited);
167
+ });
168
+ }, [handleExited, isOpen, isPresent, locked, progress]);
169
+
170
+ const { cap, origin, position } = resolvePanelFrame({
171
+ align,
172
+ maxHeight: maxHeightProp,
173
+ measuredHeight,
174
+ placed: locked ?? resolve(measuredHeight ?? 0),
175
+ placement,
176
+ width: resolvedWidth,
177
+ windowHeight: window.height,
178
+ });
179
+ const originX = origin.x;
180
+ const originY = origin.y;
181
+ const enterScale = MENU_ENTER_SCALE;
182
+
183
+ const panelStyle = useAnimatedStyle(() => {
184
+ const p = progress.value;
185
+ if (isReducedMotion) return { opacity: p, transform: [{ translateX: 0 }, { translateY: 0 }, { scale: 1 }] };
186
+ return {
187
+ opacity: p,
188
+ transform: [
189
+ { translateX: originX * (1 - p) },
190
+ { translateY: originY * (1 - p) },
191
+ { scale: enterScale + (1 - enterScale) * p },
192
+ ],
193
+ };
194
+ });
195
+ const scrimStyle = useAnimatedStyle(() => ({ opacity: progress.value }));
196
+
197
+ if (!isPresent) return null;
198
+
199
+ return (
200
+ <Modal animationType="none" onRequestClose={close} statusBarTranslucent transparent visible>
201
+ <GestureHandlerRootView style={fill}>
202
+ <Pressable
203
+ accessibilityLabel="Close menu"
204
+ className="absolute inset-0"
205
+ feedback="none"
206
+ importantForAccessibility="no"
207
+ onPress={close}
208
+ >
209
+ {hasScrim ? <Animated.View className={slots.scrim()} style={scrimStyle} /> : null}
210
+ </Pressable>
211
+ {backdrop ? (
212
+ <Animated.View
213
+ className={slots.backdrop()}
214
+ importantForAccessibility="no-hide-descendants"
215
+ pointerEvents="none"
216
+ style={scrimStyle}
217
+ >
218
+ {backdrop}
219
+ </Animated.View>
220
+ ) : null}
221
+ <Animated.View
222
+ accessibilityRole="menu"
223
+ accessibilityViewIsModal
224
+ className={slots.panel()}
225
+ pointerEvents={locked ? "auto" : "none"}
226
+ style={[position, panelStyle]}
227
+ {...props}
228
+ >
229
+ <View className={slots.content({ className })}>
230
+ {background ?? <MenuBackground />}
231
+ <ScrollView
232
+ bounces={false}
233
+ contentContainerClassName={slots.scroll()}
234
+ onContentSizeChange={handleContentSize}
235
+ scrollEnabled={isScrollable}
236
+ showsVerticalScrollIndicator
237
+ style={{ flexGrow: 0, maxHeight: cap }}
238
+ >
239
+ {rows}
240
+ </ScrollView>
241
+ </View>
242
+ </Animated.View>
243
+ </GestureHandlerRootView>
244
+ </Modal>
245
+ );
246
+ }
247
+ MenuContent.displayName = "DelacourUI.Menu.Content";
248
+
249
+ const fill = { flex: 1 };
250
+
251
+ type PanelFrameInput = {
252
+ align: MenuAlign;
253
+ maxHeight: number | undefined;
254
+ measuredHeight: number | null;
255
+ placed: MenuPlacementResult | null;
256
+ placement: MenuPlacement;
257
+ width: number;
258
+ windowHeight: number;
259
+ };
260
+
261
+ /** The panel's height cap, its absolute position, and the side its enter grows from. */
262
+ function resolvePanelFrame({
263
+ align,
264
+ maxHeight,
265
+ measuredHeight,
266
+ placed,
267
+ placement,
268
+ width,
269
+ windowHeight,
270
+ }: PanelFrameInput): {
271
+ cap: number;
272
+ origin: { x: number; y: number };
273
+ position: { top?: number; bottom?: number; left: number; width: number };
274
+ } {
275
+ const cap = Math.min(maxHeight ?? Number.POSITIVE_INFINITY, placed?.maxHeight ?? windowHeight);
276
+ const origin = resolveMenuOrigin({
277
+ align: resolveMenuPhysicalAlign(align, I18nManager.isRTL),
278
+ height: Math.min(measuredHeight ?? 0, cap),
279
+ placement: placed?.placement ?? placement,
280
+ width,
281
+ });
282
+ const position =
283
+ placed?.placement === "top"
284
+ ? { bottom: placed.bottom, left: placed.left, width }
285
+ : { left: placed?.left ?? 0, top: placed?.top ?? 0, width };
286
+ return { cap, origin, position };
287
+ }
288
+
289
+ /** Lifts a caller's `Menu.Background` out of the rows, so it is drawn behind the scroller. */
290
+ function splitBackground(children: ReactNode): { background: ReactElement | null; rows: ReactNode[] } {
291
+ let background: ReactElement | null = null;
292
+ const rows: ReactNode[] = [];
293
+
294
+ for (const child of Children.toArray(children)) {
295
+ if (isValidElement(child) && child.type === MenuBackground) {
296
+ background = child;
297
+ continue;
298
+ }
299
+ rows.push(child);
300
+ }
301
+
302
+ return { background, rows };
303
+ }
@@ -0,0 +1,162 @@
1
+ import { type ReactElement, type ReactNode, useCallback, useMemo, useRef } from "react";
2
+ import { type AccessibilityState, View } from "react-native";
3
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
+ import Animated, { useAnimatedStyle, useReducedMotion, useSharedValue, withTiming } from "react-native-reanimated";
5
+ import { scheduleOnRN } from "react-native-worklets";
6
+ import { Icon } from "../icon";
7
+ import { playHaptic } from "../pressable/pressable";
8
+ import { Text } from "../text";
9
+ import { useMenuPart } from "./menu.context";
10
+ import type { MenuItemProps } from "./menu.types";
11
+ import {
12
+ MENU_ICON_TOKEN,
13
+ MENU_PRESS,
14
+ MENU_TAP_MAX_DISTANCE,
15
+ type MenuRowKind,
16
+ menuVariants,
17
+ resolveCloseOnSelect,
18
+ resolveMenuItemAccessibility,
19
+ } from "./menu.variants";
20
+
21
+ export type { MenuItemProps } from "./menu.types";
22
+
23
+ export type MenuRowProps = MenuItemProps & {
24
+ /** Which kind of row this is — decides whether choosing it closes the menu. */
25
+ kind: MenuRowKind;
26
+ /** Replaces the leading icon column — a checkbox's tick, a radio's mark. */
27
+ leading?: ReactNode;
28
+ accessibilityState?: AccessibilityState;
29
+ };
30
+
31
+ /**
32
+ * The row every menu row is built on: the press fill, the scale, the label
33
+ * column, the icon, the shortcut and the trailing slot.
34
+ *
35
+ * **The fill is the row's background**, an absolute layer whose opacity a tap
36
+ * drives — 90ms in, 160ms out — while the row dips to 0.98. Both read one shared
37
+ * value on the UI thread. Under reduced motion the fill snaps and the scale does
38
+ * not run: the fill is colour, not movement, and it is what says the tap landed.
39
+ *
40
+ * **A tap that travels is a scroll**, so the tap fails past
41
+ * `MENU_TAP_MAX_DISTANCE` and a long menu scrolls without choosing a row.
42
+ *
43
+ * Exported from this leaf, not from `index.ts`, so ContextMenu can build on it
44
+ * with `import { MenuRow } from "../menu/menu-item"` without pulling the root.
45
+ */
46
+ export function MenuRow({
47
+ kind,
48
+ leading,
49
+ children,
50
+ className,
51
+ icon,
52
+ iconPlacement = "leading",
53
+ description,
54
+ shortcut,
55
+ trailing,
56
+ variant = "default",
57
+ isInset = false,
58
+ isDisabled = false,
59
+ isClosedOnSelect,
60
+ onSelect,
61
+ accessibilityState,
62
+ ...props
63
+ }: MenuRowProps): ReactElement {
64
+ const { close, haptic } = useMenuPart("Menu.Item");
65
+ const isReducedMotion = useReducedMotion();
66
+ const slots = menuVariants({ isDisabled, isInset, variant });
67
+ const shouldClose = resolveCloseOnSelect(kind, isClosedOnSelect);
68
+
69
+ const selectRef = useRef(onSelect);
70
+ selectRef.current = onSelect;
71
+ const handleSelect = useCallback(() => {
72
+ selectRef.current?.();
73
+ if (shouldClose) close();
74
+ }, [close, shouldClose]);
75
+
76
+ const pressed = useSharedValue(0);
77
+ const inMs = isReducedMotion ? 0 : MENU_PRESS.inMs;
78
+ const outMs = isReducedMotion ? 0 : MENU_PRESS.outMs;
79
+ const pressedScale = isReducedMotion ? 1 : MENU_PRESS.scale;
80
+
81
+ const gesture = useMemo(
82
+ () =>
83
+ Gesture.Tap()
84
+ .enabled(!isDisabled)
85
+ .maxDistance(MENU_TAP_MAX_DISTANCE)
86
+ .shouldCancelWhenOutside(true)
87
+ .onBegin(() => {
88
+ "worklet";
89
+ pressed.value = withTiming(1, { duration: inMs });
90
+ })
91
+ .onEnd(() => {
92
+ "worklet";
93
+ if (haptic) playHaptic(haptic);
94
+ scheduleOnRN(handleSelect);
95
+ })
96
+ .onFinalize(() => {
97
+ "worklet";
98
+ pressed.value = withTiming(0, { duration: outMs });
99
+ }),
100
+ [handleSelect, haptic, inMs, isDisabled, outMs, pressed]
101
+ );
102
+
103
+ const rowStyle = useAnimatedStyle(() => ({
104
+ transform: [{ scale: 1 - pressed.value * (1 - pressedScale) }],
105
+ }));
106
+ const fillStyle = useAnimatedStyle(() => ({ opacity: pressed.value }));
107
+
108
+ const glyph = icon ? <Icon className={slots.itemIcon()} color={MENU_ICON_TOKEN[variant]} icon={icon} /> : null;
109
+ const isLeadingIcon = iconPlacement === "leading";
110
+ const leadingNode =
111
+ leading ?? (isLeadingIcon && glyph ? glyph : isInset ? <View className={slots.indicator()} /> : null);
112
+ const label =
113
+ typeof children === "string" || typeof children === "number" ? (
114
+ <Text className={slots.itemLabel()} numberOfLines={1}>
115
+ {children}
116
+ </Text>
117
+ ) : (
118
+ children
119
+ );
120
+
121
+ return (
122
+ <GestureDetector gesture={gesture}>
123
+ <Animated.View
124
+ accessibilityRole="menuitem"
125
+ accessibilityState={accessibilityState ?? resolveMenuItemAccessibility({ isDisabled, kind: "item" })}
126
+ accessible
127
+ className={slots.item({ className })}
128
+ style={rowStyle}
129
+ {...props}
130
+ >
131
+ <Animated.View className={slots.itemFill()} pointerEvents="none" style={fillStyle} />
132
+ {leadingNode}
133
+ <View className={slots.itemContent()}>
134
+ {label}
135
+ {description ? <Text className={slots.itemDescription()}>{description}</Text> : null}
136
+ </View>
137
+ {shortcut ? <Text className={slots.itemShortcut()}>{shortcut}</Text> : null}
138
+ {!isLeadingIcon ? glyph : null}
139
+ {trailing}
140
+ </Animated.View>
141
+ </GestureDetector>
142
+ );
143
+ }
144
+ MenuRow.displayName = "DelacourUI.Menu.Row";
145
+
146
+ /**
147
+ * A verb: one row that runs `onSelect` and closes the menu.
148
+ *
149
+ * @example
150
+ * <Menu.Item icon={IconPencil} shortcut="⌘R" onSelect={rename}>Rename</Menu.Item>
151
+ */
152
+ export function MenuItem({ isDisabled = false, ...props }: MenuItemProps): ReactElement {
153
+ return (
154
+ <MenuRow
155
+ accessibilityState={resolveMenuItemAccessibility({ isDisabled, kind: "item" })}
156
+ isDisabled={isDisabled}
157
+ kind="item"
158
+ {...props}
159
+ />
160
+ );
161
+ }
162
+ MenuItem.displayName = "DelacourUI.Menu.Item";
@@ -0,0 +1,14 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text, type TextProps } from "../text";
3
+ import { menuVariants } from "./menu.variants";
4
+
5
+ export type MenuLabelProps = TextProps & {
6
+ /** Line up with rows that reserve the icon column. */
7
+ isInset?: boolean;
8
+ };
9
+
10
+ /** A section heading inside the panel. Not a row: it cannot be chosen. */
11
+ export function MenuLabel({ isInset = false, className, ...props }: MenuLabelProps): ReactElement {
12
+ return <Text accessibilityRole="header" className={menuVariants({ isInset }).label({ className })} {...props} />;
13
+ }
14
+ MenuLabel.displayName = "DelacourUI.Menu.Label";
@@ -0,0 +1,50 @@
1
+ import { type ReactElement, useMemo } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { useControllableState } from "../../hooks/use-controllable-state";
4
+ import { type MenuRadioGroupContextValue, MenuRadioGroupProvider } from "./menu.context";
5
+ import { resolveRadioNext } from "./menu.variants";
6
+
7
+ export type MenuRadioGroupProps = ViewProps & {
8
+ value?: string;
9
+ defaultValue?: string;
10
+ onValueChange?: (value: string) => void;
11
+ };
12
+
13
+ /**
14
+ * One choice from several, as rows. Holds the value; `Menu.RadioItem` rows read it.
15
+ *
16
+ * @example
17
+ * <Menu.RadioGroup value={density} onValueChange={setDensity}>
18
+ * <Menu.RadioItem value="comfortable">Comfortable</Menu.RadioItem>
19
+ * <Menu.RadioItem value="compact">Compact</Menu.RadioItem>
20
+ * </Menu.RadioGroup>
21
+ */
22
+ export function MenuRadioGroup({
23
+ value: valueProp,
24
+ defaultValue,
25
+ onValueChange,
26
+ children,
27
+ ...props
28
+ }: MenuRadioGroupProps): ReactElement {
29
+ const [value, setValue] = useControllableState<string | undefined>({
30
+ defaultValue,
31
+ onChange: (next) => {
32
+ if (next !== undefined) onValueChange?.(next);
33
+ },
34
+ value: valueProp,
35
+ });
36
+
37
+ const context = useMemo<MenuRadioGroupContextValue>(
38
+ () => ({ select: (next) => setValue(resolveRadioNext(value, next)), value }),
39
+ [setValue, value]
40
+ );
41
+
42
+ return (
43
+ <MenuRadioGroupProvider value={context}>
44
+ <View accessibilityRole="radiogroup" {...props}>
45
+ {children}
46
+ </View>
47
+ </MenuRadioGroupProvider>
48
+ );
49
+ }
50
+ MenuRadioGroup.displayName = "DelacourUI.Menu.RadioGroup";
@@ -0,0 +1,57 @@
1
+ import type { ReactElement } from "react";
2
+ import { View } from "react-native";
3
+ import { IconCheckmark1 } from "../../icons/central";
4
+ import { Icon } from "../icon";
5
+ import { useMenuRadioGroupPart } from "./menu.context";
6
+ import type { MenuItemProps } from "./menu.types";
7
+ import {
8
+ MENU_INDICATOR_TOKEN,
9
+ type MenuRadioIndicator,
10
+ menuVariants,
11
+ resolveMenuItemAccessibility,
12
+ } from "./menu.variants";
13
+ import { MenuRow } from "./menu-item";
14
+
15
+ export type MenuRadioItemProps = Omit<MenuItemProps, "onSelect"> & {
16
+ value: string;
17
+ /** A tick, or a 6pt dot. Default `check`. */
18
+ indicator?: MenuRadioIndicator;
19
+ };
20
+
21
+ /**
22
+ * One option in a `Menu.RadioGroup`, marked in the icon column while selected.
23
+ *
24
+ * Closes the menu on select by default — a choice made is a visit finished.
25
+ * The mark takes the leading column, so an `icon` moves to the row's end.
26
+ * Announced as `selected`.
27
+ */
28
+ export function MenuRadioItem({
29
+ value,
30
+ indicator = "check",
31
+ isDisabled = false,
32
+ ...props
33
+ }: MenuRadioItemProps): ReactElement {
34
+ const group = useMenuRadioGroupPart("Menu.RadioItem");
35
+ const isSelected = group.value === value;
36
+ const slots = menuVariants();
37
+
38
+ const mark =
39
+ indicator === "dot" ? (
40
+ <View className={slots.indicatorDot()} />
41
+ ) : (
42
+ <Icon className={slots.itemIcon()} color={MENU_INDICATOR_TOKEN} icon={IconCheckmark1} />
43
+ );
44
+
45
+ return (
46
+ <MenuRow
47
+ accessibilityState={resolveMenuItemAccessibility({ isDisabled, isSelected, kind: "radio" })}
48
+ isDisabled={isDisabled}
49
+ kind="radio"
50
+ leading={<View className={slots.indicator()}>{isSelected ? mark : null}</View>}
51
+ onSelect={() => group.select(value)}
52
+ {...props}
53
+ iconPlacement="trailing"
54
+ />
55
+ );
56
+ }
57
+ MenuRadioItem.displayName = "DelacourUI.Menu.RadioItem";
@@ -0,0 +1,11 @@
1
+ import type { ReactElement } from "react";
2
+ import { Separator, type SeparatorProps } from "../separator";
3
+ import { menuVariants } from "./menu.variants";
4
+
5
+ export type MenuSeparatorProps = Omit<SeparatorProps, "orientation">;
6
+
7
+ /** A rule between groups of rows — `Separator`, spaced for the panel. */
8
+ export function MenuSeparator({ className, ...props }: MenuSeparatorProps): ReactElement {
9
+ return <Separator className={menuVariants().separator({ className })} {...props} />;
10
+ }
11
+ MenuSeparator.displayName = "DelacourUI.Menu.Separator";
@@ -0,0 +1,72 @@
1
+ import { type ReactElement, type ReactNode, useCallback, useState } from "react";
2
+ import { type LayoutChangeEvent, StyleSheet, View, type ViewProps } from "react-native";
3
+ import Animated, { useAnimatedStyle } from "react-native-reanimated";
4
+ import { useMenuSubPart } from "./menu.context";
5
+ import { menuVariants } from "./menu.variants";
6
+
7
+ export type MenuSubContentProps = Omit<ViewProps, "style"> & {
8
+ className?: string;
9
+ children?: ReactNode;
10
+ };
11
+
12
+ /**
13
+ * The rows a `Menu.Sub` discloses, expanding in place.
14
+ *
15
+ * `Collapsible.Content`'s measured clip: the outer view's height runs from zero
16
+ * to what the out-of-flow inner view reported, so the rows below follow through
17
+ * ordinary layout and the panel's scroller sees the new height. The rows mount
18
+ * on first open; a closed submenu leaves the accessibility tree and takes no
19
+ * touches.
20
+ */
21
+ export function MenuSubContent({ className, children, ...props }: MenuSubContentProps): ReactElement {
22
+ const { contentHeight, isOpen, onMeasured, progress } = useMenuSubPart("Menu.SubContent");
23
+ const slots = menuVariants();
24
+
25
+ const [hasOpened, setHasOpened] = useState(isOpen);
26
+ if (isOpen && !hasOpened) setHasOpened(true);
27
+
28
+ const [isMeasured, setMeasured] = useState(false);
29
+
30
+ const handleLayout = useCallback(
31
+ (event: LayoutChangeEvent) => {
32
+ contentHeight.value = event.nativeEvent.layout.height;
33
+ if (isMeasured) return;
34
+ setMeasured(true);
35
+ onMeasured();
36
+ },
37
+ [contentHeight, isMeasured, onMeasured]
38
+ );
39
+
40
+ const clipStyle = useAnimatedStyle(() => ({
41
+ height: progress.value * Math.max(contentHeight.value, 0),
42
+ opacity: progress.value,
43
+ }));
44
+
45
+ return (
46
+ <Animated.View
47
+ accessibilityElementsHidden={!isOpen}
48
+ className={slots.subClip()}
49
+ importantForAccessibility={isOpen ? "auto" : "no-hide-descendants"}
50
+ pointerEvents={isOpen && isMeasured ? "auto" : "none"}
51
+ style={isMeasured ? clipStyle : subContentStyles.measuring}
52
+ >
53
+ {hasOpened ? (
54
+ <View className={slots.subInner()} onLayout={handleLayout}>
55
+ <View className={slots.subContent({ className })} {...props}>
56
+ {children}
57
+ </View>
58
+ </View>
59
+ ) : null}
60
+ </Animated.View>
61
+ );
62
+ }
63
+ MenuSubContent.displayName = "DelacourUI.Menu.SubContent";
64
+
65
+ const subContentStyles = StyleSheet.create({
66
+ /**
67
+ * Out of flow and transparent until the rows report a height — never
68
+ * `height: 0`, because a child mounted into a zero-height parent never fires
69
+ * `onLayout`. `Collapsible.Content` carries the same sheet.
70
+ */
71
+ measuring: { left: 0, opacity: 0, position: "absolute", right: 0, top: 0 },
72
+ });