@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,314 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { declaredTokens } from "../../styles/theme-tokens.test";
3
+ import { buttonVariants } from "../button/button.variants";
4
+ import {
5
+ PROGRESS_BUTTON_DEFAULT_AUTO_RESET_MS,
6
+ PROGRESS_BUTTON_DEFAULT_HINT,
7
+ PROGRESS_BUTTON_DEFAULT_HOLD_MS,
8
+ PROGRESS_BUTTON_FILL_FOREGROUND_TOKEN,
9
+ PROGRESS_BUTTON_FILL_TOKEN,
10
+ PROGRESS_BUTTON_LABEL_TOKEN,
11
+ PROGRESS_BUTTON_MIN_HOLD_MS,
12
+ PROGRESS_BUTTON_REDUCED_MOTION_STEPS,
13
+ PROGRESS_BUTTON_SHAPES,
14
+ PROGRESS_BUTTON_SIZES,
15
+ PROGRESS_BUTTON_VARIANTS,
16
+ progressButtonVariants,
17
+ resolveAutoResetDelay,
18
+ resolveHoldDuration,
19
+ resolveProgressButtonAccessibilityState,
20
+ resolveRemainingDuration,
21
+ resolveSteppedProgress,
22
+ } from "./progress-button.variants";
23
+
24
+ const LIGHT = declaredTokens("light");
25
+ const DARK = declaredTokens("dark");
26
+
27
+ /** Every theme token a class string paints with, with any `/alpha` suffix dropped. */
28
+ function colorTokens(cls: string): string[] {
29
+ const tokens: string[] = [];
30
+ for (const [, utility, token] of cls.matchAll(/\b(bg|text)-([a-z][\w-]*)(?:\/\d+)?\b/g)) {
31
+ if (utility === "text" && token.startsWith("button-")) continue;
32
+ if (utility === "text" && (token === "center" || token === "left" || token === "right")) continue;
33
+ tokens.push(token);
34
+ }
35
+ return tokens;
36
+ }
37
+
38
+ const SLOT_NAMES = ["root", "content", "fill", "fillContent", "label", "fillLabel", "done", "icon"] as const;
39
+
40
+ /** Every colour token any slot names, across the whole variant × size × shape matrix. */
41
+ function everySlotToken(): string[] {
42
+ const tokens = new Set<string>();
43
+ const combos = PROGRESS_BUTTON_VARIANTS.flatMap((variant) =>
44
+ PROGRESS_BUTTON_SIZES.flatMap((size) => PROGRESS_BUTTON_SHAPES.map((shape) => ({ shape, size, variant })))
45
+ );
46
+ for (const combo of combos) {
47
+ const slots = progressButtonVariants(combo);
48
+ for (const name of SLOT_NAMES) {
49
+ for (const token of colorTokens(slots[name]() ?? "")) tokens.add(token);
50
+ }
51
+ }
52
+ return [...tokens];
53
+ }
54
+
55
+ describe("resolveHoldDuration", () => {
56
+ test("defaults to two seconds", () => {
57
+ expect(PROGRESS_BUTTON_DEFAULT_HOLD_MS).toBe(2000);
58
+ expect(resolveHoldDuration()).toBe(2000);
59
+ expect(resolveHoldDuration(undefined)).toBe(2000);
60
+ });
61
+
62
+ test("passes a sensible value through", () => {
63
+ expect(resolveHoldDuration(1500)).toBe(1500);
64
+ expect(resolveHoldDuration(200)).toBe(200);
65
+ });
66
+
67
+ test("floors a hold too short to be deliberate", () => {
68
+ expect(PROGRESS_BUTTON_MIN_HOLD_MS).toBe(200);
69
+ expect(resolveHoldDuration(50)).toBe(200);
70
+ expect(resolveHoldDuration(0)).toBe(200);
71
+ expect(resolveHoldDuration(-1000)).toBe(200);
72
+ });
73
+
74
+ test("a non-finite value gives the default", () => {
75
+ expect(resolveHoldDuration(Number.NaN)).toBe(2000);
76
+ expect(resolveHoldDuration(Number.POSITIVE_INFINITY)).toBe(2000);
77
+ expect(resolveHoldDuration(Number.NEGATIVE_INFINITY)).toBe(2000);
78
+ });
79
+ });
80
+
81
+ describe("resolveRemainingDuration", () => {
82
+ test("forward from empty is the whole hold", () => {
83
+ expect(resolveRemainingDuration({ direction: "forward", holdDuration: 2000, progress: 0 })).toBe(2000);
84
+ });
85
+
86
+ test("forward resumes from where the fill is", () => {
87
+ expect(resolveRemainingDuration({ direction: "forward", holdDuration: 2000, progress: 0.25 })).toBe(1500);
88
+ expect(resolveRemainingDuration({ direction: "forward", holdDuration: 2000, progress: 1 })).toBe(0);
89
+ });
90
+
91
+ test("reverse plays back at the same rate", () => {
92
+ expect(resolveRemainingDuration({ direction: "reverse", holdDuration: 2000, progress: 0.9 })).toBeCloseTo(1800);
93
+ expect(resolveRemainingDuration({ direction: "reverse", holdDuration: 2000, progress: 0 })).toBe(0);
94
+ });
95
+
96
+ test("forward and reverse at one point sum to the hold", () => {
97
+ for (const progress of [0, 0.1, 0.5, 0.73, 1]) {
98
+ const forward = resolveRemainingDuration({ direction: "forward", holdDuration: 1200, progress });
99
+ const reverse = resolveRemainingDuration({ direction: "reverse", holdDuration: 1200, progress });
100
+ expect(forward + reverse).toBeCloseTo(1200);
101
+ }
102
+ });
103
+
104
+ test("clamps progress outside 0..1 and treats a non-finite one as empty", () => {
105
+ expect(resolveRemainingDuration({ direction: "forward", holdDuration: 2000, progress: -0.5 })).toBe(2000);
106
+ expect(resolveRemainingDuration({ direction: "forward", holdDuration: 2000, progress: 1.5 })).toBe(0);
107
+ expect(resolveRemainingDuration({ direction: "reverse", holdDuration: 2000, progress: Number.NaN })).toBe(0);
108
+ });
109
+ });
110
+
111
+ describe("resolveSteppedProgress", () => {
112
+ test("steps in fifths by default", () => {
113
+ expect(PROGRESS_BUTTON_REDUCED_MOTION_STEPS).toBe(5);
114
+ expect(resolveSteppedProgress(0)).toBe(0);
115
+ expect(resolveSteppedProgress(0.19)).toBe(0);
116
+ expect(resolveSteppedProgress(0.2)).toBe(0.2);
117
+ expect(resolveSteppedProgress(0.39)).toBe(0.2);
118
+ expect(resolveSteppedProgress(0.5)).toBe(0.4);
119
+ expect(resolveSteppedProgress(0.99)).toBe(0.8);
120
+ expect(resolveSteppedProgress(1)).toBe(1);
121
+ });
122
+
123
+ test("takes another step count", () => {
124
+ expect(resolveSteppedProgress(0.6, 2)).toBe(0.5);
125
+ expect(resolveSteppedProgress(0.3, 4)).toBe(0.25);
126
+ });
127
+
128
+ test("clamps, and never shows more than the hold has earned", () => {
129
+ expect(resolveSteppedProgress(-1)).toBe(0);
130
+ expect(resolveSteppedProgress(2)).toBe(1);
131
+ expect(resolveSteppedProgress(Number.NaN)).toBe(0);
132
+ for (let p = 0; p <= 1; p += 0.01) {
133
+ expect(resolveSteppedProgress(p)).toBeLessThanOrEqual(p + 1e-9);
134
+ }
135
+ });
136
+
137
+ test("a step count below one is treated as one", () => {
138
+ expect(resolveSteppedProgress(0.5, 0)).toBe(0);
139
+ expect(resolveSteppedProgress(1, 0)).toBe(1);
140
+ });
141
+ });
142
+
143
+ describe("resolveAutoResetDelay", () => {
144
+ test("defaults to a second", () => {
145
+ expect(PROGRESS_BUTTON_DEFAULT_AUTO_RESET_MS).toBe(1000);
146
+ expect(resolveAutoResetDelay()).toBe(1000);
147
+ });
148
+
149
+ test("passes a value through and floors a negative one at zero", () => {
150
+ expect(resolveAutoResetDelay(2500)).toBe(2500);
151
+ expect(resolveAutoResetDelay(0)).toBe(0);
152
+ expect(resolveAutoResetDelay(-5)).toBe(0);
153
+ });
154
+
155
+ test("a non-finite value gives the default", () => {
156
+ expect(resolveAutoResetDelay(Number.NaN)).toBe(1000);
157
+ expect(resolveAutoResetDelay(Number.POSITIVE_INFINITY)).toBe(1000);
158
+ });
159
+ });
160
+
161
+ describe("resolveProgressButtonAccessibilityState", () => {
162
+ test("announces disabled and completed as checked", () => {
163
+ expect(resolveProgressButtonAccessibilityState({ isCompleted: false, isDisabled: false })).toEqual({
164
+ checked: false,
165
+ disabled: false,
166
+ });
167
+ expect(resolveProgressButtonAccessibilityState({ isCompleted: true, isDisabled: true })).toEqual({
168
+ checked: true,
169
+ disabled: true,
170
+ });
171
+ });
172
+
173
+ test("the default hint says the button has to be held", () => {
174
+ expect(PROGRESS_BUTTON_DEFAULT_HINT.toLowerCase()).toContain("hold");
175
+ });
176
+ });
177
+
178
+ describe("progressButtonVariants — axes", () => {
179
+ test("exposes the variants, sizes and shapes the API names", () => {
180
+ expect([...PROGRESS_BUTTON_VARIANTS]).toEqual(["primary", "secondary", "destructive", "success"]);
181
+ expect([...PROGRESS_BUTTON_SIZES]).toEqual(["sm", "md", "lg"]);
182
+ expect([...PROGRESS_BUTTON_SHAPES]).toEqual(["pill", "rounded"]);
183
+ });
184
+
185
+ test("defaults to primary, md, pill", () => {
186
+ const root = progressButtonVariants().root();
187
+ expect(root).toContain("h-button-md");
188
+ expect(root).toContain("rounded-button-md");
189
+ expect(progressButtonVariants().fill()).toContain("bg-primary");
190
+ });
191
+
192
+ test("every variant rests on the same surface", () => {
193
+ for (const variant of PROGRESS_BUTTON_VARIANTS) {
194
+ const root = progressButtonVariants({ variant }).root();
195
+ expect(root).toContain("bg-secondary");
196
+ expect(colorTokens(root)).toEqual(["secondary"]);
197
+ }
198
+ });
199
+
200
+ test("the variant colour is carried by the fill and the labels", () => {
201
+ for (const variant of PROGRESS_BUTTON_VARIANTS) {
202
+ const slots = progressButtonVariants({ variant });
203
+ expect(slots.fill()).toContain(`bg-${PROGRESS_BUTTON_FILL_TOKEN[variant]}`);
204
+ expect(slots.label()).toContain(`text-${PROGRESS_BUTTON_LABEL_TOKEN[variant]}`);
205
+ expect(slots.fillLabel()).toContain(`text-${PROGRESS_BUTTON_FILL_FOREGROUND_TOKEN[variant]}`);
206
+ }
207
+ });
208
+
209
+ test("secondary fills with the foreground and draws its inner label on the background", () => {
210
+ const slots = progressButtonVariants({ variant: "secondary" });
211
+ expect(slots.fill()).toContain("bg-foreground");
212
+ expect(slots.label()).toContain("text-secondary-foreground");
213
+ expect(slots.fillLabel()).toContain("text-background");
214
+ });
215
+
216
+ test("the two label copies differ only in colour", () => {
217
+ for (const variant of PROGRESS_BUTTON_VARIANTS) {
218
+ for (const size of PROGRESS_BUTTON_SIZES) {
219
+ const slots = progressButtonVariants({ size, variant });
220
+ const strip = (cls: string) =>
221
+ cls
222
+ .split(/\s+/)
223
+ .filter((c) => colorTokens(c).length === 0)
224
+ .sort();
225
+ expect(strip(slots.label())).toEqual(strip(slots.fillLabel()));
226
+ }
227
+ }
228
+ });
229
+
230
+ test("the fill is clipped and anchored to the leading edge", () => {
231
+ const fill = progressButtonVariants().fill();
232
+ for (const cls of ["absolute", "overflow-hidden", "inset-y-0", "start-0"]) {
233
+ expect(fill).toContain(cls);
234
+ }
235
+ });
236
+ });
237
+
238
+ describe("progressButtonVariants — the button's box", () => {
239
+ test("height, padding, label step and icon step come off the button's own size tokens", () => {
240
+ for (const size of PROGRESS_BUTTON_SIZES) {
241
+ const ours = progressButtonVariants({ size });
242
+ const button = buttonVariants({ size });
243
+ const buttonRoot = button.root().split(/\s+/);
244
+ const height = buttonRoot.find((c) => c.startsWith("h-button-"));
245
+ const padding = buttonRoot.find((c) => c.startsWith("px-"));
246
+ expect(height).toBeDefined();
247
+ expect(padding).toBeDefined();
248
+ expect(ours.root()).toContain(height as string);
249
+ expect(ours.root()).toContain(padding as string);
250
+ expect(ours.fillContent()).toContain(padding as string);
251
+ expect(ours.label()).toContain(`text-button-${size}`);
252
+ expect(ours.icon()).toContain(`size-icon-${size}`);
253
+ }
254
+ });
255
+
256
+ test("a pill takes the button's corner, rounded takes the card's", () => {
257
+ for (const size of PROGRESS_BUTTON_SIZES) {
258
+ expect(progressButtonVariants({ shape: "pill", size }).root()).toContain(`rounded-button-${size}`);
259
+ const rounded = progressButtonVariants({ shape: "rounded", size }).root();
260
+ expect(rounded).toContain("rounded-lg");
261
+ expect(rounded).not.toContain("rounded-button-");
262
+ }
263
+ });
264
+
265
+ test("full width stretches", () => {
266
+ const root = progressButtonVariants({ isFullWidth: true }).root();
267
+ expect(root).toContain("w-full");
268
+ expect(root).toContain("self-stretch");
269
+ expect(progressButtonVariants({ isFullWidth: false }).root()).not.toContain("w-full");
270
+ });
271
+
272
+ test("disabled fades", () => {
273
+ expect(progressButtonVariants({ isDisabled: true }).root()).toContain("opacity-50");
274
+ expect(progressButtonVariants({ isDisabled: false }).root()).not.toContain("opacity-50");
275
+ });
276
+
277
+ test("a caller's className merges over the root's own", () => {
278
+ const root = progressButtonVariants().root({ className: "rounded-none" });
279
+ expect(root).toContain("rounded-none");
280
+ expect(root).not.toContain("rounded-button-md");
281
+ });
282
+ });
283
+
284
+ describe("progressButtonVariants — theme tokens", () => {
285
+ test("the theme reader found both variants, and the slots name some tokens", () => {
286
+ expect(LIGHT.size).toBeGreaterThan(0);
287
+ expect(DARK.size).toBeGreaterThan(0);
288
+ expect(everySlotToken().length).toBeGreaterThan(4);
289
+ });
290
+
291
+ test("every token named in every slot, at every axis, exists in both themes", () => {
292
+ const missing = everySlotToken().filter((token) => !(LIGHT.has(token) && DARK.has(token)));
293
+ expect(missing).toEqual([]);
294
+ });
295
+
296
+ test("every token in the colour tables exists in both themes", () => {
297
+ for (const table of [
298
+ PROGRESS_BUTTON_FILL_TOKEN,
299
+ PROGRESS_BUTTON_LABEL_TOKEN,
300
+ PROGRESS_BUTTON_FILL_FOREGROUND_TOKEN,
301
+ ]) {
302
+ for (const variant of PROGRESS_BUTTON_VARIANTS) {
303
+ expect(LIGHT.has(table[variant])).toBe(true);
304
+ expect(DARK.has(table[variant])).toBe(true);
305
+ }
306
+ }
307
+ });
308
+
309
+ test("the fill and its label never share a token", () => {
310
+ for (const variant of PROGRESS_BUTTON_VARIANTS) {
311
+ expect(PROGRESS_BUTTON_FILL_TOKEN[variant]).not.toBe(PROGRESS_BUTTON_FILL_FOREGROUND_TOKEN[variant]);
312
+ }
313
+ });
314
+ });
@@ -0,0 +1,236 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+
4
+ export const PROGRESS_BUTTON_VARIANTS = ["primary", "secondary", "destructive", "success"] as const;
5
+
6
+ /** The button's label sizes. A hold needs a label, so there is no square `icon-*` step. */
7
+ export const PROGRESS_BUTTON_SIZES = ["sm", "md", "lg"] as const;
8
+
9
+ /** `pill` takes the button's capsule corner; `rounded` takes the card's `rounded-lg`. */
10
+ export const PROGRESS_BUTTON_SHAPES = ["pill", "rounded"] as const;
11
+
12
+ export type ProgressButtonVariant = (typeof PROGRESS_BUTTON_VARIANTS)[number];
13
+ export type ProgressButtonSize = (typeof PROGRESS_BUTTON_SIZES)[number];
14
+ export type ProgressButtonShape = (typeof PROGRESS_BUTTON_SHAPES)[number];
15
+
16
+ /** How long a hold takes when nothing says otherwise. */
17
+ export const PROGRESS_BUTTON_DEFAULT_HOLD_MS = 2000;
18
+
19
+ /** The shortest hold accepted. Anything quicker is a long tap, not a decision. */
20
+ export const PROGRESS_BUTTON_MIN_HOLD_MS = 200;
21
+
22
+ /** How long a completed button waits before `isAutoReset` rewinds it. */
23
+ export const PROGRESS_BUTTON_DEFAULT_AUTO_RESET_MS = 1000;
24
+
25
+ /** How long a reset, or a completion set from outside, takes to travel the whole width. */
26
+ export const PROGRESS_BUTTON_TRAVEL_MS = 400;
27
+
28
+ /** How long the labels and the done mark take to cross-fade. */
29
+ export const PROGRESS_BUTTON_CROSSFADE_MS = 200;
30
+
31
+ /** How far, in points, a resting finger may drift before the hold lets go. */
32
+ export const PROGRESS_BUTTON_MAX_DRIFT = 16;
33
+
34
+ /** How many discrete steps the fill takes under reduced motion. */
35
+ export const PROGRESS_BUTTON_REDUCED_MOTION_STEPS = 5;
36
+
37
+ /** What VoiceOver and TalkBack read after the label when no `accessibilityHint` is given. */
38
+ export const PROGRESS_BUTTON_DEFAULT_HINT = "Press and hold to confirm";
39
+
40
+ /**
41
+ * Theme token each variant fills with.
42
+ *
43
+ * `secondary` fills with the foreground: its resting surface already is
44
+ * `bg-secondary`, and a fill the same colour as the surface under it would not
45
+ * be seen at all.
46
+ */
47
+ export const PROGRESS_BUTTON_FILL_TOKEN: Record<ProgressButtonVariant, string> = {
48
+ primary: "primary",
49
+ secondary: "foreground",
50
+ destructive: "destructive",
51
+ success: "success",
52
+ };
53
+
54
+ /** Theme token the resting label, and a composed icon on the surface, are drawn in. */
55
+ export const PROGRESS_BUTTON_LABEL_TOKEN: Record<ProgressButtonVariant, string> = {
56
+ primary: "primary",
57
+ secondary: "secondary-foreground",
58
+ destructive: "destructive",
59
+ success: "success",
60
+ };
61
+
62
+ /**
63
+ * Theme token the label copy inside the fill, its icons and the done mark are drawn in.
64
+ *
65
+ * `secondary` draws on `background`, the token `foreground` is the foreground
66
+ * *of* — `secondary-foreground` on a `foreground` fill would be the same colour
67
+ * twice.
68
+ */
69
+ export const PROGRESS_BUTTON_FILL_FOREGROUND_TOKEN: Record<ProgressButtonVariant, string> = {
70
+ primary: "primary-foreground",
71
+ secondary: "background",
72
+ destructive: "destructive-foreground",
73
+ success: "success-foreground",
74
+ };
75
+
76
+ /**
77
+ * Styling for every part of a progress button.
78
+ *
79
+ * The box is a button's box: `h-button-*`, the button's `px-*` and, as a pill,
80
+ * `rounded-button-*` — the same tokens `buttonVariants` reads, never new ones,
81
+ * and a test pins them equal. Every variant rests on one surface,
82
+ * `bg-secondary`, and carries its colour in the label and the fill.
83
+ *
84
+ * The label is drawn twice. `label` sits on the surface; `fillLabel` sits inside
85
+ * the clipped fill, laid out in `fillContent` at the button's measured width so
86
+ * both copies wrap the same and the wipe's edge cuts through a glyph rather
87
+ * than between two different layouts. The two differ only in colour — a test
88
+ * asserts it.
89
+ *
90
+ * Free of React Native imports so it stays unit-testable. See AGENTS.md.
91
+ */
92
+ export const progressButtonVariants = tv({
93
+ slots: {
94
+ root: "relative flex-row items-center justify-center overflow-hidden bg-secondary",
95
+ /** The surface layer: the resting label and anything composed beside it. */
96
+ content: "flex-row items-center justify-center gap-2",
97
+ fill: "absolute inset-y-0 start-0 overflow-hidden",
98
+ /** The fill's inner copy, laid out at the root's measured width. */
99
+ fillContent: "absolute inset-y-0 start-0 flex-row items-center justify-center gap-2",
100
+ label: "text-center font-semibold",
101
+ fillLabel: "text-center font-semibold",
102
+ /** Where the done mark sits: the whole box, on the fill. */
103
+ done: "absolute inset-y-0 start-0 items-center justify-center",
104
+ /** Edge length a composed `Icon`, and the default tick, inherit. */
105
+ icon: "",
106
+ },
107
+ variants: {
108
+ variant: {
109
+ primary: { fill: "bg-primary", label: "text-primary", fillLabel: "text-primary-foreground" },
110
+ secondary: {
111
+ fill: "bg-foreground",
112
+ label: "text-secondary-foreground",
113
+ fillLabel: "text-background",
114
+ },
115
+ destructive: {
116
+ fill: "bg-destructive",
117
+ label: "text-destructive",
118
+ fillLabel: "text-destructive-foreground",
119
+ },
120
+ success: { fill: "bg-success", label: "text-success", fillLabel: "text-success-foreground" },
121
+ },
122
+ // Written out rather than built: Tailwind scans source text, and a class
123
+ // assembled at runtime is never compiled.
124
+ size: {
125
+ sm: {
126
+ root: "h-button-sm px-3",
127
+ content: "gap-1.5",
128
+ fillContent: "gap-1.5 px-3",
129
+ label: "text-button-sm",
130
+ fillLabel: "text-button-sm",
131
+ icon: "size-icon-sm",
132
+ },
133
+ md: {
134
+ root: "h-button-md px-4",
135
+ fillContent: "px-4",
136
+ label: "text-button-md",
137
+ fillLabel: "text-button-md",
138
+ icon: "size-icon-md",
139
+ },
140
+ lg: {
141
+ root: "h-button-lg px-5",
142
+ fillContent: "px-5",
143
+ label: "text-button-lg",
144
+ fillLabel: "text-button-lg",
145
+ icon: "size-icon-lg",
146
+ },
147
+ },
148
+ // The pill's corner depends on the size, so it is a compound cell below.
149
+ shape: { pill: {}, rounded: { root: "rounded-lg" } },
150
+ isFullWidth: { true: { root: "w-full self-stretch" }, false: {} },
151
+ isDisabled: { true: { root: "opacity-50" }, false: {} },
152
+ },
153
+ compoundVariants: [
154
+ { shape: "pill", size: "sm", class: { root: "rounded-button-sm" } },
155
+ { shape: "pill", size: "md", class: { root: "rounded-button-md" } },
156
+ { shape: "pill", size: "lg", class: { root: "rounded-button-lg" } },
157
+ ],
158
+ defaultVariants: {
159
+ variant: "primary",
160
+ size: "md",
161
+ shape: "pill",
162
+ isFullWidth: false,
163
+ isDisabled: false,
164
+ },
165
+ });
166
+
167
+ export type ProgressButtonVariantProps = VariantProps<typeof progressButtonVariants>;
168
+
169
+ /**
170
+ * How long a full hold takes, in milliseconds.
171
+ *
172
+ * Defaults to {@link PROGRESS_BUTTON_DEFAULT_HOLD_MS} and is floored at
173
+ * {@link PROGRESS_BUTTON_MIN_HOLD_MS}. A non-finite value — `NaN` from a
174
+ * division somewhere upstream — takes the default rather than a hold that
175
+ * completes instantly or never.
176
+ */
177
+ export function resolveHoldDuration(ms?: number): number {
178
+ if (ms === undefined || !Number.isFinite(ms)) return PROGRESS_BUTTON_DEFAULT_HOLD_MS;
179
+ return Math.max(PROGRESS_BUTTON_MIN_HOLD_MS, ms);
180
+ }
181
+
182
+ /** How long a completed button waits before rewinding itself. Non-negative. */
183
+ export function resolveAutoResetDelay(ms?: number): number {
184
+ if (ms === undefined || !Number.isFinite(ms)) return PROGRESS_BUTTON_DEFAULT_AUTO_RESET_MS;
185
+ return Math.max(0, ms);
186
+ }
187
+
188
+ /**
189
+ * How long the fill takes to travel from `progress` to its end, at the hold's rate.
190
+ *
191
+ * `forward` is what is left to fill — `holdDuration × (1 − p)` — so a second
192
+ * press resumes rather than restarting. `reverse` is what is filled —
193
+ * `holdDuration × p` — so a release plays the fill back at the same speed it
194
+ * went in, and the two at one point always sum to the whole hold.
195
+ *
196
+ * The worklet that drives the fill restates this inline, because a worklet body
197
+ * must be self-contained; this copy is the one the tests pin.
198
+ */
199
+ export function resolveRemainingDuration({
200
+ holdDuration,
201
+ progress,
202
+ direction,
203
+ }: {
204
+ holdDuration: number;
205
+ progress: number;
206
+ direction: "forward" | "reverse";
207
+ }): number {
208
+ const p = Number.isFinite(progress) ? Math.min(1, Math.max(0, progress)) : 0;
209
+ return holdDuration * (direction === "forward" ? 1 - p : p);
210
+ }
211
+
212
+ /**
213
+ * The fill shown under reduced motion: `progress` rounded down to a step.
214
+ *
215
+ * Down, never to nearest, so the fill never shows more than the hold has
216
+ * earned — a full bar appears only at 1. The animated style restates this
217
+ * inline (worklet bodies must be self-contained); this copy is the tested one.
218
+ */
219
+ export function resolveSteppedProgress(progress: number, steps: number = PROGRESS_BUTTON_REDUCED_MOTION_STEPS): number {
220
+ const p = Number.isFinite(progress) ? Math.min(1, Math.max(0, progress)) : 0;
221
+ if (p >= 1) return 1;
222
+ if (!(steps >= 1)) return 0;
223
+ const count = Math.floor(steps);
224
+ return Math.floor(p * count) / count;
225
+ }
226
+
227
+ /** What a screen reader is told about the button's state. Completed reads as checked. */
228
+ export function resolveProgressButtonAccessibilityState({
229
+ isDisabled,
230
+ isCompleted,
231
+ }: {
232
+ isDisabled: boolean;
233
+ isCompleted: boolean;
234
+ }): { disabled: boolean; checked: boolean } {
235
+ return { checked: isCompleted, disabled: isDisabled };
236
+ }
@@ -15,14 +15,14 @@ everything — a root layout, an `App.tsx`.
15
15
  ## Design
16
16
 
17
17
  - **Five layers, outermost first**: `GestureHandlerRootView` →
18
- `SafeAreaProvider` → `KeyboardProvider` → `<KeyboardStateSync />` beside the
19
- children → `BottomSheetModalProvider` around them. The order is not stylistic.
20
- The gesture root has to be an ancestor native view of every handler a
21
- `Pressable` creates, and its absence is *silent* — no error, no warning,
22
- presses simply stop landing. `KeyboardStateSync` has to be a CHILD of
23
- `KeyboardProvider`, because it calls `useKeyboardContext()`, and it stays a
24
- SIBLING of the modal provider rather than moving inside it: the repair is
25
- global and has to outlive any layer that can remount.
18
+ `SafeAreaProvider` → `KeyboardProvider` → `<KeyboardStateSync />` beside
19
+ `CalmMotionProvider`, which wraps the children. The order is not stylistic. The gesture root has to be an ancestor
20
+ native view of every handler a `Pressable` creates, and its absence is
21
+ *silent* — no error, no warning, presses simply stop landing.
22
+ `KeyboardStateSync` has to be a CHILD of `KeyboardProvider`, because it calls
23
+ `useKeyboardContext()`, and it stays a SIBLING of the children rather than
24
+ wrapping them: the repair is global and has to outlive any layer that can
25
+ remount.
26
26
  - **`initialMetrics` defaults to `initialWindowMetrics`, and that is
27
27
  load-bearing.** `SafeAreaProvider` renders `null` — not unstyled children,
28
28
  *nothing* — until its native view reports the first `onInsetsChange`, so
@@ -44,25 +44,38 @@ everything — a root layout, an `App.tsx`.
44
44
  failure mode, which is the least debuggable outcome in the package. And a prop
45
45
  surface that names the layers changes shape every time a layer is added:
46
46
  `children`, `style` and `initialMetrics` say nothing about what is inside, so a
47
- future `BottomSheetModalProvider` or portal host is an edit to one file rather
48
- than a breaking change. An app that genuinely needs a different stack composes
47
+ future portal host or toast host is an edit to one file rather than a
48
+ breaking change. An app that genuinely needs a different stack composes
49
49
  the providers by hand — they are all public from their own packages, and this
50
50
  is a convenience, not a gate.
51
51
  - **A new layer goes innermost.** Anything that draws above the app — a
52
- bottom-sheet modal provider, a portal host, a toast host — has to sit inside
53
- every provider it reads, so it wraps `{children}` and nothing else moves. A
54
- layer that brings a new native peer is a peer-dependency decision first.
55
- `BottomSheetModalProvider` is the case this rule was written for and now the
56
- case it governs: it reads the gesture root for its pan, the safe area for its
57
- insets and the keyboard values a sheet's footer rides, so it is last.
58
- - **`@gorhom/bottom-sheet` is a required peer because of this component**, on
59
- exactly the argument `react-native-keyboard-controller` already carries. It was
60
- optional while nothing imported it. The recommended root now does, so every app
61
- resolves it, and a flag saying otherwise would only suppress the install
62
- warning that explains the Metro resolution error coming out of the app's root
63
- layout. It is pure JavaScript over Reanimated and Gesture Handler — both
64
- required peers already — so this costs an install and no native build. Rule 3's
65
- promise survives intact: `/button` still pulls nothing sheet-related.
52
+ portal host, a toast host — has to sit inside every provider it reads, so it
53
+ wraps `{children}` and nothing else moves. A layer that brings a new native
54
+ peer is a peer-dependency decision first.
55
+ - **`BottomSheetProvider` is the layer this component does not mount, and the
56
+ reason is the peer decision above.** The sheet's engine,
57
+ `@delacour/react-native-bottom-sheet`, is an **optional** peer of this
58
+ library, the way `@delacour/react-native-charts` is: an app with no sheet
59
+ never resolves it, and its native peer `react-native-teleport` never has to
60
+ be in that app's build. An import here would make every app resolve both,
61
+ sheet or no sheet. So the provider is exported beside `BottomSheet`, from the
62
+ sheet's own subpath, and the app mounts it itself, once,
63
+ inside `DelacourProvider` and around its navigator — innermost, exactly where
64
+ the rule above puts it, because it reads the gesture root for its pans, the
65
+ safe area for its insets and the keyboard values a sheet's footer rides.
66
+ `apps/playground/src/app/_layout.tsx` is the reference mount. Without it a
67
+ `BottomSheet.Portal` still renders, where it is written, as an inline sheet.
68
+ The previous sheet library was a required peer while this component mounted
69
+ its modal provider; nothing imports it any more.
70
+ - **`isMotionCalm` is a behaviour prop, not a layer-named one.** It feeds
71
+ `CalmMotionProvider` (`hooks/use-calm-motion.tsx`), which `useCalmMotion()`
72
+ reads beside the OS reduce-motion setting. It exists for E2E builds: a runner
73
+ that waits for the screen to settle before each gesture (Argent waits up to
74
+ 3 s) pays the whole wait on a screen holding a never-ending loop. Only
75
+ decorative loops ask — `Skeleton` today; `Spinner` and an indeterminate
76
+ `Progress` are the behaviour and keep moving. The context defaults to `false`,
77
+ so a component used without this provider (a `delacour add` copy in an app
78
+ with its own stack) still honours reduce motion.
66
79
  - **Deliberately not idempotent.** It does not detect an enclosing copy of
67
80
  itself. Nesting `GestureHandlerRootView` costs a `View`; nesting
68
81
  `SafeAreaProvider` seeds from the parent's insets and costs a native view;
@@ -83,7 +96,7 @@ everything — a root layout, an `App.tsx`.
83
96
  per-subpath and survives intact: `/button` still pulls nothing
84
97
  keyboard-related.
85
98
  - **Nothing here for `bun test`, and no `provider.variants.ts` to give it
86
- some.** The component is four nested elements and one default parameter;
99
+ some.** The component is five nested elements and one default parameter;
87
100
  extracting a `resolveInitialMetrics()` would be a unit test of `??`. The rule
88
101
  that pure decisions live in a `*.variants.ts` has no decision here to
89
102
  relocate.