@delacour/react-native-ui 0.1.0-alpha.3 → 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 (482) hide show
  1. package/README.md +11 -7
  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 +5 -0
  34. package/src/components/badge/badge.tsx +6 -1
  35. package/src/components/bottom-sheet/AGENTS.md +186 -221
  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 -207
  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 -120
  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 -52
  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 -70
  55. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -105
  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 +9 -1
  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/screen/screen-navbar-back-button.tsx +1 -1
  344. package/src/components/selection-mode/AGENTS.md +155 -0
  345. package/src/components/selection-mode/index.ts +43 -0
  346. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  347. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  348. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  349. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  350. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  351. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  352. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  353. package/src/components/selection-mode/selection-mode.tsx +207 -0
  354. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  355. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  356. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  357. package/src/components/skeleton/AGENTS.md +120 -0
  358. package/src/components/skeleton/index.ts +15 -0
  359. package/src/components/skeleton/skeleton-clock.ts +48 -0
  360. package/src/components/skeleton/skeleton-group.tsx +64 -0
  361. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  362. package/src/components/skeleton/skeleton.context.tsx +45 -0
  363. package/src/components/skeleton/skeleton.tsx +227 -0
  364. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  365. package/src/components/skeleton/skeleton.variants.ts +222 -0
  366. package/src/components/slide-button/AGENTS.md +122 -0
  367. package/src/components/slide-button/index.ts +43 -0
  368. package/src/components/slide-button/slide-button-label.tsx +70 -0
  369. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  370. package/src/components/slide-button/slide-button.context.tsx +76 -0
  371. package/src/components/slide-button/slide-button.tsx +457 -0
  372. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  373. package/src/components/slide-button/slide-button.variants.ts +324 -0
  374. package/src/components/slider/index.ts +2 -0
  375. package/src/components/slider/slider-thumb.tsx +33 -1
  376. package/src/components/slider/slider.variants.test.ts +52 -0
  377. package/src/components/slider/slider.variants.ts +56 -2
  378. package/src/components/stack-card/AGENTS.md +169 -0
  379. package/src/components/stack-card/index.ts +37 -0
  380. package/src/components/stack-card/stack-card-action.tsx +66 -0
  381. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  382. package/src/components/stack-card/stack-card-card.tsx +71 -0
  383. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  384. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  385. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  386. package/src/components/stack-card/stack-card.context.tsx +117 -0
  387. package/src/components/stack-card/stack-card.tsx +557 -0
  388. package/src/components/stack-card/stack-card.types.ts +26 -0
  389. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  390. package/src/components/stack-card/stack-card.variants.ts +360 -0
  391. package/src/components/steps/AGENTS.md +153 -0
  392. package/src/components/steps/index.ts +45 -0
  393. package/src/components/steps/steps-connector.tsx +31 -0
  394. package/src/components/steps/steps-description.tsx +26 -0
  395. package/src/components/steps/steps-indicator.tsx +106 -0
  396. package/src/components/steps/steps-item.tsx +225 -0
  397. package/src/components/steps/steps-panel.tsx +41 -0
  398. package/src/components/steps/steps-title.tsx +32 -0
  399. package/src/components/steps/steps-track.tsx +59 -0
  400. package/src/components/steps/steps.context.tsx +123 -0
  401. package/src/components/steps/steps.tsx +166 -0
  402. package/src/components/steps/steps.variants.test.ts +388 -0
  403. package/src/components/steps/steps.variants.ts +468 -0
  404. package/src/components/surface/AGENTS.md +72 -0
  405. package/src/components/surface/index.ts +15 -0
  406. package/src/components/surface/surface.context.tsx +54 -0
  407. package/src/components/surface/surface.tsx +70 -0
  408. package/src/components/surface/surface.variants.test.ts +180 -0
  409. package/src/components/surface/surface.variants.ts +133 -0
  410. package/src/components/swipe/AGENTS.md +140 -0
  411. package/src/components/swipe/index.ts +31 -0
  412. package/src/components/swipe/swipe-action.tsx +99 -0
  413. package/src/components/swipe/swipe-end.tsx +14 -0
  414. package/src/components/swipe/swipe-group.tsx +55 -0
  415. package/src/components/swipe/swipe-panel.tsx +55 -0
  416. package/src/components/swipe/swipe-start.tsx +15 -0
  417. package/src/components/swipe/swipe.context.tsx +135 -0
  418. package/src/components/swipe/swipe.tsx +393 -0
  419. package/src/components/swipe/swipe.types.ts +7 -0
  420. package/src/components/swipe/swipe.variants.test.ts +337 -0
  421. package/src/components/swipe/swipe.variants.ts +290 -0
  422. package/src/components/switch/switch.variants.ts +2 -2
  423. package/src/components/tabs/AGENTS.md +4 -3
  424. package/src/components/tabs/tabs.variants.test.ts +1 -1
  425. package/src/components/tabs/tabs.variants.ts +4 -6
  426. package/src/components/textarea/AGENTS.md +86 -0
  427. package/src/components/textarea/index.ts +16 -0
  428. package/src/components/textarea/textarea.tsx +157 -0
  429. package/src/components/textarea/textarea.variants.test.ts +219 -0
  430. package/src/components/textarea/textarea.variants.ts +171 -0
  431. package/src/components/toast/AGENTS.md +140 -0
  432. package/src/components/toast/index.ts +74 -0
  433. package/src/components/toast/toast-action.tsx +59 -0
  434. package/src/components/toast/toast-api.ts +32 -0
  435. package/src/components/toast/toast-close.tsx +44 -0
  436. package/src/components/toast/toast-content.tsx +11 -0
  437. package/src/components/toast/toast-description.tsx +11 -0
  438. package/src/components/toast/toast-indicator.tsx +34 -0
  439. package/src/components/toast/toast-item.tsx +308 -0
  440. package/src/components/toast/toast-title.tsx +13 -0
  441. package/src/components/toast/toast-viewport.tsx +135 -0
  442. package/src/components/toast/toast.context.tsx +69 -0
  443. package/src/components/toast/toast.store.test.ts +259 -0
  444. package/src/components/toast/toast.store.ts +338 -0
  445. package/src/components/toast/toast.timer.test.ts +64 -0
  446. package/src/components/toast/toast.timer.ts +51 -0
  447. package/src/components/toast/toast.tsx +132 -0
  448. package/src/components/toast/toast.variants.test.ts +393 -0
  449. package/src/components/toast/toast.variants.ts +304 -0
  450. package/src/components/toggle-button/AGENTS.md +106 -0
  451. package/src/components/toggle-button/index.ts +30 -0
  452. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  453. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  454. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  455. package/src/components/toggle-button/toggle-button.tsx +172 -0
  456. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  457. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  458. package/src/components/tooltip/AGENTS.md +133 -0
  459. package/src/components/tooltip/index.ts +28 -0
  460. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  461. package/src/components/tooltip/tooltip-content.tsx +179 -0
  462. package/src/components/tooltip/tooltip-description.tsx +19 -0
  463. package/src/components/tooltip/tooltip-text.tsx +21 -0
  464. package/src/components/tooltip/tooltip-title.tsx +20 -0
  465. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  466. package/src/components/tooltip/tooltip.context.tsx +68 -0
  467. package/src/components/tooltip/tooltip.tsx +213 -0
  468. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  469. package/src/components/tooltip/tooltip.variants.ts +177 -0
  470. package/src/hooks/use-calm-motion.tsx +31 -0
  471. package/src/lib/calm-motion.test.ts +17 -0
  472. package/src/lib/calm-motion.ts +23 -0
  473. package/src/lib/cn.test.ts +14 -0
  474. package/src/lib/cn.ts +2 -2
  475. package/src/no-gorhom.test.ts +54 -0
  476. package/src/styles/geometry.test.ts +1 -0
  477. package/src/styles/theme.css +4 -0
  478. package/src/styles/tokens.css +11 -0
  479. package/src/styles/tokens.test.ts +22 -0
  480. package/src/styles/tokens.ts +20 -0
  481. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  482. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -0,0 +1,347 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+ import type { BadgeColor } from "../badge/badge.variants";
4
+ import { CARD_SIZES } from "../card/card.variants";
5
+ import type { ChartDatum } from "../chart/chart.types";
6
+
7
+ /** The card's own three sizes — a KPI is a card, so it has no scale of its own. */
8
+ export const KPI_SIZES = CARD_SIZES;
9
+
10
+ /**
11
+ * Which way is the good news. `up` for revenue and sign-ups, `down` for churn
12
+ * and latency, `none` for a number that is neither — a headcount, a version.
13
+ */
14
+ export const KPI_GOOD_DIRECTIONS = ["up", "down", "none"] as const;
15
+
16
+ /** Which way the number moved. `flat` is a movement inside the threshold. */
17
+ export const KPI_DIRECTIONS = ["up", "down", "flat"] as const;
18
+
19
+ /** What the movement means — the colour it is painted, whatever its sign. */
20
+ export const KPI_TONES = ["good", "bad", "neutral"] as const;
21
+
22
+ /** `text` is a line of colour under the number; `badge` puts a pill round it, with an arrow. */
23
+ export const KPI_TREND_VARIANTS = ["text", "badge"] as const;
24
+
25
+ /** `below` puts the sparkline under everything; `inline` sets it in a column beside the number. */
26
+ export const KPI_LAYOUTS = ["below", "inline"] as const;
27
+
28
+ /** The five slots of the theme's series ramp, `--chart-1` … `--chart-5`. */
29
+ export const KPI_COLOR_INDEXES = [1, 2, 3, 4, 5] as const;
30
+
31
+ /** `horizontal` splits a row between the metrics; `vertical` stacks them. */
32
+ export const KPI_GROUP_ORIENTATIONS = ["horizontal", "vertical"] as const;
33
+
34
+ export type KpiSize = (typeof KPI_SIZES)[number];
35
+ export type KpiGoodDirection = (typeof KPI_GOOD_DIRECTIONS)[number];
36
+ export type KpiDirection = (typeof KPI_DIRECTIONS)[number];
37
+ export type KpiTone = (typeof KPI_TONES)[number];
38
+ export type KpiTrendVariant = (typeof KPI_TREND_VARIANTS)[number];
39
+ export type KpiLayout = (typeof KPI_LAYOUTS)[number];
40
+ export type KpiColorIndex = (typeof KPI_COLOR_INDEXES)[number];
41
+ export type KpiGroupOrientation = (typeof KPI_GROUP_ORIENTATIONS)[number];
42
+
43
+ /** A badge trend's colour, by what the movement means. */
44
+ export const KPI_TONE_BADGE_COLOR: Record<KpiTone, BadgeColor> = {
45
+ good: "success",
46
+ bad: "destructive",
47
+ neutral: "default",
48
+ };
49
+
50
+ /**
51
+ * Styling for every part of a KPI.
52
+ *
53
+ * The card itself — fill, hairline, corner, the vertical rhythm and the inset
54
+ * — is `Card`'s. The header and content slots here carry **no inset**: the
55
+ * parts compose them onto `cardVariants`' own `header` and `content`, so a
56
+ * KPI's number sits on exactly the line its card's footer does and a retune of
57
+ * the card moves both.
58
+ *
59
+ * The value is keyed on `plane` the same way `Card.Title` is, so a KPI nested
60
+ * in a secondary surface reads `secondary-foreground` rather than the card's
61
+ * token on something else. The title is `muted-foreground` everywhere: it is
62
+ * a label, and the value is the thing being read.
63
+ *
64
+ * The title never grows. A growing child of a column absorbs that column's
65
+ * spare height, which in a row of cards lands each number at a different
66
+ * height; the header pushes its action to the end with `ml-auto` instead.
67
+ *
68
+ * The trend is coloured by `tone` — what the movement means — and never by its
69
+ * sign: a fall in churn is good news, drawn as good news. The two colours are
70
+ * the `-soft-foreground` state tokens, which are built to be read as text on a
71
+ * card in both themes.
72
+ *
73
+ * The icon square is the series colour at 15%, one token per slot written out,
74
+ * because Tailwind's scanner cannot see a `bg-chart-${n}` built at runtime.
75
+ *
76
+ * View slots carry no `text-*` (rule 1). Free of React Native imports so it
77
+ * stays unit-testable.
78
+ */
79
+ export const kpiVariants = tv({
80
+ slots: {
81
+ header: "flex-row items-center gap-2",
82
+ icon: "shrink-0 items-center justify-center rounded-md",
83
+ /** Not a view — the class an unstyled `Icon` inside `Kpi.Icon` adopts. */
84
+ iconGlyph: "",
85
+ title: "font-medium text-muted-foreground",
86
+ action: "ml-auto shrink-0 flex-row items-center gap-2",
87
+ content: "self-stretch",
88
+ stat: "gap-1",
89
+ value: "font-semibold",
90
+ valuePlaceholder: "rounded-md bg-muted-foreground/15",
91
+ trend: "flex-row flex-wrap items-center gap-x-1.5 gap-y-1",
92
+ trendText: "font-medium",
93
+ trendCaption: "text-muted-foreground",
94
+ trendPlaceholder: "rounded-sm bg-muted-foreground/15",
95
+ sparkline: "",
96
+ sparklineFrame: "",
97
+ sparklinePlaceholder: "w-full rounded-md bg-muted-foreground/15",
98
+ group: "gap-3",
99
+ groupItem: "",
100
+ },
101
+ variants: {
102
+ size: {
103
+ sm: {
104
+ icon: "size-6",
105
+ iconGlyph: "size-icon-xs",
106
+ title: "text-xs",
107
+ value: "text-2xl",
108
+ valuePlaceholder: "h-8 w-24",
109
+ trendText: "text-xs",
110
+ trendCaption: "text-xs",
111
+ trendPlaceholder: "h-4 w-20",
112
+ },
113
+ md: {
114
+ icon: "size-8",
115
+ iconGlyph: "size-icon-sm",
116
+ title: "text-sm",
117
+ value: "text-3xl",
118
+ valuePlaceholder: "h-9 w-28",
119
+ trendText: "text-sm",
120
+ trendCaption: "text-sm",
121
+ trendPlaceholder: "h-5 w-24",
122
+ },
123
+ lg: {
124
+ icon: "size-10",
125
+ iconGlyph: "size-icon-lg",
126
+ title: "text-base",
127
+ value: "text-4xl",
128
+ valuePlaceholder: "h-10 w-36",
129
+ trendText: "text-base",
130
+ trendCaption: "text-base",
131
+ trendPlaceholder: "h-6 w-28",
132
+ },
133
+ },
134
+ plane: {
135
+ default: { value: "text-card-foreground" },
136
+ secondary: { value: "text-secondary-foreground" },
137
+ tertiary: { value: "text-tertiary-foreground" },
138
+ none: { value: "text-foreground" },
139
+ },
140
+ tone: {
141
+ good: { trendText: "text-success-soft-foreground" },
142
+ bad: { trendText: "text-destructive-soft-foreground" },
143
+ neutral: { trendText: "text-muted-foreground" },
144
+ },
145
+ colorIndex: {
146
+ 1: { icon: "bg-chart-1/15" },
147
+ 2: { icon: "bg-chart-2/15" },
148
+ 3: { icon: "bg-chart-3/15" },
149
+ 4: { icon: "bg-chart-4/15" },
150
+ 5: { icon: "bg-chart-5/15" },
151
+ },
152
+ layout: {
153
+ below: { sparkline: "w-full" },
154
+ inline: {
155
+ content: "flex-row items-end gap-4",
156
+ stat: "min-w-0 flex-1",
157
+ sparkline: "w-32 shrink-0",
158
+ },
159
+ },
160
+ orientation: {
161
+ horizontal: { group: "flex-row", groupItem: "min-w-0 flex-1" },
162
+ vertical: {},
163
+ },
164
+ // A separated group is one surface with rules between its metrics; the
165
+ // rule is the spacing, so there is no gap to add to it.
166
+ separated: {
167
+ true: { group: "gap-0" },
168
+ false: {},
169
+ },
170
+ },
171
+ compoundVariants: [
172
+ { layout: "below", size: "sm", class: { sparklineFrame: "h-12", sparklinePlaceholder: "h-12" } },
173
+ { layout: "below", size: "md", class: { sparklineFrame: "h-16", sparklinePlaceholder: "h-16" } },
174
+ { layout: "below", size: "lg", class: { sparklineFrame: "h-20", sparklinePlaceholder: "h-20" } },
175
+ { layout: "inline", size: "sm", class: { sparklineFrame: "h-8", sparklinePlaceholder: "h-8" } },
176
+ { layout: "inline", size: "md", class: { sparklineFrame: "h-10", sparklinePlaceholder: "h-10" } },
177
+ { layout: "inline", size: "lg", class: { sparklineFrame: "h-12", sparklinePlaceholder: "h-12" } },
178
+ ],
179
+ defaultVariants: {
180
+ size: "md",
181
+ plane: "default",
182
+ tone: "neutral",
183
+ colorIndex: 1,
184
+ layout: "below",
185
+ orientation: "vertical",
186
+ separated: false,
187
+ },
188
+ });
189
+
190
+ export type KpiVariantProps = VariantProps<typeof kpiVariants>;
191
+
192
+ /**
193
+ * Which way a number moved and what that means.
194
+ *
195
+ * The sign carries the direction, so there is no separate direction prop to
196
+ * keep in step with the value. A movement no larger than `threshold` counts as
197
+ * none, and a value that is not finite — a change against a zero baseline —
198
+ * is flat rather than painted as news of either kind.
199
+ */
200
+ export function resolveKpiTrend({
201
+ value,
202
+ goodDirection = "up",
203
+ threshold = 0,
204
+ }: {
205
+ value: number;
206
+ goodDirection?: KpiGoodDirection;
207
+ threshold?: number;
208
+ }): { direction: KpiDirection; tone: KpiTone } {
209
+ const floor = Math.max(0, threshold);
210
+ if (!Number.isFinite(value) || Math.abs(value) <= floor) {
211
+ return { direction: "flat", tone: "neutral" };
212
+ }
213
+ const direction = value > 0 ? "up" : "down";
214
+ if (goodDirection === "none") return { direction, tone: "neutral" };
215
+ return { direction, tone: direction === goodDirection ? "good" : "bad" };
216
+ }
217
+
218
+ /** U+2212, the width of a plus sign. A hyphen-minus is narrower and misaligns a column. */
219
+ const MINUS = "−";
220
+
221
+ /**
222
+ * A signed percentage to one decimal place: `+7.8%`, `−4.2%`, `0.0%`.
223
+ *
224
+ * Signed explicitly both ways, with the typographic minus. A value that rounds
225
+ * to zero is printed unsigned — `−0.0%` would claim a fall nothing shows. A
226
+ * value that is not finite prints as an em dash.
227
+ */
228
+ export function formatKpiTrend(value: number): string {
229
+ if (!Number.isFinite(value)) return "—";
230
+ const magnitude = Math.abs(value).toFixed(1);
231
+ if (Number(magnitude) === 0) return `${magnitude}%`;
232
+ return `${value > 0 ? "+" : MINUS}${magnitude}%`;
233
+ }
234
+
235
+ const DIRECTION_WORD: Record<KpiDirection, string> = { up: "Up", down: "Down", flat: "No change" };
236
+
237
+ /**
238
+ * What a screen reader says for a trend, as one string.
239
+ *
240
+ * "Up 7.8 percent, vs last month" rather than an arrow, a number and a caption
241
+ * read as three unrelated stops. A caller's own `formatted` text may not be a
242
+ * percentage at all, so it is read as written after the direction.
243
+ */
244
+ export function kpiTrendAccessibilityLabel({
245
+ value,
246
+ direction,
247
+ caption,
248
+ formatted,
249
+ }: {
250
+ value: number;
251
+ direction: KpiDirection;
252
+ caption?: string;
253
+ formatted?: string;
254
+ }): string {
255
+ const head =
256
+ direction === "flat"
257
+ ? DIRECTION_WORD.flat
258
+ : formatted === undefined
259
+ ? `${DIRECTION_WORD[direction]} ${Math.abs(value).toFixed(1)} percent`
260
+ : `${DIRECTION_WORD[direction]}, ${formatted}`;
261
+ return caption === undefined || caption === "" ? head : `${head}, ${caption}`;
262
+ }
263
+
264
+ /**
265
+ * A sparkline's data: a bare list of numbers, or rows with the keys to read.
266
+ *
267
+ * The two are told apart by `xKey`, which rows need and numbers do not have.
268
+ */
269
+ export type KpiSparklineData =
270
+ | { readonly data: readonly number[]; readonly xKey?: undefined; readonly yKey?: undefined }
271
+ | { readonly data: readonly ChartDatum[]; readonly xKey: string; readonly yKey: string };
272
+
273
+ /** What the sparkline hands `Chart`, plus the plain values for everything else to read. */
274
+ export type KpiSparklineSeries = {
275
+ readonly rows: readonly ChartDatum[];
276
+ readonly xKey: string;
277
+ readonly yKey: string;
278
+ /** One per row, `NaN` where a row has no number. */
279
+ readonly values: readonly number[];
280
+ };
281
+
282
+ function numberOf(value: unknown): number {
283
+ return typeof value === "number" ? value : Number.NaN;
284
+ }
285
+
286
+ /**
287
+ * Turns either data shape into rows `Chart` can plot.
288
+ *
289
+ * A bare list of numbers is indexed — `{ index, value }` per point — because a
290
+ * sparkline's x is only ever its order. Rows pass through untouched, keyed by
291
+ * the caller.
292
+ */
293
+ export function resolveSparklineSeries(input: KpiSparklineData): KpiSparklineSeries {
294
+ if (input.xKey === undefined) {
295
+ const rows = input.data.map((value, index) => ({ index, value }));
296
+ return { rows, xKey: "index", yKey: "value", values: input.data };
297
+ }
298
+ const { data, xKey, yKey } = input;
299
+ return { rows: data, xKey, yKey, values: data.map((row) => numberOf(row[yKey])) };
300
+ }
301
+
302
+ /** The fraction of the data's span added above its peak and below its trough. */
303
+ const SPARKLINE_HEADROOM = 0.1;
304
+
305
+ /**
306
+ * The y bounds a sparkline plots against: the data's own extent, padded.
307
+ *
308
+ * A sparkline has no axis and no padding, so a line drawn to the data's exact
309
+ * extent puts its peak and its trough on the frame's edge with half the stroke
310
+ * off the canvas. A tenth of the span each side keeps both inside. A flat
311
+ * series has no span, so it gets a unit either side and sits centred.
312
+ */
313
+ export function resolveSparklineDomain(values: readonly number[]): readonly [number, number] | undefined {
314
+ const finite = values.filter(Number.isFinite);
315
+ if (finite.length === 0) return undefined;
316
+ const min = Math.min(...finite);
317
+ const max = Math.max(...finite);
318
+ const pad = max === min ? 1 : (max - min) * SPARKLINE_HEADROOM;
319
+ return [min - pad, max + pad];
320
+ }
321
+
322
+ /**
323
+ * Whether the region under the line is filled.
324
+ *
325
+ * On under the card, where the chart has the full width and is looked at
326
+ * properly; off beside the number, where the chart is a gesture and a fill
327
+ * would make it a second block competing with the value. An explicit choice
328
+ * wins either way.
329
+ */
330
+ export function resolveSparklineFilled({ layout, filled }: { layout: KpiLayout; filled?: boolean }): boolean {
331
+ return filled ?? layout === "below";
332
+ }
333
+
334
+ /**
335
+ * What a screen reader says for a sparkline: how many points, and where it
336
+ * started and ended — the two numbers a glance at the shape is reading.
337
+ */
338
+ export function kpiSparklineAccessibilityLabel(
339
+ values: readonly number[],
340
+ format: (value: number) => string = String
341
+ ): string {
342
+ const finite = values.filter(Number.isFinite);
343
+ const first = finite[0];
344
+ const last = finite[finite.length - 1];
345
+ if (first === undefined || last === undefined) return "No trend data";
346
+ return `Trend over ${values.length} points, from ${format(first)} to ${format(last)}`;
347
+ }
@@ -0,0 +1,89 @@
1
+ # Label
2
+
3
+ The name of a form control, with required, invalid and disabled states. One
4
+ component, no parts.
5
+
6
+ `import { Label } from "@delacour/react-native-ui/label";`
7
+
8
+ ## Files
9
+
10
+ | File | What it holds |
11
+ | --- | --- |
12
+ | `index.ts` | → `@delacour/react-native-ui/label` |
13
+ | `label.tsx` | `Label` |
14
+ | `label.variants.ts` | Pure `tv()` slots + resolvers, no RN imports |
15
+ | `label.variants.test.ts` | |
16
+
17
+ ## Design
18
+
19
+ - **States**: `isRequired`, `isInvalid`, `isDisabled`. All default `false`, all
20
+ independent — a required field that is also wrong shows both.
21
+ - **It *is* `Text.Label`, and passes a colour, never a scale.** The same move
22
+ [`Field`](../field/AGENTS.md)'s text parts make: a `text-sm font-medium` in a
23
+ slot here would be a second definition of [`Text.Label`](../text/AGENTS.md)
24
+ that could drift from it. `resolveLabelColor` returns `destructive` when
25
+ invalid and `undefined` otherwise, and `undefined` means "leave the preset's
26
+ own colour alone". A caller's `color` beats it; `size` and `weight` are
27
+ `Text`'s own axes and work unchanged. A test asserts neither slot carries a
28
+ size, weight or colour.
29
+ - **The required mark is a nested `Text`, not a sibling.** A row of two `Text`s
30
+ would put the asterisk beside the label's **box**: a label long enough to wrap
31
+ would carry it in a column of its own at the right edge of the first line. As a
32
+ nested run it flows with the text and lands after the last word — and a
33
+ no-break space in front of it (` *`) keeps it from wrapping onto a line by
34
+ itself. It inherits the label's size and weight through `Text`'s own nesting,
35
+ so there is nothing to keep in step.
36
+ - **The mark is destructive in every state.** Required is a warning about what
37
+ the form will refuse, so it wears the colour of that refusal whether or not the
38
+ value is wrong yet. `LABEL_REQUIRED_MARK_COLOR` is the constant;
39
+ `requiredMarkClassName` restyles it.
40
+ - **Only the root fades when disabled.** The mark is a run inside the root's own
41
+ text, so the root's `opacity-50` already reaches it. Fading it again would
42
+ square the fade. A disabled label also reports `accessibilityState.disabled`,
43
+ so a screen reader says "dimmed" rather than leaving the fade visual-only.
44
+ - **A required label is announced as "Email, required", not "Email, star".**
45
+ React Native reads a `Text` and its nested runs as one string, so without an
46
+ `accessibilityLabel` VoiceOver speaks the punctuation. `resolveLabelAccessibilityLabel`
47
+ builds one when the children are plain text — `labelText` reads strings,
48
+ numbers and arrays of them, and gives up on anything else rather than guessing
49
+ at an element's words. A caller's own `accessibilityLabel` always wins, and is
50
+ the answer when the children are not text.
51
+ - **`children` and `accessibilityLabel` are narrowed from `Text`'s.** `Text`
52
+ renders `Animated.Text`, whose props also accept Reanimated `SharedValue`s. A
53
+ label has to append its mark to its children and read them as text, and a
54
+ shared value supports neither, so the narrower type is the honest one — the
55
+ reason [`Field.Error`](../field/AGENTS.md) narrows its own children.
56
+ - **It reads no context.** A standalone primitive is whatever it is told, which
57
+ is what makes it usable outside a `Field` — above a `Slider`, beside a
58
+ `Switch`, in a settings row — without a layout it does not need.
59
+ `Field.Label` stays the field-aware counterpart: it reads the enclosing
60
+ field's state, and it is **not** built on `Label`, because doing so would
61
+ change its public surface — `Label` narrows `children`, and a disabled `Label`
62
+ reports `accessibilityState.disabled`, which `Field.Label` does not. The two
63
+ must still read as one component, so `label.variants.test.ts` pins
64
+ `resolveLabelColor` and the disabled fade to `fieldVariants`' label slot and
65
+ `resolveFieldTextColor("label", …)`. Change one and the test names the other.
66
+ - **Inside a `Field`, `Label` is the way to say "required".** `Field.Label` has
67
+ no `isRequired`. Pass the field's state yourself —
68
+ `<Label isInvalid={isInvalid} isRequired>Email</Label>` — or read it in a
69
+ custom part with `useField()`.
70
+ - **There is no `htmlFor`.** React Native has no `<label>` element and no
71
+ label-for-control association. On Android, give the label a `nativeID` and the
72
+ control `accessibilityLabelledBy` with the same string; iOS has no equivalent,
73
+ which is why a control's own `accessibilityLabel` matters more than the label
74
+ beside it.
75
+ - **There is no `Label.Text` part.** The label is already text; a `View` root
76
+ holding a text part would be the row of two boxes the nested mark exists to
77
+ avoid, and would take `ViewProps` where every caller wants `Text`'s `size`,
78
+ `weight` and `numberOfLines`.
79
+ - **No `onPress` wiring.** `Text` already takes `onPress`, so a label that should
80
+ focus its control passes one — the same thing `Field`'s whole-row press does
81
+ for a checkbox. Mounting a gesture detector under every label in a form would
82
+ announce static text as something to activate, which
83
+ [`Badge`](../badge/AGENTS.md) refuses for the same reason. A `Text` with `onPress` is exposed to VoiceOver as a **link**, and
84
+ `accessible={false}` does not change that on iOS — checked on a simulator,
85
+ where it still read as `AXLink`. A caller wiring one should pass
86
+ `accessibilityElementsHidden` and `importantForAccessibility="no-hide-descendants"`,
87
+ and let the control, which carries its own `accessibilityLabel`, be the element
88
+ a screen reader lands on — the move `Field`'s row makes. The playground's
89
+ `/label` "Beside a switch" demo shows it.
@@ -0,0 +1,10 @@
1
+ export { Label, type LabelProps } from "./label";
2
+ export {
3
+ LABEL_REQUIRED_MARK,
4
+ LABEL_REQUIRED_MARK_COLOR,
5
+ type LabelVariantProps,
6
+ labelText,
7
+ labelVariants,
8
+ resolveLabelAccessibilityLabel,
9
+ resolveLabelColor,
10
+ } from "./label.variants";
@@ -0,0 +1,97 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import { Text, type TextProps } from "../text";
3
+ import {
4
+ LABEL_REQUIRED_MARK,
5
+ LABEL_REQUIRED_MARK_COLOR,
6
+ labelVariants,
7
+ resolveLabelAccessibilityLabel,
8
+ resolveLabelColor,
9
+ } from "./label.variants";
10
+
11
+ /**
12
+ * `Text`'s props minus the role it already is, with two narrowed.
13
+ *
14
+ * `Text` renders `Animated.Text`, so its `children` and `accessibilityLabel` may
15
+ * also be Reanimated `SharedValue`s. A label has to append its required mark to
16
+ * its children and read them as text for a screen reader, and neither can be
17
+ * done to a shared value, so the narrower type is the honest one — the move
18
+ * `Field.Error` makes for the same reason.
19
+ */
20
+ export type LabelProps = Omit<TextProps, "variant" | "children" | "accessibilityLabel"> & {
21
+ children?: ReactNode;
22
+ /** What a screen reader announces. Defaults to "Email, required" for a required label of plain text. */
23
+ accessibilityLabel?: string;
24
+ /** Appends a destructive asterisk, and announces the label as required. */
25
+ isRequired?: boolean;
26
+ /** Turns the label destructive, to match a control whose value is wrong. */
27
+ isInvalid?: boolean;
28
+ /** Fades the label, to match a control that is unavailable. */
29
+ isDisabled?: boolean;
30
+ /** Merged into the required mark's class. */
31
+ requiredMarkClassName?: string;
32
+ };
33
+
34
+ /**
35
+ * The name of a form control.
36
+ *
37
+ * Renders `Text.Label` and passes it a colour, never a size or a weight, so the
38
+ * type scale stays the preset's. `isInvalid` turns it destructive and
39
+ * `isDisabled` fades it, to read as one state with the control beside it.
40
+ *
41
+ * `isRequired` appends an asterisk as a **nested** `Text` rather than a sibling,
42
+ * with a no-break space in front of it, so a label long enough to wrap carries
43
+ * the mark on its last line, glued to its last word, instead of in a column of
44
+ * its own. A nested run is read as part of the label's text, so a required label
45
+ * whose children are text is announced as "Email, required" rather than
46
+ * "Email, star". Pass `accessibilityLabel` when the children are not text.
47
+ *
48
+ * A standalone primitive: it reads no context, so its state is whatever it is
49
+ * told. `Field.Label` is the field-aware counterpart — it reads a `Field`'s
50
+ * state for itself, and colours and fades exactly as this does, which a test
51
+ * holds. Reach for this one outside a `Field`, or inside one when the label has
52
+ * to say "required", which `Field.Label` does not.
53
+ *
54
+ * There is no `htmlFor`: React Native has no label-for-control association. On
55
+ * Android, give the label a `nativeID` and the control
56
+ * `accessibilityLabelledBy` with the same string.
57
+ *
58
+ * @example
59
+ * <Label isRequired>Email</Label>
60
+ *
61
+ * @example
62
+ * <Label isInvalid={!isValid} isRequired>
63
+ * Email
64
+ * </Label>
65
+ */
66
+ export function Label({
67
+ accessibilityLabel,
68
+ accessibilityState,
69
+ children,
70
+ className,
71
+ color,
72
+ isDisabled = false,
73
+ isInvalid = false,
74
+ isRequired = false,
75
+ requiredMarkClassName,
76
+ ...props
77
+ }: LabelProps): ReactElement {
78
+ const slots = labelVariants({ isDisabled, isInvalid });
79
+
80
+ return (
81
+ <Text.Label
82
+ accessibilityLabel={resolveLabelAccessibilityLabel({ accessibilityLabel, children, isRequired })}
83
+ accessibilityState={isDisabled ? { ...accessibilityState, disabled: true } : accessibilityState}
84
+ className={slots.root({ className })}
85
+ color={color ?? resolveLabelColor(isInvalid)}
86
+ {...props}
87
+ >
88
+ {children}
89
+ {isRequired ? (
90
+ <Text className={slots.requiredMark({ className: requiredMarkClassName })} color={LABEL_REQUIRED_MARK_COLOR}>
91
+ {` ${LABEL_REQUIRED_MARK}`}
92
+ </Text>
93
+ ) : null}
94
+ </Text.Label>
95
+ );
96
+ }
97
+ Label.displayName = "DelacourUI.Label";