@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,118 @@
1
+ import type { ReactElement } from "react";
2
+ import { View } from "react-native";
3
+ import Animated, { useAnimatedStyle } from "react-native-reanimated";
4
+ import { Icon, type IconComponent } from "../icon";
5
+ import { Pressable } from "../pressable";
6
+ import { Text } from "../text";
7
+ import { useFabActionItem, useFabGroupPart } from "./fab.context";
8
+ import { FAB_DIAL_STAGGER, FAB_FOREGROUND_TOKEN, fabVariants, resolveDialProgress } from "./fab.variants";
9
+
10
+ /** How far below its resting place a closed action sits, in points. */
11
+ const ACTION_RISE = 16;
12
+
13
+ /** An action's scale at the start of its window. */
14
+ const ACTION_START_SCALE = 0.6;
15
+
16
+ export type FabActionProps = {
17
+ icon: IconComponent;
18
+ /** The chip beside the button, and the button's accessibility label. */
19
+ label: string;
20
+ onPress: () => void;
21
+ isDisabled?: boolean;
22
+ /** Draws the glyph in the destructive colour. The surface stays the dial's. */
23
+ isDestructive?: boolean;
24
+ className?: string;
25
+ labelClassName?: string;
26
+ /** Lands on the round button. */
27
+ testID?: string;
28
+ };
29
+
30
+ /**
31
+ * One action in a `Fab.Group`'s dial: a small round button on the trigger's
32
+ * centre line, and a label chip on the side facing into the screen.
33
+ *
34
+ * Takes its glyph as a prop rather than composing it, unlike `Fab`: the action
35
+ * draws two things — a button and a chip — from one label and one icon, and
36
+ * the dial owns how both look.
37
+ *
38
+ * Pressing runs the action, then closes the dial. The chip presses too, since a
39
+ * finger aims at the words, but it is hidden from assistive technology so a
40
+ * screen reader meets each action once.
41
+ *
42
+ * Its stagger window comes from its place in the dial, read on the UI thread
43
+ * from the dial's one spring. Under reduced motion it fades in place.
44
+ */
45
+ export function FabAction({
46
+ icon,
47
+ label,
48
+ onPress,
49
+ isDisabled = false,
50
+ isDestructive = false,
51
+ className,
52
+ labelClassName,
53
+ testID,
54
+ }: FabActionProps): ReactElement {
55
+ const { progress, count, labelSide, size, haptic, isReducedMotion, close } = useFabGroupPart("Fab.Action");
56
+ const { index } = useFabActionItem("Fab.Action");
57
+ const slots = fabVariants({ size, labelSide, isDisabled });
58
+
59
+ const style = useAnimatedStyle(() => {
60
+ if (isReducedMotion) {
61
+ const value = progress.value;
62
+ return {
63
+ opacity: value < 0 ? 0 : value > 1 ? 1 : value,
64
+ transform: [{ translateY: 0 }, { scale: 1 }],
65
+ };
66
+ }
67
+ const local = resolveDialProgress({ open: progress.value, index, count, stagger: FAB_DIAL_STAGGER });
68
+ return {
69
+ opacity: local,
70
+ transform: [
71
+ { translateY: (1 - local) * ACTION_RISE },
72
+ { scale: ACTION_START_SCALE + (1 - ACTION_START_SCALE) * local },
73
+ ],
74
+ };
75
+ });
76
+
77
+ const handlePress = () => {
78
+ onPress();
79
+ close();
80
+ };
81
+
82
+ return (
83
+ <Animated.View className={slots.action({ className })} style={style}>
84
+ <Pressable
85
+ accessibilityElementsHidden
86
+ accessible={false}
87
+ className={slots.actionLabel()}
88
+ disabled={isDisabled}
89
+ feedback="fade"
90
+ importantForAccessibility="no-hide-descendants"
91
+ onPress={handlePress}
92
+ >
93
+ <Text className={slots.actionLabelText({ className: labelClassName })} numberOfLines={1}>
94
+ {label}
95
+ </Text>
96
+ </Pressable>
97
+ <View className={slots.actionAnchor()}>
98
+ <Pressable
99
+ accessibilityLabel={label}
100
+ accessibilityRole="button"
101
+ className={slots.actionButton()}
102
+ disabled={isDisabled}
103
+ feedback="scale"
104
+ haptic={haptic}
105
+ onPress={handlePress}
106
+ testID={testID}
107
+ >
108
+ <Icon
109
+ className={slots.actionIcon()}
110
+ color={isDestructive ? "destructive" : FAB_FOREGROUND_TOKEN.surface}
111
+ icon={icon}
112
+ />
113
+ </Pressable>
114
+ </View>
115
+ </Animated.View>
116
+ );
117
+ }
118
+ FabAction.displayName = "DelacourUI.Fab.Action";
@@ -0,0 +1,214 @@
1
+ import {
2
+ Children,
3
+ isValidElement,
4
+ type ReactElement,
5
+ type ReactNode,
6
+ use,
7
+ useCallback,
8
+ useEffect,
9
+ useMemo,
10
+ useState,
11
+ } from "react";
12
+ import { BackHandler, View, type ViewProps } from "react-native";
13
+ import Animated, {
14
+ useAnimatedStyle,
15
+ useReducedMotion,
16
+ useSharedValue,
17
+ withSpring,
18
+ withTiming,
19
+ } from "react-native-reanimated";
20
+ import { SafeAreaInsetsContext } from "react-native-safe-area-context";
21
+ import { scheduleOnRN } from "react-native-worklets";
22
+ import { useControllableState } from "../../hooks/use-controllable-state";
23
+ import { Icon, type IconComponent } from "../icon";
24
+ import { Pressable } from "../pressable";
25
+ import { Text } from "../text";
26
+ import { FabActionItemProvider, type FabGroupContextValue, FabGroupProvider } from "./fab.context";
27
+ import type { FabSharedProps } from "./fab.types";
28
+ import {
29
+ FAB_DEFAULT_OFFSET,
30
+ FAB_FOREGROUND_TOKEN,
31
+ FAB_OPEN_ROTATION_DEG,
32
+ FAB_REDUCED_MOTION_MS,
33
+ type FabPlacement,
34
+ fabVariants,
35
+ resolveFabPlacementStyle,
36
+ resolveLabelSide,
37
+ } from "./fab.variants";
38
+
39
+ /** One spring for the whole dial, with a little overshoot so it lands rather than stops. */
40
+ const DIAL_SPRING = { damping: 16, stiffness: 220, mass: 0.8 } as const;
41
+
42
+ export type FabGroupProps = Omit<ViewProps, "children"> &
43
+ FabSharedProps & {
44
+ /** `Fab.Action`s, written top to bottom as they appear above the trigger. */
45
+ children: ReactNode;
46
+ /** The trigger's glyph. Turns 45° as the dial opens — a plus becomes a cross. */
47
+ icon: IconComponent;
48
+ /** Extends the trigger with this text while the dial is closed. */
49
+ label?: string;
50
+ isOpen?: boolean;
51
+ defaultOpen?: boolean;
52
+ onOpenChange?: (isOpen: boolean) => void;
53
+ /** Default `bottom-end`. A group is always pinned. */
54
+ placement?: FabPlacement;
55
+ /** Turn the glyph 45° as the dial opens. Default `true`. */
56
+ isRotatedOnOpen?: boolean;
57
+ /** Lands on the trigger, the one element a test or a flow presses. */
58
+ testID?: string;
59
+ /** Required: the trigger is a glyph, and the dial it opens needs a name. */
60
+ accessibilityLabel: string;
61
+ className?: string;
62
+ };
63
+
64
+ /**
65
+ * A trigger that unfolds a dial of related actions, over a scrim.
66
+ *
67
+ * Renders a layer covering its parent — the scrim and the pinned dial — so it
68
+ * must be written in the screen's root container, as a sibling of the content
69
+ * it floats over. Nested inside a scroll view's content, the scrim would cover
70
+ * the content box rather than the screen.
71
+ *
72
+ * One spring drives the dial. Each action reads its own window of it, so
73
+ * closing halfway through opening runs the same cascade backwards. Actions are
74
+ * unmounted once the close settles, so a screen reader can never walk into a
75
+ * button nobody can see.
76
+ */
77
+ export function FabGroup({
78
+ children,
79
+ icon,
80
+ label,
81
+ isOpen: isOpenProp,
82
+ defaultOpen = false,
83
+ onOpenChange,
84
+ placement = "bottom-end",
85
+ offset = FAB_DEFAULT_OFFSET,
86
+ isSafeAreaAware = true,
87
+ size = "md",
88
+ variant = "primary",
89
+ isDisabled = false,
90
+ haptic = false,
91
+ isRotatedOnOpen = true,
92
+ accessibilityLabel,
93
+ className,
94
+ testID,
95
+ ...props
96
+ }: FabGroupProps): ReactElement {
97
+ const [isOpen, setIsOpen] = useControllableState({
98
+ value: isOpenProp,
99
+ defaultValue: defaultOpen,
100
+ onChange: onOpenChange,
101
+ });
102
+ const isReducedMotion = useReducedMotion();
103
+ const insets = use(SafeAreaInsetsContext);
104
+ const progress = useSharedValue(isOpen ? 1 : 0);
105
+ const [isMounted, setIsMounted] = useState(isOpen);
106
+
107
+ // Mount before the open spring starts, in the same render that opened it,
108
+ // so the first frame of the dial already has its actions.
109
+ if (isOpen && !isMounted) setIsMounted(true);
110
+
111
+ useEffect(() => {
112
+ const target = isOpen ? 1 : 0;
113
+ const onSettled = (isFinished?: boolean) => {
114
+ "worklet";
115
+ if (isFinished && target === 0) scheduleOnRN(setIsMounted, false);
116
+ };
117
+ progress.value = isReducedMotion
118
+ ? withTiming(target, { duration: FAB_REDUCED_MOTION_MS }, onSettled)
119
+ : withSpring(target, DIAL_SPRING, onSettled);
120
+ }, [isOpen, isReducedMotion, progress]);
121
+
122
+ useEffect(() => {
123
+ if (!isOpen) return;
124
+ const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
125
+ setIsOpen(false);
126
+ return true;
127
+ });
128
+ return () => subscription.remove();
129
+ }, [isOpen, setIsOpen]);
130
+
131
+ const actions = Children.toArray(children).filter(isValidElement);
132
+ const count = actions.length;
133
+ const labelSide = resolveLabelSide(placement);
134
+
135
+ const close = useCallback(() => setIsOpen(false), [setIsOpen]);
136
+ const toggle = () => setIsOpen(!isOpen);
137
+
138
+ const context = useMemo<FabGroupContextValue>(
139
+ () => ({ progress, isOpen, count, labelSide, size, haptic, isReducedMotion, close }),
140
+ [progress, isOpen, count, labelSide, size, haptic, isReducedMotion, close]
141
+ );
142
+
143
+ const isExtended = label !== undefined && !isOpen;
144
+ const slots = fabVariants({ size, variant, isExtended, isDisabled, labelSide });
145
+
146
+ const scrimStyle = useAnimatedStyle(() => {
147
+ const value = progress.value;
148
+ return { opacity: value < 0 ? 0 : value > 1 ? 1 : value };
149
+ });
150
+
151
+ const glyphStyle = useAnimatedStyle(() => {
152
+ const value = progress.value;
153
+ const clamped = value < 0 ? 0 : value > 1 ? 1 : value;
154
+ return { transform: [{ rotate: `${isRotatedOnOpen ? clamped * FAB_OPEN_ROTATION_DEG : 0}deg` }] };
155
+ });
156
+
157
+ const anchorStyle = resolveFabPlacementStyle({
158
+ placement,
159
+ offset,
160
+ insetBottom: isSafeAreaAware ? (insets?.bottom ?? 0) : 0,
161
+ });
162
+
163
+ // Written top to bottom, but the action nearest the trigger leads the
164
+ // cascade — so the last child is index 0.
165
+ const dial = isMounted
166
+ ? actions.map((action, position) => (
167
+ <FabActionItemProvider key={action.key ?? position} value={{ index: count - 1 - position }}>
168
+ {action}
169
+ </FabActionItemProvider>
170
+ ))
171
+ : null;
172
+
173
+ return (
174
+ <FabGroupProvider value={context}>
175
+ <View accessibilityViewIsModal={isOpen} className="absolute inset-0" pointerEvents="box-none">
176
+ {isMounted ? (
177
+ <Animated.View className={slots.scrim()} style={scrimStyle}>
178
+ <Pressable
179
+ accessibilityLabel="Close"
180
+ accessibilityRole="button"
181
+ className="flex-1"
182
+ feedback="none"
183
+ onPress={close}
184
+ />
185
+ </Animated.View>
186
+ ) : null}
187
+ <View className={slots.dial({ className })} pointerEvents="box-none" style={anchorStyle} {...props}>
188
+ {dial}
189
+ <Pressable
190
+ accessibilityLabel={accessibilityLabel}
191
+ accessibilityRole="button"
192
+ accessibilityState={{ expanded: isOpen }}
193
+ className={slots.root()}
194
+ disabled={isDisabled}
195
+ feedback="scale"
196
+ haptic={haptic}
197
+ onPress={toggle}
198
+ testID={testID}
199
+ >
200
+ <Animated.View style={glyphStyle}>
201
+ <Icon className={slots.icon()} color={FAB_FOREGROUND_TOKEN[variant]} icon={icon} />
202
+ </Animated.View>
203
+ {isExtended ? (
204
+ <Text className={slots.label()} numberOfLines={1}>
205
+ {label}
206
+ </Text>
207
+ ) : null}
208
+ </Pressable>
209
+ </View>
210
+ </View>
211
+ </FabGroupProvider>
212
+ );
213
+ }
214
+ FabGroup.displayName = "DelacourUI.Fab.Group";
@@ -0,0 +1,18 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import { Text, type TextProps } from "../text";
3
+ import { useFabPart } from "./fab.context";
4
+ import { fabVariants } from "./fab.variants";
5
+
6
+ export type FabLabelProps = Omit<TextProps, "children"> & { children: ReactNode; className?: string };
7
+
8
+ /**
9
+ * An extended fab's text.
10
+ *
11
+ * Carries its own colour, read from the fab's variant: a `View` does not cascade
12
+ * colour to a `Text`, so a colour on the root would be lost.
13
+ */
14
+ export function FabLabel({ className, ...props }: FabLabelProps): ReactElement {
15
+ const { variant } = useFabPart("Fab.Label");
16
+ return <Text className={fabVariants({ variant }).label({ className })} numberOfLines={1} {...props} />;
17
+ }
18
+ FabLabel.displayName = "DelacourUI.Fab.Label";
@@ -0,0 +1,125 @@
1
+ import { createContext, type ReactElement, type ReactNode, use } from "react";
2
+ import type { SharedValue } from "react-native-reanimated";
3
+ import type { HapticFeedback } from "../pressable/pressable";
4
+ import type { FabLabelSide, FabSize, FabVariant } from "./fab.variants";
5
+
6
+ export type FabContextValue = {
7
+ size: FabSize;
8
+ variant: FabVariant;
9
+ /** A stadium with a label, rather than a circle. */
10
+ isExtended: boolean;
11
+ isDisabled: boolean;
12
+ };
13
+
14
+ const FabContext = createContext<FabContextValue | null>(null);
15
+
16
+ /**
17
+ * Supplies the enclosing fab's axes to its subtree, so `Fab.Label` and a custom
18
+ * child can match it without the root passing props down.
19
+ *
20
+ * A leaf of its own, importing nothing but types, so a part reads it without
21
+ * importing `./fab` and closing a cycle.
22
+ */
23
+ export function FabProvider({ value, children }: { value: FabContextValue; children: ReactNode }): ReactElement {
24
+ return <FabContext value={value}>{children}</FabContext>;
25
+ }
26
+ FabProvider.displayName = "DelacourUI.Fab.Provider";
27
+
28
+ /** The enclosing fab's axes, or null outside a `<Fab>`. */
29
+ export function useFabContext(): FabContextValue | null {
30
+ return use(FabContext);
31
+ }
32
+
33
+ /** Reads the enclosing fab's axes. Throws outside a `<Fab>`. */
34
+ export function useFab(): FabContextValue {
35
+ const context = useFabContext();
36
+ if (!context) throw new Error("useFab must be called inside a <Fab>.");
37
+ return context;
38
+ }
39
+
40
+ /** The enclosing fab's axes, for a part that cannot work without one. Internal. */
41
+ export function useFabPart(component: string): FabContextValue {
42
+ const context = useFabContext();
43
+ if (!context) throw new Error(`${component} must be rendered inside a <Fab>.`);
44
+ return context;
45
+ }
46
+
47
+ export type FabGroupContextValue = {
48
+ /** The dial's one spring, 0 closed → 1 open. It overshoots; readers clamp. */
49
+ progress: SharedValue<number>;
50
+ isOpen: boolean;
51
+ /** How many actions the dial holds — each action's stagger window depends on it. */
52
+ count: number;
53
+ labelSide: FabLabelSide;
54
+ /** The trigger's size. Actions are always `sm`, centred on the trigger's centre line. */
55
+ size: FabSize;
56
+ haptic: false | HapticFeedback;
57
+ /** Fade in place instead of rising with a stagger. */
58
+ isReducedMotion: boolean;
59
+ close: () => void;
60
+ };
61
+
62
+ const FabGroupContext = createContext<FabGroupContextValue | null>(null);
63
+
64
+ /** Supplies the dial's spring and state to every action in it. */
65
+ export function FabGroupProvider({
66
+ value,
67
+ children,
68
+ }: {
69
+ value: FabGroupContextValue;
70
+ children: ReactNode;
71
+ }): ReactElement {
72
+ return <FabGroupContext value={value}>{children}</FabGroupContext>;
73
+ }
74
+ FabGroupProvider.displayName = "DelacourUI.Fab.Group.Provider";
75
+
76
+ /** The enclosing dial, or null outside a `<Fab.Group>`. */
77
+ export function useFabGroupContext(): FabGroupContextValue | null {
78
+ return use(FabGroupContext);
79
+ }
80
+
81
+ /** Reads the enclosing dial. Throws outside a `<Fab.Group>`. */
82
+ export function useFabGroup(): FabGroupContextValue {
83
+ const context = useFabGroupContext();
84
+ if (!context) throw new Error("useFabGroup must be called inside a <Fab.Group>.");
85
+ return context;
86
+ }
87
+
88
+ /** The enclosing dial, for a part that cannot work without one. Internal. */
89
+ export function useFabGroupPart(component: string): FabGroupContextValue {
90
+ const context = useFabGroupContext();
91
+ if (!context) throw new Error(`${component} must be rendered inside a <Fab.Group>.`);
92
+ return context;
93
+ }
94
+
95
+ export type FabActionItemContextValue = {
96
+ /** 0 is the action nearest the trigger; the dial unfolds outward from it. */
97
+ index: number;
98
+ };
99
+
100
+ const FabActionItemContext = createContext<FabActionItemContextValue | null>(null);
101
+
102
+ /**
103
+ * Supplies one action its place in the dial.
104
+ *
105
+ * The group wraps each child in one of these rather than cloning an `index`
106
+ * prop onto it: a provider reaches an action a caller wrapped in a fragment or
107
+ * produced from a helper, and adds no view to the layout.
108
+ */
109
+ export function FabActionItemProvider({
110
+ value,
111
+ children,
112
+ }: {
113
+ value: FabActionItemContextValue;
114
+ children: ReactNode;
115
+ }): ReactElement {
116
+ return <FabActionItemContext value={value}>{children}</FabActionItemContext>;
117
+ }
118
+ FabActionItemProvider.displayName = "DelacourUI.Fab.Group.ItemProvider";
119
+
120
+ /** This action's place in the dial. Throws outside a `<Fab.Group>`. Internal. */
121
+ export function useFabActionItem(component: string): FabActionItemContextValue {
122
+ const context = use(FabActionItemContext);
123
+ if (!context) throw new Error(`${component} must be rendered inside a <Fab.Group>.`);
124
+ return context;
125
+ }
@@ -0,0 +1,156 @@
1
+ import { Children, type ReactElement, type ReactNode, use, useEffect, useMemo } from "react";
2
+ import { View } from "react-native";
3
+ import { SafeAreaInsetsContext } from "react-native-safe-area-context";
4
+ import { IconDefaultsProvider } from "../icon";
5
+ import { Pressable, type PressableProps } from "../pressable";
6
+ import { TextClassProvider } from "../text/text.context";
7
+ import { type FabContextValue, FabProvider } from "./fab.context";
8
+ import type { FabSharedProps } from "./fab.types";
9
+ import {
10
+ FAB_DEFAULT_OFFSET,
11
+ FAB_FOREGROUND_TOKEN,
12
+ type FabPlacement,
13
+ fabVariants,
14
+ resolveFabPlacementStyle,
15
+ } from "./fab.variants";
16
+ import { FabAction } from "./fab-action";
17
+ import { FabGroup } from "./fab-group";
18
+ import { FabLabel } from "./fab-label";
19
+
20
+ export type FabProps = Omit<PressableProps, "busy" | "children" | "disabled" | "haptic"> &
21
+ FabSharedProps & {
22
+ /** A stadium holding a `Fab.Label` beside the icon, rather than a circle. */
23
+ isExtended?: boolean;
24
+ /**
25
+ * Pins the fab to the bottom of its nearest positioned ancestor. Omitted,
26
+ * the fab sits in flow like any other view.
27
+ */
28
+ placement?: FabPlacement;
29
+ /** Required in practice for a round fab — there is no text to read. */
30
+ accessibilityLabel?: string;
31
+ children?: ReactNode;
32
+ };
33
+
34
+ function FabRoot({
35
+ size = "md",
36
+ variant = "primary",
37
+ isExtended = false,
38
+ placement,
39
+ offset = FAB_DEFAULT_OFFSET,
40
+ isSafeAreaAware = true,
41
+ isDisabled = false,
42
+ haptic = false,
43
+ feedback = "scale",
44
+ accessibilityLabel,
45
+ className,
46
+ children,
47
+ ...props
48
+ }: FabProps): ReactElement {
49
+ const insets = use(SafeAreaInsetsContext);
50
+
51
+ useEffect(() => {
52
+ if (process.env.NODE_ENV === "production") return;
53
+ if (!isExtended && !accessibilityLabel) {
54
+ console.warn(
55
+ "Fab: a round fab has no text for a screen reader to read. Pass `accessibilityLabel`, or `isExtended` with a `Fab.Label`."
56
+ );
57
+ }
58
+ }, [isExtended, accessibilityLabel]);
59
+
60
+ const context = useMemo<FabContextValue>(
61
+ () => ({ size, variant, isExtended, isDisabled }),
62
+ [size, variant, isExtended, isDisabled]
63
+ );
64
+
65
+ const slots = fabVariants({ size, variant, isExtended, isDisabled });
66
+ const iconClassName = slots.icon();
67
+ const iconDefaults = useMemo(
68
+ () => ({ className: iconClassName, color: FAB_FOREGROUND_TOKEN[variant] }),
69
+ [iconClassName, variant]
70
+ );
71
+
72
+ const button = (
73
+ <FabProvider value={context}>
74
+ <Pressable
75
+ accessibilityLabel={accessibilityLabel}
76
+ accessibilityRole="button"
77
+ className={slots.root({ className })}
78
+ disabled={isDisabled}
79
+ feedback={feedback}
80
+ haptic={haptic}
81
+ {...props}
82
+ >
83
+ <IconDefaultsProvider value={iconDefaults}>
84
+ <TextClassProvider value={slots.label()}>{wrapTextChildren(children)}</TextClassProvider>
85
+ </IconDefaultsProvider>
86
+ </Pressable>
87
+ </FabProvider>
88
+ );
89
+
90
+ if (!placement) return button;
91
+
92
+ const style = resolveFabPlacementStyle({
93
+ placement,
94
+ offset,
95
+ insetBottom: isSafeAreaAware ? (insets?.bottom ?? 0) : 0,
96
+ });
97
+
98
+ return (
99
+ <View pointerEvents="box-none" style={style}>
100
+ {button}
101
+ </View>
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Wraps bare text children in a `Fab.Label` — React Native cannot render a
107
+ * string outside a `<Text>`.
108
+ */
109
+ function wrapTextChildren(children: ReactNode): ReactNode {
110
+ return Children.map(children, (child) =>
111
+ typeof child === "string" || typeof child === "number" ? <FabLabel>{child}</FabLabel> : child
112
+ );
113
+ }
114
+
115
+ /**
116
+ * One primary action floating over the screen it belongs to — "New note" over
117
+ * a list.
118
+ *
119
+ * The icon is composed, the way a `Button`'s is: the fab publishes its icon
120
+ * size and its variant's foreground through `IconDefaultsProvider`, so a bare
121
+ * `<Icon icon={IconPlusLarge} />` comes out right with nothing said.
122
+ *
123
+ * `placement` pins it to the bottom of its nearest positioned ancestor, lifted
124
+ * by `offset` and, unless told otherwise, the bottom safe-area inset. Omitted,
125
+ * the fab sits in flow. Pad the bottom of a list it floats over by the fab's
126
+ * height plus its offset, or the last row hides behind it.
127
+ *
128
+ * `Fab.Group` turns the trigger into a dial of related actions that unfold
129
+ * above it.
130
+ *
131
+ * @example
132
+ * <Fab accessibilityLabel="New note" onPress={compose} placement="bottom-end">
133
+ * <Icon icon={IconPlusLarge} />
134
+ * </Fab>
135
+ *
136
+ * @example
137
+ * <Fab isExtended onPress={write} placement="bottom-end">
138
+ * <Icon icon={IconPencil} />
139
+ * <Fab.Label>Write</Fab.Label>
140
+ * </Fab>
141
+ *
142
+ * @example
143
+ * <Fab.Group accessibilityLabel="Add something" icon={IconPlusLarge}>
144
+ * <Fab.Action icon={IconImages} label="Photo" onPress={addPhoto} />
145
+ * <Fab.Action icon={IconTrashCan} isDestructive label="Empty drafts" onPress={empty} />
146
+ * </Fab.Group>
147
+ */
148
+ export const Fab = Object.assign(FabRoot, {
149
+ /** An extended fab's text, coloured for the fab's variant. */
150
+ Label: FabLabel,
151
+ /** A trigger that unfolds a dial of related actions, over a scrim. */
152
+ Group: FabGroup,
153
+ /** One action in a `Fab.Group`'s dial: a small round button and a label chip. */
154
+ Action: FabAction,
155
+ displayName: "DelacourUI.Fab",
156
+ });
@@ -0,0 +1,17 @@
1
+ import type { HapticFeedback } from "../pressable/pressable";
2
+ import type { FabSize, FabVariant } from "./fab.variants";
3
+
4
+ /** The axes a `Fab` and a `Fab.Group`'s trigger share. */
5
+ export type FabSharedProps = {
6
+ /** 44, 56 or 64pt. Default `md`. */
7
+ size?: FabSize;
8
+ /** Default `primary`. */
9
+ variant?: FabVariant;
10
+ isDisabled?: boolean;
11
+ /** Haptic played on press. Off by default. */
12
+ haptic?: false | HapticFeedback;
13
+ /** Distance from the bottom and side edges, before the safe-area inset. Default 16. */
14
+ offset?: number;
15
+ /** Add the bottom safe-area inset to `offset`. Default `true`. Ignored in flow. */
16
+ isSafeAreaAware?: boolean;
17
+ };