@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,292 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { declaredTokens } from "../../styles/theme-tokens.test";
3
+ import {
4
+ resolveBarVisible,
5
+ resolveGridItemWidth,
6
+ resolveGroupLayout,
7
+ resolveHeaderCount,
8
+ resolveIndicatorShown,
9
+ resolveIsAllSelected,
10
+ resolveIsSameSelection,
11
+ resolveItemAccessibility,
12
+ resolveItemPress,
13
+ resolveSelectAll,
14
+ resolveToggle,
15
+ SELECTION_BAR_PLACEMENTS,
16
+ SELECTION_INDICATORS,
17
+ SELECTION_MODE_ACTION_FOREGROUND_TOKEN,
18
+ SELECTION_MODE_CHECK_TOKEN,
19
+ SELECTION_MODE_INDICATOR_OFFSET,
20
+ selectionModeVariants,
21
+ } from "./selection-mode.variants";
22
+
23
+ const LIGHT = declaredTokens("light");
24
+ const DARK = declaredTokens("dark");
25
+
26
+ describe("resolveToggle", () => {
27
+ test("adds an unpicked value at the end, keeping the order things were picked in", () => {
28
+ expect(resolveToggle({ selected: ["b", "a"], value: "c" })).toEqual(["b", "a", "c"]);
29
+ });
30
+
31
+ test("removes a picked value", () => {
32
+ expect(resolveToggle({ selected: ["b", "a", "c"], value: "a" })).toEqual(["b", "c"]);
33
+ });
34
+
35
+ test("a disabled value never moves", () => {
36
+ expect(resolveToggle({ isDisabled: true, selected: ["a"], value: "b" })).toEqual(["a"]);
37
+ expect(resolveToggle({ isDisabled: true, selected: ["a"], value: "a" })).toEqual(["a"]);
38
+ });
39
+
40
+ test("refuses to add past the cap, but still removes at it", () => {
41
+ expect(resolveToggle({ max: 2, selected: ["a", "b"], value: "c" })).toEqual(["a", "b"]);
42
+ expect(resolveToggle({ max: 2, selected: ["a", "b"], value: "a" })).toEqual(["b"]);
43
+ expect(resolveToggle({ max: 2, selected: ["a"], value: "c" })).toEqual(["a", "c"]);
44
+ });
45
+
46
+ test("never mutates the list it was handed", () => {
47
+ const selected = ["a"] as const;
48
+ resolveToggle({ selected, value: "b" });
49
+ expect(selected).toEqual(["a"]);
50
+ });
51
+ });
52
+
53
+ describe("resolveSelectAll", () => {
54
+ test("keeps existing picks first, then fills in list order", () => {
55
+ expect(resolveSelectAll({ selected: ["c"], values: ["a", "b", "c", "d"] })).toEqual(["c", "a", "b", "d"]);
56
+ });
57
+
58
+ test("stops at the cap", () => {
59
+ expect(resolveSelectAll({ max: 2, selected: [], values: ["a", "b", "c"] })).toEqual(["a", "b"]);
60
+ expect(resolveSelectAll({ max: 2, selected: ["c"], values: ["a", "b", "c"] })).toEqual(["c", "a"]);
61
+ });
62
+
63
+ test("never drops a pick already past the cap", () => {
64
+ expect(resolveSelectAll({ max: 1, selected: ["a", "b"], values: ["a", "b", "c"] })).toEqual(["a", "b"]);
65
+ });
66
+
67
+ test("with nothing to pick from, changes nothing", () => {
68
+ expect(resolveSelectAll({ selected: ["a"], values: [] })).toEqual(["a"]);
69
+ });
70
+ });
71
+
72
+ describe("resolveIsAllSelected", () => {
73
+ test("true once every value is picked", () => {
74
+ expect(resolveIsAllSelected({ selected: ["b", "a"], values: ["a", "b"] })).toBe(true);
75
+ expect(resolveIsAllSelected({ selected: ["a"], values: ["a", "b"] })).toBe(false);
76
+ });
77
+
78
+ test("true at the cap when the cap is below the total", () => {
79
+ expect(resolveIsAllSelected({ max: 2, selected: ["a", "c"], values: ["a", "b", "c"] })).toBe(true);
80
+ expect(resolveIsAllSelected({ max: 2, selected: ["a"], values: ["a", "b", "c"] })).toBe(false);
81
+ });
82
+
83
+ test("a cap above the total changes nothing", () => {
84
+ expect(resolveIsAllSelected({ max: 9, selected: ["a"], values: ["a", "b"] })).toBe(false);
85
+ expect(resolveIsAllSelected({ max: 9, selected: ["a", "b"], values: ["a", "b"] })).toBe(true);
86
+ });
87
+
88
+ test("an empty list is never all selected", () => {
89
+ expect(resolveIsAllSelected({ selected: [], values: [] })).toBe(false);
90
+ });
91
+ });
92
+
93
+ describe("resolveIsSameSelection", () => {
94
+ test("compares contents in order", () => {
95
+ expect(resolveIsSameSelection(["a", "b"], ["a", "b"])).toBe(true);
96
+ expect(resolveIsSameSelection(["a", "b"], ["b", "a"])).toBe(false);
97
+ expect(resolveIsSameSelection(["a"], ["a", "b"])).toBe(false);
98
+ expect(resolveIsSameSelection([], [])).toBe(true);
99
+ });
100
+ });
101
+
102
+ describe("resolveItemPress", () => {
103
+ test("inactive, a press is the row's own", () => {
104
+ expect(resolveItemPress({ isActive: false, isDisabled: false })).toEqual({ kind: "press" });
105
+ });
106
+
107
+ test("active, a press toggles", () => {
108
+ expect(resolveItemPress({ isActive: true, isDisabled: false })).toEqual({ kind: "toggle" });
109
+ });
110
+
111
+ test("a disabled item keeps its own press, even while active", () => {
112
+ expect(resolveItemPress({ isActive: true, isDisabled: true })).toEqual({ kind: "press" });
113
+ expect(resolveItemPress({ isActive: false, isDisabled: true })).toEqual({ kind: "press" });
114
+ });
115
+
116
+ test("a press with no handler to run is no press at all", () => {
117
+ expect(resolveItemPress({ hasOnPress: false, isActive: false, isDisabled: false })).toEqual({ kind: "none" });
118
+ expect(resolveItemPress({ hasOnPress: false, isActive: true, isDisabled: true })).toEqual({ kind: "none" });
119
+ expect(resolveItemPress({ hasOnPress: false, isActive: true, isDisabled: false })).toEqual({ kind: "toggle" });
120
+ });
121
+ });
122
+
123
+ describe("resolveHeaderCount", () => {
124
+ test("n of m with a total, prefixed by the title", () => {
125
+ expect(resolveHeaderCount({ count: 3, total: 10 })).toBe("3 of 10");
126
+ expect(resolveHeaderCount({ count: 3, title: "Select", total: 10 })).toBe("Select · 3 of 10");
127
+ });
128
+
129
+ test("n selected without one", () => {
130
+ expect(resolveHeaderCount({ count: 2 })).toBe("2 selected");
131
+ expect(resolveHeaderCount({ count: 2, title: "Select" })).toBe("2 selected");
132
+ });
133
+ });
134
+
135
+ describe("resolveGroupLayout", () => {
136
+ test("stacks by default", () => {
137
+ expect(resolveGroupLayout({})).toEqual({ kind: "stack" });
138
+ expect(resolveGroupLayout({ columns: 1 })).toEqual({ kind: "stack" });
139
+ });
140
+
141
+ test("a grid at two or more columns, floored", () => {
142
+ expect(resolveGroupLayout({ columns: 5 })).toEqual({ columns: 5, kind: "grid" });
143
+ expect(resolveGroupLayout({ columns: 3.7 })).toEqual({ columns: 3, kind: "grid" });
144
+ });
145
+
146
+ test("horizontal wins over columns", () => {
147
+ expect(resolveGroupLayout({ columns: 4, isHorizontal: true })).toEqual({ kind: "strip" });
148
+ });
149
+ });
150
+
151
+ describe("resolveGridItemWidth", () => {
152
+ test("splits the row less its gaps", () => {
153
+ expect(resolveGridItemWidth({ columns: 4, containerWidth: 336, gap: 12 })).toBe(75);
154
+ });
155
+
156
+ test("zero before the row has been measured", () => {
157
+ expect(resolveGridItemWidth({ columns: 4, containerWidth: 0, gap: 12 })).toBe(0);
158
+ });
159
+ });
160
+
161
+ describe("resolveIndicatorShown", () => {
162
+ test("shown only while active, unless always shown", () => {
163
+ expect(resolveIndicatorShown({ indicator: "leading", isActive: false })).toBe(false);
164
+ expect(resolveIndicatorShown({ indicator: "leading", isActive: true })).toBe(true);
165
+ expect(resolveIndicatorShown({ indicator: "leading", isActive: false, isIndicatorAlwaysShown: true })).toBe(true);
166
+ });
167
+
168
+ test("none never draws", () => {
169
+ expect(resolveIndicatorShown({ indicator: "none", isActive: true, isIndicatorAlwaysShown: true })).toBe(false);
170
+ });
171
+ });
172
+
173
+ describe("resolveBarVisible", () => {
174
+ test("hidden while nothing is picked, unless asked otherwise", () => {
175
+ expect(resolveBarVisible({ count: 0, isActive: true })).toBe(false);
176
+ expect(resolveBarVisible({ count: 0, isActive: true, isShownWhenEmpty: true })).toBe(true);
177
+ expect(resolveBarVisible({ count: 1, isActive: true })).toBe(true);
178
+ });
179
+
180
+ test("never shown outside the mode", () => {
181
+ expect(resolveBarVisible({ count: 2, isActive: false, isShownWhenEmpty: true })).toBe(false);
182
+ });
183
+ });
184
+
185
+ describe("resolveItemAccessibility", () => {
186
+ test("a checkbox while active", () => {
187
+ expect(resolveItemAccessibility({ isActive: true, isDisabled: false, isSelected: true })).toEqual({
188
+ accessibilityHint: "Double-tap to select",
189
+ accessibilityRole: "checkbox",
190
+ accessibilityState: { checked: true, disabled: false },
191
+ kind: "checkbox",
192
+ });
193
+ });
194
+
195
+ test("inactive, offers the long press as an action", () => {
196
+ expect(resolveItemAccessibility({ isActive: false, isDisabled: false, isSelected: false })).toEqual({
197
+ accessibilityActions: [{ label: "Start selecting", name: "longpress" }],
198
+ kind: "inherit",
199
+ });
200
+ });
201
+
202
+ test("an inactive disabled item offers nothing it cannot do", () => {
203
+ expect(resolveItemAccessibility({ isActive: false, isDisabled: true, isSelected: false })).toEqual({
204
+ accessibilityActions: [],
205
+ kind: "inherit",
206
+ });
207
+ });
208
+ });
209
+
210
+ describe("selectionModeVariants", () => {
211
+ test("the indicator is round, and fills with the action colour when picked", () => {
212
+ const off = selectionModeVariants({ isSelected: false }).indicator();
213
+ const on = selectionModeVariants({ isSelected: true }).indicator();
214
+ expect(off).toContain("rounded-full");
215
+ expect(off).toContain("border-border");
216
+ expect(on).toContain("bg-primary");
217
+ expect(on).toContain("border-primary");
218
+ });
219
+
220
+ test("a picked leading row takes a faint fill; ring and none do not", () => {
221
+ expect(selectionModeVariants({ indicator: "leading", isSelected: true }).item()).toContain("bg-primary/5");
222
+ expect(selectionModeVariants({ indicator: "leading", isSelected: false }).item()).not.toContain("bg-primary/5");
223
+ expect(selectionModeVariants({ indicator: "ring", isSelected: true }).item()).not.toContain("bg-primary/5");
224
+ expect(selectionModeVariants({ indicator: "none", isSelected: true }).item()).not.toContain("bg-primary/5");
225
+ });
226
+
227
+ test("only a leading row lays out in a row", () => {
228
+ expect(selectionModeVariants({ indicator: "leading" }).item()).toContain("flex-row");
229
+ expect(selectionModeVariants({ indicator: "ring" }).item()).not.toContain("flex-row");
230
+ });
231
+
232
+ test("the ring sits a gap outside the item", () => {
233
+ const ring = selectionModeVariants({}).ring();
234
+ expect(ring).toContain("-inset-1");
235
+ expect(ring).toContain("border-2");
236
+ expect(ring).toContain("border-primary");
237
+ });
238
+
239
+ test("edge spans the width on the page; floating is an inset card", () => {
240
+ const edge = selectionModeVariants({ placement: "edge" }).bar();
241
+ const floating = selectionModeVariants({ placement: "floating" }).bar();
242
+ expect(edge).toContain("bg-background");
243
+ expect(edge).toContain("border-t");
244
+ expect(floating).toContain("mx-4");
245
+ expect(floating).toContain("rounded-2xl");
246
+ expect(floating).toContain("bg-elevated");
247
+ expect(floating).not.toContain("border-t");
248
+ });
249
+
250
+ test("a destructive action's label reads in the destructive colour", () => {
251
+ expect(selectionModeVariants({ isDestructive: true }).actionLabel()).toContain("text-destructive");
252
+ expect(selectionModeVariants({ isDestructive: false }).actionLabel()).toContain("text-foreground");
253
+ });
254
+
255
+ test("text colour sits on the label, never on the action", () => {
256
+ expect(selectionModeVariants({ isDestructive: true }).action()).not.toMatch(/\btext-(destructive|foreground)\b/);
257
+ });
258
+
259
+ test("the leading offset is the indicator plus its gap", () => {
260
+ expect(SELECTION_MODE_INDICATOR_OFFSET).toBe(34);
261
+ });
262
+
263
+ test("every axis value resolves", () => {
264
+ for (const indicator of SELECTION_INDICATORS) {
265
+ for (const placement of SELECTION_BAR_PLACEMENTS) {
266
+ expect(() => selectionModeVariants({ indicator, placement }).root()).not.toThrow();
267
+ }
268
+ }
269
+ });
270
+ });
271
+
272
+ describe("tokens", () => {
273
+ test("the action label and its icon read the same token", () => {
274
+ expect(selectionModeVariants({ isDestructive: true }).actionLabel()).toContain(
275
+ `text-${SELECTION_MODE_ACTION_FOREGROUND_TOKEN.destructive}`
276
+ );
277
+ expect(selectionModeVariants({ isDestructive: false }).actionLabel()).toContain(
278
+ `text-${SELECTION_MODE_ACTION_FOREGROUND_TOKEN.default}`
279
+ );
280
+ });
281
+
282
+ test("names only tokens both themes declare", () => {
283
+ for (const token of [...Object.values(SELECTION_MODE_ACTION_FOREGROUND_TOKEN), SELECTION_MODE_CHECK_TOKEN]) {
284
+ expect(LIGHT.has(token)).toBe(true);
285
+ expect(DARK.has(token)).toBe(true);
286
+ }
287
+ for (const token of ["primary", "border", "elevated", "background", "destructive"]) {
288
+ expect(LIGHT.has(token)).toBe(true);
289
+ expect(DARK.has(token)).toBe(true);
290
+ }
291
+ });
292
+ });
@@ -0,0 +1,329 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+
4
+ /**
5
+ * How an item shows it is picked.
6
+ *
7
+ * `leading` slides a round indicator in from the leading edge and pushes the
8
+ * content over — a row in a list. `ring` draws a primary ring a gap outside the
9
+ * item, with a small check badge — a swatch, an avatar, a tile. `none` draws
10
+ * nothing, and the caller reads `isSelected` to paint its own.
11
+ */
12
+ export const SELECTION_INDICATORS = ["leading", "ring", "none"] as const;
13
+
14
+ /** `edge` is a full-width bar on the page's own surface; `floating` is an inset card above it. */
15
+ export const SELECTION_BAR_PLACEMENTS = ["edge", "floating"] as const;
16
+
17
+ export type SelectionIndicator = (typeof SELECTION_INDICATORS)[number];
18
+ export type SelectionBarPlacement = (typeof SELECTION_BAR_PLACEMENTS)[number];
19
+
20
+ /**
21
+ * How far a `leading` indicator pushes the content: the 22pt circle plus a 12pt
22
+ * gap. A number because the slide animates a width, which a class cannot.
23
+ */
24
+ export const SELECTION_MODE_INDICATOR_OFFSET = 34;
25
+
26
+ /** Theme token an action's label and icon both read, so the two cannot drift. */
27
+ export const SELECTION_MODE_ACTION_FOREGROUND_TOKEN = {
28
+ default: "foreground",
29
+ destructive: "destructive",
30
+ } as const;
31
+
32
+ /** Theme token the check inside a picked indicator is drawn in. */
33
+ export const SELECTION_MODE_CHECK_TOKEN = "primary-foreground";
34
+
35
+ /** The timings every transition here shares. Reduced motion collapses each to a 150ms fade. */
36
+ export const SELECTION_MODE_MOTION = {
37
+ reducedFadeMs: 150,
38
+ fadeMs: 200,
39
+ headerTravel: 8,
40
+ spring: { damping: 18, mass: 0.6, stiffness: 260 },
41
+ } as const;
42
+
43
+ /**
44
+ * Styling for every part of a selection.
45
+ *
46
+ * `text-*` lives on the label slots only — a React Native `View` does not
47
+ * cascade colour to its text (package rule 1).
48
+ *
49
+ * Free of React Native imports so it stays unit-testable. See AGENTS.md.
50
+ */
51
+ export const selectionModeVariants = tv({
52
+ slots: {
53
+ root: "flex-1",
54
+ item: "relative",
55
+ indicatorSlot: "absolute inset-y-0 start-4 justify-center",
56
+ indicator: "size-5.5 items-center justify-center rounded-full border-2",
57
+ indicatorCheck: "size-icon-xs",
58
+ ring: "absolute -inset-1 rounded-xl border-2 border-primary",
59
+ ringBadge:
60
+ "absolute -end-1.5 -top-1.5 size-icon-md items-center justify-center rounded-full border-2 border-background bg-primary",
61
+ header: "h-navbar-row flex-row items-center gap-2 border-b border-border bg-background px-2",
62
+ headerTitle: "flex-1 font-semibold text-base text-foreground",
63
+ bar: "absolute inset-x-0 bottom-0 flex-row items-stretch px-2 pt-2",
64
+ action: "min-h-14 flex-1 items-center justify-center gap-1 rounded-lg px-1 py-1.5",
65
+ actionLabel: "font-medium text-xs",
66
+ group: "",
67
+ groupLabel: "px-1 pb-2 font-medium text-muted-foreground text-sm",
68
+ },
69
+ variants: {
70
+ indicator: {
71
+ leading: { item: "flex-row items-center" },
72
+ ring: {},
73
+ none: {},
74
+ },
75
+ placement: {
76
+ edge: { bar: "border-t border-border bg-background" },
77
+ floating: { bar: "mx-4 mb-2 rounded-2xl bg-elevated pb-2 shadow-lg" },
78
+ },
79
+ isSelected: {
80
+ true: { indicator: "border-primary bg-primary" },
81
+ false: { indicator: "border-border" },
82
+ },
83
+ isDestructive: {
84
+ true: { actionLabel: "text-destructive" },
85
+ false: { actionLabel: "text-foreground" },
86
+ },
87
+ isDisabled: {
88
+ true: { action: "opacity-50" },
89
+ false: {},
90
+ },
91
+ layout: {
92
+ stack: { group: "overflow-hidden rounded-lg border border-border bg-card" },
93
+ grid: { group: "flex-row flex-wrap" },
94
+ strip: { group: "" },
95
+ },
96
+ },
97
+ compoundVariants: [{ indicator: "leading", isSelected: true, class: { item: "bg-primary/5" } }],
98
+ defaultVariants: {
99
+ indicator: "leading",
100
+ placement: "edge",
101
+ isSelected: false,
102
+ isDestructive: false,
103
+ isDisabled: false,
104
+ layout: "stack",
105
+ },
106
+ });
107
+
108
+ export type SelectionModeVariantProps = VariantProps<typeof selectionModeVariants>;
109
+
110
+ /**
111
+ * The selection after `value` is pressed.
112
+ *
113
+ * Adds an unpicked value at the end, so the list keeps the order things were
114
+ * picked in, and removes a picked one. A disabled value never moves. At `max`
115
+ * an unpicked value does not go on, but a picked one still comes off — the way
116
+ * out of a full selection is always open.
117
+ *
118
+ * Always a fresh array; {@link resolveIsSameSelection} is how a caller tells a
119
+ * no-op apart, so `onSelectedChange` is never called with what it already holds.
120
+ */
121
+ export function resolveToggle({
122
+ selected,
123
+ value,
124
+ max,
125
+ isDisabled = false,
126
+ }: {
127
+ selected: readonly string[];
128
+ value: string;
129
+ max?: number;
130
+ isDisabled?: boolean;
131
+ }): string[] {
132
+ if (isDisabled) return [...selected];
133
+ if (selected.includes(value)) return selected.filter((entry) => entry !== value);
134
+ if (max !== undefined && selected.length >= max) return [...selected];
135
+ return [...selected, value];
136
+ }
137
+
138
+ /**
139
+ * Every value picked, up to `max`.
140
+ *
141
+ * Existing picks keep their place at the front, then `values` fill in list
142
+ * order until the cap. A pick already past the cap — a controlled selection
143
+ * handed more than `max` — is never dropped; select-all only adds.
144
+ */
145
+ export function resolveSelectAll({
146
+ selected,
147
+ values,
148
+ max,
149
+ }: {
150
+ selected: readonly string[];
151
+ values: readonly string[];
152
+ max?: number;
153
+ }): string[] {
154
+ const next = [...selected];
155
+ const cap = max ?? Number.POSITIVE_INFINITY;
156
+ for (const value of values) {
157
+ if (next.length >= cap) break;
158
+ if (!next.includes(value)) next.push(value);
159
+ }
160
+ return next;
161
+ }
162
+
163
+ /**
164
+ * Whether select-all has nothing left to do — the moment the control becomes
165
+ * "Deselect all".
166
+ *
167
+ * True once every value is picked, or once the cap is reached when the cap is
168
+ * below the total: past that point select-all would add nothing.
169
+ */
170
+ export function resolveIsAllSelected({
171
+ selected,
172
+ values,
173
+ max,
174
+ }: {
175
+ selected: readonly string[];
176
+ values: readonly string[];
177
+ max?: number;
178
+ }): boolean {
179
+ if (values.length === 0) return false;
180
+ if (max !== undefined && max < values.length && selected.length >= max) return true;
181
+ return values.every((value) => selected.includes(value));
182
+ }
183
+
184
+ /** Whether two selections hold the same ids in the same order. */
185
+ export function resolveIsSameSelection(a: readonly string[], b: readonly string[]): boolean {
186
+ return a.length === b.length && a.every((value, index) => value === b[index]);
187
+ }
188
+
189
+ /** What one press on an item does — see {@link resolveItemPress}. */
190
+ export type SelectionItemPress = { kind: "toggle" } | { kind: "press" } | { kind: "none" };
191
+
192
+ /**
193
+ * What a press on an item does.
194
+ *
195
+ * Inactive, the row's own `onPress` runs. Active, the press toggles the item and
196
+ * the row's own handler does not run. A disabled item never toggles, but its own
197
+ * press still runs even while active — a section header, a "load more" row.
198
+ * `none` is a press with no handler to run.
199
+ */
200
+ export function resolveItemPress({
201
+ isActive,
202
+ isDisabled,
203
+ hasOnPress = true,
204
+ }: {
205
+ isActive: boolean;
206
+ isDisabled: boolean;
207
+ hasOnPress?: boolean;
208
+ }): SelectionItemPress {
209
+ if (isActive && !isDisabled) return { kind: "toggle" };
210
+ return hasOnPress ? { kind: "press" } : { kind: "none" };
211
+ }
212
+
213
+ /**
214
+ * The header's label: `"{title} · n of m"` when the total is known, or
215
+ * `"n selected"` when it is not. The title is dropped without a total — on its
216
+ * own it would say nothing the count does not.
217
+ */
218
+ export function resolveHeaderCount({ count, total, title }: { count: number; total?: number; title?: string }): string {
219
+ if (total === undefined) return `${count} selected`;
220
+ const tally = `${count} of ${total}`;
221
+ return title ? `${title} · ${tally}` : tally;
222
+ }
223
+
224
+ /** How a group lays its items out — see {@link resolveGroupLayout}. */
225
+ export type SelectionGroupLayout = { kind: "stack" } | { kind: "grid"; columns: number } | { kind: "strip" };
226
+
227
+ /**
228
+ * How a group lays its items out.
229
+ *
230
+ * `isHorizontal` is a single scrolling row and wins over `columns`. Two or more
231
+ * columns is a grid; one column, or none, is a stacked card list.
232
+ */
233
+ export function resolveGroupLayout({
234
+ columns,
235
+ isHorizontal = false,
236
+ }: {
237
+ columns?: number;
238
+ isHorizontal?: boolean;
239
+ }): SelectionGroupLayout {
240
+ if (isHorizontal) return { kind: "strip" };
241
+ const whole = columns === undefined ? 1 : Math.floor(columns);
242
+ if (whole >= 2) return { columns: whole, kind: "grid" };
243
+ return { kind: "stack" };
244
+ }
245
+
246
+ /** One grid cell's width: the measured row less its gaps, split evenly. Zero before measurement. */
247
+ export function resolveGridItemWidth({
248
+ containerWidth,
249
+ columns,
250
+ gap,
251
+ }: {
252
+ containerWidth: number;
253
+ columns: number;
254
+ gap: number;
255
+ }): number {
256
+ if (containerWidth <= 0 || columns <= 0) return 0;
257
+ return Math.max(0, (containerWidth - gap * (columns - 1)) / columns);
258
+ }
259
+
260
+ /** Whether an item's indicator is drawn: only while the mode is on, unless always shown. `none` never draws. */
261
+ export function resolveIndicatorShown({
262
+ indicator,
263
+ isActive,
264
+ isIndicatorAlwaysShown = false,
265
+ }: {
266
+ indicator: SelectionIndicator;
267
+ isActive: boolean;
268
+ isIndicatorAlwaysShown?: boolean;
269
+ }): boolean {
270
+ if (indicator === "none") return false;
271
+ return isActive || isIndicatorAlwaysShown;
272
+ }
273
+
274
+ /** Whether the action bar is up: only in the mode, and only with something picked unless `isShownWhenEmpty`. */
275
+ export function resolveBarVisible({
276
+ count,
277
+ isActive,
278
+ isShownWhenEmpty = false,
279
+ }: {
280
+ count: number;
281
+ isActive: boolean;
282
+ isShownWhenEmpty?: boolean;
283
+ }): boolean {
284
+ return isActive && (count > 0 || isShownWhenEmpty);
285
+ }
286
+
287
+ /** The long-press action an inactive item offers a screen reader. */
288
+ export const SELECTION_MODE_START_ACTION = { label: "Start selecting", name: "longpress" } as const;
289
+
290
+ /** What a screen reader is told an item is — see {@link resolveItemAccessibility}. */
291
+ export type SelectionItemAccessibility =
292
+ | {
293
+ kind: "checkbox";
294
+ accessibilityRole: "checkbox";
295
+ accessibilityState: { checked: boolean; disabled: boolean };
296
+ accessibilityHint: string;
297
+ }
298
+ | {
299
+ kind: "inherit";
300
+ accessibilityActions: { name: string; label: string }[];
301
+ };
302
+
303
+ /**
304
+ * What a screen reader is told an item is.
305
+ *
306
+ * Active, it is a checkbox, checked or not — the press toggles it. Inactive it
307
+ * keeps the semantics of whatever it wraps, and offers the long press as a named
308
+ * action so the mode can be entered without the gesture. A disabled item cannot
309
+ * start the mode, so it offers nothing.
310
+ */
311
+ export function resolveItemAccessibility({
312
+ isActive,
313
+ isDisabled,
314
+ isSelected,
315
+ }: {
316
+ isActive: boolean;
317
+ isDisabled: boolean;
318
+ isSelected: boolean;
319
+ }): SelectionItemAccessibility {
320
+ if (isActive) {
321
+ return {
322
+ accessibilityHint: "Double-tap to select",
323
+ accessibilityRole: "checkbox",
324
+ accessibilityState: { checked: isSelected, disabled: isDisabled },
325
+ kind: "checkbox",
326
+ };
327
+ }
328
+ return { accessibilityActions: isDisabled ? [] : [{ ...SELECTION_MODE_START_ACTION }], kind: "inherit" };
329
+ }