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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (493) hide show
  1. package/README.md +10 -6
  2. package/package.json +49 -5
  3. package/src/components/accordion/AGENTS.md +21 -9
  4. package/src/components/accordion/accordion-content.tsx +7 -4
  5. package/src/components/accordion/accordion-item.tsx +15 -13
  6. package/src/components/accordion/accordion.context.tsx +11 -5
  7. package/src/components/accordion/accordion.variants.test.ts +64 -3
  8. package/src/components/accordion/accordion.variants.ts +33 -9
  9. package/src/components/alert/AGENTS.md +102 -0
  10. package/src/components/alert/alert-action.tsx +17 -0
  11. package/src/components/alert/alert-close-button.tsx +50 -0
  12. package/src/components/alert/alert-content.tsx +15 -0
  13. package/src/components/alert/alert-description.tsx +16 -0
  14. package/src/components/alert/alert-glyphs.ts +25 -0
  15. package/src/components/alert/alert-indicator.tsx +40 -0
  16. package/src/components/alert/alert-title.tsx +18 -0
  17. package/src/components/alert/alert.context.tsx +69 -0
  18. package/src/components/alert/alert.tsx +160 -0
  19. package/src/components/alert/alert.types.ts +10 -0
  20. package/src/components/alert/alert.variants.test.ts +218 -0
  21. package/src/components/alert/alert.variants.ts +180 -0
  22. package/src/components/alert/index.ts +23 -0
  23. package/src/components/avatar/AGENTS.md +106 -0
  24. package/src/components/avatar/avatar-badge.tsx +51 -0
  25. package/src/components/avatar/avatar-group.tsx +139 -0
  26. package/src/components/avatar/avatar.context.tsx +93 -0
  27. package/src/components/avatar/avatar.tsx +201 -0
  28. package/src/components/avatar/avatar.utils.test.ts +174 -0
  29. package/src/components/avatar/avatar.utils.ts +141 -0
  30. package/src/components/avatar/avatar.variants.test.ts +251 -0
  31. package/src/components/avatar/avatar.variants.ts +270 -0
  32. package/src/components/avatar/index.ts +39 -0
  33. package/src/components/badge/AGENTS.md +10 -0
  34. package/src/components/badge/badge-close-button.tsx +2 -2
  35. package/src/components/badge/badge.context.tsx +2 -1
  36. package/src/components/badge/badge.tsx +13 -4
  37. package/src/components/badge/badge.variants.test.ts +26 -0
  38. package/src/components/badge/badge.variants.ts +21 -0
  39. package/src/components/bottom-sheet/AGENTS.md +186 -231
  40. package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
  41. package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
  42. package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -208
  43. package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
  44. package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
  45. package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
  46. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -122
  47. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
  48. package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
  49. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
  50. package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
  51. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -62
  52. package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
  53. package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
  54. package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
  55. package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
  56. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
  57. package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
  58. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -87
  59. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
  60. package/src/components/bottom-sheet/index.ts +39 -17
  61. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
  62. package/src/components/button/AGENTS.md +51 -11
  63. package/src/components/button/button-group.tsx +44 -8
  64. package/src/components/button/button.context.tsx +10 -0
  65. package/src/components/button/button.tsx +7 -1
  66. package/src/components/button/button.variants.test.ts +190 -0
  67. package/src/components/button/button.variants.ts +87 -6
  68. package/src/components/button/index.ts +2 -0
  69. package/src/components/calendar/AGENTS.md +142 -0
  70. package/src/components/calendar/calendar-caption.tsx +116 -0
  71. package/src/components/calendar/calendar-day.tsx +103 -0
  72. package/src/components/calendar/calendar-grid.tsx +183 -0
  73. package/src/components/calendar/calendar-header.tsx +34 -0
  74. package/src/components/calendar/calendar-nav.tsx +51 -0
  75. package/src/components/calendar/calendar-picker.tsx +128 -0
  76. package/src/components/calendar/calendar-weekdays.tsx +48 -0
  77. package/src/components/calendar/calendar.context.tsx +108 -0
  78. package/src/components/calendar/calendar.date.test.ts +456 -0
  79. package/src/components/calendar/calendar.date.ts +584 -0
  80. package/src/components/calendar/calendar.tsx +389 -0
  81. package/src/components/calendar/calendar.types.ts +87 -0
  82. package/src/components/calendar/calendar.variants.test.ts +363 -0
  83. package/src/components/calendar/calendar.variants.ts +426 -0
  84. package/src/components/calendar/index.ts +96 -0
  85. package/src/components/card/AGENTS.md +92 -0
  86. package/src/components/card/card-action.tsx +16 -0
  87. package/src/components/card/card-content.tsx +12 -0
  88. package/src/components/card/card-description.tsx +12 -0
  89. package/src/components/card/card-footer.tsx +30 -0
  90. package/src/components/card/card-header.tsx +45 -0
  91. package/src/components/card/card-title.tsx +25 -0
  92. package/src/components/card/card.context.tsx +64 -0
  93. package/src/components/card/card.tsx +100 -0
  94. package/src/components/card/card.types.ts +13 -0
  95. package/src/components/card/card.variants.test.ts +240 -0
  96. package/src/components/card/card.variants.ts +177 -0
  97. package/src/components/card/index.ts +15 -0
  98. package/src/components/chart/AGENTS.md +8 -0
  99. package/src/components/chart/chart.tsx +8 -1
  100. package/src/components/chip/AGENTS.md +113 -0
  101. package/src/components/chip/chip-close-button.tsx +48 -0
  102. package/src/components/chip/chip-end-content.tsx +15 -0
  103. package/src/components/chip/chip-label.tsx +21 -0
  104. package/src/components/chip/chip-start-content.tsx +16 -0
  105. package/src/components/chip/chip.context.tsx +66 -0
  106. package/src/components/chip/chip.tsx +268 -0
  107. package/src/components/chip/chip.types.ts +10 -0
  108. package/src/components/chip/chip.variants.test.ts +335 -0
  109. package/src/components/chip/chip.variants.ts +321 -0
  110. package/src/components/chip/index.ts +24 -0
  111. package/src/components/collapsible/AGENTS.md +97 -0
  112. package/src/components/collapsible/collapsible-content.tsx +97 -0
  113. package/src/components/collapsible/collapsible-description.tsx +17 -0
  114. package/src/components/collapsible/collapsible-indicator.tsx +92 -0
  115. package/src/components/collapsible/collapsible-title.tsx +17 -0
  116. package/src/components/collapsible/collapsible-trigger.tsx +133 -0
  117. package/src/components/collapsible/collapsible.context.tsx +91 -0
  118. package/src/components/collapsible/collapsible.tsx +167 -0
  119. package/src/components/collapsible/collapsible.types.ts +9 -0
  120. package/src/components/collapsible/collapsible.variants.test.ts +338 -0
  121. package/src/components/collapsible/collapsible.variants.ts +228 -0
  122. package/src/components/collapsible/index.ts +32 -0
  123. package/src/components/context-menu/AGENTS.md +103 -0
  124. package/src/components/context-menu/context-menu-content.tsx +64 -0
  125. package/src/components/context-menu/context-menu-item.tsx +22 -0
  126. package/src/components/context-menu/context-menu-preview.tsx +60 -0
  127. package/src/components/context-menu/context-menu-trigger.tsx +160 -0
  128. package/src/components/context-menu/context-menu.context.tsx +91 -0
  129. package/src/components/context-menu/context-menu.tsx +90 -0
  130. package/src/components/context-menu/context-menu.variants.test.ts +266 -0
  131. package/src/components/context-menu/context-menu.variants.ts +147 -0
  132. package/src/components/context-menu/index.ts +18 -0
  133. package/src/components/dialog/AGENTS.md +97 -0
  134. package/src/components/dialog/dialog-body.tsx +21 -0
  135. package/src/components/dialog/dialog-close.tsx +82 -0
  136. package/src/components/dialog/dialog-content.tsx +153 -0
  137. package/src/components/dialog/dialog-description.tsx +23 -0
  138. package/src/components/dialog/dialog-footer.tsx +32 -0
  139. package/src/components/dialog/dialog-header.tsx +20 -0
  140. package/src/components/dialog/dialog-title.tsx +42 -0
  141. package/src/components/dialog/dialog-trigger.tsx +54 -0
  142. package/src/components/dialog/dialog.context.tsx +90 -0
  143. package/src/components/dialog/dialog.tsx +119 -0
  144. package/src/components/dialog/dialog.variants.test.ts +224 -0
  145. package/src/components/dialog/dialog.variants.ts +173 -0
  146. package/src/components/dialog/index.ts +30 -0
  147. package/src/components/drawer/AGENTS.md +123 -0
  148. package/src/components/drawer/drawer-body.tsx +46 -0
  149. package/src/components/drawer/drawer-close.tsx +76 -0
  150. package/src/components/drawer/drawer-content.tsx +186 -0
  151. package/src/components/drawer/drawer-description.tsx +23 -0
  152. package/src/components/drawer/drawer-footer.tsx +21 -0
  153. package/src/components/drawer/drawer-header.tsx +42 -0
  154. package/src/components/drawer/drawer-title.tsx +35 -0
  155. package/src/components/drawer/drawer-trigger.tsx +54 -0
  156. package/src/components/drawer/drawer.context.tsx +114 -0
  157. package/src/components/drawer/drawer.tsx +119 -0
  158. package/src/components/drawer/drawer.variants.test.ts +392 -0
  159. package/src/components/drawer/drawer.variants.ts +244 -0
  160. package/src/components/drawer/index.ts +41 -0
  161. package/src/components/drawer/use-drawer-pan.ts +108 -0
  162. package/src/components/empty-state/AGENTS.md +90 -0
  163. package/src/components/empty-state/empty-state-content.tsx +17 -0
  164. package/src/components/empty-state/empty-state-description.tsx +12 -0
  165. package/src/components/empty-state/empty-state-header.tsx +17 -0
  166. package/src/components/empty-state/empty-state-media.tsx +56 -0
  167. package/src/components/empty-state/empty-state-title.tsx +18 -0
  168. package/src/components/empty-state/empty-state.context.tsx +65 -0
  169. package/src/components/empty-state/empty-state.tsx +84 -0
  170. package/src/components/empty-state/empty-state.types.ts +14 -0
  171. package/src/components/empty-state/empty-state.variants.test.ts +245 -0
  172. package/src/components/empty-state/empty-state.variants.ts +132 -0
  173. package/src/components/empty-state/index.ts +20 -0
  174. package/src/components/fab/AGENTS.md +119 -0
  175. package/src/components/fab/fab-action.tsx +118 -0
  176. package/src/components/fab/fab-group.tsx +214 -0
  177. package/src/components/fab/fab-label.tsx +18 -0
  178. package/src/components/fab/fab.context.tsx +125 -0
  179. package/src/components/fab/fab.tsx +156 -0
  180. package/src/components/fab/fab.types.ts +17 -0
  181. package/src/components/fab/fab.variants.test.ts +274 -0
  182. package/src/components/fab/fab.variants.ts +234 -0
  183. package/src/components/fab/index.ts +37 -0
  184. package/src/components/feedback/AGENTS.md +95 -0
  185. package/src/components/feedback/feedback-action.tsx +16 -0
  186. package/src/components/feedback/feedback-cancel.tsx +34 -0
  187. package/src/components/feedback/feedback-close.tsx +21 -0
  188. package/src/components/feedback/feedback-content.tsx +24 -0
  189. package/src/components/feedback/feedback-field.tsx +70 -0
  190. package/src/components/feedback/feedback-footer.tsx +21 -0
  191. package/src/components/feedback/feedback-panel.tsx +54 -0
  192. package/src/components/feedback/feedback-submit.tsx +74 -0
  193. package/src/components/feedback/feedback-title.tsx +34 -0
  194. package/src/components/feedback/feedback-trigger.tsx +20 -0
  195. package/src/components/feedback/feedback.context.tsx +73 -0
  196. package/src/components/feedback/feedback.tsx +135 -0
  197. package/src/components/feedback/feedback.variants.test.ts +185 -0
  198. package/src/components/feedback/feedback.variants.ts +128 -0
  199. package/src/components/feedback/index.ts +24 -0
  200. package/src/components/field/AGENTS.md +19 -0
  201. package/src/components/field/field-label.tsx +19 -5
  202. package/src/components/field/field.context.tsx +17 -0
  203. package/src/components/field/field.tsx +15 -2
  204. package/src/components/field/field.variants.test.ts +24 -0
  205. package/src/components/field/field.variants.ts +20 -0
  206. package/src/components/field/index.ts +1 -0
  207. package/src/components/input/AGENTS.md +5 -0
  208. package/src/components/input/input.variants.test.ts +32 -0
  209. package/src/components/input/input.variants.ts +13 -1
  210. package/src/components/item/AGENTS.md +92 -0
  211. package/src/components/item/index.ts +25 -0
  212. package/src/components/item/item-actions.tsx +28 -0
  213. package/src/components/item/item-content.tsx +15 -0
  214. package/src/components/item/item-description.tsx +12 -0
  215. package/src/components/item/item-footer.tsx +13 -0
  216. package/src/components/item/item-group.tsx +32 -0
  217. package/src/components/item/item-header.tsx +13 -0
  218. package/src/components/item/item-media.tsx +37 -0
  219. package/src/components/item/item-separator.tsx +13 -0
  220. package/src/components/item/item-title.tsx +17 -0
  221. package/src/components/item/item.context.tsx +66 -0
  222. package/src/components/item/item.tsx +218 -0
  223. package/src/components/item/item.types.ts +12 -0
  224. package/src/components/item/item.variants.test.ts +263 -0
  225. package/src/components/item/item.variants.ts +200 -0
  226. package/src/components/kpi/AGENTS.md +125 -0
  227. package/src/components/kpi/index.ts +45 -0
  228. package/src/components/kpi/kpi-action.tsx +12 -0
  229. package/src/components/kpi/kpi-content.tsx +35 -0
  230. package/src/components/kpi/kpi-footer.tsx +52 -0
  231. package/src/components/kpi/kpi-group.tsx +70 -0
  232. package/src/components/kpi/kpi-header.tsx +25 -0
  233. package/src/components/kpi/kpi-icon.tsx +28 -0
  234. package/src/components/kpi/kpi-sparkline.tsx +145 -0
  235. package/src/components/kpi/kpi-stat.tsx +20 -0
  236. package/src/components/kpi/kpi-title.tsx +19 -0
  237. package/src/components/kpi/kpi-trend.tsx +105 -0
  238. package/src/components/kpi/kpi-value.tsx +32 -0
  239. package/src/components/kpi/kpi.context.tsx +111 -0
  240. package/src/components/kpi/kpi.tsx +154 -0
  241. package/src/components/kpi/kpi.types.ts +9 -0
  242. package/src/components/kpi/kpi.variants.test.ts +428 -0
  243. package/src/components/kpi/kpi.variants.ts +347 -0
  244. package/src/components/label/AGENTS.md +89 -0
  245. package/src/components/label/index.ts +10 -0
  246. package/src/components/label/label.tsx +97 -0
  247. package/src/components/label/label.variants.test.ts +176 -0
  248. package/src/components/label/label.variants.ts +123 -0
  249. package/src/components/menu/AGENTS.md +176 -0
  250. package/src/components/menu/index.ts +44 -0
  251. package/src/components/menu/menu-background.tsx +17 -0
  252. package/src/components/menu/menu-checkbox-item.tsx +57 -0
  253. package/src/components/menu/menu-content.tsx +303 -0
  254. package/src/components/menu/menu-item.tsx +162 -0
  255. package/src/components/menu/menu-label.tsx +14 -0
  256. package/src/components/menu/menu-radio-group.tsx +50 -0
  257. package/src/components/menu/menu-radio-item.tsx +57 -0
  258. package/src/components/menu/menu-separator.tsx +11 -0
  259. package/src/components/menu/menu-sub-content.tsx +72 -0
  260. package/src/components/menu/menu-sub-trigger.tsx +50 -0
  261. package/src/components/menu/menu-sub.tsx +75 -0
  262. package/src/components/menu/menu-trigger.tsx +57 -0
  263. package/src/components/menu/menu.context.tsx +214 -0
  264. package/src/components/menu/menu.tsx +88 -0
  265. package/src/components/menu/menu.types.ts +33 -0
  266. package/src/components/menu/menu.variants.test.ts +400 -0
  267. package/src/components/menu/menu.variants.ts +353 -0
  268. package/src/components/meter/AGENTS.md +135 -0
  269. package/src/components/meter/index.ts +43 -0
  270. package/src/components/meter/meter-fill.tsx +19 -0
  271. package/src/components/meter/meter-header.tsx +17 -0
  272. package/src/components/meter/meter-label.tsx +18 -0
  273. package/src/components/meter/meter-output.tsx +42 -0
  274. package/src/components/meter/meter-segments.tsx +83 -0
  275. package/src/components/meter/meter-track.tsx +16 -0
  276. package/src/components/meter/meter.context.tsx +62 -0
  277. package/src/components/meter/meter.tsx +286 -0
  278. package/src/components/meter/meter.types.ts +30 -0
  279. package/src/components/meter/meter.variants.test.ts +430 -0
  280. package/src/components/meter/meter.variants.ts +347 -0
  281. package/src/components/overlay/AGENTS.md +89 -0
  282. package/src/components/overlay/index.ts +27 -0
  283. package/src/components/overlay/overlay-portal.tsx +84 -0
  284. package/src/components/overlay/overlay-presence.test.ts +46 -0
  285. package/src/components/overlay/overlay-presence.ts +49 -0
  286. package/src/components/overlay/overlay-registry.test.ts +96 -0
  287. package/src/components/overlay/overlay-registry.ts +99 -0
  288. package/src/components/overlay/overlay-scrim.tsx +49 -0
  289. package/src/components/overlay/overlay.context.tsx +48 -0
  290. package/src/components/overlay/overlay.tsx +118 -0
  291. package/src/components/overlay/overlay.variants.test.ts +37 -0
  292. package/src/components/overlay/overlay.variants.ts +33 -0
  293. package/src/components/overlay/use-overlay-back-handler.ts +34 -0
  294. package/src/components/overlay/use-overlay-presence.ts +87 -0
  295. package/src/components/popover/AGENTS.md +138 -0
  296. package/src/components/popover/index.ts +48 -0
  297. package/src/components/popover/popover-anchor.tsx +49 -0
  298. package/src/components/popover/popover-arrow.tsx +89 -0
  299. package/src/components/popover/popover-close.tsx +73 -0
  300. package/src/components/popover/popover-content.tsx +235 -0
  301. package/src/components/popover/popover-description.tsx +18 -0
  302. package/src/components/popover/popover-title.tsx +33 -0
  303. package/src/components/popover/popover-trigger.tsx +56 -0
  304. package/src/components/popover/popover.context.tsx +71 -0
  305. package/src/components/popover/popover.position.test.ts +354 -0
  306. package/src/components/popover/popover.position.ts +286 -0
  307. package/src/components/popover/popover.tsx +123 -0
  308. package/src/components/popover/popover.variants.test.ts +135 -0
  309. package/src/components/popover/popover.variants.ts +85 -0
  310. package/src/components/popover/use-anchor-measure.ts +76 -0
  311. package/src/components/popover/use-anchored-content.ts +219 -0
  312. package/src/components/pressable/AGENTS.md +11 -1
  313. package/src/components/pressable/pressable.tsx +7 -2
  314. package/src/components/pressable/pressable.variants.test.ts +29 -0
  315. package/src/components/pressable/pressable.variants.ts +17 -0
  316. package/src/components/progress/AGENTS.md +120 -0
  317. package/src/components/progress/index.ts +40 -0
  318. package/src/components/progress/progress-fill.tsx +152 -0
  319. package/src/components/progress/progress-header.tsx +19 -0
  320. package/src/components/progress/progress-label.tsx +27 -0
  321. package/src/components/progress/progress-output.tsx +47 -0
  322. package/src/components/progress/progress-track.tsx +38 -0
  323. package/src/components/progress/progress.context.tsx +79 -0
  324. package/src/components/progress/progress.tsx +171 -0
  325. package/src/components/progress/progress.types.ts +23 -0
  326. package/src/components/progress/progress.variants.test.ts +335 -0
  327. package/src/components/progress/progress.variants.ts +284 -0
  328. package/src/components/progress-button/AGENTS.md +116 -0
  329. package/src/components/progress-button/index.ts +36 -0
  330. package/src/components/progress-button/progress-button-done.tsx +33 -0
  331. package/src/components/progress-button/progress-button-label.tsx +27 -0
  332. package/src/components/progress-button/progress-button.context.tsx +93 -0
  333. package/src/components/progress-button/progress-button.tsx +414 -0
  334. package/src/components/progress-button/progress-button.variants.test.ts +314 -0
  335. package/src/components/progress-button/progress-button.variants.ts +236 -0
  336. package/src/components/provider/AGENTS.md +38 -25
  337. package/src/components/provider/provider.tsx +38 -11
  338. package/src/components/radio/AGENTS.md +8 -10
  339. package/src/components/radio/radio.variants.test.ts +5 -6
  340. package/src/components/radio/radio.variants.ts +6 -7
  341. package/src/components/rating/AGENTS.md +143 -0
  342. package/src/components/rating/index.ts +43 -0
  343. package/src/components/rating/rating-output.tsx +39 -0
  344. package/src/components/rating/rating-star.tsx +70 -0
  345. package/src/components/rating/rating-stars.tsx +219 -0
  346. package/src/components/rating/rating.context.tsx +79 -0
  347. package/src/components/rating/rating.tsx +163 -0
  348. package/src/components/rating/rating.variants.test.ts +347 -0
  349. package/src/components/rating/rating.variants.ts +354 -0
  350. package/src/components/selection-mode/AGENTS.md +155 -0
  351. package/src/components/selection-mode/index.ts +43 -0
  352. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  353. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  354. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  355. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  356. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  357. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  358. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  359. package/src/components/selection-mode/selection-mode.tsx +207 -0
  360. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  361. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  362. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  363. package/src/components/skeleton/AGENTS.md +120 -0
  364. package/src/components/skeleton/index.ts +15 -0
  365. package/src/components/skeleton/skeleton-clock.ts +48 -0
  366. package/src/components/skeleton/skeleton-group.tsx +64 -0
  367. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  368. package/src/components/skeleton/skeleton.context.tsx +45 -0
  369. package/src/components/skeleton/skeleton.tsx +227 -0
  370. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  371. package/src/components/skeleton/skeleton.variants.ts +222 -0
  372. package/src/components/slide-button/AGENTS.md +122 -0
  373. package/src/components/slide-button/index.ts +43 -0
  374. package/src/components/slide-button/slide-button-label.tsx +70 -0
  375. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  376. package/src/components/slide-button/slide-button.context.tsx +76 -0
  377. package/src/components/slide-button/slide-button.tsx +457 -0
  378. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  379. package/src/components/slide-button/slide-button.variants.ts +324 -0
  380. package/src/components/slider/index.ts +2 -0
  381. package/src/components/slider/slider-thumb.tsx +33 -1
  382. package/src/components/slider/slider.variants.test.ts +52 -0
  383. package/src/components/slider/slider.variants.ts +56 -2
  384. package/src/components/stack-card/AGENTS.md +169 -0
  385. package/src/components/stack-card/index.ts +37 -0
  386. package/src/components/stack-card/stack-card-action.tsx +66 -0
  387. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  388. package/src/components/stack-card/stack-card-card.tsx +71 -0
  389. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  390. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  391. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  392. package/src/components/stack-card/stack-card.context.tsx +117 -0
  393. package/src/components/stack-card/stack-card.tsx +557 -0
  394. package/src/components/stack-card/stack-card.types.ts +26 -0
  395. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  396. package/src/components/stack-card/stack-card.variants.ts +360 -0
  397. package/src/components/steps/AGENTS.md +153 -0
  398. package/src/components/steps/index.ts +45 -0
  399. package/src/components/steps/steps-connector.tsx +31 -0
  400. package/src/components/steps/steps-description.tsx +26 -0
  401. package/src/components/steps/steps-indicator.tsx +106 -0
  402. package/src/components/steps/steps-item.tsx +225 -0
  403. package/src/components/steps/steps-panel.tsx +41 -0
  404. package/src/components/steps/steps-title.tsx +32 -0
  405. package/src/components/steps/steps-track.tsx +59 -0
  406. package/src/components/steps/steps.context.tsx +123 -0
  407. package/src/components/steps/steps.tsx +166 -0
  408. package/src/components/steps/steps.variants.test.ts +388 -0
  409. package/src/components/steps/steps.variants.ts +468 -0
  410. package/src/components/surface/AGENTS.md +82 -0
  411. package/src/components/surface/index.ts +15 -0
  412. package/src/components/surface/surface.context.tsx +56 -0
  413. package/src/components/surface/surface.tsx +87 -0
  414. package/src/components/surface/surface.variants.test.ts +227 -0
  415. package/src/components/surface/surface.variants.ts +165 -0
  416. package/src/components/swipe/AGENTS.md +140 -0
  417. package/src/components/swipe/index.ts +31 -0
  418. package/src/components/swipe/swipe-action.tsx +99 -0
  419. package/src/components/swipe/swipe-end.tsx +14 -0
  420. package/src/components/swipe/swipe-group.tsx +55 -0
  421. package/src/components/swipe/swipe-panel.tsx +55 -0
  422. package/src/components/swipe/swipe-start.tsx +15 -0
  423. package/src/components/swipe/swipe.context.tsx +135 -0
  424. package/src/components/swipe/swipe.tsx +393 -0
  425. package/src/components/swipe/swipe.types.ts +7 -0
  426. package/src/components/swipe/swipe.variants.test.ts +337 -0
  427. package/src/components/swipe/swipe.variants.ts +290 -0
  428. package/src/components/switch/switch.variants.ts +2 -2
  429. package/src/components/tabs/AGENTS.md +4 -3
  430. package/src/components/tabs/tabs.variants.test.ts +1 -1
  431. package/src/components/tabs/tabs.variants.ts +4 -6
  432. package/src/components/text/AGENTS.md +8 -2
  433. package/src/components/text/text.tsx +7 -0
  434. package/src/components/text/text.variants.test.ts +24 -1
  435. package/src/components/text/text.variants.ts +5 -0
  436. package/src/components/textarea/AGENTS.md +89 -0
  437. package/src/components/textarea/index.ts +16 -0
  438. package/src/components/textarea/textarea.tsx +157 -0
  439. package/src/components/textarea/textarea.variants.test.ts +219 -0
  440. package/src/components/textarea/textarea.variants.ts +171 -0
  441. package/src/components/toast/AGENTS.md +140 -0
  442. package/src/components/toast/index.ts +74 -0
  443. package/src/components/toast/toast-action.tsx +59 -0
  444. package/src/components/toast/toast-api.ts +32 -0
  445. package/src/components/toast/toast-close.tsx +44 -0
  446. package/src/components/toast/toast-content.tsx +11 -0
  447. package/src/components/toast/toast-description.tsx +11 -0
  448. package/src/components/toast/toast-indicator.tsx +34 -0
  449. package/src/components/toast/toast-item.tsx +308 -0
  450. package/src/components/toast/toast-title.tsx +13 -0
  451. package/src/components/toast/toast-viewport.tsx +135 -0
  452. package/src/components/toast/toast.context.tsx +69 -0
  453. package/src/components/toast/toast.store.test.ts +259 -0
  454. package/src/components/toast/toast.store.ts +338 -0
  455. package/src/components/toast/toast.timer.test.ts +64 -0
  456. package/src/components/toast/toast.timer.ts +51 -0
  457. package/src/components/toast/toast.tsx +132 -0
  458. package/src/components/toast/toast.variants.test.ts +393 -0
  459. package/src/components/toast/toast.variants.ts +304 -0
  460. package/src/components/toggle-button/AGENTS.md +106 -0
  461. package/src/components/toggle-button/index.ts +30 -0
  462. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  463. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  464. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  465. package/src/components/toggle-button/toggle-button.tsx +172 -0
  466. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  467. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  468. package/src/components/tooltip/AGENTS.md +133 -0
  469. package/src/components/tooltip/index.ts +28 -0
  470. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  471. package/src/components/tooltip/tooltip-content.tsx +179 -0
  472. package/src/components/tooltip/tooltip-description.tsx +19 -0
  473. package/src/components/tooltip/tooltip-text.tsx +21 -0
  474. package/src/components/tooltip/tooltip-title.tsx +20 -0
  475. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  476. package/src/components/tooltip/tooltip.context.tsx +68 -0
  477. package/src/components/tooltip/tooltip.tsx +213 -0
  478. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  479. package/src/components/tooltip/tooltip.variants.ts +177 -0
  480. package/src/hooks/use-calm-motion.tsx +31 -0
  481. package/src/lib/calm-motion.test.ts +17 -0
  482. package/src/lib/calm-motion.ts +23 -0
  483. package/src/lib/cn.parity.test.ts +62 -0
  484. package/src/lib/cn.test.ts +14 -0
  485. package/src/lib/cn.ts +11 -6
  486. package/src/no-gorhom.test.ts +54 -0
  487. package/src/styles/geometry.test.ts +1 -0
  488. package/src/styles/theme.css +4 -0
  489. package/src/styles/tokens.css +44 -0
  490. package/src/styles/tokens.test.ts +56 -0
  491. package/src/styles/tokens.ts +29 -1
  492. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  493. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -0,0 +1,45 @@
1
+ import { isValidElement, type ReactElement, type ReactNode } from "react";
2
+ import { View } from "react-native";
3
+ import { useCardPart } from "./card.context";
4
+ import type { CardSlotProps } from "./card.types";
5
+ import { cardVariants, splitCardHeaderChildren } from "./card.variants";
6
+ import { CardAction } from "./card-action";
7
+ import { CardTitle } from "./card-title";
8
+
9
+ function isCardAction(node: ReactNode): boolean {
10
+ return isValidElement(node) && node.type === CardAction;
11
+ }
12
+
13
+ /**
14
+ * Wraps a bare string or number in a `Card.Title`. A raw string inside a `View`
15
+ * is a red box in React Native, and a header's bare text is its title.
16
+ */
17
+ function wrapBareText(node: ReactNode, index: number): ReactNode {
18
+ return typeof node === "string" || typeof node === "number" ? (
19
+ <CardTitle key={`title-${index}`}>{node}</CardTitle>
20
+ ) : (
21
+ node
22
+ );
23
+ }
24
+
25
+ /**
26
+ * The card's opening block: a title and description stacked on the left, and
27
+ * any `Card.Action` pinned to the top right.
28
+ *
29
+ * The header lifts every action out of the column wherever it is written, so
30
+ * the order of children does not matter and the text wraps in the width the
31
+ * action leaves. Bare text becomes a title.
32
+ */
33
+ export function CardHeader({ className, children, ...props }: CardSlotProps): ReactElement {
34
+ const { size } = useCardPart("Card.Header");
35
+ const slots = cardVariants({ size });
36
+ const { text, actions } = splitCardHeaderChildren(children, isCardAction);
37
+
38
+ return (
39
+ <View className={slots.header({ className })} {...props}>
40
+ <View className={slots.headerText()}>{text.map(wrapBareText)}</View>
41
+ {actions}
42
+ </View>
43
+ );
44
+ }
45
+ CardHeader.displayName = "DelacourUI.Card.Header";
@@ -0,0 +1,25 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text } from "../text";
3
+ import { useCardPart } from "./card.context";
4
+ import type { CardTextProps } from "./card.types";
5
+ import { cardVariants } from "./card.variants";
6
+
7
+ /**
8
+ * The card's heading.
9
+ *
10
+ * Carries its own colour — the foreground token of the plane the card resolved
11
+ * to — and its own step on the type scale, read from the card's context: a
12
+ * React Native `View` does not cascade colour to a `Text` descendant. Announced
13
+ * as a header, so a screen reader's rotor stops on each card in a list.
14
+ */
15
+ export function CardTitle({ className, ...props }: CardTextProps): ReactElement {
16
+ const { size, plane } = useCardPart("Card.Title");
17
+ return (
18
+ <Text
19
+ accessibilityRole="header"
20
+ className={cardVariants({ plane: plane ?? "none", size }).title({ className })}
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+ CardTitle.displayName = "DelacourUI.Card.Title";
@@ -0,0 +1,64 @@
1
+ import { createContext, type ReactElement, type ReactNode, use } from "react";
2
+ import type { SurfacePlane, SurfaceVariant } from "../surface/surface.variants";
3
+ import type { CardSize } from "./card.variants";
4
+
5
+ export type CardContextValue = {
6
+ /** Size of the card — the padding, the gaps and the title and description scale. */
7
+ size: CardSize;
8
+ /** The fill the card resolved to — explicit, or stepped from the surface it sits in. */
9
+ variant: SurfaceVariant;
10
+ /**
11
+ * The fill the card's content sits on. The same as `variant` unless that is
12
+ * `transparent`, which passes the plane beneath it through — `null` on the page.
13
+ */
14
+ plane: SurfacePlane | null;
15
+ };
16
+
17
+ const CardContext = createContext<CardContextValue | null>(null);
18
+
19
+ /**
20
+ * Supplies the enclosing card's size and fill to its subtree.
21
+ *
22
+ * Lives in its own module, importing nothing but types, so a part the card
23
+ * renders can read it without importing `./card` and closing a cycle through
24
+ * `card.tsx`. See AGENTS.md rule 3.
25
+ */
26
+ export function CardProvider({ value, children }: { value: CardContextValue; children: ReactNode }): ReactElement {
27
+ return <CardContext value={value}>{children}</CardContext>;
28
+ }
29
+ CardProvider.displayName = "DelacourUI.Card.Provider";
30
+
31
+ /** The enclosing card's context, or null outside a `<Card>`. */
32
+ export function useCardContext(): CardContextValue | null {
33
+ return use(CardContext);
34
+ }
35
+
36
+ /**
37
+ * Reads the enclosing card's size and fill.
38
+ *
39
+ * Lets a custom part match the card — the same inset, a colour that reads on
40
+ * its plane — without the card passing props down. Throws outside a `<Card>`;
41
+ * use {@link useCardContext} where the enclosing card is optional.
42
+ */
43
+ export function useCard(): CardContextValue {
44
+ const context = useCardContext();
45
+ if (!context) {
46
+ throw new Error("useCard must be called inside a <Card>.");
47
+ }
48
+ return context;
49
+ }
50
+
51
+ /**
52
+ * The enclosing card's context, for a compound part that cannot work without one.
53
+ *
54
+ * Internal: deliberately not re-exported from `index.ts`. A caller outside the
55
+ * library wants {@link useCard}, whose error message names the hook rather than
56
+ * a part.
57
+ */
58
+ export function useCardPart(component: string): CardContextValue {
59
+ const context = useCardContext();
60
+ if (!context) {
61
+ throw new Error(`${component} must be rendered inside a <Card>.`);
62
+ }
63
+ return context;
64
+ }
@@ -0,0 +1,100 @@
1
+ import { type ReactElement, useMemo } from "react";
2
+ import { Surface, type SurfaceProps, useSurfaceContext } from "../surface";
3
+ import { resolveSurfacePlane, resolveSurfaceVariant } from "../surface/surface.variants";
4
+ import { type CardContextValue, CardProvider } from "./card.context";
5
+ import { type CardSize, cardVariants } from "./card.variants";
6
+ import { CardAction } from "./card-action";
7
+ import { CardContent } from "./card-content";
8
+ import { CardDescription } from "./card-description";
9
+ import { CardFooter } from "./card-footer";
10
+ import { CardHeader } from "./card-header";
11
+ import { CardTitle } from "./card-title";
12
+
13
+ export type CardProps = Omit<SurfaceProps, "padding"> & {
14
+ /** The inset, the gaps between the parts, and the title and description scale. */
15
+ size?: CardSize;
16
+ };
17
+
18
+ function CardRoot({ variant, material = "flat", size = "md", className, children, ...props }: CardProps): ReactElement {
19
+ const parentPlane = useSurfaceContext()?.plane ?? null;
20
+
21
+ // Resolved here as well as inside `Surface`, because the parts need the plane
22
+ // the card landed on — the title's colour and a band footer's fill key off it.
23
+ const resolved = resolveSurfaceVariant({ parentPlane, variant });
24
+ const plane = resolveSurfacePlane({ material, parentPlane, variant: resolved });
25
+
26
+ const context = useMemo<CardContextValue>(() => ({ plane, size, variant: resolved }), [plane, size, resolved]);
27
+
28
+ return (
29
+ <CardProvider value={context}>
30
+ <Surface
31
+ className={cardVariants({ size }).root({ className })}
32
+ material={material}
33
+ padding="none"
34
+ variant={resolved}
35
+ {...props}
36
+ >
37
+ {children}
38
+ </Surface>
39
+ </CardProvider>
40
+ );
41
+ }
42
+
43
+ /**
44
+ * A content surface with a header, a body and a footer.
45
+ *
46
+ * Built on `Surface`, so it takes the same four fills and steps the same way:
47
+ * a card that names no variant is the hairlined `default` at the top of a
48
+ * screen and the next fill down inside another surface. The root is
49
+ * `padding="none"`, which clips — the padding lives on the parts, so an image
50
+ * placed straight in the card reaches its side edges, and `className="pt-0"`
51
+ * bleeds it to the top.
52
+ *
53
+ * `size` reaches every part through context, so the header, the body and the
54
+ * footer line up on one inset and the title picks its own scale and colour —
55
+ * a React Native `View` cannot cascade either to a `Text`.
56
+ *
57
+ * @example
58
+ * <Card>
59
+ * <Card.Header>
60
+ * <Card.Title>Monthly report</Card.Title>
61
+ * <Card.Description>Revenue and retention for October.</Card.Description>
62
+ * <Card.Action>
63
+ * <Badge>New</Badge>
64
+ * </Card.Action>
65
+ * </Card.Header>
66
+ * <Card.Content>
67
+ * <Text.Title>$48,120</Text.Title>
68
+ * </Card.Content>
69
+ * <Card.Footer>
70
+ * <Button size="sm" variant="outline">Export</Button>
71
+ * <Button size="sm">Open</Button>
72
+ * </Card.Footer>
73
+ * </Card>
74
+ *
75
+ * @example
76
+ * <Card className="pt-0">
77
+ * <Image className="h-40 w-full" source={cover} />
78
+ * <Card.Header>
79
+ * <Card.Title>Lake Tekapo</Card.Title>
80
+ * </Card.Header>
81
+ * <Card.Footer variant="band">
82
+ * <Button size="sm">Book</Button>
83
+ * </Card.Footer>
84
+ * </Card>
85
+ */
86
+ export const Card = Object.assign(CardRoot, {
87
+ /** The opening block. Stacks a title and description, and pins any `Card.Action` to the top right. */
88
+ Header: CardHeader,
89
+ /** The card's heading, on its plane's foreground token. Announced as a header. */
90
+ Title: CardTitle,
91
+ /** The muted supporting line under the title. */
92
+ Description: CardDescription,
93
+ /** A control pinned to the header's top-right corner, wherever in the header it is written. */
94
+ Action: CardAction,
95
+ /** The card's body, inset to line up with the header and footer. */
96
+ Content: CardContent,
97
+ /** The last row — a row of actions, or with `variant="band"` a strip set into the card. */
98
+ Footer: CardFooter,
99
+ displayName: "DelacourUI.Card",
100
+ });
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ViewProps } from "react-native";
3
+ import type { TextPresetProps } from "../text";
4
+
5
+ /**
6
+ * The shape of a card's layout parts — `Header`, `Action` and `Content`, and
7
+ * extended by `Footer` — so it lives in a leaf rather than in one of them
8
+ * arbitrarily.
9
+ */
10
+ export type CardSlotProps = ViewProps & { className?: string; children?: ReactNode };
11
+
12
+ /** The shape of a card's two text parts, `Title` and `Description`. */
13
+ export type CardTextProps = TextPresetProps;
@@ -0,0 +1,240 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createElement, Fragment, type ReactNode } from "react";
3
+ import { SURFACE_FILLED_VARIANTS } from "../surface/surface.variants";
4
+ import {
5
+ CARD_FOOTER_VARIANTS,
6
+ CARD_PLANES,
7
+ CARD_SIZES,
8
+ cardVariants,
9
+ resolveCardFooterFill,
10
+ splitCardHeaderChildren,
11
+ } from "./card.variants";
12
+
13
+ /** Every slot the variant function can produce, one call per combination. */
14
+ function everyCombination(): ReturnType<typeof cardVariants>[] {
15
+ return CARD_SIZES.flatMap((size) =>
16
+ CARD_PLANES.flatMap((plane) =>
17
+ CARD_FOOTER_VARIANTS.flatMap((footer) =>
18
+ SURFACE_FILLED_VARIANTS.map((footerFill) => cardVariants({ footer, footerFill, plane, size }))
19
+ )
20
+ )
21
+ );
22
+ }
23
+
24
+ /** The numeric step of the first `${prefix}-N` utility in a class string. */
25
+ function step(cls: string, prefix: string): string | undefined {
26
+ return cls.match(new RegExp(`(?:^|\\s)${prefix}-(\\d+(?:\\.5)?)(?:\\s|$)`))?.[1];
27
+ }
28
+
29
+ describe("cardVariants root slot", () => {
30
+ // The padding lives on the parts, so the root carries only the vertical
31
+ // rhythm between them — horizontal padding here would stop a bled image
32
+ // reaching the card's side edges.
33
+ test("carries no horizontal padding at any size", () => {
34
+ for (const size of CARD_SIZES) {
35
+ const cls = cardVariants({ size }).root();
36
+ expect(cls).not.toMatch(/(^|\s)(p|px|pl|pr)-/);
37
+ }
38
+ });
39
+
40
+ test("spaces its parts by the same step it pads its top and bottom", () => {
41
+ for (const size of CARD_SIZES) {
42
+ const cls = cardVariants({ size }).root();
43
+ expect(step(cls, "pt")).toBeDefined();
44
+ expect(step(cls, "pb")).toBe(step(cls, "pt"));
45
+ expect(step(cls, "gap")).toBe(step(cls, "pt"));
46
+ }
47
+ });
48
+
49
+ // The card sits on a `padding="none"` surface, whose root carries `p-0`.
50
+ // Uniwind resolves that shorthand over a `py-*` on the same view, so the
51
+ // card's vertical padding vanished on a device; the longhands hold.
52
+ test("pads top and bottom with longhands, never the py shorthand", () => {
53
+ for (const size of CARD_SIZES) {
54
+ expect(cardVariants({ size }).root()).not.toMatch(/(^|\s)py-/);
55
+ }
56
+ });
57
+
58
+ // Rule 1: a React Native View does not cascade colour to a Text descendant.
59
+ test("carries no text treatment on the root, header, content or footer", () => {
60
+ for (const slots of everyCombination()) {
61
+ for (const cls of [slots.root(), slots.header(), slots.content(), slots.footer()]) {
62
+ expect(cls).not.toMatch(/(^|\s)text-/);
63
+ }
64
+ }
65
+ });
66
+
67
+ test("merges an incoming className last", () => {
68
+ expect(cardVariants().root({ className: "pt-0" })).toContain("pt-0");
69
+ expect(cardVariants().root({ className: "gap-8" })).not.toMatch(/(^|\s)gap-4(\s|$)/);
70
+ });
71
+ });
72
+
73
+ describe("cardVariants size", () => {
74
+ test("defaults to md", () => {
75
+ expect(cardVariants().root()).toBe(cardVariants({ size: "md" }).root());
76
+ expect(cardVariants().title()).toBe(cardVariants({ size: "md" }).title());
77
+ });
78
+
79
+ test("matches Surface's padding steps: 3, 4 and 6", () => {
80
+ expect(step(cardVariants({ size: "sm" }).root(), "pt")).toBe("3");
81
+ expect(step(cardVariants({ size: "md" }).root(), "pt")).toBe("4");
82
+ expect(step(cardVariants({ size: "lg" }).root(), "pt")).toBe("6");
83
+ });
84
+
85
+ // The header, the body and the footer have to line up on one inset, or the
86
+ // title sits a few points off the button beneath it.
87
+ test("header, content and footer share one horizontal inset, equal to the vertical one", () => {
88
+ for (const size of CARD_SIZES) {
89
+ const slots = cardVariants({ size });
90
+ const inset = step(slots.root(), "pt");
91
+ expect(step(slots.header(), "px")).toBe(inset);
92
+ expect(step(slots.content(), "px")).toBe(inset);
93
+ expect(step(slots.footer(), "px")).toBe(inset);
94
+ }
95
+ });
96
+
97
+ test("steps the title and description type scale with the size", () => {
98
+ expect(cardVariants({ size: "sm" }).title()).toContain("text-sm");
99
+ expect(cardVariants({ size: "md" }).title()).toContain("text-base");
100
+ expect(cardVariants({ size: "lg" }).title()).toContain("text-lg");
101
+ expect(cardVariants({ size: "sm" }).description()).toContain("text-xs");
102
+ expect(cardVariants({ size: "md" }).description()).toContain("text-sm");
103
+ expect(cardVariants({ size: "lg" }).description()).toContain("text-base");
104
+ });
105
+
106
+ test("gives every size a distinct root", () => {
107
+ const seen = new Set(CARD_SIZES.map((size) => cardVariants({ size }).root()));
108
+ expect(seen.size).toBe(CARD_SIZES.length);
109
+ });
110
+ });
111
+
112
+ describe("cardVariants title and description", () => {
113
+ test("the title is the X-foreground token of the plane it sits on", () => {
114
+ expect(cardVariants({ plane: "default" }).title()).toContain("text-card-foreground");
115
+ expect(cardVariants({ plane: "secondary" }).title()).toContain("text-secondary-foreground");
116
+ expect(cardVariants({ plane: "tertiary" }).title()).toContain("text-tertiary-foreground");
117
+ expect(cardVariants({ plane: "none" }).title()).toContain("text-foreground");
118
+ });
119
+
120
+ test("the title is semibold at every size", () => {
121
+ for (const size of CARD_SIZES) {
122
+ expect(cardVariants({ size }).title()).toContain("font-semibold");
123
+ }
124
+ });
125
+
126
+ test("the description sits on the muted token on every plane", () => {
127
+ for (const plane of CARD_PLANES) {
128
+ expect(cardVariants({ plane }).description()).toContain("text-muted-foreground");
129
+ }
130
+ });
131
+
132
+ // Two colour classes on one Text and tailwind-merge keeps only the last —
133
+ // which would be a page colour, not the plane's.
134
+ test("the title carries exactly one text colour", () => {
135
+ for (const slots of everyCombination()) {
136
+ const colours = slots
137
+ .title()
138
+ .split(/\s+/)
139
+ .filter((name) => /^text-(card|secondary|tertiary|foreground|muted)/.test(name));
140
+ expect(colours).toHaveLength(1);
141
+ }
142
+ });
143
+ });
144
+
145
+ describe("cardVariants header", () => {
146
+ test("lays the text column and the action out in a row", () => {
147
+ const slots = cardVariants();
148
+ expect(slots.header()).toContain("flex-row");
149
+ expect(slots.headerText()).toContain("flex-1");
150
+ expect(slots.action()).toContain("shrink-0");
151
+ });
152
+ });
153
+
154
+ describe("cardVariants footer", () => {
155
+ test("defaults to the plain row of actions", () => {
156
+ expect(cardVariants().footer()).toBe(cardVariants({ footer: "default" }).footer());
157
+ expect(cardVariants().footer()).toContain("flex-row");
158
+ expect(cardVariants().footer()).not.toContain("border-t");
159
+ });
160
+
161
+ test("a band draws a rule across its top and a fill of its own", () => {
162
+ for (const footerFill of SURFACE_FILLED_VARIANTS) {
163
+ const cls = cardVariants({ footer: "band", footerFill }).footer();
164
+ expect(cls).toContain("border-t");
165
+ expect(cls).toContain("border-border");
166
+ expect(cls).toMatch(/(^|\s)bg-/);
167
+ }
168
+ });
169
+
170
+ test("only a band paints a fill", () => {
171
+ for (const footerFill of SURFACE_FILLED_VARIANTS) {
172
+ expect(cardVariants({ footer: "default", footerFill }).footer()).not.toMatch(/(^|\s)bg-/);
173
+ }
174
+ });
175
+
176
+ test("maps each band fill to its surface token", () => {
177
+ expect(cardVariants({ footer: "band", footerFill: "default" }).footer()).toContain("bg-card");
178
+ expect(cardVariants({ footer: "band", footerFill: "secondary" }).footer()).toContain("bg-secondary");
179
+ expect(cardVariants({ footer: "band", footerFill: "tertiary" }).footer()).toContain("bg-tertiary");
180
+ });
181
+
182
+ // A band is set INTO the card: it pulls down over the root's bottom padding
183
+ // by exactly that padding, and restates it inside, so it meets the card's
184
+ // bottom edge and its own content keeps the same breathing room.
185
+ test("a band cancels the root's bottom padding and restates it inside", () => {
186
+ for (const size of CARD_SIZES) {
187
+ const inset = step(cardVariants({ size }).root(), "pb");
188
+ const cls = cardVariants({ footer: "band", size }).footer();
189
+ expect(cls).toMatch(new RegExp(`(^|\\s)-mb-${inset?.replace(".", "\\.")}(\\s|$)`));
190
+ expect(step(cls, "py")).toBe(inset);
191
+ }
192
+ });
193
+ });
194
+
195
+ describe("resolveCardFooterFill", () => {
196
+ test("a band is a step from the card's own fill, never the same one", () => {
197
+ for (const plane of SURFACE_FILLED_VARIANTS) {
198
+ expect(resolveCardFooterFill(plane)).not.toBe(plane);
199
+ }
200
+ });
201
+
202
+ test("a card on the default fill gets a secondary band", () => {
203
+ expect(resolveCardFooterFill("default")).toBe("secondary");
204
+ });
205
+
206
+ // A transparent card on the page paints nothing, so its band steps from the
207
+ // page itself — the card fill, a step above it.
208
+ test("a card with no plane of its own gets a default band", () => {
209
+ expect(resolveCardFooterFill(null)).toBe("default");
210
+ });
211
+ });
212
+
213
+ describe("splitCardHeaderChildren", () => {
214
+ function Action(): null {
215
+ return null;
216
+ }
217
+ const isAction = (node: ReactNode): boolean =>
218
+ typeof node === "object" && node !== null && "type" in node && node.type === Action;
219
+
220
+ test("puts everything in the text column when there is no action", () => {
221
+ const { text, actions } = splitCardHeaderChildren(["Title", createElement(Fragment, null, "x")], isAction);
222
+ expect(text).toHaveLength(2);
223
+ expect(actions).toHaveLength(0);
224
+ });
225
+
226
+ test("lifts every action out of the text column, keeping order", () => {
227
+ const first = createElement(Action, { key: "a" });
228
+ const second = createElement(Action, { key: "b" });
229
+ const { text, actions } = splitCardHeaderChildren([first, "Title", second, "Description"], isAction);
230
+ expect(text).toEqual(["Title", "Description"]);
231
+ expect(actions).toHaveLength(2);
232
+ });
233
+
234
+ // A conditional action leaves `false` behind; it must not count as text.
235
+ test("drops the nulls and booleans a conditional child leaves behind", () => {
236
+ const { text, actions } = splitCardHeaderChildren(["Title", false, null, undefined], isAction);
237
+ expect(text).toEqual(["Title"]);
238
+ expect(actions).toHaveLength(0);
239
+ });
240
+ });
@@ -0,0 +1,177 @@
1
+ import { Children, type ReactNode } from "react";
2
+ import type { VariantProps } from "tailwind-variants";
3
+ import { tv } from "../../lib/tv";
4
+ import { resolveSurfaceVariant, type SurfacePlane } from "../surface/surface.variants";
5
+
6
+ export const CARD_SIZES = ["sm", "md", "lg"] as const;
7
+
8
+ /** `default` is a row of actions; `band` sets the footer into the card as a strip of its own. */
9
+ export const CARD_FOOTER_VARIANTS = ["default", "band"] as const;
10
+
11
+ /**
12
+ * The planes a card's title can sit on: the three surface fills, plus `none`
13
+ * for a transparent card on the page, which has no fill of its own.
14
+ */
15
+ export const CARD_PLANES = ["default", "secondary", "tertiary", "none"] as const;
16
+
17
+ export type CardSize = (typeof CARD_SIZES)[number];
18
+ export type CardFooterVariant = (typeof CARD_FOOTER_VARIANTS)[number];
19
+ export type CardPlane = (typeof CARD_PLANES)[number];
20
+
21
+ /**
22
+ * Styling for every part of a card.
23
+ *
24
+ * The card's own surface — fill, hairline, corner, clip — is `Surface`'s, at
25
+ * `padding="none"`. What this adds is the rhythm between the parts, and it
26
+ * puts the padding on the PARTS rather than on the root. The root holds only
27
+ * `pt`/`pb` and a `gap` of the same step; the header, content and footer each
28
+ * carry the same step as `px`. So an image placed straight in the card reaches
29
+ * both side edges with nothing to undo, and `className="pt-0"` on the card is
30
+ * all it takes to bleed one to the top as well.
31
+ *
32
+ * `pt`/`pb` rather than `py`, and that is load-bearing: the root is also the
33
+ * surface's, which carries `p-0` at `padding="none"`, and Uniwind resolves that
34
+ * shorthand over a `py-*` on the same view. The card rendered with no vertical
35
+ * padding at all until the longhands replaced it.
36
+ *
37
+ * Size is one axis for five numbers — the vertical padding, the gap, the
38
+ * horizontal inset and the title and description type scale — and the steps
39
+ * are Surface's own `sm`/`md`/`lg` (3, 4, 6), so a card and a surface beside
40
+ * it at the same size hold their content at the same distance from the edge.
41
+ *
42
+ * The title colour is keyed on `plane`, the fill the card resolved to, so a
43
+ * card nested in another reads `text-secondary-foreground` on the secondary
44
+ * fill rather than the card's token on something else. The classes are written
45
+ * out per plane because Tailwind's scanner cannot see a `text-${token}` built at
46
+ * runtime.
47
+ *
48
+ * A `band` footer is set INTO the card: `-mb-*` pulls it down over the root's
49
+ * bottom padding by exactly that padding and `py-*` restates it inside, so it
50
+ * meets the card's bottom edge — the root clips, so it takes the card's own
51
+ * corners — while its content keeps the same breathing room. Its fill is the
52
+ * next rung of the surface ladder from the card's, see
53
+ * {@link resolveCardFooterFill}.
54
+ *
55
+ * The root, header, content and footer carry no `text-*` (rule 1): a `View`
56
+ * does not cascade colour to a `Text`, so the tokens live on the title and
57
+ * description slots.
58
+ *
59
+ * Free of React Native imports so it stays unit-testable — `bun test` cannot
60
+ * parse React Native's Flow-typed source. See AGENTS.md.
61
+ */
62
+ export const cardVariants = tv({
63
+ slots: {
64
+ root: "",
65
+ header: "flex-row items-start gap-3",
66
+ /** The column holding the title and description, beside any action. */
67
+ headerText: "flex-1",
68
+ action: "shrink-0 flex-row items-center self-start",
69
+ title: "font-semibold",
70
+ description: "text-muted-foreground",
71
+ content: "",
72
+ footer: "flex-row flex-wrap items-center gap-2",
73
+ },
74
+ variants: {
75
+ size: {
76
+ sm: {
77
+ root: "gap-3 pt-3 pb-3",
78
+ header: "px-3",
79
+ headerText: "gap-0.5",
80
+ content: "gap-2 px-3",
81
+ footer: "px-3",
82
+ title: "text-sm",
83
+ description: "text-xs",
84
+ },
85
+ md: {
86
+ root: "gap-4 pt-4 pb-4",
87
+ header: "px-4",
88
+ headerText: "gap-1",
89
+ content: "gap-3 px-4",
90
+ footer: "px-4",
91
+ title: "text-base",
92
+ description: "text-sm",
93
+ },
94
+ lg: {
95
+ root: "gap-6 pt-6 pb-6",
96
+ header: "px-6",
97
+ headerText: "gap-1.5",
98
+ content: "gap-4 px-6",
99
+ footer: "px-6",
100
+ title: "text-lg",
101
+ description: "text-base",
102
+ },
103
+ },
104
+ plane: {
105
+ default: { title: "text-card-foreground" },
106
+ secondary: { title: "text-secondary-foreground" },
107
+ tertiary: { title: "text-tertiary-foreground" },
108
+ none: { title: "text-foreground" },
109
+ },
110
+ footer: {
111
+ default: {},
112
+ band: { footer: "border-t border-border" },
113
+ },
114
+ // Read only by a band; the plain footer paints nothing whatever this is.
115
+ footerFill: {
116
+ default: {},
117
+ secondary: {},
118
+ tertiary: {},
119
+ },
120
+ },
121
+ compoundVariants: [
122
+ { footer: "band", size: "sm", class: { footer: "-mb-3 py-3" } },
123
+ { footer: "band", size: "md", class: { footer: "-mb-4 py-4" } },
124
+ { footer: "band", size: "lg", class: { footer: "-mb-6 py-6" } },
125
+ { footer: "band", footerFill: "default", class: { footer: "bg-card" } },
126
+ { footer: "band", footerFill: "secondary", class: { footer: "bg-secondary" } },
127
+ { footer: "band", footerFill: "tertiary", class: { footer: "bg-tertiary" } },
128
+ ],
129
+ defaultVariants: {
130
+ size: "md",
131
+ plane: "default",
132
+ footer: "default",
133
+ footerFill: "secondary",
134
+ },
135
+ });
136
+
137
+ export type CardVariantProps = VariantProps<typeof cardVariants>;
138
+
139
+ /**
140
+ * The fill a `band` footer paints: the next rung of the surface ladder from the
141
+ * card's own, so the band is always told apart from the card it is set into.
142
+ *
143
+ * It is the same step a surface nested in the card would take, which is the
144
+ * point — a band is the card's plane stepped once, not a colour of its own. A
145
+ * transparent card on the page has no plane, so its band takes the card fill.
146
+ */
147
+ export function resolveCardFooterFill(plane: SurfacePlane | null): SurfacePlane {
148
+ const next = resolveSurfaceVariant({ parentPlane: plane });
149
+ return next === "transparent" ? "default" : next;
150
+ }
151
+
152
+ /**
153
+ * Splits a header's children into its text column and its trailing actions.
154
+ *
155
+ * A header is a row — the title and description stacked on the left, an action
156
+ * pinned to the top right — and React Native has no grid to place a child by
157
+ * type. So the header walks its children and lifts every action out, keeping
158
+ * the rest in order for the column. `Children.toArray` drops the nulls and
159
+ * booleans a conditional child leaves behind, so an action rendered only some
160
+ * of the time does not leave a hole in either list.
161
+ *
162
+ * `isAction` is passed in rather than imported so this stays free of the part
163
+ * it looks for, and of React Native with it.
164
+ */
165
+ export function splitCardHeaderChildren(
166
+ children: ReactNode,
167
+ isAction: (node: ReactNode) => boolean
168
+ ): { text: ReactNode[]; actions: ReactNode[] } {
169
+ const text: ReactNode[] = [];
170
+ const actions: ReactNode[] = [];
171
+
172
+ for (const child of Children.toArray(children)) {
173
+ (isAction(child) ? actions : text).push(child);
174
+ }
175
+
176
+ return { actions, text };
177
+ }
@@ -0,0 +1,15 @@
1
+ export { Card, type CardProps } from "./card";
2
+ export { type CardContextValue, CardProvider, useCard, useCardContext } from "./card.context";
3
+ export type { CardSlotProps, CardTextProps } from "./card.types";
4
+ export {
5
+ CARD_FOOTER_VARIANTS,
6
+ CARD_PLANES,
7
+ CARD_SIZES,
8
+ type CardFooterVariant,
9
+ type CardPlane,
10
+ type CardSize,
11
+ type CardVariantProps,
12
+ cardVariants,
13
+ resolveCardFooterFill,
14
+ } from "./card.variants";
15
+ export type { CardFooterProps } from "./card-footer";