@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,130 @@
1
+ import { createContext, type ReactElement, type ReactNode, use } from "react";
2
+ import type { ButtonSize } from "../button/button.variants";
3
+ import type { HapticFeedback } from "../pressable";
4
+ import type {
5
+ ToggleButtonGroupLayout,
6
+ ToggleButtonGroupOrientation,
7
+ ToggleButtonSelectionMode,
8
+ ToggleButtonVariant,
9
+ } from "./toggle-button.variants";
10
+
11
+ export type ToggleButtonContextValue = {
12
+ /** Whether the toggle is pressed in right now. */
13
+ isSelected: boolean;
14
+ /** Whether the toggle is disabled, once its group has had its say. */
15
+ isDisabled: boolean;
16
+ /** The toggle's own variant, not the button variant it resolves to. */
17
+ variant: ToggleButtonVariant;
18
+ /** The size the toggle asked for, or `undefined` to take the button's default. */
19
+ size: ButtonSize | undefined;
20
+ };
21
+
22
+ const ToggleButtonContext = createContext<ToggleButtonContextValue | null>(null);
23
+
24
+ /**
25
+ * Supplies the enclosing toggle's state to its subtree.
26
+ *
27
+ * In a leaf of its own, importing nothing but types, so a part can read it
28
+ * without importing `./toggle-button` and closing a cycle (package AGENTS.md
29
+ * rule 3).
30
+ */
31
+ export function ToggleButtonProvider({
32
+ value,
33
+ children,
34
+ }: {
35
+ value: ToggleButtonContextValue;
36
+ children: ReactNode;
37
+ }): ReactElement {
38
+ return <ToggleButtonContext value={value}>{children}</ToggleButtonContext>;
39
+ }
40
+ ToggleButtonProvider.displayName = "DelacourUI.ToggleButton.Provider";
41
+
42
+ /** The enclosing toggle's state, or null outside a `<ToggleButton>`. */
43
+ export function useToggleButtonContext(): ToggleButtonContextValue | null {
44
+ return use(ToggleButtonContext);
45
+ }
46
+
47
+ /**
48
+ * Reads the enclosing toggle's state.
49
+ *
50
+ * For a custom child that changes with the toggle — a glyph that fills when
51
+ * selected. Throws outside a `<ToggleButton>` — use
52
+ * {@link useToggleButtonContext} where the toggle is optional.
53
+ */
54
+ export function useToggleButton(): ToggleButtonContextValue {
55
+ const context = useToggleButtonContext();
56
+ if (!context) {
57
+ throw new Error("useToggleButton must be called inside a <ToggleButton>.");
58
+ }
59
+ return context;
60
+ }
61
+
62
+ /**
63
+ * The enclosing toggle's state, for a compound part that cannot work without it.
64
+ *
65
+ * Internal: deliberately not re-exported from `index.ts`.
66
+ */
67
+ export function useToggleButtonPart(component: string): ToggleButtonContextValue {
68
+ const context = useToggleButtonContext();
69
+ if (!context) {
70
+ throw new Error(`${component} must be rendered inside a <ToggleButton>.`);
71
+ }
72
+ return context;
73
+ }
74
+
75
+ export type ToggleButtonGroupContextValue = {
76
+ /** The `value` of every selected member. */
77
+ selected: readonly string[];
78
+ /** Presses the member called `value`, as the selection mode decides. */
79
+ toggle: (value: string) => void;
80
+ selectionMode: ToggleButtonSelectionMode;
81
+ layout: ToggleButtonGroupLayout;
82
+ orientation: ToggleButtonGroupOrientation;
83
+ /** A default a member may override. */
84
+ variant?: ToggleButtonVariant;
85
+ /** A default a member may override. An attached group owns the step outright. */
86
+ size?: ButtonSize;
87
+ /** A default a member may override, so one option can disable itself. */
88
+ isDisabled?: boolean;
89
+ /** A default a member may override. */
90
+ haptic?: false | HapticFeedback;
91
+ };
92
+
93
+ const ToggleButtonGroupContext = createContext<ToggleButtonGroupContextValue | null>(null);
94
+
95
+ /**
96
+ * Supplies a group's selection and shared axes to its members.
97
+ *
98
+ * The optional axes are published raw, so `undefined` means "the group said
99
+ * nothing" and a member's `??` ladder can see past it.
100
+ */
101
+ export function ToggleButtonGroupProvider({
102
+ value,
103
+ children,
104
+ }: {
105
+ value: ToggleButtonGroupContextValue;
106
+ children: ReactNode;
107
+ }): ReactElement {
108
+ return <ToggleButtonGroupContext value={value}>{children}</ToggleButtonGroupContext>;
109
+ }
110
+ ToggleButtonGroupProvider.displayName = "DelacourUI.ToggleButton.Group.Provider";
111
+
112
+ /** The enclosing group, or null for a toggle standing on its own. */
113
+ export function useToggleButtonGroupContext(): ToggleButtonGroupContextValue | null {
114
+ return use(ToggleButtonGroupContext);
115
+ }
116
+
117
+ /**
118
+ * Reads the enclosing group's selection and axes.
119
+ *
120
+ * Throws outside a `<ToggleButton.Group>` — use
121
+ * {@link useToggleButtonGroupContext} where the group is optional, as a
122
+ * `ToggleButton` itself does.
123
+ */
124
+ export function useToggleButtonGroup(): ToggleButtonGroupContextValue {
125
+ const context = useToggleButtonGroupContext();
126
+ if (!context) {
127
+ throw new Error("useToggleButtonGroup must be called inside a <ToggleButton.Group>.");
128
+ }
129
+ return context;
130
+ }
@@ -0,0 +1,172 @@
1
+ import { type ReactElement, type ReactNode, useCallback, useMemo } from "react";
2
+ import { useControllableState } from "../../hooks/use-controllable-state";
3
+ import { Button, type ButtonProps } from "../button";
4
+ import {
5
+ type ToggleButtonContextValue,
6
+ ToggleButtonProvider,
7
+ useToggleButtonGroupContext,
8
+ } from "./toggle-button.context";
9
+ import {
10
+ resolveToggleButtonAccessibility,
11
+ resolveToggleButtonVariant,
12
+ type ToggleButtonVariant,
13
+ } from "./toggle-button.variants";
14
+ import { ToggleButtonGroup } from "./toggle-button-group";
15
+ import { ToggleButtonLabel } from "./toggle-button-label";
16
+
17
+ /** What a render-function child is handed. */
18
+ export type ToggleButtonRenderState = {
19
+ isSelected: boolean;
20
+ isDisabled: boolean;
21
+ };
22
+
23
+ export type ToggleButtonProps = Omit<
24
+ ButtonProps,
25
+ "accessibilityRole" | "accessibilityState" | "children" | "variant"
26
+ > & {
27
+ /** Identifies the toggle inside a `ToggleButton.Group`. Required there. */
28
+ value?: string;
29
+ /** How the toggle paints itself, off and on. Inherited from a group when not given. */
30
+ variant?: ToggleButtonVariant;
31
+ /** Controlled selection. Ignored inside a group, which owns the state. */
32
+ isSelected?: boolean;
33
+ /** Starting state while uncontrolled and outside a group. */
34
+ defaultSelected?: boolean;
35
+ /** Called with the new state on every press. Not called inside a group — the group's `onSelected` is. */
36
+ onSelected?: (isSelected: boolean) => void;
37
+ /** Content, or a function of the toggle's state for content that changes with it. */
38
+ children?: ReactNode | ((state: ToggleButtonRenderState) => ReactNode);
39
+ };
40
+
41
+ function ToggleButtonRoot({
42
+ value,
43
+ variant,
44
+ size,
45
+ isSelected,
46
+ defaultSelected = false,
47
+ onSelected,
48
+ isDisabled,
49
+ haptic,
50
+ onPress,
51
+ children,
52
+ ...props
53
+ }: ToggleButtonProps): ReactElement {
54
+ const group = useToggleButtonGroupContext();
55
+
56
+ // Called unconditionally, grouped or not: a hook may not sit behind a branch.
57
+ // Inside a group its state is simply never read.
58
+ const [ownSelected, setOwnSelected] = useControllableState<boolean>({
59
+ defaultValue: defaultSelected,
60
+ onChange: onSelected,
61
+ value: isSelected,
62
+ });
63
+
64
+ if (process.env.NODE_ENV !== "production" && group && value === undefined) {
65
+ console.warn(
66
+ "ToggleButton: a <ToggleButton> inside a <ToggleButton.Group> needs a `value`, or it can never be selected."
67
+ );
68
+ }
69
+
70
+ const resolvedIsSelected = group ? value !== undefined && group.selected.includes(value) : ownSelected;
71
+ const resolvedVariant = variant ?? group?.variant ?? "default";
72
+ const resolvedSize = size ?? group?.size;
73
+ const resolvedIsDisabled = isDisabled ?? group?.isDisabled ?? false;
74
+
75
+ // The caller's `onPress` runs first, then the state moves — so a handler
76
+ // reading the state it closed over sees what the toggle was when pressed.
77
+ const handlePress = useCallback(() => {
78
+ onPress?.();
79
+ if (group) {
80
+ if (value !== undefined) group.toggle(value);
81
+ return;
82
+ }
83
+ setOwnSelected(!ownSelected);
84
+ }, [group, onPress, ownSelected, setOwnSelected, value]);
85
+
86
+ const context = useMemo<ToggleButtonContextValue>(
87
+ () => ({
88
+ isDisabled: resolvedIsDisabled,
89
+ isSelected: resolvedIsSelected,
90
+ size: resolvedSize,
91
+ variant: resolvedVariant,
92
+ }),
93
+ [resolvedIsDisabled, resolvedIsSelected, resolvedSize, resolvedVariant]
94
+ );
95
+
96
+ const accessibility = resolveToggleButtonAccessibility(group?.selectionMode ?? null, resolvedIsSelected);
97
+
98
+ const content =
99
+ typeof children === "function"
100
+ ? children({ isDisabled: resolvedIsDisabled, isSelected: resolvedIsSelected })
101
+ : children;
102
+
103
+ // The role, state and press handler are set after the caller's props on
104
+ // purpose: they are what keeps the announced state and the drawn state one
105
+ // and the same, so nothing passed in may pull them apart.
106
+ return (
107
+ <ToggleButtonProvider value={context}>
108
+ <Button
109
+ {...props}
110
+ accessibilityRole={accessibility.accessibilityRole}
111
+ accessibilityState={accessibility.accessibilityState}
112
+ haptic={haptic ?? group?.haptic}
113
+ isDisabled={resolvedIsDisabled}
114
+ onPress={handlePress}
115
+ size={resolvedSize}
116
+ variant={resolveToggleButtonVariant(resolvedVariant, resolvedIsSelected)}
117
+ >
118
+ {content}
119
+ </Button>
120
+ </ToggleButtonProvider>
121
+ );
122
+ }
123
+
124
+ /**
125
+ * A button that stays pressed — on its own, or as one of a group.
126
+ *
127
+ * It is a `Button`, and draws itself with one: each state names a button
128
+ * variant (`TOGGLE_BUTTON_APPEARANCE`), so the fill, the label colour and the
129
+ * colour a composed `Icon` inherits all come off the button's own tables. Sizes
130
+ * are the button's, square `icon-*` steps included, and so are `haptic`,
131
+ * `feedback`, `isLoading` and the rest.
132
+ *
133
+ * On its own it is controlled with `isSelected` and `onSelected`, or
134
+ * uncontrolled from `defaultSelected`. Inside a `ToggleButton.Group` its `value`
135
+ * identifies it and the group owns the state.
136
+ *
137
+ * The state is announced, not only drawn: a toggle is a `togglebutton` that is
138
+ * checked or not, and a member of a `single` group is a `radio` that is selected
139
+ * or not. A square toggle holds no text, so give it an `accessibilityLabel`.
140
+ *
141
+ * Children may be a function of the state, for content that changes with it.
142
+ *
143
+ * @example
144
+ * <ToggleButton defaultSelected>Follow</ToggleButton>
145
+ *
146
+ * @example
147
+ * <ToggleButton isSelected={saved} onSelected={setSaved} variant="outline">
148
+ * {({ isSelected }) => (
149
+ * <>
150
+ * <Icon icon={isSelected ? IconBookmarkCheck : IconBookmark} />
151
+ * <ToggleButton.Label>{isSelected ? "Saved" : "Save"}</ToggleButton.Label>
152
+ * </>
153
+ * )}
154
+ * </ToggleButton>
155
+ *
156
+ * @example
157
+ * <ToggleButton.Group selectionMode="single" defaultSelected={["left"]} isSelectionRequired>
158
+ * <ToggleButton accessibilityLabel="Align left" size="icon-md" value="left">
159
+ * <Icon icon={IconAlignmentLeft} />
160
+ * </ToggleButton>
161
+ * <ToggleButton accessibilityLabel="Align centre" size="icon-md" value="center">
162
+ * <Icon icon={IconAlignmentCenter} />
163
+ * </ToggleButton>
164
+ * </ToggleButton.Group>
165
+ */
166
+ export const ToggleButton = Object.assign(ToggleButtonRoot, {
167
+ /** The toggle's text. Picks its colour from the state the toggle is in. */
168
+ Label: ToggleButtonLabel,
169
+ /** Toggles sharing one selection, joined into a run or spaced apart. */
170
+ Group: ToggleButtonGroup,
171
+ displayName: "DelacourUI.ToggleButton",
172
+ });
@@ -0,0 +1,189 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BUTTON_VARIANTS } from "../button/button.variants";
3
+ import {
4
+ resolveToggleButtonAccessibility,
5
+ resolveToggleButtonVariant,
6
+ resolveToggleSelection,
7
+ TOGGLE_BUTTON_APPEARANCE,
8
+ TOGGLE_BUTTON_GROUP_LAYOUTS,
9
+ TOGGLE_BUTTON_SELECTION_MODES,
10
+ TOGGLE_BUTTON_VARIANTS,
11
+ toggleButtonVariants,
12
+ } from "./toggle-button.variants";
13
+
14
+ describe("TOGGLE_BUTTON_APPEARANCE", () => {
15
+ test("covers every variant, in both states", () => {
16
+ expect(Object.keys(TOGGLE_BUTTON_APPEARANCE).sort()).toEqual([...TOGGLE_BUTTON_VARIANTS].sort());
17
+ });
18
+
19
+ test("names only button variants, so the button's own paint and foreground apply", () => {
20
+ for (const variant of TOGGLE_BUTTON_VARIANTS) {
21
+ expect(BUTTON_VARIANTS).toContain(TOGGLE_BUTTON_APPEARANCE[variant].off);
22
+ expect(BUTTON_VARIANTS).toContain(TOGGLE_BUTTON_APPEARANCE[variant].on);
23
+ }
24
+ });
25
+
26
+ test("a selected button never looks like an unselected one", () => {
27
+ for (const variant of TOGGLE_BUTTON_VARIANTS) {
28
+ expect(TOGGLE_BUTTON_APPEARANCE[variant].on).not.toBe(TOGGLE_BUTTON_APPEARANCE[variant].off);
29
+ }
30
+ });
31
+
32
+ test("never paints a toggle destructive", () => {
33
+ for (const variant of TOGGLE_BUTTON_VARIANTS) {
34
+ for (const state of ["on", "off"] as const) {
35
+ expect(TOGGLE_BUTTON_APPEARANCE[variant][state].startsWith("destructive")).toBe(false);
36
+ }
37
+ }
38
+ });
39
+ });
40
+
41
+ describe("resolveToggleButtonVariant", () => {
42
+ test("reads the appearance table", () => {
43
+ expect(resolveToggleButtonVariant("default", false)).toBe("secondary");
44
+ expect(resolveToggleButtonVariant("default", true)).toBe("primary");
45
+ expect(resolveToggleButtonVariant("outline", false)).toBe("outline");
46
+ expect(resolveToggleButtonVariant("outline", true)).toBe("primary");
47
+ expect(resolveToggleButtonVariant("ghost", false)).toBe("ghost");
48
+ expect(resolveToggleButtonVariant("ghost", true)).toBe("secondary");
49
+ });
50
+ });
51
+
52
+ describe("resolveToggleSelection — multiple", () => {
53
+ const mode = "multiple" as const;
54
+
55
+ test("adds a value that was not selected, keeping press order", () => {
56
+ expect(resolveToggleSelection({ current: ["bold"], selectionMode: mode, value: "italic" })).toEqual([
57
+ "bold",
58
+ "italic",
59
+ ]);
60
+ });
61
+
62
+ test("removes a value that was selected", () => {
63
+ expect(resolveToggleSelection({ current: ["bold", "italic"], selectionMode: mode, value: "bold" })).toEqual([
64
+ "italic",
65
+ ]);
66
+ });
67
+
68
+ test("clears the last value when a selection is not required", () => {
69
+ expect(resolveToggleSelection({ current: ["bold"], selectionMode: mode, value: "bold" })).toEqual([]);
70
+ });
71
+
72
+ test("refuses to clear the last value when a selection is required", () => {
73
+ expect(
74
+ resolveToggleSelection({ current: ["bold"], isSelectionRequired: true, selectionMode: mode, value: "bold" })
75
+ ).toBeNull();
76
+ });
77
+
78
+ test("still removes one of several when a selection is required", () => {
79
+ expect(
80
+ resolveToggleSelection({
81
+ current: ["bold", "italic"],
82
+ isSelectionRequired: true,
83
+ selectionMode: mode,
84
+ value: "bold",
85
+ })
86
+ ).toEqual(["italic"]);
87
+ });
88
+
89
+ test("does not mutate the list it was given", () => {
90
+ const current = ["bold"];
91
+ resolveToggleSelection({ current, selectionMode: mode, value: "italic" });
92
+ resolveToggleSelection({ current, selectionMode: mode, value: "bold" });
93
+ expect(current).toEqual(["bold"]);
94
+ });
95
+ });
96
+
97
+ describe("resolveToggleSelection — single", () => {
98
+ const mode = "single" as const;
99
+
100
+ test("replaces the selection with the pressed value", () => {
101
+ expect(resolveToggleSelection({ current: ["left"], selectionMode: mode, value: "center" })).toEqual(["center"]);
102
+ });
103
+
104
+ test("selects from nothing", () => {
105
+ expect(resolveToggleSelection({ current: [], selectionMode: mode, value: "center" })).toEqual(["center"]);
106
+ });
107
+
108
+ test("a re-press clears the selection when one is not required", () => {
109
+ expect(resolveToggleSelection({ current: ["left"], selectionMode: mode, value: "left" })).toEqual([]);
110
+ });
111
+
112
+ test("a re-press is no change when a selection is required", () => {
113
+ expect(
114
+ resolveToggleSelection({ current: ["left"], isSelectionRequired: true, selectionMode: mode, value: "left" })
115
+ ).toBeNull();
116
+ });
117
+
118
+ test("collapses a controlled list that arrived with several values", () => {
119
+ expect(resolveToggleSelection({ current: ["left", "right"], selectionMode: mode, value: "center" })).toEqual([
120
+ "center",
121
+ ]);
122
+ });
123
+ });
124
+
125
+ describe("resolveToggleButtonAccessibility", () => {
126
+ test("a standalone button is a toggle button that is checked or not", () => {
127
+ expect(resolveToggleButtonAccessibility(null, true)).toEqual({
128
+ accessibilityRole: "togglebutton",
129
+ accessibilityState: { checked: true },
130
+ });
131
+ expect(resolveToggleButtonAccessibility(null, false)).toEqual({
132
+ accessibilityRole: "togglebutton",
133
+ accessibilityState: { checked: false },
134
+ });
135
+ });
136
+
137
+ test("a member of a multiple-selection group is still a toggle button", () => {
138
+ expect(resolveToggleButtonAccessibility("multiple", true)).toEqual({
139
+ accessibilityRole: "togglebutton",
140
+ accessibilityState: { checked: true },
141
+ });
142
+ });
143
+
144
+ test("a member of a single-selection group is a radio, selected or not", () => {
145
+ expect(resolveToggleButtonAccessibility("single", true)).toEqual({
146
+ accessibilityRole: "radio",
147
+ accessibilityState: { selected: true },
148
+ });
149
+ expect(resolveToggleButtonAccessibility("single", false)).toEqual({
150
+ accessibilityRole: "radio",
151
+ accessibilityState: { selected: false },
152
+ });
153
+ });
154
+ });
155
+
156
+ describe("toggleButtonVariants", () => {
157
+ test("exposes both selection modes and both layouts", () => {
158
+ expect([...TOGGLE_BUTTON_SELECTION_MODES]).toEqual(["single", "multiple"]);
159
+ expect([...TOGGLE_BUTTON_GROUP_LAYOUTS]).toEqual(["attached", "detached"]);
160
+ });
161
+
162
+ test("an attached group adds nothing — Button.Group draws the run", () => {
163
+ // `tv` returns undefined rather than "" for a slot with no classes.
164
+ expect(toggleButtonVariants({ layout: "attached", orientation: "horizontal" }).group() ?? "").toBe("");
165
+ expect(toggleButtonVariants({ layout: "attached", orientation: "vertical" }).group() ?? "").toBe("");
166
+ });
167
+
168
+ test("a detached horizontal group wraps with a gap", () => {
169
+ const group = toggleButtonVariants({ layout: "detached", orientation: "horizontal" }).group();
170
+ expect(group).toContain("flex-row");
171
+ expect(group).toContain("flex-wrap");
172
+ expect(group).toContain("gap-2");
173
+ });
174
+
175
+ test("a detached vertical group stacks with a gap", () => {
176
+ const group = toggleButtonVariants({ layout: "detached", orientation: "vertical" }).group();
177
+ expect(group).toContain("flex-col");
178
+ expect(group).toContain("gap-2");
179
+ expect(group).not.toContain("flex-row");
180
+ });
181
+
182
+ test("a caller's className merges over the group's own", () => {
183
+ const group = toggleButtonVariants({ layout: "detached", orientation: "horizontal" }).group({
184
+ className: "gap-4",
185
+ });
186
+ expect(group).toContain("gap-4");
187
+ expect(group).not.toContain("gap-2");
188
+ });
189
+ });
@@ -0,0 +1,160 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+ import type { ButtonVariant } from "../button/button.variants";
4
+
5
+ /**
6
+ * How a toggle paints itself, off and on.
7
+ *
8
+ * `default` is a quiet fill that turns into the action colour; `outline` is a
9
+ * bordered button that fills; `ghost` is bare until pressed and then takes the
10
+ * quiet fill, which is what a toolbar of formatting marks wants.
11
+ */
12
+ export const TOGGLE_BUTTON_VARIANTS = ["default", "outline", "ghost"] as const;
13
+
14
+ /**
15
+ * `multiple` is a set of independent marks — bold *and* italic. `single` is an
16
+ * either-or choice, where picking one clears the last.
17
+ */
18
+ export const TOGGLE_BUTTON_SELECTION_MODES = ["single", "multiple"] as const;
19
+
20
+ /**
21
+ * `attached` joins the members into one run through `Button.Group`, sharing a
22
+ * corner and a seam. `detached` spaces them apart and wraps — a row of filters.
23
+ */
24
+ export const TOGGLE_BUTTON_GROUP_LAYOUTS = ["attached", "detached"] as const;
25
+
26
+ export const TOGGLE_BUTTON_GROUP_ORIENTATIONS = ["horizontal", "vertical"] as const;
27
+
28
+ export type ToggleButtonVariant = (typeof TOGGLE_BUTTON_VARIANTS)[number];
29
+ export type ToggleButtonSelectionMode = (typeof TOGGLE_BUTTON_SELECTION_MODES)[number];
30
+ export type ToggleButtonGroupLayout = (typeof TOGGLE_BUTTON_GROUP_LAYOUTS)[number];
31
+ export type ToggleButtonGroupOrientation = (typeof TOGGLE_BUTTON_GROUP_ORIENTATIONS)[number];
32
+
33
+ /**
34
+ * The `Button` variant each toggle variant draws with, off and on.
35
+ *
36
+ * A toggle owns no paint of its own. Every state is a button variant, so the
37
+ * fill, the border, the label colour and the colour a composed `Icon` inherits
38
+ * all come off `buttonVariants` and `BUTTON_FOREGROUND_TOKEN` — there is no
39
+ * second table of colours here to drift from the button's. Every variant the
40
+ * button already has draws with the same `border` box, so switching state never
41
+ * resizes the toggle.
42
+ *
43
+ * No state is ever destructive: a pressed toggle is a choice, not a warning.
44
+ */
45
+ export const TOGGLE_BUTTON_APPEARANCE: Record<ToggleButtonVariant, { off: ButtonVariant; on: ButtonVariant }> = {
46
+ default: { off: "secondary", on: "primary" },
47
+ outline: { off: "outline", on: "primary" },
48
+ ghost: { off: "ghost", on: "secondary" },
49
+ };
50
+
51
+ /**
52
+ * Styling for the group's box. The toggle itself is a `Button` and carries none.
53
+ *
54
+ * An `attached` group adds nothing: it renders a `Button.Group`, which already
55
+ * owns the run's direction, its corners and its seams. A `detached` group is a
56
+ * plain wrapping row, or a column, spaced by a gap — the one thing an attached
57
+ * run must never have.
58
+ *
59
+ * Free of React Native imports so it stays unit-testable. See AGENTS.md.
60
+ */
61
+ export const toggleButtonVariants = tv({
62
+ slots: {
63
+ group: "",
64
+ },
65
+ variants: {
66
+ layout: { attached: {}, detached: {} },
67
+ orientation: { horizontal: {}, vertical: {} },
68
+ },
69
+ compoundVariants: [
70
+ { layout: "detached", orientation: "horizontal", class: { group: "flex-row flex-wrap gap-2" } },
71
+ { layout: "detached", orientation: "vertical", class: { group: "flex-col gap-2" } },
72
+ ],
73
+ defaultVariants: {
74
+ layout: "attached",
75
+ orientation: "horizontal",
76
+ },
77
+ });
78
+
79
+ /**
80
+ * The `Button` variant a toggle draws with right now.
81
+ *
82
+ * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
83
+ */
84
+ export function resolveToggleButtonVariant(variant: ToggleButtonVariant, isSelected: boolean): ButtonVariant {
85
+ return TOGGLE_BUTTON_APPEARANCE[variant][isSelected ? "on" : "off"];
86
+ }
87
+
88
+ /**
89
+ * The group's next selection after `value` is pressed, or `null` for no change.
90
+ *
91
+ * `null` rather than the same list back, so the group never re-notifies a caller
92
+ * with a value it already holds — the same rule `Radio.Group` keeps for a
93
+ * re-press of the current option.
94
+ *
95
+ * - `multiple` adds or removes `value`, keeping the order things were pressed in.
96
+ * - `single` replaces the selection with `value`. A re-press of the selected one
97
+ * clears it, unless `isSelectionRequired`, when it is no change at all — a
98
+ * segmented choice like text alignment always has an answer.
99
+ * - `isSelectionRequired` also refuses to empty a `multiple` group, so the last
100
+ * mark cannot be taken off.
101
+ *
102
+ * A controlled `single` group handed several values collapses to the one pressed
103
+ * rather than carrying the others along.
104
+ *
105
+ * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
106
+ */
107
+ export function resolveToggleSelection({
108
+ current,
109
+ value,
110
+ selectionMode,
111
+ isSelectionRequired = false,
112
+ }: {
113
+ current: readonly string[];
114
+ value: string;
115
+ selectionMode: ToggleButtonSelectionMode;
116
+ isSelectionRequired?: boolean;
117
+ }): string[] | null {
118
+ const isSelected = current.includes(value);
119
+
120
+ if (selectionMode === "single") {
121
+ if (!isSelected) return [value];
122
+ if (isSelectionRequired) return null;
123
+ return [];
124
+ }
125
+
126
+ if (!isSelected) return [...current, value];
127
+ const next = current.filter((entry) => entry !== value);
128
+ if (isSelectionRequired && next.length === 0) return null;
129
+ return next;
130
+ }
131
+
132
+ /** A toggle's announced role — see {@link resolveToggleButtonAccessibility}. */
133
+ export type ToggleButtonAccessibility =
134
+ | { accessibilityRole: "togglebutton"; accessibilityState: { checked: boolean } }
135
+ | { accessibilityRole: "radio"; accessibilityState: { selected: boolean } };
136
+
137
+ /**
138
+ * What a screen reader is told a toggle is.
139
+ *
140
+ * A toggle standing alone, or one of a set of independent marks, is a
141
+ * `togglebutton` that is checked or not — read out as on or off. A member of a
142
+ * `single` group is a choice among options, which is a radio: announcing it as a
143
+ * toggle would promise that pressing it again turns it off, and in a group that
144
+ * requires a selection it does not.
145
+ *
146
+ * `null` is "not in a group".
147
+ *
148
+ * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
149
+ */
150
+ export function resolveToggleButtonAccessibility(
151
+ selectionMode: ToggleButtonSelectionMode | null,
152
+ isSelected: boolean
153
+ ): ToggleButtonAccessibility {
154
+ if (selectionMode === "single") {
155
+ return { accessibilityRole: "radio", accessibilityState: { selected: isSelected } };
156
+ }
157
+ return { accessibilityRole: "togglebutton", accessibilityState: { checked: isSelected } };
158
+ }
159
+
160
+ export type ToggleButtonVariantProps = VariantProps<typeof toggleButtonVariants>;