@delacour/react-native-ui 0.1.0-alpha.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. package/README.md +10 -6
  2. package/package.json +48 -4
  3. package/src/components/accordion/AGENTS.md +21 -9
  4. package/src/components/accordion/accordion-content.tsx +7 -4
  5. package/src/components/accordion/accordion-item.tsx +15 -13
  6. package/src/components/accordion/accordion.context.tsx +11 -5
  7. package/src/components/accordion/accordion.variants.test.ts +64 -3
  8. package/src/components/accordion/accordion.variants.ts +33 -9
  9. package/src/components/alert/AGENTS.md +102 -0
  10. package/src/components/alert/alert-action.tsx +17 -0
  11. package/src/components/alert/alert-close-button.tsx +50 -0
  12. package/src/components/alert/alert-content.tsx +15 -0
  13. package/src/components/alert/alert-description.tsx +16 -0
  14. package/src/components/alert/alert-glyphs.ts +25 -0
  15. package/src/components/alert/alert-indicator.tsx +40 -0
  16. package/src/components/alert/alert-title.tsx +18 -0
  17. package/src/components/alert/alert.context.tsx +69 -0
  18. package/src/components/alert/alert.tsx +160 -0
  19. package/src/components/alert/alert.types.ts +10 -0
  20. package/src/components/alert/alert.variants.test.ts +218 -0
  21. package/src/components/alert/alert.variants.ts +180 -0
  22. package/src/components/alert/index.ts +23 -0
  23. package/src/components/avatar/AGENTS.md +106 -0
  24. package/src/components/avatar/avatar-badge.tsx +51 -0
  25. package/src/components/avatar/avatar-group.tsx +139 -0
  26. package/src/components/avatar/avatar.context.tsx +93 -0
  27. package/src/components/avatar/avatar.tsx +201 -0
  28. package/src/components/avatar/avatar.utils.test.ts +174 -0
  29. package/src/components/avatar/avatar.utils.ts +141 -0
  30. package/src/components/avatar/avatar.variants.test.ts +251 -0
  31. package/src/components/avatar/avatar.variants.ts +270 -0
  32. package/src/components/avatar/index.ts +39 -0
  33. package/src/components/badge/AGENTS.md +5 -0
  34. package/src/components/badge/badge.tsx +6 -1
  35. package/src/components/bottom-sheet/AGENTS.md +186 -231
  36. package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
  37. package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
  38. package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -208
  39. package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
  40. package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
  41. package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
  42. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -122
  43. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
  44. package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
  45. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
  46. package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
  47. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -62
  48. package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
  49. package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
  50. package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
  51. package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
  52. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
  53. package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
  54. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -87
  55. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
  56. package/src/components/bottom-sheet/index.ts +39 -17
  57. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
  58. package/src/components/button/AGENTS.md +45 -11
  59. package/src/components/button/button-group.tsx +44 -8
  60. package/src/components/button/button.context.tsx +10 -0
  61. package/src/components/button/button.tsx +2 -1
  62. package/src/components/button/button.variants.test.ts +154 -0
  63. package/src/components/button/button.variants.ts +71 -6
  64. package/src/components/button/index.ts +2 -0
  65. package/src/components/calendar/AGENTS.md +142 -0
  66. package/src/components/calendar/calendar-caption.tsx +116 -0
  67. package/src/components/calendar/calendar-day.tsx +103 -0
  68. package/src/components/calendar/calendar-grid.tsx +183 -0
  69. package/src/components/calendar/calendar-header.tsx +34 -0
  70. package/src/components/calendar/calendar-nav.tsx +51 -0
  71. package/src/components/calendar/calendar-picker.tsx +128 -0
  72. package/src/components/calendar/calendar-weekdays.tsx +48 -0
  73. package/src/components/calendar/calendar.context.tsx +108 -0
  74. package/src/components/calendar/calendar.date.test.ts +456 -0
  75. package/src/components/calendar/calendar.date.ts +584 -0
  76. package/src/components/calendar/calendar.tsx +389 -0
  77. package/src/components/calendar/calendar.types.ts +87 -0
  78. package/src/components/calendar/calendar.variants.test.ts +363 -0
  79. package/src/components/calendar/calendar.variants.ts +426 -0
  80. package/src/components/calendar/index.ts +96 -0
  81. package/src/components/card/AGENTS.md +88 -0
  82. package/src/components/card/card-action.tsx +16 -0
  83. package/src/components/card/card-content.tsx +12 -0
  84. package/src/components/card/card-description.tsx +12 -0
  85. package/src/components/card/card-footer.tsx +30 -0
  86. package/src/components/card/card-header.tsx +45 -0
  87. package/src/components/card/card-title.tsx +25 -0
  88. package/src/components/card/card.context.tsx +64 -0
  89. package/src/components/card/card.tsx +94 -0
  90. package/src/components/card/card.types.ts +13 -0
  91. package/src/components/card/card.variants.test.ts +240 -0
  92. package/src/components/card/card.variants.ts +177 -0
  93. package/src/components/card/index.ts +15 -0
  94. package/src/components/chart/AGENTS.md +8 -0
  95. package/src/components/chart/chart.tsx +8 -1
  96. package/src/components/chip/AGENTS.md +113 -0
  97. package/src/components/chip/chip-close-button.tsx +48 -0
  98. package/src/components/chip/chip-end-content.tsx +15 -0
  99. package/src/components/chip/chip-label.tsx +21 -0
  100. package/src/components/chip/chip-start-content.tsx +16 -0
  101. package/src/components/chip/chip.context.tsx +66 -0
  102. package/src/components/chip/chip.tsx +268 -0
  103. package/src/components/chip/chip.types.ts +10 -0
  104. package/src/components/chip/chip.variants.test.ts +335 -0
  105. package/src/components/chip/chip.variants.ts +321 -0
  106. package/src/components/chip/index.ts +24 -0
  107. package/src/components/collapsible/AGENTS.md +97 -0
  108. package/src/components/collapsible/collapsible-content.tsx +97 -0
  109. package/src/components/collapsible/collapsible-description.tsx +17 -0
  110. package/src/components/collapsible/collapsible-indicator.tsx +92 -0
  111. package/src/components/collapsible/collapsible-title.tsx +17 -0
  112. package/src/components/collapsible/collapsible-trigger.tsx +133 -0
  113. package/src/components/collapsible/collapsible.context.tsx +91 -0
  114. package/src/components/collapsible/collapsible.tsx +167 -0
  115. package/src/components/collapsible/collapsible.types.ts +9 -0
  116. package/src/components/collapsible/collapsible.variants.test.ts +338 -0
  117. package/src/components/collapsible/collapsible.variants.ts +228 -0
  118. package/src/components/collapsible/index.ts +32 -0
  119. package/src/components/context-menu/AGENTS.md +103 -0
  120. package/src/components/context-menu/context-menu-content.tsx +64 -0
  121. package/src/components/context-menu/context-menu-item.tsx +22 -0
  122. package/src/components/context-menu/context-menu-preview.tsx +60 -0
  123. package/src/components/context-menu/context-menu-trigger.tsx +160 -0
  124. package/src/components/context-menu/context-menu.context.tsx +91 -0
  125. package/src/components/context-menu/context-menu.tsx +90 -0
  126. package/src/components/context-menu/context-menu.variants.test.ts +266 -0
  127. package/src/components/context-menu/context-menu.variants.ts +147 -0
  128. package/src/components/context-menu/index.ts +18 -0
  129. package/src/components/dialog/AGENTS.md +97 -0
  130. package/src/components/dialog/dialog-body.tsx +21 -0
  131. package/src/components/dialog/dialog-close.tsx +82 -0
  132. package/src/components/dialog/dialog-content.tsx +153 -0
  133. package/src/components/dialog/dialog-description.tsx +23 -0
  134. package/src/components/dialog/dialog-footer.tsx +32 -0
  135. package/src/components/dialog/dialog-header.tsx +20 -0
  136. package/src/components/dialog/dialog-title.tsx +42 -0
  137. package/src/components/dialog/dialog-trigger.tsx +54 -0
  138. package/src/components/dialog/dialog.context.tsx +90 -0
  139. package/src/components/dialog/dialog.tsx +119 -0
  140. package/src/components/dialog/dialog.variants.test.ts +224 -0
  141. package/src/components/dialog/dialog.variants.ts +173 -0
  142. package/src/components/dialog/index.ts +30 -0
  143. package/src/components/drawer/AGENTS.md +123 -0
  144. package/src/components/drawer/drawer-body.tsx +46 -0
  145. package/src/components/drawer/drawer-close.tsx +76 -0
  146. package/src/components/drawer/drawer-content.tsx +186 -0
  147. package/src/components/drawer/drawer-description.tsx +23 -0
  148. package/src/components/drawer/drawer-footer.tsx +21 -0
  149. package/src/components/drawer/drawer-header.tsx +42 -0
  150. package/src/components/drawer/drawer-title.tsx +35 -0
  151. package/src/components/drawer/drawer-trigger.tsx +54 -0
  152. package/src/components/drawer/drawer.context.tsx +114 -0
  153. package/src/components/drawer/drawer.tsx +119 -0
  154. package/src/components/drawer/drawer.variants.test.ts +392 -0
  155. package/src/components/drawer/drawer.variants.ts +244 -0
  156. package/src/components/drawer/index.ts +41 -0
  157. package/src/components/drawer/use-drawer-pan.ts +108 -0
  158. package/src/components/empty-state/AGENTS.md +90 -0
  159. package/src/components/empty-state/empty-state-content.tsx +17 -0
  160. package/src/components/empty-state/empty-state-description.tsx +12 -0
  161. package/src/components/empty-state/empty-state-header.tsx +17 -0
  162. package/src/components/empty-state/empty-state-media.tsx +56 -0
  163. package/src/components/empty-state/empty-state-title.tsx +18 -0
  164. package/src/components/empty-state/empty-state.context.tsx +65 -0
  165. package/src/components/empty-state/empty-state.tsx +84 -0
  166. package/src/components/empty-state/empty-state.types.ts +14 -0
  167. package/src/components/empty-state/empty-state.variants.test.ts +245 -0
  168. package/src/components/empty-state/empty-state.variants.ts +132 -0
  169. package/src/components/empty-state/index.ts +20 -0
  170. package/src/components/fab/AGENTS.md +119 -0
  171. package/src/components/fab/fab-action.tsx +118 -0
  172. package/src/components/fab/fab-group.tsx +214 -0
  173. package/src/components/fab/fab-label.tsx +18 -0
  174. package/src/components/fab/fab.context.tsx +125 -0
  175. package/src/components/fab/fab.tsx +156 -0
  176. package/src/components/fab/fab.types.ts +17 -0
  177. package/src/components/fab/fab.variants.test.ts +274 -0
  178. package/src/components/fab/fab.variants.ts +234 -0
  179. package/src/components/fab/index.ts +37 -0
  180. package/src/components/feedback/AGENTS.md +95 -0
  181. package/src/components/feedback/feedback-action.tsx +16 -0
  182. package/src/components/feedback/feedback-cancel.tsx +34 -0
  183. package/src/components/feedback/feedback-close.tsx +21 -0
  184. package/src/components/feedback/feedback-content.tsx +24 -0
  185. package/src/components/feedback/feedback-field.tsx +70 -0
  186. package/src/components/feedback/feedback-footer.tsx +21 -0
  187. package/src/components/feedback/feedback-panel.tsx +54 -0
  188. package/src/components/feedback/feedback-submit.tsx +74 -0
  189. package/src/components/feedback/feedback-title.tsx +34 -0
  190. package/src/components/feedback/feedback-trigger.tsx +20 -0
  191. package/src/components/feedback/feedback.context.tsx +73 -0
  192. package/src/components/feedback/feedback.tsx +135 -0
  193. package/src/components/feedback/feedback.variants.test.ts +185 -0
  194. package/src/components/feedback/feedback.variants.ts +128 -0
  195. package/src/components/feedback/index.ts +24 -0
  196. package/src/components/field/AGENTS.md +19 -0
  197. package/src/components/field/field-label.tsx +19 -5
  198. package/src/components/field/field.context.tsx +17 -0
  199. package/src/components/field/field.tsx +15 -2
  200. package/src/components/field/field.variants.test.ts +24 -0
  201. package/src/components/field/field.variants.ts +20 -0
  202. package/src/components/field/index.ts +1 -0
  203. package/src/components/item/AGENTS.md +92 -0
  204. package/src/components/item/index.ts +25 -0
  205. package/src/components/item/item-actions.tsx +28 -0
  206. package/src/components/item/item-content.tsx +15 -0
  207. package/src/components/item/item-description.tsx +12 -0
  208. package/src/components/item/item-footer.tsx +13 -0
  209. package/src/components/item/item-group.tsx +32 -0
  210. package/src/components/item/item-header.tsx +13 -0
  211. package/src/components/item/item-media.tsx +37 -0
  212. package/src/components/item/item-separator.tsx +13 -0
  213. package/src/components/item/item-title.tsx +17 -0
  214. package/src/components/item/item.context.tsx +66 -0
  215. package/src/components/item/item.tsx +218 -0
  216. package/src/components/item/item.types.ts +12 -0
  217. package/src/components/item/item.variants.test.ts +263 -0
  218. package/src/components/item/item.variants.ts +200 -0
  219. package/src/components/kpi/AGENTS.md +125 -0
  220. package/src/components/kpi/index.ts +45 -0
  221. package/src/components/kpi/kpi-action.tsx +12 -0
  222. package/src/components/kpi/kpi-content.tsx +35 -0
  223. package/src/components/kpi/kpi-footer.tsx +52 -0
  224. package/src/components/kpi/kpi-group.tsx +70 -0
  225. package/src/components/kpi/kpi-header.tsx +25 -0
  226. package/src/components/kpi/kpi-icon.tsx +28 -0
  227. package/src/components/kpi/kpi-sparkline.tsx +145 -0
  228. package/src/components/kpi/kpi-stat.tsx +20 -0
  229. package/src/components/kpi/kpi-title.tsx +19 -0
  230. package/src/components/kpi/kpi-trend.tsx +105 -0
  231. package/src/components/kpi/kpi-value.tsx +32 -0
  232. package/src/components/kpi/kpi.context.tsx +111 -0
  233. package/src/components/kpi/kpi.tsx +154 -0
  234. package/src/components/kpi/kpi.types.ts +9 -0
  235. package/src/components/kpi/kpi.variants.test.ts +428 -0
  236. package/src/components/kpi/kpi.variants.ts +347 -0
  237. package/src/components/label/AGENTS.md +89 -0
  238. package/src/components/label/index.ts +10 -0
  239. package/src/components/label/label.tsx +97 -0
  240. package/src/components/label/label.variants.test.ts +176 -0
  241. package/src/components/label/label.variants.ts +123 -0
  242. package/src/components/menu/AGENTS.md +176 -0
  243. package/src/components/menu/index.ts +44 -0
  244. package/src/components/menu/menu-background.tsx +17 -0
  245. package/src/components/menu/menu-checkbox-item.tsx +57 -0
  246. package/src/components/menu/menu-content.tsx +303 -0
  247. package/src/components/menu/menu-item.tsx +162 -0
  248. package/src/components/menu/menu-label.tsx +14 -0
  249. package/src/components/menu/menu-radio-group.tsx +50 -0
  250. package/src/components/menu/menu-radio-item.tsx +57 -0
  251. package/src/components/menu/menu-separator.tsx +11 -0
  252. package/src/components/menu/menu-sub-content.tsx +72 -0
  253. package/src/components/menu/menu-sub-trigger.tsx +50 -0
  254. package/src/components/menu/menu-sub.tsx +75 -0
  255. package/src/components/menu/menu-trigger.tsx +57 -0
  256. package/src/components/menu/menu.context.tsx +214 -0
  257. package/src/components/menu/menu.tsx +88 -0
  258. package/src/components/menu/menu.types.ts +33 -0
  259. package/src/components/menu/menu.variants.test.ts +400 -0
  260. package/src/components/menu/menu.variants.ts +353 -0
  261. package/src/components/meter/AGENTS.md +135 -0
  262. package/src/components/meter/index.ts +43 -0
  263. package/src/components/meter/meter-fill.tsx +19 -0
  264. package/src/components/meter/meter-header.tsx +17 -0
  265. package/src/components/meter/meter-label.tsx +18 -0
  266. package/src/components/meter/meter-output.tsx +42 -0
  267. package/src/components/meter/meter-segments.tsx +83 -0
  268. package/src/components/meter/meter-track.tsx +16 -0
  269. package/src/components/meter/meter.context.tsx +62 -0
  270. package/src/components/meter/meter.tsx +286 -0
  271. package/src/components/meter/meter.types.ts +30 -0
  272. package/src/components/meter/meter.variants.test.ts +430 -0
  273. package/src/components/meter/meter.variants.ts +347 -0
  274. package/src/components/overlay/AGENTS.md +89 -0
  275. package/src/components/overlay/index.ts +27 -0
  276. package/src/components/overlay/overlay-portal.tsx +84 -0
  277. package/src/components/overlay/overlay-presence.test.ts +46 -0
  278. package/src/components/overlay/overlay-presence.ts +49 -0
  279. package/src/components/overlay/overlay-registry.test.ts +96 -0
  280. package/src/components/overlay/overlay-registry.ts +99 -0
  281. package/src/components/overlay/overlay-scrim.tsx +49 -0
  282. package/src/components/overlay/overlay.context.tsx +48 -0
  283. package/src/components/overlay/overlay.tsx +118 -0
  284. package/src/components/overlay/overlay.variants.test.ts +37 -0
  285. package/src/components/overlay/overlay.variants.ts +33 -0
  286. package/src/components/overlay/use-overlay-back-handler.ts +34 -0
  287. package/src/components/overlay/use-overlay-presence.ts +87 -0
  288. package/src/components/popover/AGENTS.md +138 -0
  289. package/src/components/popover/index.ts +48 -0
  290. package/src/components/popover/popover-anchor.tsx +49 -0
  291. package/src/components/popover/popover-arrow.tsx +89 -0
  292. package/src/components/popover/popover-close.tsx +73 -0
  293. package/src/components/popover/popover-content.tsx +235 -0
  294. package/src/components/popover/popover-description.tsx +18 -0
  295. package/src/components/popover/popover-title.tsx +33 -0
  296. package/src/components/popover/popover-trigger.tsx +56 -0
  297. package/src/components/popover/popover.context.tsx +71 -0
  298. package/src/components/popover/popover.position.test.ts +354 -0
  299. package/src/components/popover/popover.position.ts +286 -0
  300. package/src/components/popover/popover.tsx +123 -0
  301. package/src/components/popover/popover.variants.test.ts +135 -0
  302. package/src/components/popover/popover.variants.ts +85 -0
  303. package/src/components/popover/use-anchor-measure.ts +76 -0
  304. package/src/components/popover/use-anchored-content.ts +219 -0
  305. package/src/components/pressable/AGENTS.md +11 -1
  306. package/src/components/pressable/pressable.tsx +7 -2
  307. package/src/components/pressable/pressable.variants.test.ts +29 -0
  308. package/src/components/pressable/pressable.variants.ts +17 -0
  309. package/src/components/progress/AGENTS.md +120 -0
  310. package/src/components/progress/index.ts +40 -0
  311. package/src/components/progress/progress-fill.tsx +152 -0
  312. package/src/components/progress/progress-header.tsx +19 -0
  313. package/src/components/progress/progress-label.tsx +27 -0
  314. package/src/components/progress/progress-output.tsx +47 -0
  315. package/src/components/progress/progress-track.tsx +38 -0
  316. package/src/components/progress/progress.context.tsx +79 -0
  317. package/src/components/progress/progress.tsx +171 -0
  318. package/src/components/progress/progress.types.ts +23 -0
  319. package/src/components/progress/progress.variants.test.ts +335 -0
  320. package/src/components/progress/progress.variants.ts +284 -0
  321. package/src/components/progress-button/AGENTS.md +116 -0
  322. package/src/components/progress-button/index.ts +36 -0
  323. package/src/components/progress-button/progress-button-done.tsx +33 -0
  324. package/src/components/progress-button/progress-button-label.tsx +27 -0
  325. package/src/components/progress-button/progress-button.context.tsx +93 -0
  326. package/src/components/progress-button/progress-button.tsx +414 -0
  327. package/src/components/progress-button/progress-button.variants.test.ts +314 -0
  328. package/src/components/progress-button/progress-button.variants.ts +236 -0
  329. package/src/components/provider/AGENTS.md +38 -25
  330. package/src/components/provider/provider.tsx +38 -11
  331. package/src/components/radio/AGENTS.md +8 -10
  332. package/src/components/radio/radio.variants.test.ts +5 -6
  333. package/src/components/radio/radio.variants.ts +6 -7
  334. package/src/components/rating/AGENTS.md +143 -0
  335. package/src/components/rating/index.ts +43 -0
  336. package/src/components/rating/rating-output.tsx +39 -0
  337. package/src/components/rating/rating-star.tsx +70 -0
  338. package/src/components/rating/rating-stars.tsx +219 -0
  339. package/src/components/rating/rating.context.tsx +79 -0
  340. package/src/components/rating/rating.tsx +163 -0
  341. package/src/components/rating/rating.variants.test.ts +347 -0
  342. package/src/components/rating/rating.variants.ts +354 -0
  343. package/src/components/selection-mode/AGENTS.md +155 -0
  344. package/src/components/selection-mode/index.ts +43 -0
  345. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  346. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  347. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  348. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  349. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  350. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  351. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  352. package/src/components/selection-mode/selection-mode.tsx +207 -0
  353. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  354. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  355. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  356. package/src/components/skeleton/AGENTS.md +120 -0
  357. package/src/components/skeleton/index.ts +15 -0
  358. package/src/components/skeleton/skeleton-clock.ts +48 -0
  359. package/src/components/skeleton/skeleton-group.tsx +64 -0
  360. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  361. package/src/components/skeleton/skeleton.context.tsx +45 -0
  362. package/src/components/skeleton/skeleton.tsx +227 -0
  363. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  364. package/src/components/skeleton/skeleton.variants.ts +222 -0
  365. package/src/components/slide-button/AGENTS.md +122 -0
  366. package/src/components/slide-button/index.ts +43 -0
  367. package/src/components/slide-button/slide-button-label.tsx +70 -0
  368. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  369. package/src/components/slide-button/slide-button.context.tsx +76 -0
  370. package/src/components/slide-button/slide-button.tsx +457 -0
  371. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  372. package/src/components/slide-button/slide-button.variants.ts +324 -0
  373. package/src/components/slider/index.ts +2 -0
  374. package/src/components/slider/slider-thumb.tsx +33 -1
  375. package/src/components/slider/slider.variants.test.ts +52 -0
  376. package/src/components/slider/slider.variants.ts +56 -2
  377. package/src/components/stack-card/AGENTS.md +169 -0
  378. package/src/components/stack-card/index.ts +37 -0
  379. package/src/components/stack-card/stack-card-action.tsx +66 -0
  380. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  381. package/src/components/stack-card/stack-card-card.tsx +71 -0
  382. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  383. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  384. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  385. package/src/components/stack-card/stack-card.context.tsx +117 -0
  386. package/src/components/stack-card/stack-card.tsx +557 -0
  387. package/src/components/stack-card/stack-card.types.ts +26 -0
  388. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  389. package/src/components/stack-card/stack-card.variants.ts +360 -0
  390. package/src/components/steps/AGENTS.md +153 -0
  391. package/src/components/steps/index.ts +45 -0
  392. package/src/components/steps/steps-connector.tsx +31 -0
  393. package/src/components/steps/steps-description.tsx +26 -0
  394. package/src/components/steps/steps-indicator.tsx +106 -0
  395. package/src/components/steps/steps-item.tsx +225 -0
  396. package/src/components/steps/steps-panel.tsx +41 -0
  397. package/src/components/steps/steps-title.tsx +32 -0
  398. package/src/components/steps/steps-track.tsx +59 -0
  399. package/src/components/steps/steps.context.tsx +123 -0
  400. package/src/components/steps/steps.tsx +166 -0
  401. package/src/components/steps/steps.variants.test.ts +388 -0
  402. package/src/components/steps/steps.variants.ts +468 -0
  403. package/src/components/surface/AGENTS.md +72 -0
  404. package/src/components/surface/index.ts +15 -0
  405. package/src/components/surface/surface.context.tsx +54 -0
  406. package/src/components/surface/surface.tsx +70 -0
  407. package/src/components/surface/surface.variants.test.ts +180 -0
  408. package/src/components/surface/surface.variants.ts +133 -0
  409. package/src/components/swipe/AGENTS.md +140 -0
  410. package/src/components/swipe/index.ts +31 -0
  411. package/src/components/swipe/swipe-action.tsx +99 -0
  412. package/src/components/swipe/swipe-end.tsx +14 -0
  413. package/src/components/swipe/swipe-group.tsx +55 -0
  414. package/src/components/swipe/swipe-panel.tsx +55 -0
  415. package/src/components/swipe/swipe-start.tsx +15 -0
  416. package/src/components/swipe/swipe.context.tsx +135 -0
  417. package/src/components/swipe/swipe.tsx +393 -0
  418. package/src/components/swipe/swipe.types.ts +7 -0
  419. package/src/components/swipe/swipe.variants.test.ts +337 -0
  420. package/src/components/swipe/swipe.variants.ts +290 -0
  421. package/src/components/switch/switch.variants.ts +2 -2
  422. package/src/components/tabs/AGENTS.md +4 -3
  423. package/src/components/tabs/tabs.variants.test.ts +1 -1
  424. package/src/components/tabs/tabs.variants.ts +4 -6
  425. package/src/components/textarea/AGENTS.md +86 -0
  426. package/src/components/textarea/index.ts +16 -0
  427. package/src/components/textarea/textarea.tsx +157 -0
  428. package/src/components/textarea/textarea.variants.test.ts +219 -0
  429. package/src/components/textarea/textarea.variants.ts +171 -0
  430. package/src/components/toast/AGENTS.md +140 -0
  431. package/src/components/toast/index.ts +74 -0
  432. package/src/components/toast/toast-action.tsx +59 -0
  433. package/src/components/toast/toast-api.ts +32 -0
  434. package/src/components/toast/toast-close.tsx +44 -0
  435. package/src/components/toast/toast-content.tsx +11 -0
  436. package/src/components/toast/toast-description.tsx +11 -0
  437. package/src/components/toast/toast-indicator.tsx +34 -0
  438. package/src/components/toast/toast-item.tsx +308 -0
  439. package/src/components/toast/toast-title.tsx +13 -0
  440. package/src/components/toast/toast-viewport.tsx +135 -0
  441. package/src/components/toast/toast.context.tsx +69 -0
  442. package/src/components/toast/toast.store.test.ts +259 -0
  443. package/src/components/toast/toast.store.ts +338 -0
  444. package/src/components/toast/toast.timer.test.ts +64 -0
  445. package/src/components/toast/toast.timer.ts +51 -0
  446. package/src/components/toast/toast.tsx +132 -0
  447. package/src/components/toast/toast.variants.test.ts +393 -0
  448. package/src/components/toast/toast.variants.ts +304 -0
  449. package/src/components/toggle-button/AGENTS.md +106 -0
  450. package/src/components/toggle-button/index.ts +30 -0
  451. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  452. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  453. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  454. package/src/components/toggle-button/toggle-button.tsx +172 -0
  455. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  456. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  457. package/src/components/tooltip/AGENTS.md +133 -0
  458. package/src/components/tooltip/index.ts +28 -0
  459. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  460. package/src/components/tooltip/tooltip-content.tsx +179 -0
  461. package/src/components/tooltip/tooltip-description.tsx +19 -0
  462. package/src/components/tooltip/tooltip-text.tsx +21 -0
  463. package/src/components/tooltip/tooltip-title.tsx +20 -0
  464. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  465. package/src/components/tooltip/tooltip.context.tsx +68 -0
  466. package/src/components/tooltip/tooltip.tsx +213 -0
  467. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  468. package/src/components/tooltip/tooltip.variants.ts +177 -0
  469. package/src/hooks/use-calm-motion.tsx +31 -0
  470. package/src/lib/calm-motion.test.ts +17 -0
  471. package/src/lib/calm-motion.ts +23 -0
  472. package/src/lib/cn.test.ts +14 -0
  473. package/src/lib/cn.ts +2 -2
  474. package/src/no-gorhom.test.ts +54 -0
  475. package/src/styles/geometry.test.ts +1 -0
  476. package/src/styles/theme.css +4 -0
  477. package/src/styles/tokens.css +11 -0
  478. package/src/styles/tokens.test.ts +22 -0
  479. package/src/styles/tokens.ts +20 -0
  480. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  481. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -0,0 +1,176 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { fieldVariants, resolveFieldTextColor } from "../field/field.variants";
3
+ import { TEXT_COLORS } from "../text/text.variants";
4
+ import {
5
+ LABEL_REQUIRED_MARK,
6
+ LABEL_REQUIRED_MARK_COLOR,
7
+ labelText,
8
+ labelVariants,
9
+ resolveLabelAccessibilityLabel,
10
+ resolveLabelColor,
11
+ } from "./label.variants";
12
+
13
+ /**
14
+ * A slot's class string, with `tv`'s empty-slot `undefined` flattened.
15
+ *
16
+ * Both slots are handed to a `Text` and carry state only, so they are empty most
17
+ * of the time — and a bare `not.toMatch()` throws on `undefined` rather than
18
+ * passing.
19
+ */
20
+ function cls(value: string | undefined): string {
21
+ return value ?? "";
22
+ }
23
+
24
+ const SLOTS = ["root", "requiredMark"] as const;
25
+
26
+ describe("the slots", () => {
27
+ // The type scale belongs to `Text.Label`. A size or a weight here would be a
28
+ // second definition of it that could drift from the preset.
29
+ test("restate no type scale in any state", () => {
30
+ for (const isDisabled of [false, true]) {
31
+ for (const isInvalid of [false, true]) {
32
+ const slots = labelVariants({ isDisabled, isInvalid });
33
+ for (const slot of SLOTS) {
34
+ expect(cls(slots[slot]())).not.toMatch(/\btext-(xs|sm|base|lg|xl|2xl|3xl)\b/);
35
+ expect(cls(slots[slot]())).not.toMatch(/\bfont-(normal|medium|semibold|bold)\b/);
36
+ }
37
+ }
38
+ }
39
+ });
40
+
41
+ // Colour rides `Text`'s own axis, so a caller's `color` beats it through the
42
+ // same channel rather than through a tailwind-merge race.
43
+ test("carry no colour, since that rides the preset's own axis", () => {
44
+ for (const isInvalid of [false, true]) {
45
+ const slots = labelVariants({ isInvalid });
46
+ for (const slot of SLOTS) {
47
+ expect(cls(slots[slot]())).not.toMatch(/\btext-\w+-?\w*\b/);
48
+ }
49
+ }
50
+ });
51
+
52
+ test("merge an incoming className last", () => {
53
+ expect(labelVariants({ isDisabled: true }).root({ className: "opacity-80" })).toContain("opacity-80");
54
+ expect(labelVariants({ isDisabled: true }).root({ className: "opacity-80" })).not.toContain("opacity-50");
55
+ expect(labelVariants().requiredMark({ className: "ml-1" })).toContain("ml-1");
56
+ });
57
+ });
58
+
59
+ describe("a disabled label", () => {
60
+ test("fades", () => {
61
+ expect(cls(labelVariants({ isDisabled: true }).root())).toContain("opacity-50");
62
+ expect(cls(labelVariants({ isDisabled: false }).root())).not.toContain("opacity-50");
63
+ });
64
+
65
+ // The mark is a nested run inside the label's own text, so the root's opacity
66
+ // already reaches it. Fading it again would square the fade.
67
+ test("does not fade the required mark a second time", () => {
68
+ expect(cls(labelVariants({ isDisabled: true }).requiredMark())).not.toContain("opacity");
69
+ });
70
+
71
+ test("is enabled by default", () => {
72
+ expect(labelVariants().root()).toBe(labelVariants({ isDisabled: false }).root());
73
+ });
74
+ });
75
+
76
+ describe("resolveLabelColor", () => {
77
+ test("turns destructive only while invalid", () => {
78
+ expect(resolveLabelColor(true)).toBe("destructive");
79
+ expect(resolveLabelColor(false)).toBeUndefined();
80
+ });
81
+
82
+ // `undefined` means "leave the preset's own colour alone" — `Text`'s colour
83
+ // axis emits nothing when it is not named.
84
+ test("names a colour Text actually has", () => {
85
+ expect(TEXT_COLORS).toContain(resolveLabelColor(true) ?? "default");
86
+ });
87
+ });
88
+
89
+ describe("the required mark", () => {
90
+ test("is an asterisk", () => {
91
+ expect(LABEL_REQUIRED_MARK).toBe("*");
92
+ });
93
+
94
+ // Required is a warning about what the form will refuse, so it is drawn in
95
+ // the colour of the refusal whether or not the field is currently wrong.
96
+ test("is destructive, and a colour Text has", () => {
97
+ expect(LABEL_REQUIRED_MARK_COLOR).toBe("destructive");
98
+ expect(TEXT_COLORS).toContain(LABEL_REQUIRED_MARK_COLOR);
99
+ });
100
+ });
101
+
102
+ describe("labelText", () => {
103
+ test("reads a string or a number", () => {
104
+ expect(labelText("Email")).toBe("Email");
105
+ expect(labelText(42)).toBe("42");
106
+ });
107
+
108
+ test("joins an array of strings, the shape `{name} address` compiles to", () => {
109
+ expect(labelText(["Work", " ", "email"])).toBe("Work email");
110
+ expect(labelText(["Line ", 2])).toBe("Line 2");
111
+ });
112
+
113
+ // The holes a conditional child leaves behind are not text.
114
+ test("skips null, undefined and booleans inside an array", () => {
115
+ expect(labelText(["Email", null, undefined, false, true])).toBe("Email");
116
+ });
117
+
118
+ // An element could be anything — an icon, a link — and guessing at its text
119
+ // would put words in a screen reader's mouth the caller never wrote.
120
+ test("gives up on anything it cannot read as text", () => {
121
+ expect(labelText({ type: "Icon" })).toBeUndefined();
122
+ expect(labelText(["Email", { type: "Icon" }])).toBeUndefined();
123
+ expect(labelText(undefined)).toBeUndefined();
124
+ expect(labelText(null)).toBeUndefined();
125
+ });
126
+
127
+ test("treats empty text as nothing to read", () => {
128
+ expect(labelText("")).toBeUndefined();
129
+ expect(labelText(["", null])).toBeUndefined();
130
+ });
131
+ });
132
+
133
+ describe("resolveLabelAccessibilityLabel", () => {
134
+ // A nested asterisk reads as "star" — so a required label is announced by
135
+ // what it means instead of by its punctuation.
136
+ test("reads a required label as required, not as an asterisk", () => {
137
+ expect(resolveLabelAccessibilityLabel({ children: "Email", isRequired: true })).toBe("Email, required");
138
+ });
139
+
140
+ test("leaves an optional label to the text itself", () => {
141
+ expect(resolveLabelAccessibilityLabel({ children: "Email", isRequired: false })).toBeUndefined();
142
+ });
143
+
144
+ test("lets the caller's own label win", () => {
145
+ expect(
146
+ resolveLabelAccessibilityLabel({ accessibilityLabel: "Work email", children: "Email", isRequired: true })
147
+ ).toBe("Work email");
148
+ expect(
149
+ resolveLabelAccessibilityLabel({ accessibilityLabel: "Work email", children: "Email", isRequired: false })
150
+ ).toBe("Work email");
151
+ });
152
+
153
+ // With children it cannot read, the text stays whatever the platform makes
154
+ // of it rather than becoming a bare "required" with the name dropped.
155
+ test("says nothing when it cannot read the children", () => {
156
+ expect(resolveLabelAccessibilityLabel({ children: { type: "Icon" }, isRequired: true })).toBeUndefined();
157
+ });
158
+ });
159
+
160
+ // `Field.Label` is the field-aware counterpart of `Label`, styled from
161
+ // `fieldVariants`. A label in a field and a label outside one must not read as
162
+ // two different components, so the decision written twice is held together by a
163
+ // test rather than by a comment hoping it stays so.
164
+ describe("parity with Field", () => {
165
+ test("colours the label the way Field's resolver does", () => {
166
+ for (const isInvalid of [false, true]) {
167
+ expect(resolveLabelColor(isInvalid)).toBe(resolveFieldTextColor("label", isInvalid));
168
+ }
169
+ });
170
+
171
+ test("fades the label the way Field's slot does", () => {
172
+ for (const isDisabled of [false, true]) {
173
+ expect(cls(labelVariants({ isDisabled }).root())).toBe(cls(fieldVariants({ isDisabled }).label()));
174
+ }
175
+ });
176
+ });
@@ -0,0 +1,123 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+ import type { TextColor } from "../text/text.variants";
4
+
5
+ /** What a required label appends. */
6
+ export const LABEL_REQUIRED_MARK = "*";
7
+
8
+ /**
9
+ * The required mark's colour, in every state.
10
+ *
11
+ * Required is a warning about what the form will refuse, so it is drawn in the
12
+ * colour of that refusal whether or not the value is wrong right now.
13
+ */
14
+ export const LABEL_REQUIRED_MARK_COLOR: TextColor = "destructive";
15
+
16
+ /**
17
+ * State for a form label and the required mark inside it.
18
+ *
19
+ * Two slots because the mark is a second `Text` nested in the label's own, not
20
+ * because either carries a style of its own: **no slot here holds a type scale
21
+ * or a colour.** The label renders `Text.Label` and passes a colour through
22
+ * {@link resolveLabelColor}, so the scale lives in exactly one place — a
23
+ * `text-sm font-medium` written here would be a second definition of
24
+ * `Text.Label` that could drift from it. The mark inherits that scale by
25
+ * nesting.
26
+ *
27
+ * Only the root fades. The mark is a run inside the root's text, so the root's
28
+ * opacity already reaches it; fading it again would square the fade.
29
+ *
30
+ * Free of React Native imports so it stays unit-testable — `bun test` cannot
31
+ * parse React Native's Flow-typed source. See AGENTS.md.
32
+ */
33
+ export const labelVariants = tv({
34
+ slots: {
35
+ root: "",
36
+ requiredMark: "",
37
+ },
38
+ variants: {
39
+ // The empty `false` branches are load-bearing typing, not placeholders.
40
+ // `tv` derives the prop type from the declared keys, so a map with only
41
+ // `true` types the prop as `true` rather than `boolean`. See the note in
42
+ // button.variants.ts.
43
+ isInvalid: { true: {}, false: {} },
44
+ isDisabled: { true: { root: "opacity-50" }, false: {} },
45
+ },
46
+ defaultVariants: {
47
+ isInvalid: false,
48
+ isDisabled: false,
49
+ },
50
+ });
51
+
52
+ /**
53
+ * The `Text` colour a label takes.
54
+ *
55
+ * Returning `undefined` is meaningful rather than lazy: `Text`'s `color` axis
56
+ * emits nothing when it is not named, so the label falls through to
57
+ * `Text.Label`'s own `text-foreground` without this module knowing which token
58
+ * the preset chose.
59
+ *
60
+ * Pure, so the matrix is reachable from `bun test`. See AGENTS.md.
61
+ */
62
+ export function resolveLabelColor(isInvalid: boolean): TextColor | undefined {
63
+ return isInvalid ? "destructive" : undefined;
64
+ }
65
+
66
+ /**
67
+ * The plain text a label's children spell, or `undefined` when they are
68
+ * anything but text.
69
+ *
70
+ * Strings and numbers are read, and an array of them is joined — that is what
71
+ * `{name} address` compiles to. The `null`, `undefined` and booleans a
72
+ * conditional child leaves behind are skipped. An element is not guessed at: it
73
+ * could be an icon or a link, and inventing its words would put something in a
74
+ * screen reader's mouth the caller never wrote.
75
+ *
76
+ * Typed on `unknown` rather than `ReactNode` so this module takes not even a
77
+ * type import from React, which is what keeps it reachable from `bun test`.
78
+ */
79
+ export function labelText(children: unknown): string | undefined {
80
+ const parts = Array.isArray(children) ? children : [children];
81
+ let text = "";
82
+
83
+ for (const part of parts) {
84
+ if (part === null || part === undefined || typeof part === "boolean") continue;
85
+ if (typeof part === "string" || typeof part === "number") {
86
+ text += String(part);
87
+ continue;
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ return text === "" ? undefined : text;
93
+ }
94
+
95
+ /**
96
+ * What a screen reader announces for a label.
97
+ *
98
+ * The caller's own `accessibilityLabel` always wins. Otherwise a required label
99
+ * whose children are text is announced as `"Email, required"`: the mark is a
100
+ * nested `Text`, and React Native reads a `Text` and its nested runs as one
101
+ * string, so without this VoiceOver says "Email, star". An optional label, or
102
+ * one whose children cannot be read as text, returns `undefined` and is left to
103
+ * the platform.
104
+ *
105
+ * Pure, so the matrix is reachable from `bun test`. See AGENTS.md.
106
+ */
107
+ export function resolveLabelAccessibilityLabel({
108
+ accessibilityLabel,
109
+ children,
110
+ isRequired,
111
+ }: {
112
+ accessibilityLabel?: string;
113
+ children: unknown;
114
+ isRequired: boolean;
115
+ }): string | undefined {
116
+ if (accessibilityLabel !== undefined) return accessibilityLabel;
117
+ if (!isRequired) return undefined;
118
+
119
+ const text = labelText(children);
120
+ return text === undefined ? undefined : `${text}, required`;
121
+ }
122
+
123
+ export type LabelVariantProps = VariantProps<typeof labelVariants>;
@@ -0,0 +1,176 @@
1
+ # Menu
2
+
3
+ A list of **verbs** dropped from a control that exists to open it — a ⋯ button,
4
+ a toolbar item. Rename, duplicate, share, delete. A menu is not a select: its
5
+ rows run actions, they never become the trigger's value. Compound root plus
6
+ `Menu.Trigger`, `Menu.Content`, `Menu.Background`, `Menu.Label`, `Menu.Item`,
7
+ `Menu.CheckboxItem`, `Menu.RadioGroup`, `Menu.RadioItem`, `Menu.Separator`,
8
+ `Menu.Sub`, `Menu.SubTrigger` and `Menu.SubContent`.
9
+
10
+ `import { Menu } from "@delacour/react-native-ui/menu";`
11
+
12
+ ## Files
13
+
14
+ | File | What it holds |
15
+ | --- | --- |
16
+ | `index.ts` | → `@delacour/react-native-ui/menu` |
17
+ | `menu.tsx` | Root — `useMenuRootValue` into `MenuRootProvider`; the `Object.assign` |
18
+ | `menu-trigger.tsx` | `Menu.Trigger` — measures itself and toggles; `asChild` donates the press |
19
+ | `menu-content.tsx` | `Menu.Content` — the transparent `Modal`, the outside-tap layer, the placed and animated panel, the scroller |
20
+ | `menu-background.tsx` | `Menu.Background` — the `bg-popover` surface behind the scroller |
21
+ | `menu-label.tsx` | `Menu.Label` — a section heading |
22
+ | `menu-item.tsx` | `MenuRow`, the row every row is built on, and `Menu.Item` |
23
+ | `menu-checkbox-item.tsx` | `Menu.CheckboxItem` |
24
+ | `menu-radio-group.tsx` | `Menu.RadioGroup` — holds the value |
25
+ | `menu-radio-item.tsx` | `Menu.RadioItem` |
26
+ | `menu-separator.tsx` | `Menu.Separator` — `Separator` with `my-1` |
27
+ | `menu-sub.tsx` | `Menu.Sub` — the submenu's open state, `progress` and measured height |
28
+ | `menu-sub-trigger.tsx` | `Menu.SubTrigger` — the row with the turning chevron |
29
+ | `menu-sub-content.tsx` | `Menu.SubContent` — the measured clip |
30
+ | `menu.context.tsx` | The menu, submenu and radio-group contexts; `useMenu`, `useMenuRootValue`, `MenuRootProvider` |
31
+ | `menu.types.ts` | `MenuItemProps`, shared by every row |
32
+ | `menu.variants.ts` | The slotted `tv()`, the constants and the pure resolvers — no RN imports |
33
+ | `menu.variants.test.ts` | Placement (flip, cap, clamp, RTL, the zero-size anchor), width, close rules, accessibility, tokens |
34
+
35
+ ## The panel
36
+
37
+ - **It is a minimal anchored panel built here, not a Popover.** There is no
38
+ Popover in the package yet. `Menu.Content` renders inside a transparent RN
39
+ `Modal` — top-most layer, Android back through `onRequestClose`, screen-reader
40
+ containment — so it needs no new peer. **When Popover lands, Menu.Content moves
41
+ onto it and `resolveMenuPlacement` moves with it.** The resolver is exported
42
+ for exactly that.
43
+ - **Placement takes a rect, never a ref.** `resolveMenuPlacement({ anchor, content,
44
+ window, insets, placement, align, offset, isRTL })` places against
45
+ `{ x, y, width, height }`, so a zero-size point — ContextMenu's long-press —
46
+ goes through the same arithmetic with no special case, and a test proves it.
47
+ - **Flip, then cap.** The requested side wins when the content fits; otherwise
48
+ the other side if it fits there; otherwise the larger side, and `maxHeight`
49
+ caps it so the rows scroll. Room is measured inside the safe area plus an 8pt
50
+ margin, and the panel is clamped horizontally inside the same bounds. `align`
51
+ start and end swap under RTL.
52
+ - **Measure, place, show.** The panel's first frame is at opacity 0; the
53
+ scroller's `onContentSizeChange` reports the rows' height, placement runs with
54
+ it, and only then does the enter start — there is no jump.
55
+ - **The side is locked for the visit.** A submenu expanding mid-visit must not
56
+ flip the panel under the finger. A panel above the anchor is positioned by
57
+ its **bottom** edge (`MenuPlacementResult.bottom`), so it grows upward, away
58
+ from the anchor, with nothing re-placed; past the cap the rows scroll.
59
+ - **Its surface is `bg-popover`, never `bg-overlay`.** `overlay` is the
60
+ translucent scrim colour (`oklch(0 0 0 / 45%)`); a panel in it is a
61
+ see-through card. `hasScrim` (ContextMenu sets it) draws `bg-overlay` behind
62
+ the panel, fading with it.
63
+ - **The shadow is on an outer layer with no clip.** iOS clips a shadow to a
64
+ clipping view's bounds, so `panel` casts `shadow-lg` and the inner `content`
65
+ slot carries the corner, the hairline and `overflow-hidden`.
66
+ - **A caller's `Menu.Background` is lifted out by type** and drawn behind the
67
+ scroller, so it stays put while the rows scroll. The default is drawn when
68
+ there is none.
69
+ - **Enter 160ms, exit 120ms**, opacity with a 0.96 scale from the anchor's side.
70
+ React Native has no animatable transform origin on both platforms, so
71
+ `resolveMenuOrigin` approximates it with a translate that falls to zero as the
72
+ panel arrives. Under reduced motion it is opacity alone — the panel still
73
+ appears, so the information is not lost. The `Modal` unmounts when the exit's
74
+ timing finishes; a reopen mid-exit cancels it and runs the enter instead.
75
+ - **Gesture Handler needs its own root inside a `Modal`.** On Android a `Modal`
76
+ is a separate window the app's root view cannot see into, so every tap in the
77
+ panel would be dead. `Menu.Content` wraps its tree in a `GestureHandlerRootView`.
78
+
79
+ ## Rows
80
+
81
+ - **One row, `MenuRow`, under every row kind.** `Menu.Item`, `CheckboxItem`,
82
+ `RadioItem` and `SubTrigger` are `MenuRow` with a `kind`, a leading override
83
+ and an `accessibilityState`. Four hand-rolled rows would be four press fills
84
+ that drift.
85
+ - **The press fill is the row's background.** An absolute `bg-accent` layer
86
+ (`bg-destructive-soft` on a destructive row) fades in over 90ms and out over
87
+ 160ms while the row dips to 0.98 — one shared value for both. There is no
88
+ wash or ripple on top. A row is built on its own Gesture Handler tap rather
89
+ than `Pressable`, because `Pressable` owns its `pressed` value privately and
90
+ the fill has to read it.
91
+ - **Under reduced motion the fill snaps and the scale does not run.** The
92
+ scale is movement and goes; the fill is colour, and it is what says the tap
93
+ landed, so it stays — instantly.
94
+ - **A tap that travels is a scroll.** The tap fails past 10pt
95
+ (`MENU_TAP_MAX_DISTANCE`), so a long menu scrolls without choosing the row the
96
+ finger started on.
97
+ - **Rows take `ViewProps`, not `PressableProps`.** The spec sketched
98
+ `Omit<PressableProps, …>`, but a row ignores `feedback`, `asChild`, `busy` and
99
+ the rest; a type that accepts props it drops is a quiet bug. `isDisabled` is
100
+ the row's own.
101
+ - **Close rules live in one resolver.** `resolveCloseOnSelect(kind, explicit?)`:
102
+ item and radio close, checkbox and sub-trigger stay open; an explicit
103
+ `isClosedOnSelect` wins. `resolveRadioNext` pins that re-choosing a selected
104
+ radio keeps it — a radio never clears.
105
+ - **A checkbox's tick and a radio's mark take the leading icon column**, so the
106
+ labels of every row line up; an `icon` on those rows moves to the trailing
107
+ edge. `isInset` reserves the same column on a plain row, and on a `Menu.Label`.
108
+ - **The icon column is 18pt, `size-icon-md`.** The spec named `size-icon-sm`,
109
+ which is 16pt in `tokens.css`; the 18pt it also named wins, since that is the
110
+ footprint `isInset` and the label's `ps-[42px]` (12 + 18 + 12) are built on.
111
+ - **Haptics are the root's `haptic`, played on select**, from the tap's worklet,
112
+ and off by default.
113
+
114
+ ## Submenus
115
+
116
+ - **They expand in place, never fly out.** A phone has no room beside the
117
+ panel, and a second layer over the first hides where the user came from.
118
+ `Menu.SubContent` is `Collapsible.Content`'s measured clip — restated, not
119
+ imported, so `delacour add menu` copies one folder — and `MENU_SUB_SPRING` is
120
+ `Collapsible`'s spring.
121
+ - **The chevron turns 90° off the submenu's own `progress`**, so it and the
122
+ height cannot drift. Under RTL it is mirrored with `scaleX: -1` on a wrapper
123
+ view, so it points to the trailing edge and still turns downward; the
124
+ rotation lives on the inner view.
125
+ - **Nothing on the JS thread reads `contentHeight.value`** — the accordion's
126
+ Release-build bug. "Measured" is React state.
127
+
128
+ ## State
129
+
130
+ - **`isOpen` / `defaultOpen` / `onOpenChange` through `useControllableState`**,
131
+ in `useMenuRootValue`. `open(anchor?)` opens at a rect, or measures the
132
+ trigger first; `close()`; `toggle()`. Callbacks are ref-backed so the context
133
+ keeps its identity.
134
+ - **A controlled open is measured too.** A parent flipping `isOpen` to `true`
135
+ without a trigger press still gets the trigger measured, by an effect that
136
+ tells such an open from one `open()` started.
137
+ - **`Menu.Trigger asChild` donates the press**, `BottomSheet.Trigger`'s rule: a
138
+ `Button` wrapped in a pressable trigger would win the touch and the menu
139
+ would never open. The child gets `onPress`, the measured ref and the
140
+ accessibility props through `Slot`.
141
+
142
+ ## Accessibility
143
+
144
+ - Content: `accessibilityRole="menu"`, `accessibilityViewIsModal`.
145
+ - Rows: `accessibilityRole="menuitem"`; checkbox → `checked`, radio →
146
+ `selected`, sub-trigger → `expanded`, all → `disabled`. One source:
147
+ `resolveMenuItemAccessibility`, a discriminated union on `kind`.
148
+ - Trigger: `accessibilityState={{ expanded }}`, hint "Opens a menu".
149
+ - A closed submenu is out of the accessibility tree, not just clipped.
150
+
151
+ ## Reuse contract for ContextMenu
152
+
153
+ ContextMenu renders this component's rows and panel rather than a copy:
154
+
155
+ - **Import leaves, never `../menu`.** `../menu/menu-item` (`MenuRow`,
156
+ `MenuItem`), `../menu/menu-content`, `../menu/menu.context` and
157
+ `../menu/menu.variants` are each importable on their own. None imports
158
+ `./menu`, so no cycle can close (rule 3).
159
+ - **Drive the state with your own anchor.** Build a value with
160
+ `useMenuRootValue(...)`, wrap your tree in `MenuRootProvider`, and call
161
+ `open({ x, y, width: 0, height: 0 })` from the long-press. Or pass
162
+ `anchor` straight to `Menu.Content`.
163
+ - **`hasScrim`** tints the screen behind the panel; **`iconPlacement="trailing"`**
164
+ puts a row's icon at its end.
165
+ - **`backdrop`** draws a node inside the `Modal`, over the scrim and behind the
166
+ panel, fading with them and taking no touches. A `Modal` covers everything,
167
+ so anything meant to show above the scrim — ContextMenu's lifted preview — has
168
+ to be inside it; outside, it would sit under the tint.
169
+
170
+ ## Out of scope
171
+
172
+ - **`presentation="bottom-sheet"`.** Importing `../bottom-sheet` would force the
173
+ optional sheet engine and `react-native-teleport` on every Menu consumer. A
174
+ follow-up.
175
+ - **Keyboard navigation and focus return on web.**
176
+ - **Typeahead, and rows that open a second panel.**
@@ -0,0 +1,44 @@
1
+ export { Menu, type MenuProps } from "./menu";
2
+ export {
3
+ type MenuContextValue,
4
+ type MenuMeasurable,
5
+ MenuRootProvider,
6
+ type UseMenuRootValueOptions,
7
+ useMenu,
8
+ useMenuRootValue,
9
+ } from "./menu.context";
10
+ export type { MenuItemProps } from "./menu.types";
11
+ export {
12
+ MENU_ALIGNS,
13
+ MENU_DEFAULT_MIN_WIDTH,
14
+ MENU_DEFAULT_OFFSET,
15
+ MENU_EDGE_MARGIN,
16
+ MENU_ITEM_VARIANTS,
17
+ MENU_PLACEMENTS,
18
+ MENU_RADIO_INDICATORS,
19
+ type MenuAlign,
20
+ type MenuAnchorRect,
21
+ type MenuInsets,
22
+ type MenuItemVariant,
23
+ type MenuPlacement,
24
+ type MenuPlacementInput,
25
+ type MenuPlacementResult,
26
+ type MenuRadioIndicator,
27
+ type MenuVariantProps,
28
+ menuVariants,
29
+ resolveCloseOnSelect,
30
+ resolveMenuPlacement,
31
+ resolveMenuWidth,
32
+ resolveRadioNext,
33
+ } from "./menu.variants";
34
+ export type { MenuBackgroundProps } from "./menu-background";
35
+ export type { MenuCheckboxItemProps } from "./menu-checkbox-item";
36
+ export type { MenuContentProps } from "./menu-content";
37
+ export type { MenuLabelProps } from "./menu-label";
38
+ export type { MenuRadioGroupProps } from "./menu-radio-group";
39
+ export type { MenuRadioItemProps } from "./menu-radio-item";
40
+ export type { MenuSeparatorProps } from "./menu-separator";
41
+ export type { MenuSubProps } from "./menu-sub";
42
+ export type { MenuSubContentProps } from "./menu-sub-content";
43
+ export type { MenuSubTriggerProps } from "./menu-sub-trigger";
44
+ export type { MenuTriggerProps } from "./menu-trigger";
@@ -0,0 +1,17 @@
1
+ import type { ReactElement } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { menuVariants } from "./menu.variants";
4
+
5
+ export type MenuBackgroundProps = ViewProps & { className?: string };
6
+
7
+ /**
8
+ * The panel's surface: an absolute-fill layer in `bg-popover`, behind the rows.
9
+ *
10
+ * `Menu.Content` draws one by default. Pass your own as a child of
11
+ * `Menu.Content` to repaint it — the content lifts it out of the children and
12
+ * renders it outside the scroller, where it stays put while the rows scroll.
13
+ */
14
+ export function MenuBackground({ className, ...props }: MenuBackgroundProps): ReactElement {
15
+ return <View className={menuVariants().background({ className })} pointerEvents="none" {...props} />;
16
+ }
17
+ MenuBackground.displayName = "DelacourUI.Menu.Background";
@@ -0,0 +1,57 @@
1
+ import type { ReactElement } from "react";
2
+ import { View } from "react-native";
3
+ import { useControllableState } from "../../hooks/use-controllable-state";
4
+ import { IconCheckmark1 } from "../../icons/central";
5
+ import { Icon } from "../icon";
6
+ import type { MenuItemProps } from "./menu.types";
7
+ import { MENU_INDICATOR_TOKEN, menuVariants, resolveMenuItemAccessibility } from "./menu.variants";
8
+ import { MenuRow } from "./menu-item";
9
+
10
+ export type MenuCheckboxItemProps = Omit<MenuItemProps, "onSelect" | "isClosedOnSelect"> & {
11
+ isChecked?: boolean;
12
+ defaultChecked?: boolean;
13
+ onCheckedChange?: (isChecked: boolean) => void;
14
+ /** Whether choosing the row closes the menu. Default `false` — several are usually toggled in one visit. */
15
+ isClosedOnSelect?: boolean;
16
+ };
17
+
18
+ /**
19
+ * A row that toggles a setting, with a tick in the icon column while it is on.
20
+ *
21
+ * Stays open on select by default. The tick takes the leading column, so an
22
+ * `icon` moves to the row's end. Announced as `checked`.
23
+ *
24
+ * @example
25
+ * <Menu.CheckboxItem isChecked={showDone} onCheckedChange={setShowDone}>Completed</Menu.CheckboxItem>
26
+ */
27
+ export function MenuCheckboxItem({
28
+ isChecked: isCheckedProp,
29
+ defaultChecked = false,
30
+ onCheckedChange,
31
+ isDisabled = false,
32
+ ...props
33
+ }: MenuCheckboxItemProps): ReactElement {
34
+ const [isChecked, setChecked] = useControllableState<boolean>({
35
+ defaultValue: defaultChecked,
36
+ onChange: onCheckedChange,
37
+ value: isCheckedProp,
38
+ });
39
+ const slots = menuVariants();
40
+
41
+ return (
42
+ <MenuRow
43
+ accessibilityState={resolveMenuItemAccessibility({ isChecked, isDisabled, kind: "checkbox" })}
44
+ isDisabled={isDisabled}
45
+ kind="checkbox"
46
+ leading={
47
+ <View className={slots.indicator()}>
48
+ {isChecked ? <Icon className={slots.itemIcon()} color={MENU_INDICATOR_TOKEN} icon={IconCheckmark1} /> : null}
49
+ </View>
50
+ }
51
+ onSelect={() => setChecked(!isChecked)}
52
+ {...props}
53
+ iconPlacement="trailing"
54
+ />
55
+ );
56
+ }
57
+ MenuCheckboxItem.displayName = "DelacourUI.Menu.CheckboxItem";