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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. package/README.md +10 -6
  2. package/package.json +48 -4
  3. package/src/components/accordion/AGENTS.md +21 -9
  4. package/src/components/accordion/accordion-content.tsx +7 -4
  5. package/src/components/accordion/accordion-item.tsx +15 -13
  6. package/src/components/accordion/accordion.context.tsx +11 -5
  7. package/src/components/accordion/accordion.variants.test.ts +64 -3
  8. package/src/components/accordion/accordion.variants.ts +33 -9
  9. package/src/components/alert/AGENTS.md +102 -0
  10. package/src/components/alert/alert-action.tsx +17 -0
  11. package/src/components/alert/alert-close-button.tsx +50 -0
  12. package/src/components/alert/alert-content.tsx +15 -0
  13. package/src/components/alert/alert-description.tsx +16 -0
  14. package/src/components/alert/alert-glyphs.ts +25 -0
  15. package/src/components/alert/alert-indicator.tsx +40 -0
  16. package/src/components/alert/alert-title.tsx +18 -0
  17. package/src/components/alert/alert.context.tsx +69 -0
  18. package/src/components/alert/alert.tsx +160 -0
  19. package/src/components/alert/alert.types.ts +10 -0
  20. package/src/components/alert/alert.variants.test.ts +218 -0
  21. package/src/components/alert/alert.variants.ts +180 -0
  22. package/src/components/alert/index.ts +23 -0
  23. package/src/components/avatar/AGENTS.md +106 -0
  24. package/src/components/avatar/avatar-badge.tsx +51 -0
  25. package/src/components/avatar/avatar-group.tsx +139 -0
  26. package/src/components/avatar/avatar.context.tsx +93 -0
  27. package/src/components/avatar/avatar.tsx +201 -0
  28. package/src/components/avatar/avatar.utils.test.ts +174 -0
  29. package/src/components/avatar/avatar.utils.ts +141 -0
  30. package/src/components/avatar/avatar.variants.test.ts +251 -0
  31. package/src/components/avatar/avatar.variants.ts +270 -0
  32. package/src/components/avatar/index.ts +39 -0
  33. package/src/components/badge/AGENTS.md +5 -0
  34. package/src/components/badge/badge.tsx +6 -1
  35. package/src/components/bottom-sheet/AGENTS.md +186 -231
  36. package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
  37. package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
  38. package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -208
  39. package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
  40. package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
  41. package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
  42. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -122
  43. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
  44. package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
  45. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
  46. package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
  47. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -62
  48. package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
  49. package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
  50. package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
  51. package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
  52. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
  53. package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
  54. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -87
  55. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
  56. package/src/components/bottom-sheet/index.ts +39 -17
  57. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
  58. package/src/components/button/AGENTS.md +45 -11
  59. package/src/components/button/button-group.tsx +44 -8
  60. package/src/components/button/button.context.tsx +10 -0
  61. package/src/components/button/button.tsx +2 -1
  62. package/src/components/button/button.variants.test.ts +154 -0
  63. package/src/components/button/button.variants.ts +71 -6
  64. package/src/components/button/index.ts +2 -0
  65. package/src/components/calendar/AGENTS.md +142 -0
  66. package/src/components/calendar/calendar-caption.tsx +116 -0
  67. package/src/components/calendar/calendar-day.tsx +103 -0
  68. package/src/components/calendar/calendar-grid.tsx +183 -0
  69. package/src/components/calendar/calendar-header.tsx +34 -0
  70. package/src/components/calendar/calendar-nav.tsx +51 -0
  71. package/src/components/calendar/calendar-picker.tsx +128 -0
  72. package/src/components/calendar/calendar-weekdays.tsx +48 -0
  73. package/src/components/calendar/calendar.context.tsx +108 -0
  74. package/src/components/calendar/calendar.date.test.ts +456 -0
  75. package/src/components/calendar/calendar.date.ts +584 -0
  76. package/src/components/calendar/calendar.tsx +389 -0
  77. package/src/components/calendar/calendar.types.ts +87 -0
  78. package/src/components/calendar/calendar.variants.test.ts +363 -0
  79. package/src/components/calendar/calendar.variants.ts +426 -0
  80. package/src/components/calendar/index.ts +96 -0
  81. package/src/components/card/AGENTS.md +88 -0
  82. package/src/components/card/card-action.tsx +16 -0
  83. package/src/components/card/card-content.tsx +12 -0
  84. package/src/components/card/card-description.tsx +12 -0
  85. package/src/components/card/card-footer.tsx +30 -0
  86. package/src/components/card/card-header.tsx +45 -0
  87. package/src/components/card/card-title.tsx +25 -0
  88. package/src/components/card/card.context.tsx +64 -0
  89. package/src/components/card/card.tsx +94 -0
  90. package/src/components/card/card.types.ts +13 -0
  91. package/src/components/card/card.variants.test.ts +240 -0
  92. package/src/components/card/card.variants.ts +177 -0
  93. package/src/components/card/index.ts +15 -0
  94. package/src/components/chart/AGENTS.md +8 -0
  95. package/src/components/chart/chart.tsx +8 -1
  96. package/src/components/chip/AGENTS.md +113 -0
  97. package/src/components/chip/chip-close-button.tsx +48 -0
  98. package/src/components/chip/chip-end-content.tsx +15 -0
  99. package/src/components/chip/chip-label.tsx +21 -0
  100. package/src/components/chip/chip-start-content.tsx +16 -0
  101. package/src/components/chip/chip.context.tsx +66 -0
  102. package/src/components/chip/chip.tsx +268 -0
  103. package/src/components/chip/chip.types.ts +10 -0
  104. package/src/components/chip/chip.variants.test.ts +335 -0
  105. package/src/components/chip/chip.variants.ts +321 -0
  106. package/src/components/chip/index.ts +24 -0
  107. package/src/components/collapsible/AGENTS.md +97 -0
  108. package/src/components/collapsible/collapsible-content.tsx +97 -0
  109. package/src/components/collapsible/collapsible-description.tsx +17 -0
  110. package/src/components/collapsible/collapsible-indicator.tsx +92 -0
  111. package/src/components/collapsible/collapsible-title.tsx +17 -0
  112. package/src/components/collapsible/collapsible-trigger.tsx +133 -0
  113. package/src/components/collapsible/collapsible.context.tsx +91 -0
  114. package/src/components/collapsible/collapsible.tsx +167 -0
  115. package/src/components/collapsible/collapsible.types.ts +9 -0
  116. package/src/components/collapsible/collapsible.variants.test.ts +338 -0
  117. package/src/components/collapsible/collapsible.variants.ts +228 -0
  118. package/src/components/collapsible/index.ts +32 -0
  119. package/src/components/context-menu/AGENTS.md +103 -0
  120. package/src/components/context-menu/context-menu-content.tsx +64 -0
  121. package/src/components/context-menu/context-menu-item.tsx +22 -0
  122. package/src/components/context-menu/context-menu-preview.tsx +60 -0
  123. package/src/components/context-menu/context-menu-trigger.tsx +160 -0
  124. package/src/components/context-menu/context-menu.context.tsx +91 -0
  125. package/src/components/context-menu/context-menu.tsx +90 -0
  126. package/src/components/context-menu/context-menu.variants.test.ts +266 -0
  127. package/src/components/context-menu/context-menu.variants.ts +147 -0
  128. package/src/components/context-menu/index.ts +18 -0
  129. package/src/components/dialog/AGENTS.md +97 -0
  130. package/src/components/dialog/dialog-body.tsx +21 -0
  131. package/src/components/dialog/dialog-close.tsx +82 -0
  132. package/src/components/dialog/dialog-content.tsx +153 -0
  133. package/src/components/dialog/dialog-description.tsx +23 -0
  134. package/src/components/dialog/dialog-footer.tsx +32 -0
  135. package/src/components/dialog/dialog-header.tsx +20 -0
  136. package/src/components/dialog/dialog-title.tsx +42 -0
  137. package/src/components/dialog/dialog-trigger.tsx +54 -0
  138. package/src/components/dialog/dialog.context.tsx +90 -0
  139. package/src/components/dialog/dialog.tsx +119 -0
  140. package/src/components/dialog/dialog.variants.test.ts +224 -0
  141. package/src/components/dialog/dialog.variants.ts +173 -0
  142. package/src/components/dialog/index.ts +30 -0
  143. package/src/components/drawer/AGENTS.md +123 -0
  144. package/src/components/drawer/drawer-body.tsx +46 -0
  145. package/src/components/drawer/drawer-close.tsx +76 -0
  146. package/src/components/drawer/drawer-content.tsx +186 -0
  147. package/src/components/drawer/drawer-description.tsx +23 -0
  148. package/src/components/drawer/drawer-footer.tsx +21 -0
  149. package/src/components/drawer/drawer-header.tsx +42 -0
  150. package/src/components/drawer/drawer-title.tsx +35 -0
  151. package/src/components/drawer/drawer-trigger.tsx +54 -0
  152. package/src/components/drawer/drawer.context.tsx +114 -0
  153. package/src/components/drawer/drawer.tsx +119 -0
  154. package/src/components/drawer/drawer.variants.test.ts +392 -0
  155. package/src/components/drawer/drawer.variants.ts +244 -0
  156. package/src/components/drawer/index.ts +41 -0
  157. package/src/components/drawer/use-drawer-pan.ts +108 -0
  158. package/src/components/empty-state/AGENTS.md +90 -0
  159. package/src/components/empty-state/empty-state-content.tsx +17 -0
  160. package/src/components/empty-state/empty-state-description.tsx +12 -0
  161. package/src/components/empty-state/empty-state-header.tsx +17 -0
  162. package/src/components/empty-state/empty-state-media.tsx +56 -0
  163. package/src/components/empty-state/empty-state-title.tsx +18 -0
  164. package/src/components/empty-state/empty-state.context.tsx +65 -0
  165. package/src/components/empty-state/empty-state.tsx +84 -0
  166. package/src/components/empty-state/empty-state.types.ts +14 -0
  167. package/src/components/empty-state/empty-state.variants.test.ts +245 -0
  168. package/src/components/empty-state/empty-state.variants.ts +132 -0
  169. package/src/components/empty-state/index.ts +20 -0
  170. package/src/components/fab/AGENTS.md +119 -0
  171. package/src/components/fab/fab-action.tsx +118 -0
  172. package/src/components/fab/fab-group.tsx +214 -0
  173. package/src/components/fab/fab-label.tsx +18 -0
  174. package/src/components/fab/fab.context.tsx +125 -0
  175. package/src/components/fab/fab.tsx +156 -0
  176. package/src/components/fab/fab.types.ts +17 -0
  177. package/src/components/fab/fab.variants.test.ts +274 -0
  178. package/src/components/fab/fab.variants.ts +234 -0
  179. package/src/components/fab/index.ts +37 -0
  180. package/src/components/feedback/AGENTS.md +95 -0
  181. package/src/components/feedback/feedback-action.tsx +16 -0
  182. package/src/components/feedback/feedback-cancel.tsx +34 -0
  183. package/src/components/feedback/feedback-close.tsx +21 -0
  184. package/src/components/feedback/feedback-content.tsx +24 -0
  185. package/src/components/feedback/feedback-field.tsx +70 -0
  186. package/src/components/feedback/feedback-footer.tsx +21 -0
  187. package/src/components/feedback/feedback-panel.tsx +54 -0
  188. package/src/components/feedback/feedback-submit.tsx +74 -0
  189. package/src/components/feedback/feedback-title.tsx +34 -0
  190. package/src/components/feedback/feedback-trigger.tsx +20 -0
  191. package/src/components/feedback/feedback.context.tsx +73 -0
  192. package/src/components/feedback/feedback.tsx +135 -0
  193. package/src/components/feedback/feedback.variants.test.ts +185 -0
  194. package/src/components/feedback/feedback.variants.ts +128 -0
  195. package/src/components/feedback/index.ts +24 -0
  196. package/src/components/field/AGENTS.md +19 -0
  197. package/src/components/field/field-label.tsx +19 -5
  198. package/src/components/field/field.context.tsx +17 -0
  199. package/src/components/field/field.tsx +15 -2
  200. package/src/components/field/field.variants.test.ts +24 -0
  201. package/src/components/field/field.variants.ts +20 -0
  202. package/src/components/field/index.ts +1 -0
  203. package/src/components/item/AGENTS.md +92 -0
  204. package/src/components/item/index.ts +25 -0
  205. package/src/components/item/item-actions.tsx +28 -0
  206. package/src/components/item/item-content.tsx +15 -0
  207. package/src/components/item/item-description.tsx +12 -0
  208. package/src/components/item/item-footer.tsx +13 -0
  209. package/src/components/item/item-group.tsx +32 -0
  210. package/src/components/item/item-header.tsx +13 -0
  211. package/src/components/item/item-media.tsx +37 -0
  212. package/src/components/item/item-separator.tsx +13 -0
  213. package/src/components/item/item-title.tsx +17 -0
  214. package/src/components/item/item.context.tsx +66 -0
  215. package/src/components/item/item.tsx +218 -0
  216. package/src/components/item/item.types.ts +12 -0
  217. package/src/components/item/item.variants.test.ts +263 -0
  218. package/src/components/item/item.variants.ts +200 -0
  219. package/src/components/kpi/AGENTS.md +125 -0
  220. package/src/components/kpi/index.ts +45 -0
  221. package/src/components/kpi/kpi-action.tsx +12 -0
  222. package/src/components/kpi/kpi-content.tsx +35 -0
  223. package/src/components/kpi/kpi-footer.tsx +52 -0
  224. package/src/components/kpi/kpi-group.tsx +70 -0
  225. package/src/components/kpi/kpi-header.tsx +25 -0
  226. package/src/components/kpi/kpi-icon.tsx +28 -0
  227. package/src/components/kpi/kpi-sparkline.tsx +145 -0
  228. package/src/components/kpi/kpi-stat.tsx +20 -0
  229. package/src/components/kpi/kpi-title.tsx +19 -0
  230. package/src/components/kpi/kpi-trend.tsx +105 -0
  231. package/src/components/kpi/kpi-value.tsx +32 -0
  232. package/src/components/kpi/kpi.context.tsx +111 -0
  233. package/src/components/kpi/kpi.tsx +154 -0
  234. package/src/components/kpi/kpi.types.ts +9 -0
  235. package/src/components/kpi/kpi.variants.test.ts +428 -0
  236. package/src/components/kpi/kpi.variants.ts +347 -0
  237. package/src/components/label/AGENTS.md +89 -0
  238. package/src/components/label/index.ts +10 -0
  239. package/src/components/label/label.tsx +97 -0
  240. package/src/components/label/label.variants.test.ts +176 -0
  241. package/src/components/label/label.variants.ts +123 -0
  242. package/src/components/menu/AGENTS.md +176 -0
  243. package/src/components/menu/index.ts +44 -0
  244. package/src/components/menu/menu-background.tsx +17 -0
  245. package/src/components/menu/menu-checkbox-item.tsx +57 -0
  246. package/src/components/menu/menu-content.tsx +303 -0
  247. package/src/components/menu/menu-item.tsx +162 -0
  248. package/src/components/menu/menu-label.tsx +14 -0
  249. package/src/components/menu/menu-radio-group.tsx +50 -0
  250. package/src/components/menu/menu-radio-item.tsx +57 -0
  251. package/src/components/menu/menu-separator.tsx +11 -0
  252. package/src/components/menu/menu-sub-content.tsx +72 -0
  253. package/src/components/menu/menu-sub-trigger.tsx +50 -0
  254. package/src/components/menu/menu-sub.tsx +75 -0
  255. package/src/components/menu/menu-trigger.tsx +57 -0
  256. package/src/components/menu/menu.context.tsx +214 -0
  257. package/src/components/menu/menu.tsx +88 -0
  258. package/src/components/menu/menu.types.ts +33 -0
  259. package/src/components/menu/menu.variants.test.ts +400 -0
  260. package/src/components/menu/menu.variants.ts +353 -0
  261. package/src/components/meter/AGENTS.md +135 -0
  262. package/src/components/meter/index.ts +43 -0
  263. package/src/components/meter/meter-fill.tsx +19 -0
  264. package/src/components/meter/meter-header.tsx +17 -0
  265. package/src/components/meter/meter-label.tsx +18 -0
  266. package/src/components/meter/meter-output.tsx +42 -0
  267. package/src/components/meter/meter-segments.tsx +83 -0
  268. package/src/components/meter/meter-track.tsx +16 -0
  269. package/src/components/meter/meter.context.tsx +62 -0
  270. package/src/components/meter/meter.tsx +286 -0
  271. package/src/components/meter/meter.types.ts +30 -0
  272. package/src/components/meter/meter.variants.test.ts +430 -0
  273. package/src/components/meter/meter.variants.ts +347 -0
  274. package/src/components/overlay/AGENTS.md +89 -0
  275. package/src/components/overlay/index.ts +27 -0
  276. package/src/components/overlay/overlay-portal.tsx +84 -0
  277. package/src/components/overlay/overlay-presence.test.ts +46 -0
  278. package/src/components/overlay/overlay-presence.ts +49 -0
  279. package/src/components/overlay/overlay-registry.test.ts +96 -0
  280. package/src/components/overlay/overlay-registry.ts +99 -0
  281. package/src/components/overlay/overlay-scrim.tsx +49 -0
  282. package/src/components/overlay/overlay.context.tsx +48 -0
  283. package/src/components/overlay/overlay.tsx +118 -0
  284. package/src/components/overlay/overlay.variants.test.ts +37 -0
  285. package/src/components/overlay/overlay.variants.ts +33 -0
  286. package/src/components/overlay/use-overlay-back-handler.ts +34 -0
  287. package/src/components/overlay/use-overlay-presence.ts +87 -0
  288. package/src/components/popover/AGENTS.md +138 -0
  289. package/src/components/popover/index.ts +48 -0
  290. package/src/components/popover/popover-anchor.tsx +49 -0
  291. package/src/components/popover/popover-arrow.tsx +89 -0
  292. package/src/components/popover/popover-close.tsx +73 -0
  293. package/src/components/popover/popover-content.tsx +235 -0
  294. package/src/components/popover/popover-description.tsx +18 -0
  295. package/src/components/popover/popover-title.tsx +33 -0
  296. package/src/components/popover/popover-trigger.tsx +56 -0
  297. package/src/components/popover/popover.context.tsx +71 -0
  298. package/src/components/popover/popover.position.test.ts +354 -0
  299. package/src/components/popover/popover.position.ts +286 -0
  300. package/src/components/popover/popover.tsx +123 -0
  301. package/src/components/popover/popover.variants.test.ts +135 -0
  302. package/src/components/popover/popover.variants.ts +85 -0
  303. package/src/components/popover/use-anchor-measure.ts +76 -0
  304. package/src/components/popover/use-anchored-content.ts +219 -0
  305. package/src/components/pressable/AGENTS.md +11 -1
  306. package/src/components/pressable/pressable.tsx +7 -2
  307. package/src/components/pressable/pressable.variants.test.ts +29 -0
  308. package/src/components/pressable/pressable.variants.ts +17 -0
  309. package/src/components/progress/AGENTS.md +120 -0
  310. package/src/components/progress/index.ts +40 -0
  311. package/src/components/progress/progress-fill.tsx +152 -0
  312. package/src/components/progress/progress-header.tsx +19 -0
  313. package/src/components/progress/progress-label.tsx +27 -0
  314. package/src/components/progress/progress-output.tsx +47 -0
  315. package/src/components/progress/progress-track.tsx +38 -0
  316. package/src/components/progress/progress.context.tsx +79 -0
  317. package/src/components/progress/progress.tsx +171 -0
  318. package/src/components/progress/progress.types.ts +23 -0
  319. package/src/components/progress/progress.variants.test.ts +335 -0
  320. package/src/components/progress/progress.variants.ts +284 -0
  321. package/src/components/progress-button/AGENTS.md +116 -0
  322. package/src/components/progress-button/index.ts +36 -0
  323. package/src/components/progress-button/progress-button-done.tsx +33 -0
  324. package/src/components/progress-button/progress-button-label.tsx +27 -0
  325. package/src/components/progress-button/progress-button.context.tsx +93 -0
  326. package/src/components/progress-button/progress-button.tsx +414 -0
  327. package/src/components/progress-button/progress-button.variants.test.ts +314 -0
  328. package/src/components/progress-button/progress-button.variants.ts +236 -0
  329. package/src/components/provider/AGENTS.md +38 -25
  330. package/src/components/provider/provider.tsx +38 -11
  331. package/src/components/radio/AGENTS.md +8 -10
  332. package/src/components/radio/radio.variants.test.ts +5 -6
  333. package/src/components/radio/radio.variants.ts +6 -7
  334. package/src/components/rating/AGENTS.md +143 -0
  335. package/src/components/rating/index.ts +43 -0
  336. package/src/components/rating/rating-output.tsx +39 -0
  337. package/src/components/rating/rating-star.tsx +70 -0
  338. package/src/components/rating/rating-stars.tsx +219 -0
  339. package/src/components/rating/rating.context.tsx +79 -0
  340. package/src/components/rating/rating.tsx +163 -0
  341. package/src/components/rating/rating.variants.test.ts +347 -0
  342. package/src/components/rating/rating.variants.ts +354 -0
  343. package/src/components/selection-mode/AGENTS.md +155 -0
  344. package/src/components/selection-mode/index.ts +43 -0
  345. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  346. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  347. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  348. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  349. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  350. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  351. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  352. package/src/components/selection-mode/selection-mode.tsx +207 -0
  353. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  354. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  355. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  356. package/src/components/skeleton/AGENTS.md +120 -0
  357. package/src/components/skeleton/index.ts +15 -0
  358. package/src/components/skeleton/skeleton-clock.ts +48 -0
  359. package/src/components/skeleton/skeleton-group.tsx +64 -0
  360. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  361. package/src/components/skeleton/skeleton.context.tsx +45 -0
  362. package/src/components/skeleton/skeleton.tsx +227 -0
  363. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  364. package/src/components/skeleton/skeleton.variants.ts +222 -0
  365. package/src/components/slide-button/AGENTS.md +122 -0
  366. package/src/components/slide-button/index.ts +43 -0
  367. package/src/components/slide-button/slide-button-label.tsx +70 -0
  368. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  369. package/src/components/slide-button/slide-button.context.tsx +76 -0
  370. package/src/components/slide-button/slide-button.tsx +457 -0
  371. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  372. package/src/components/slide-button/slide-button.variants.ts +324 -0
  373. package/src/components/slider/index.ts +2 -0
  374. package/src/components/slider/slider-thumb.tsx +33 -1
  375. package/src/components/slider/slider.variants.test.ts +52 -0
  376. package/src/components/slider/slider.variants.ts +56 -2
  377. package/src/components/stack-card/AGENTS.md +169 -0
  378. package/src/components/stack-card/index.ts +37 -0
  379. package/src/components/stack-card/stack-card-action.tsx +66 -0
  380. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  381. package/src/components/stack-card/stack-card-card.tsx +71 -0
  382. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  383. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  384. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  385. package/src/components/stack-card/stack-card.context.tsx +117 -0
  386. package/src/components/stack-card/stack-card.tsx +557 -0
  387. package/src/components/stack-card/stack-card.types.ts +26 -0
  388. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  389. package/src/components/stack-card/stack-card.variants.ts +360 -0
  390. package/src/components/steps/AGENTS.md +153 -0
  391. package/src/components/steps/index.ts +45 -0
  392. package/src/components/steps/steps-connector.tsx +31 -0
  393. package/src/components/steps/steps-description.tsx +26 -0
  394. package/src/components/steps/steps-indicator.tsx +106 -0
  395. package/src/components/steps/steps-item.tsx +225 -0
  396. package/src/components/steps/steps-panel.tsx +41 -0
  397. package/src/components/steps/steps-title.tsx +32 -0
  398. package/src/components/steps/steps-track.tsx +59 -0
  399. package/src/components/steps/steps.context.tsx +123 -0
  400. package/src/components/steps/steps.tsx +166 -0
  401. package/src/components/steps/steps.variants.test.ts +388 -0
  402. package/src/components/steps/steps.variants.ts +468 -0
  403. package/src/components/surface/AGENTS.md +72 -0
  404. package/src/components/surface/index.ts +15 -0
  405. package/src/components/surface/surface.context.tsx +54 -0
  406. package/src/components/surface/surface.tsx +70 -0
  407. package/src/components/surface/surface.variants.test.ts +180 -0
  408. package/src/components/surface/surface.variants.ts +133 -0
  409. package/src/components/swipe/AGENTS.md +140 -0
  410. package/src/components/swipe/index.ts +31 -0
  411. package/src/components/swipe/swipe-action.tsx +99 -0
  412. package/src/components/swipe/swipe-end.tsx +14 -0
  413. package/src/components/swipe/swipe-group.tsx +55 -0
  414. package/src/components/swipe/swipe-panel.tsx +55 -0
  415. package/src/components/swipe/swipe-start.tsx +15 -0
  416. package/src/components/swipe/swipe.context.tsx +135 -0
  417. package/src/components/swipe/swipe.tsx +393 -0
  418. package/src/components/swipe/swipe.types.ts +7 -0
  419. package/src/components/swipe/swipe.variants.test.ts +337 -0
  420. package/src/components/swipe/swipe.variants.ts +290 -0
  421. package/src/components/switch/switch.variants.ts +2 -2
  422. package/src/components/tabs/AGENTS.md +4 -3
  423. package/src/components/tabs/tabs.variants.test.ts +1 -1
  424. package/src/components/tabs/tabs.variants.ts +4 -6
  425. package/src/components/textarea/AGENTS.md +86 -0
  426. package/src/components/textarea/index.ts +16 -0
  427. package/src/components/textarea/textarea.tsx +157 -0
  428. package/src/components/textarea/textarea.variants.test.ts +219 -0
  429. package/src/components/textarea/textarea.variants.ts +171 -0
  430. package/src/components/toast/AGENTS.md +140 -0
  431. package/src/components/toast/index.ts +74 -0
  432. package/src/components/toast/toast-action.tsx +59 -0
  433. package/src/components/toast/toast-api.ts +32 -0
  434. package/src/components/toast/toast-close.tsx +44 -0
  435. package/src/components/toast/toast-content.tsx +11 -0
  436. package/src/components/toast/toast-description.tsx +11 -0
  437. package/src/components/toast/toast-indicator.tsx +34 -0
  438. package/src/components/toast/toast-item.tsx +308 -0
  439. package/src/components/toast/toast-title.tsx +13 -0
  440. package/src/components/toast/toast-viewport.tsx +135 -0
  441. package/src/components/toast/toast.context.tsx +69 -0
  442. package/src/components/toast/toast.store.test.ts +259 -0
  443. package/src/components/toast/toast.store.ts +338 -0
  444. package/src/components/toast/toast.timer.test.ts +64 -0
  445. package/src/components/toast/toast.timer.ts +51 -0
  446. package/src/components/toast/toast.tsx +132 -0
  447. package/src/components/toast/toast.variants.test.ts +393 -0
  448. package/src/components/toast/toast.variants.ts +304 -0
  449. package/src/components/toggle-button/AGENTS.md +106 -0
  450. package/src/components/toggle-button/index.ts +30 -0
  451. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  452. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  453. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  454. package/src/components/toggle-button/toggle-button.tsx +172 -0
  455. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  456. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  457. package/src/components/tooltip/AGENTS.md +133 -0
  458. package/src/components/tooltip/index.ts +28 -0
  459. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  460. package/src/components/tooltip/tooltip-content.tsx +179 -0
  461. package/src/components/tooltip/tooltip-description.tsx +19 -0
  462. package/src/components/tooltip/tooltip-text.tsx +21 -0
  463. package/src/components/tooltip/tooltip-title.tsx +20 -0
  464. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  465. package/src/components/tooltip/tooltip.context.tsx +68 -0
  466. package/src/components/tooltip/tooltip.tsx +213 -0
  467. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  468. package/src/components/tooltip/tooltip.variants.ts +177 -0
  469. package/src/hooks/use-calm-motion.tsx +31 -0
  470. package/src/lib/calm-motion.test.ts +17 -0
  471. package/src/lib/calm-motion.ts +23 -0
  472. package/src/lib/cn.test.ts +14 -0
  473. package/src/lib/cn.ts +2 -2
  474. package/src/no-gorhom.test.ts +54 -0
  475. package/src/styles/geometry.test.ts +1 -0
  476. package/src/styles/theme.css +4 -0
  477. package/src/styles/tokens.css +11 -0
  478. package/src/styles/tokens.test.ts +22 -0
  479. package/src/styles/tokens.ts +20 -0
  480. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  481. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -0,0 +1,335 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { declarationCount } from "../../styles/theme-tokens.test";
3
+ import { SLIDER_COLORS, SLIDER_OUTPUT_TEXT_SIZE, SLIDER_SIZES } from "../slider/slider.variants";
4
+ import { TEXT_SIZES } from "../text/text.variants";
5
+ import {
6
+ fillTranslate,
7
+ formatProgressValue,
8
+ indeterminateSegment,
9
+ PROGRESS_COLORS,
10
+ PROGRESS_DEFAULT_COLOR,
11
+ PROGRESS_DEFAULT_SIZE,
12
+ PROGRESS_FILL_DURATION_MS,
13
+ PROGRESS_INDETERMINATE_DURATION_MS,
14
+ PROGRESS_INDETERMINATE_SEGMENT,
15
+ PROGRESS_MAX_VALUE,
16
+ PROGRESS_MIN_VALUE,
17
+ PROGRESS_OUTPUT_TEXT_SIZE,
18
+ PROGRESS_PULSE,
19
+ PROGRESS_SIZES,
20
+ progressRatio,
21
+ progressVariants,
22
+ resolveProgressAccessibility,
23
+ resolveProgressAxes,
24
+ } from "./progress.variants";
25
+
26
+ /** The bare colour a `bg-*` class names — `bg-success` yields `success`. */
27
+ function backgroundToken(value: string): string | undefined {
28
+ return value.match(/\bbg-([\w-]+)\b/)?.[1];
29
+ }
30
+
31
+ /** A slot's class string, with `tv`'s empty-slot `undefined` flattened. */
32
+ function cls(value: string | undefined): string {
33
+ return value ?? "";
34
+ }
35
+
36
+ /** The `h-*` step a class string sets — `h-1.5` yields 1.5. */
37
+ function heightStep(value: string): number {
38
+ return Number(value.match(/\bh-(\d+(?:\.\d+)?)\b/)?.[1]);
39
+ }
40
+
41
+ describe("the theme reader", () => {
42
+ // Guard: every token assertion below is worthless if the parse found nothing.
43
+ test("finds both variants of the theme", () => {
44
+ expect(declarationCount("primary")).toBe(2);
45
+ });
46
+ });
47
+
48
+ describe("the axes", () => {
49
+ // A meter stacked on this bar, and a slider beside it, have to be able to
50
+ // name the same colour and the same size with the same word.
51
+ test("share the slider's six colours and three sizes", () => {
52
+ expect([...PROGRESS_COLORS]).toEqual([...SLIDER_COLORS]);
53
+ expect([...PROGRESS_SIZES]).toEqual([...SLIDER_SIZES]);
54
+ });
55
+
56
+ test("default to values the axes actually hold", () => {
57
+ expect(PROGRESS_COLORS).toContain(PROGRESS_DEFAULT_COLOR);
58
+ expect(PROGRESS_SIZES).toContain(PROGRESS_DEFAULT_SIZE);
59
+ });
60
+
61
+ test("default the range to 0–100", () => {
62
+ expect(PROGRESS_MIN_VALUE).toBe(0);
63
+ expect(PROGRESS_MAX_VALUE).toBe(100);
64
+ });
65
+ });
66
+
67
+ describe("resolveProgressAxes", () => {
68
+ test("falls back to the defaults", () => {
69
+ expect(resolveProgressAxes({})).toEqual({
70
+ color: PROGRESS_DEFAULT_COLOR,
71
+ isIndeterminate: false,
72
+ size: PROGRESS_DEFAULT_SIZE,
73
+ });
74
+ });
75
+
76
+ test("keeps what the call site named", () => {
77
+ expect(resolveProgressAxes({ color: "success", isIndeterminate: true, size: "lg" })).toEqual({
78
+ color: "success",
79
+ isIndeterminate: true,
80
+ size: "lg",
81
+ });
82
+ });
83
+
84
+ // The progress bar is not a form control, so an enclosing Field has nothing
85
+ // to hand it. The resolver takes no second argument, and this pins that.
86
+ test("takes the call site alone", () => {
87
+ expect(resolveProgressAxes.length).toBe(1);
88
+ });
89
+ });
90
+
91
+ describe("progressVariants", () => {
92
+ test("paints the fill differently for every colour", () => {
93
+ const painted = new Set(PROGRESS_COLORS.map((color) => cls(progressVariants({ color }).fill())));
94
+ expect(painted.size).toBe(PROGRESS_COLORS.length);
95
+ });
96
+
97
+ test("never paints the track in a colour", () => {
98
+ const tracks = new Set(PROGRESS_COLORS.map((color) => cls(progressVariants({ color }).track())));
99
+ expect(tracks.size).toBe(1);
100
+ });
101
+
102
+ test("names only tokens both variants of the theme declare", () => {
103
+ const tokens = [
104
+ backgroundToken(cls(progressVariants({}).track())),
105
+ ...PROGRESS_COLORS.map((color) => backgroundToken(cls(progressVariants({ color }).fill()))),
106
+ ];
107
+ for (const token of tokens) {
108
+ expect(token).toBeDefined();
109
+ expect(declarationCount(token as string)).toBe(2);
110
+ }
111
+ });
112
+
113
+ test("never paints the fill in the track's own colour", () => {
114
+ const track = backgroundToken(cls(progressVariants({}).track()));
115
+ for (const color of PROGRESS_COLORS) {
116
+ expect(backgroundToken(cls(progressVariants({ color }).fill()))).not.toBe(track);
117
+ }
118
+ });
119
+
120
+ test("thickens the track as the size steps up", () => {
121
+ const heights = PROGRESS_SIZES.map((size) => heightStep(cls(progressVariants({ size }).track())));
122
+ for (const height of heights) expect(Number.isFinite(height)).toBe(true);
123
+ expect([...heights].sort((a, b) => a - b)).toEqual(heights);
124
+ expect(new Set(heights).size).toBe(heights.length);
125
+ });
126
+
127
+ // The fill slides under the track's rounded clip; a track that stops clipping
128
+ // shows the fill parked to its left at every value below the maximum.
129
+ test("clips the fill to the track", () => {
130
+ expect(cls(progressVariants({}).track())).toMatch(/\boverflow-hidden\b/);
131
+ expect(cls(progressVariants({}).fill())).toMatch(/\babsolute\b/);
132
+ });
133
+
134
+ // No slot worn by a View carries type — the readout names a Text step instead.
135
+ test("puts no text styling on a View slot", () => {
136
+ for (const size of PROGRESS_SIZES) {
137
+ const slots = progressVariants({ size });
138
+ for (const slot of [slots.root(), slots.track(), slots.fill()]) {
139
+ expect(cls(slot)).not.toMatch(/\b(text|font)-/);
140
+ }
141
+ }
142
+ });
143
+ });
144
+
145
+ describe("PROGRESS_OUTPUT_TEXT_SIZE", () => {
146
+ test("names a size Text actually has, for every progress size", () => {
147
+ for (const size of PROGRESS_SIZES) {
148
+ expect(TEXT_SIZES).toContain(PROGRESS_OUTPUT_TEXT_SIZE[size]);
149
+ }
150
+ });
151
+
152
+ // A progress bar under a slider in one form reads at the same type step.
153
+ test("matches the slider's readout", () => {
154
+ expect(PROGRESS_OUTPUT_TEXT_SIZE).toEqual(SLIDER_OUTPUT_TEXT_SIZE);
155
+ });
156
+ });
157
+
158
+ describe("the motion constants", () => {
159
+ test("are positive and ordered the way the eye reads them", () => {
160
+ expect(PROGRESS_FILL_DURATION_MS).toBeGreaterThan(0);
161
+ expect(PROGRESS_INDETERMINATE_DURATION_MS).toBeGreaterThan(PROGRESS_FILL_DURATION_MS);
162
+ });
163
+
164
+ test("keep the indeterminate segment a strict fraction of the track", () => {
165
+ expect(PROGRESS_INDETERMINATE_SEGMENT).toBeGreaterThan(0);
166
+ expect(PROGRESS_INDETERMINATE_SEGMENT).toBeLessThan(1);
167
+ });
168
+
169
+ // A pulse that dims to nothing reads as the bar disappearing, not breathing.
170
+ test("never pulses to fully transparent", () => {
171
+ expect(PROGRESS_PULSE.from).toBeGreaterThan(0);
172
+ expect(PROGRESS_PULSE.to).toBe(1);
173
+ expect(PROGRESS_PULSE.from).toBeLessThan(PROGRESS_PULSE.to);
174
+ expect(PROGRESS_PULSE.durationMs).toBeGreaterThan(0);
175
+ });
176
+ });
177
+
178
+ describe("progressRatio", () => {
179
+ test("maps the range onto 0–1", () => {
180
+ expect(progressRatio(0, 0, 100)).toBe(0);
181
+ expect(progressRatio(25, 0, 100)).toBe(0.25);
182
+ expect(progressRatio(100, 0, 100)).toBe(1);
183
+ expect(progressRatio(18, 0, 24)).toBe(0.75);
184
+ expect(progressRatio(15, 10, 20)).toBe(0.5);
185
+ });
186
+
187
+ test("clamps rather than extrapolating", () => {
188
+ expect(progressRatio(-5, 0, 100)).toBe(0);
189
+ expect(progressRatio(150, 0, 100)).toBe(1);
190
+ });
191
+
192
+ // NaN in a shared value freezes the fill for good — no later frame recovers.
193
+ test("returns 0 for a degenerate range or a value that is not a number", () => {
194
+ expect(progressRatio(5, 10, 10)).toBe(0);
195
+ expect(progressRatio(5, 20, 10)).toBe(0);
196
+ expect(progressRatio(Number.NaN, 0, 100)).toBe(0);
197
+ expect(progressRatio(Number.POSITIVE_INFINITY, 0, 100)).toBe(0);
198
+ });
199
+ });
200
+
201
+ describe("fillTranslate", () => {
202
+ test("parks the full-length fill left of the track by what is still to go", () => {
203
+ expect(fillTranslate({ ratio: 0, trackSize: 200 })).toBe(-200);
204
+ expect(fillTranslate({ ratio: 0.25, trackSize: 200 })).toBe(-150);
205
+ expect(fillTranslate({ ratio: 1, trackSize: 200 })).toBe(0);
206
+ });
207
+
208
+ test("clamps a ratio outside 0–1", () => {
209
+ expect(fillTranslate({ ratio: -1, trackSize: 200 })).toBe(-200);
210
+ expect(fillTranslate({ ratio: 2, trackSize: 200 })).toBe(0);
211
+ });
212
+
213
+ // An unmeasured track is 0 wide; the bar is hidden until it is measured.
214
+ test("is 0 for an unmeasured track", () => {
215
+ expect(fillTranslate({ ratio: 0.5, trackSize: 0 })).toBe(0);
216
+ });
217
+ });
218
+
219
+ describe("indeterminateSegment", () => {
220
+ test("sizes the segment as a fraction of the track", () => {
221
+ expect(indeterminateSegment({ phase: 0, segment: 0.4, trackSize: 200 }).width).toBe(80);
222
+ });
223
+
224
+ // Both ends of the loop are off-track, so the seam where it repeats is never
225
+ // on screen.
226
+ test("starts wholly left of the track and ends wholly right of it", () => {
227
+ const start = indeterminateSegment({ phase: 0, segment: 0.4, trackSize: 200 });
228
+ const end = indeterminateSegment({ phase: 1, segment: 0.4, trackSize: 200 });
229
+ expect(start.translate + start.width).toBe(0);
230
+ expect(end.translate).toBe(200);
231
+ });
232
+
233
+ test("moves monotonically", () => {
234
+ let last = Number.NEGATIVE_INFINITY;
235
+ for (let phase = 0; phase <= 1; phase += 0.1) {
236
+ const { translate } = indeterminateSegment({ phase, segment: 0.4, trackSize: 200 });
237
+ expect(translate).toBeGreaterThan(last);
238
+ last = translate;
239
+ }
240
+ });
241
+
242
+ test("draws nothing on an unmeasured track", () => {
243
+ expect(indeterminateSegment({ phase: 0.5, segment: 0.4, trackSize: 0 })).toEqual({ translate: 0, width: 0 });
244
+ });
245
+ });
246
+
247
+ describe("formatProgressValue", () => {
248
+ test("reads as a whole percentage of the range by default", () => {
249
+ expect(formatProgressValue({ maxValue: 100, minValue: 0, value: 72 })).toBe("72%");
250
+ expect(formatProgressValue({ maxValue: 24, minValue: 0, value: 18 })).toBe("75%");
251
+ expect(formatProgressValue({ maxValue: 100, minValue: 0, value: 33.4 })).toBe("33%");
252
+ });
253
+
254
+ test("clamps the percentage the way the fill clamps", () => {
255
+ expect(formatProgressValue({ maxValue: 100, minValue: 0, value: 140 })).toBe("100%");
256
+ });
257
+
258
+ test("formats the value itself when given a non-percent style", () => {
259
+ const formatted = formatProgressValue({
260
+ formatOptions: { currency: "NZD", maximumFractionDigits: 0, style: "currency" },
261
+ maxValue: 2000,
262
+ minValue: 0,
263
+ value: 1250,
264
+ });
265
+ expect(formatted).toMatch(/1,250/);
266
+ expect(formatted).not.toMatch(/%/);
267
+ });
268
+
269
+ // `style: "percent"` would print 7200% if it formatted the value, so a percent
270
+ // style still formats the ratio — with the caller's other options applied.
271
+ test("keeps formatting the ratio under a percent style", () => {
272
+ expect(
273
+ formatProgressValue({
274
+ formatOptions: { maximumFractionDigits: 1, style: "percent" },
275
+ maxValue: 1000,
276
+ minValue: 0,
277
+ value: 125,
278
+ })
279
+ ).toBe("12.5%");
280
+ });
281
+ });
282
+
283
+ describe("resolveProgressAccessibility", () => {
284
+ test("reports the range and the value for a determinate bar", () => {
285
+ expect(resolveProgressAccessibility({ isIndeterminate: false, maxValue: 100, minValue: 0, value: 72 })).toEqual({
286
+ busy: false,
287
+ value: { max: 100, min: 0, now: 72, text: "72%" },
288
+ });
289
+ });
290
+
291
+ // "18 of 24" says more than "75%": the count is what the screen is about.
292
+ test("reads a count as value of maximum when the range is not 0–100", () => {
293
+ expect(
294
+ resolveProgressAccessibility({ isIndeterminate: false, maxValue: 24, minValue: 0, value: 18 }).value
295
+ ).toEqual({ max: 24, min: 0, now: 18, text: "18 of 24" });
296
+ });
297
+
298
+ test("speaks a formatted value when the readout formats one", () => {
299
+ const { value } = resolveProgressAccessibility({
300
+ formatOptions: { currency: "NZD", maximumFractionDigits: 0, style: "currency" },
301
+ isIndeterminate: false,
302
+ maxValue: 2000,
303
+ minValue: 0,
304
+ value: 1250,
305
+ });
306
+ expect(value?.text).toMatch(/1,250 of .*2,000/);
307
+ });
308
+
309
+ test("prefers the caller's own words", () => {
310
+ expect(
311
+ resolveProgressAccessibility({
312
+ isIndeterminate: false,
313
+ maxValue: 10,
314
+ minValue: 0,
315
+ value: 3,
316
+ valueLabel: "Step 3 of 10",
317
+ }).value?.text
318
+ ).toBe("Step 3 of 10");
319
+ });
320
+
321
+ test("clamps the reported value into the range", () => {
322
+ expect(
323
+ resolveProgressAccessibility({ isIndeterminate: false, maxValue: 100, minValue: 0, value: 130 }).value?.now
324
+ ).toBe(100);
325
+ });
326
+
327
+ // An indeterminate bar has no value to report, and a `now` of 0 would be read
328
+ // out as "zero percent" — which is a lie about work that is under way.
329
+ test("reports busy and no value while indeterminate", () => {
330
+ expect(resolveProgressAccessibility({ isIndeterminate: true, maxValue: 100, minValue: 0, value: 40 })).toEqual({
331
+ busy: true,
332
+ value: undefined,
333
+ });
334
+ });
335
+ });
@@ -0,0 +1,284 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+ import type { TextSize } from "../text/text.variants";
4
+
5
+ /** What the filled part of the bar means. The slider's set, so the two name a colour alike. */
6
+ export const PROGRESS_COLORS = ["default", "primary", "success", "warning", "destructive", "info"] as const;
7
+
8
+ export const PROGRESS_SIZES = ["sm", "md", "lg"] as const;
9
+
10
+ export type ProgressColor = (typeof PROGRESS_COLORS)[number];
11
+ export type ProgressSize = (typeof PROGRESS_SIZES)[number];
12
+
13
+ /**
14
+ * The axes a progress bar falls back to when nothing names one.
15
+ *
16
+ * Named once because two places read them — `defaultVariants` below and
17
+ * {@link resolveProgressAxes}, which runs before `tv` is ever called.
18
+ */
19
+ export const PROGRESS_DEFAULT_COLOR: ProgressColor = "default";
20
+ export const PROGRESS_DEFAULT_SIZE: ProgressSize = "md";
21
+
22
+ /** The range a progress bar covers when the caller names none. */
23
+ export const PROGRESS_MIN_VALUE = 0;
24
+ export const PROGRESS_MAX_VALUE = 100;
25
+
26
+ /**
27
+ * How long the fill takes to reach a new value.
28
+ *
29
+ * Short enough that a bar fed every hundred milliseconds by an upload never
30
+ * falls visibly behind, long enough that a jump from 20 to 60 reads as travel
31
+ * rather than as a cut.
32
+ */
33
+ export const PROGRESS_FILL_DURATION_MS = 320;
34
+
35
+ /** One pass of the indeterminate segment, from off the left edge to off the right. */
36
+ export const PROGRESS_INDETERMINATE_DURATION_MS = 1400;
37
+
38
+ /** The indeterminate segment's length, as a fraction of the track. */
39
+ export const PROGRESS_INDETERMINATE_SEGMENT = 0.4;
40
+
41
+ /**
42
+ * The reduce-motion stand-in for the indeterminate sweep: the whole track fills
43
+ * and breathes between these two opacities.
44
+ *
45
+ * Never down to zero — a bar that fades out entirely reads as the work having
46
+ * stopped, which is the one thing an indeterminate bar must not say.
47
+ */
48
+ export const PROGRESS_PULSE = { from: 0.35, to: 1, durationMs: 900 } as const;
49
+
50
+ /**
51
+ * The `Text` size step each progress size hands its label and its readout.
52
+ *
53
+ * The slider's own map, restated rather than imported so this leaf imports no
54
+ * other component's variants; a test pins the two together.
55
+ */
56
+ export const PROGRESS_OUTPUT_TEXT_SIZE: Record<ProgressSize, TextSize> = { sm: "sm", md: "md", lg: "lg" };
57
+
58
+ /**
59
+ * Styling for every part of a progress bar.
60
+ *
61
+ * One slotted `tv()` because `progress-label.tsx`, `progress-output.tsx`,
62
+ * `progress-track.tsx` and `progress-fill.tsx` cannot import the root without
63
+ * closing a cycle (AGENTS.md rule 3), yet all four read the same `color` and
64
+ * `size`.
65
+ *
66
+ * **The colour paints the fill and never the track.** An empty bar is the same
67
+ * chrome at every colour — the slider's groove, `bg-secondary` — and a test
68
+ * pins that.
69
+ *
70
+ * **The fill is a full-length bar slid under a clipping track**, not a bar whose
71
+ * width changes. Its position is a `translateX`, which is a transform — composited
72
+ * on the UI thread with no layout pass per frame, where an animated `width`
73
+ * re-lays the bar out every frame of every update. The track's `overflow-hidden`
74
+ * and `rounded-full` are what make that read as a bar growing from the left: the
75
+ * part parked beyond the start is clipped, and the leading end keeps its round
76
+ * cap because it is the fill's own end rather than a cut.
77
+ *
78
+ * **The size is the track's thickness and nothing else.** There is no thumb to
79
+ * keep flush and no touch target to pad, so these are plain spacing steps —
80
+ * thinner than the slider's groove, because a bar you read is lighter than a
81
+ * groove you grab.
82
+ *
83
+ * Free of React Native imports so it stays unit-testable — `bun test` cannot parse
84
+ * React Native's Flow-typed source. See AGENTS.md.
85
+ */
86
+ export const progressVariants = tv({
87
+ slots: {
88
+ /** The column holding the header and the track. */
89
+ root: "w-full gap-2",
90
+ /** The row a label and a readout share, when both are given. */
91
+ header: "flex-row items-center justify-between gap-3",
92
+ /** Handed to a `Text.Label`. Layout only. */
93
+ label: "shrink",
94
+ /** Handed to a `Text.Label`. Layout only. */
95
+ output: "",
96
+ /** The clipped, rounded groove the fill slides along. */
97
+ track: "relative w-full overflow-hidden rounded-full bg-secondary",
98
+ /** The painted bar. Its position is an animated transform, never a class. */
99
+ fill: "absolute top-0 bottom-0 left-0 rounded-full",
100
+ },
101
+ variants: {
102
+ size: {
103
+ sm: { track: "h-1" },
104
+ md: { track: "h-2" },
105
+ lg: { track: "h-3" },
106
+ },
107
+ color: {
108
+ default: { fill: "bg-foreground" },
109
+ primary: { fill: "bg-primary" },
110
+ success: { fill: "bg-success" },
111
+ warning: { fill: "bg-warning" },
112
+ destructive: { fill: "bg-destructive" },
113
+ info: { fill: "bg-info" },
114
+ },
115
+ // The empty branches are load-bearing typing: `tv` types a map holding only
116
+ // `true` as `true` rather than `boolean`.
117
+ isIndeterminate: { true: {}, false: {} },
118
+ },
119
+ defaultVariants: {
120
+ color: PROGRESS_DEFAULT_COLOR,
121
+ size: PROGRESS_DEFAULT_SIZE,
122
+ isIndeterminate: false,
123
+ },
124
+ });
125
+
126
+ /**
127
+ * Where a value sits in the range, as 0–1.
128
+ *
129
+ * Clamped rather than extrapolated, and `0` for a degenerate range or a value
130
+ * that is not a finite number: a `NaN` written into a shared value freezes the
131
+ * fill for good, and an upload that divides by a total it has not learned yet
132
+ * produces exactly that.
133
+ */
134
+ export function progressRatio(value: number, minValue: number, maxValue: number): number {
135
+ "worklet";
136
+ const span = maxValue - minValue;
137
+ if (!(span > 0) || !Number.isFinite(value)) return 0;
138
+ const ratio = (value - minValue) / span;
139
+ return ratio < 0 ? 0 : ratio > 1 ? 1 : ratio;
140
+ }
141
+
142
+ /**
143
+ * The fill's `translateX`: how far left of the track's start the full-length bar
144
+ * is parked.
145
+ *
146
+ * A bar at `ratio` shows `ratio × trackSize` of itself, so it sits back by the
147
+ * rest. `0` on an unmeasured track — the fill is hidden until the first layout,
148
+ * so there is nothing to place.
149
+ */
150
+ export function fillTranslate({ ratio, trackSize }: { ratio: number; trackSize: number }): number {
151
+ "worklet";
152
+ if (trackSize <= 0) return 0;
153
+ const clamped = ratio < 0 ? 0 : ratio > 1 ? 1 : ratio;
154
+ return (clamped - 1) * trackSize;
155
+ }
156
+
157
+ /**
158
+ * The indeterminate segment's box at one point in its loop.
159
+ *
160
+ * `phase` runs 0–1. At 0 the segment sits wholly off the track's left edge and at
161
+ * 1 wholly off its right, so the frame where the loop restarts is never on screen
162
+ * — a sweep that began at the track's start would pop into view every pass.
163
+ */
164
+ export function indeterminateSegment({
165
+ phase,
166
+ segment,
167
+ trackSize,
168
+ }: {
169
+ phase: number;
170
+ segment: number;
171
+ trackSize: number;
172
+ }): { translate: number; width: number } {
173
+ "worklet";
174
+ if (trackSize <= 0) return { translate: 0, width: 0 };
175
+ const width = segment * trackSize;
176
+ return { translate: -width + phase * (trackSize + width), width };
177
+ }
178
+
179
+ /** What {@link formatProgressValue} and {@link resolveProgressAccessibility} read. */
180
+ export type ProgressFormatInput = {
181
+ value: number;
182
+ minValue: number;
183
+ maxValue: number;
184
+ formatOptions?: Intl.NumberFormatOptions;
185
+ };
186
+
187
+ /** Whether the readout speaks a percentage of the range, rather than the value itself. */
188
+ function isPercentFormat(formatOptions: Intl.NumberFormatOptions | undefined): boolean {
189
+ return formatOptions?.style === undefined || formatOptions.style === "percent";
190
+ }
191
+
192
+ /**
193
+ * The readout's default text.
194
+ *
195
+ * With no `formatOptions`, or a `percent` style, it is the share of the range —
196
+ * `72%`, or `75%` for 18 of 24. A percent style formats the *ratio* rather than
197
+ * the value, because `Intl` multiplies by a hundred and a value of 72 would read
198
+ * `7,200%`. Any other style formats the value itself, so a budget reads `$1,250`.
199
+ *
200
+ * JS-thread only — `Intl` is not available to a worklet.
201
+ */
202
+ export function formatProgressValue({ value, minValue, maxValue, formatOptions }: ProgressFormatInput): string {
203
+ if (isPercentFormat(formatOptions)) {
204
+ const format = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0, ...formatOptions, style: "percent" });
205
+ return format.format(progressRatio(value, minValue, maxValue));
206
+ }
207
+ return new Intl.NumberFormat(undefined, formatOptions).format(value);
208
+ }
209
+
210
+ /** The accessibility surface a progress bar publishes on its root. */
211
+ export type ProgressAccessibility = {
212
+ busy: boolean;
213
+ value: { min: number; max: number; now: number; text: string } | undefined;
214
+ };
215
+
216
+ /**
217
+ * What assistive technology is told.
218
+ *
219
+ * **The spoken text names the range, not only the share of it.** A bar over
220
+ * 0–100 reads its percentage; any other range reads `18 of 24`, formatted the way
221
+ * the readout formats the value — because the count is what the screen is about,
222
+ * and "seventy-five percent" makes the listener do the division back.
223
+ * `valueLabel` replaces both.
224
+ *
225
+ * **An indeterminate bar reports `busy` and no value at all.** A `now` of zero
226
+ * would be read out as "zero percent", which is a lie about work that is under
227
+ * way.
228
+ */
229
+ export function resolveProgressAccessibility({
230
+ value,
231
+ minValue,
232
+ maxValue,
233
+ formatOptions,
234
+ isIndeterminate,
235
+ valueLabel,
236
+ }: ProgressFormatInput & { isIndeterminate: boolean; valueLabel?: string }): ProgressAccessibility {
237
+ if (isIndeterminate) return { busy: true, value: undefined };
238
+
239
+ const ratio = progressRatio(value, minValue, maxValue);
240
+ const now = minValue + ratio * (maxValue - minValue);
241
+ const isHundredScale = minValue === 0 && maxValue === 100;
242
+
243
+ let text = valueLabel;
244
+ if (text === undefined) {
245
+ if (isHundredScale && isPercentFormat(formatOptions)) {
246
+ text = formatProgressValue({ formatOptions, maxValue, minValue, value });
247
+ } else {
248
+ const format = new Intl.NumberFormat(
249
+ undefined,
250
+ isPercentFormat(formatOptions) ? { maximumFractionDigits: 2 } : formatOptions
251
+ );
252
+ text = `${format.format(now)} of ${format.format(maxValue)}`;
253
+ }
254
+ }
255
+
256
+ return { busy: false, value: { max: maxValue, min: minValue, now, text } };
257
+ }
258
+
259
+ /** What a progress bar was given at its own call site. */
260
+ export type ProgressOwnAxes = {
261
+ color?: ProgressColor;
262
+ size?: ProgressSize;
263
+ isIndeterminate?: boolean;
264
+ };
265
+
266
+ /** Every axis settled, ready to hand to {@link progressVariants} and to context. */
267
+ export type ProgressAxes = Required<ProgressOwnAxes>;
268
+
269
+ /**
270
+ * Settles a progress bar's axes.
271
+ *
272
+ * One rung, deliberately: a progress bar is an output, not a form control, so an
273
+ * enclosing `Field` has no disabled or invalid state to hand it. `??` rather than
274
+ * `||`, so an explicit `false` is a value rather than an absence.
275
+ */
276
+ export function resolveProgressAxes(own: ProgressOwnAxes): ProgressAxes {
277
+ return {
278
+ color: own.color ?? PROGRESS_DEFAULT_COLOR,
279
+ isIndeterminate: own.isIndeterminate ?? false,
280
+ size: own.size ?? PROGRESS_DEFAULT_SIZE,
281
+ };
282
+ }
283
+
284
+ export type ProgressVariantProps = VariantProps<typeof progressVariants>;