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

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 (493) hide show
  1. package/README.md +10 -6
  2. package/package.json +49 -5
  3. package/src/components/accordion/AGENTS.md +21 -9
  4. package/src/components/accordion/accordion-content.tsx +7 -4
  5. package/src/components/accordion/accordion-item.tsx +15 -13
  6. package/src/components/accordion/accordion.context.tsx +11 -5
  7. package/src/components/accordion/accordion.variants.test.ts +64 -3
  8. package/src/components/accordion/accordion.variants.ts +33 -9
  9. package/src/components/alert/AGENTS.md +102 -0
  10. package/src/components/alert/alert-action.tsx +17 -0
  11. package/src/components/alert/alert-close-button.tsx +50 -0
  12. package/src/components/alert/alert-content.tsx +15 -0
  13. package/src/components/alert/alert-description.tsx +16 -0
  14. package/src/components/alert/alert-glyphs.ts +25 -0
  15. package/src/components/alert/alert-indicator.tsx +40 -0
  16. package/src/components/alert/alert-title.tsx +18 -0
  17. package/src/components/alert/alert.context.tsx +69 -0
  18. package/src/components/alert/alert.tsx +160 -0
  19. package/src/components/alert/alert.types.ts +10 -0
  20. package/src/components/alert/alert.variants.test.ts +218 -0
  21. package/src/components/alert/alert.variants.ts +180 -0
  22. package/src/components/alert/index.ts +23 -0
  23. package/src/components/avatar/AGENTS.md +106 -0
  24. package/src/components/avatar/avatar-badge.tsx +51 -0
  25. package/src/components/avatar/avatar-group.tsx +139 -0
  26. package/src/components/avatar/avatar.context.tsx +93 -0
  27. package/src/components/avatar/avatar.tsx +201 -0
  28. package/src/components/avatar/avatar.utils.test.ts +174 -0
  29. package/src/components/avatar/avatar.utils.ts +141 -0
  30. package/src/components/avatar/avatar.variants.test.ts +251 -0
  31. package/src/components/avatar/avatar.variants.ts +270 -0
  32. package/src/components/avatar/index.ts +39 -0
  33. package/src/components/badge/AGENTS.md +10 -0
  34. package/src/components/badge/badge-close-button.tsx +2 -2
  35. package/src/components/badge/badge.context.tsx +2 -1
  36. package/src/components/badge/badge.tsx +13 -4
  37. package/src/components/badge/badge.variants.test.ts +26 -0
  38. package/src/components/badge/badge.variants.ts +21 -0
  39. package/src/components/bottom-sheet/AGENTS.md +186 -231
  40. package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
  41. package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
  42. package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -208
  43. package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
  44. package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
  45. package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
  46. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -122
  47. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
  48. package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
  49. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
  50. package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
  51. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -62
  52. package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
  53. package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
  54. package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
  55. package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
  56. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
  57. package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
  58. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -87
  59. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
  60. package/src/components/bottom-sheet/index.ts +39 -17
  61. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
  62. package/src/components/button/AGENTS.md +51 -11
  63. package/src/components/button/button-group.tsx +44 -8
  64. package/src/components/button/button.context.tsx +10 -0
  65. package/src/components/button/button.tsx +7 -1
  66. package/src/components/button/button.variants.test.ts +190 -0
  67. package/src/components/button/button.variants.ts +87 -6
  68. package/src/components/button/index.ts +2 -0
  69. package/src/components/calendar/AGENTS.md +142 -0
  70. package/src/components/calendar/calendar-caption.tsx +116 -0
  71. package/src/components/calendar/calendar-day.tsx +103 -0
  72. package/src/components/calendar/calendar-grid.tsx +183 -0
  73. package/src/components/calendar/calendar-header.tsx +34 -0
  74. package/src/components/calendar/calendar-nav.tsx +51 -0
  75. package/src/components/calendar/calendar-picker.tsx +128 -0
  76. package/src/components/calendar/calendar-weekdays.tsx +48 -0
  77. package/src/components/calendar/calendar.context.tsx +108 -0
  78. package/src/components/calendar/calendar.date.test.ts +456 -0
  79. package/src/components/calendar/calendar.date.ts +584 -0
  80. package/src/components/calendar/calendar.tsx +389 -0
  81. package/src/components/calendar/calendar.types.ts +87 -0
  82. package/src/components/calendar/calendar.variants.test.ts +363 -0
  83. package/src/components/calendar/calendar.variants.ts +426 -0
  84. package/src/components/calendar/index.ts +96 -0
  85. package/src/components/card/AGENTS.md +92 -0
  86. package/src/components/card/card-action.tsx +16 -0
  87. package/src/components/card/card-content.tsx +12 -0
  88. package/src/components/card/card-description.tsx +12 -0
  89. package/src/components/card/card-footer.tsx +30 -0
  90. package/src/components/card/card-header.tsx +45 -0
  91. package/src/components/card/card-title.tsx +25 -0
  92. package/src/components/card/card.context.tsx +64 -0
  93. package/src/components/card/card.tsx +100 -0
  94. package/src/components/card/card.types.ts +13 -0
  95. package/src/components/card/card.variants.test.ts +240 -0
  96. package/src/components/card/card.variants.ts +177 -0
  97. package/src/components/card/index.ts +15 -0
  98. package/src/components/chart/AGENTS.md +8 -0
  99. package/src/components/chart/chart.tsx +8 -1
  100. package/src/components/chip/AGENTS.md +113 -0
  101. package/src/components/chip/chip-close-button.tsx +48 -0
  102. package/src/components/chip/chip-end-content.tsx +15 -0
  103. package/src/components/chip/chip-label.tsx +21 -0
  104. package/src/components/chip/chip-start-content.tsx +16 -0
  105. package/src/components/chip/chip.context.tsx +66 -0
  106. package/src/components/chip/chip.tsx +268 -0
  107. package/src/components/chip/chip.types.ts +10 -0
  108. package/src/components/chip/chip.variants.test.ts +335 -0
  109. package/src/components/chip/chip.variants.ts +321 -0
  110. package/src/components/chip/index.ts +24 -0
  111. package/src/components/collapsible/AGENTS.md +97 -0
  112. package/src/components/collapsible/collapsible-content.tsx +97 -0
  113. package/src/components/collapsible/collapsible-description.tsx +17 -0
  114. package/src/components/collapsible/collapsible-indicator.tsx +92 -0
  115. package/src/components/collapsible/collapsible-title.tsx +17 -0
  116. package/src/components/collapsible/collapsible-trigger.tsx +133 -0
  117. package/src/components/collapsible/collapsible.context.tsx +91 -0
  118. package/src/components/collapsible/collapsible.tsx +167 -0
  119. package/src/components/collapsible/collapsible.types.ts +9 -0
  120. package/src/components/collapsible/collapsible.variants.test.ts +338 -0
  121. package/src/components/collapsible/collapsible.variants.ts +228 -0
  122. package/src/components/collapsible/index.ts +32 -0
  123. package/src/components/context-menu/AGENTS.md +103 -0
  124. package/src/components/context-menu/context-menu-content.tsx +64 -0
  125. package/src/components/context-menu/context-menu-item.tsx +22 -0
  126. package/src/components/context-menu/context-menu-preview.tsx +60 -0
  127. package/src/components/context-menu/context-menu-trigger.tsx +160 -0
  128. package/src/components/context-menu/context-menu.context.tsx +91 -0
  129. package/src/components/context-menu/context-menu.tsx +90 -0
  130. package/src/components/context-menu/context-menu.variants.test.ts +266 -0
  131. package/src/components/context-menu/context-menu.variants.ts +147 -0
  132. package/src/components/context-menu/index.ts +18 -0
  133. package/src/components/dialog/AGENTS.md +97 -0
  134. package/src/components/dialog/dialog-body.tsx +21 -0
  135. package/src/components/dialog/dialog-close.tsx +82 -0
  136. package/src/components/dialog/dialog-content.tsx +153 -0
  137. package/src/components/dialog/dialog-description.tsx +23 -0
  138. package/src/components/dialog/dialog-footer.tsx +32 -0
  139. package/src/components/dialog/dialog-header.tsx +20 -0
  140. package/src/components/dialog/dialog-title.tsx +42 -0
  141. package/src/components/dialog/dialog-trigger.tsx +54 -0
  142. package/src/components/dialog/dialog.context.tsx +90 -0
  143. package/src/components/dialog/dialog.tsx +119 -0
  144. package/src/components/dialog/dialog.variants.test.ts +224 -0
  145. package/src/components/dialog/dialog.variants.ts +173 -0
  146. package/src/components/dialog/index.ts +30 -0
  147. package/src/components/drawer/AGENTS.md +123 -0
  148. package/src/components/drawer/drawer-body.tsx +46 -0
  149. package/src/components/drawer/drawer-close.tsx +76 -0
  150. package/src/components/drawer/drawer-content.tsx +186 -0
  151. package/src/components/drawer/drawer-description.tsx +23 -0
  152. package/src/components/drawer/drawer-footer.tsx +21 -0
  153. package/src/components/drawer/drawer-header.tsx +42 -0
  154. package/src/components/drawer/drawer-title.tsx +35 -0
  155. package/src/components/drawer/drawer-trigger.tsx +54 -0
  156. package/src/components/drawer/drawer.context.tsx +114 -0
  157. package/src/components/drawer/drawer.tsx +119 -0
  158. package/src/components/drawer/drawer.variants.test.ts +392 -0
  159. package/src/components/drawer/drawer.variants.ts +244 -0
  160. package/src/components/drawer/index.ts +41 -0
  161. package/src/components/drawer/use-drawer-pan.ts +108 -0
  162. package/src/components/empty-state/AGENTS.md +90 -0
  163. package/src/components/empty-state/empty-state-content.tsx +17 -0
  164. package/src/components/empty-state/empty-state-description.tsx +12 -0
  165. package/src/components/empty-state/empty-state-header.tsx +17 -0
  166. package/src/components/empty-state/empty-state-media.tsx +56 -0
  167. package/src/components/empty-state/empty-state-title.tsx +18 -0
  168. package/src/components/empty-state/empty-state.context.tsx +65 -0
  169. package/src/components/empty-state/empty-state.tsx +84 -0
  170. package/src/components/empty-state/empty-state.types.ts +14 -0
  171. package/src/components/empty-state/empty-state.variants.test.ts +245 -0
  172. package/src/components/empty-state/empty-state.variants.ts +132 -0
  173. package/src/components/empty-state/index.ts +20 -0
  174. package/src/components/fab/AGENTS.md +119 -0
  175. package/src/components/fab/fab-action.tsx +118 -0
  176. package/src/components/fab/fab-group.tsx +214 -0
  177. package/src/components/fab/fab-label.tsx +18 -0
  178. package/src/components/fab/fab.context.tsx +125 -0
  179. package/src/components/fab/fab.tsx +156 -0
  180. package/src/components/fab/fab.types.ts +17 -0
  181. package/src/components/fab/fab.variants.test.ts +274 -0
  182. package/src/components/fab/fab.variants.ts +234 -0
  183. package/src/components/fab/index.ts +37 -0
  184. package/src/components/feedback/AGENTS.md +95 -0
  185. package/src/components/feedback/feedback-action.tsx +16 -0
  186. package/src/components/feedback/feedback-cancel.tsx +34 -0
  187. package/src/components/feedback/feedback-close.tsx +21 -0
  188. package/src/components/feedback/feedback-content.tsx +24 -0
  189. package/src/components/feedback/feedback-field.tsx +70 -0
  190. package/src/components/feedback/feedback-footer.tsx +21 -0
  191. package/src/components/feedback/feedback-panel.tsx +54 -0
  192. package/src/components/feedback/feedback-submit.tsx +74 -0
  193. package/src/components/feedback/feedback-title.tsx +34 -0
  194. package/src/components/feedback/feedback-trigger.tsx +20 -0
  195. package/src/components/feedback/feedback.context.tsx +73 -0
  196. package/src/components/feedback/feedback.tsx +135 -0
  197. package/src/components/feedback/feedback.variants.test.ts +185 -0
  198. package/src/components/feedback/feedback.variants.ts +128 -0
  199. package/src/components/feedback/index.ts +24 -0
  200. package/src/components/field/AGENTS.md +19 -0
  201. package/src/components/field/field-label.tsx +19 -5
  202. package/src/components/field/field.context.tsx +17 -0
  203. package/src/components/field/field.tsx +15 -2
  204. package/src/components/field/field.variants.test.ts +24 -0
  205. package/src/components/field/field.variants.ts +20 -0
  206. package/src/components/field/index.ts +1 -0
  207. package/src/components/input/AGENTS.md +5 -0
  208. package/src/components/input/input.variants.test.ts +32 -0
  209. package/src/components/input/input.variants.ts +13 -1
  210. package/src/components/item/AGENTS.md +92 -0
  211. package/src/components/item/index.ts +25 -0
  212. package/src/components/item/item-actions.tsx +28 -0
  213. package/src/components/item/item-content.tsx +15 -0
  214. package/src/components/item/item-description.tsx +12 -0
  215. package/src/components/item/item-footer.tsx +13 -0
  216. package/src/components/item/item-group.tsx +32 -0
  217. package/src/components/item/item-header.tsx +13 -0
  218. package/src/components/item/item-media.tsx +37 -0
  219. package/src/components/item/item-separator.tsx +13 -0
  220. package/src/components/item/item-title.tsx +17 -0
  221. package/src/components/item/item.context.tsx +66 -0
  222. package/src/components/item/item.tsx +218 -0
  223. package/src/components/item/item.types.ts +12 -0
  224. package/src/components/item/item.variants.test.ts +263 -0
  225. package/src/components/item/item.variants.ts +200 -0
  226. package/src/components/kpi/AGENTS.md +125 -0
  227. package/src/components/kpi/index.ts +45 -0
  228. package/src/components/kpi/kpi-action.tsx +12 -0
  229. package/src/components/kpi/kpi-content.tsx +35 -0
  230. package/src/components/kpi/kpi-footer.tsx +52 -0
  231. package/src/components/kpi/kpi-group.tsx +70 -0
  232. package/src/components/kpi/kpi-header.tsx +25 -0
  233. package/src/components/kpi/kpi-icon.tsx +28 -0
  234. package/src/components/kpi/kpi-sparkline.tsx +145 -0
  235. package/src/components/kpi/kpi-stat.tsx +20 -0
  236. package/src/components/kpi/kpi-title.tsx +19 -0
  237. package/src/components/kpi/kpi-trend.tsx +105 -0
  238. package/src/components/kpi/kpi-value.tsx +32 -0
  239. package/src/components/kpi/kpi.context.tsx +111 -0
  240. package/src/components/kpi/kpi.tsx +154 -0
  241. package/src/components/kpi/kpi.types.ts +9 -0
  242. package/src/components/kpi/kpi.variants.test.ts +428 -0
  243. package/src/components/kpi/kpi.variants.ts +347 -0
  244. package/src/components/label/AGENTS.md +89 -0
  245. package/src/components/label/index.ts +10 -0
  246. package/src/components/label/label.tsx +97 -0
  247. package/src/components/label/label.variants.test.ts +176 -0
  248. package/src/components/label/label.variants.ts +123 -0
  249. package/src/components/menu/AGENTS.md +176 -0
  250. package/src/components/menu/index.ts +44 -0
  251. package/src/components/menu/menu-background.tsx +17 -0
  252. package/src/components/menu/menu-checkbox-item.tsx +57 -0
  253. package/src/components/menu/menu-content.tsx +303 -0
  254. package/src/components/menu/menu-item.tsx +162 -0
  255. package/src/components/menu/menu-label.tsx +14 -0
  256. package/src/components/menu/menu-radio-group.tsx +50 -0
  257. package/src/components/menu/menu-radio-item.tsx +57 -0
  258. package/src/components/menu/menu-separator.tsx +11 -0
  259. package/src/components/menu/menu-sub-content.tsx +72 -0
  260. package/src/components/menu/menu-sub-trigger.tsx +50 -0
  261. package/src/components/menu/menu-sub.tsx +75 -0
  262. package/src/components/menu/menu-trigger.tsx +57 -0
  263. package/src/components/menu/menu.context.tsx +214 -0
  264. package/src/components/menu/menu.tsx +88 -0
  265. package/src/components/menu/menu.types.ts +33 -0
  266. package/src/components/menu/menu.variants.test.ts +400 -0
  267. package/src/components/menu/menu.variants.ts +353 -0
  268. package/src/components/meter/AGENTS.md +135 -0
  269. package/src/components/meter/index.ts +43 -0
  270. package/src/components/meter/meter-fill.tsx +19 -0
  271. package/src/components/meter/meter-header.tsx +17 -0
  272. package/src/components/meter/meter-label.tsx +18 -0
  273. package/src/components/meter/meter-output.tsx +42 -0
  274. package/src/components/meter/meter-segments.tsx +83 -0
  275. package/src/components/meter/meter-track.tsx +16 -0
  276. package/src/components/meter/meter.context.tsx +62 -0
  277. package/src/components/meter/meter.tsx +286 -0
  278. package/src/components/meter/meter.types.ts +30 -0
  279. package/src/components/meter/meter.variants.test.ts +430 -0
  280. package/src/components/meter/meter.variants.ts +347 -0
  281. package/src/components/overlay/AGENTS.md +89 -0
  282. package/src/components/overlay/index.ts +27 -0
  283. package/src/components/overlay/overlay-portal.tsx +84 -0
  284. package/src/components/overlay/overlay-presence.test.ts +46 -0
  285. package/src/components/overlay/overlay-presence.ts +49 -0
  286. package/src/components/overlay/overlay-registry.test.ts +96 -0
  287. package/src/components/overlay/overlay-registry.ts +99 -0
  288. package/src/components/overlay/overlay-scrim.tsx +49 -0
  289. package/src/components/overlay/overlay.context.tsx +48 -0
  290. package/src/components/overlay/overlay.tsx +118 -0
  291. package/src/components/overlay/overlay.variants.test.ts +37 -0
  292. package/src/components/overlay/overlay.variants.ts +33 -0
  293. package/src/components/overlay/use-overlay-back-handler.ts +34 -0
  294. package/src/components/overlay/use-overlay-presence.ts +87 -0
  295. package/src/components/popover/AGENTS.md +138 -0
  296. package/src/components/popover/index.ts +48 -0
  297. package/src/components/popover/popover-anchor.tsx +49 -0
  298. package/src/components/popover/popover-arrow.tsx +89 -0
  299. package/src/components/popover/popover-close.tsx +73 -0
  300. package/src/components/popover/popover-content.tsx +235 -0
  301. package/src/components/popover/popover-description.tsx +18 -0
  302. package/src/components/popover/popover-title.tsx +33 -0
  303. package/src/components/popover/popover-trigger.tsx +56 -0
  304. package/src/components/popover/popover.context.tsx +71 -0
  305. package/src/components/popover/popover.position.test.ts +354 -0
  306. package/src/components/popover/popover.position.ts +286 -0
  307. package/src/components/popover/popover.tsx +123 -0
  308. package/src/components/popover/popover.variants.test.ts +135 -0
  309. package/src/components/popover/popover.variants.ts +85 -0
  310. package/src/components/popover/use-anchor-measure.ts +76 -0
  311. package/src/components/popover/use-anchored-content.ts +219 -0
  312. package/src/components/pressable/AGENTS.md +11 -1
  313. package/src/components/pressable/pressable.tsx +7 -2
  314. package/src/components/pressable/pressable.variants.test.ts +29 -0
  315. package/src/components/pressable/pressable.variants.ts +17 -0
  316. package/src/components/progress/AGENTS.md +120 -0
  317. package/src/components/progress/index.ts +40 -0
  318. package/src/components/progress/progress-fill.tsx +152 -0
  319. package/src/components/progress/progress-header.tsx +19 -0
  320. package/src/components/progress/progress-label.tsx +27 -0
  321. package/src/components/progress/progress-output.tsx +47 -0
  322. package/src/components/progress/progress-track.tsx +38 -0
  323. package/src/components/progress/progress.context.tsx +79 -0
  324. package/src/components/progress/progress.tsx +171 -0
  325. package/src/components/progress/progress.types.ts +23 -0
  326. package/src/components/progress/progress.variants.test.ts +335 -0
  327. package/src/components/progress/progress.variants.ts +284 -0
  328. package/src/components/progress-button/AGENTS.md +116 -0
  329. package/src/components/progress-button/index.ts +36 -0
  330. package/src/components/progress-button/progress-button-done.tsx +33 -0
  331. package/src/components/progress-button/progress-button-label.tsx +27 -0
  332. package/src/components/progress-button/progress-button.context.tsx +93 -0
  333. package/src/components/progress-button/progress-button.tsx +414 -0
  334. package/src/components/progress-button/progress-button.variants.test.ts +314 -0
  335. package/src/components/progress-button/progress-button.variants.ts +236 -0
  336. package/src/components/provider/AGENTS.md +38 -25
  337. package/src/components/provider/provider.tsx +38 -11
  338. package/src/components/radio/AGENTS.md +8 -10
  339. package/src/components/radio/radio.variants.test.ts +5 -6
  340. package/src/components/radio/radio.variants.ts +6 -7
  341. package/src/components/rating/AGENTS.md +143 -0
  342. package/src/components/rating/index.ts +43 -0
  343. package/src/components/rating/rating-output.tsx +39 -0
  344. package/src/components/rating/rating-star.tsx +70 -0
  345. package/src/components/rating/rating-stars.tsx +219 -0
  346. package/src/components/rating/rating.context.tsx +79 -0
  347. package/src/components/rating/rating.tsx +163 -0
  348. package/src/components/rating/rating.variants.test.ts +347 -0
  349. package/src/components/rating/rating.variants.ts +354 -0
  350. package/src/components/selection-mode/AGENTS.md +155 -0
  351. package/src/components/selection-mode/index.ts +43 -0
  352. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  353. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  354. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  355. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  356. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  357. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  358. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  359. package/src/components/selection-mode/selection-mode.tsx +207 -0
  360. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  361. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  362. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  363. package/src/components/skeleton/AGENTS.md +120 -0
  364. package/src/components/skeleton/index.ts +15 -0
  365. package/src/components/skeleton/skeleton-clock.ts +48 -0
  366. package/src/components/skeleton/skeleton-group.tsx +64 -0
  367. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  368. package/src/components/skeleton/skeleton.context.tsx +45 -0
  369. package/src/components/skeleton/skeleton.tsx +227 -0
  370. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  371. package/src/components/skeleton/skeleton.variants.ts +222 -0
  372. package/src/components/slide-button/AGENTS.md +122 -0
  373. package/src/components/slide-button/index.ts +43 -0
  374. package/src/components/slide-button/slide-button-label.tsx +70 -0
  375. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  376. package/src/components/slide-button/slide-button.context.tsx +76 -0
  377. package/src/components/slide-button/slide-button.tsx +457 -0
  378. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  379. package/src/components/slide-button/slide-button.variants.ts +324 -0
  380. package/src/components/slider/index.ts +2 -0
  381. package/src/components/slider/slider-thumb.tsx +33 -1
  382. package/src/components/slider/slider.variants.test.ts +52 -0
  383. package/src/components/slider/slider.variants.ts +56 -2
  384. package/src/components/stack-card/AGENTS.md +169 -0
  385. package/src/components/stack-card/index.ts +37 -0
  386. package/src/components/stack-card/stack-card-action.tsx +66 -0
  387. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  388. package/src/components/stack-card/stack-card-card.tsx +71 -0
  389. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  390. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  391. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  392. package/src/components/stack-card/stack-card.context.tsx +117 -0
  393. package/src/components/stack-card/stack-card.tsx +557 -0
  394. package/src/components/stack-card/stack-card.types.ts +26 -0
  395. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  396. package/src/components/stack-card/stack-card.variants.ts +360 -0
  397. package/src/components/steps/AGENTS.md +153 -0
  398. package/src/components/steps/index.ts +45 -0
  399. package/src/components/steps/steps-connector.tsx +31 -0
  400. package/src/components/steps/steps-description.tsx +26 -0
  401. package/src/components/steps/steps-indicator.tsx +106 -0
  402. package/src/components/steps/steps-item.tsx +225 -0
  403. package/src/components/steps/steps-panel.tsx +41 -0
  404. package/src/components/steps/steps-title.tsx +32 -0
  405. package/src/components/steps/steps-track.tsx +59 -0
  406. package/src/components/steps/steps.context.tsx +123 -0
  407. package/src/components/steps/steps.tsx +166 -0
  408. package/src/components/steps/steps.variants.test.ts +388 -0
  409. package/src/components/steps/steps.variants.ts +468 -0
  410. package/src/components/surface/AGENTS.md +82 -0
  411. package/src/components/surface/index.ts +15 -0
  412. package/src/components/surface/surface.context.tsx +56 -0
  413. package/src/components/surface/surface.tsx +87 -0
  414. package/src/components/surface/surface.variants.test.ts +227 -0
  415. package/src/components/surface/surface.variants.ts +165 -0
  416. package/src/components/swipe/AGENTS.md +140 -0
  417. package/src/components/swipe/index.ts +31 -0
  418. package/src/components/swipe/swipe-action.tsx +99 -0
  419. package/src/components/swipe/swipe-end.tsx +14 -0
  420. package/src/components/swipe/swipe-group.tsx +55 -0
  421. package/src/components/swipe/swipe-panel.tsx +55 -0
  422. package/src/components/swipe/swipe-start.tsx +15 -0
  423. package/src/components/swipe/swipe.context.tsx +135 -0
  424. package/src/components/swipe/swipe.tsx +393 -0
  425. package/src/components/swipe/swipe.types.ts +7 -0
  426. package/src/components/swipe/swipe.variants.test.ts +337 -0
  427. package/src/components/swipe/swipe.variants.ts +290 -0
  428. package/src/components/switch/switch.variants.ts +2 -2
  429. package/src/components/tabs/AGENTS.md +4 -3
  430. package/src/components/tabs/tabs.variants.test.ts +1 -1
  431. package/src/components/tabs/tabs.variants.ts +4 -6
  432. package/src/components/text/AGENTS.md +8 -2
  433. package/src/components/text/text.tsx +7 -0
  434. package/src/components/text/text.variants.test.ts +24 -1
  435. package/src/components/text/text.variants.ts +5 -0
  436. package/src/components/textarea/AGENTS.md +89 -0
  437. package/src/components/textarea/index.ts +16 -0
  438. package/src/components/textarea/textarea.tsx +157 -0
  439. package/src/components/textarea/textarea.variants.test.ts +219 -0
  440. package/src/components/textarea/textarea.variants.ts +171 -0
  441. package/src/components/toast/AGENTS.md +140 -0
  442. package/src/components/toast/index.ts +74 -0
  443. package/src/components/toast/toast-action.tsx +59 -0
  444. package/src/components/toast/toast-api.ts +32 -0
  445. package/src/components/toast/toast-close.tsx +44 -0
  446. package/src/components/toast/toast-content.tsx +11 -0
  447. package/src/components/toast/toast-description.tsx +11 -0
  448. package/src/components/toast/toast-indicator.tsx +34 -0
  449. package/src/components/toast/toast-item.tsx +308 -0
  450. package/src/components/toast/toast-title.tsx +13 -0
  451. package/src/components/toast/toast-viewport.tsx +135 -0
  452. package/src/components/toast/toast.context.tsx +69 -0
  453. package/src/components/toast/toast.store.test.ts +259 -0
  454. package/src/components/toast/toast.store.ts +338 -0
  455. package/src/components/toast/toast.timer.test.ts +64 -0
  456. package/src/components/toast/toast.timer.ts +51 -0
  457. package/src/components/toast/toast.tsx +132 -0
  458. package/src/components/toast/toast.variants.test.ts +393 -0
  459. package/src/components/toast/toast.variants.ts +304 -0
  460. package/src/components/toggle-button/AGENTS.md +106 -0
  461. package/src/components/toggle-button/index.ts +30 -0
  462. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  463. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  464. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  465. package/src/components/toggle-button/toggle-button.tsx +172 -0
  466. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  467. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  468. package/src/components/tooltip/AGENTS.md +133 -0
  469. package/src/components/tooltip/index.ts +28 -0
  470. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  471. package/src/components/tooltip/tooltip-content.tsx +179 -0
  472. package/src/components/tooltip/tooltip-description.tsx +19 -0
  473. package/src/components/tooltip/tooltip-text.tsx +21 -0
  474. package/src/components/tooltip/tooltip-title.tsx +20 -0
  475. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  476. package/src/components/tooltip/tooltip.context.tsx +68 -0
  477. package/src/components/tooltip/tooltip.tsx +213 -0
  478. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  479. package/src/components/tooltip/tooltip.variants.ts +177 -0
  480. package/src/hooks/use-calm-motion.tsx +31 -0
  481. package/src/lib/calm-motion.test.ts +17 -0
  482. package/src/lib/calm-motion.ts +23 -0
  483. package/src/lib/cn.parity.test.ts +62 -0
  484. package/src/lib/cn.test.ts +14 -0
  485. package/src/lib/cn.ts +11 -6
  486. package/src/no-gorhom.test.ts +54 -0
  487. package/src/styles/geometry.test.ts +1 -0
  488. package/src/styles/theme.css +4 -0
  489. package/src/styles/tokens.css +44 -0
  490. package/src/styles/tokens.test.ts +56 -0
  491. package/src/styles/tokens.ts +29 -1
  492. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  493. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -0,0 +1,155 @@
1
+ # SelectionMode
2
+
3
+ Pick several things at once — messages to archive, people to share with,
4
+ swatches to apply — then act on them from a bar. A compound root plus
5
+ `SelectionMode.Item`, `.Indicator`, `.Header`, `.Bar`, `.Action` and `.Group`,
6
+ and `useSelectionMode()`.
7
+
8
+ `import { SelectionMode } from "@delacour/react-native-ui/selection-mode";`
9
+
10
+ ## Files
11
+
12
+ | File | What it holds |
13
+ | --- | --- |
14
+ | `index.ts` | → `@delacour/react-native-ui/selection-mode` |
15
+ | `selection-mode.tsx` | Root — owns the mode and the selection — plus the `Object.assign` compound surface |
16
+ | `selection-mode-item.tsx` | `SelectionMode.Item`: wraps a child, decides what a press does, slides the leading mark in |
17
+ | `selection-mode-indicator.tsx` | `SelectionMode.Indicator`, the round mark, and the internal `ring` (`DelacourUI.SelectionMode.Item.Ring`) |
18
+ | `selection-mode-header.tsx` | `SelectionMode.Header`: close, the count and select-all, while the mode is on |
19
+ | `selection-mode-bar.tsx` | `SelectionMode.Bar`: the absolute action bar over the bottom edge |
20
+ | `selection-mode-action.tsx` | `SelectionMode.Action`: an icon over a label, handed the selection |
21
+ | `selection-mode-group.tsx` | `SelectionMode.Group`: a stacked card, a grid or a horizontal strip |
22
+ | `selection-mode.context.tsx` | The selection context and the per-item context, with their hooks |
23
+ | `selection-mode.types.ts` | `SelectionModeState` — what the root builds and the context carries |
24
+ | `selection-mode.variants.ts` | The slotted `tv()`, the motion constants, the tokens and every pure resolver |
25
+ | `selection-mode.variants.test.ts` | |
26
+
27
+ ## Design
28
+
29
+ - **Axes**: `indicator` (`leading` default, `ring`, `none`) on an item;
30
+ `placement` (`edge` default, `floating`) on the bar; `isSelected` and
31
+ `isDestructive` drive the indicator's fill and an action's colour.
32
+ - **Selection is a set of ids, never indices.** A `string[]` in the order things
33
+ were picked. An index changes meaning the moment a list is sorted, filtered or
34
+ paged; an id does not, so a selection survives a refetch.
35
+ - **It is a mode, not a property of each row.** Off, the list is for reading and
36
+ each item runs its own `onPress`. A long press turns the mode on with that item
37
+ picked; then a press toggles and the row's own handler does not run.
38
+ `resolveItemPress` is the whole decision and is pure.
39
+ - **A disabled item keeps its own press, even while the mode is on.** It never
40
+ toggles and never starts the mode, but a section header or a "load more" row
41
+ still has to work while someone is picking.
42
+ - **Exit clears an uncontrolled selection and leaves a controlled one alone.**
43
+ The close button and `exit()` call `setActive(false)`; only when `selected` is
44
+ uncontrolled do they also clear. A caller that owns `selected` may want to keep
45
+ it — a draft that survives leaving the mode — and clearing behind its back
46
+ would fight it.
47
+ - **Both pairs are controllable.** `isActive`/`defaultActive`/`onActiveChange`
48
+ and `selected`/`defaultSelected`/`onSelectedChange`, through
49
+ `useControllableState`. A change that changes nothing — a toggle refused at
50
+ the cap, a select-all with nothing left to add — notifies nobody
51
+ (`resolveIsSameSelection`), the rule `ToggleButton.Group` keeps.
52
+ - **`max` caps toggling and select-all, and never takes a pick away.** An
53
+ unpicked item at the cap does not go on, but a picked one still comes off, so
54
+ the way out of a full selection is always open. Select-all fills up to the cap
55
+ keeping existing picks first, and never drops a controlled pick that is already
56
+ past it.
57
+ - **Select-all flips to "Deselect all" at the cap, not only at the total.**
58
+ `resolveIsAllSelected` is true once every value is picked or once the cap is
59
+ reached below the total — past that point select-all would do nothing, and a
60
+ control that does nothing reads as broken.
61
+ - **`values` is optional, and the header degrades without it.** With it the
62
+ label is `"{title} · n of m"` and select-all shows; without it the label is
63
+ `"n selected"` and select-all is dropped, because there is nothing to pick all
64
+ of.
65
+ - **The indicator is round on purpose.** A square box is a form control being
66
+ filled in; a round one is a thing picked out of a set. `Checkbox` stays square.
67
+ 22pt (`size-5.5` — the icon scale skips 22), `border-2 border-border`, picked
68
+ `bg-primary` with a `primary-foreground` check. It pops from 0.8 on a spring.
69
+ - **The leading mark pushes the content rather than covering it.** A spacer
70
+ opens from 0 to 34pt (`SELECTION_MODE_INDICATOR_OFFSET`, the mark plus a 12pt
71
+ gap) and the mark is drawn *after* the content, absolutely positioned over the
72
+ gap, so a child with a fill of its own never paints over it. A picked leading
73
+ row also takes `bg-primary/5`.
74
+ - **`ring` draws a 2pt primary ring a 2pt gap outside the item** (`-inset-1`)
75
+ with a small check badge at the top-end. Its corner is `rounded-xl` by default;
76
+ `ringClassName` reshapes it — `rounded-full` around a round swatch. That prop
77
+ is an addition: a ring that cannot match the item's own corner looks wrong on
78
+ anything round.
79
+ - **`none` draws nothing.** The caller reads `useSelectionMode().isSelected` and
80
+ paints its own state, or places a `SelectionMode.Indicator` itself.
81
+ - **The bar is absolute, over the list.** It never shrinks the list, so the list
82
+ must pad its own bottom (`pb-24`). It is hidden while nothing is picked unless
83
+ `isShownWhenEmpty`, and never shown with the mode off. `edge` pads its own
84
+ bottom with the safe-area inset (at least 8pt); `floating` lifts the whole
85
+ card by the inset instead, so the card keeps its own padding.
86
+ - **An action is handed the selection as it stands at the press.** A handler
87
+ that closed over `selected` would see a stale list; `onPress(selected)` cannot.
88
+ `isExitOnPress` exits afterwards — a delete, whose picks no longer exist.
89
+ - **The root fills the height it is offered** (`flex-1`). Give it one: inside a
90
+ `ScrollView` with no height the bar has no bottom to sit on. An `h-*` class on
91
+ the root itself is not enough — `flex-1` sets a zero basis, and in a parent
92
+ with no height of its own the root collapses to nothing. Size the parent, or
93
+ add `flex-none` to a root that should take its content's height (an always-on
94
+ picker with no bar).
95
+ - **Haptics are off by default.** `haptic` plays on entering the mode and on each
96
+ toggle that changed something — called from JS through `playHaptic`, since
97
+ neither moment is a gesture callback.
98
+ - **Every part but the root throws outside a `SelectionMode`**, by name.
99
+ - **The header's two controls take their own `testID`s** — `closeTestID` and
100
+ `selectAllTestID` — the way `Alert` and `Chip` forward one to their close
101
+ control. A `testID` on the header lands on its row, and a test that has to
102
+ leave the mode cannot reach a button the component draws for itself.
103
+
104
+ ## Motion
105
+
106
+ - Indicator: the check fades in 200ms and the mark pops on a spring. The leading
107
+ spacer opens on the same spring while the mark fades.
108
+ - Header: fades in from 8pt above (`FadeInDown` with an 8pt initial value).
109
+ - Bar: slides up and fades in (`FadeInUp`), fades out downward.
110
+ - **Reduced motion is opacity-only, 150ms, and never disappears.** Every
111
+ transition sets `ReduceMotion.Never` on its 150ms fade, because under the
112
+ default policy a fade completes instantly — and the fade *is* the information
113
+ that the mode changed. The spacer snaps rather than sliding.
114
+
115
+ ## Accessibility
116
+
117
+ - **While the mode is on, an item is a checkbox.** `accessibilityRole="checkbox"`,
118
+ `accessibilityState={{ checked, disabled }}`, hint "Double-tap to select".
119
+ `resolveItemAccessibility` is the decision and is pure.
120
+ - **While off, an item keeps the semantics of what it wraps** — a `button` when
121
+ it has an `onPress`, no role otherwise — and offers a `longpress` action
122
+ labelled "Start selecting", so the mode can be entered without the gesture. A
123
+ disabled item offers nothing it cannot do.
124
+ - **The count is announced.** The header label is a polite live region on
125
+ Android; iOS has none, so the header calls
126
+ `AccessibilityInfo.announceForAccessibility` when the label changes.
127
+ - **A group's `label` is its `accessibilityLabel`**, and a grid or strip is a
128
+ `list`.
129
+
130
+ ## Group
131
+
132
+ - **`resolveGroupLayout` decides the shape.** `isHorizontal` is a strip and wins
133
+ over `columns`; two or more columns is a grid (floored); otherwise a stack.
134
+ - **A stack is a card** (`rounded-lg border border-border bg-card`) with a
135
+ `Separator` between rows unless `hasSeparators={false}`.
136
+ - **A grid measures itself.** Each cell is `resolveGridItemWidth` — the measured
137
+ width less the gaps, split evenly — and nothing renders until the first layout,
138
+ so no cell flashes at the wrong width. `hasSeparators` does not apply.
139
+ - **A strip pads its scroll content by 8pt** so a ring and its badge are not
140
+ clipped by the scroll view.
141
+
142
+ ## Out of scope
143
+
144
+ - **`SelectionMode.Sheet`.** Importing `../bottom-sheet` would force the optional
145
+ sheet engine and `react-native-teleport` on every consumer of this component.
146
+ The `in-a-sheet` demo shows the composition instead: `SelectionMode` wraps the
147
+ `BottomSheet` (context reaches through the portal), the items go in the sheet's
148
+ scroll view, and the bar goes in `BottomSheet.Footer sticky` as
149
+ `className="relative border-t-0 bg-transparent"` with `isSafeAreaAware={false}`. A
150
+ first-class part is a follow-up, possibly on its own subpath.
151
+ - **Drag to select a range.** A pan across rows would fight the list's own
152
+ scroll; not in v1.
153
+ - **Replacing the screen's navbar.** The header renders where it is placed; a
154
+ screen that wants it in place of its navbar hides the navbar itself while
155
+ `isActive`.
@@ -0,0 +1,43 @@
1
+ export { SelectionMode, type SelectionModeProps } from "./selection-mode";
2
+ export {
3
+ type SelectionModeContextValue,
4
+ type SelectionModeItemContextValue,
5
+ SelectionModeProvider,
6
+ useSelectionMode,
7
+ useSelectionModeContext,
8
+ useSelectionModeItemContext,
9
+ } from "./selection-mode.context";
10
+ export type { SelectionModeState } from "./selection-mode.types";
11
+ export {
12
+ resolveBarVisible,
13
+ resolveGridItemWidth,
14
+ resolveGroupLayout,
15
+ resolveHeaderCount,
16
+ resolveIndicatorShown,
17
+ resolveIsAllSelected,
18
+ resolveIsSameSelection,
19
+ resolveItemAccessibility,
20
+ resolveItemPress,
21
+ resolveSelectAll,
22
+ resolveToggle,
23
+ SELECTION_BAR_PLACEMENTS,
24
+ SELECTION_INDICATORS,
25
+ SELECTION_MODE_ACTION_FOREGROUND_TOKEN,
26
+ SELECTION_MODE_CHECK_TOKEN,
27
+ SELECTION_MODE_INDICATOR_OFFSET,
28
+ SELECTION_MODE_MOTION,
29
+ SELECTION_MODE_START_ACTION,
30
+ type SelectionBarPlacement,
31
+ type SelectionGroupLayout,
32
+ type SelectionIndicator,
33
+ type SelectionItemAccessibility,
34
+ type SelectionItemPress,
35
+ type SelectionModeVariantProps,
36
+ selectionModeVariants,
37
+ } from "./selection-mode.variants";
38
+ export type { SelectionModeActionProps } from "./selection-mode-action";
39
+ export type { SelectionModeBarProps } from "./selection-mode-bar";
40
+ export type { SelectionModeGroupProps } from "./selection-mode-group";
41
+ export type { SelectionModeHeaderProps } from "./selection-mode-header";
42
+ export type { SelectionModeIndicatorProps } from "./selection-mode-indicator";
43
+ export type { SelectionModeItemProps } from "./selection-mode-item";
@@ -0,0 +1,65 @@
1
+ import { type ReactElement, type ReactNode, useCallback } from "react";
2
+ import { Icon, type IconComponent } from "../icon";
3
+ import { Pressable } from "../pressable";
4
+ import { Text } from "../text";
5
+ import { useSelectionModePart } from "./selection-mode.context";
6
+ import { SELECTION_MODE_ACTION_FOREGROUND_TOKEN, selectionModeVariants } from "./selection-mode.variants";
7
+
8
+ export type SelectionModeActionProps = {
9
+ /** The label, under the icon. */
10
+ children: ReactNode;
11
+ icon?: IconComponent;
12
+ /** Called with the selection as it stands at the press. */
13
+ onPress: (selected: string[]) => void;
14
+ /** Leave the mode once `onPress` has run. */
15
+ isExitOnPress?: boolean;
16
+ /** Draw the label and icon in the destructive colour. */
17
+ isDestructive?: boolean;
18
+ isDisabled?: boolean;
19
+ className?: string;
20
+ labelClassName?: string;
21
+ };
22
+
23
+ /**
24
+ * One action in the bar: an icon over a label, sharing the bar's width evenly
25
+ * with its siblings.
26
+ *
27
+ * `onPress` receives the current selection, so the handler never closes over a
28
+ * stale one. `isExitOnPress` leaves the mode afterwards — a delete or an archive,
29
+ * whose picks no longer exist to stay picked.
30
+ */
31
+ export function SelectionModeAction({
32
+ children,
33
+ icon,
34
+ onPress,
35
+ isExitOnPress = false,
36
+ isDestructive = false,
37
+ isDisabled = false,
38
+ className,
39
+ labelClassName,
40
+ }: SelectionModeActionProps): ReactElement {
41
+ const { selected, exit } = useSelectionModePart("SelectionMode.Action");
42
+ const slots = selectionModeVariants({ isDestructive, isDisabled });
43
+ const token = SELECTION_MODE_ACTION_FOREGROUND_TOKEN[isDestructive ? "destructive" : "default"];
44
+
45
+ const handlePress = useCallback(() => {
46
+ onPress([...selected]);
47
+ if (isExitOnPress) exit();
48
+ }, [exit, isExitOnPress, onPress, selected]);
49
+
50
+ return (
51
+ <Pressable
52
+ accessibilityLabel={typeof children === "string" ? children : undefined}
53
+ className={slots.action({ className })}
54
+ disabled={isDisabled}
55
+ feedback="fade"
56
+ onPress={handlePress}
57
+ >
58
+ {icon ? <Icon color={token} icon={icon} size="xl" /> : null}
59
+ <Text className={slots.actionLabel({ className: labelClassName })} numberOfLines={1}>
60
+ {children}
61
+ </Text>
62
+ </Pressable>
63
+ );
64
+ }
65
+ SelectionModeAction.displayName = "DelacourUI.SelectionMode.Action";
@@ -0,0 +1,79 @@
1
+ import type { ReactElement } from "react";
2
+ import type { ViewProps } from "react-native";
3
+ import Animated, {
4
+ FadeIn,
5
+ FadeInUp,
6
+ FadeOut,
7
+ FadeOutDown,
8
+ ReduceMotion,
9
+ useReducedMotion,
10
+ } from "react-native-reanimated";
11
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
12
+ import { useSelectionModePart } from "./selection-mode.context";
13
+ import {
14
+ resolveBarVisible,
15
+ SELECTION_MODE_MOTION,
16
+ type SelectionBarPlacement,
17
+ selectionModeVariants,
18
+ } from "./selection-mode.variants";
19
+
20
+ export type SelectionModeBarProps = ViewProps & {
21
+ /** `edge` spans the bottom of the page; `floating` is an inset card above it. Default `edge`. */
22
+ placement?: SelectionBarPlacement;
23
+ /** Keep the bar up while nothing is picked. Default `false`. */
24
+ isShownWhenEmpty?: boolean;
25
+ /** Clear the home indicator with the bottom safe-area inset. Default `true`. */
26
+ isSafeAreaAware?: boolean;
27
+ className?: string;
28
+ };
29
+
30
+ const ENTERING = FadeInUp.duration(SELECTION_MODE_MOTION.fadeMs);
31
+ const EXITING = FadeOutDown.duration(SELECTION_MODE_MOTION.reducedFadeMs);
32
+ const REDUCED_ENTERING = FadeIn.duration(SELECTION_MODE_MOTION.reducedFadeMs).reduceMotion(ReduceMotion.Never);
33
+ const REDUCED_EXITING = FadeOut.duration(SELECTION_MODE_MOTION.reducedFadeMs).reduceMotion(ReduceMotion.Never);
34
+
35
+ /** The breathing room under the actions when there is no inset to clear. */
36
+ const BAR_MIN_BOTTOM = 8;
37
+
38
+ /**
39
+ * The actions for what is picked, over the bottom edge of the root.
40
+ *
41
+ * Absolute, so it sits over the list rather than shrinking it — pad the bottom
42
+ * of the list so its last row can scroll clear. Hidden while nothing is picked
43
+ * unless `isShownWhenEmpty`, and never shown with the mode off. It slides up
44
+ * and fades in; under reduced motion it only fades.
45
+ *
46
+ * `edge` pads its own bottom with the safe-area inset. `floating` lifts the
47
+ * whole card by it instead, so the card keeps its own padding.
48
+ */
49
+ export function SelectionModeBar({
50
+ placement = "edge",
51
+ isShownWhenEmpty = false,
52
+ isSafeAreaAware = true,
53
+ className,
54
+ style,
55
+ children,
56
+ ...props
57
+ }: SelectionModeBarProps): ReactElement | null {
58
+ const { isActive, count } = useSelectionModePart("SelectionMode.Bar");
59
+ const isReducedMotion = useReducedMotion();
60
+ const insets = useSafeAreaInsets();
61
+ const inset = isSafeAreaAware ? insets.bottom : 0;
62
+
63
+ if (!resolveBarVisible({ count, isActive, isShownWhenEmpty })) return null;
64
+
65
+ const placementStyle = placement === "edge" ? { paddingBottom: Math.max(inset, BAR_MIN_BOTTOM) } : { bottom: inset };
66
+
67
+ return (
68
+ <Animated.View
69
+ className={selectionModeVariants({ placement }).bar({ className })}
70
+ entering={isReducedMotion ? REDUCED_ENTERING : ENTERING}
71
+ exiting={isReducedMotion ? REDUCED_EXITING : EXITING}
72
+ style={[placementStyle, style]}
73
+ {...props}
74
+ >
75
+ {children}
76
+ </Animated.View>
77
+ );
78
+ }
79
+ SelectionModeBar.displayName = "DelacourUI.SelectionMode.Bar";
@@ -0,0 +1,123 @@
1
+ import { Children, Fragment, type ReactElement, type ReactNode, useCallback, useState } from "react";
2
+ import { type LayoutChangeEvent, ScrollView, View, type ViewProps } from "react-native";
3
+ import { Separator } from "../separator";
4
+ import { Text } from "../text";
5
+ import { useSelectionModePart } from "./selection-mode.context";
6
+ import { resolveGridItemWidth, resolveGroupLayout, selectionModeVariants } from "./selection-mode.variants";
7
+
8
+ export type SelectionModeGroupProps = ViewProps & {
9
+ /** Lay the items out in a grid of this many columns. */
10
+ columns?: number;
11
+ /** A single scrolling row. Wins over `columns`. */
12
+ isHorizontal?: boolean;
13
+ /** Each item's width in a horizontal strip. Default 44. */
14
+ itemWidth?: number;
15
+ /** Space between items in a grid or a strip. Default 12. */
16
+ gap?: number;
17
+ /** A caption above the items, and the group's accessibility label. */
18
+ label?: string;
19
+ /** Hairlines between stacked items. Default `true` for a stack, `false` for a grid or a strip. */
20
+ hasSeparators?: boolean;
21
+ className?: string;
22
+ labelClassName?: string;
23
+ children?: ReactNode;
24
+ };
25
+
26
+ /**
27
+ * The room a ring and its badge need outside an item, so a strip's scroll view
28
+ * does not clip them.
29
+ */
30
+ const STRIP_INSET = 8;
31
+
32
+ /**
33
+ * Lays items out as a stacked card list, a grid or a horizontal strip.
34
+ *
35
+ * A stack is a card with hairlines between its rows. A grid splits the measured
36
+ * width evenly between `columns`, less the gaps. A strip scrolls sideways, each
37
+ * item `itemWidth` wide, with room around it for a `ring` to draw.
38
+ *
39
+ * A grid or a strip is announced as a list; `label` names it either way.
40
+ */
41
+ export function SelectionModeGroup({
42
+ columns,
43
+ isHorizontal = false,
44
+ itemWidth = 44,
45
+ gap = 12,
46
+ label,
47
+ hasSeparators,
48
+ className,
49
+ labelClassName,
50
+ children,
51
+ ...props
52
+ }: SelectionModeGroupProps): ReactElement {
53
+ useSelectionModePart("SelectionMode.Group");
54
+ const layout = resolveGroupLayout({ columns, isHorizontal });
55
+ const slots = selectionModeVariants({ layout: layout.kind });
56
+ const items = Children.toArray(children);
57
+ const [width, setWidth] = useState(0);
58
+
59
+ const handleLayout = useCallback((event: LayoutChangeEvent) => setWidth(event.nativeEvent.layout.width), []);
60
+
61
+ const renderBody = (): ReactElement => {
62
+ if (layout.kind === "strip") {
63
+ return (
64
+ <ScrollView
65
+ contentContainerStyle={{ gap, padding: STRIP_INSET }}
66
+ horizontal
67
+ showsHorizontalScrollIndicator={false}
68
+ >
69
+ {items.map((child, index) => (
70
+ <View key={keyOf(child, index)} style={{ width: itemWidth }}>
71
+ {child}
72
+ </View>
73
+ ))}
74
+ </ScrollView>
75
+ );
76
+ }
77
+
78
+ if (layout.kind === "grid") {
79
+ const cellWidth = resolveGridItemWidth({ columns: layout.columns, containerWidth: width, gap });
80
+ return (
81
+ <View className={slots.group()} onLayout={handleLayout} style={{ gap }}>
82
+ {width > 0
83
+ ? items.map((child, index) => (
84
+ <View key={keyOf(child, index)} style={{ width: cellWidth }}>
85
+ {child}
86
+ </View>
87
+ ))
88
+ : null}
89
+ </View>
90
+ );
91
+ }
92
+
93
+ const isSeparated = hasSeparators ?? true;
94
+ return (
95
+ <View className={slots.group()}>
96
+ {items.map((child, index) => (
97
+ <Fragment key={keyOf(child, index)}>
98
+ {isSeparated && index > 0 ? <Separator /> : null}
99
+ {child}
100
+ </Fragment>
101
+ ))}
102
+ </View>
103
+ );
104
+ };
105
+
106
+ return (
107
+ <View
108
+ accessibilityLabel={label}
109
+ accessibilityRole={layout.kind === "stack" ? undefined : "list"}
110
+ className={className}
111
+ {...props}
112
+ >
113
+ {label ? <Text className={slots.groupLabel({ className: labelClassName })}>{label}</Text> : null}
114
+ {renderBody()}
115
+ </View>
116
+ );
117
+ }
118
+ SelectionModeGroup.displayName = "DelacourUI.SelectionMode.Group";
119
+
120
+ /** The key `Children.toArray` already gave a child, or its index for a bare value. */
121
+ function keyOf(child: ReturnType<typeof Children.toArray>[number], index: number): string | number {
122
+ return typeof child === "object" && child !== null && "key" in child && child.key !== null ? child.key : index;
123
+ }
@@ -0,0 +1,85 @@
1
+ import { type ReactElement, useEffect } from "react";
2
+ import { AccessibilityInfo, Platform, type ViewProps } from "react-native";
3
+ import Animated, { FadeIn, FadeInDown, FadeOut, ReduceMotion, useReducedMotion } from "react-native-reanimated";
4
+ import { IconCrossMedium } from "../../icons/central";
5
+ import { Button } from "../button";
6
+ import { Icon } from "../icon";
7
+ import { Text } from "../text";
8
+ import { useSelectionModePart } from "./selection-mode.context";
9
+ import { resolveHeaderCount, SELECTION_MODE_MOTION, selectionModeVariants } from "./selection-mode.variants";
10
+
11
+ export type SelectionModeHeaderProps = ViewProps & {
12
+ /** Leads the count when the root knows its `values`. Default `"Select"`. */
13
+ title?: string;
14
+ /** Drop the select-all control. It is also dropped when the root has no `values` to pick from. */
15
+ isSelectAllHidden?: boolean;
16
+ /** `testID` for the close control, so a test or automation can leave the mode. */
17
+ closeTestID?: string;
18
+ /** `testID` for the select-all control. */
19
+ selectAllTestID?: string;
20
+ className?: string;
21
+ };
22
+
23
+ const ENTERING = FadeInDown.duration(SELECTION_MODE_MOTION.fadeMs).withInitialValues({
24
+ opacity: 0,
25
+ transform: [{ translateY: -SELECTION_MODE_MOTION.headerTravel }],
26
+ });
27
+ const EXITING = FadeOut.duration(SELECTION_MODE_MOTION.reducedFadeMs);
28
+ const REDUCED_ENTERING = FadeIn.duration(SELECTION_MODE_MOTION.reducedFadeMs).reduceMotion(ReduceMotion.Never);
29
+ const REDUCED_EXITING = FadeOut.duration(SELECTION_MODE_MOTION.reducedFadeMs).reduceMotion(ReduceMotion.Never);
30
+
31
+ /**
32
+ * Says the mode is on and offers the way out.
33
+ *
34
+ * Renders only while the mode is on: a close button that exits, the count —
35
+ * `"{title} · n of m"`, or `"n selected"` when the root has no `values` — and
36
+ * select-all, which turns into "Deselect all" once there is nothing left to add.
37
+ * It fades in from 8pt above; under reduced motion it only fades.
38
+ *
39
+ * The count is announced as it changes: a polite live region on Android, an
40
+ * announcement on iOS, which has no live regions.
41
+ */
42
+ export function SelectionModeHeader({
43
+ title = "Select",
44
+ isSelectAllHidden = false,
45
+ closeTestID,
46
+ selectAllTestID,
47
+ className,
48
+ ...props
49
+ }: SelectionModeHeaderProps): ReactElement | null {
50
+ const { isActive, count, total, isAllSelected, exit, selectAll, clear } =
51
+ useSelectionModePart("SelectionMode.Header");
52
+ const isReducedMotion = useReducedMotion();
53
+ const label = resolveHeaderCount({ count, title, total });
54
+ const slots = selectionModeVariants();
55
+
56
+ useEffect(() => {
57
+ if (isActive && Platform.OS === "ios") AccessibilityInfo.announceForAccessibility(label);
58
+ }, [isActive, label]);
59
+
60
+ if (!isActive) return null;
61
+
62
+ const hasSelectAll = !isSelectAllHidden && total !== undefined && total > 0;
63
+
64
+ return (
65
+ <Animated.View
66
+ className={slots.header({ className })}
67
+ entering={isReducedMotion ? REDUCED_ENTERING : ENTERING}
68
+ exiting={isReducedMotion ? REDUCED_EXITING : EXITING}
69
+ {...props}
70
+ >
71
+ <Button accessibilityLabel="Stop selecting" onPress={exit} size="icon-md" testID={closeTestID} variant="ghost">
72
+ <Icon icon={IconCrossMedium} />
73
+ </Button>
74
+ <Text accessibilityLiveRegion="polite" className={slots.headerTitle()} numberOfLines={1}>
75
+ {label}
76
+ </Text>
77
+ {hasSelectAll ? (
78
+ <Button onPress={isAllSelected ? clear : selectAll} size="sm" testID={selectAllTestID} variant="ghost">
79
+ <Button.Label>{isAllSelected ? "Deselect all" : "Select all"}</Button.Label>
80
+ </Button>
81
+ ) : null}
82
+ </Animated.View>
83
+ );
84
+ }
85
+ SelectionModeHeader.displayName = "DelacourUI.SelectionMode.Header";
@@ -0,0 +1,118 @@
1
+ import { type ReactElement, useEffect } from "react";
2
+ import type { ViewProps, ViewStyle } from "react-native";
3
+ import Animated, {
4
+ ReduceMotion,
5
+ useAnimatedStyle,
6
+ useReducedMotion,
7
+ useSharedValue,
8
+ withSequence,
9
+ withSpring,
10
+ withTiming,
11
+ } from "react-native-reanimated";
12
+ import { IconCheckmark1Small } from "../../icons/central";
13
+ import { Icon } from "../icon";
14
+ import { useSelectionModeItemContext, useSelectionModePart } from "./selection-mode.context";
15
+ import { SELECTION_MODE_CHECK_TOKEN, SELECTION_MODE_MOTION, selectionModeVariants } from "./selection-mode.variants";
16
+
17
+ export type SelectionModeIndicatorProps = ViewProps & {
18
+ /** The id it reports on. Defaults to the enclosing `SelectionMode.Item`'s. */
19
+ value?: string;
20
+ className?: string;
21
+ };
22
+
23
+ /**
24
+ * Animates a picked state in: the check fades, and the whole mark pops from 0.8
25
+ * on a spring. Under reduced motion the pop is dropped and the fade is 150ms.
26
+ */
27
+ function usePickedStyle(isSelected: boolean): ReturnType<typeof useAnimatedStyle<ViewStyle>> {
28
+ const isReducedMotion = useReducedMotion();
29
+ const picked = useSharedValue(isSelected ? 1 : 0);
30
+ const scale = useSharedValue(1);
31
+
32
+ useEffect(() => {
33
+ const target = isSelected ? 1 : 0;
34
+ if (isReducedMotion) {
35
+ picked.value = withTiming(target, {
36
+ duration: SELECTION_MODE_MOTION.reducedFadeMs,
37
+ reduceMotion: ReduceMotion.Never,
38
+ });
39
+ return;
40
+ }
41
+ picked.value = withTiming(target, { duration: SELECTION_MODE_MOTION.fadeMs });
42
+ if (isSelected)
43
+ scale.value = withSequence(withTiming(0.8, { duration: 0 }), withSpring(1, SELECTION_MODE_MOTION.spring));
44
+ }, [isReducedMotion, isSelected, picked, scale]);
45
+
46
+ return useAnimatedStyle(() => ({ opacity: picked.value, transform: [{ scale: scale.value }] }));
47
+ }
48
+
49
+ /**
50
+ * The round mark a `leading` item shows: an empty ring, or a filled circle with
51
+ * a check.
52
+ *
53
+ * Round on purpose. A square box is a form control being filled in; a round
54
+ * one is a thing picked out of a set. `Checkbox` stays square.
55
+ *
56
+ * Rendered by `SelectionMode.Item` itself. Render one by hand only for an item
57
+ * drawn with `indicator="none"` that wants the mark somewhere else in its
58
+ * layout — inside an item it reads that item's id, elsewhere it takes `value`.
59
+ */
60
+ export function SelectionModeIndicator({ value, className, ...props }: SelectionModeIndicatorProps): ReactElement {
61
+ const selection = useSelectionModePart("SelectionMode.Indicator");
62
+ const item = useSelectionModeItemContext();
63
+ const id = value ?? item?.value;
64
+ if (id === undefined) {
65
+ throw new Error("SelectionMode.Indicator needs a `value`, or an enclosing <SelectionMode.Item>.");
66
+ }
67
+
68
+ const isSelected = selection.isSelected(id);
69
+ const slots = selectionModeVariants({ isSelected });
70
+ const checkStyle = usePickedStyle(isSelected);
71
+
72
+ return (
73
+ <Animated.View
74
+ accessibilityElementsHidden
75
+ className={slots.indicator({ className })}
76
+ importantForAccessibility="no-hide-descendants"
77
+ {...props}
78
+ >
79
+ <Animated.View style={checkStyle}>
80
+ <Icon className={slots.indicatorCheck()} color={SELECTION_MODE_CHECK_TOKEN} icon={IconCheckmark1Small} />
81
+ </Animated.View>
82
+ </Animated.View>
83
+ );
84
+ }
85
+ SelectionModeIndicator.displayName = "DelacourUI.SelectionMode.Indicator";
86
+
87
+ /**
88
+ * The `ring` indicator: a primary ring a 2pt gap outside the item, and a small
89
+ * check badge at its top-end corner. Drawn only while the item is picked.
90
+ *
91
+ * Internal — `SelectionMode.Item` renders it. `className` reshapes the ring to
92
+ * the item's own corner: `rounded-full` around a round swatch.
93
+ */
94
+ export function SelectionModeRing({
95
+ isSelected,
96
+ className,
97
+ }: {
98
+ isSelected: boolean;
99
+ className?: string;
100
+ }): ReactElement {
101
+ const slots = selectionModeVariants({ indicator: "ring", isSelected });
102
+ const pickedStyle = usePickedStyle(isSelected);
103
+
104
+ return (
105
+ <Animated.View
106
+ accessibilityElementsHidden
107
+ className={slots.ring({ className })}
108
+ importantForAccessibility="no-hide-descendants"
109
+ pointerEvents="none"
110
+ style={pickedStyle}
111
+ >
112
+ <Animated.View className={slots.ringBadge()}>
113
+ <Icon className={slots.indicatorCheck()} color={SELECTION_MODE_CHECK_TOKEN} icon={IconCheckmark1Small} />
114
+ </Animated.View>
115
+ </Animated.View>
116
+ );
117
+ }
118
+ SelectionModeRing.displayName = "DelacourUI.SelectionMode.Item.Ring";