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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (493) hide show
  1. package/README.md +10 -6
  2. package/package.json +49 -5
  3. package/src/components/accordion/AGENTS.md +21 -9
  4. package/src/components/accordion/accordion-content.tsx +7 -4
  5. package/src/components/accordion/accordion-item.tsx +15 -13
  6. package/src/components/accordion/accordion.context.tsx +11 -5
  7. package/src/components/accordion/accordion.variants.test.ts +64 -3
  8. package/src/components/accordion/accordion.variants.ts +33 -9
  9. package/src/components/alert/AGENTS.md +102 -0
  10. package/src/components/alert/alert-action.tsx +17 -0
  11. package/src/components/alert/alert-close-button.tsx +50 -0
  12. package/src/components/alert/alert-content.tsx +15 -0
  13. package/src/components/alert/alert-description.tsx +16 -0
  14. package/src/components/alert/alert-glyphs.ts +25 -0
  15. package/src/components/alert/alert-indicator.tsx +40 -0
  16. package/src/components/alert/alert-title.tsx +18 -0
  17. package/src/components/alert/alert.context.tsx +69 -0
  18. package/src/components/alert/alert.tsx +160 -0
  19. package/src/components/alert/alert.types.ts +10 -0
  20. package/src/components/alert/alert.variants.test.ts +218 -0
  21. package/src/components/alert/alert.variants.ts +180 -0
  22. package/src/components/alert/index.ts +23 -0
  23. package/src/components/avatar/AGENTS.md +106 -0
  24. package/src/components/avatar/avatar-badge.tsx +51 -0
  25. package/src/components/avatar/avatar-group.tsx +139 -0
  26. package/src/components/avatar/avatar.context.tsx +93 -0
  27. package/src/components/avatar/avatar.tsx +201 -0
  28. package/src/components/avatar/avatar.utils.test.ts +174 -0
  29. package/src/components/avatar/avatar.utils.ts +141 -0
  30. package/src/components/avatar/avatar.variants.test.ts +251 -0
  31. package/src/components/avatar/avatar.variants.ts +270 -0
  32. package/src/components/avatar/index.ts +39 -0
  33. package/src/components/badge/AGENTS.md +10 -0
  34. package/src/components/badge/badge-close-button.tsx +2 -2
  35. package/src/components/badge/badge.context.tsx +2 -1
  36. package/src/components/badge/badge.tsx +13 -4
  37. package/src/components/badge/badge.variants.test.ts +26 -0
  38. package/src/components/badge/badge.variants.ts +21 -0
  39. package/src/components/bottom-sheet/AGENTS.md +186 -231
  40. package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
  41. package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
  42. package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -208
  43. package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
  44. package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
  45. package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
  46. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -122
  47. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
  48. package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
  49. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
  50. package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
  51. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -62
  52. package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
  53. package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
  54. package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
  55. package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
  56. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
  57. package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
  58. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -87
  59. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
  60. package/src/components/bottom-sheet/index.ts +39 -17
  61. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
  62. package/src/components/button/AGENTS.md +51 -11
  63. package/src/components/button/button-group.tsx +44 -8
  64. package/src/components/button/button.context.tsx +10 -0
  65. package/src/components/button/button.tsx +7 -1
  66. package/src/components/button/button.variants.test.ts +190 -0
  67. package/src/components/button/button.variants.ts +87 -6
  68. package/src/components/button/index.ts +2 -0
  69. package/src/components/calendar/AGENTS.md +142 -0
  70. package/src/components/calendar/calendar-caption.tsx +116 -0
  71. package/src/components/calendar/calendar-day.tsx +103 -0
  72. package/src/components/calendar/calendar-grid.tsx +183 -0
  73. package/src/components/calendar/calendar-header.tsx +34 -0
  74. package/src/components/calendar/calendar-nav.tsx +51 -0
  75. package/src/components/calendar/calendar-picker.tsx +128 -0
  76. package/src/components/calendar/calendar-weekdays.tsx +48 -0
  77. package/src/components/calendar/calendar.context.tsx +108 -0
  78. package/src/components/calendar/calendar.date.test.ts +456 -0
  79. package/src/components/calendar/calendar.date.ts +584 -0
  80. package/src/components/calendar/calendar.tsx +389 -0
  81. package/src/components/calendar/calendar.types.ts +87 -0
  82. package/src/components/calendar/calendar.variants.test.ts +363 -0
  83. package/src/components/calendar/calendar.variants.ts +426 -0
  84. package/src/components/calendar/index.ts +96 -0
  85. package/src/components/card/AGENTS.md +92 -0
  86. package/src/components/card/card-action.tsx +16 -0
  87. package/src/components/card/card-content.tsx +12 -0
  88. package/src/components/card/card-description.tsx +12 -0
  89. package/src/components/card/card-footer.tsx +30 -0
  90. package/src/components/card/card-header.tsx +45 -0
  91. package/src/components/card/card-title.tsx +25 -0
  92. package/src/components/card/card.context.tsx +64 -0
  93. package/src/components/card/card.tsx +100 -0
  94. package/src/components/card/card.types.ts +13 -0
  95. package/src/components/card/card.variants.test.ts +240 -0
  96. package/src/components/card/card.variants.ts +177 -0
  97. package/src/components/card/index.ts +15 -0
  98. package/src/components/chart/AGENTS.md +8 -0
  99. package/src/components/chart/chart.tsx +8 -1
  100. package/src/components/chip/AGENTS.md +113 -0
  101. package/src/components/chip/chip-close-button.tsx +48 -0
  102. package/src/components/chip/chip-end-content.tsx +15 -0
  103. package/src/components/chip/chip-label.tsx +21 -0
  104. package/src/components/chip/chip-start-content.tsx +16 -0
  105. package/src/components/chip/chip.context.tsx +66 -0
  106. package/src/components/chip/chip.tsx +268 -0
  107. package/src/components/chip/chip.types.ts +10 -0
  108. package/src/components/chip/chip.variants.test.ts +335 -0
  109. package/src/components/chip/chip.variants.ts +321 -0
  110. package/src/components/chip/index.ts +24 -0
  111. package/src/components/collapsible/AGENTS.md +97 -0
  112. package/src/components/collapsible/collapsible-content.tsx +97 -0
  113. package/src/components/collapsible/collapsible-description.tsx +17 -0
  114. package/src/components/collapsible/collapsible-indicator.tsx +92 -0
  115. package/src/components/collapsible/collapsible-title.tsx +17 -0
  116. package/src/components/collapsible/collapsible-trigger.tsx +133 -0
  117. package/src/components/collapsible/collapsible.context.tsx +91 -0
  118. package/src/components/collapsible/collapsible.tsx +167 -0
  119. package/src/components/collapsible/collapsible.types.ts +9 -0
  120. package/src/components/collapsible/collapsible.variants.test.ts +338 -0
  121. package/src/components/collapsible/collapsible.variants.ts +228 -0
  122. package/src/components/collapsible/index.ts +32 -0
  123. package/src/components/context-menu/AGENTS.md +103 -0
  124. package/src/components/context-menu/context-menu-content.tsx +64 -0
  125. package/src/components/context-menu/context-menu-item.tsx +22 -0
  126. package/src/components/context-menu/context-menu-preview.tsx +60 -0
  127. package/src/components/context-menu/context-menu-trigger.tsx +160 -0
  128. package/src/components/context-menu/context-menu.context.tsx +91 -0
  129. package/src/components/context-menu/context-menu.tsx +90 -0
  130. package/src/components/context-menu/context-menu.variants.test.ts +266 -0
  131. package/src/components/context-menu/context-menu.variants.ts +147 -0
  132. package/src/components/context-menu/index.ts +18 -0
  133. package/src/components/dialog/AGENTS.md +97 -0
  134. package/src/components/dialog/dialog-body.tsx +21 -0
  135. package/src/components/dialog/dialog-close.tsx +82 -0
  136. package/src/components/dialog/dialog-content.tsx +153 -0
  137. package/src/components/dialog/dialog-description.tsx +23 -0
  138. package/src/components/dialog/dialog-footer.tsx +32 -0
  139. package/src/components/dialog/dialog-header.tsx +20 -0
  140. package/src/components/dialog/dialog-title.tsx +42 -0
  141. package/src/components/dialog/dialog-trigger.tsx +54 -0
  142. package/src/components/dialog/dialog.context.tsx +90 -0
  143. package/src/components/dialog/dialog.tsx +119 -0
  144. package/src/components/dialog/dialog.variants.test.ts +224 -0
  145. package/src/components/dialog/dialog.variants.ts +173 -0
  146. package/src/components/dialog/index.ts +30 -0
  147. package/src/components/drawer/AGENTS.md +123 -0
  148. package/src/components/drawer/drawer-body.tsx +46 -0
  149. package/src/components/drawer/drawer-close.tsx +76 -0
  150. package/src/components/drawer/drawer-content.tsx +186 -0
  151. package/src/components/drawer/drawer-description.tsx +23 -0
  152. package/src/components/drawer/drawer-footer.tsx +21 -0
  153. package/src/components/drawer/drawer-header.tsx +42 -0
  154. package/src/components/drawer/drawer-title.tsx +35 -0
  155. package/src/components/drawer/drawer-trigger.tsx +54 -0
  156. package/src/components/drawer/drawer.context.tsx +114 -0
  157. package/src/components/drawer/drawer.tsx +119 -0
  158. package/src/components/drawer/drawer.variants.test.ts +392 -0
  159. package/src/components/drawer/drawer.variants.ts +244 -0
  160. package/src/components/drawer/index.ts +41 -0
  161. package/src/components/drawer/use-drawer-pan.ts +108 -0
  162. package/src/components/empty-state/AGENTS.md +90 -0
  163. package/src/components/empty-state/empty-state-content.tsx +17 -0
  164. package/src/components/empty-state/empty-state-description.tsx +12 -0
  165. package/src/components/empty-state/empty-state-header.tsx +17 -0
  166. package/src/components/empty-state/empty-state-media.tsx +56 -0
  167. package/src/components/empty-state/empty-state-title.tsx +18 -0
  168. package/src/components/empty-state/empty-state.context.tsx +65 -0
  169. package/src/components/empty-state/empty-state.tsx +84 -0
  170. package/src/components/empty-state/empty-state.types.ts +14 -0
  171. package/src/components/empty-state/empty-state.variants.test.ts +245 -0
  172. package/src/components/empty-state/empty-state.variants.ts +132 -0
  173. package/src/components/empty-state/index.ts +20 -0
  174. package/src/components/fab/AGENTS.md +119 -0
  175. package/src/components/fab/fab-action.tsx +118 -0
  176. package/src/components/fab/fab-group.tsx +214 -0
  177. package/src/components/fab/fab-label.tsx +18 -0
  178. package/src/components/fab/fab.context.tsx +125 -0
  179. package/src/components/fab/fab.tsx +156 -0
  180. package/src/components/fab/fab.types.ts +17 -0
  181. package/src/components/fab/fab.variants.test.ts +274 -0
  182. package/src/components/fab/fab.variants.ts +234 -0
  183. package/src/components/fab/index.ts +37 -0
  184. package/src/components/feedback/AGENTS.md +95 -0
  185. package/src/components/feedback/feedback-action.tsx +16 -0
  186. package/src/components/feedback/feedback-cancel.tsx +34 -0
  187. package/src/components/feedback/feedback-close.tsx +21 -0
  188. package/src/components/feedback/feedback-content.tsx +24 -0
  189. package/src/components/feedback/feedback-field.tsx +70 -0
  190. package/src/components/feedback/feedback-footer.tsx +21 -0
  191. package/src/components/feedback/feedback-panel.tsx +54 -0
  192. package/src/components/feedback/feedback-submit.tsx +74 -0
  193. package/src/components/feedback/feedback-title.tsx +34 -0
  194. package/src/components/feedback/feedback-trigger.tsx +20 -0
  195. package/src/components/feedback/feedback.context.tsx +73 -0
  196. package/src/components/feedback/feedback.tsx +135 -0
  197. package/src/components/feedback/feedback.variants.test.ts +185 -0
  198. package/src/components/feedback/feedback.variants.ts +128 -0
  199. package/src/components/feedback/index.ts +24 -0
  200. package/src/components/field/AGENTS.md +19 -0
  201. package/src/components/field/field-label.tsx +19 -5
  202. package/src/components/field/field.context.tsx +17 -0
  203. package/src/components/field/field.tsx +15 -2
  204. package/src/components/field/field.variants.test.ts +24 -0
  205. package/src/components/field/field.variants.ts +20 -0
  206. package/src/components/field/index.ts +1 -0
  207. package/src/components/input/AGENTS.md +5 -0
  208. package/src/components/input/input.variants.test.ts +32 -0
  209. package/src/components/input/input.variants.ts +13 -1
  210. package/src/components/item/AGENTS.md +92 -0
  211. package/src/components/item/index.ts +25 -0
  212. package/src/components/item/item-actions.tsx +28 -0
  213. package/src/components/item/item-content.tsx +15 -0
  214. package/src/components/item/item-description.tsx +12 -0
  215. package/src/components/item/item-footer.tsx +13 -0
  216. package/src/components/item/item-group.tsx +32 -0
  217. package/src/components/item/item-header.tsx +13 -0
  218. package/src/components/item/item-media.tsx +37 -0
  219. package/src/components/item/item-separator.tsx +13 -0
  220. package/src/components/item/item-title.tsx +17 -0
  221. package/src/components/item/item.context.tsx +66 -0
  222. package/src/components/item/item.tsx +218 -0
  223. package/src/components/item/item.types.ts +12 -0
  224. package/src/components/item/item.variants.test.ts +263 -0
  225. package/src/components/item/item.variants.ts +200 -0
  226. package/src/components/kpi/AGENTS.md +125 -0
  227. package/src/components/kpi/index.ts +45 -0
  228. package/src/components/kpi/kpi-action.tsx +12 -0
  229. package/src/components/kpi/kpi-content.tsx +35 -0
  230. package/src/components/kpi/kpi-footer.tsx +52 -0
  231. package/src/components/kpi/kpi-group.tsx +70 -0
  232. package/src/components/kpi/kpi-header.tsx +25 -0
  233. package/src/components/kpi/kpi-icon.tsx +28 -0
  234. package/src/components/kpi/kpi-sparkline.tsx +145 -0
  235. package/src/components/kpi/kpi-stat.tsx +20 -0
  236. package/src/components/kpi/kpi-title.tsx +19 -0
  237. package/src/components/kpi/kpi-trend.tsx +105 -0
  238. package/src/components/kpi/kpi-value.tsx +32 -0
  239. package/src/components/kpi/kpi.context.tsx +111 -0
  240. package/src/components/kpi/kpi.tsx +154 -0
  241. package/src/components/kpi/kpi.types.ts +9 -0
  242. package/src/components/kpi/kpi.variants.test.ts +428 -0
  243. package/src/components/kpi/kpi.variants.ts +347 -0
  244. package/src/components/label/AGENTS.md +89 -0
  245. package/src/components/label/index.ts +10 -0
  246. package/src/components/label/label.tsx +97 -0
  247. package/src/components/label/label.variants.test.ts +176 -0
  248. package/src/components/label/label.variants.ts +123 -0
  249. package/src/components/menu/AGENTS.md +176 -0
  250. package/src/components/menu/index.ts +44 -0
  251. package/src/components/menu/menu-background.tsx +17 -0
  252. package/src/components/menu/menu-checkbox-item.tsx +57 -0
  253. package/src/components/menu/menu-content.tsx +303 -0
  254. package/src/components/menu/menu-item.tsx +162 -0
  255. package/src/components/menu/menu-label.tsx +14 -0
  256. package/src/components/menu/menu-radio-group.tsx +50 -0
  257. package/src/components/menu/menu-radio-item.tsx +57 -0
  258. package/src/components/menu/menu-separator.tsx +11 -0
  259. package/src/components/menu/menu-sub-content.tsx +72 -0
  260. package/src/components/menu/menu-sub-trigger.tsx +50 -0
  261. package/src/components/menu/menu-sub.tsx +75 -0
  262. package/src/components/menu/menu-trigger.tsx +57 -0
  263. package/src/components/menu/menu.context.tsx +214 -0
  264. package/src/components/menu/menu.tsx +88 -0
  265. package/src/components/menu/menu.types.ts +33 -0
  266. package/src/components/menu/menu.variants.test.ts +400 -0
  267. package/src/components/menu/menu.variants.ts +353 -0
  268. package/src/components/meter/AGENTS.md +135 -0
  269. package/src/components/meter/index.ts +43 -0
  270. package/src/components/meter/meter-fill.tsx +19 -0
  271. package/src/components/meter/meter-header.tsx +17 -0
  272. package/src/components/meter/meter-label.tsx +18 -0
  273. package/src/components/meter/meter-output.tsx +42 -0
  274. package/src/components/meter/meter-segments.tsx +83 -0
  275. package/src/components/meter/meter-track.tsx +16 -0
  276. package/src/components/meter/meter.context.tsx +62 -0
  277. package/src/components/meter/meter.tsx +286 -0
  278. package/src/components/meter/meter.types.ts +30 -0
  279. package/src/components/meter/meter.variants.test.ts +430 -0
  280. package/src/components/meter/meter.variants.ts +347 -0
  281. package/src/components/overlay/AGENTS.md +89 -0
  282. package/src/components/overlay/index.ts +27 -0
  283. package/src/components/overlay/overlay-portal.tsx +84 -0
  284. package/src/components/overlay/overlay-presence.test.ts +46 -0
  285. package/src/components/overlay/overlay-presence.ts +49 -0
  286. package/src/components/overlay/overlay-registry.test.ts +96 -0
  287. package/src/components/overlay/overlay-registry.ts +99 -0
  288. package/src/components/overlay/overlay-scrim.tsx +49 -0
  289. package/src/components/overlay/overlay.context.tsx +48 -0
  290. package/src/components/overlay/overlay.tsx +118 -0
  291. package/src/components/overlay/overlay.variants.test.ts +37 -0
  292. package/src/components/overlay/overlay.variants.ts +33 -0
  293. package/src/components/overlay/use-overlay-back-handler.ts +34 -0
  294. package/src/components/overlay/use-overlay-presence.ts +87 -0
  295. package/src/components/popover/AGENTS.md +138 -0
  296. package/src/components/popover/index.ts +48 -0
  297. package/src/components/popover/popover-anchor.tsx +49 -0
  298. package/src/components/popover/popover-arrow.tsx +89 -0
  299. package/src/components/popover/popover-close.tsx +73 -0
  300. package/src/components/popover/popover-content.tsx +235 -0
  301. package/src/components/popover/popover-description.tsx +18 -0
  302. package/src/components/popover/popover-title.tsx +33 -0
  303. package/src/components/popover/popover-trigger.tsx +56 -0
  304. package/src/components/popover/popover.context.tsx +71 -0
  305. package/src/components/popover/popover.position.test.ts +354 -0
  306. package/src/components/popover/popover.position.ts +286 -0
  307. package/src/components/popover/popover.tsx +123 -0
  308. package/src/components/popover/popover.variants.test.ts +135 -0
  309. package/src/components/popover/popover.variants.ts +85 -0
  310. package/src/components/popover/use-anchor-measure.ts +76 -0
  311. package/src/components/popover/use-anchored-content.ts +219 -0
  312. package/src/components/pressable/AGENTS.md +11 -1
  313. package/src/components/pressable/pressable.tsx +7 -2
  314. package/src/components/pressable/pressable.variants.test.ts +29 -0
  315. package/src/components/pressable/pressable.variants.ts +17 -0
  316. package/src/components/progress/AGENTS.md +120 -0
  317. package/src/components/progress/index.ts +40 -0
  318. package/src/components/progress/progress-fill.tsx +152 -0
  319. package/src/components/progress/progress-header.tsx +19 -0
  320. package/src/components/progress/progress-label.tsx +27 -0
  321. package/src/components/progress/progress-output.tsx +47 -0
  322. package/src/components/progress/progress-track.tsx +38 -0
  323. package/src/components/progress/progress.context.tsx +79 -0
  324. package/src/components/progress/progress.tsx +171 -0
  325. package/src/components/progress/progress.types.ts +23 -0
  326. package/src/components/progress/progress.variants.test.ts +335 -0
  327. package/src/components/progress/progress.variants.ts +284 -0
  328. package/src/components/progress-button/AGENTS.md +116 -0
  329. package/src/components/progress-button/index.ts +36 -0
  330. package/src/components/progress-button/progress-button-done.tsx +33 -0
  331. package/src/components/progress-button/progress-button-label.tsx +27 -0
  332. package/src/components/progress-button/progress-button.context.tsx +93 -0
  333. package/src/components/progress-button/progress-button.tsx +414 -0
  334. package/src/components/progress-button/progress-button.variants.test.ts +314 -0
  335. package/src/components/progress-button/progress-button.variants.ts +236 -0
  336. package/src/components/provider/AGENTS.md +38 -25
  337. package/src/components/provider/provider.tsx +38 -11
  338. package/src/components/radio/AGENTS.md +8 -10
  339. package/src/components/radio/radio.variants.test.ts +5 -6
  340. package/src/components/radio/radio.variants.ts +6 -7
  341. package/src/components/rating/AGENTS.md +143 -0
  342. package/src/components/rating/index.ts +43 -0
  343. package/src/components/rating/rating-output.tsx +39 -0
  344. package/src/components/rating/rating-star.tsx +70 -0
  345. package/src/components/rating/rating-stars.tsx +219 -0
  346. package/src/components/rating/rating.context.tsx +79 -0
  347. package/src/components/rating/rating.tsx +163 -0
  348. package/src/components/rating/rating.variants.test.ts +347 -0
  349. package/src/components/rating/rating.variants.ts +354 -0
  350. package/src/components/selection-mode/AGENTS.md +155 -0
  351. package/src/components/selection-mode/index.ts +43 -0
  352. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  353. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  354. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  355. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  356. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  357. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  358. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  359. package/src/components/selection-mode/selection-mode.tsx +207 -0
  360. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  361. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  362. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  363. package/src/components/skeleton/AGENTS.md +120 -0
  364. package/src/components/skeleton/index.ts +15 -0
  365. package/src/components/skeleton/skeleton-clock.ts +48 -0
  366. package/src/components/skeleton/skeleton-group.tsx +64 -0
  367. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  368. package/src/components/skeleton/skeleton.context.tsx +45 -0
  369. package/src/components/skeleton/skeleton.tsx +227 -0
  370. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  371. package/src/components/skeleton/skeleton.variants.ts +222 -0
  372. package/src/components/slide-button/AGENTS.md +122 -0
  373. package/src/components/slide-button/index.ts +43 -0
  374. package/src/components/slide-button/slide-button-label.tsx +70 -0
  375. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  376. package/src/components/slide-button/slide-button.context.tsx +76 -0
  377. package/src/components/slide-button/slide-button.tsx +457 -0
  378. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  379. package/src/components/slide-button/slide-button.variants.ts +324 -0
  380. package/src/components/slider/index.ts +2 -0
  381. package/src/components/slider/slider-thumb.tsx +33 -1
  382. package/src/components/slider/slider.variants.test.ts +52 -0
  383. package/src/components/slider/slider.variants.ts +56 -2
  384. package/src/components/stack-card/AGENTS.md +169 -0
  385. package/src/components/stack-card/index.ts +37 -0
  386. package/src/components/stack-card/stack-card-action.tsx +66 -0
  387. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  388. package/src/components/stack-card/stack-card-card.tsx +71 -0
  389. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  390. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  391. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  392. package/src/components/stack-card/stack-card.context.tsx +117 -0
  393. package/src/components/stack-card/stack-card.tsx +557 -0
  394. package/src/components/stack-card/stack-card.types.ts +26 -0
  395. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  396. package/src/components/stack-card/stack-card.variants.ts +360 -0
  397. package/src/components/steps/AGENTS.md +153 -0
  398. package/src/components/steps/index.ts +45 -0
  399. package/src/components/steps/steps-connector.tsx +31 -0
  400. package/src/components/steps/steps-description.tsx +26 -0
  401. package/src/components/steps/steps-indicator.tsx +106 -0
  402. package/src/components/steps/steps-item.tsx +225 -0
  403. package/src/components/steps/steps-panel.tsx +41 -0
  404. package/src/components/steps/steps-title.tsx +32 -0
  405. package/src/components/steps/steps-track.tsx +59 -0
  406. package/src/components/steps/steps.context.tsx +123 -0
  407. package/src/components/steps/steps.tsx +166 -0
  408. package/src/components/steps/steps.variants.test.ts +388 -0
  409. package/src/components/steps/steps.variants.ts +468 -0
  410. package/src/components/surface/AGENTS.md +82 -0
  411. package/src/components/surface/index.ts +15 -0
  412. package/src/components/surface/surface.context.tsx +56 -0
  413. package/src/components/surface/surface.tsx +87 -0
  414. package/src/components/surface/surface.variants.test.ts +227 -0
  415. package/src/components/surface/surface.variants.ts +165 -0
  416. package/src/components/swipe/AGENTS.md +140 -0
  417. package/src/components/swipe/index.ts +31 -0
  418. package/src/components/swipe/swipe-action.tsx +99 -0
  419. package/src/components/swipe/swipe-end.tsx +14 -0
  420. package/src/components/swipe/swipe-group.tsx +55 -0
  421. package/src/components/swipe/swipe-panel.tsx +55 -0
  422. package/src/components/swipe/swipe-start.tsx +15 -0
  423. package/src/components/swipe/swipe.context.tsx +135 -0
  424. package/src/components/swipe/swipe.tsx +393 -0
  425. package/src/components/swipe/swipe.types.ts +7 -0
  426. package/src/components/swipe/swipe.variants.test.ts +337 -0
  427. package/src/components/swipe/swipe.variants.ts +290 -0
  428. package/src/components/switch/switch.variants.ts +2 -2
  429. package/src/components/tabs/AGENTS.md +4 -3
  430. package/src/components/tabs/tabs.variants.test.ts +1 -1
  431. package/src/components/tabs/tabs.variants.ts +4 -6
  432. package/src/components/text/AGENTS.md +8 -2
  433. package/src/components/text/text.tsx +7 -0
  434. package/src/components/text/text.variants.test.ts +24 -1
  435. package/src/components/text/text.variants.ts +5 -0
  436. package/src/components/textarea/AGENTS.md +89 -0
  437. package/src/components/textarea/index.ts +16 -0
  438. package/src/components/textarea/textarea.tsx +157 -0
  439. package/src/components/textarea/textarea.variants.test.ts +219 -0
  440. package/src/components/textarea/textarea.variants.ts +171 -0
  441. package/src/components/toast/AGENTS.md +140 -0
  442. package/src/components/toast/index.ts +74 -0
  443. package/src/components/toast/toast-action.tsx +59 -0
  444. package/src/components/toast/toast-api.ts +32 -0
  445. package/src/components/toast/toast-close.tsx +44 -0
  446. package/src/components/toast/toast-content.tsx +11 -0
  447. package/src/components/toast/toast-description.tsx +11 -0
  448. package/src/components/toast/toast-indicator.tsx +34 -0
  449. package/src/components/toast/toast-item.tsx +308 -0
  450. package/src/components/toast/toast-title.tsx +13 -0
  451. package/src/components/toast/toast-viewport.tsx +135 -0
  452. package/src/components/toast/toast.context.tsx +69 -0
  453. package/src/components/toast/toast.store.test.ts +259 -0
  454. package/src/components/toast/toast.store.ts +338 -0
  455. package/src/components/toast/toast.timer.test.ts +64 -0
  456. package/src/components/toast/toast.timer.ts +51 -0
  457. package/src/components/toast/toast.tsx +132 -0
  458. package/src/components/toast/toast.variants.test.ts +393 -0
  459. package/src/components/toast/toast.variants.ts +304 -0
  460. package/src/components/toggle-button/AGENTS.md +106 -0
  461. package/src/components/toggle-button/index.ts +30 -0
  462. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  463. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  464. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  465. package/src/components/toggle-button/toggle-button.tsx +172 -0
  466. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  467. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  468. package/src/components/tooltip/AGENTS.md +133 -0
  469. package/src/components/tooltip/index.ts +28 -0
  470. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  471. package/src/components/tooltip/tooltip-content.tsx +179 -0
  472. package/src/components/tooltip/tooltip-description.tsx +19 -0
  473. package/src/components/tooltip/tooltip-text.tsx +21 -0
  474. package/src/components/tooltip/tooltip-title.tsx +20 -0
  475. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  476. package/src/components/tooltip/tooltip.context.tsx +68 -0
  477. package/src/components/tooltip/tooltip.tsx +213 -0
  478. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  479. package/src/components/tooltip/tooltip.variants.ts +177 -0
  480. package/src/hooks/use-calm-motion.tsx +31 -0
  481. package/src/lib/calm-motion.test.ts +17 -0
  482. package/src/lib/calm-motion.ts +23 -0
  483. package/src/lib/cn.parity.test.ts +62 -0
  484. package/src/lib/cn.test.ts +14 -0
  485. package/src/lib/cn.ts +11 -6
  486. package/src/no-gorhom.test.ts +54 -0
  487. package/src/styles/geometry.test.ts +1 -0
  488. package/src/styles/theme.css +4 -0
  489. package/src/styles/tokens.css +44 -0
  490. package/src/styles/tokens.test.ts +56 -0
  491. package/src/styles/tokens.ts +29 -1
  492. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  493. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -1,9 +1,9 @@
1
1
  # Text
2
2
 
3
3
  The library's type scale, and the one component that reproduces React Native's
4
- own text inheritance through classNames. Compound root plus twelve presets:
4
+ own text inheritance through classNames. Compound root plus thirteen presets:
5
5
  `Display`, `Title`, `Header`, `Subheader`, `Paragraph`, `Label`, `Caption`,
6
- `Overline`, and the four inline ones — `Strong`, `Emphasis`, `Link`, `Code`.
6
+ `Overline`, `Kicker`, and the four inline ones — `Strong`, `Emphasis`, `Link`, `Code`.
7
7
 
8
8
  `import { Text } from "@delacour/react-native-ui/text";`
9
9
 
@@ -61,6 +61,12 @@ own text inheritance through classNames. Compound root plus twelve presets:
61
61
  so Tailwind v4's logical-property utilities resolve to a value RN rejects.
62
62
  `transform: "none"` emits `normal-case` rather than an empty string, so it can
63
63
  actually clear an `Overline`'s `uppercase`.
64
+ - **`Kicker` is the mono, 10-point, 0.2em-tracked label — an opt-in sibling of
65
+ `Overline`, not a replacement.** Its size and tracking are the `--text-kicker`
66
+ and `--tracking-kicker` tokens in `tokens.css`, registered with
67
+ `tailwind-merge` as a pair (`KICKER_TOKEN`) so `text-kicker` is read as a
68
+ size and never as a colour. `font-mono` is deliberate: it labels, it does not
69
+ read as a sentence. Nothing in the default look uses it.
64
70
  - **A nested `Text.Code` cannot be padded.** A nested `<Text>` is laid out by the
65
71
  platform's text engine — an `NSAttributedString` run on iOS, a `Span` on
66
72
  Android — and both ignore padding, margin and border radius on an inner
@@ -142,6 +142,12 @@ function TextOverline(props: TextPresetProps): ReactElement {
142
142
  }
143
143
  TextOverline.displayName = "DelacourUI.Text.Overline";
144
144
 
145
+ /** The tiny tracked mono label over a group, a screen or a field. */
146
+ function TextKicker(props: TextPresetProps): ReactElement {
147
+ return <TextRoot variant="kicker" {...props} />;
148
+ }
149
+ TextKicker.displayName = "DelacourUI.Text.Kicker";
150
+
145
151
  /**
146
152
  * Inline emphasis by weight.
147
153
  *
@@ -238,6 +244,7 @@ export const Text = Object.assign(TextRoot, {
238
244
  /** Supporting copy — 14pt on the muted token. */
239
245
  Caption: TextCaption,
240
246
  /** An eyebrow above a section — 12pt semibold, uppercase, muted. */
247
+ Kicker: TextKicker,
241
248
  Overline: TextOverline,
242
249
  /** Inline emphasis by weight. Inherits size and colour from the text around it. */
243
250
  Strong: TextStrong,
@@ -17,7 +17,7 @@ import {
17
17
  } from "./text.variants";
18
18
 
19
19
  /** Tailwind's type scale, smallest first — position, so a test can say "a step down". */
20
- const TYPE_SCALE = ["text-xs", "text-sm", "text-base", "text-lg", "text-xl", "text-2xl", "text-3xl"];
20
+ const TYPE_SCALE = ["text-kicker", "text-xs", "text-sm", "text-base", "text-lg", "text-xl", "text-2xl", "text-3xl"];
21
21
 
22
22
  /** The three alignment utilities, which share the `text-` prefix with sizes and colours. */
23
23
  const ALIGNMENTS = ["text-left", "text-center", "text-right"];
@@ -420,3 +420,26 @@ describe("TEXT_MAX_FONT_SIZE_MULTIPLIER", () => {
420
420
  expect(TEXT_MAX_FONT_SIZE_MULTIPLIER).toBeGreaterThan(1);
421
421
  });
422
422
  });
423
+
424
+ describe("the kicker variant", () => {
425
+ const cls = textVariants({ variant: "kicker" });
426
+
427
+ test("is a ten-point uppercase mono label, tracked wide", () => {
428
+ expect(cls).toContain("font-mono");
429
+ expect(cls).toContain("text-kicker");
430
+ expect(cls).toContain("uppercase");
431
+ expect(cls).toContain("tracking-kicker");
432
+ expect(cls).toContain("text-muted-foreground");
433
+ });
434
+
435
+ test("is listed, and is not an inline variant", () => {
436
+ expect(TEXT_VARIANTS).toContain("kicker");
437
+ expect(isInlineTextVariant("kicker")).toBe(false);
438
+ });
439
+
440
+ test("survives resolveTextClass without losing its size to the base class", () => {
441
+ const resolved = resolveTextClass({ variant: "kicker" });
442
+ expect(resolved).toContain("text-kicker");
443
+ expect(resolved).not.toContain("text-base");
444
+ });
445
+ });
@@ -18,6 +18,7 @@ export const TEXT_VARIANTS = [
18
18
  "label",
19
19
  "caption",
20
20
  "overline",
21
+ "kicker",
21
22
  "strong",
22
23
  "emphasis",
23
24
  "link",
@@ -136,6 +137,10 @@ export const textVariants = tv({
136
137
  label: "font-medium text-sm text-foreground",
137
138
  caption: "text-sm text-muted-foreground",
138
139
  overline: "font-semibold text-muted-foreground text-xs uppercase tracking-wide",
140
+ // The tiny tracked mono label over a group or a screen. `font-mono`
141
+ // rather than `font-sans`, which is the point of it: it reads as a
142
+ // measurement, not a sentence. Tokens, not literals — see tokens.css.
143
+ kicker: "font-mono text-kicker text-muted-foreground uppercase tracking-kicker",
139
144
  // The four below emit a delta and nothing else. See TEXT_INLINE_VARIANTS.
140
145
  strong: "font-semibold",
141
146
  emphasis: "italic",
@@ -0,0 +1,89 @@
1
+ # Textarea
2
+
3
+ A multiline text field, sized in rows. One component, no parts: it renders an
4
+ [`Input`](../input/AGENTS.md) and adds a height and an optional character count.
5
+
6
+ `import { Textarea } from "@delacour/react-native-ui/textarea";`
7
+
8
+ ## Files
9
+
10
+ | File | What it holds |
11
+ | --- | --- |
12
+ | `index.ts` | → `@delacour/react-native-ui/textarea` |
13
+ | `textarea.tsx` | `Textarea` |
14
+ | `textarea.variants.ts` | Pure `tv()` slots, the line metrics and the height and count resolvers, no RN imports |
15
+ | `textarea.variants.test.ts` | |
16
+
17
+ ## Design
18
+
19
+ - **The box is `Input`'s, and nothing here restates it.** Variants (`primary`,
20
+ `secondary`), sizes (`sm`, `md`, `lg`), the focus ring, the invalid border
21
+ that outranks it, the disabled fade, the `accent-*` caret and placeholder, and
22
+ the `Field` cascade all come from rendering an `Input` with `multiline`. A
23
+ second `tv()` for the same border would be a second thing that can drift, and
24
+ a textarea under a row of single-line fields has to read as the same control
25
+ at a different height — not as a close match.
26
+ - **`rows` is a line count, and it becomes points in JavaScript.** A row count
27
+ only turns into a height once the height of a line is a number, so
28
+ `TEXTAREA_LINE_HEIGHTS` holds it per size and `resolveTextareaRowsHeight` adds
29
+ the padding and the border either side. The result is a **style**, never a
30
+ class: a runtime `` `h-[${n}px]` `` is never compiled by Tailwind's static
31
+ scanner and would draw nothing — the rule in
32
+ [Sizing](../../../AGENTS.md#sizing). `size` moves the type and the leading, so
33
+ a small three-row field and a large one are both three rows.
34
+ - **Every number the height counts is pinned to the class that draws it.** The
35
+ line height is set by a `leading-*` class in `textareaVariants`' `field` slot,
36
+ and the padding is `Input`'s multiline `py-*`. `textarea.variants.test.ts`
37
+ resolves both classes and fails if either disagrees with its number — a
38
+ retune of `Input`'s padding would otherwise mis-size every textarea by a few
39
+ points, silently, with the last line clipped at the bottom edge.
40
+ - **A paragraph gets paragraph leading.** `Input` sets `leading-tight` because
41
+ it centres one line in a fixed box; four lines at 1.25× read as a wall. The
42
+ textarea's `leading-5`/`-6`/`-7` replace it through `Input`'s own className
43
+ merge rather than sitting beside it, and a test holds each between 1.2× and
44
+ 1.6× the `--text-input-*` size so the type scale can be retuned without this
45
+ going stale.
46
+ - **`autoGrow` measures nothing.** It sets `minHeight` at `rows` and
47
+ `maxHeight` at `maxRows`, and between the two React Native's multiline
48
+ `TextInput` sizes itself to its content; past the cap it scrolls. An
49
+ `onContentSizeChange` loop would draw one frame at the old height after every
50
+ new line, which is exactly the jump a growing field exists to avoid. The field
51
+ never shrinks below `rows`, so a message typed and cleared goes back to where
52
+ it started rather than collapsing to one line.
53
+ - **`maxRows` only type-checks with `autoGrow`, and `showCount` only with
54
+ `maxLength`.** Both are unions in `TextareaProps`. A ceiling on a field that
55
+ does not grow is a number nothing reads, and a count with no limit to count
56
+ towards says nothing — a runtime warning would only find either out on a
57
+ device.
58
+ - **A bad row count degrades rather than throws.** `resolveTextareaRows` floors
59
+ to whole lines, clamps to at least one, falls back on `NaN` or `Infinity`, and
60
+ lifts a `maxRows` below `rows` up to it — otherwise a growing field would
61
+ shrink as it grew.
62
+ - **The count turns destructive at the limit, not past it.** Native `maxLength`
63
+ stops typing at the limit, so "over" is a state someone typing never sees;
64
+ the count has to change at the moment the keyboard stops working. It renders
65
+ `Text.Caption` and passes its colour through the preset's `color` prop, so the
66
+ `count` slot carries no colour class to fight it. It uses `tabular-nums` so it
67
+ does not jitter as it counts, and announces as "12 of 280 characters" rather
68
+ than as a fraction.
69
+ - **The count works controlled and uncontrolled.** A `value` is read directly;
70
+ without one, the last text the field reported is, seeded from `defaultValue`
71
+ so the first frame is already right.
72
+ - **There is always a wrapping `View`.** `className` reaches the field, as it
73
+ does on `Input`, and `containerClassName` reaches the wrapper that holds the
74
+ field and its count. The wrapper is there whether or not the count is, so
75
+ flipping `showCount` never remounts the field and drops focus.
76
+ - **`multiline` and `numberOfLines` are withheld.** It is always multiline, and
77
+ `rows` is its height; `numberOfLines` would be a second answer to the same
78
+ question, and on Android the one that wins.
79
+ - **There is no `label`, `description` or `errorMessage` prop.** A textarea goes
80
+ in a [`Field`](../field/AGENTS.md) the way an `Input` does — the reason
81
+ `Input` ships no label part holds here too. `apps/playground`'s
82
+ `/textarea` gallery ends with a composed form showing the trade at a call site.
83
+ - **Not for `Input.Group` or `Button.Group`.** An inner `Input` would read
84
+ either context and take its box or its corner, but a decorated or joined
85
+ paragraph is not a control this library draws, and the height math assumes
86
+ the box is the field's own.
87
+
88
+ - **Takes `Input`'s `etched` variant** — the box is `Input`'s, so the third `INPUT_VARIANTS` value
89
+ reaches the textarea unchanged.
@@ -0,0 +1,16 @@
1
+ export { Textarea, type TextareaProps } from "./textarea";
2
+ export {
3
+ resolveTextareaCount,
4
+ resolveTextareaFieldClass,
5
+ resolveTextareaHeightStyle,
6
+ resolveTextareaRows,
7
+ resolveTextareaRowsHeight,
8
+ TEXTAREA_BORDER_WIDTH,
9
+ TEXTAREA_DEFAULT_MAX_ROWS,
10
+ TEXTAREA_DEFAULT_ROWS,
11
+ TEXTAREA_LINE_HEIGHTS,
12
+ TEXTAREA_PADDING_Y,
13
+ type TextareaCount,
14
+ type TextareaVariantProps,
15
+ textareaVariants,
16
+ } from "./textarea.variants";
@@ -0,0 +1,157 @@
1
+ import { type ReactElement, useCallback, useState } from "react";
2
+ import { View } from "react-native";
3
+ import { Input, type InputProps } from "../input";
4
+ import { Text } from "../text";
5
+ import {
6
+ resolveTextareaCount,
7
+ resolveTextareaFieldClass,
8
+ resolveTextareaHeightStyle,
9
+ textareaVariants,
10
+ } from "./textarea.variants";
11
+
12
+ /**
13
+ * Whether the field grows with its text.
14
+ *
15
+ * A union so `maxRows` is only accepted where it means something: on a field
16
+ * that does not grow, a ceiling is a number nothing reads.
17
+ */
18
+ type TextareaGrowProps =
19
+ | {
20
+ /** Stay `rows` tall and scroll past it. The default. */
21
+ autoGrow?: false;
22
+ maxRows?: never;
23
+ }
24
+ | {
25
+ /** Grow with the text, from `rows` up to `maxRows`, then scroll. */
26
+ autoGrow: true;
27
+ /** The tallest the field grows, in lines. Default 10. */
28
+ maxRows?: number;
29
+ };
30
+
31
+ /**
32
+ * Whether a character count sits under the field.
33
+ *
34
+ * A union so the count cannot be asked for without a limit: a count with
35
+ * nothing to count towards says nothing, and a runtime check would only find
36
+ * that out on a device.
37
+ */
38
+ type TextareaCountProps =
39
+ | {
40
+ showCount?: false;
41
+ /** The most characters the field accepts. */
42
+ maxLength?: number;
43
+ }
44
+ | {
45
+ /** Show how much of `maxLength` is used, under the field's trailing edge. */
46
+ showCount: true;
47
+ maxLength: number;
48
+ };
49
+
50
+ export type TextareaProps = Omit<InputProps, "multiline" | "numberOfLines" | "maxLength"> &
51
+ TextareaGrowProps &
52
+ TextareaCountProps & {
53
+ /** How many lines tall the field is — its height when fixed, its floor when growing. Default 4. */
54
+ rows?: number;
55
+ /** Class for the view holding the field and its count. `className` styles the field itself. */
56
+ containerClassName?: string;
57
+ };
58
+
59
+ /**
60
+ * A multiline text field, sized in rows.
61
+ *
62
+ * The box is `Input`'s — its variants, sizes, focus ring, invalid border,
63
+ * disabled fade and the `Field` cascade all come from there unchanged, so a
64
+ * textarea and the single-line fields above it in a form are the same control
65
+ * at a different height rather than two that happen to match.
66
+ *
67
+ * What this adds is the height. `rows` is a line count, and it becomes points
68
+ * through the paragraph leading and the box's own padding and border — see
69
+ * `resolveTextareaHeightStyle`. It is applied as a style, never a class,
70
+ * because a runtime number cannot be a Tailwind class. `size` changes the type
71
+ * and leading, so a small three-row field and a large one are both three rows.
72
+ *
73
+ * `autoGrow` floors the field at `rows` and caps it at `maxRows`; between the
74
+ * two, React Native's multiline `TextInput` sizes itself to its content, and
75
+ * past the cap it scrolls. `showCount` with `maxLength` puts a count under the
76
+ * trailing edge that turns destructive at the limit.
77
+ *
78
+ * `multiline` and `numberOfLines` are withheld: it is always multiline, and its
79
+ * height comes from `rows`.
80
+ *
81
+ * Label, description and error come from `Field`, exactly as they do for
82
+ * `Input` — there is no `label` prop.
83
+ *
84
+ * @example
85
+ * <Field>
86
+ * <Field.Label>Notes</Field.Label>
87
+ * <Textarea placeholder="Anything we should know?" />
88
+ * </Field>
89
+ *
90
+ * @example
91
+ * <Textarea autoGrow maxRows={8} rows={2} placeholder="Message" />
92
+ *
93
+ * @example
94
+ * <Textarea maxLength={280} onChangeText={setStatus} showCount value={status} />
95
+ */
96
+ export function Textarea({
97
+ rows,
98
+ autoGrow,
99
+ maxRows,
100
+ showCount,
101
+ maxLength,
102
+ size,
103
+ className,
104
+ containerClassName,
105
+ style,
106
+ value,
107
+ defaultValue,
108
+ onChangeText,
109
+ ...props
110
+ }: TextareaProps): ReactElement {
111
+ const resolvedSize = size ?? "md";
112
+ const slots = textareaVariants({ size: resolvedSize });
113
+
114
+ // The count reads the value's length whether or not the caller owns the
115
+ // value. Controlled, `value` is the truth; uncontrolled, the last text the
116
+ // field reported is — seeded from `defaultValue` so the count is right on
117
+ // the first frame rather than after the first keystroke.
118
+ const [ownLength, setOwnLength] = useState(() => defaultValue?.length ?? 0);
119
+ const length = value === undefined ? ownLength : value.length;
120
+
121
+ const handleChangeText = useCallback(
122
+ (text: string) => {
123
+ setOwnLength(text.length);
124
+ onChangeText?.(text);
125
+ },
126
+ [onChangeText]
127
+ );
128
+
129
+ const count = showCount && maxLength !== undefined ? resolveTextareaCount({ length, maxLength }) : null;
130
+
131
+ return (
132
+ <View className={slots.root({ className: containerClassName })}>
133
+ <Input
134
+ {...props}
135
+ className={resolveTextareaFieldClass({ className, size: resolvedSize })}
136
+ defaultValue={defaultValue}
137
+ maxLength={maxLength}
138
+ multiline
139
+ onChangeText={handleChangeText}
140
+ size={resolvedSize}
141
+ style={[resolveTextareaHeightStyle({ autoGrow, maxRows, rows, size: resolvedSize }), style]}
142
+ value={value}
143
+ />
144
+ {count ? (
145
+ <Text.Caption
146
+ accessibilityLabel={count.accessibilityLabel}
147
+ accessibilityLiveRegion="polite"
148
+ className={slots.count()}
149
+ color={count.color}
150
+ >
151
+ {count.label}
152
+ </Text.Caption>
153
+ ) : null}
154
+ </View>
155
+ );
156
+ }
157
+ Textarea.displayName = "DelacourUI.Textarea";
@@ -0,0 +1,219 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { INPUT_SIZES, inputVariants, resolveInputFieldClass } from "../input/input.variants";
5
+ import {
6
+ resolveTextareaCount,
7
+ resolveTextareaFieldClass,
8
+ resolveTextareaHeightStyle,
9
+ resolveTextareaRows,
10
+ resolveTextareaRowsHeight,
11
+ TEXTAREA_BORDER_WIDTH,
12
+ TEXTAREA_DEFAULT_MAX_ROWS,
13
+ TEXTAREA_DEFAULT_ROWS,
14
+ TEXTAREA_LINE_HEIGHTS,
15
+ TEXTAREA_PADDING_Y,
16
+ textareaVariants,
17
+ } from "./textarea.variants";
18
+
19
+ const TOKENS_CSS = readFileSync(join(import.meta.dirname, "../../styles/tokens.css"), "utf-8");
20
+
21
+ /** Points behind a `--text-input-*` token in `tokens.css`. */
22
+ function inputTextPoints(size: string): number {
23
+ const match = TOKENS_CSS.match(new RegExp(`--text-input-${size}:\\s*([\\d.]+)px;`));
24
+ if (!match?.[1]) throw new Error(`tokens.css defines no --text-input-${size}`);
25
+ return Number(match[1]);
26
+ }
27
+
28
+ /** Points behind the first `<prefix>-N` spacing utility in a class string, at Tailwind's 4pt step. */
29
+ function spacingPoints(cls: string, prefix: string): number | undefined {
30
+ const match = cls.match(new RegExp(`(?:^|\\s)${prefix}-(\\d+(?:\\.\\d+)?)(?:\\s|$)`));
31
+ return match?.[1] === undefined ? undefined : Number(match[1]) * 4;
32
+ }
33
+
34
+ describe("the metrics the height is derived from", () => {
35
+ // The height is a number, so the padding it counts has to be the padding
36
+ // the box actually wears. That padding is `Input`'s multiline `py-*`, and
37
+ // restating it here is only safe because this test reads it back.
38
+ test("count the vertical padding Input's multiline box wears", () => {
39
+ for (const size of INPUT_SIZES) {
40
+ const cls = resolveInputFieldClass({ isMultiline: true, size });
41
+ expect(spacingPoints(cls, "py")).toBe(TEXTAREA_PADDING_Y[size]);
42
+ }
43
+ });
44
+
45
+ test("count the one-pixel border every variant draws", () => {
46
+ expect(inputVariants().root()).toMatch(/(?:^|\s)border(?:\s|$)/);
47
+ expect(TEXTAREA_BORDER_WIDTH).toBe(1);
48
+ });
49
+
50
+ test("give each size the line height its field class sets", () => {
51
+ for (const size of INPUT_SIZES) {
52
+ const cls = resolveTextareaFieldClass({ size });
53
+ expect(spacingPoints(cls, "leading")).toBe(TEXTAREA_LINE_HEIGHTS[size]);
54
+ }
55
+ });
56
+
57
+ // A line shorter than its type clips descenders; one under 1.2× reads as
58
+ // a wall. The ratio is what is pinned, so the type scale can be retuned.
59
+ test("set every line between 1.2× and 1.6× the value's type size", () => {
60
+ for (const size of INPUT_SIZES) {
61
+ const ratio = TEXTAREA_LINE_HEIGHTS[size] / inputTextPoints(size);
62
+ expect(ratio).toBeGreaterThanOrEqual(1.2);
63
+ expect(ratio).toBeLessThanOrEqual(1.6);
64
+ }
65
+ });
66
+
67
+ test("step the line height with the size", () => {
68
+ const heights = INPUT_SIZES.map((size) => TEXTAREA_LINE_HEIGHTS[size]);
69
+ expect([...heights].sort((a, b) => a - b)).toEqual(heights);
70
+ expect(new Set(heights).size).toBe(heights.length);
71
+ });
72
+ });
73
+
74
+ describe("resolveTextareaFieldClass", () => {
75
+ test("replaces Input's single-line leading rather than adding a second", () => {
76
+ for (const size of INPUT_SIZES) {
77
+ const cls = resolveInputFieldClass({
78
+ className: resolveTextareaFieldClass({ size }),
79
+ isMultiline: true,
80
+ size,
81
+ });
82
+ expect(cls).not.toContain("leading-tight");
83
+ expect(cls.match(/\bleading-/g)).toHaveLength(1);
84
+ }
85
+ });
86
+
87
+ test("merges an incoming className last", () => {
88
+ expect(resolveTextareaFieldClass({ className: "leading-9", size: "md" })).toContain("leading-9");
89
+ expect(resolveTextareaFieldClass({ className: "leading-9", size: "md" })).not.toContain("leading-6");
90
+ });
91
+ });
92
+
93
+ describe("resolveTextareaRows", () => {
94
+ test("defaults to four rows and a ten-row ceiling", () => {
95
+ expect(TEXTAREA_DEFAULT_ROWS).toBe(4);
96
+ expect(TEXTAREA_DEFAULT_MAX_ROWS).toBe(10);
97
+ expect(resolveTextareaRows({})).toEqual({ maxRows: 10, rows: 4 });
98
+ });
99
+
100
+ test("never goes below one row", () => {
101
+ expect(resolveTextareaRows({ rows: 0 }).rows).toBe(1);
102
+ expect(resolveTextareaRows({ rows: -3 }).rows).toBe(1);
103
+ });
104
+
105
+ test("rounds a fractional row count down to whole lines", () => {
106
+ expect(resolveTextareaRows({ rows: 2.7 }).rows).toBe(2);
107
+ expect(resolveTextareaRows({ maxRows: 5.9, rows: 2 }).maxRows).toBe(5);
108
+ });
109
+
110
+ // A ceiling below the floor would make the field shrink as it grew.
111
+ test("lifts a ceiling below the floor up to it", () => {
112
+ expect(resolveTextareaRows({ maxRows: 2, rows: 6 })).toEqual({ maxRows: 6, rows: 6 });
113
+ });
114
+
115
+ test("falls back on a non-finite count", () => {
116
+ expect(resolveTextareaRows({ rows: Number.NaN })).toEqual({ maxRows: 10, rows: 4 });
117
+ expect(resolveTextareaRows({ maxRows: Number.POSITIVE_INFINITY, rows: 3 }).maxRows).toBe(10);
118
+ });
119
+ });
120
+
121
+ describe("resolveTextareaRowsHeight", () => {
122
+ test("is the lines plus the padding and border either side", () => {
123
+ for (const size of INPUT_SIZES) {
124
+ const expected = 3 * TEXTAREA_LINE_HEIGHTS[size] + 2 * TEXTAREA_PADDING_Y[size] + 2 * TEXTAREA_BORDER_WIDTH;
125
+ expect(resolveTextareaRowsHeight({ rows: 3, size })).toBe(expected);
126
+ }
127
+ });
128
+
129
+ test("grows by exactly one line per row", () => {
130
+ for (const size of INPUT_SIZES) {
131
+ const step = resolveTextareaRowsHeight({ rows: 5, size }) - resolveTextareaRowsHeight({ rows: 4, size });
132
+ expect(step).toBe(TEXTAREA_LINE_HEIGHTS[size]);
133
+ }
134
+ });
135
+
136
+ // A one-row textarea should not be shorter than the single-line field it
137
+ // sits beside in a form.
138
+ test("is never shorter than a single-line Input at one row", () => {
139
+ const INPUT_HEIGHTS = { lg: 52, md: 44, sm: 36 } as const;
140
+ for (const size of INPUT_SIZES) {
141
+ expect(resolveTextareaRowsHeight({ rows: 1, size })).toBeGreaterThanOrEqual(INPUT_HEIGHTS[size]);
142
+ }
143
+ });
144
+ });
145
+
146
+ describe("resolveTextareaHeightStyle", () => {
147
+ test("fixes the height at the row count when it does not grow", () => {
148
+ expect(resolveTextareaHeightStyle({ rows: 3, size: "md" })).toEqual({
149
+ height: resolveTextareaRowsHeight({ rows: 3, size: "md" }),
150
+ });
151
+ });
152
+
153
+ test("floors at rows and caps at maxRows when it grows", () => {
154
+ expect(resolveTextareaHeightStyle({ autoGrow: true, maxRows: 6, rows: 2, size: "sm" })).toEqual({
155
+ maxHeight: resolveTextareaRowsHeight({ rows: 6, size: "sm" }),
156
+ minHeight: resolveTextareaRowsHeight({ rows: 2, size: "sm" }),
157
+ });
158
+ });
159
+
160
+ test("caps a growing field at ten rows when maxRows is unset", () => {
161
+ expect(resolveTextareaHeightStyle({ autoGrow: true, size: "md" })).toEqual({
162
+ maxHeight: resolveTextareaRowsHeight({ rows: 10, size: "md" }),
163
+ minHeight: resolveTextareaRowsHeight({ rows: 4, size: "md" }),
164
+ });
165
+ });
166
+
167
+ test("ignores maxRows on a field that does not grow", () => {
168
+ expect(resolveTextareaHeightStyle({ maxRows: 8, rows: 2, size: "lg" })).toEqual({
169
+ height: resolveTextareaRowsHeight({ rows: 2, size: "lg" }),
170
+ });
171
+ });
172
+ });
173
+
174
+ describe("resolveTextareaCount", () => {
175
+ test("reads as used over limit", () => {
176
+ expect(resolveTextareaCount({ length: 12, maxLength: 280 }).label).toBe("12/280");
177
+ });
178
+
179
+ test("announces the count as a sentence", () => {
180
+ expect(resolveTextareaCount({ length: 1, maxLength: 280 }).accessibilityLabel).toBe("1 of 280 characters");
181
+ });
182
+
183
+ test("reports the limit once it is reached, and not before", () => {
184
+ expect(resolveTextareaCount({ length: 279, maxLength: 280 }).isAtLimit).toBe(false);
185
+ expect(resolveTextareaCount({ length: 280, maxLength: 280 }).isAtLimit).toBe(true);
186
+ });
187
+
188
+ test("is muted under the limit and destructive at it", () => {
189
+ expect(resolveTextareaCount({ length: 3, maxLength: 10 }).color).toBe("muted");
190
+ expect(resolveTextareaCount({ length: 10, maxLength: 10 }).color).toBe("destructive");
191
+ });
192
+
193
+ // A controlled value set past the limit in code is still over it; the
194
+ // native `maxLength` only stops typing.
195
+ test("still reports a value past the limit", () => {
196
+ expect(resolveTextareaCount({ length: 300, maxLength: 280 })).toEqual({
197
+ accessibilityLabel: "300 of 280 characters",
198
+ color: "destructive",
199
+ isAtLimit: true,
200
+ label: "300/280",
201
+ });
202
+ });
203
+ });
204
+
205
+ describe("the count slot", () => {
206
+ test("sits at the trailing edge", () => {
207
+ expect(textareaVariants().count()).toContain("self-end");
208
+ });
209
+
210
+ test("uses tabular figures, so the count does not jitter as it changes", () => {
211
+ expect(textareaVariants().count()).toContain("tabular-nums");
212
+ });
213
+
214
+ // Colour belongs to the `Text` preset's `color` prop, not to a class that
215
+ // would fight it in the merge.
216
+ test("carries no text colour of its own", () => {
217
+ expect(textareaVariants().count()).not.toMatch(/\btext-(?:muted|destructive|foreground)/);
218
+ });
219
+ });