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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. package/README.md +10 -6
  2. package/package.json +48 -4
  3. package/src/components/accordion/AGENTS.md +21 -9
  4. package/src/components/accordion/accordion-content.tsx +7 -4
  5. package/src/components/accordion/accordion-item.tsx +15 -13
  6. package/src/components/accordion/accordion.context.tsx +11 -5
  7. package/src/components/accordion/accordion.variants.test.ts +64 -3
  8. package/src/components/accordion/accordion.variants.ts +33 -9
  9. package/src/components/alert/AGENTS.md +102 -0
  10. package/src/components/alert/alert-action.tsx +17 -0
  11. package/src/components/alert/alert-close-button.tsx +50 -0
  12. package/src/components/alert/alert-content.tsx +15 -0
  13. package/src/components/alert/alert-description.tsx +16 -0
  14. package/src/components/alert/alert-glyphs.ts +25 -0
  15. package/src/components/alert/alert-indicator.tsx +40 -0
  16. package/src/components/alert/alert-title.tsx +18 -0
  17. package/src/components/alert/alert.context.tsx +69 -0
  18. package/src/components/alert/alert.tsx +160 -0
  19. package/src/components/alert/alert.types.ts +10 -0
  20. package/src/components/alert/alert.variants.test.ts +218 -0
  21. package/src/components/alert/alert.variants.ts +180 -0
  22. package/src/components/alert/index.ts +23 -0
  23. package/src/components/avatar/AGENTS.md +106 -0
  24. package/src/components/avatar/avatar-badge.tsx +51 -0
  25. package/src/components/avatar/avatar-group.tsx +139 -0
  26. package/src/components/avatar/avatar.context.tsx +93 -0
  27. package/src/components/avatar/avatar.tsx +201 -0
  28. package/src/components/avatar/avatar.utils.test.ts +174 -0
  29. package/src/components/avatar/avatar.utils.ts +141 -0
  30. package/src/components/avatar/avatar.variants.test.ts +251 -0
  31. package/src/components/avatar/avatar.variants.ts +270 -0
  32. package/src/components/avatar/index.ts +39 -0
  33. package/src/components/badge/AGENTS.md +5 -0
  34. package/src/components/badge/badge.tsx +6 -1
  35. package/src/components/bottom-sheet/AGENTS.md +186 -231
  36. package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
  37. package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
  38. package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -208
  39. package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
  40. package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
  41. package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
  42. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -122
  43. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
  44. package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
  45. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
  46. package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
  47. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -62
  48. package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
  49. package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
  50. package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
  51. package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
  52. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
  53. package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
  54. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -87
  55. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
  56. package/src/components/bottom-sheet/index.ts +39 -17
  57. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
  58. package/src/components/button/AGENTS.md +45 -11
  59. package/src/components/button/button-group.tsx +44 -8
  60. package/src/components/button/button.context.tsx +10 -0
  61. package/src/components/button/button.tsx +2 -1
  62. package/src/components/button/button.variants.test.ts +154 -0
  63. package/src/components/button/button.variants.ts +71 -6
  64. package/src/components/button/index.ts +2 -0
  65. package/src/components/calendar/AGENTS.md +142 -0
  66. package/src/components/calendar/calendar-caption.tsx +116 -0
  67. package/src/components/calendar/calendar-day.tsx +103 -0
  68. package/src/components/calendar/calendar-grid.tsx +183 -0
  69. package/src/components/calendar/calendar-header.tsx +34 -0
  70. package/src/components/calendar/calendar-nav.tsx +51 -0
  71. package/src/components/calendar/calendar-picker.tsx +128 -0
  72. package/src/components/calendar/calendar-weekdays.tsx +48 -0
  73. package/src/components/calendar/calendar.context.tsx +108 -0
  74. package/src/components/calendar/calendar.date.test.ts +456 -0
  75. package/src/components/calendar/calendar.date.ts +584 -0
  76. package/src/components/calendar/calendar.tsx +389 -0
  77. package/src/components/calendar/calendar.types.ts +87 -0
  78. package/src/components/calendar/calendar.variants.test.ts +363 -0
  79. package/src/components/calendar/calendar.variants.ts +426 -0
  80. package/src/components/calendar/index.ts +96 -0
  81. package/src/components/card/AGENTS.md +88 -0
  82. package/src/components/card/card-action.tsx +16 -0
  83. package/src/components/card/card-content.tsx +12 -0
  84. package/src/components/card/card-description.tsx +12 -0
  85. package/src/components/card/card-footer.tsx +30 -0
  86. package/src/components/card/card-header.tsx +45 -0
  87. package/src/components/card/card-title.tsx +25 -0
  88. package/src/components/card/card.context.tsx +64 -0
  89. package/src/components/card/card.tsx +94 -0
  90. package/src/components/card/card.types.ts +13 -0
  91. package/src/components/card/card.variants.test.ts +240 -0
  92. package/src/components/card/card.variants.ts +177 -0
  93. package/src/components/card/index.ts +15 -0
  94. package/src/components/chart/AGENTS.md +8 -0
  95. package/src/components/chart/chart.tsx +8 -1
  96. package/src/components/chip/AGENTS.md +113 -0
  97. package/src/components/chip/chip-close-button.tsx +48 -0
  98. package/src/components/chip/chip-end-content.tsx +15 -0
  99. package/src/components/chip/chip-label.tsx +21 -0
  100. package/src/components/chip/chip-start-content.tsx +16 -0
  101. package/src/components/chip/chip.context.tsx +66 -0
  102. package/src/components/chip/chip.tsx +268 -0
  103. package/src/components/chip/chip.types.ts +10 -0
  104. package/src/components/chip/chip.variants.test.ts +335 -0
  105. package/src/components/chip/chip.variants.ts +321 -0
  106. package/src/components/chip/index.ts +24 -0
  107. package/src/components/collapsible/AGENTS.md +97 -0
  108. package/src/components/collapsible/collapsible-content.tsx +97 -0
  109. package/src/components/collapsible/collapsible-description.tsx +17 -0
  110. package/src/components/collapsible/collapsible-indicator.tsx +92 -0
  111. package/src/components/collapsible/collapsible-title.tsx +17 -0
  112. package/src/components/collapsible/collapsible-trigger.tsx +133 -0
  113. package/src/components/collapsible/collapsible.context.tsx +91 -0
  114. package/src/components/collapsible/collapsible.tsx +167 -0
  115. package/src/components/collapsible/collapsible.types.ts +9 -0
  116. package/src/components/collapsible/collapsible.variants.test.ts +338 -0
  117. package/src/components/collapsible/collapsible.variants.ts +228 -0
  118. package/src/components/collapsible/index.ts +32 -0
  119. package/src/components/context-menu/AGENTS.md +103 -0
  120. package/src/components/context-menu/context-menu-content.tsx +64 -0
  121. package/src/components/context-menu/context-menu-item.tsx +22 -0
  122. package/src/components/context-menu/context-menu-preview.tsx +60 -0
  123. package/src/components/context-menu/context-menu-trigger.tsx +160 -0
  124. package/src/components/context-menu/context-menu.context.tsx +91 -0
  125. package/src/components/context-menu/context-menu.tsx +90 -0
  126. package/src/components/context-menu/context-menu.variants.test.ts +266 -0
  127. package/src/components/context-menu/context-menu.variants.ts +147 -0
  128. package/src/components/context-menu/index.ts +18 -0
  129. package/src/components/dialog/AGENTS.md +97 -0
  130. package/src/components/dialog/dialog-body.tsx +21 -0
  131. package/src/components/dialog/dialog-close.tsx +82 -0
  132. package/src/components/dialog/dialog-content.tsx +153 -0
  133. package/src/components/dialog/dialog-description.tsx +23 -0
  134. package/src/components/dialog/dialog-footer.tsx +32 -0
  135. package/src/components/dialog/dialog-header.tsx +20 -0
  136. package/src/components/dialog/dialog-title.tsx +42 -0
  137. package/src/components/dialog/dialog-trigger.tsx +54 -0
  138. package/src/components/dialog/dialog.context.tsx +90 -0
  139. package/src/components/dialog/dialog.tsx +119 -0
  140. package/src/components/dialog/dialog.variants.test.ts +224 -0
  141. package/src/components/dialog/dialog.variants.ts +173 -0
  142. package/src/components/dialog/index.ts +30 -0
  143. package/src/components/drawer/AGENTS.md +123 -0
  144. package/src/components/drawer/drawer-body.tsx +46 -0
  145. package/src/components/drawer/drawer-close.tsx +76 -0
  146. package/src/components/drawer/drawer-content.tsx +186 -0
  147. package/src/components/drawer/drawer-description.tsx +23 -0
  148. package/src/components/drawer/drawer-footer.tsx +21 -0
  149. package/src/components/drawer/drawer-header.tsx +42 -0
  150. package/src/components/drawer/drawer-title.tsx +35 -0
  151. package/src/components/drawer/drawer-trigger.tsx +54 -0
  152. package/src/components/drawer/drawer.context.tsx +114 -0
  153. package/src/components/drawer/drawer.tsx +119 -0
  154. package/src/components/drawer/drawer.variants.test.ts +392 -0
  155. package/src/components/drawer/drawer.variants.ts +244 -0
  156. package/src/components/drawer/index.ts +41 -0
  157. package/src/components/drawer/use-drawer-pan.ts +108 -0
  158. package/src/components/empty-state/AGENTS.md +90 -0
  159. package/src/components/empty-state/empty-state-content.tsx +17 -0
  160. package/src/components/empty-state/empty-state-description.tsx +12 -0
  161. package/src/components/empty-state/empty-state-header.tsx +17 -0
  162. package/src/components/empty-state/empty-state-media.tsx +56 -0
  163. package/src/components/empty-state/empty-state-title.tsx +18 -0
  164. package/src/components/empty-state/empty-state.context.tsx +65 -0
  165. package/src/components/empty-state/empty-state.tsx +84 -0
  166. package/src/components/empty-state/empty-state.types.ts +14 -0
  167. package/src/components/empty-state/empty-state.variants.test.ts +245 -0
  168. package/src/components/empty-state/empty-state.variants.ts +132 -0
  169. package/src/components/empty-state/index.ts +20 -0
  170. package/src/components/fab/AGENTS.md +119 -0
  171. package/src/components/fab/fab-action.tsx +118 -0
  172. package/src/components/fab/fab-group.tsx +214 -0
  173. package/src/components/fab/fab-label.tsx +18 -0
  174. package/src/components/fab/fab.context.tsx +125 -0
  175. package/src/components/fab/fab.tsx +156 -0
  176. package/src/components/fab/fab.types.ts +17 -0
  177. package/src/components/fab/fab.variants.test.ts +274 -0
  178. package/src/components/fab/fab.variants.ts +234 -0
  179. package/src/components/fab/index.ts +37 -0
  180. package/src/components/feedback/AGENTS.md +95 -0
  181. package/src/components/feedback/feedback-action.tsx +16 -0
  182. package/src/components/feedback/feedback-cancel.tsx +34 -0
  183. package/src/components/feedback/feedback-close.tsx +21 -0
  184. package/src/components/feedback/feedback-content.tsx +24 -0
  185. package/src/components/feedback/feedback-field.tsx +70 -0
  186. package/src/components/feedback/feedback-footer.tsx +21 -0
  187. package/src/components/feedback/feedback-panel.tsx +54 -0
  188. package/src/components/feedback/feedback-submit.tsx +74 -0
  189. package/src/components/feedback/feedback-title.tsx +34 -0
  190. package/src/components/feedback/feedback-trigger.tsx +20 -0
  191. package/src/components/feedback/feedback.context.tsx +73 -0
  192. package/src/components/feedback/feedback.tsx +135 -0
  193. package/src/components/feedback/feedback.variants.test.ts +185 -0
  194. package/src/components/feedback/feedback.variants.ts +128 -0
  195. package/src/components/feedback/index.ts +24 -0
  196. package/src/components/field/AGENTS.md +19 -0
  197. package/src/components/field/field-label.tsx +19 -5
  198. package/src/components/field/field.context.tsx +17 -0
  199. package/src/components/field/field.tsx +15 -2
  200. package/src/components/field/field.variants.test.ts +24 -0
  201. package/src/components/field/field.variants.ts +20 -0
  202. package/src/components/field/index.ts +1 -0
  203. package/src/components/item/AGENTS.md +92 -0
  204. package/src/components/item/index.ts +25 -0
  205. package/src/components/item/item-actions.tsx +28 -0
  206. package/src/components/item/item-content.tsx +15 -0
  207. package/src/components/item/item-description.tsx +12 -0
  208. package/src/components/item/item-footer.tsx +13 -0
  209. package/src/components/item/item-group.tsx +32 -0
  210. package/src/components/item/item-header.tsx +13 -0
  211. package/src/components/item/item-media.tsx +37 -0
  212. package/src/components/item/item-separator.tsx +13 -0
  213. package/src/components/item/item-title.tsx +17 -0
  214. package/src/components/item/item.context.tsx +66 -0
  215. package/src/components/item/item.tsx +218 -0
  216. package/src/components/item/item.types.ts +12 -0
  217. package/src/components/item/item.variants.test.ts +263 -0
  218. package/src/components/item/item.variants.ts +200 -0
  219. package/src/components/kpi/AGENTS.md +125 -0
  220. package/src/components/kpi/index.ts +45 -0
  221. package/src/components/kpi/kpi-action.tsx +12 -0
  222. package/src/components/kpi/kpi-content.tsx +35 -0
  223. package/src/components/kpi/kpi-footer.tsx +52 -0
  224. package/src/components/kpi/kpi-group.tsx +70 -0
  225. package/src/components/kpi/kpi-header.tsx +25 -0
  226. package/src/components/kpi/kpi-icon.tsx +28 -0
  227. package/src/components/kpi/kpi-sparkline.tsx +145 -0
  228. package/src/components/kpi/kpi-stat.tsx +20 -0
  229. package/src/components/kpi/kpi-title.tsx +19 -0
  230. package/src/components/kpi/kpi-trend.tsx +105 -0
  231. package/src/components/kpi/kpi-value.tsx +32 -0
  232. package/src/components/kpi/kpi.context.tsx +111 -0
  233. package/src/components/kpi/kpi.tsx +154 -0
  234. package/src/components/kpi/kpi.types.ts +9 -0
  235. package/src/components/kpi/kpi.variants.test.ts +428 -0
  236. package/src/components/kpi/kpi.variants.ts +347 -0
  237. package/src/components/label/AGENTS.md +89 -0
  238. package/src/components/label/index.ts +10 -0
  239. package/src/components/label/label.tsx +97 -0
  240. package/src/components/label/label.variants.test.ts +176 -0
  241. package/src/components/label/label.variants.ts +123 -0
  242. package/src/components/menu/AGENTS.md +176 -0
  243. package/src/components/menu/index.ts +44 -0
  244. package/src/components/menu/menu-background.tsx +17 -0
  245. package/src/components/menu/menu-checkbox-item.tsx +57 -0
  246. package/src/components/menu/menu-content.tsx +303 -0
  247. package/src/components/menu/menu-item.tsx +162 -0
  248. package/src/components/menu/menu-label.tsx +14 -0
  249. package/src/components/menu/menu-radio-group.tsx +50 -0
  250. package/src/components/menu/menu-radio-item.tsx +57 -0
  251. package/src/components/menu/menu-separator.tsx +11 -0
  252. package/src/components/menu/menu-sub-content.tsx +72 -0
  253. package/src/components/menu/menu-sub-trigger.tsx +50 -0
  254. package/src/components/menu/menu-sub.tsx +75 -0
  255. package/src/components/menu/menu-trigger.tsx +57 -0
  256. package/src/components/menu/menu.context.tsx +214 -0
  257. package/src/components/menu/menu.tsx +88 -0
  258. package/src/components/menu/menu.types.ts +33 -0
  259. package/src/components/menu/menu.variants.test.ts +400 -0
  260. package/src/components/menu/menu.variants.ts +353 -0
  261. package/src/components/meter/AGENTS.md +135 -0
  262. package/src/components/meter/index.ts +43 -0
  263. package/src/components/meter/meter-fill.tsx +19 -0
  264. package/src/components/meter/meter-header.tsx +17 -0
  265. package/src/components/meter/meter-label.tsx +18 -0
  266. package/src/components/meter/meter-output.tsx +42 -0
  267. package/src/components/meter/meter-segments.tsx +83 -0
  268. package/src/components/meter/meter-track.tsx +16 -0
  269. package/src/components/meter/meter.context.tsx +62 -0
  270. package/src/components/meter/meter.tsx +286 -0
  271. package/src/components/meter/meter.types.ts +30 -0
  272. package/src/components/meter/meter.variants.test.ts +430 -0
  273. package/src/components/meter/meter.variants.ts +347 -0
  274. package/src/components/overlay/AGENTS.md +89 -0
  275. package/src/components/overlay/index.ts +27 -0
  276. package/src/components/overlay/overlay-portal.tsx +84 -0
  277. package/src/components/overlay/overlay-presence.test.ts +46 -0
  278. package/src/components/overlay/overlay-presence.ts +49 -0
  279. package/src/components/overlay/overlay-registry.test.ts +96 -0
  280. package/src/components/overlay/overlay-registry.ts +99 -0
  281. package/src/components/overlay/overlay-scrim.tsx +49 -0
  282. package/src/components/overlay/overlay.context.tsx +48 -0
  283. package/src/components/overlay/overlay.tsx +118 -0
  284. package/src/components/overlay/overlay.variants.test.ts +37 -0
  285. package/src/components/overlay/overlay.variants.ts +33 -0
  286. package/src/components/overlay/use-overlay-back-handler.ts +34 -0
  287. package/src/components/overlay/use-overlay-presence.ts +87 -0
  288. package/src/components/popover/AGENTS.md +138 -0
  289. package/src/components/popover/index.ts +48 -0
  290. package/src/components/popover/popover-anchor.tsx +49 -0
  291. package/src/components/popover/popover-arrow.tsx +89 -0
  292. package/src/components/popover/popover-close.tsx +73 -0
  293. package/src/components/popover/popover-content.tsx +235 -0
  294. package/src/components/popover/popover-description.tsx +18 -0
  295. package/src/components/popover/popover-title.tsx +33 -0
  296. package/src/components/popover/popover-trigger.tsx +56 -0
  297. package/src/components/popover/popover.context.tsx +71 -0
  298. package/src/components/popover/popover.position.test.ts +354 -0
  299. package/src/components/popover/popover.position.ts +286 -0
  300. package/src/components/popover/popover.tsx +123 -0
  301. package/src/components/popover/popover.variants.test.ts +135 -0
  302. package/src/components/popover/popover.variants.ts +85 -0
  303. package/src/components/popover/use-anchor-measure.ts +76 -0
  304. package/src/components/popover/use-anchored-content.ts +219 -0
  305. package/src/components/pressable/AGENTS.md +11 -1
  306. package/src/components/pressable/pressable.tsx +7 -2
  307. package/src/components/pressable/pressable.variants.test.ts +29 -0
  308. package/src/components/pressable/pressable.variants.ts +17 -0
  309. package/src/components/progress/AGENTS.md +120 -0
  310. package/src/components/progress/index.ts +40 -0
  311. package/src/components/progress/progress-fill.tsx +152 -0
  312. package/src/components/progress/progress-header.tsx +19 -0
  313. package/src/components/progress/progress-label.tsx +27 -0
  314. package/src/components/progress/progress-output.tsx +47 -0
  315. package/src/components/progress/progress-track.tsx +38 -0
  316. package/src/components/progress/progress.context.tsx +79 -0
  317. package/src/components/progress/progress.tsx +171 -0
  318. package/src/components/progress/progress.types.ts +23 -0
  319. package/src/components/progress/progress.variants.test.ts +335 -0
  320. package/src/components/progress/progress.variants.ts +284 -0
  321. package/src/components/progress-button/AGENTS.md +116 -0
  322. package/src/components/progress-button/index.ts +36 -0
  323. package/src/components/progress-button/progress-button-done.tsx +33 -0
  324. package/src/components/progress-button/progress-button-label.tsx +27 -0
  325. package/src/components/progress-button/progress-button.context.tsx +93 -0
  326. package/src/components/progress-button/progress-button.tsx +414 -0
  327. package/src/components/progress-button/progress-button.variants.test.ts +314 -0
  328. package/src/components/progress-button/progress-button.variants.ts +236 -0
  329. package/src/components/provider/AGENTS.md +38 -25
  330. package/src/components/provider/provider.tsx +38 -11
  331. package/src/components/radio/AGENTS.md +8 -10
  332. package/src/components/radio/radio.variants.test.ts +5 -6
  333. package/src/components/radio/radio.variants.ts +6 -7
  334. package/src/components/rating/AGENTS.md +143 -0
  335. package/src/components/rating/index.ts +43 -0
  336. package/src/components/rating/rating-output.tsx +39 -0
  337. package/src/components/rating/rating-star.tsx +70 -0
  338. package/src/components/rating/rating-stars.tsx +219 -0
  339. package/src/components/rating/rating.context.tsx +79 -0
  340. package/src/components/rating/rating.tsx +163 -0
  341. package/src/components/rating/rating.variants.test.ts +347 -0
  342. package/src/components/rating/rating.variants.ts +354 -0
  343. package/src/components/selection-mode/AGENTS.md +155 -0
  344. package/src/components/selection-mode/index.ts +43 -0
  345. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  346. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  347. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  348. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  349. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  350. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  351. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  352. package/src/components/selection-mode/selection-mode.tsx +207 -0
  353. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  354. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  355. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  356. package/src/components/skeleton/AGENTS.md +120 -0
  357. package/src/components/skeleton/index.ts +15 -0
  358. package/src/components/skeleton/skeleton-clock.ts +48 -0
  359. package/src/components/skeleton/skeleton-group.tsx +64 -0
  360. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  361. package/src/components/skeleton/skeleton.context.tsx +45 -0
  362. package/src/components/skeleton/skeleton.tsx +227 -0
  363. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  364. package/src/components/skeleton/skeleton.variants.ts +222 -0
  365. package/src/components/slide-button/AGENTS.md +122 -0
  366. package/src/components/slide-button/index.ts +43 -0
  367. package/src/components/slide-button/slide-button-label.tsx +70 -0
  368. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  369. package/src/components/slide-button/slide-button.context.tsx +76 -0
  370. package/src/components/slide-button/slide-button.tsx +457 -0
  371. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  372. package/src/components/slide-button/slide-button.variants.ts +324 -0
  373. package/src/components/slider/index.ts +2 -0
  374. package/src/components/slider/slider-thumb.tsx +33 -1
  375. package/src/components/slider/slider.variants.test.ts +52 -0
  376. package/src/components/slider/slider.variants.ts +56 -2
  377. package/src/components/stack-card/AGENTS.md +169 -0
  378. package/src/components/stack-card/index.ts +37 -0
  379. package/src/components/stack-card/stack-card-action.tsx +66 -0
  380. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  381. package/src/components/stack-card/stack-card-card.tsx +71 -0
  382. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  383. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  384. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  385. package/src/components/stack-card/stack-card.context.tsx +117 -0
  386. package/src/components/stack-card/stack-card.tsx +557 -0
  387. package/src/components/stack-card/stack-card.types.ts +26 -0
  388. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  389. package/src/components/stack-card/stack-card.variants.ts +360 -0
  390. package/src/components/steps/AGENTS.md +153 -0
  391. package/src/components/steps/index.ts +45 -0
  392. package/src/components/steps/steps-connector.tsx +31 -0
  393. package/src/components/steps/steps-description.tsx +26 -0
  394. package/src/components/steps/steps-indicator.tsx +106 -0
  395. package/src/components/steps/steps-item.tsx +225 -0
  396. package/src/components/steps/steps-panel.tsx +41 -0
  397. package/src/components/steps/steps-title.tsx +32 -0
  398. package/src/components/steps/steps-track.tsx +59 -0
  399. package/src/components/steps/steps.context.tsx +123 -0
  400. package/src/components/steps/steps.tsx +166 -0
  401. package/src/components/steps/steps.variants.test.ts +388 -0
  402. package/src/components/steps/steps.variants.ts +468 -0
  403. package/src/components/surface/AGENTS.md +72 -0
  404. package/src/components/surface/index.ts +15 -0
  405. package/src/components/surface/surface.context.tsx +54 -0
  406. package/src/components/surface/surface.tsx +70 -0
  407. package/src/components/surface/surface.variants.test.ts +180 -0
  408. package/src/components/surface/surface.variants.ts +133 -0
  409. package/src/components/swipe/AGENTS.md +140 -0
  410. package/src/components/swipe/index.ts +31 -0
  411. package/src/components/swipe/swipe-action.tsx +99 -0
  412. package/src/components/swipe/swipe-end.tsx +14 -0
  413. package/src/components/swipe/swipe-group.tsx +55 -0
  414. package/src/components/swipe/swipe-panel.tsx +55 -0
  415. package/src/components/swipe/swipe-start.tsx +15 -0
  416. package/src/components/swipe/swipe.context.tsx +135 -0
  417. package/src/components/swipe/swipe.tsx +393 -0
  418. package/src/components/swipe/swipe.types.ts +7 -0
  419. package/src/components/swipe/swipe.variants.test.ts +337 -0
  420. package/src/components/swipe/swipe.variants.ts +290 -0
  421. package/src/components/switch/switch.variants.ts +2 -2
  422. package/src/components/tabs/AGENTS.md +4 -3
  423. package/src/components/tabs/tabs.variants.test.ts +1 -1
  424. package/src/components/tabs/tabs.variants.ts +4 -6
  425. package/src/components/textarea/AGENTS.md +86 -0
  426. package/src/components/textarea/index.ts +16 -0
  427. package/src/components/textarea/textarea.tsx +157 -0
  428. package/src/components/textarea/textarea.variants.test.ts +219 -0
  429. package/src/components/textarea/textarea.variants.ts +171 -0
  430. package/src/components/toast/AGENTS.md +140 -0
  431. package/src/components/toast/index.ts +74 -0
  432. package/src/components/toast/toast-action.tsx +59 -0
  433. package/src/components/toast/toast-api.ts +32 -0
  434. package/src/components/toast/toast-close.tsx +44 -0
  435. package/src/components/toast/toast-content.tsx +11 -0
  436. package/src/components/toast/toast-description.tsx +11 -0
  437. package/src/components/toast/toast-indicator.tsx +34 -0
  438. package/src/components/toast/toast-item.tsx +308 -0
  439. package/src/components/toast/toast-title.tsx +13 -0
  440. package/src/components/toast/toast-viewport.tsx +135 -0
  441. package/src/components/toast/toast.context.tsx +69 -0
  442. package/src/components/toast/toast.store.test.ts +259 -0
  443. package/src/components/toast/toast.store.ts +338 -0
  444. package/src/components/toast/toast.timer.test.ts +64 -0
  445. package/src/components/toast/toast.timer.ts +51 -0
  446. package/src/components/toast/toast.tsx +132 -0
  447. package/src/components/toast/toast.variants.test.ts +393 -0
  448. package/src/components/toast/toast.variants.ts +304 -0
  449. package/src/components/toggle-button/AGENTS.md +106 -0
  450. package/src/components/toggle-button/index.ts +30 -0
  451. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  452. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  453. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  454. package/src/components/toggle-button/toggle-button.tsx +172 -0
  455. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  456. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  457. package/src/components/tooltip/AGENTS.md +133 -0
  458. package/src/components/tooltip/index.ts +28 -0
  459. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  460. package/src/components/tooltip/tooltip-content.tsx +179 -0
  461. package/src/components/tooltip/tooltip-description.tsx +19 -0
  462. package/src/components/tooltip/tooltip-text.tsx +21 -0
  463. package/src/components/tooltip/tooltip-title.tsx +20 -0
  464. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  465. package/src/components/tooltip/tooltip.context.tsx +68 -0
  466. package/src/components/tooltip/tooltip.tsx +213 -0
  467. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  468. package/src/components/tooltip/tooltip.variants.ts +177 -0
  469. package/src/hooks/use-calm-motion.tsx +31 -0
  470. package/src/lib/calm-motion.test.ts +17 -0
  471. package/src/lib/calm-motion.ts +23 -0
  472. package/src/lib/cn.test.ts +14 -0
  473. package/src/lib/cn.ts +2 -2
  474. package/src/no-gorhom.test.ts +54 -0
  475. package/src/styles/geometry.test.ts +1 -0
  476. package/src/styles/theme.css +4 -0
  477. package/src/styles/tokens.css +11 -0
  478. package/src/styles/tokens.test.ts +22 -0
  479. package/src/styles/tokens.ts +20 -0
  480. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  481. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -0,0 +1,52 @@
1
+ import { Children, type ReactElement, type ReactNode } from "react";
2
+ import type { CardFooterProps } from "../card";
3
+ import { Card } from "../card";
4
+ import { Text } from "../text";
5
+ import { useKpiPart } from "./kpi.context";
6
+ import { kpiVariants } from "./kpi.variants";
7
+
8
+ export type KpiFooterProps = CardFooterProps;
9
+
10
+ /**
11
+ * Wraps each run of bare strings and numbers in one muted caption. A raw string
12
+ * inside a `View` is a red box in React Native, and the run is joined rather
13
+ * than wrapped piece by piece: `last {days} days` is three children, and three
14
+ * `Text`s in the footer's row would sit apart by its gap.
15
+ */
16
+ function wrapBareText(children: ReactNode, className: string): ReactNode[] {
17
+ const output: ReactNode[] = [];
18
+ let run: string[] = [];
19
+ const flush = (): void => {
20
+ if (run.length === 0) return;
21
+ output.push(
22
+ <Text className={className} key={`text-${output.length}`}>
23
+ {run.join("")}
24
+ </Text>
25
+ );
26
+ run = [];
27
+ };
28
+
29
+ for (const child of Children.toArray(children)) {
30
+ if (typeof child === "string" || typeof child === "number") {
31
+ run.push(String(child));
32
+ } else {
33
+ flush();
34
+ output.push(child);
35
+ }
36
+ }
37
+ flush();
38
+ return output;
39
+ }
40
+
41
+ /**
42
+ * The bottom strip — a comparison period, a caveat, a link.
43
+ *
44
+ * `Card.Footer`, so `variant="band"` sets it into the card on the next fill
45
+ * down, and it must be the last child for the same reason. Bare text becomes a
46
+ * muted caption at the card's scale.
47
+ */
48
+ export function KpiFooter({ children, ...props }: KpiFooterProps): ReactElement {
49
+ const { size } = useKpiPart("Kpi.Footer");
50
+ return <Card.Footer {...props}>{wrapBareText(children, kpiVariants({ size }).trendCaption())}</Card.Footer>;
51
+ }
52
+ KpiFooter.displayName = "DelacourUI.Kpi.Footer";
@@ -0,0 +1,70 @@
1
+ import { Children, type ReactElement, type ReactNode, useMemo } from "react";
2
+ import { View } from "react-native";
3
+ import { Separator } from "../separator";
4
+ import { Surface } from "../surface";
5
+ import { type KpiGroupContextValue, KpiGroupProvider } from "./kpi.context";
6
+ import type { KpiSlotProps } from "./kpi.types";
7
+ import { type KpiGroupOrientation, kpiVariants } from "./kpi.variants";
8
+
9
+ export type KpiGroupProps = KpiSlotProps & {
10
+ /** `horizontal` splits the row between the metrics; `vertical` stacks them. */
11
+ orientation?: KpiGroupOrientation;
12
+ /**
13
+ * Set the metrics on one surface with a rule between them, rather than as
14
+ * separate cards spaced apart. Several metrics divided by a rule read as one
15
+ * panel; several spaced apart read as several panels that happen to be
16
+ * adjacent.
17
+ */
18
+ separated?: boolean;
19
+ };
20
+
21
+ /** Puts a rule between adjacent children. `Separator` is hidden from assistive technology. */
22
+ function withRules(children: ReactNode, orientation: KpiGroupOrientation): ReactNode[] {
23
+ const output: ReactNode[] = [];
24
+ for (const [index, child] of Children.toArray(children).entries()) {
25
+ if (index > 0) {
26
+ output.push(
27
+ <Separator key={`rule-${index}`} orientation={orientation === "horizontal" ? "vertical" : "horizontal"} />
28
+ );
29
+ }
30
+ output.push(child);
31
+ }
32
+ return output;
33
+ }
34
+
35
+ /**
36
+ * Several metrics as one arrangement, in a row or a column.
37
+ *
38
+ * Each `Kpi` inside reads the group: in a row it takes an equal share of the
39
+ * width, and in a `separated` group it drops its own fill and hairline so the
40
+ * group's one surface holds them all, a rule between each.
41
+ */
42
+ export function KpiGroup({
43
+ orientation = "vertical",
44
+ separated = false,
45
+ className,
46
+ children,
47
+ ...props
48
+ }: KpiGroupProps): ReactElement {
49
+ const context = useMemo<KpiGroupContextValue>(() => ({ orientation, separated }), [orientation, separated]);
50
+ const slots = kpiVariants({ orientation, separated });
51
+
52
+ if (separated) {
53
+ return (
54
+ <KpiGroupProvider value={context}>
55
+ <Surface className={slots.group({ className })} padding="none" {...props}>
56
+ {withRules(children, orientation)}
57
+ </Surface>
58
+ </KpiGroupProvider>
59
+ );
60
+ }
61
+
62
+ return (
63
+ <KpiGroupProvider value={context}>
64
+ <View className={slots.group({ className })} {...props}>
65
+ {children}
66
+ </View>
67
+ </KpiGroupProvider>
68
+ );
69
+ }
70
+ KpiGroup.displayName = "DelacourUI.Kpi.Group";
@@ -0,0 +1,25 @@
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 { useKpiPart } from "./kpi.context";
6
+ import type { KpiSlotProps } from "./kpi.types";
7
+ import { kpiVariants } from "./kpi.variants";
8
+
9
+ /**
10
+ * The top row: a tinted icon, the metric's name, and anything acting on it.
11
+ *
12
+ * Sits on the card header's own inset — the classes are the card's, with the
13
+ * row centred rather than top-aligned — so the title lines up with the number
14
+ * and the footer below it at every size.
15
+ */
16
+ export function KpiHeader({ className, ...props }: KpiSlotProps): ReactElement {
17
+ const { size } = useKpiPart("Kpi.Header");
18
+ return (
19
+ <View
20
+ className={cardVariants({ size }).header({ className: cn(kpiVariants({ size }).header(), className) })}
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+ KpiHeader.displayName = "DelacourUI.Kpi.Header";
@@ -0,0 +1,28 @@
1
+ import { type ReactElement, useMemo } from "react";
2
+ import { View } from "react-native";
3
+ import { IconDefaultsProvider } from "../icon";
4
+ import { useKpiPart } from "./kpi.context";
5
+ import type { KpiSlotProps } from "./kpi.types";
6
+ import { kpiVariants } from "./kpi.variants";
7
+
8
+ /**
9
+ * A tinted square for a glyph.
10
+ *
11
+ * It takes the element rather than drawing one — a metric's icon is the app's
12
+ * choice — and hands an unstyled `Icon` inside it the card's series colour and
13
+ * a step of the icon scale sized to the square. The square is that colour at
14
+ * 15%, so a row of cards given five colour indexes reads as five series.
15
+ */
16
+ export function KpiIcon({ className, children, ...props }: KpiSlotProps): ReactElement {
17
+ const { size, colorIndex } = useKpiPart("Kpi.Icon");
18
+ const slots = kpiVariants({ colorIndex, size });
19
+ const glyph = slots.iconGlyph();
20
+ const defaults = useMemo(() => ({ className: glyph, color: `chart-${colorIndex}` }), [glyph, colorIndex]);
21
+
22
+ return (
23
+ <View className={slots.icon({ className })} {...props}>
24
+ <IconDefaultsProvider value={defaults}>{children}</IconDefaultsProvider>
25
+ </View>
26
+ );
27
+ }
28
+ KpiIcon.displayName = "DelacourUI.Kpi.Icon";
@@ -0,0 +1,145 @@
1
+ import type { CurveType } from "@delacour/react-native-charts/core";
2
+ import { type ReactElement, useMemo } from "react";
3
+ import { View } from "react-native";
4
+ import { useAnimatedReaction } from "react-native-reanimated";
5
+ import { scheduleOnRN } from "react-native-worklets";
6
+ import { Chart, type ChartConfig } from "../chart";
7
+ import { useChart } from "../chart/chart.context";
8
+ import { useKpiLayout, useKpiPart } from "./kpi.context";
9
+ import {
10
+ type KpiColorIndex,
11
+ type KpiSparklineData,
12
+ kpiSparklineAccessibilityLabel,
13
+ kpiVariants,
14
+ resolveSparklineDomain,
15
+ resolveSparklineFilled,
16
+ resolveSparklineSeries,
17
+ } from "./kpi.variants";
18
+
19
+ export type KpiSparklineProps = KpiSparklineData & {
20
+ /** Overrides the KPI's `colorIndex` for this chart. */
21
+ colorIndex?: KpiColorIndex;
22
+ /** A theme token or a literal, over the series colour entirely. */
23
+ color?: string;
24
+ /**
25
+ * Fill under the line. Defaults on under the card and off beside the
26
+ * number, where a fill would make the chart a second block competing with
27
+ * the value.
28
+ */
29
+ filled?: boolean;
30
+ strokeWidth?: number;
31
+ curve?: CurveType;
32
+ /**
33
+ * Hold and drag to scrub a point: a dot rides the line and the KPI's
34
+ * `activeIndex` follows it, so a custom part can print that point's value.
35
+ * On by default. The scrub starts on a hold, so a sparkline in a scrolling
36
+ * list does not steal the scroll.
37
+ */
38
+ interactive?: boolean;
39
+ /** Formats the first and last point for the screen-reader summary. */
40
+ formatValue?: (value: number) => string;
41
+ /** Replaces the screen-reader summary — "Trend over 30 points, from 120 to 184". */
42
+ accessibilityLabel?: string;
43
+ className?: string;
44
+ testID?: string;
45
+ };
46
+
47
+ /**
48
+ * Reports the scrubbed index to the KPI, as `null` once the finger lifts.
49
+ *
50
+ * A child of `<Chart>` that is not a mark, so the chart mounts it beside the
51
+ * canvas where its context — and the scrub's shared values — are in reach. It
52
+ * crosses to the JS thread only when the index changes, which is a few times
53
+ * per drag rather than once a frame.
54
+ */
55
+ function KpiSparklineScrub({ onChange }: { onChange: (index: number | null) => void }): null {
56
+ const { scrub } = useChart();
57
+
58
+ useAnimatedReaction(
59
+ () => (scrub.isActive.value ? scrub.index.value : -1),
60
+ (next, previous) => {
61
+ if (next === previous) return;
62
+ scheduleOnRN(onChange, next < 0 ? null : next);
63
+ }
64
+ );
65
+
66
+ return null;
67
+ }
68
+ KpiSparklineScrub.displayName = "DelacourUI.Kpi.Sparkline.Scrub";
69
+
70
+ /**
71
+ * The shape the number made getting there — a `Chart` line with no grid, no
72
+ * axes and no padding, in the KPI's series colour.
73
+ *
74
+ * Takes a bare list of numbers, or rows with an `xKey` and a `yKey`. Under the
75
+ * card it is full width and filled; beside the number (`Kpi.Content
76
+ * layout="inline"`) it is a fixed 128pt column, unfilled, so a stack of cards
77
+ * lines its shapes up down the right-hand edge whatever their labels. The y
78
+ * bounds are the data's own, padded a tenth each way, so the peak and trough
79
+ * are not drawn half off the canvas.
80
+ *
81
+ * Announced as one image — how many points and where they started and ended.
82
+ * While the KPI loads it is a muted block of its own size.
83
+ */
84
+ export function KpiSparkline(props: KpiSparklineProps): ReactElement {
85
+ const {
86
+ data,
87
+ xKey,
88
+ yKey,
89
+ colorIndex: colorIndexProp,
90
+ color,
91
+ filled: filledProp,
92
+ strokeWidth = 2,
93
+ curve,
94
+ interactive = true,
95
+ formatValue,
96
+ accessibilityLabel,
97
+ className,
98
+ testID,
99
+ } = props;
100
+ const context = useKpiPart("Kpi.Sparkline");
101
+ const layout = useKpiLayout();
102
+ const slots = kpiVariants({ layout, size: context.size });
103
+ const colorIndex = colorIndexProp ?? context.colorIndex;
104
+
105
+ // Keyed on the three fields the series reads rather than on the props
106
+ // object, which is new every render and would re-plot the chart with it.
107
+ // biome-ignore lint/correctness/useExhaustiveDependencies: see above
108
+ const series = useMemo(() => resolveSparklineSeries(props), [data, xKey, yKey]);
109
+ const domain = useMemo(() => ({ y: resolveSparklineDomain(series.values) }), [series]);
110
+ const config = useMemo<ChartConfig>(
111
+ () => ({ [series.yKey]: { label: series.yKey, color: color ?? `chart-${colorIndex}` } }),
112
+ [series.yKey, color, colorIndex]
113
+ );
114
+
115
+ if (context.isLoading) {
116
+ return <View className={slots.sparklinePlaceholder({ className: slots.sparkline({ className }) })} />;
117
+ }
118
+
119
+ const filled = resolveSparklineFilled({ filled: filledProp, layout });
120
+
121
+ return (
122
+ <View
123
+ accessibilityLabel={accessibilityLabel ?? kpiSparklineAccessibilityLabel(series.values, formatValue)}
124
+ accessibilityRole="image"
125
+ accessible
126
+ className={slots.sparkline({ className })}
127
+ >
128
+ <Chart
129
+ config={config}
130
+ curve={curve}
131
+ data={series.rows}
132
+ domain={domain}
133
+ frameClassName={slots.sparklineFrame()}
134
+ testID={testID}
135
+ xKey={series.xKey}
136
+ >
137
+ {filled ? <Chart.Area yKey={series.yKey} /> : null}
138
+ <Chart.Line strokeWidth={strokeWidth} yKey={series.yKey} />
139
+ {interactive ? <Chart.Tooltip.Dot yKey={series.yKey} /> : null}
140
+ {interactive ? <KpiSparklineScrub onChange={context.setActiveIndex} /> : null}
141
+ </Chart>
142
+ </View>
143
+ );
144
+ }
145
+ KpiSparkline.displayName = "DelacourUI.Kpi.Sparkline";
@@ -0,0 +1,20 @@
1
+ import type { ReactElement } from "react";
2
+ import { View } from "react-native";
3
+ import { useKpiLayout, useKpiPart } from "./kpi.context";
4
+ import type { KpiSlotProps } from "./kpi.types";
5
+ import { kpiVariants } from "./kpi.variants";
6
+
7
+ /**
8
+ * The value and its change, stacked tight.
9
+ *
10
+ * Its own container rather than loose children of the content, because the
11
+ * number and its change are one fact in two lines and the card's spacing is
12
+ * for the gaps *between* facts. Beside an `inline` sparkline it takes the
13
+ * row's width, leaving the chart its column on the end.
14
+ */
15
+ export function KpiStat({ className, ...props }: KpiSlotProps): ReactElement {
16
+ const { size } = useKpiPart("Kpi.Stat");
17
+ const layout = useKpiLayout();
18
+ return <View className={kpiVariants({ layout, size }).stat({ className })} {...props} />;
19
+ }
20
+ KpiStat.displayName = "DelacourUI.Kpi.Stat";
@@ -0,0 +1,19 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text } from "../text";
3
+ import { useKpiPart } from "./kpi.context";
4
+ import type { KpiTextProps } from "./kpi.types";
5
+ import { kpiVariants } from "./kpi.variants";
6
+
7
+ /**
8
+ * The metric's name. Quiet on purpose — `muted-foreground`, a step down the
9
+ * type scale — because the value is the thing being read.
10
+ *
11
+ * It never grows to fill its row: in a column that would absorb the spare
12
+ * height and land a row of cards' numbers at different heights. The header
13
+ * pushes its action to the end instead.
14
+ */
15
+ export function KpiTitle({ className, ...props }: KpiTextProps): ReactElement {
16
+ const { size } = useKpiPart("Kpi.Title");
17
+ return <Text className={kpiVariants({ size }).title({ className })} numberOfLines={1} {...props} />;
18
+ }
19
+ KpiTitle.displayName = "DelacourUI.Kpi.Title";
@@ -0,0 +1,105 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { IconArrowDownRight, IconArrowRight, IconArrowUpRight } from "../../icons/central";
4
+ import { Badge } from "../badge";
5
+ import { Icon, type IconComponent } from "../icon";
6
+ import { Text } from "../text";
7
+ import { useKpiPart } from "./kpi.context";
8
+ import {
9
+ formatKpiTrend,
10
+ KPI_TONE_BADGE_COLOR,
11
+ type KpiDirection,
12
+ type KpiGoodDirection,
13
+ type KpiSize,
14
+ type KpiTrendVariant,
15
+ kpiTrendAccessibilityLabel,
16
+ kpiVariants,
17
+ resolveKpiTrend,
18
+ } from "./kpi.variants";
19
+
20
+ export type KpiTrendProps = Omit<ViewProps, "children"> & {
21
+ /**
22
+ * How much it moved, as a percentage. The sign carries the direction, so
23
+ * `-4.2` is a fall of 4.2%; there is no direction prop to keep in step.
24
+ */
25
+ value: number;
26
+ /** Writes the number yourself. Receives the raw value, sign and all. */
27
+ format?: (value: number) => string;
28
+ /** Overrides the KPI's own `goodDirection` for this one figure. */
29
+ goodDirection?: KpiGoodDirection;
30
+ /**
31
+ * `text` is a line of colour under the number — what a stat card usually
32
+ * wants. `badge` puts a pill round it with an arrow, for a card busy enough
33
+ * that a bare line of colour is lost in it.
34
+ */
35
+ variant?: KpiTrendVariant;
36
+ /** What it is compared against — "vs last month", "last 30d". Read after the change. */
37
+ caption?: string;
38
+ /** A movement no larger than this counts as none. Defaults to `0`. */
39
+ threshold?: number;
40
+ className?: string;
41
+ /** Anything after the caption, when a string is not enough. */
42
+ children?: ReactNode;
43
+ };
44
+
45
+ const ARROW: Record<KpiDirection, IconComponent> = {
46
+ up: IconArrowUpRight,
47
+ down: IconArrowDownRight,
48
+ flat: IconArrowRight,
49
+ };
50
+
51
+ const BADGE_SIZE: Record<KpiSize, "sm" | "md"> = { sm: "sm", md: "sm", lg: "md" };
52
+
53
+ /**
54
+ * The change: a signed percentage, coloured by what it means.
55
+ *
56
+ * It derives its own direction from the sign and its own tone from the KPI's
57
+ * `goodDirection`, so a fall in churn is drawn as good news without the call
58
+ * site naming a colour. The arrow on a badge says the direction again in a
59
+ * shape, so the meaning never rests on colour alone.
60
+ *
61
+ * Announced as one string — "Up 7.8 percent, vs last month" — rather than a
62
+ * number, an arrow and a caption read as three stops. While the KPI loads it
63
+ * is a short muted bar.
64
+ */
65
+ export function KpiTrend({
66
+ value,
67
+ format,
68
+ goodDirection: goodDirectionProp,
69
+ variant = "text",
70
+ caption,
71
+ threshold,
72
+ className,
73
+ children,
74
+ ...props
75
+ }: KpiTrendProps): ReactElement {
76
+ const context = useKpiPart("Kpi.Trend");
77
+ const { size, isLoading } = context;
78
+ const goodDirection = goodDirectionProp ?? context.goodDirection;
79
+ const { direction, tone } = resolveKpiTrend({ goodDirection, threshold, value });
80
+ const slots = kpiVariants({ size, tone });
81
+
82
+ if (isLoading) {
83
+ return <View className={slots.trendPlaceholder()} />;
84
+ }
85
+
86
+ const formatted = format?.(value);
87
+ const text = formatted ?? formatKpiTrend(value);
88
+ const accessibilityLabel = kpiTrendAccessibilityLabel({ caption, direction, formatted, value });
89
+
90
+ return (
91
+ <View accessibilityLabel={accessibilityLabel} accessible className={slots.trend({ className })} {...props}>
92
+ {variant === "badge" ? (
93
+ <Badge color={KPI_TONE_BADGE_COLOR[tone]} size={BADGE_SIZE[size]} variant="soft">
94
+ <Icon icon={ARROW[direction]} />
95
+ <Badge.Label>{text}</Badge.Label>
96
+ </Badge>
97
+ ) : (
98
+ <Text className={slots.trendText()}>{text}</Text>
99
+ )}
100
+ {caption === undefined ? null : <Text className={slots.trendCaption()}>{caption}</Text>}
101
+ {children}
102
+ </View>
103
+ );
104
+ }
105
+ KpiTrend.displayName = "DelacourUI.Kpi.Trend";
@@ -0,0 +1,32 @@
1
+ import type { ReactElement } from "react";
2
+ import { View } from "react-native";
3
+ import { useCardContext } from "../card/card.context";
4
+ import { Text } from "../text";
5
+ import { useKpiPart } from "./kpi.context";
6
+ import type { KpiTextProps } from "./kpi.types";
7
+ import { kpiVariants } from "./kpi.variants";
8
+
9
+ /**
10
+ * The number.
11
+ *
12
+ * Formatted by the caller, not here — separators, currency and units are
13
+ * locale decisions a component would get wrong in a way that is hard to
14
+ * notice and impossible to override. It takes the foreground token of the
15
+ * plane the card landed on, like `Card.Title`, and one line: a number that
16
+ * wraps is no longer read as one.
17
+ *
18
+ * While the KPI is loading it is a muted block the height of the number's
19
+ * line, announced as loading, so the card does not jump when the data lands.
20
+ */
21
+ export function KpiValue({ className, ...props }: KpiTextProps): ReactElement {
22
+ const { size, isLoading } = useKpiPart("Kpi.Value");
23
+ const plane = useCardContext()?.plane ?? null;
24
+ const slots = kpiVariants({ plane: plane ?? "none", size });
25
+
26
+ if (isLoading) {
27
+ return <View accessibilityLabel="Loading" accessible className={slots.valuePlaceholder()} />;
28
+ }
29
+
30
+ return <Text adjustsFontSizeToFit className={slots.value({ className })} numberOfLines={1} {...props} />;
31
+ }
32
+ KpiValue.displayName = "DelacourUI.Kpi.Value";
@@ -0,0 +1,111 @@
1
+ import { createContext, type ReactElement, type ReactNode, use } from "react";
2
+ import type { KpiColorIndex, KpiGoodDirection, KpiGroupOrientation, KpiLayout, KpiSize } from "./kpi.variants";
3
+
4
+ export type KpiContextValue = {
5
+ /** The card's size — the inset, the value's scale and the sparkline's height. */
6
+ size: KpiSize;
7
+ /** Which `--chart-*` token the sparkline and the icon take. */
8
+ colorIndex: KpiColorIndex;
9
+ /** Which way is the good news, for every trend inside that does not override it. */
10
+ goodDirection: KpiGoodDirection;
11
+ /** Whether the numbers are still on their way. The value, trend and sparkline hold placeholders. */
12
+ isLoading: boolean;
13
+ /**
14
+ * The sparkline point being scrubbed, or `null` when nothing is. A custom
15
+ * part reads it to print that point's value in place of the latest one.
16
+ */
17
+ activeIndex: number | null;
18
+ /** Moves the scrubbed point. The sparkline calls it; a caller holding the state may too. */
19
+ setActiveIndex: (index: number | null) => void;
20
+ };
21
+
22
+ const KpiContext = createContext<KpiContextValue | null>(null);
23
+
24
+ /**
25
+ * Supplies the enclosing KPI's size, colour, direction and scrub to its subtree.
26
+ *
27
+ * Lives in its own module, importing nothing but types, so a part the KPI
28
+ * renders can read it without importing `./kpi` and closing a cycle through
29
+ * `kpi.tsx`. See AGENTS.md rule 3.
30
+ */
31
+ export function KpiProvider({ value, children }: { value: KpiContextValue; children: ReactNode }): ReactElement {
32
+ return <KpiContext value={value}>{children}</KpiContext>;
33
+ }
34
+ KpiProvider.displayName = "DelacourUI.Kpi.Provider";
35
+
36
+ /** The enclosing KPI's context, or null outside a `<Kpi>`. */
37
+ export function useKpiContext(): KpiContextValue | null {
38
+ return use(KpiContext);
39
+ }
40
+
41
+ /**
42
+ * Reads the enclosing KPI's size, colour, direction, loading state and scrub.
43
+ *
44
+ * What a custom part uses to print the scrubbed point's value, match the
45
+ * card's series colour, or hold a placeholder while it loads. Throws outside a
46
+ * `<Kpi>`; use {@link useKpiContext} where the enclosing KPI is optional.
47
+ */
48
+ export function useKpi(): KpiContextValue {
49
+ const context = useKpiContext();
50
+ if (!context) {
51
+ throw new Error("useKpi must be called inside a <Kpi>.");
52
+ }
53
+ return context;
54
+ }
55
+
56
+ /**
57
+ * The enclosing KPI's context, for a compound part that cannot work without one.
58
+ *
59
+ * Internal: deliberately not re-exported from `index.ts`. A caller outside the
60
+ * library wants {@link useKpi}, whose error message names the hook rather than
61
+ * a part.
62
+ */
63
+ export function useKpiPart(component: string): KpiContextValue {
64
+ const context = useKpiContext();
65
+ if (!context) {
66
+ throw new Error(`${component} must be rendered inside a <Kpi>.`);
67
+ }
68
+ return context;
69
+ }
70
+
71
+ const KpiLayoutContext = createContext<KpiLayout>("below");
72
+
73
+ /**
74
+ * Supplies `Kpi.Content`'s layout to the stat and the sparkline inside it —
75
+ * the stat takes the row's width and the sparkline a fixed column when it is
76
+ * `inline`.
77
+ */
78
+ export function KpiLayoutProvider({ value, children }: { value: KpiLayout; children: ReactNode }): ReactElement {
79
+ return <KpiLayoutContext value={value}>{children}</KpiLayoutContext>;
80
+ }
81
+ KpiLayoutProvider.displayName = "DelacourUI.Kpi.Content.Provider";
82
+
83
+ /** The enclosing `Kpi.Content`'s layout — `below` outside one. */
84
+ export function useKpiLayout(): KpiLayout {
85
+ return use(KpiLayoutContext);
86
+ }
87
+
88
+ export type KpiGroupContextValue = {
89
+ orientation: KpiGroupOrientation;
90
+ /** Whether the metrics share one surface with a rule between them. */
91
+ separated: boolean;
92
+ };
93
+
94
+ const KpiGroupContext = createContext<KpiGroupContextValue | null>(null);
95
+
96
+ /** Supplies a `Kpi.Group`'s arrangement to the metrics inside it. */
97
+ export function KpiGroupProvider({
98
+ value,
99
+ children,
100
+ }: {
101
+ value: KpiGroupContextValue;
102
+ children: ReactNode;
103
+ }): ReactElement {
104
+ return <KpiGroupContext value={value}>{children}</KpiGroupContext>;
105
+ }
106
+ KpiGroupProvider.displayName = "DelacourUI.Kpi.Group.Provider";
107
+
108
+ /** The enclosing `Kpi.Group`, or null outside one. */
109
+ export function useKpiGroupContext(): KpiGroupContextValue | null {
110
+ return use(KpiGroupContext);
111
+ }