@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,185 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { declaredTokens } from "../../styles/theme-tokens.test";
3
+ import {
4
+ canSubmitFeedback,
5
+ FEEDBACK_DEFAULT_MAX_ROWS,
6
+ FEEDBACK_DEFAULT_MIN_ROWS,
7
+ FEEDBACK_FIELD_LINE_HEIGHT,
8
+ FEEDBACK_PANEL_RESIZE_MS,
9
+ feedbackVariants,
10
+ resolveFeedbackFieldHeightStyle,
11
+ resolveFeedbackFieldMinHeight,
12
+ } from "./feedback.variants";
13
+
14
+ const LIGHT = declaredTokens("light");
15
+ const DARK = declaredTokens("dark");
16
+
17
+ /** `border-t` and friends set a width, not a colour, and name no token. */
18
+ const STRUCTURAL_BORDER_SUFFIXES = new Set(["t", "b", "l", "r", "x", "y", "s", "e"]);
19
+
20
+ /** Every theme token a class string paints with, with any `/alpha` suffix dropped. */
21
+ function colorTokens(cls: string): string[] {
22
+ const tokens: string[] = [];
23
+
24
+ for (const [, utility, token] of cls.matchAll(/\b(bg|border|text)-([a-z][\w-]*)(?:\/\d+)?\b/g)) {
25
+ if (utility === "border" && STRUCTURAL_BORDER_SUFFIXES.has(token)) continue;
26
+ // `text-input-md` is a size token, not a colour.
27
+ if (utility === "text" && token.startsWith("input-")) continue;
28
+ tokens.push(token);
29
+ }
30
+
31
+ return tokens;
32
+ }
33
+
34
+ /** Pinned rather than derived — `tv` adds a `base` slot of its own. */
35
+ const SLOT_NAMES = ["content", "clip", "panel", "title", "close", "field", "footer"] as const;
36
+
37
+ describe("feedbackVariants", () => {
38
+ test("the shell paints the muted band and is tighter than a dialog's p-5", () => {
39
+ const content = feedbackVariants().content();
40
+ expect(content).toContain("bg-muted");
41
+ expect(content).toMatch(/\bp-[1-3]\b/);
42
+ expect(content).not.toContain("p-5");
43
+ });
44
+
45
+ test("the panel is a recessed well on the page colour, inset with the md corner and a hairline", () => {
46
+ const panel = feedbackVariants().panel();
47
+ expect(panel).toContain("bg-background");
48
+ expect(panel).toContain("rounded-md");
49
+ expect(panel).toContain("border");
50
+ expect(panel).toContain("border-border");
51
+ expect(panel).toMatch(/\bp-\d/);
52
+ });
53
+
54
+ test("the clip hides the well's overflow mid-resize and shares its corner", () => {
55
+ const clip = feedbackVariants().clip();
56
+ expect(clip).toContain("overflow-hidden");
57
+ expect(clip).toContain("rounded-md");
58
+ });
59
+
60
+ test("the title reserves clearance for the close glyph and carries no type of its own", () => {
61
+ const title = feedbackVariants().title();
62
+ expect(title).toBe("pr-8");
63
+ });
64
+
65
+ test("the close glyph sits in the panel's top-right corner", () => {
66
+ const close = feedbackVariants().close();
67
+ expect(close).toContain("absolute");
68
+ expect(close).toMatch(/\btop-\d/);
69
+ expect(close).toMatch(/\bright-\d/);
70
+ });
71
+
72
+ test("the field has no box of its own and sets the input type on the house face", () => {
73
+ const field = feedbackVariants().field();
74
+ expect(field).toContain("font-sans");
75
+ expect(field).toContain("text-input-md");
76
+ expect(field).toContain("text-foreground");
77
+ expect(field).toContain("leading-6");
78
+ expect(field).not.toMatch(/\bborder\b/);
79
+ expect(field).not.toMatch(/\bbg-/);
80
+ });
81
+
82
+ test("the footer is an end-aligned row with a narrower inset than the well's", () => {
83
+ const footer = feedbackVariants().footer();
84
+ expect(footer).toContain("flex-row");
85
+ expect(footer).toContain("items-center");
86
+ expect(footer).toContain("justify-end");
87
+ expect(footer).toContain("gap-2");
88
+ expect(footer).not.toMatch(/\bp-[4-9]\b/);
89
+ });
90
+
91
+ test("a caller's className reaches each slot and wins a conflict", () => {
92
+ const panel = feedbackVariants().panel({ className: "rounded-xl" });
93
+ expect(panel).toContain("rounded-xl");
94
+ expect(panel).not.toContain("rounded-md");
95
+ });
96
+ });
97
+
98
+ describe("every token the slots name", () => {
99
+ test("is declared in both variants of theme.css", () => {
100
+ const slots = feedbackVariants();
101
+ for (const name of SLOT_NAMES) {
102
+ for (const token of colorTokens(slots[name]())) {
103
+ expect({ inLight: LIGHT.has(token), slot: name, token }).toEqual({ inLight: true, slot: name, token });
104
+ expect({ inDark: DARK.has(token), slot: name, token }).toEqual({ inDark: true, slot: name, token });
105
+ }
106
+ }
107
+ });
108
+
109
+ test("the reader found tokens at all", () => {
110
+ expect(colorTokens(feedbackVariants().content())).toContain("muted");
111
+ expect(colorTokens(feedbackVariants().panel())).toEqual(expect.arrayContaining(["background", "border"]));
112
+ expect(colorTokens(feedbackVariants().field())).toEqual(["foreground"]);
113
+ });
114
+ });
115
+
116
+ describe("canSubmitFeedback", () => {
117
+ test("an empty message cannot be sent", () => {
118
+ expect(canSubmitFeedback({ value: "" })).toBe(false);
119
+ });
120
+
121
+ test("whitespace is empty", () => {
122
+ expect(canSubmitFeedback({ value: " \n\t " })).toBe(false);
123
+ });
124
+
125
+ test("any text can be sent", () => {
126
+ expect(canSubmitFeedback({ value: "The export button is hidden" })).toBe(true);
127
+ expect(canSubmitFeedback({ value: " x " })).toBe(true);
128
+ });
129
+
130
+ test("canSubmitEmpty lets an empty message through", () => {
131
+ expect(canSubmitFeedback({ canSubmitEmpty: true, value: "" })).toBe(true);
132
+ expect(canSubmitFeedback({ canSubmitEmpty: true, value: " " })).toBe(true);
133
+ });
134
+
135
+ test("disabled outranks everything", () => {
136
+ expect(canSubmitFeedback({ isDisabled: true, value: "text" })).toBe(false);
137
+ expect(canSubmitFeedback({ canSubmitEmpty: true, isDisabled: true, value: "" })).toBe(false);
138
+ });
139
+ });
140
+
141
+ describe("resolveFeedbackFieldMinHeight", () => {
142
+ test("the line height is the field's leading-6", () => {
143
+ expect(FEEDBACK_FIELD_LINE_HEIGHT).toBe(24);
144
+ });
145
+
146
+ test("defaults to six rows", () => {
147
+ expect(FEEDBACK_DEFAULT_MIN_ROWS).toBe(6);
148
+ expect(resolveFeedbackFieldMinHeight(undefined, FEEDBACK_FIELD_LINE_HEIGHT)).toBe(144);
149
+ });
150
+
151
+ test("is rows times the line height", () => {
152
+ expect(resolveFeedbackFieldMinHeight(3, 24)).toBe(72);
153
+ expect(resolveFeedbackFieldMinHeight(1, 20)).toBe(20);
154
+ });
155
+
156
+ test("floors to whole rows and clamps to at least one", () => {
157
+ expect(resolveFeedbackFieldMinHeight(2.7, 24)).toBe(48);
158
+ expect(resolveFeedbackFieldMinHeight(0, 24)).toBe(24);
159
+ expect(resolveFeedbackFieldMinHeight(-3, 24)).toBe(24);
160
+ });
161
+
162
+ test("falls back to the default on a number that is not one", () => {
163
+ expect(resolveFeedbackFieldMinHeight(Number.NaN, 24)).toBe(144);
164
+ expect(resolveFeedbackFieldMinHeight(Number.POSITIVE_INFINITY, 24)).toBe(144);
165
+ });
166
+ });
167
+
168
+ describe("resolveFeedbackFieldHeightStyle", () => {
169
+ test("floors at minRows and caps at maxRows", () => {
170
+ expect(FEEDBACK_DEFAULT_MAX_ROWS).toBe(12);
171
+ expect(resolveFeedbackFieldHeightStyle({})).toEqual({ maxHeight: 288, minHeight: 144 });
172
+ expect(resolveFeedbackFieldHeightStyle({ maxRows: 4, minRows: 2 })).toEqual({ maxHeight: 96, minHeight: 48 });
173
+ });
174
+
175
+ test("lifts a ceiling below the floor up to it, so the field never shrinks as it grows", () => {
176
+ expect(resolveFeedbackFieldHeightStyle({ maxRows: 2, minRows: 6 })).toEqual({ maxHeight: 144, minHeight: 144 });
177
+ });
178
+ });
179
+
180
+ describe("FEEDBACK_PANEL_RESIZE_MS", () => {
181
+ test("is short and finite, so a step change settles before the next tap", () => {
182
+ expect(FEEDBACK_PANEL_RESIZE_MS).toBeGreaterThan(0);
183
+ expect(FEEDBACK_PANEL_RESIZE_MS).toBeLessThanOrEqual(250);
184
+ });
185
+ });
@@ -0,0 +1,128 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+
4
+ /** How many lines tall the field is before it grows, when `minRows` is not named. */
5
+ export const FEEDBACK_DEFAULT_MIN_ROWS = 6;
6
+
7
+ /** The tallest the field grows before it scrolls, when `maxRows` is not named. */
8
+ export const FEEDBACK_DEFAULT_MAX_ROWS = 12;
9
+
10
+ /**
11
+ * How long the well takes to follow a change in its content's height — a
12
+ * multi-step swap, or a line added while typing. Short, so a new line is never
13
+ * clipped for long, and finite, so an E2E runner's settle wait ends.
14
+ */
15
+ export const FEEDBACK_PANEL_RESIZE_MS = 200;
16
+
17
+ /**
18
+ * Points per line of the field — the `leading-6` its slot sets. The test reads
19
+ * the class back, so the two cannot drift apart.
20
+ */
21
+ export const FEEDBACK_FIELD_LINE_HEIGHT = 24;
22
+
23
+ /**
24
+ * Styling for every part of a feedback dialog.
25
+ *
26
+ * The shell is `Dialog.Content` with these classes merged over the dialog's
27
+ * own, so `content` only names what differs from a confirm dialog: the muted
28
+ * band and a tighter padding, which is what makes the well read as inset.
29
+ * Everything else — the corner, the hairline, the width cap — stays Dialog's.
30
+ *
31
+ * Free of React Native imports so it stays unit-testable. See AGENTS.md.
32
+ */
33
+ export const feedbackVariants = tv({
34
+ slots: {
35
+ /** The shell: the dialog's card, on the band colour, with a tight inset around the well. */
36
+ content: "gap-2 bg-muted p-2",
37
+ /**
38
+ * The animated clip around the well. Its height follows the well's on a
39
+ * timing; its corner matches the well's so nothing pokes out mid-resize.
40
+ */
41
+ clip: "overflow-hidden rounded-md",
42
+ /**
43
+ * The recessed well: the page colour, one step down the corner ramp from
44
+ * the card it sits in so the two read as nested, and a hairline. Relative
45
+ * so the corner ✕ positions against it rather than the shell.
46
+ */
47
+ panel: "relative gap-2 overflow-hidden rounded-md border border-border bg-background p-4",
48
+ /** Clearance for `Feedback.Close`, and nothing else — the type is `Text.Header`'s. */
49
+ title: "pr-8",
50
+ close: "absolute top-3 right-3 z-10",
51
+ /**
52
+ * A raw `TextInput` with no box: the well is the box. It restates
53
+ * `font-sans` for the reason `Input`'s field does — a `TextInput`
54
+ * inherits nothing — and sets paragraph leading, which its min height
55
+ * is counted in.
56
+ */
57
+ field: "p-0 font-sans text-foreground text-input-md leading-6",
58
+ /** Narrower than the well's inset, so the actions sit on the band rather than in it. */
59
+ footer: "flex-row items-center justify-end gap-2 px-1 pb-1",
60
+ },
61
+ });
62
+
63
+ export type FeedbackVariantProps = VariantProps<typeof feedbackVariants>;
64
+
65
+ export type CanSubmitFeedbackInput = {
66
+ /** The draft as typed. */
67
+ value: string;
68
+ /** Allow an empty message — when chips or a rating carry the answer. Default false. */
69
+ canSubmitEmpty?: boolean;
70
+ /** The submit is off — disabled by the caller, or a send already in flight. Default false. */
71
+ isDisabled?: boolean;
72
+ };
73
+
74
+ /**
75
+ * Whether `Feedback.Submit` can be pressed.
76
+ *
77
+ * Whitespace is empty: a message of three spaces sends nothing, and `onSubmit`
78
+ * receives the trimmed text anyway. Disabled outranks `canSubmitEmpty`.
79
+ *
80
+ * Pure, so it is reachable from `bun test`. See AGENTS.md.
81
+ */
82
+ export function canSubmitFeedback({
83
+ value,
84
+ canSubmitEmpty = false,
85
+ isDisabled = false,
86
+ }: CanSubmitFeedbackInput): boolean {
87
+ if (isDisabled) return false;
88
+ if (canSubmitEmpty) return true;
89
+ return value.trim().length > 0;
90
+ }
91
+
92
+ /**
93
+ * The field's minimum height, in points — `minRows` lines of `lineHeight`.
94
+ *
95
+ * A style, never a class: a runtime `` `min-h-[${n}px]` `` is never compiled by
96
+ * Tailwind's static scanner. A bad row count degrades the way `Textarea`'s
97
+ * does — floored to whole lines, clamped to one, the default on `NaN`.
98
+ *
99
+ * Pure, so it is reachable from `bun test`. See AGENTS.md.
100
+ */
101
+ export function resolveFeedbackFieldMinHeight(minRows: number | undefined, lineHeight: number): number {
102
+ const rows =
103
+ minRows === undefined || !Number.isFinite(minRows) ? FEEDBACK_DEFAULT_MIN_ROWS : Math.max(1, Math.floor(minRows));
104
+ return rows * lineHeight;
105
+ }
106
+
107
+ /**
108
+ * The field's height bounds, as a style: floored at `minRows`, capped at
109
+ * `maxRows`, and between the two React Native's multiline `TextInput` sizes
110
+ * itself to its text. Past the cap it scrolls — a dialog that grew without
111
+ * one would push its footer under the keyboard.
112
+ *
113
+ * A ceiling below the floor is lifted up to it, so the field never shrinks as
114
+ * it grows. Pure, so it is reachable from `bun test`. See AGENTS.md.
115
+ */
116
+ export function resolveFeedbackFieldHeightStyle({
117
+ minRows,
118
+ maxRows,
119
+ lineHeight = FEEDBACK_FIELD_LINE_HEIGHT,
120
+ }: {
121
+ minRows?: number;
122
+ maxRows?: number;
123
+ lineHeight?: number;
124
+ }): { minHeight: number; maxHeight: number } {
125
+ const minHeight = resolveFeedbackFieldMinHeight(minRows, lineHeight);
126
+ const ceiling = resolveFeedbackFieldMinHeight(maxRows ?? FEEDBACK_DEFAULT_MAX_ROWS, lineHeight);
127
+ return { maxHeight: Math.max(minHeight, ceiling), minHeight };
128
+ }
@@ -0,0 +1,24 @@
1
+ export { Feedback, type FeedbackProps } from "./feedback";
2
+ export { type FeedbackContextValue, useFeedback, useFeedbackContext } from "./feedback.context";
3
+ export {
4
+ type CanSubmitFeedbackInput,
5
+ canSubmitFeedback,
6
+ FEEDBACK_DEFAULT_MAX_ROWS,
7
+ FEEDBACK_DEFAULT_MIN_ROWS,
8
+ FEEDBACK_FIELD_LINE_HEIGHT,
9
+ FEEDBACK_PANEL_RESIZE_MS,
10
+ type FeedbackVariantProps,
11
+ feedbackVariants,
12
+ resolveFeedbackFieldHeightStyle,
13
+ resolveFeedbackFieldMinHeight,
14
+ } from "./feedback.variants";
15
+ export type { FeedbackActionProps } from "./feedback-action";
16
+ export type { FeedbackCancelProps } from "./feedback-cancel";
17
+ export type { FeedbackCloseProps } from "./feedback-close";
18
+ export type { FeedbackContentProps } from "./feedback-content";
19
+ export type { FeedbackFieldProps } from "./feedback-field";
20
+ export type { FeedbackFooterProps } from "./feedback-footer";
21
+ export type { FeedbackPanelProps } from "./feedback-panel";
22
+ export type { FeedbackSubmitProps } from "./feedback-submit";
23
+ export type { FeedbackTitleProps } from "./feedback-title";
24
+ export type { FeedbackTriggerProps } from "./feedback-trigger";
@@ -52,6 +52,18 @@ A form field's layout, and the one place its state is written down. Root plus
52
52
  `accessible={false}`, so the control stays the element a screen reader sees,
53
53
  and the inner detector claims a tap on the box itself rather than firing both.
54
54
  A field holds one control, so a second registration replaces the first.
55
+ - **The label names the control, through the same context.** `Field.Label`
56
+ registers its text with `registerLabel`, the mirror of `registerPress`: a
57
+ control hands its press up, a label hands its name down. A control with no
58
+ text of its own — a [Slider](../slider/AGENTS.md) thumb is a capsule — reads
59
+ `field.label` as its `accessibilityLabel`, so "Volume" is announced rather than
60
+ a bare "40". React Native has no `<label for>`; this is that association, done
61
+ by hand. `resolveFieldLabelText` decides what qualifies and it is strict on
62
+ purpose: a plain string or number, trimmed, and nothing else. A label built
63
+ from elements has no text this side of a render, and guessing at one — an
64
+ icon's key, say — would name the control after the wrong thing. State rather
65
+ than a ref for the same reason as the press: the control renders differently
66
+ for it, and the label registers in an effect, so it costs one render on mount.
55
67
  - **A data-attribute class would also leave `bun test`.** Even for a part styling
56
68
  itself, `data-invalid:text-destructive` moves the decision from `field.variants.ts`
57
69
  into uniwind's runtime matcher, where no unit test can see it. The parts style
@@ -91,6 +103,13 @@ A form field's layout, and the one place its state is written down. Root plus
91
103
  - **`Field.Group` inserts no dividers**, unlike [`ListGroup`](../list-group/AGENTS.md). A list of rows
92
104
  without lines is a wall of text; fields are already held apart by whitespace,
93
105
  and a rule between every one is noise.
106
+ - **`Field.Label` is not built on [`Label`](../label/AGENTS.md)**, the standalone
107
+ primitive. `Label` narrows `children` from `Text`'s and reports
108
+ `accessibilityState.disabled`, so wrapping it would change this part's public
109
+ surface. The two colour and fade identically instead, and
110
+ `label.variants.test.ts` holds them to it against `fieldVariants`' label slot
111
+ and `resolveFieldTextColor`. A required field uses `Label` directly, since
112
+ `Field.Label` has no `isRequired`.
94
113
  - **There is no `Field.Title`.** On the web it exists because a `<div>` is not a
95
114
  `<label>` — label-styled text with nothing to point `htmlFor` at. React Native
96
115
  has neither element nor association, so it and `Field.Label` would render the
@@ -1,8 +1,8 @@
1
- import type { ReactElement } from "react";
1
+ import { type ReactElement, useEffect } from "react";
2
2
  import { Text } from "../text";
3
3
  import { useFieldContext } from "./field.context";
4
4
  import type { FieldTextProps } from "./field.types";
5
- import { fieldVariants, resolveFieldTextColor } from "./field.variants";
5
+ import { fieldVariants, resolveFieldLabelText, resolveFieldTextColor } from "./field.variants";
6
6
 
7
7
  /**
8
8
  * The name of the control this field holds.
@@ -15,19 +15,33 @@ import { fieldVariants, resolveFieldTextColor } from "./field.variants";
15
15
  * There is no `htmlFor`. React Native has no `<label>` element and no
16
16
  * label-for-control association, which is also why there is no `Field.Title`:
17
17
  * on the web that part exists only because a `<div>` is not a `<label>`, and
18
- * here both would be the same `Text`.
18
+ * here both would be the same `Text`. **The association is made by hand
19
+ * instead**: a string label hands its text to the field's context, and a control
20
+ * inside with no text of its own — a `Slider.Thumb` — reads it as its accessible
21
+ * name. `resolveFieldLabelText` decides what qualifies.
19
22
  */
20
- export function FieldLabel({ className, color, ...props }: FieldTextProps): ReactElement {
23
+ export function FieldLabel({ className, color, children, ...props }: FieldTextProps): ReactElement {
21
24
  const field = useFieldContext();
22
25
  const isInvalid = field?.isInvalid ?? false;
23
26
  const isDisabled = field?.isDisabled ?? false;
24
27
 
28
+ const registerLabel = field?.registerLabel;
29
+ const labelText = resolveFieldLabelText(children);
30
+
31
+ useEffect(() => {
32
+ if (!registerLabel) return;
33
+ registerLabel(labelText);
34
+ return () => registerLabel(null);
35
+ }, [labelText, registerLabel]);
36
+
25
37
  return (
26
38
  <Text.Label
27
39
  className={fieldVariants({ isDisabled, isInvalid }).label({ className })}
28
40
  color={color ?? resolveFieldTextColor("label", isInvalid)}
29
41
  {...props}
30
- />
42
+ >
43
+ {children}
44
+ </Text.Label>
31
45
  );
32
46
  }
33
47
  FieldLabel.displayName = "DelacourUI.Field.Label";
@@ -17,6 +17,23 @@ export type FieldContextValue = {
17
17
  * second registration replaces the first.
18
18
  */
19
19
  registerPress: (press: (() => void) | null) => void;
20
+ /**
21
+ * The text of the `Field.Label` inside, or `null` while there is none.
22
+ *
23
+ * What a control reads for its accessible name when it holds no text of its
24
+ * own — a `Slider.Thumb` is a capsule, and a `Switch` is a pill. React Native
25
+ * has no `<label for>`; this is that association, carried by the same context
26
+ * the state cascades down.
27
+ */
28
+ label: string | null;
29
+ /**
30
+ * A `Field.Label` offers its text here. Pass `null` on unmount.
31
+ *
32
+ * The counterpart of {@link FieldContextValue.registerPress} going the same
33
+ * way: a control hands its press up, a label hands its name down. A field
34
+ * holds one label, so a second registration replaces the first.
35
+ */
36
+ registerLabel: (label: string | null) => void;
20
37
  };
21
38
 
22
39
  const FieldContext = createContext<FieldContextValue | null>(null);
@@ -37,9 +37,15 @@ function FieldRoot({
37
37
  // The updater form would call `press` instead of storing it.
38
38
  const registerPress = useCallback((press: (() => void) | null) => setControlPress(() => press), []);
39
39
 
40
+ // The label's text, for a control inside to read as its accessible name.
41
+ // State for the same reason as the press: a control renders differently for
42
+ // it, and the label registers in an effect, so it costs one render on mount.
43
+ const [label, setLabel] = useState<string | null>(null);
44
+ const registerLabel = useCallback((next: string | null) => setLabel(next), []);
45
+
40
46
  const context = useMemo<FieldContextValue>(
41
- () => ({ isDisabled, isInvalid, orientation, registerPress }),
42
- [isDisabled, isInvalid, orientation, registerPress]
47
+ () => ({ isDisabled, isInvalid, label, orientation, registerLabel, registerPress }),
48
+ [isDisabled, isInvalid, label, orientation, registerLabel, registerPress]
43
49
  );
44
50
 
45
51
  const rootClassName = fieldVariants({ isDisabled, isInvalid, orientation }).root({ className });
@@ -86,6 +92,13 @@ function FieldRoot({
86
92
  * A control's own prop still wins, so one field inside an invalid group can opt
87
93
  * out with `<Input isInvalid={false} />`.
88
94
  *
95
+ * **The label names the control, too.** `Field.Label` hands its text down the
96
+ * same context, so a control with no text of its own — a `Slider.Thumb`, a
97
+ * `Switch` — reads "Volume" to a screen reader rather than a bare number. React
98
+ * Native has no `<label for>`; this is that association. Only a plain string or
99
+ * number label is lent, since a label built from elements has no text to read
100
+ * before it renders.
101
+ *
89
102
  * **The whole row drives the control**, once one offers a press through the same
90
103
  * context — so tapping "Accept the terms", or the description under it, ticks
91
104
  * the `Checkbox` beside it. A checkbox in a form is a small square next to a
@@ -7,6 +7,7 @@ import {
7
7
  type FieldTextPart,
8
8
  fieldVariants,
9
9
  resolveFieldInteractive,
10
+ resolveFieldLabelText,
10
11
  resolveFieldTextColor,
11
12
  } from "./field.variants";
12
13
 
@@ -214,3 +215,26 @@ describe("resolveFieldInteractive", () => {
214
215
  expect(resolveFieldInteractive(() => undefined)).toBe(true);
215
216
  });
216
217
  });
218
+
219
+ describe("resolveFieldLabelText", () => {
220
+ test("hands a string label through, trimmed", () => {
221
+ expect(resolveFieldLabelText("Volume")).toBe("Volume");
222
+ expect(resolveFieldLabelText(" Volume\n")).toBe("Volume");
223
+ });
224
+
225
+ test("reads a number the way a screen reader would", () => {
226
+ expect(resolveFieldLabelText(42)).toBe("42");
227
+ });
228
+
229
+ // A label made of elements has no text this side of a render, and an empty
230
+ // string is not a name — both leave the control to name itself.
231
+ test("offers nothing for an element, an empty string, or no children at all", () => {
232
+ expect(resolveFieldLabelText("")).toBeNull();
233
+ expect(resolveFieldLabelText(" ")).toBeNull();
234
+ expect(resolveFieldLabelText(undefined)).toBeNull();
235
+ expect(resolveFieldLabelText(null)).toBeNull();
236
+ expect(resolveFieldLabelText(false)).toBeNull();
237
+ expect(resolveFieldLabelText({ type: "Text", props: {} })).toBeNull();
238
+ expect(resolveFieldLabelText(["a", "b"])).toBeNull();
239
+ });
240
+ });
@@ -133,4 +133,24 @@ export function resolveFieldInteractive(press: (() => void) | null): press is ()
133
133
  return press !== null;
134
134
  }
135
135
 
136
+ /**
137
+ * The text a `Field.Label` can lend to the control beside it as its accessible
138
+ * name, or `null` when it has none to lend.
139
+ *
140
+ * Only a bare string or number qualifies. A label built from elements has no
141
+ * text this side of a render, and guessing at one would name a control after
142
+ * the wrong thing — an icon's key, say. Whitespace is trimmed and an empty
143
+ * string is an absence, because a screen reader handed `""` reads nothing at
144
+ * all, which is worse than reading the value alone.
145
+ *
146
+ * React Native has no `<label for>`; this is the association, done by hand
147
+ * through the field's context. Pure, so `bun test` reaches it. See AGENTS.md.
148
+ */
149
+ export function resolveFieldLabelText(children: unknown): string | null {
150
+ if (typeof children === "number") return String(children);
151
+ if (typeof children !== "string") return null;
152
+ const text = children.trim();
153
+ return text === "" ? null : text;
154
+ }
155
+
136
156
  export type FieldVariantProps = VariantProps<typeof fieldVariants>;
@@ -11,6 +11,7 @@ export {
11
11
  type FieldVariantProps,
12
12
  fieldVariants,
13
13
  resolveFieldInteractive,
14
+ resolveFieldLabelText,
14
15
  resolveFieldTextColor,
15
16
  } from "./field.variants";
16
17
  export type { FieldContentProps } from "./field-content";
@@ -0,0 +1,92 @@
1
+ # Item
2
+
3
+ A row of media, text and actions, for lists and settings. Compound root plus
4
+ `Item.Media`, `Item.Content`, `Item.Title`, `Item.Description`, `Item.Actions`,
5
+ `Item.Header` and `Item.Footer`, and two layout parts, `Item.Group` and
6
+ `Item.Separator`.
7
+
8
+ `import { Item } from "@delacour/react-native-ui/item";`
9
+
10
+ ## Files
11
+
12
+ | File | What it holds |
13
+ | --- | --- |
14
+ | `index.ts` | → `@delacour/react-native-ui/item` |
15
+ | `item.tsx` | Root + the `Object.assign` compound surface, and the bare-text wrap it owns |
16
+ | `item-media.tsx` | `Item.Media` |
17
+ | `item-content.tsx` | `Item.Content` |
18
+ | `item-title.tsx` | `Item.Title` |
19
+ | `item-description.tsx` | `Item.Description` |
20
+ | `item-actions.tsx` | `Item.Actions` |
21
+ | `item-header.tsx` | `Item.Header` |
22
+ | `item-footer.tsx` | `Item.Footer` |
23
+ | `item-group.tsx` | `Item.Group` |
24
+ | `item-separator.tsx` | `Item.Separator` |
25
+ | `item.context.tsx` | `ItemProvider`, `useItem()`, `useItemContext()`, `useItemPart()` |
26
+ | `item.types.ts` | Prop types shared by two or more parts |
27
+ | `item.variants.ts` | Pure `tv()` slots and the four resolvers, no RN imports |
28
+ | `item.variants.test.ts` | |
29
+
30
+ ## Design
31
+
32
+ - **Axes**: `variant` — `default`, `outline`, `muted`; `size` — `sm`, `md`,
33
+ `lg`; `orientation` — `horizontal`, `vertical`. States: `isDisabled`,
34
+ `isSelected`, and `busy` inherited from `Pressable`. `Item.Media` has its own
35
+ `variant` — `default`, `icon`, `image`.
36
+ - **It drops into a `ListGroup` as a row, and that is the point of it.** An item
37
+ reads the group's context through the `list-group.context` leaf (rule 3 —
38
+ never `../list-group`) and changes three things when it finds one:
39
+ - **The surface becomes `grouped`.** `resolveItemSurface` ignores the variant:
40
+ the group draws the border, fill and corner, and a row repeating any of them
41
+ would draw a card inside a card. `surface` rather than `variant` is the
42
+ `tv()` axis for that reason.
43
+ - **Size defaults to the group's.** `resolveItemSize` — own, else group, else
44
+ `md`. The two components share the `sm`/`md`/`lg` scale on purpose.
45
+ - **Press feedback defaults to `fade`**, not `scale` — a full-bleed row that
46
+ scales reads as the whole card flexing. `resolveItemFeedback`.
47
+ - **Row metrics are `ListGroup`'s, number for number.** The group insets each
48
+ divider by its own row padding, so an item padded differently would sit with
49
+ its text and its divider out of line. The tests assert the item's `min-h`,
50
+ `gap`, `px` and `py` against `listGroupVariants().item()` at every size, and
51
+ the bare media icon against the group's `prefixIcon` — change one without the
52
+ other and `bun test` fails by name.
53
+ - **Pressable only with a handler.** `resolveItemRender` returns one of two:
54
+ `pressable` (a handler — a `Pressable`, role `button`, children merged into
55
+ one accessible element; disabled, it keeps the role, reports `disabled` and
56
+ dims through `opacity-50`, which `Pressable` composes with its press
57
+ animation) or `static` (no handler — a plain `Animated.View` with no role, so
58
+ a static row never announces itself as a button). The plain branch is
59
+ `Animated.View` rather than `View` so the `ref` type is one type.
60
+ - **A pressable item must not hold another control.** iOS merges an accessible
61
+ element's children into it, so a `Button` in the actions of a pressable item
62
+ is unreachable by VoiceOver. Either leave the item static and let the actions
63
+ be the controls, or make the row the control: a switch row takes
64
+ `accessibilityRole="switch"` and `accessibilityState={{ checked }}` on the
65
+ item, toggles from its `onPress`, and renders the `Switch` inside for show.
66
+ - **`isSelected` lays `bg-accent` over any surface**, the muted fill included —
67
+ declared after `surface` in the `tv()` so tailwind-merge drops the losing
68
+ `bg-*`. It also sets `accessibilityState.selected`. What marks the choice (a
69
+ check, a radio) is composed into `Item.Actions` by the caller.
70
+ - **Icons are composed, never passed as props.** `Item.Media` and
71
+ `Item.Actions` each wrap their subtree in an `IconDefaultsProvider`. Media
72
+ gets the foreground token at the size its variant calls for — a bare icon
73
+ matches a `ListGroup` prefix, an `icon` tile's glyph sits two steps smaller
74
+ inside it. Actions gets `muted-foreground` a step lower still, so a chevron
75
+ reads as a hint. A `Button` in the actions publishes its own defaults and is
76
+ unaffected.
77
+ - **`Header` and `Footer` work on both orientations.** The horizontal root is
78
+ `flex-wrap` and the strips are `w-full`, so a strip takes a line of its own
79
+ above or below media, text and actions without the item having to become a
80
+ column.
81
+ - **`Content` only flexes along a row.** Vertical, it is `self-stretch`: a
82
+ `flex-1` column in an auto-height parent collapses to zero in Yoga.
83
+ - **`Item.Group` is spaced, not divided.** It is for standalone items — outlined
84
+ cards, a carousel — announced with `accessibilityRole="list"`. Rows sharing
85
+ one card with dividers belong in a `ListGroup`, which already does both.
86
+ `Item.Separator` is a `Separator` under the item's name, for the occasional
87
+ hand-placed rule in a group.
88
+ - **String children** are wrapped in a `Content` around a `Title`, consecutive
89
+ strings collapsing into one — the same rule, and the same reason, as
90
+ [`ListGroup`](../list-group/AGENTS.md).
91
+ - **Text colour goes on the text.** No slot but `title` and `description` holds
92
+ a `text-*` colour; the tests assert the root never does.
@@ -0,0 +1,25 @@
1
+ export { Item, type ItemProps } from "./item";
2
+ export { type ItemContextValue, ItemProvider, useItem, useItemContext } from "./item.context";
3
+ export type { ItemSlotProps, ItemTextProps } from "./item.types";
4
+ export {
5
+ ITEM_ACTIONS_ICON_TOKEN,
6
+ ITEM_MEDIA_ICON_TOKEN,
7
+ ITEM_MEDIA_VARIANTS,
8
+ ITEM_ORIENTATIONS,
9
+ ITEM_SIZES,
10
+ ITEM_VARIANTS,
11
+ type ItemMediaVariant,
12
+ type ItemOrientation,
13
+ type ItemRender,
14
+ type ItemSize,
15
+ type ItemSurface,
16
+ type ItemVariant,
17
+ type ItemVariantProps,
18
+ itemVariants,
19
+ resolveItemFeedback,
20
+ resolveItemRender,
21
+ resolveItemSize,
22
+ resolveItemSurface,
23
+ } from "./item.variants";
24
+ export type { ItemGroupProps } from "./item-group";
25
+ export type { ItemMediaProps } from "./item-media";