@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,84 @@
1
+ import { type ReactElement, type ReactNode, useMemo } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { type EmptyStateContextValue, EmptyStateProvider } from "./empty-state.context";
4
+ import { type EmptyStateSize, type EmptyStateVariant, emptyStateVariants } from "./empty-state.variants";
5
+ import { EmptyStateContent } from "./empty-state-content";
6
+ import { EmptyStateDescription } from "./empty-state-description";
7
+ import { EmptyStateHeader } from "./empty-state-header";
8
+ import { EmptyStateMedia } from "./empty-state-media";
9
+ import { EmptyStateTitle } from "./empty-state-title";
10
+
11
+ export type EmptyStateProps = ViewProps & {
12
+ /** `default` grows into its parent; `card` is a self-contained dashed block. */
13
+ variant?: EmptyStateVariant;
14
+ /** Drives the padding, both gaps, the media, the glyph and both type scales. */
15
+ size?: EmptyStateSize;
16
+ className?: string;
17
+ children?: ReactNode;
18
+ };
19
+
20
+ function EmptyStateRoot({
21
+ variant = "default",
22
+ size = "md",
23
+ className,
24
+ children,
25
+ ...props
26
+ }: EmptyStateProps): ReactElement {
27
+ const context = useMemo<EmptyStateContextValue>(() => ({ size, variant }), [size, variant]);
28
+
29
+ return (
30
+ <EmptyStateProvider value={context}>
31
+ <View className={emptyStateVariants({ size, variant }).root({ className })} {...props}>
32
+ {children}
33
+ </View>
34
+ </EmptyStateProvider>
35
+ );
36
+ }
37
+
38
+ /**
39
+ * A placeholder for a list or a screen with nothing in it yet: a picture, a
40
+ * title, a line of explanation, and the action that fixes it.
41
+ *
42
+ * `size` and `variant` reach the parts through context, so the title picks its
43
+ * own type scale and a bare `Icon` in `EmptyState.Media` its own size and
44
+ * colour. Actions are composed — put a `Button` in `EmptyState.Content`.
45
+ *
46
+ * `default` grows to fill a bounded parent and centres itself there, without
47
+ * collapsing inside a `ScrollView`. `card` is a dashed block for an empty
48
+ * section sitting beside populated ones.
49
+ *
50
+ * @example
51
+ * <EmptyState>
52
+ * <EmptyState.Header>
53
+ * <EmptyState.Media variant="icon">
54
+ * <Icon icon={IconInbox} />
55
+ * </EmptyState.Media>
56
+ * <EmptyState.Title>No messages</EmptyState.Title>
57
+ * <EmptyState.Description>New conversations will show up here.</EmptyState.Description>
58
+ * </EmptyState.Header>
59
+ * <EmptyState.Content>
60
+ * <Button onPress={compose}>New message</Button>
61
+ * </EmptyState.Content>
62
+ * </EmptyState>
63
+ *
64
+ * @example
65
+ * <EmptyState size="sm" variant="card">
66
+ * <EmptyState.Header>
67
+ * <EmptyState.Title>No results</EmptyState.Title>
68
+ * <EmptyState.Description>Try a different search.</EmptyState.Description>
69
+ * </EmptyState.Header>
70
+ * </EmptyState>
71
+ */
72
+ export const EmptyState = Object.assign(EmptyStateRoot, {
73
+ /** The centred column holding the media, the title and the description. */
74
+ Header: EmptyStateHeader,
75
+ /** The picture above the title. A bare `Icon` inside it inherits size and colour; `variant="icon"` tints a box behind it. */
76
+ Media: EmptyStateMedia,
77
+ /** The heading, announced as a header. Carries its own colour — a `View` cannot cascade one to a `Text`. */
78
+ Title: EmptyStateTitle,
79
+ /** The supporting line under the title, a step down in scale and on the muted token. */
80
+ Description: EmptyStateDescription,
81
+ /** The action slot under the header: a centred row that wraps. */
82
+ Content: EmptyStateContent,
83
+ displayName: "DelacourUI.EmptyState",
84
+ });
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react";
2
+ import type { TextProps, ViewProps } from "react-native";
3
+
4
+ /**
5
+ * The shape of the header and content slots.
6
+ *
7
+ * Shared by `EmptyState.Header` and `EmptyState.Content`, and extended by
8
+ * `EmptyState.Media`, so it lives in a leaf rather than in one of them
9
+ * arbitrarily.
10
+ */
11
+ export type EmptyStateSlotProps = ViewProps & { className?: string; children?: ReactNode };
12
+
13
+ /** The shape of the two text parts, `EmptyState.Title` and `EmptyState.Description`. */
14
+ export type EmptyStateTextProps = TextProps & { className?: string };
@@ -0,0 +1,245 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { declaredTokens } from "../../styles/theme-tokens.test";
3
+ import { ICON_SIZE_TOKENS } from "../../styles/tokens";
4
+ import {
5
+ EMPTY_STATE_MEDIA_FOREGROUND_TOKEN,
6
+ EMPTY_STATE_MEDIA_VARIANTS,
7
+ EMPTY_STATE_SIZES,
8
+ EMPTY_STATE_VARIANTS,
9
+ emptyStateVariants,
10
+ } from "./empty-state.variants";
11
+
12
+ const LIGHT = declaredTokens("light");
13
+ const DARK = declaredTokens("dark");
14
+
15
+ /** Tailwind's spacing step a class string sets a utility from, e.g. `p` or `gap`. */
16
+ function step(cls: string, utility: string): number {
17
+ return Number(cls.match(new RegExp(`\\b${utility}-([\\d.]+)\\b`))?.[1]);
18
+ }
19
+
20
+ /**
21
+ * Position of a class string's font size on the type scale, largest last.
22
+ *
23
+ * Compared by step rather than points so a retune in `tokens.css` does not
24
+ * break a test that only means "bigger".
25
+ */
26
+ const TEXT_SCALE = ["text-xs", "text-sm", "text-base", "text-lg", "text-xl", "text-2xl"] as const;
27
+ function textStep(cls: string): number {
28
+ return TEXT_SCALE.findIndex((name) => new RegExp(`\\b${name}\\b`).test(cls));
29
+ }
30
+
31
+ /** Position of a slot's `size-icon-*` token on the shared icon scale. */
32
+ function iconStep(cls: string): number {
33
+ const token = cls.match(/\bsize-(icon-[\w-]+)\b/)?.[1];
34
+ return ICON_SIZE_TOKENS.indexOf(token as (typeof ICON_SIZE_TOKENS)[number]);
35
+ }
36
+
37
+ /** Every slot that is a `View` — none of them may carry a text colour. */
38
+ const VIEW_SLOTS = ["root", "header", "media", "content"] as const;
39
+
40
+ /** A strictly ascending, all-distinct scale. */
41
+ function expectAscending(scale: readonly number[]): void {
42
+ expect(scale.every(Number.isFinite)).toBe(true);
43
+ expect(new Set(scale).size).toBe(scale.length);
44
+ expect([...scale]).toEqual([...scale].sort((a, b) => a - b));
45
+ }
46
+
47
+ describe("the theme.css reader", () => {
48
+ test("finds both variants", () => {
49
+ expect(LIGHT.size).toBeGreaterThan(0);
50
+ expect(DARK.size).toBeGreaterThan(0);
51
+ });
52
+ });
53
+
54
+ describe("emptyStateVariants root slot", () => {
55
+ test("defaults to the md default variant", () => {
56
+ const cls = emptyStateVariants().root();
57
+ expect(cls).toContain("bg-transparent");
58
+ expect(cls).toContain(`p-${step(emptyStateVariants({ size: "md" }).root(), "p")}`);
59
+ });
60
+
61
+ test("centres its content on both axes, at every variant", () => {
62
+ for (const variant of EMPTY_STATE_VARIANTS) {
63
+ const cls = emptyStateVariants({ variant }).root();
64
+ expect(cls).toContain("items-center");
65
+ expect(cls).toContain("justify-center");
66
+ }
67
+ });
68
+
69
+ // `grow`, never `flex-1`. `flex-1` is a zero basis, so inside a ScrollView's
70
+ // content container — which has no height to grow into — the empty state
71
+ // collapses to nothing. `grow` keeps the content height as a floor and fills
72
+ // whatever a bounded parent has spare.
73
+ test("the default variant grows into its parent without a zero basis", () => {
74
+ const cls = emptyStateVariants({ variant: "default" }).root();
75
+ expect(cls).toMatch(/\bgrow\b/);
76
+ expect(cls).not.toMatch(/\bflex-1\b/);
77
+ });
78
+
79
+ test("the card variant is a card-shaped surface with a dashed border", () => {
80
+ const cls = emptyStateVariants({ variant: "card" }).root();
81
+ expect(cls).toContain("bg-card");
82
+ expect(cls).toContain("border-border");
83
+ expect(cls).toContain("border-dashed");
84
+ expect(cls).not.toMatch(/\bgrow\b/);
85
+ });
86
+
87
+ // The border is in the box on both variants, so switching to `card` changes
88
+ // a colour and not the layout.
89
+ test("reserves the border on every variant", () => {
90
+ for (const variant of EMPTY_STATE_VARIANTS) {
91
+ expect(emptyStateVariants({ variant }).root()).toMatch(/\bborder\b/);
92
+ }
93
+ expect(emptyStateVariants({ variant: "default" }).root()).toContain("border-transparent");
94
+ });
95
+
96
+ test("a card is rounded-lg at md and lg, and steps down to rounded-md at sm", () => {
97
+ expect(emptyStateVariants({ variant: "card", size: "sm" }).root()).toContain("rounded-md");
98
+ expect(emptyStateVariants({ variant: "card", size: "md" }).root()).toContain("rounded-lg");
99
+ expect(emptyStateVariants({ variant: "card", size: "lg" }).root()).toContain("rounded-lg");
100
+ });
101
+
102
+ test("gives the two variants distinct treatments", () => {
103
+ const seen = new Set(EMPTY_STATE_VARIANTS.map((variant) => emptyStateVariants({ variant }).root()));
104
+ expect(seen.size).toBe(EMPTY_STATE_VARIANTS.length);
105
+ });
106
+
107
+ test("merges an incoming className last", () => {
108
+ const cls = emptyStateVariants({ variant: "card" }).root({ className: "bg-muted" });
109
+ expect(cls).toContain("bg-muted");
110
+ expect(cls).not.toContain("bg-card");
111
+ });
112
+ });
113
+
114
+ describe("emptyStateVariants size axis", () => {
115
+ test("scales padding and the gap between header and content together", () => {
116
+ const roots = EMPTY_STATE_SIZES.map((size) => emptyStateVariants({ size }).root());
117
+ expectAscending(roots.map((cls) => step(cls, "p")));
118
+ expectAscending(roots.map((cls) => step(cls, "gap")));
119
+ });
120
+
121
+ test("scales the header's own gap", () => {
122
+ expectAscending(EMPTY_STATE_SIZES.map((size) => step(emptyStateVariants({ size }).header(), "gap")));
123
+ });
124
+
125
+ test("scales the title and description type together, title always the larger", () => {
126
+ const titles = EMPTY_STATE_SIZES.map((size) => textStep(emptyStateVariants({ size }).title()));
127
+ const descriptions = EMPTY_STATE_SIZES.map((size) => textStep(emptyStateVariants({ size }).description()));
128
+
129
+ expect(titles.every((index) => index >= 0)).toBe(true);
130
+ expect(descriptions.every((index) => index >= 0)).toBe(true);
131
+ expect([...titles]).toEqual([...titles].sort((a, b) => a - b));
132
+ expect([...descriptions]).toEqual([...descriptions].sort((a, b) => a - b));
133
+ expect(new Set(titles).size).toBe(EMPTY_STATE_SIZES.length);
134
+
135
+ for (const [index, title] of titles.entries()) {
136
+ expect(title).toBeGreaterThan(descriptions[index] ?? Number.POSITIVE_INFINITY);
137
+ }
138
+ });
139
+
140
+ // Padding, never a height. A fixed height clips the description at a large
141
+ // accessibility font step instead of letting the block grow.
142
+ test("takes no height", () => {
143
+ for (const size of EMPTY_STATE_SIZES) {
144
+ expect(emptyStateVariants({ size }).root()).not.toMatch(/\bh-/);
145
+ }
146
+ });
147
+ });
148
+
149
+ describe("emptyStateVariants header slot", () => {
150
+ test("is a centred column", () => {
151
+ const cls = emptyStateVariants().header();
152
+ expect(cls).toContain("items-center");
153
+ expect(cls).not.toContain("flex-row");
154
+ });
155
+ });
156
+
157
+ describe("emptyStateVariants media slot", () => {
158
+ test("the icon media is a tinted, rounded square that scales with size", () => {
159
+ const boxes = EMPTY_STATE_SIZES.map((size) => emptyStateVariants({ size, media: "icon" }).media());
160
+ for (const cls of boxes) {
161
+ expect(cls).toContain("bg-muted");
162
+ expect(cls).toMatch(/\brounded-/);
163
+ }
164
+ expectAscending(boxes.map((cls) => step(cls, "size")));
165
+ });
166
+
167
+ test("the default media paints nothing and takes no size, so an illustration sizes itself", () => {
168
+ for (const size of EMPTY_STATE_SIZES) {
169
+ const cls = emptyStateVariants({ size, media: "default" }).media();
170
+ expect(cls).not.toMatch(/\bbg-/);
171
+ expect(cls).not.toMatch(/\bsize-/);
172
+ }
173
+ });
174
+
175
+ // A glyph in an empty state has to sit on the scale `Icon`, `Spinner` and
176
+ // `Button` share rather than carry a private number.
177
+ test("gives every size a distinct icon token, increasing with it", () => {
178
+ for (const media of EMPTY_STATE_MEDIA_VARIANTS) {
179
+ const steps = EMPTY_STATE_SIZES.map((size) => iconStep(emptyStateVariants({ size, media }).mediaIcon()));
180
+ expect(steps.every((index) => index >= 0)).toBe(true);
181
+ expectAscending(steps);
182
+ }
183
+ });
184
+
185
+ // Media and title are one visual unit only if the glyph clears the title by
186
+ // more than the title clears its description.
187
+ test("sets the media apart from the title by more than the header gap", () => {
188
+ for (const size of EMPTY_STATE_SIZES) {
189
+ const slots = emptyStateVariants({ size });
190
+ expect(step(slots.media(), "mb")).toBeGreaterThan(0);
191
+ }
192
+ });
193
+ });
194
+
195
+ describe("text colour", () => {
196
+ test("lives on the title and description, never on a View slot", () => {
197
+ for (const variant of EMPTY_STATE_VARIANTS) {
198
+ for (const size of EMPTY_STATE_SIZES) {
199
+ const slots = emptyStateVariants({ variant, size });
200
+ for (const slot of VIEW_SLOTS) {
201
+ expect(slots[slot]()).not.toMatch(/\btext-(?!center\b)/);
202
+ }
203
+ expect(slots.title()).toContain("text-foreground");
204
+ expect(slots.description()).toContain("text-muted-foreground");
205
+ }
206
+ }
207
+ });
208
+
209
+ test("title and description are centred", () => {
210
+ const slots = emptyStateVariants();
211
+ expect(slots.title()).toContain("text-center");
212
+ expect(slots.description()).toContain("text-center");
213
+ });
214
+ });
215
+
216
+ describe("EMPTY_STATE_MEDIA_FOREGROUND_TOKEN", () => {
217
+ test("names a token for every media variant", () => {
218
+ for (const media of EMPTY_STATE_MEDIA_VARIANTS) {
219
+ expect(EMPTY_STATE_MEDIA_FOREGROUND_TOKEN[media]).toBeTruthy();
220
+ }
221
+ });
222
+
223
+ test("names only tokens both themes declare", () => {
224
+ for (const media of EMPTY_STATE_MEDIA_VARIANTS) {
225
+ const token = EMPTY_STATE_MEDIA_FOREGROUND_TOKEN[media];
226
+ expect(LIGHT.has(token)).toBe(true);
227
+ expect(DARK.has(token)).toBe(true);
228
+ }
229
+ });
230
+
231
+ // `muted-foreground` on `bg-muted` is two greys a step apart. The tinted box
232
+ // already says "decoration", so the glyph inside it takes full contrast.
233
+ test("draws the icon media's glyph at full contrast on its muted box", () => {
234
+ expect(EMPTY_STATE_MEDIA_FOREGROUND_TOKEN.icon).toBe("foreground");
235
+ });
236
+ });
237
+
238
+ describe("emptyStateVariants content slot", () => {
239
+ test("lays actions out in a centred, wrapping row", () => {
240
+ const cls = emptyStateVariants().content();
241
+ expect(cls).toContain("flex-row");
242
+ expect(cls).toContain("flex-wrap");
243
+ expect(cls).toContain("justify-center");
244
+ });
245
+ });
@@ -0,0 +1,132 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+
4
+ /** How the block sits on the screen: filling its parent, or as a self-contained card. */
5
+ export const EMPTY_STATE_VARIANTS = ["default", "card"] as const;
6
+
7
+ export const EMPTY_STATE_SIZES = ["sm", "md", "lg"] as const;
8
+
9
+ /** What `EmptyState.Media` draws behind its content: nothing, or a tinted box for a glyph. */
10
+ export const EMPTY_STATE_MEDIA_VARIANTS = ["default", "icon"] as const;
11
+
12
+ export type EmptyStateVariant = (typeof EMPTY_STATE_VARIANTS)[number];
13
+ export type EmptyStateSize = (typeof EMPTY_STATE_SIZES)[number];
14
+ export type EmptyStateMediaVariant = (typeof EMPTY_STATE_MEDIA_VARIANTS)[number];
15
+
16
+ /**
17
+ * Theme token an `Icon` composed into `EmptyState.Media` inherits.
18
+ *
19
+ * A colour that reaches an SVG paint prop cannot be a class, so it travels as a
20
+ * token through `IconDefaultsProvider` — the same route `Button` and `ListGroup`
21
+ * use. A bare glyph is `muted-foreground`, because an empty state's picture is
22
+ * decoration and must not outweigh the title. Inside the tinted box it takes
23
+ * full contrast: `muted-foreground` on `bg-muted` is two greys a step apart.
24
+ */
25
+ export const EMPTY_STATE_MEDIA_FOREGROUND_TOKEN: Record<EmptyStateMediaVariant, string> = {
26
+ default: "muted-foreground",
27
+ icon: "foreground",
28
+ };
29
+
30
+ /**
31
+ * Styling for every part of an empty state.
32
+ *
33
+ * One slotted `tv()` because size is not decoration here: it drives the
34
+ * padding, both gaps, the media box, the glyph inside it and both type scales.
35
+ * Those belong on one axis rather than in seven numbers that drift apart.
36
+ *
37
+ * `default` uses `grow`, never `flex-1`. `flex-1` is a zero flex basis, so inside
38
+ * a `ScrollView`'s content container — which has no height to grow into — the
39
+ * block would collapse to nothing. `grow` keeps its content height as a floor
40
+ * and fills whatever spare height a bounded parent has, which is what "fills its
41
+ * container" has to mean on both.
42
+ *
43
+ * `card` is a dashed outline on the card surface. The dash is the conventional
44
+ * mark of a slot waiting for content, and it keeps an embedded empty state from
45
+ * reading as one more populated card beside real ones. Its corner follows the
46
+ * card step (`rounded-lg`), stepping down to `rounded-md` at `sm` the way
47
+ * `ListGroup` and `Accordion` do. The border is reserved on both variants, so
48
+ * switching to `card` changes a colour rather than the layout.
49
+ *
50
+ * A size is padding, never a height. The description respects OS font scaling,
51
+ * and a fixed height would clip it at a large accessibility step.
52
+ *
53
+ * The glyph steps with size on the shared icon scale, the same three steps
54
+ * inside the tinted box and out of it — the scale tops out at `icon-2xl`, so a
55
+ * bare glyph a step larger would leave `md` and `lg` identical.
56
+ *
57
+ * The media keeps a bottom margin on top of the header's gap, so the picture
58
+ * clears the title by more than the title clears its description — the two
59
+ * lines read as one unit and the glyph as something above them.
60
+ *
61
+ * No `View` slot carries a `text-*` utility: a React Native `View` does not
62
+ * cascade colour to a `Text` descendant, so colour lives on `title` and
63
+ * `description`, and a glyph's colour on {@link EMPTY_STATE_MEDIA_FOREGROUND_TOKEN}.
64
+ *
65
+ * Free of React Native imports so it stays unit-testable — `bun test` cannot
66
+ * parse React Native's Flow-typed source. See AGENTS.md.
67
+ */
68
+ export const emptyStateVariants = tv({
69
+ slots: {
70
+ root: "w-full items-center justify-center border",
71
+ header: "w-full max-w-sm items-center",
72
+ media: "items-center justify-center",
73
+ /** Edge length an `Icon` composed into the media inherits. */
74
+ mediaIcon: "",
75
+ title: "text-center font-semibold text-foreground",
76
+ description: "text-center text-muted-foreground",
77
+ content: "w-full max-w-sm flex-row flex-wrap items-center justify-center",
78
+ },
79
+ variants: {
80
+ variant: {
81
+ default: { root: "grow border-transparent bg-transparent" },
82
+ card: { root: "border-dashed border-border bg-card" },
83
+ },
84
+ size: {
85
+ sm: {
86
+ mediaIcon: "size-icon-lg",
87
+ root: "gap-4 p-4",
88
+ header: "gap-1",
89
+ media: "mb-2",
90
+ title: "text-base",
91
+ description: "text-sm",
92
+ content: "gap-2",
93
+ },
94
+ md: {
95
+ mediaIcon: "size-icon-xl",
96
+ root: "gap-5 p-6",
97
+ header: "gap-1.5",
98
+ media: "mb-3",
99
+ title: "text-lg",
100
+ description: "text-sm",
101
+ content: "gap-2.5",
102
+ },
103
+ lg: {
104
+ mediaIcon: "size-icon-2xl",
105
+ root: "gap-6 p-8",
106
+ header: "gap-2",
107
+ media: "mb-4",
108
+ title: "text-xl",
109
+ description: "text-base",
110
+ content: "gap-3",
111
+ },
112
+ },
113
+ media: {
114
+ default: {},
115
+ icon: { media: "bg-muted" },
116
+ },
117
+ },
118
+ compoundVariants: [
119
+ { variant: "card", size: "sm", class: { root: "rounded-md" } },
120
+ { variant: "card", size: ["md", "lg"], class: { root: "rounded-lg" } },
121
+ { media: "icon", size: "sm", class: { media: "size-10 rounded-md" } },
122
+ { media: "icon", size: "md", class: { media: "size-12 rounded-lg" } },
123
+ { media: "icon", size: "lg", class: { media: "size-16 rounded-xl" } },
124
+ ],
125
+ defaultVariants: {
126
+ variant: "default",
127
+ size: "md",
128
+ media: "default",
129
+ },
130
+ });
131
+
132
+ export type EmptyStateVariantProps = VariantProps<typeof emptyStateVariants>;
@@ -0,0 +1,20 @@
1
+ export { EmptyState, type EmptyStateProps } from "./empty-state";
2
+ export {
3
+ type EmptyStateContextValue,
4
+ EmptyStateProvider,
5
+ useEmptyState,
6
+ useEmptyStateContext,
7
+ } from "./empty-state.context";
8
+ export type { EmptyStateSlotProps, EmptyStateTextProps } from "./empty-state.types";
9
+ export {
10
+ EMPTY_STATE_MEDIA_FOREGROUND_TOKEN,
11
+ EMPTY_STATE_MEDIA_VARIANTS,
12
+ EMPTY_STATE_SIZES,
13
+ EMPTY_STATE_VARIANTS,
14
+ type EmptyStateMediaVariant,
15
+ type EmptyStateSize,
16
+ type EmptyStateVariant,
17
+ type EmptyStateVariantProps,
18
+ emptyStateVariants,
19
+ } from "./empty-state.variants";
20
+ export type { EmptyStateMediaProps } from "./empty-state-media";
@@ -0,0 +1,119 @@
1
+ # Fab
2
+
3
+ One primary action floating over the screen it belongs to — "New note" over a
4
+ list — and, as `Fab.Group`, a dial of related actions that unfold out of it.
5
+ Compound root plus `Fab.Label`, `Fab.Group` and `Fab.Action`.
6
+
7
+ `import { Fab } from "@delacour/react-native-ui/fab";`
8
+
9
+ ## Files
10
+
11
+ | File | What it holds |
12
+ | --- | --- |
13
+ | `index.ts` | → `@delacour/react-native-ui/fab` |
14
+ | `fab.tsx` | Root + the `Object.assign` compound surface |
15
+ | `fab-label.tsx` | `Fab.Label` |
16
+ | `fab-group.tsx` | `Fab.Group` — the trigger, the scrim and the dial's one spring |
17
+ | `fab-action.tsx` | `Fab.Action` — one dial entry: a small round button and a label chip |
18
+ | `fab.context.tsx` | `FabContext`, `FabGroupContext` and the per-action index context, with their hooks |
19
+ | `fab.types.ts` | `FabSharedProps` — the axes `Fab` and `Fab.Group` both take |
20
+ | `fab.variants.ts` | The slotted `tv()` and the pure resolvers, no RN imports |
21
+ | `fab.variants.test.ts` | |
22
+
23
+ ## Design
24
+
25
+ - **Sizes**: `sm`, `md` (default), `lg` — 44, 56 and 64pt, from
26
+ `--spacing-fab-*`. **Variants**: `primary` (default), `secondary`, `surface`,
27
+ `destructive`. **Placements**: `bottom-start`, `bottom-center`, `bottom-end`.
28
+ - **The footprint is its own token, not the button's.** A fab sits over
29
+ content rather than in a row of controls, so it is a step larger than any
30
+ button, and a form retuning `--spacing-button-*` must not move the one thing
31
+ floating over its list. Declared in both `tokens.css` and theme.css's
32
+ `@variant native` block, per the package rule, and registered in
33
+ `styles/tokens.ts`; `tokens.test.ts` holds the floor at 44pt.
34
+ - **Icons on `Fab` are composed; on `Fab.Group` and `Fab.Action` they are a
35
+ prop.** `Fab` matches `Button`: it publishes its icon step (`sm` → `icon-md`,
36
+ `md`/`lg` → `icon-lg`, `resolveFabIconSize`) and its variant's foreground
37
+ through `IconDefaultsProvider`, so a bare `<Icon />` comes out right. The
38
+ group renders and **rotates** its trigger glyph, and each action draws two
39
+ things — a round button and a label chip — from one icon and one label, so
40
+ both take an `icon` component and own how it is drawn.
41
+ - **It casts a shadow, and nothing else in the kit does.** `shadow-lg` on the
42
+ fab and on each dial button. A fab is the one control floating above the
43
+ content; flat, it reads as a sticker on the list. The root carries no
44
+ `overflow-hidden`, which would clip that shadow on Android.
45
+ - **`placement` pins; omitting it leaves the fab in flow.** Pinned, the fab sits
46
+ in a `pointerEvents="box-none"` box at `bottom = offset + insets.bottom` and
47
+ `start`/`end = offset` — logical edges, so RTL flips with no code.
48
+ `isSafeAreaAware` (default `true`) is what adds the inset; the inset is read
49
+ from `SafeAreaInsetsContext` directly, so a fab outside a safe-area provider
50
+ gets `0` rather than a throw.
51
+ - **Centre spans the row instead of using `alignSelf`.** An absolute child's
52
+ `alignSelf` is read on the parent's *cross* axis — horizontal only in a
53
+ column — so a fab written into a `flex-row` would have centred vertically.
54
+ `resolveFabPlacementStyle` returns `start: 0, end: 0, alignItems: "center"`
55
+ instead. That is the one shape this resolver returns that the spec sketch
56
+ listed as `alignSelf`.
57
+ - **One spring drives the dial.** `Fab.Group` holds a single `progress` shared
58
+ value, `withSpring` to 0 or 1 with a little overshoot. Each action derives
59
+ its own window on the UI thread with `resolveDialProgress` —
60
+ `clamp((open − i·0.08) / (1 − (n−1)·0.08), 0, 1)`, index 0 nearest the
61
+ trigger — so there are no per-action timers, and closing halfway through
62
+ opening simply runs the same cascade backwards. The window is floored so a
63
+ stagger too wide for the count degrades to a step, never a NaN.
64
+ - **Actions are unmounted while closed, not hidden.** They mount in the render
65
+ that opens the dial and unmount from the close animation's `finished`
66
+ callback, so a screen reader cannot walk into invisible buttons. A reopen
67
+ mid-close cancels that animation, `finished` is false, and nothing unmounts.
68
+ - **The glyph turns 45°, plus → cross**, read off the same spring and clamped
69
+ so the overshoot does not wobble it. `isRotatedOnOpen={false}` holds it still
70
+ for a glyph that is not a plus.
71
+ - **Pressing an action runs it, then closes the dial.** The label chip presses
72
+ too — a finger aims at the words — but it is hidden from assistive technology
73
+ so a screen reader meets each action once, by its button.
74
+ - **The chip sits on the side facing into the screen.** `resolveLabelSide`:
75
+ left of the button at `bottom-end`, right at `bottom-start`, above at
76
+ `bottom-center`. Each action's button sits in an anchor as wide as the
77
+ trigger (`w-fab-*`), so the small buttons share the trigger's centre line.
78
+ - **The group is a layer, so write it in the screen's root container.** It
79
+ renders an `absolute inset-0` box holding the scrim and the pinned dial, and
80
+ covers whatever its parent is. Inside a scroll view's content it would cover
81
+ the content box, not the screen. The layer is `box-none`, so while closed it
82
+ takes no touches.
83
+ - **The scrim is `bg-overlay`, faded by the spring.** The token carries its own
84
+ alpha, so the fade runs 0 → 1 and lands on the theme's overlay. Tapping it
85
+ closes; it is a button labelled "Close".
86
+ - **Android back closes an open dial**, through a `BackHandler` subscription
87
+ that exists only while the dial is open.
88
+ - **`testID` lands on what a finger presses.** `Fab.Group`'s goes on the
89
+ trigger, not the layer or the anchor — the layer covers the whole parent and
90
+ the anchor's centre is inside the dial once it opens, so neither is something
91
+ a test can tap. `Fab.Action`'s goes on its round button.
92
+ - **`isOpen` / `defaultOpen` / `onOpenChange`** through `useControllableState`.
93
+ - **Haptics are off by default.** `haptic` on `Fab` plays on press; on
94
+ `Fab.Group` it plays on the trigger and on every action.
95
+ - **Reduced motion fades the dial in 150ms** with no rise, scale or stagger —
96
+ the dial still appears, because appearing is the information. The glyph
97
+ still turns, on the same timing.
98
+ - **A11y.** Every fab and action is a `button`. The group trigger carries
99
+ `accessibilityState={{ expanded }}` and a required `accessibilityLabel`; the
100
+ layer is `accessibilityViewIsModal` while open. A round `Fab` with no
101
+ `accessibilityLabel` warns in development — there is no text to fall back on.
102
+ - **The group trigger extends with `label` while closed.** Open, it is a
103
+ circle again: the label names what the dial is for, and the cross that
104
+ replaces the plus is the only thing the trigger does then.
105
+ - **`Fab.Group` also takes `offset` and `isSafeAreaAware`**, which the spec
106
+ sketch listed only on `Fab`. Both are on `FabSharedProps`, so a group pinned
107
+ above a tab bar can clear it the same way a lone fab does.
108
+
109
+ ## Out of scope
110
+
111
+ - **A glass material.** It needs a native peer the catalog does not carry.
112
+ - **`layout="menu"` and `layout="native"`** — a menu panel out of the fab. A
113
+ follow-up once `Menu` has merged, reusing its rows.
114
+ - **A blurred scrim.** It needs `expo-blur`, which is not a peer.
115
+ - **Design-system styles do not retune the fab.** `@delacour/design-system`'s
116
+ `STYLES` write `spacing-fab-*` as the same 44/56/64 under every style
117
+ (`LIBRARY_FAB` in `styles.ts`), so a style switch moves the buttons and leaves
118
+ the fab where it is. Give a style its own numbers there when one needs a
119
+ different fab.