@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,263 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { ICON_SIZE_TOKENS } from "../../styles/tokens";
3
+ import { LIST_GROUP_SIZES, listGroupVariants } from "../list-group/list-group.variants";
4
+ import {
5
+ ITEM_MEDIA_VARIANTS,
6
+ ITEM_ORIENTATIONS,
7
+ ITEM_SIZES,
8
+ ITEM_VARIANTS,
9
+ itemVariants,
10
+ resolveItemFeedback,
11
+ resolveItemRender,
12
+ resolveItemSize,
13
+ resolveItemSurface,
14
+ } from "./item.variants";
15
+
16
+ /** Pulls the horizontal padding step out of a class string — `px-4` yields 4. */
17
+ function paddingStep(cls: string): string | undefined {
18
+ return cls.match(/\bpx-(\d+(?:\.\d+)?)\b/)?.[1];
19
+ }
20
+
21
+ /** Position of a class string's `size-icon-*` token on the shared icon scale. */
22
+ function iconStep(cls: string): number {
23
+ const token = cls.match(/\bsize-(icon-[\w-]+)\b/)?.[1];
24
+ return ICON_SIZE_TOKENS.indexOf(token as (typeof ICON_SIZE_TOKENS)[number]);
25
+ }
26
+
27
+ describe("resolveItemSurface", () => {
28
+ test("a standalone item keeps its own variant", () => {
29
+ for (const variant of ITEM_VARIANTS) {
30
+ expect(resolveItemSurface(variant, false)).toBe(variant);
31
+ }
32
+ });
33
+
34
+ // The group owns the surface: a border or a fill on each row would draw a
35
+ // card inside a card.
36
+ test("an item inside a ListGroup is always grouped, whatever its variant", () => {
37
+ for (const variant of ITEM_VARIANTS) {
38
+ expect(resolveItemSurface(variant, true)).toBe("grouped");
39
+ }
40
+ });
41
+ });
42
+
43
+ describe("resolveItemSize", () => {
44
+ test("an explicit size wins over the group's", () => {
45
+ expect(resolveItemSize("lg", "sm")).toBe("lg");
46
+ });
47
+
48
+ test("inherits the enclosing group's size when unset", () => {
49
+ for (const size of LIST_GROUP_SIZES) {
50
+ expect(resolveItemSize(undefined, size)).toBe(size);
51
+ }
52
+ });
53
+
54
+ test("falls back to md outside a group", () => {
55
+ expect(resolveItemSize(undefined, undefined)).toBe("md");
56
+ });
57
+ });
58
+
59
+ describe("resolveItemFeedback", () => {
60
+ test("a caller's feedback always wins", () => {
61
+ expect(resolveItemFeedback("none", true)).toBe("none");
62
+ expect(resolveItemFeedback("scale-fade", false)).toBe("scale-fade");
63
+ });
64
+
65
+ // A full-bleed row that scales reads as the whole card flexing.
66
+ test("fades inside a group and scales standalone", () => {
67
+ expect(resolveItemFeedback(undefined, true)).toBe("fade");
68
+ expect(resolveItemFeedback(undefined, false)).toBe("scale");
69
+ });
70
+ });
71
+
72
+ describe("resolveItemRender", () => {
73
+ const noop = () => {};
74
+
75
+ test("is static without a press handler, disabled or not", () => {
76
+ expect(resolveItemRender({ isDisabled: false })).toBe("static");
77
+ expect(resolveItemRender({ isDisabled: true })).toBe("static");
78
+ });
79
+
80
+ test("is a pressable with a handler", () => {
81
+ expect(resolveItemRender({ isDisabled: false, onPress: noop })).toBe("pressable");
82
+ expect(resolveItemRender({ isDisabled: false, onLongPress: noop })).toBe("pressable");
83
+ });
84
+
85
+ // Pressable composes its resting opacity with the press animation, so a
86
+ // disabled Pressable dims through `opacity-50` like any other row.
87
+ test("stays a pressable when disabled", () => {
88
+ expect(resolveItemRender({ isDisabled: true, onPress: noop })).toBe("pressable");
89
+ expect(resolveItemRender({ isDisabled: true, onLongPress: noop })).toBe("pressable");
90
+ });
91
+ });
92
+
93
+ describe("itemVariants root slot", () => {
94
+ test("gives every surface a distinct class string", () => {
95
+ const surfaces = [...ITEM_VARIANTS, "grouped"] as const;
96
+ const seen = new Set(surfaces.map((surface) => itemVariants({ surface }).root()));
97
+ expect(seen.size).toBe(surfaces.length);
98
+ });
99
+
100
+ test("maps each surface", () => {
101
+ expect(itemVariants({ surface: "default" }).root()).toContain("bg-transparent");
102
+ expect(itemVariants({ surface: "outline" }).root()).toContain("border-border");
103
+ expect(itemVariants({ surface: "muted" }).root()).toContain("bg-muted");
104
+ });
105
+
106
+ test("a standalone surface is rounded; a grouped one is not", () => {
107
+ for (const surface of ITEM_VARIANTS) {
108
+ expect(itemVariants({ surface }).root()).toMatch(/\brounded-/);
109
+ }
110
+ expect(itemVariants({ surface: "grouped" }).root()).not.toMatch(/\brounded-/);
111
+ expect(itemVariants({ surface: "grouped" }).root()).not.toContain("border-border");
112
+ });
113
+
114
+ // Without it a short row leaves the press feedback ending mid-card.
115
+ test("a grouped row spans the group", () => {
116
+ expect(itemVariants({ surface: "grouped" }).root()).toContain("w-full");
117
+ });
118
+
119
+ test("holds no text colour — a View cannot cascade one", () => {
120
+ for (const surface of [...ITEM_VARIANTS, "grouped"] as const) {
121
+ expect(itemVariants({ surface }).root()).not.toMatch(/\btext-/);
122
+ }
123
+ });
124
+
125
+ test("orientation picks the main axis", () => {
126
+ expect(itemVariants({ orientation: "horizontal" }).root()).toContain("flex-row");
127
+ expect(itemVariants({ orientation: "vertical" }).root()).toContain("flex-col");
128
+ expect(itemVariants({ orientation: "vertical" }).root()).not.toContain("flex-row");
129
+ });
130
+
131
+ test("disabled dims the row", () => {
132
+ expect(itemVariants({ isDisabled: true }).root()).toContain("opacity-50");
133
+ expect(itemVariants({ isDisabled: false }).root()).not.toContain("opacity-50");
134
+ });
135
+
136
+ test("selected lays the accent over every surface, the muted fill included", () => {
137
+ for (const surface of [...ITEM_VARIANTS, "grouped"] as const) {
138
+ const cls = itemVariants({ isSelected: true, surface }).root();
139
+ expect(cls).toContain("bg-accent");
140
+ expect(cls).not.toContain("bg-muted");
141
+ expect(cls).not.toContain("bg-transparent");
142
+ }
143
+ });
144
+ });
145
+
146
+ describe("itemVariants sizes", () => {
147
+ // This is what makes an Item drop into a ListGroup: the group insets its
148
+ // dividers by its own row padding, so the item's padding has to match it.
149
+ test("row padding matches ListGroup's divider inset at every size", () => {
150
+ for (const size of ITEM_SIZES) {
151
+ const itemPadding = paddingStep(itemVariants({ size }).root());
152
+ const dividerInset = listGroupVariants({ size })
153
+ .divider()
154
+ .match(/\bmx-(\d+(?:\.\d+)?)\b/)?.[1];
155
+ expect(itemPadding).toBeDefined();
156
+ expect(itemPadding).toBe(dividerInset);
157
+ }
158
+ });
159
+
160
+ test("row metrics match a ListGroup row at every size", () => {
161
+ for (const size of ITEM_SIZES) {
162
+ const item = itemVariants({ size }).root();
163
+ for (const cls of listGroupVariants({ size }).item().split(" ")) {
164
+ if (/^(min-h|gap|px|py)-/.test(cls)) expect(item).toContain(cls);
165
+ }
166
+ }
167
+ });
168
+
169
+ test("title and description step up with size", () => {
170
+ expect(itemVariants({ size: "sm" }).title()).toContain("text-sm");
171
+ expect(itemVariants({ size: "md" }).title()).toContain("text-base");
172
+ expect(itemVariants({ size: "lg" }).title()).toContain("text-lg");
173
+ expect(itemVariants({ size: "sm" }).description()).toContain("text-xs");
174
+ expect(itemVariants({ size: "md" }).description()).toContain("text-sm");
175
+ expect(itemVariants({ size: "lg" }).description()).toContain("text-base");
176
+ });
177
+
178
+ test("text carries its own colour tokens", () => {
179
+ expect(itemVariants().title()).toContain("text-foreground");
180
+ expect(itemVariants().description()).toContain("text-muted-foreground");
181
+ });
182
+
183
+ test("every media icon names an ascending step on the shared icon scale", () => {
184
+ for (const mediaVariant of ITEM_MEDIA_VARIANTS) {
185
+ if (mediaVariant === "image") continue;
186
+ const steps = ITEM_SIZES.map((size) => iconStep(itemVariants({ mediaVariant, size }).mediaIcon()));
187
+ for (const step of steps) expect(step).toBeGreaterThanOrEqual(0);
188
+ expect([...steps].sort((a, b) => a - b)).toEqual(steps);
189
+ expect(new Set(steps).size).toBe(steps.length);
190
+ }
191
+ });
192
+
193
+ test("a bare media icon matches ListGroup's prefix icon", () => {
194
+ for (const size of ITEM_SIZES) {
195
+ expect(itemVariants({ mediaVariant: "default", size }).mediaIcon()).toBe(
196
+ listGroupVariants({ size }).prefixIcon()
197
+ );
198
+ }
199
+ });
200
+ });
201
+
202
+ describe("itemVariants media slot", () => {
203
+ test("the icon tile has a fill and a corner", () => {
204
+ const cls = itemVariants({ mediaVariant: "icon" }).media();
205
+ expect(cls).toContain("bg-muted");
206
+ expect(cls).toMatch(/\brounded-/);
207
+ });
208
+
209
+ test("an image clips its child to the corner", () => {
210
+ expect(itemVariants({ mediaVariant: "image" }).media()).toContain("overflow-hidden");
211
+ });
212
+
213
+ test("the icon tile and image are fixed squares that grow with size", () => {
214
+ for (const mediaVariant of ["icon", "image"] as const) {
215
+ const edges = ITEM_SIZES.map((size) => {
216
+ const edge = itemVariants({ mediaVariant, size })
217
+ .media()
218
+ .match(/\bsize-(\d+)\b/)?.[1];
219
+ return Number(edge);
220
+ });
221
+ for (const edge of edges) expect(edge).toBeGreaterThan(0);
222
+ expect([...edges].sort((a, b) => a - b)).toEqual(edges);
223
+ }
224
+ });
225
+
226
+ test("a tile stays visible on the muted and the selected surface", () => {
227
+ for (const mediaVariant of ["icon", "image"] as const) {
228
+ for (const cls of [
229
+ itemVariants({ mediaVariant, surface: "muted" }).media(),
230
+ itemVariants({ isSelected: true, mediaVariant }).media(),
231
+ ]) {
232
+ expect(cls).toContain("bg-background");
233
+ expect(cls).not.toContain("bg-muted");
234
+ }
235
+ }
236
+ });
237
+
238
+ test("a bare media slot draws nothing of its own", () => {
239
+ const cls = itemVariants({ mediaVariant: "default" }).media();
240
+ expect(cls).not.toMatch(/\bbg-/);
241
+ expect(cls).not.toMatch(/\bsize-\d/);
242
+ });
243
+ });
244
+
245
+ describe("itemVariants orientation", () => {
246
+ // A `flex-1` column in an auto-height parent collapses to nothing in Yoga.
247
+ test("content only flexes along a row", () => {
248
+ expect(itemVariants({ orientation: "horizontal" }).content()).toContain("flex-1");
249
+ expect(itemVariants({ orientation: "vertical" }).content()).not.toContain("flex-1");
250
+ });
251
+
252
+ test("header and footer are full-width strips on both axes", () => {
253
+ for (const orientation of ITEM_ORIENTATIONS) {
254
+ expect(itemVariants({ orientation }).header()).toContain("w-full");
255
+ expect(itemVariants({ orientation }).footer()).toContain("w-full");
256
+ }
257
+ });
258
+
259
+ test("a group runs down by default and across when horizontal", () => {
260
+ expect(itemVariants({ groupOrientation: "vertical" }).group()).toContain("flex-col");
261
+ expect(itemVariants({ groupOrientation: "horizontal" }).group()).toContain("flex-row");
262
+ });
263
+ });
@@ -0,0 +1,200 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+ import type { PressableFeedback } from "../pressable/pressable.variants";
4
+
5
+ export const ITEM_VARIANTS = ["default", "outline", "muted"] as const;
6
+
7
+ export const ITEM_SIZES = ["sm", "md", "lg"] as const;
8
+
9
+ export const ITEM_ORIENTATIONS = ["horizontal", "vertical"] as const;
10
+
11
+ export const ITEM_MEDIA_VARIANTS = ["default", "icon", "image"] as const;
12
+
13
+ export type ItemVariant = (typeof ITEM_VARIANTS)[number];
14
+ export type ItemSize = (typeof ITEM_SIZES)[number];
15
+ export type ItemOrientation = (typeof ITEM_ORIENTATIONS)[number];
16
+ export type ItemMediaVariant = (typeof ITEM_MEDIA_VARIANTS)[number];
17
+
18
+ /**
19
+ * The surface a row actually draws.
20
+ *
21
+ * One of the three variants a caller picks, or `grouped` — the surface an item
22
+ * takes inside a `ListGroup`, where the group already draws the card and the
23
+ * row must draw nothing but itself.
24
+ */
25
+ export type ItemSurface = ItemVariant | "grouped";
26
+
27
+ /** Theme token an icon in `Item.Media` inherits. */
28
+ export const ITEM_MEDIA_ICON_TOKEN = "foreground";
29
+
30
+ /** Theme token an icon in `Item.Actions` inherits — a trailing hint, not the row's subject. */
31
+ export const ITEM_ACTIONS_ICON_TOKEN = "muted-foreground";
32
+
33
+ /**
34
+ * The surface an item draws, given its variant and whether a `ListGroup`
35
+ * encloses it.
36
+ *
37
+ * Inside a group the variant is ignored: the group owns the border, the fill
38
+ * and the corner, and a row repeating any of them would draw a card inside a
39
+ * card.
40
+ */
41
+ export function resolveItemSurface(variant: ItemVariant, isInGroup: boolean): ItemSurface {
42
+ return isInGroup ? "grouped" : variant;
43
+ }
44
+
45
+ /**
46
+ * An item's size: its own when set, else the enclosing `ListGroup`'s, else
47
+ * `md`.
48
+ *
49
+ * The two share one scale on purpose, so an item dropped into a `sm` group
50
+ * comes out `sm` with nothing said at the call site.
51
+ */
52
+ export function resolveItemSize(size: ItemSize | undefined, groupSize: ItemSize | undefined): ItemSize {
53
+ return size ?? groupSize ?? "md";
54
+ }
55
+
56
+ /**
57
+ * The press feedback for an item, when the caller has not named one.
58
+ *
59
+ * A full-bleed row inside a group fades, because a row that scales reads as
60
+ * the whole card flexing. A standalone item is its own card, so it scales the
61
+ * way a card does.
62
+ */
63
+ export function resolveItemFeedback(feedback: PressableFeedback | undefined, isInGroup: boolean): PressableFeedback {
64
+ return feedback ?? (isInGroup ? "fade" : "scale");
65
+ }
66
+
67
+ /**
68
+ * What an item renders as.
69
+ *
70
+ * - `pressable` — it has a handler: a `Pressable`, announced as a button. A
71
+ * disabled one keeps the role, reports `disabled`, and dims through
72
+ * `opacity-50`, which `Pressable` composes with its press animation.
73
+ * - `static` — no handler: a plain view with no role. A static row announcing
74
+ * itself as a button is a lie VoiceOver tells on every swipe.
75
+ */
76
+ export type ItemRender = "pressable" | "static";
77
+
78
+ export function resolveItemRender(options: {
79
+ onPress?: () => void;
80
+ onLongPress?: () => void;
81
+ isDisabled: boolean;
82
+ }): ItemRender {
83
+ const hasHandler = options.onPress !== undefined || options.onLongPress !== undefined;
84
+ return hasHandler ? "pressable" : "static";
85
+ }
86
+
87
+ /**
88
+ * Styling for every part of an item.
89
+ *
90
+ * Row metrics are the same numbers a `ListGroup` row uses, per size — asserted
91
+ * as a pair in the tests — because a group insets its dividers by its own row
92
+ * padding. An item whose padding drifted from that would sit in a group with
93
+ * its text and its divider out of line.
94
+ *
95
+ * `surface` rather than `variant` is the axis here, because inside a group the
96
+ * variant a caller passed does not decide what is drawn. `resolveItemSurface`
97
+ * is where that decision lives.
98
+ *
99
+ * The horizontal root wraps, and `header` and `footer` are `w-full`: a strip
100
+ * then takes a line of its own above or below the media, text and actions, on
101
+ * either orientation, with no extra prop. `content` only flexes along a row — a
102
+ * `flex-1` column in an auto-height parent collapses to nothing in Yoga.
103
+ *
104
+ * No slot but the two text slots holds a `text-*` colour — a React Native
105
+ * `View` does not cascade one to a `Text`.
106
+ *
107
+ * Free of React Native imports so it stays unit-testable — `bun test` cannot
108
+ * parse React Native's Flow-typed source. See AGENTS.md.
109
+ */
110
+ export const itemVariants = tv({
111
+ slots: {
112
+ root: "",
113
+ media: "items-center justify-center",
114
+ /** Edge length an `Icon` in the media slot inherits. */
115
+ mediaIcon: "",
116
+ content: "justify-center gap-0.5",
117
+ title: "font-medium text-foreground",
118
+ description: "text-muted-foreground",
119
+ actions: "flex-row items-center gap-2",
120
+ /** Edge length an `Icon` in the actions slot inherits. */
121
+ actionsIcon: "",
122
+ header: "w-full flex-row items-center justify-between gap-2",
123
+ footer: "w-full flex-row items-center justify-between gap-2",
124
+ group: "",
125
+ },
126
+ variants: {
127
+ surface: {
128
+ default: { root: "rounded-lg border border-transparent bg-transparent" },
129
+ outline: { root: "rounded-lg border border-border bg-transparent" },
130
+ muted: { root: "rounded-lg border border-transparent bg-muted" },
131
+ grouped: { root: "w-full bg-transparent" },
132
+ },
133
+ size: {
134
+ sm: {
135
+ root: "min-h-12 gap-2.5 px-3 py-2",
136
+ title: "text-sm",
137
+ description: "text-xs",
138
+ actionsIcon: "size-icon-xs",
139
+ },
140
+ md: {
141
+ root: "min-h-14 gap-3 px-4 py-3",
142
+ title: "text-base",
143
+ description: "text-sm",
144
+ actionsIcon: "size-icon-sm",
145
+ },
146
+ lg: {
147
+ root: "min-h-16 gap-3.5 px-5 py-4",
148
+ title: "text-lg",
149
+ description: "text-base",
150
+ actionsIcon: "size-icon-md",
151
+ },
152
+ },
153
+ orientation: {
154
+ horizontal: { root: "flex-row flex-wrap items-center", content: "flex-1" },
155
+ vertical: { root: "flex-col items-start", content: "self-stretch" },
156
+ },
157
+ mediaVariant: {
158
+ default: {},
159
+ icon: { media: "rounded-md bg-muted" },
160
+ image: { media: "overflow-hidden rounded-md bg-muted" },
161
+ },
162
+ groupOrientation: {
163
+ vertical: { group: "flex-col gap-2" },
164
+ horizontal: { group: "flex-row gap-3" },
165
+ },
166
+ // The empty `false` branches type the props as `boolean` rather than
167
+ // `true`. See the note in button.variants.ts.
168
+ isDisabled: { true: { root: "opacity-50" }, false: {} },
169
+ isSelected: { true: { root: "bg-accent" }, false: {} },
170
+ },
171
+ compoundVariants: [
172
+ // A card-shaped surface is `rounded-lg`, stepping down to `rounded-md` at
173
+ // `sm` — the same one step `ListGroup` and `Accordion` take.
174
+ { size: "sm", surface: ["default", "outline", "muted"], class: { root: "rounded-md" } },
175
+ { mediaVariant: "default", size: "sm", class: { mediaIcon: "size-icon-md" } },
176
+ { mediaVariant: "default", size: "md", class: { mediaIcon: "size-icon-lg" } },
177
+ { mediaVariant: "default", size: "lg", class: { mediaIcon: "size-icon-xl" } },
178
+ { mediaVariant: "icon", size: "sm", class: { media: "size-8", mediaIcon: "size-icon-sm" } },
179
+ { mediaVariant: "icon", size: "md", class: { media: "size-10", mediaIcon: "size-icon-md" } },
180
+ { mediaVariant: "icon", size: "lg", class: { media: "size-12", mediaIcon: "size-icon-lg" } },
181
+ { mediaVariant: "image", size: "sm", class: { media: "size-10" } },
182
+ { mediaVariant: "image", size: "md", class: { media: "size-12" } },
183
+ { mediaVariant: "image", size: "lg", class: { media: "size-14" } },
184
+ // A tile is `bg-muted`, so on a muted or selected surface it would vanish
185
+ // into the row.
186
+ { mediaVariant: ["icon", "image"], surface: "muted", class: { media: "bg-background" } },
187
+ { mediaVariant: ["icon", "image"], isSelected: true, class: { media: "bg-background" } },
188
+ ],
189
+ defaultVariants: {
190
+ surface: "default",
191
+ size: "md",
192
+ orientation: "horizontal",
193
+ mediaVariant: "default",
194
+ groupOrientation: "vertical",
195
+ isDisabled: false,
196
+ isSelected: false,
197
+ },
198
+ });
199
+
200
+ export type ItemVariantProps = VariantProps<typeof itemVariants>;
@@ -0,0 +1,125 @@
1
+ # Kpi
2
+
3
+ One number, what it is doing, and the shape it made getting there — a
4
+ [`Card`](../card/AGENTS.md) with a vocabulary for a metric, whose sparkline is a
5
+ [`Chart`](../chart/AGENTS.md) line. Compound root plus eleven parts: `Header`,
6
+ `Icon`, `Title`, `Action`, `Content`, `Stat`, `Value`, `Trend`, `Sparkline`,
7
+ `Footer`, `Group`.
8
+
9
+ `import { Kpi } from "@delacour/react-native-ui/kpi";`
10
+
11
+ ## Files
12
+
13
+ | File | What it holds |
14
+ | --- | --- |
15
+ | `index.ts` | → `@delacour/react-native-ui/kpi` |
16
+ | `kpi.tsx` | Root + the `Object.assign` compound surface |
17
+ | `kpi-header.tsx` | `Kpi.Header`, on the card header's inset |
18
+ | `kpi-icon.tsx` | `Kpi.Icon`, and the icon defaults it publishes |
19
+ | `kpi-title.tsx` | `Kpi.Title` |
20
+ | `kpi-action.tsx` | `Kpi.Action` |
21
+ | `kpi-content.tsx` | `Kpi.Content`, and the layout it publishes |
22
+ | `kpi-stat.tsx` | `Kpi.Stat` |
23
+ | `kpi-value.tsx` | `Kpi.Value`, and its loading placeholder |
24
+ | `kpi-trend.tsx` | `Kpi.Trend` — text or badge |
25
+ | `kpi-sparkline.tsx` | `Kpi.Sparkline`, and the internal scrub reader |
26
+ | `kpi-footer.tsx` | `Kpi.Footer`, over `Card.Footer` |
27
+ | `kpi-group.tsx` | `Kpi.Group` |
28
+ | `kpi.context.tsx` | `KpiProvider`, `useKpi()`, `useKpiContext()`, `useKpiPart()`, the layout and group contexts |
29
+ | `kpi.types.ts` | Prop types shared by two or more parts |
30
+ | `kpi.variants.ts` | The slotted `tv()`, the axes, and every pure resolver — no RN imports |
31
+ | `kpi.variants.test.ts` | |
32
+
33
+ ## Design
34
+
35
+ - **A KPI is a card, not a second definition of one.** The root renders `Card`
36
+ with the caller's `variant` and `size`, so it takes the four fills, steps
37
+ when nested, and has no scale of its own — `KPI_SIZES` *is* `CARD_SIZES`.
38
+ `Kpi.Header` and `Kpi.Content` compose their classes onto `cardVariants`'
39
+ own `header` and `content` slots, and the test asserts the card's inset
40
+ survives the merge, so the number, the title and the footer sit on one line
41
+ and a retune of the card moves all three.
42
+ - **The trend is coloured by what the movement means, never by its sign.**
43
+ `resolveKpiTrend` turns a signed percentage and a `goodDirection` into a
44
+ `direction` and a `tone`. A fall in churn is `down` and `good`, drawn in
45
+ `success-soft-foreground`. `goodDirection` is said once on the card and every
46
+ trend follows it unless it overrides it; `none` keeps the direction and drops
47
+ the judgement. A value that is not finite — a change against a zero baseline
48
+ — is flat and neutral rather than news of either kind.
49
+ - **There is no direction prop.** The sign is the direction. A separate prop
50
+ would be a second statement of one fact that the call site has to keep in
51
+ step, and the first time it did not the card would paint a fall green.
52
+ - **The trend is printed with a typographic minus.** `−4.2%` rather than
53
+ `-4.2%`: a hyphen is narrower than a plus, so a column of changes would not
54
+ line up on the sign. A value that rounds to zero is unsigned — `−0.0%` would
55
+ claim a fall nothing shows.
56
+ - **Colour is never the only signal.** The sign carries the direction in text,
57
+ the badge adds an arrow, and the trend is one accessible string — "Up 7.8
58
+ percent, vs last month" — rather than a number, an arrow and a caption read
59
+ as three stops.
60
+ - **The badge is `Badge`, soft.** `KPI_TONE_BADGE_COLOR` maps the tone onto
61
+ `success`, `destructive` and `default`, so a trend badge is the same object as
62
+ any other status badge on the screen.
63
+ - **The value is formatted by the caller.** Separators, currency and units are
64
+ locale decisions a component would get wrong in a way that is hard to notice
65
+ and impossible to override. It is one line, shrinking to fit — a number that
66
+ wraps is no longer read as one — and it takes the foreground token of the
67
+ plane the card landed on, the way `Card.Title` does.
68
+ - **The title never grows.** A growing child of a column absorbs the column's
69
+ spare height, which in a row of cards lands every number at a different
70
+ height. The header pushes `Kpi.Action` to the end with `ml-auto` instead, and
71
+ a test forbids `flex-1` on the title.
72
+ - **`Kpi.Stat` exists because the value and its change are one fact.** The
73
+ card's gap is for the space between facts; written straight into the content
74
+ the two drift apart and stop reading as a unit. Beside an `inline` sparkline
75
+ the stat takes the row's width.
76
+ - **The sparkline is `Chart`, with nothing but the line.** No grid, no axes, no
77
+ tooltip readout — `Chart` reserves an axis gutter only for an axis that is
78
+ placed, so the plot is the whole frame. The frame is sized through `Chart`'s
79
+ `frameClassName`, which exists for this: the chart's three `size` heights are
80
+ for a chart someone reads with axes. Rows or a bare list of numbers both work;
81
+ `resolveSparklineSeries` indexes a list.
82
+ - **The y bounds are the data's own, padded a tenth.** With no padding a line
83
+ drawn to its exact extent puts the peak and trough on the frame's edge, half
84
+ the stroke off the canvas. `resolveSparklineDomain` pads the span and gives a
85
+ flat series a unit either side so it sits centred.
86
+ - **Inline is a fixed 128pt column; below is the full width.** A stack of cards
87
+ has labels of every length, and a chart taking whatever the text left would
88
+ be a different width on every card. Fixed, the shapes line up down the
89
+ right-hand edge, which is the reason to put them there. Below, the chart is
90
+ filled; inline it is not, where a fill would make it a second block competing
91
+ with the number — `resolveSparklineFilled`, overridable.
92
+ - **`colorIndex` is on the card, not the chart.** The icon and the sparkline
93
+ share it, and a row of cards gets five series colours by setting one prop on
94
+ each. The trend ignores it: a series colour has nothing to say about whether
95
+ the number went the right way. The icon square is the series colour at 15%,
96
+ written out per index because Tailwind's scanner cannot see a class built at
97
+ runtime.
98
+ - **Holding the sparkline scrubs it, and the scrub is state.** A
99
+ `Chart.Tooltip.Dot` rides the line, and an internal `KpiSparklineScrub` —
100
+ a child of `<Chart>` that is not a mark, so the chart mounts it beside the
101
+ canvas with the scrub's shared values in reach — reports the index to the
102
+ root through `scheduleOnRN` only when it changes, and `null` when the finger
103
+ lifts. The root holds it with `useControllableState`: `activeIndex` and
104
+ `onActiveIndexChange` control it, `defaultActiveIndex` seeds it, and
105
+ `useKpi()` reads it, so a part of the caller's own prints the scrubbed
106
+ point's value in place of the latest. A controlled index moves what the
107
+ caller prints; it draws no dot, because the dot is the finger's.
108
+ - **Loading is a placeholder the size of what it stands for.** `isLoading`
109
+ swaps the value, the trend and the sparkline for muted blocks — the value's
110
+ is the height of its line at each size, which a test pins — and marks the
111
+ card busy, so nothing jumps when the data lands. They are
112
+ `muted-foreground` at 15%, not `bg-muted`: `muted` is the secondary fill, and
113
+ the first build's placeholders vanished on a secondary card. A translucent
114
+ foreground reads on every plane, and a test forbids a fill token there.
115
+ - **No interaction on the card.** Like `Card`, a KPI that selects or navigates
116
+ is a `Pressable` around it, which keeps the role, the haptic and the pressed
117
+ state at the call site that knows what the press means. The playground's
118
+ metric picker is the example.
119
+ - **`Kpi.Group` arranges; it does not restyle.** A row gives each metric an
120
+ equal share of the width. `separated` sets them on one `Surface` with a
121
+ `Separator` between each — hidden from assistive technology, so a rule is not
122
+ an unlabelled stop between two numbers — and each metric drops its own fill
123
+ to `transparent` unless it names one.
124
+ - **No text treatment on a view slot** (rule 1). The tests assert it across
125
+ every combination.
@@ -0,0 +1,45 @@
1
+ export { Kpi, type KpiProps } from "./kpi";
2
+ export {
3
+ type KpiContextValue,
4
+ type KpiGroupContextValue,
5
+ KpiProvider,
6
+ useKpi,
7
+ useKpiContext,
8
+ useKpiLayout,
9
+ } from "./kpi.context";
10
+ export type { KpiSlotProps, KpiTextProps } from "./kpi.types";
11
+ export {
12
+ formatKpiTrend,
13
+ KPI_COLOR_INDEXES,
14
+ KPI_DIRECTIONS,
15
+ KPI_GOOD_DIRECTIONS,
16
+ KPI_GROUP_ORIENTATIONS,
17
+ KPI_LAYOUTS,
18
+ KPI_SIZES,
19
+ KPI_TONE_BADGE_COLOR,
20
+ KPI_TONES,
21
+ KPI_TREND_VARIANTS,
22
+ type KpiColorIndex,
23
+ type KpiDirection,
24
+ type KpiGoodDirection,
25
+ type KpiGroupOrientation,
26
+ type KpiLayout,
27
+ type KpiSize,
28
+ type KpiSparklineData,
29
+ type KpiSparklineSeries,
30
+ type KpiTone,
31
+ type KpiTrendVariant,
32
+ type KpiVariantProps,
33
+ kpiSparklineAccessibilityLabel,
34
+ kpiTrendAccessibilityLabel,
35
+ kpiVariants,
36
+ resolveKpiTrend,
37
+ resolveSparklineDomain,
38
+ resolveSparklineFilled,
39
+ resolveSparklineSeries,
40
+ } from "./kpi.variants";
41
+ export type { KpiContentProps } from "./kpi-content";
42
+ export type { KpiFooterProps } from "./kpi-footer";
43
+ export type { KpiGroupProps } from "./kpi-group";
44
+ export type { KpiSparklineProps } from "./kpi-sparkline";
45
+ export type { KpiTrendProps } from "./kpi-trend";
@@ -0,0 +1,12 @@
1
+ import type { ReactElement } from "react";
2
+ import { View } from "react-native";
3
+ import { useKpiPart } from "./kpi.context";
4
+ import type { KpiSlotProps } from "./kpi.types";
5
+ import { kpiVariants } from "./kpi.variants";
6
+
7
+ /** The header's trailing end — a menu button, a period filter, a link — pushed to the right edge. */
8
+ export function KpiAction({ className, ...props }: KpiSlotProps): ReactElement {
9
+ const { size } = useKpiPart("Kpi.Action");
10
+ return <View className={kpiVariants({ size }).action({ className })} {...props} />;
11
+ }
12
+ KpiAction.displayName = "DelacourUI.Kpi.Action";
@@ -0,0 +1,35 @@
1
+ import type { ReactElement } from "react";
2
+ import { View } from "react-native";
3
+ import { cn } from "../../lib/cn";
4
+ import { cardVariants } from "../card/card.variants";
5
+ import { KpiLayoutProvider, useKpiPart } from "./kpi.context";
6
+ import type { KpiSlotProps } from "./kpi.types";
7
+ import { type KpiLayout, kpiVariants } from "./kpi.variants";
8
+
9
+ export type KpiContentProps = KpiSlotProps & {
10
+ /**
11
+ * `below` stacks the stat over a full-width sparkline. `inline` sets the
12
+ * sparkline in a fixed column beside the number, for a stack of cards whose
13
+ * shapes should line up down the right-hand edge.
14
+ */
15
+ layout?: KpiLayout;
16
+ };
17
+
18
+ /**
19
+ * The body: the stat and the sparkline. Sits on the card content's own inset,
20
+ * and tells the stat and the sparkline inside it which layout they are in.
21
+ */
22
+ export function KpiContent({ layout = "below", className, ...props }: KpiContentProps): ReactElement {
23
+ const { size } = useKpiPart("Kpi.Content");
24
+ return (
25
+ <KpiLayoutProvider value={layout}>
26
+ <View
27
+ className={cardVariants({ size }).content({
28
+ className: cn(kpiVariants({ layout, size }).content(), className),
29
+ })}
30
+ {...props}
31
+ />
32
+ </KpiLayoutProvider>
33
+ );
34
+ }
35
+ KpiContent.displayName = "DelacourUI.Kpi.Content";