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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (482) hide show
  1. package/README.md +11 -7
  2. package/package.json +49 -5
  3. package/src/components/accordion/AGENTS.md +21 -9
  4. package/src/components/accordion/accordion-content.tsx +7 -4
  5. package/src/components/accordion/accordion-item.tsx +15 -13
  6. package/src/components/accordion/accordion.context.tsx +11 -5
  7. package/src/components/accordion/accordion.variants.test.ts +64 -3
  8. package/src/components/accordion/accordion.variants.ts +33 -9
  9. package/src/components/alert/AGENTS.md +102 -0
  10. package/src/components/alert/alert-action.tsx +17 -0
  11. package/src/components/alert/alert-close-button.tsx +50 -0
  12. package/src/components/alert/alert-content.tsx +15 -0
  13. package/src/components/alert/alert-description.tsx +16 -0
  14. package/src/components/alert/alert-glyphs.ts +25 -0
  15. package/src/components/alert/alert-indicator.tsx +40 -0
  16. package/src/components/alert/alert-title.tsx +18 -0
  17. package/src/components/alert/alert.context.tsx +69 -0
  18. package/src/components/alert/alert.tsx +160 -0
  19. package/src/components/alert/alert.types.ts +10 -0
  20. package/src/components/alert/alert.variants.test.ts +218 -0
  21. package/src/components/alert/alert.variants.ts +180 -0
  22. package/src/components/alert/index.ts +23 -0
  23. package/src/components/avatar/AGENTS.md +106 -0
  24. package/src/components/avatar/avatar-badge.tsx +51 -0
  25. package/src/components/avatar/avatar-group.tsx +139 -0
  26. package/src/components/avatar/avatar.context.tsx +93 -0
  27. package/src/components/avatar/avatar.tsx +201 -0
  28. package/src/components/avatar/avatar.utils.test.ts +174 -0
  29. package/src/components/avatar/avatar.utils.ts +141 -0
  30. package/src/components/avatar/avatar.variants.test.ts +251 -0
  31. package/src/components/avatar/avatar.variants.ts +270 -0
  32. package/src/components/avatar/index.ts +39 -0
  33. package/src/components/badge/AGENTS.md +5 -0
  34. package/src/components/badge/badge.tsx +6 -1
  35. package/src/components/bottom-sheet/AGENTS.md +186 -221
  36. package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
  37. package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
  38. package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -207
  39. package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
  40. package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
  41. package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
  42. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -120
  43. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
  44. package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
  45. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
  46. package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
  47. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -52
  48. package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
  49. package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
  50. package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
  51. package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
  52. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
  53. package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
  54. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -70
  55. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -105
  56. package/src/components/bottom-sheet/index.ts +39 -17
  57. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
  58. package/src/components/button/AGENTS.md +45 -11
  59. package/src/components/button/button-group.tsx +44 -8
  60. package/src/components/button/button.context.tsx +10 -0
  61. package/src/components/button/button.tsx +2 -1
  62. package/src/components/button/button.variants.test.ts +154 -0
  63. package/src/components/button/button.variants.ts +71 -6
  64. package/src/components/button/index.ts +2 -0
  65. package/src/components/calendar/AGENTS.md +142 -0
  66. package/src/components/calendar/calendar-caption.tsx +116 -0
  67. package/src/components/calendar/calendar-day.tsx +103 -0
  68. package/src/components/calendar/calendar-grid.tsx +183 -0
  69. package/src/components/calendar/calendar-header.tsx +34 -0
  70. package/src/components/calendar/calendar-nav.tsx +51 -0
  71. package/src/components/calendar/calendar-picker.tsx +128 -0
  72. package/src/components/calendar/calendar-weekdays.tsx +48 -0
  73. package/src/components/calendar/calendar.context.tsx +108 -0
  74. package/src/components/calendar/calendar.date.test.ts +456 -0
  75. package/src/components/calendar/calendar.date.ts +584 -0
  76. package/src/components/calendar/calendar.tsx +389 -0
  77. package/src/components/calendar/calendar.types.ts +87 -0
  78. package/src/components/calendar/calendar.variants.test.ts +363 -0
  79. package/src/components/calendar/calendar.variants.ts +426 -0
  80. package/src/components/calendar/index.ts +96 -0
  81. package/src/components/card/AGENTS.md +88 -0
  82. package/src/components/card/card-action.tsx +16 -0
  83. package/src/components/card/card-content.tsx +12 -0
  84. package/src/components/card/card-description.tsx +12 -0
  85. package/src/components/card/card-footer.tsx +30 -0
  86. package/src/components/card/card-header.tsx +45 -0
  87. package/src/components/card/card-title.tsx +25 -0
  88. package/src/components/card/card.context.tsx +64 -0
  89. package/src/components/card/card.tsx +94 -0
  90. package/src/components/card/card.types.ts +13 -0
  91. package/src/components/card/card.variants.test.ts +240 -0
  92. package/src/components/card/card.variants.ts +177 -0
  93. package/src/components/card/index.ts +15 -0
  94. package/src/components/chart/AGENTS.md +9 -1
  95. package/src/components/chart/chart.tsx +8 -1
  96. package/src/components/chip/AGENTS.md +113 -0
  97. package/src/components/chip/chip-close-button.tsx +48 -0
  98. package/src/components/chip/chip-end-content.tsx +15 -0
  99. package/src/components/chip/chip-label.tsx +21 -0
  100. package/src/components/chip/chip-start-content.tsx +16 -0
  101. package/src/components/chip/chip.context.tsx +66 -0
  102. package/src/components/chip/chip.tsx +268 -0
  103. package/src/components/chip/chip.types.ts +10 -0
  104. package/src/components/chip/chip.variants.test.ts +335 -0
  105. package/src/components/chip/chip.variants.ts +321 -0
  106. package/src/components/chip/index.ts +24 -0
  107. package/src/components/collapsible/AGENTS.md +97 -0
  108. package/src/components/collapsible/collapsible-content.tsx +97 -0
  109. package/src/components/collapsible/collapsible-description.tsx +17 -0
  110. package/src/components/collapsible/collapsible-indicator.tsx +92 -0
  111. package/src/components/collapsible/collapsible-title.tsx +17 -0
  112. package/src/components/collapsible/collapsible-trigger.tsx +133 -0
  113. package/src/components/collapsible/collapsible.context.tsx +91 -0
  114. package/src/components/collapsible/collapsible.tsx +167 -0
  115. package/src/components/collapsible/collapsible.types.ts +9 -0
  116. package/src/components/collapsible/collapsible.variants.test.ts +338 -0
  117. package/src/components/collapsible/collapsible.variants.ts +228 -0
  118. package/src/components/collapsible/index.ts +32 -0
  119. package/src/components/context-menu/AGENTS.md +103 -0
  120. package/src/components/context-menu/context-menu-content.tsx +64 -0
  121. package/src/components/context-menu/context-menu-item.tsx +22 -0
  122. package/src/components/context-menu/context-menu-preview.tsx +60 -0
  123. package/src/components/context-menu/context-menu-trigger.tsx +160 -0
  124. package/src/components/context-menu/context-menu.context.tsx +91 -0
  125. package/src/components/context-menu/context-menu.tsx +90 -0
  126. package/src/components/context-menu/context-menu.variants.test.ts +266 -0
  127. package/src/components/context-menu/context-menu.variants.ts +147 -0
  128. package/src/components/context-menu/index.ts +18 -0
  129. package/src/components/dialog/AGENTS.md +97 -0
  130. package/src/components/dialog/dialog-body.tsx +21 -0
  131. package/src/components/dialog/dialog-close.tsx +82 -0
  132. package/src/components/dialog/dialog-content.tsx +153 -0
  133. package/src/components/dialog/dialog-description.tsx +23 -0
  134. package/src/components/dialog/dialog-footer.tsx +32 -0
  135. package/src/components/dialog/dialog-header.tsx +20 -0
  136. package/src/components/dialog/dialog-title.tsx +42 -0
  137. package/src/components/dialog/dialog-trigger.tsx +54 -0
  138. package/src/components/dialog/dialog.context.tsx +90 -0
  139. package/src/components/dialog/dialog.tsx +119 -0
  140. package/src/components/dialog/dialog.variants.test.ts +224 -0
  141. package/src/components/dialog/dialog.variants.ts +173 -0
  142. package/src/components/dialog/index.ts +30 -0
  143. package/src/components/drawer/AGENTS.md +123 -0
  144. package/src/components/drawer/drawer-body.tsx +46 -0
  145. package/src/components/drawer/drawer-close.tsx +76 -0
  146. package/src/components/drawer/drawer-content.tsx +186 -0
  147. package/src/components/drawer/drawer-description.tsx +23 -0
  148. package/src/components/drawer/drawer-footer.tsx +21 -0
  149. package/src/components/drawer/drawer-header.tsx +42 -0
  150. package/src/components/drawer/drawer-title.tsx +35 -0
  151. package/src/components/drawer/drawer-trigger.tsx +54 -0
  152. package/src/components/drawer/drawer.context.tsx +114 -0
  153. package/src/components/drawer/drawer.tsx +119 -0
  154. package/src/components/drawer/drawer.variants.test.ts +392 -0
  155. package/src/components/drawer/drawer.variants.ts +244 -0
  156. package/src/components/drawer/index.ts +41 -0
  157. package/src/components/drawer/use-drawer-pan.ts +108 -0
  158. package/src/components/empty-state/AGENTS.md +90 -0
  159. package/src/components/empty-state/empty-state-content.tsx +17 -0
  160. package/src/components/empty-state/empty-state-description.tsx +12 -0
  161. package/src/components/empty-state/empty-state-header.tsx +17 -0
  162. package/src/components/empty-state/empty-state-media.tsx +56 -0
  163. package/src/components/empty-state/empty-state-title.tsx +18 -0
  164. package/src/components/empty-state/empty-state.context.tsx +65 -0
  165. package/src/components/empty-state/empty-state.tsx +84 -0
  166. package/src/components/empty-state/empty-state.types.ts +14 -0
  167. package/src/components/empty-state/empty-state.variants.test.ts +245 -0
  168. package/src/components/empty-state/empty-state.variants.ts +132 -0
  169. package/src/components/empty-state/index.ts +20 -0
  170. package/src/components/fab/AGENTS.md +119 -0
  171. package/src/components/fab/fab-action.tsx +118 -0
  172. package/src/components/fab/fab-group.tsx +214 -0
  173. package/src/components/fab/fab-label.tsx +18 -0
  174. package/src/components/fab/fab.context.tsx +125 -0
  175. package/src/components/fab/fab.tsx +156 -0
  176. package/src/components/fab/fab.types.ts +17 -0
  177. package/src/components/fab/fab.variants.test.ts +274 -0
  178. package/src/components/fab/fab.variants.ts +234 -0
  179. package/src/components/fab/index.ts +37 -0
  180. package/src/components/feedback/AGENTS.md +95 -0
  181. package/src/components/feedback/feedback-action.tsx +16 -0
  182. package/src/components/feedback/feedback-cancel.tsx +34 -0
  183. package/src/components/feedback/feedback-close.tsx +21 -0
  184. package/src/components/feedback/feedback-content.tsx +24 -0
  185. package/src/components/feedback/feedback-field.tsx +70 -0
  186. package/src/components/feedback/feedback-footer.tsx +21 -0
  187. package/src/components/feedback/feedback-panel.tsx +54 -0
  188. package/src/components/feedback/feedback-submit.tsx +74 -0
  189. package/src/components/feedback/feedback-title.tsx +34 -0
  190. package/src/components/feedback/feedback-trigger.tsx +20 -0
  191. package/src/components/feedback/feedback.context.tsx +73 -0
  192. package/src/components/feedback/feedback.tsx +135 -0
  193. package/src/components/feedback/feedback.variants.test.ts +185 -0
  194. package/src/components/feedback/feedback.variants.ts +128 -0
  195. package/src/components/feedback/index.ts +24 -0
  196. package/src/components/field/AGENTS.md +19 -0
  197. package/src/components/field/field-label.tsx +19 -5
  198. package/src/components/field/field.context.tsx +17 -0
  199. package/src/components/field/field.tsx +15 -2
  200. package/src/components/field/field.variants.test.ts +24 -0
  201. package/src/components/field/field.variants.ts +20 -0
  202. package/src/components/field/index.ts +1 -0
  203. package/src/components/item/AGENTS.md +92 -0
  204. package/src/components/item/index.ts +25 -0
  205. package/src/components/item/item-actions.tsx +28 -0
  206. package/src/components/item/item-content.tsx +15 -0
  207. package/src/components/item/item-description.tsx +12 -0
  208. package/src/components/item/item-footer.tsx +13 -0
  209. package/src/components/item/item-group.tsx +32 -0
  210. package/src/components/item/item-header.tsx +13 -0
  211. package/src/components/item/item-media.tsx +37 -0
  212. package/src/components/item/item-separator.tsx +13 -0
  213. package/src/components/item/item-title.tsx +17 -0
  214. package/src/components/item/item.context.tsx +66 -0
  215. package/src/components/item/item.tsx +218 -0
  216. package/src/components/item/item.types.ts +12 -0
  217. package/src/components/item/item.variants.test.ts +263 -0
  218. package/src/components/item/item.variants.ts +200 -0
  219. package/src/components/kpi/AGENTS.md +125 -0
  220. package/src/components/kpi/index.ts +45 -0
  221. package/src/components/kpi/kpi-action.tsx +12 -0
  222. package/src/components/kpi/kpi-content.tsx +35 -0
  223. package/src/components/kpi/kpi-footer.tsx +52 -0
  224. package/src/components/kpi/kpi-group.tsx +70 -0
  225. package/src/components/kpi/kpi-header.tsx +25 -0
  226. package/src/components/kpi/kpi-icon.tsx +28 -0
  227. package/src/components/kpi/kpi-sparkline.tsx +145 -0
  228. package/src/components/kpi/kpi-stat.tsx +20 -0
  229. package/src/components/kpi/kpi-title.tsx +19 -0
  230. package/src/components/kpi/kpi-trend.tsx +105 -0
  231. package/src/components/kpi/kpi-value.tsx +32 -0
  232. package/src/components/kpi/kpi.context.tsx +111 -0
  233. package/src/components/kpi/kpi.tsx +154 -0
  234. package/src/components/kpi/kpi.types.ts +9 -0
  235. package/src/components/kpi/kpi.variants.test.ts +428 -0
  236. package/src/components/kpi/kpi.variants.ts +347 -0
  237. package/src/components/label/AGENTS.md +89 -0
  238. package/src/components/label/index.ts +10 -0
  239. package/src/components/label/label.tsx +97 -0
  240. package/src/components/label/label.variants.test.ts +176 -0
  241. package/src/components/label/label.variants.ts +123 -0
  242. package/src/components/menu/AGENTS.md +176 -0
  243. package/src/components/menu/index.ts +44 -0
  244. package/src/components/menu/menu-background.tsx +17 -0
  245. package/src/components/menu/menu-checkbox-item.tsx +57 -0
  246. package/src/components/menu/menu-content.tsx +303 -0
  247. package/src/components/menu/menu-item.tsx +162 -0
  248. package/src/components/menu/menu-label.tsx +14 -0
  249. package/src/components/menu/menu-radio-group.tsx +50 -0
  250. package/src/components/menu/menu-radio-item.tsx +57 -0
  251. package/src/components/menu/menu-separator.tsx +11 -0
  252. package/src/components/menu/menu-sub-content.tsx +72 -0
  253. package/src/components/menu/menu-sub-trigger.tsx +50 -0
  254. package/src/components/menu/menu-sub.tsx +75 -0
  255. package/src/components/menu/menu-trigger.tsx +57 -0
  256. package/src/components/menu/menu.context.tsx +214 -0
  257. package/src/components/menu/menu.tsx +88 -0
  258. package/src/components/menu/menu.types.ts +33 -0
  259. package/src/components/menu/menu.variants.test.ts +400 -0
  260. package/src/components/menu/menu.variants.ts +353 -0
  261. package/src/components/meter/AGENTS.md +135 -0
  262. package/src/components/meter/index.ts +43 -0
  263. package/src/components/meter/meter-fill.tsx +19 -0
  264. package/src/components/meter/meter-header.tsx +17 -0
  265. package/src/components/meter/meter-label.tsx +18 -0
  266. package/src/components/meter/meter-output.tsx +42 -0
  267. package/src/components/meter/meter-segments.tsx +83 -0
  268. package/src/components/meter/meter-track.tsx +16 -0
  269. package/src/components/meter/meter.context.tsx +62 -0
  270. package/src/components/meter/meter.tsx +286 -0
  271. package/src/components/meter/meter.types.ts +30 -0
  272. package/src/components/meter/meter.variants.test.ts +430 -0
  273. package/src/components/meter/meter.variants.ts +347 -0
  274. package/src/components/overlay/AGENTS.md +89 -0
  275. package/src/components/overlay/index.ts +27 -0
  276. package/src/components/overlay/overlay-portal.tsx +84 -0
  277. package/src/components/overlay/overlay-presence.test.ts +46 -0
  278. package/src/components/overlay/overlay-presence.ts +49 -0
  279. package/src/components/overlay/overlay-registry.test.ts +96 -0
  280. package/src/components/overlay/overlay-registry.ts +99 -0
  281. package/src/components/overlay/overlay-scrim.tsx +49 -0
  282. package/src/components/overlay/overlay.context.tsx +48 -0
  283. package/src/components/overlay/overlay.tsx +118 -0
  284. package/src/components/overlay/overlay.variants.test.ts +37 -0
  285. package/src/components/overlay/overlay.variants.ts +33 -0
  286. package/src/components/overlay/use-overlay-back-handler.ts +34 -0
  287. package/src/components/overlay/use-overlay-presence.ts +87 -0
  288. package/src/components/popover/AGENTS.md +138 -0
  289. package/src/components/popover/index.ts +48 -0
  290. package/src/components/popover/popover-anchor.tsx +49 -0
  291. package/src/components/popover/popover-arrow.tsx +89 -0
  292. package/src/components/popover/popover-close.tsx +73 -0
  293. package/src/components/popover/popover-content.tsx +235 -0
  294. package/src/components/popover/popover-description.tsx +18 -0
  295. package/src/components/popover/popover-title.tsx +33 -0
  296. package/src/components/popover/popover-trigger.tsx +56 -0
  297. package/src/components/popover/popover.context.tsx +71 -0
  298. package/src/components/popover/popover.position.test.ts +354 -0
  299. package/src/components/popover/popover.position.ts +286 -0
  300. package/src/components/popover/popover.tsx +123 -0
  301. package/src/components/popover/popover.variants.test.ts +135 -0
  302. package/src/components/popover/popover.variants.ts +85 -0
  303. package/src/components/popover/use-anchor-measure.ts +76 -0
  304. package/src/components/popover/use-anchored-content.ts +219 -0
  305. package/src/components/pressable/AGENTS.md +11 -1
  306. package/src/components/pressable/pressable.tsx +7 -2
  307. package/src/components/pressable/pressable.variants.test.ts +29 -0
  308. package/src/components/pressable/pressable.variants.ts +17 -0
  309. package/src/components/progress/AGENTS.md +120 -0
  310. package/src/components/progress/index.ts +40 -0
  311. package/src/components/progress/progress-fill.tsx +152 -0
  312. package/src/components/progress/progress-header.tsx +19 -0
  313. package/src/components/progress/progress-label.tsx +27 -0
  314. package/src/components/progress/progress-output.tsx +47 -0
  315. package/src/components/progress/progress-track.tsx +38 -0
  316. package/src/components/progress/progress.context.tsx +79 -0
  317. package/src/components/progress/progress.tsx +171 -0
  318. package/src/components/progress/progress.types.ts +23 -0
  319. package/src/components/progress/progress.variants.test.ts +335 -0
  320. package/src/components/progress/progress.variants.ts +284 -0
  321. package/src/components/progress-button/AGENTS.md +116 -0
  322. package/src/components/progress-button/index.ts +36 -0
  323. package/src/components/progress-button/progress-button-done.tsx +33 -0
  324. package/src/components/progress-button/progress-button-label.tsx +27 -0
  325. package/src/components/progress-button/progress-button.context.tsx +93 -0
  326. package/src/components/progress-button/progress-button.tsx +414 -0
  327. package/src/components/progress-button/progress-button.variants.test.ts +314 -0
  328. package/src/components/progress-button/progress-button.variants.ts +236 -0
  329. package/src/components/provider/AGENTS.md +38 -25
  330. package/src/components/provider/provider.tsx +38 -11
  331. package/src/components/radio/AGENTS.md +8 -10
  332. package/src/components/radio/radio.variants.test.ts +5 -6
  333. package/src/components/radio/radio.variants.ts +6 -7
  334. package/src/components/rating/AGENTS.md +143 -0
  335. package/src/components/rating/index.ts +43 -0
  336. package/src/components/rating/rating-output.tsx +39 -0
  337. package/src/components/rating/rating-star.tsx +70 -0
  338. package/src/components/rating/rating-stars.tsx +219 -0
  339. package/src/components/rating/rating.context.tsx +79 -0
  340. package/src/components/rating/rating.tsx +163 -0
  341. package/src/components/rating/rating.variants.test.ts +347 -0
  342. package/src/components/rating/rating.variants.ts +354 -0
  343. package/src/components/screen/screen-navbar-back-button.tsx +1 -1
  344. package/src/components/selection-mode/AGENTS.md +155 -0
  345. package/src/components/selection-mode/index.ts +43 -0
  346. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  347. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  348. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  349. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  350. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  351. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  352. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  353. package/src/components/selection-mode/selection-mode.tsx +207 -0
  354. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  355. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  356. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  357. package/src/components/skeleton/AGENTS.md +120 -0
  358. package/src/components/skeleton/index.ts +15 -0
  359. package/src/components/skeleton/skeleton-clock.ts +48 -0
  360. package/src/components/skeleton/skeleton-group.tsx +64 -0
  361. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  362. package/src/components/skeleton/skeleton.context.tsx +45 -0
  363. package/src/components/skeleton/skeleton.tsx +227 -0
  364. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  365. package/src/components/skeleton/skeleton.variants.ts +222 -0
  366. package/src/components/slide-button/AGENTS.md +122 -0
  367. package/src/components/slide-button/index.ts +43 -0
  368. package/src/components/slide-button/slide-button-label.tsx +70 -0
  369. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  370. package/src/components/slide-button/slide-button.context.tsx +76 -0
  371. package/src/components/slide-button/slide-button.tsx +457 -0
  372. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  373. package/src/components/slide-button/slide-button.variants.ts +324 -0
  374. package/src/components/slider/index.ts +2 -0
  375. package/src/components/slider/slider-thumb.tsx +33 -1
  376. package/src/components/slider/slider.variants.test.ts +52 -0
  377. package/src/components/slider/slider.variants.ts +56 -2
  378. package/src/components/stack-card/AGENTS.md +169 -0
  379. package/src/components/stack-card/index.ts +37 -0
  380. package/src/components/stack-card/stack-card-action.tsx +66 -0
  381. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  382. package/src/components/stack-card/stack-card-card.tsx +71 -0
  383. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  384. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  385. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  386. package/src/components/stack-card/stack-card.context.tsx +117 -0
  387. package/src/components/stack-card/stack-card.tsx +557 -0
  388. package/src/components/stack-card/stack-card.types.ts +26 -0
  389. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  390. package/src/components/stack-card/stack-card.variants.ts +360 -0
  391. package/src/components/steps/AGENTS.md +153 -0
  392. package/src/components/steps/index.ts +45 -0
  393. package/src/components/steps/steps-connector.tsx +31 -0
  394. package/src/components/steps/steps-description.tsx +26 -0
  395. package/src/components/steps/steps-indicator.tsx +106 -0
  396. package/src/components/steps/steps-item.tsx +225 -0
  397. package/src/components/steps/steps-panel.tsx +41 -0
  398. package/src/components/steps/steps-title.tsx +32 -0
  399. package/src/components/steps/steps-track.tsx +59 -0
  400. package/src/components/steps/steps.context.tsx +123 -0
  401. package/src/components/steps/steps.tsx +166 -0
  402. package/src/components/steps/steps.variants.test.ts +388 -0
  403. package/src/components/steps/steps.variants.ts +468 -0
  404. package/src/components/surface/AGENTS.md +72 -0
  405. package/src/components/surface/index.ts +15 -0
  406. package/src/components/surface/surface.context.tsx +54 -0
  407. package/src/components/surface/surface.tsx +70 -0
  408. package/src/components/surface/surface.variants.test.ts +180 -0
  409. package/src/components/surface/surface.variants.ts +133 -0
  410. package/src/components/swipe/AGENTS.md +140 -0
  411. package/src/components/swipe/index.ts +31 -0
  412. package/src/components/swipe/swipe-action.tsx +99 -0
  413. package/src/components/swipe/swipe-end.tsx +14 -0
  414. package/src/components/swipe/swipe-group.tsx +55 -0
  415. package/src/components/swipe/swipe-panel.tsx +55 -0
  416. package/src/components/swipe/swipe-start.tsx +15 -0
  417. package/src/components/swipe/swipe.context.tsx +135 -0
  418. package/src/components/swipe/swipe.tsx +393 -0
  419. package/src/components/swipe/swipe.types.ts +7 -0
  420. package/src/components/swipe/swipe.variants.test.ts +337 -0
  421. package/src/components/swipe/swipe.variants.ts +290 -0
  422. package/src/components/switch/switch.variants.ts +2 -2
  423. package/src/components/tabs/AGENTS.md +4 -3
  424. package/src/components/tabs/tabs.variants.test.ts +1 -1
  425. package/src/components/tabs/tabs.variants.ts +4 -6
  426. package/src/components/textarea/AGENTS.md +86 -0
  427. package/src/components/textarea/index.ts +16 -0
  428. package/src/components/textarea/textarea.tsx +157 -0
  429. package/src/components/textarea/textarea.variants.test.ts +219 -0
  430. package/src/components/textarea/textarea.variants.ts +171 -0
  431. package/src/components/toast/AGENTS.md +140 -0
  432. package/src/components/toast/index.ts +74 -0
  433. package/src/components/toast/toast-action.tsx +59 -0
  434. package/src/components/toast/toast-api.ts +32 -0
  435. package/src/components/toast/toast-close.tsx +44 -0
  436. package/src/components/toast/toast-content.tsx +11 -0
  437. package/src/components/toast/toast-description.tsx +11 -0
  438. package/src/components/toast/toast-indicator.tsx +34 -0
  439. package/src/components/toast/toast-item.tsx +308 -0
  440. package/src/components/toast/toast-title.tsx +13 -0
  441. package/src/components/toast/toast-viewport.tsx +135 -0
  442. package/src/components/toast/toast.context.tsx +69 -0
  443. package/src/components/toast/toast.store.test.ts +259 -0
  444. package/src/components/toast/toast.store.ts +338 -0
  445. package/src/components/toast/toast.timer.test.ts +64 -0
  446. package/src/components/toast/toast.timer.ts +51 -0
  447. package/src/components/toast/toast.tsx +132 -0
  448. package/src/components/toast/toast.variants.test.ts +393 -0
  449. package/src/components/toast/toast.variants.ts +304 -0
  450. package/src/components/toggle-button/AGENTS.md +106 -0
  451. package/src/components/toggle-button/index.ts +30 -0
  452. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  453. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  454. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  455. package/src/components/toggle-button/toggle-button.tsx +172 -0
  456. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  457. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  458. package/src/components/tooltip/AGENTS.md +133 -0
  459. package/src/components/tooltip/index.ts +28 -0
  460. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  461. package/src/components/tooltip/tooltip-content.tsx +179 -0
  462. package/src/components/tooltip/tooltip-description.tsx +19 -0
  463. package/src/components/tooltip/tooltip-text.tsx +21 -0
  464. package/src/components/tooltip/tooltip-title.tsx +20 -0
  465. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  466. package/src/components/tooltip/tooltip.context.tsx +68 -0
  467. package/src/components/tooltip/tooltip.tsx +213 -0
  468. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  469. package/src/components/tooltip/tooltip.variants.ts +177 -0
  470. package/src/hooks/use-calm-motion.tsx +31 -0
  471. package/src/lib/calm-motion.test.ts +17 -0
  472. package/src/lib/calm-motion.ts +23 -0
  473. package/src/lib/cn.test.ts +14 -0
  474. package/src/lib/cn.ts +2 -2
  475. package/src/no-gorhom.test.ts +54 -0
  476. package/src/styles/geometry.test.ts +1 -0
  477. package/src/styles/theme.css +4 -0
  478. package/src/styles/tokens.css +11 -0
  479. package/src/styles/tokens.test.ts +22 -0
  480. package/src/styles/tokens.ts +20 -0
  481. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  482. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -1,10 +1,12 @@
1
1
  import { type ComponentProps, type ReactElement, useCallback } from "react";
2
2
  import type { AccessibilityActionEvent, LayoutChangeEvent, ViewProps } from "react-native";
3
3
  import Animated, { useAnimatedStyle, useDerivedValue, withSpring } from "react-native-reanimated";
4
+ import { useFieldContext } from "../field/field.context";
4
5
  import { useSliderPart } from "./slider.context";
5
6
  import {
6
7
  formatSliderValue,
7
8
  progressOf,
9
+ resolveThumbAccessibilityLabel,
8
10
  SLIDER_THUMB_ANIMATION,
9
11
  SLIDER_THUMB_SPRING,
10
12
  sliderVariants,
@@ -16,6 +18,15 @@ const CONTINUOUS_ACCESSIBILITY_STEPS = 10;
16
18
  export type SliderThumbProps = Omit<ViewProps, "children" | "style"> & {
17
19
  /** Which value this thumb drives. `0` unless the slider holds a range. */
18
20
  index?: number;
21
+ /**
22
+ * The name a screen reader reads before the value.
23
+ *
24
+ * Defaults to the enclosing `Field.Label`'s text — "Volume", or for a range
25
+ * "Price range, minimum" and "Price range, maximum". Set it to name one thumb
26
+ * yourself; it wins outright. A slider with no `Field` and no label here reads
27
+ * only its value.
28
+ */
29
+ accessibilityLabel?: string;
19
30
  className?: string;
20
31
  /**
21
32
  * Passed to the knob inside the capsule.
@@ -56,8 +67,21 @@ export type SliderThumbProps = Omit<ViewProps, "children" | "style"> & {
56
67
  * TalkBack read; the increment and decrement actions are what their swipe
57
68
  * gestures call. Without them a slider with no gesture on its thumb would have no
58
69
  * assistive path to its value at all.
70
+ *
71
+ * **The name comes from the `Field.Label` a row away.** The thumb is a capsule
72
+ * with no text, so a value alone — "40" — says nothing about what it is a value
73
+ * *of*. React Native has no `<label for>`, so the field's context carries the
74
+ * label's text down and `resolveThumbAccessibilityLabel` turns it into a name:
75
+ * the label itself for a lone thumb, the label plus the end it holds for a
76
+ * range. An `accessibilityLabel` written here wins over all of it.
59
77
  */
60
- export function SliderThumb({ index = 0, className, knobProps, ...props }: SliderThumbProps): ReactElement {
78
+ export function SliderThumb({
79
+ index = 0,
80
+ accessibilityLabel,
81
+ className,
82
+ knobProps,
83
+ ...props
84
+ }: SliderThumbProps): ReactElement {
61
85
  const {
62
86
  positions,
63
87
  trackSize,
@@ -75,6 +99,7 @@ export function SliderThumb({ index = 0, className, knobProps, ...props }: Slide
75
99
  isDisabled,
76
100
  updateValue,
77
101
  } = useSliderPart("Slider.Thumb");
102
+ const field = useFieldContext();
78
103
  const isVertical = orientation === "vertical";
79
104
  const slots = sliderVariants({ color, isDisabled, isInvalid, orientation, size });
80
105
 
@@ -118,6 +143,12 @@ export function SliderThumb({ index = 0, className, knobProps, ...props }: Slide
118
143
  const knobStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] }));
119
144
 
120
145
  const value = values[index] ?? minValue;
146
+ const label = resolveThumbAccessibilityLabel({
147
+ count: values.length,
148
+ index,
149
+ label: field?.label,
150
+ override: accessibilityLabel,
151
+ });
121
152
  const nudge = step > 0 ? step : (maxValue - minValue) / CONTINUOUS_ACCESSIBILITY_STEPS;
122
153
 
123
154
  const handleAccessibilityAction = useCallback(
@@ -131,6 +162,7 @@ export function SliderThumb({ index = 0, className, knobProps, ...props }: Slide
131
162
  return (
132
163
  <Animated.View
133
164
  accessibilityActions={ACCESSIBILITY_ACTIONS}
165
+ accessibilityLabel={label}
134
166
  accessibilityRole="adjustable"
135
167
  accessibilityState={{ disabled: isDisabled }}
136
168
  accessibilityValue={{
@@ -10,6 +10,7 @@ import {
10
10
  nearestThumbIndex,
11
11
  progressOf,
12
12
  resolveSliderAxes,
13
+ resolveThumbAccessibilityLabel,
13
14
  SLIDER_COLORS,
14
15
  SLIDER_DEFAULT_COLOR,
15
16
  SLIDER_DEFAULT_ORIENTATION,
@@ -23,6 +24,7 @@ import {
23
24
  SLIDER_SIZES,
24
25
  SLIDER_STEP,
25
26
  SLIDER_THUMB_ANIMATION,
27
+ SLIDER_THUMB_NAMES,
26
28
  SLIDER_THUMB_SPRING,
27
29
  shouldTickHaptic,
28
30
  sliderVariants,
@@ -802,3 +804,53 @@ describe("shouldTickHaptic", () => {
802
804
  expect(shouldTickHaptic({ ...base, position: 0, lastPosition: SLIDER_HAPTIC_MIN_TRAVEL })).toBe(true);
803
805
  });
804
806
  });
807
+
808
+ describe("resolveThumbAccessibilityLabel", () => {
809
+ // A lone thumb *is* the control, so it takes the control's name and nothing
810
+ // more — "Volume", not "Volume, thumb".
811
+ test("names a lone thumb after the slider", () => {
812
+ expect(resolveThumbAccessibilityLabel({ label: "Volume", index: 0, count: 1 })).toBe("Volume");
813
+ });
814
+
815
+ // Nothing to say is better than a made-up name: an unlabelled single slider
816
+ // falls through to the value alone, which is what the caller has to fix by
817
+ // wrapping it in a Field or passing a label.
818
+ test("names an unlabelled lone thumb nothing", () => {
819
+ expect(resolveThumbAccessibilityLabel({ label: null, index: 0, count: 1 })).toBeUndefined();
820
+ expect(resolveThumbAccessibilityLabel({ index: 0, count: 1 })).toBeUndefined();
821
+ });
822
+
823
+ test("tells a range's two thumbs apart by which end they hold", () => {
824
+ expect(resolveThumbAccessibilityLabel({ label: "Price range", index: 0, count: 2 })).toBe("Price range, minimum");
825
+ expect(resolveThumbAccessibilityLabel({ label: "Price range", index: 1, count: 2 })).toBe("Price range, maximum");
826
+ });
827
+
828
+ // Two thumbs reading the same bare number would be indistinguishable, so a
829
+ // range names its ends even with no label to hang them on.
830
+ test("still tells an unlabelled range's thumbs apart", () => {
831
+ expect(resolveThumbAccessibilityLabel({ label: null, index: 0, count: 2 })).toBe(SLIDER_THUMB_NAMES.minimum);
832
+ expect(resolveThumbAccessibilityLabel({ label: null, index: 1, count: 2 })).toBe(SLIDER_THUMB_NAMES.maximum);
833
+ });
834
+
835
+ test("counts the thumbs of a range with more than two", () => {
836
+ expect(resolveThumbAccessibilityLabel({ label: "Stops", index: 1, count: 3 })).toBe("Stops, 2 of 3");
837
+ expect(resolveThumbAccessibilityLabel({ label: null, index: 2, count: 3 })).toBe("Thumb 3 of 3");
838
+ });
839
+
840
+ test("lets an explicit name win over everything, even in a range", () => {
841
+ expect(resolveThumbAccessibilityLabel({ label: "Price range", index: 0, count: 2, override: "Lowest price" })).toBe(
842
+ "Lowest price"
843
+ );
844
+ expect(resolveThumbAccessibilityLabel({ index: 0, count: 1, override: "Brightness" })).toBe("Brightness");
845
+ });
846
+
847
+ // An empty override is an absence, not a name — the `??`-not-`||` rule read the
848
+ // other way round, because a screen reader given "" reads nothing at all.
849
+ test("treats an empty override as no override", () => {
850
+ expect(resolveThumbAccessibilityLabel({ label: "Volume", index: 0, count: 1, override: "" })).toBe("Volume");
851
+ });
852
+
853
+ test("ignores whitespace around the label it is handed", () => {
854
+ expect(resolveThumbAccessibilityLabel({ label: " Volume ", index: 0, count: 1 })).toBe("Volume");
855
+ });
856
+ });
@@ -167,8 +167,7 @@ export const SLIDER_RANGE_SEPARATOR = " – ";
167
167
  * wrong one.
168
168
  *
169
169
  * **The fade lands on the root**, which is a plain `View` here rather than a
170
- * `Pressable` — so unlike `Radio`, an `opacity-50` class on it is not overwritten
171
- * by an animated style writing `opacity` every frame.
170
+ * `Pressable`, so nothing animated shares the `opacity` property with it.
172
171
  *
173
172
  * No slot worn by a `View` carries `text-*` or `font-*`: `Slider.Output` renders
174
173
  * a `Text` preset and names a step — see {@link SLIDER_OUTPUT_TEXT_SIZE}.
@@ -604,4 +603,59 @@ export function resolveSliderAxes({ own, field }: { own?: SliderOwnAxes; field?:
604
603
  };
605
604
  }
606
605
 
606
+ /**
607
+ * What a range's thumbs are called when nothing names them.
608
+ *
609
+ * Written out once so the two ends and the counted form cannot drift apart, and
610
+ * so the playground and the docs can quote the same words.
611
+ */
612
+ export const SLIDER_THUMB_NAMES = { minimum: "Minimum", maximum: "Maximum", thumb: "Thumb" } as const;
613
+
614
+ /**
615
+ * The accessible name a `Slider.Thumb` announces.
616
+ *
617
+ * A thumb is a capsule with no text, so without this a screen reader reads a
618
+ * bare number and nothing about what it is a number *of*. The ladder is the one
619
+ * every prop in this package climbs: an explicit `accessibilityLabel` on the
620
+ * thumb wins, then the slider's name, then nothing — an empty override counts as
621
+ * no override, because a screen reader handed `""` reads nothing at all.
622
+ *
623
+ * A lone thumb *is* the control, so it takes the control's name unadorned. A
624
+ * range has to tell its two thumbs apart, so each is named for the end it holds
625
+ * — "Price range, minimum" — and that holds even with no name to hang it on,
626
+ * since two thumbs reading the same bare number are indistinguishable. Past two
627
+ * thumbs there are no ends to name, so they count: "Stops, 2 of 3".
628
+ *
629
+ * Pure, so the whole ladder is reachable from `bun test`. See AGENTS.md.
630
+ */
631
+ export function resolveThumbAccessibilityLabel({
632
+ label,
633
+ index,
634
+ count,
635
+ override,
636
+ }: {
637
+ /** The slider's own name — a `Field.Label`'s text, or nothing. */
638
+ label?: string | null;
639
+ /** Which thumb, in the order the values were given. */
640
+ index: number;
641
+ /** How many thumbs the slider holds. */
642
+ count: number;
643
+ /** An `accessibilityLabel` written on the thumb itself. */
644
+ override?: string;
645
+ }): string | undefined {
646
+ const explicit = override?.trim();
647
+ if (explicit) return explicit;
648
+
649
+ const name = label?.trim() || undefined;
650
+ if (count <= 1) return name;
651
+
652
+ if (count === 2) {
653
+ const end = index === 0 ? SLIDER_THUMB_NAMES.minimum : SLIDER_THUMB_NAMES.maximum;
654
+ return name ? `${name}, ${end.toLowerCase()}` : end;
655
+ }
656
+
657
+ const position = `${index + 1} of ${count}`;
658
+ return name ? `${name}, ${position}` : `${SLIDER_THUMB_NAMES.thumb} ${position}`;
659
+ }
660
+
607
661
  export type SliderVariantProps = VariantProps<typeof sliderVariants>;
@@ -0,0 +1,169 @@
1
+ # StackCard
2
+
3
+ A pile of cards taken one at a time by **throwing the top one off**. Compound
4
+ root plus `Card`, `Stamp`, `Empty`, `Actions` and `Action`. For a queue where
5
+ each item gets one decision and is then gone — a review queue, flashcards,
6
+ suggestions. It shows one card, so it is wrong for anything the reader must
7
+ compare or skim: browsing wants a carousel, and one row's actions want a swipe
8
+ row.
9
+
10
+ `import { StackCard } from "@delacour/react-native-ui/stack-card";`
11
+
12
+ ```tsx
13
+ <StackCard className="h-[460px]" onSwipe={(dir, i) => decide(people[i], dir)}>
14
+ <StackCard.Stamp direction="right" color="success">Yes</StackCard.Stamp>
15
+ <StackCard.Stamp direction="left" color="destructive">No</StackCard.Stamp>
16
+ {people.map((p) => <StackCard.Card key={p.id}>…</StackCard.Card>)}
17
+ <StackCard.Empty>All caught up</StackCard.Empty>
18
+ <StackCard.Actions>
19
+ <StackCard.Action action="left" icon={IconCrossSmall} label="Skip" />
20
+ <StackCard.Action action="undo" icon={IconArrowRotateCounterClockwise} />
21
+ <StackCard.Action action="right" icon={IconCheckmark2} label="Save" />
22
+ </StackCard.Actions>
23
+ </StackCard>
24
+ ```
25
+
26
+ ## Files
27
+
28
+ | File | What it holds |
29
+ | --- | --- |
30
+ | `index.ts` | → `@delacour/react-native-ui/stack-card` |
31
+ | `stack-card.tsx` | Root: partitions children, owns the pan, the throw, undo, decline, the mounted window |
32
+ | `stack-card-slot.tsx` | Internal animated box one card sits in — reads the shared offset, places itself |
33
+ | `stack-card-card.tsx` | `StackCard.Card` — the card's look, and the top card's accessibility actions |
34
+ | `stack-card-stamp.tsx` | `StackCard.Stamp` — fades in with progress toward its direction |
35
+ | `stack-card-empty.tsx` | `StackCard.Empty` |
36
+ | `stack-card-actions.tsx` | `StackCard.Actions` — the centred row under the pile |
37
+ | `stack-card-action.tsx` | `StackCard.Action` — a round `Button` that throws or undoes |
38
+ | `stack-card.context.tsx` | `StackCardProvider`, `useStackCard()`, `useStackCardContext()`, `useStackCardPart()`, the slot context |
39
+ | `stack-card.types.ts` | `StackCardHandle`, `StackCardState` — shared by the root and the context |
40
+ | `stack-card.variants.ts` | The slotted `tv()` and every pure resolver, the gesture's worklets included — no RN imports |
41
+ | `stack-card.variants.test.ts` | |
42
+
43
+ ## Axes
44
+
45
+ - **`directions`**: any of `left`, `right`, `up`, `down`. Default left and right.
46
+ - **`layout`** (the cards behind only): `stack` steps each down 8 and smaller by
47
+ 4%; `fan` rotates them ±3° per step, alternating, about the bottom centre;
48
+ `flat` hides them.
49
+ - **`depth`**: cards drawn behind the top, 0..4, default 2.
50
+ - **`threshold`**: the fraction of the card's width (or height) a drag must
51
+ cover, default 0.3.
52
+ - **Stamp `color`**: `primary`, `success`, `warning`, `info`, `destructive` —
53
+ each a token declared in both themes, which the test checks.
54
+
55
+ ## Design
56
+
57
+ - **One shared value drives everything: the top card's `{x, y}`.** The top card
58
+ follows it and tilts `x / width · 12°`, the stamps read progress toward their
59
+ direction from it, and the cards behind interpolate toward the slot ahead of
60
+ them on the furthest progress (`resolveDragProgress`). A drag writes shared
61
+ values and nothing else, so **nothing re-renders during a drag** — React only
62
+ hears about a throw after it lands.
63
+ - **The card behind is in place at the moment of release.** Progress reaches 1
64
+ at the threshold, and `resolveBehindTransform` at progress 1 is exactly the
65
+ transform of the slot ahead — a test asserts it for every layout. So when the
66
+ top card is thrown, the next card is already where the top was, and advancing
67
+ moves nothing. Without it the pile would jump a step on every throw.
68
+ - **The top moves on the UI thread, ahead of React.** `top` is a shared value the
69
+ throw sets the frame it lands; every slot places itself from `cardIndex − top`.
70
+ Waiting for React to re-render the new index would leave a frame where the
71
+ thrown card is gone and the next one has not been promoted.
72
+ - **Each worklet is self-contained**, and the resolvers it calls each carry
73
+ `"worklet"` and call no other module helper (see
74
+ [Pressable](../pressable/AGENTS.md) for the crash a module-scope helper
75
+ causes). `resolveBehindTransform`'s inner `at` is declared inside its body for
76
+ that reason.
77
+ - **Release:** `resolveStackRelease` projects the position 0.15 s along the
78
+ release velocity, so a flick throws from a short drag and a flick back cancels
79
+ a long one. Each axis is measured as a fraction of the card's own size along
80
+ it, and the dominant axis alone decides. A direction the deck does not allow
81
+ gives `null` and the card springs back.
82
+ - **A disallowed direction still gives a quarter** (`resolveDragOffset`) and
83
+ returns. A card that does not budge at all reads as frozen, where one that
84
+ gives and returns reads as "not that way".
85
+ - **A horizontal-only deck lets vertical drags through** — `activeOffsetX` and
86
+ `failOffsetY`, so it sits inside a vertical `ScrollView`. Vertical-only is the
87
+ mirror. A deck that throws all four ways claims both axes with `minDistance`
88
+ and **must not be nested in a scroller** — the doc comment says so. This is
89
+ the opposite of `Slider`'s `minDistance(0)`: a slider must win every touch on
90
+ its track, a deck must give up the ones that are scrolls.
91
+ - **A controlled deck that does not move `index` declines.** A throw calls
92
+ `onSwipe`, then `onIndexChange`, and records the throw as pending. The effect
93
+ after the next render compares: an index that moved is accepted and pushed to
94
+ the undo history; an index that stayed put flies the card back in from the
95
+ side it left. The pending record is state, not a ref, so the deck re-renders
96
+ and checks even when the parent's `onIndexChange` changes nothing. A confirm
97
+ step is built on this: decline, ask, and on "yes" call `ref.swipe(dir)` again
98
+ and accept it — the `controlled-decline` demo.
99
+ - **Undo restores from the side it left.** The deck keeps a history of the
100
+ directions it threw; `undo` pops one, steps the index back and flies the card
101
+ in from that side. `onSwipe` is not called for an undo. An index changed from
102
+ outside (a refill) clears the history, since the cards it remembers may not be
103
+ the ones now in the deck.
104
+ - **A card returns on a critically damped spring.** An underdamped return
105
+ overshoots rest to the other side, and the stamp for the opposite direction
106
+ fades in there — a card undone from the left would land flashing the
107
+ right-hand answer. The damping sits just above `2·√(stiffness·mass)`, so the
108
+ card arrives without crossing.
109
+ - **Only a window is mounted:** `[index − 1, index + depth + 1]` —
110
+ one behind for undo, one beyond the visible depth so the next card fades in
111
+ rather than popping. A deck of 500 mounts what a deck of five does
112
+ (`resolveMountedWindow`). The one beyond starts at opacity 0 and reaches the
113
+ last visible slot's opacity at progress 1.
114
+ - **Slots render deepest first**, so the top card draws over the ones behind and
115
+ the card just thrown — kept mounted for undo, hidden — draws over everything
116
+ when it flies back in.
117
+ - **Every transform pivots on the bottom centre.** That is what makes `fan` fan,
118
+ and what makes a smaller `stack` card's lower edge peek out below the one in
119
+ front. The pile keeps `depth · 8` points clear under the cards in `stack` so
120
+ that edge is not clipped by whatever sits below.
121
+ - **Anything that is not a `Stamp`, `Empty` or `Actions` is a card.** A caller
122
+ who wraps `StackCard.Card` in a component of their own still gets a card,
123
+ rather than a child silently dropped for having the wrong type
124
+ (`partitionStackChildren` takes the predicate as an argument to stay free of
125
+ the parts and of React Native).
126
+ - **Stamps are declared once on the deck and drawn on the top card**, on the side
127
+ they answer for — a right-hand stamp sits top-start, tilted −12°, where a card
128
+ leaning right lifts it into view. A stamp stays dark while the other axis
129
+ dominates, so a diagonal drag in a four-way deck shows one answer, not two.
130
+ Their text colour is on `stampLabel`, never on the stamp (rule 1).
131
+ - **An action's glyph takes the colour of its direction's stamp**, so the button
132
+ and the stamp read as one answer. `undo` is a `ghost` button; the directions
133
+ are `secondary`; all are `icon-lg` circles. A direction button works whether or
134
+ not the gesture allows that direction — a button is an explicit answer.
135
+ - **Haptics default to `selection`**, which is a deliberate departure from the
136
+ package's off-by-default: the tick when a drag crosses the threshold is the
137
+ feedback that says "let go now", and a deck without it feels uncertain. It
138
+ re-arms when the drag crosses back. A throw knocks with `medium`. `haptic={false}`
139
+ silences both.
140
+
141
+ ## Reduced motion
142
+
143
+ - **A card leaves by fading, not flying**, and an undone or declined card fades
144
+ back in at rest. Which card is on top is the information, so the swap itself
145
+ is kept.
146
+ - **The pile swaps without stepping:** under reduced motion the cards behind do
147
+ not interpolate during a drag; they take their new places when the throw lands.
148
+ - A short drag released returns at once rather than springing.
149
+
150
+ ## Accessibility
151
+
152
+ - **The top card is one `accessible` element** with a custom action per allowed
153
+ direction — named by `directionLabels[dir]`, or "Swipe left" and so on — plus
154
+ "Undo" while there is a card to bring back. No role is set: it is not
155
+ `adjustable`, since the actions are not a value going up and down.
156
+ - **Every card but the top is hidden** — `importantForAccessibility="no-hide-descendants"`
157
+ on Android, `accessibilityElementsHidden` on iOS — and takes no touches.
158
+ - Because the top card is one element, a control inside it is not separately
159
+ reachable. The deck's controls belong in `StackCard.Actions`, whose buttons
160
+ carry the same labels.
161
+ - Stamps are hidden from assistive technology; the actions say the same thing in
162
+ words.
163
+
164
+ ## Out of scope
165
+
166
+ - A carousel or any browsing mode: one card shows, by design.
167
+ - RTL mirroring of stamp placement — the stamps sit by physical side, because the
168
+ direction they answer for is physical too.
169
+ - A render callback per card, or virtualising beyond the mounted window.
@@ -0,0 +1,37 @@
1
+ export { StackCard, type StackCardProps } from "./stack-card";
2
+ export {
3
+ type StackCardContextValue,
4
+ StackCardProvider,
5
+ useStackCard,
6
+ useStackCardContext,
7
+ } from "./stack-card.context";
8
+ export type { StackCardHandle, StackCardState } from "./stack-card.types";
9
+ export {
10
+ partitionStackChildren,
11
+ resolveBehindTransform,
12
+ resolveDragOffset,
13
+ resolveDragProgress,
14
+ resolveExitOffset,
15
+ resolveMountedWindow,
16
+ resolveStackDepth,
17
+ resolveStackRelease,
18
+ resolveStampOpacity,
19
+ STACK_CARD_DEFAULT_DIRECTION_LABELS,
20
+ STACK_CARD_DEFAULT_DIRECTIONS,
21
+ STACK_CARD_DEFAULT_THRESHOLD,
22
+ STACK_CARD_DIRECTIONS,
23
+ STACK_CARD_LAYOUTS,
24
+ STACK_CARD_STAMP_COLORS,
25
+ type StackCardChildKind,
26
+ type StackCardDirection,
27
+ type StackCardLayout,
28
+ type StackCardStampColor,
29
+ type StackCardTransform,
30
+ type StackCardVariantProps,
31
+ stackCardVariants,
32
+ } from "./stack-card.variants";
33
+ export type { StackCardActionProps } from "./stack-card-action";
34
+ export type { StackCardActionsProps } from "./stack-card-actions";
35
+ export type { StackCardCardProps } from "./stack-card-card";
36
+ export type { StackCardEmptyProps } from "./stack-card-empty";
37
+ export type { StackCardStampProps } from "./stack-card-stamp";
@@ -0,0 +1,66 @@
1
+ import { type ReactElement, useCallback } from "react";
2
+ import { Button } from "../button";
3
+ import { Icon, type IconComponent } from "../icon";
4
+ import { useStackCardPart } from "./stack-card.context";
5
+ import type { StackCardDirection } from "./stack-card.variants";
6
+
7
+ export type StackCardActionProps = {
8
+ /** Throw the top card this way, or bring the last one back. */
9
+ action: StackCardDirection | "undo";
10
+ icon: IconComponent;
11
+ /**
12
+ * What a screen reader calls the button. Defaults to the deck's
13
+ * `directionLabels` for the direction, or "Undo".
14
+ */
15
+ label?: string;
16
+ /** Called before the deck acts. */
17
+ onPress?: () => void;
18
+ className?: string;
19
+ testID?: string;
20
+ };
21
+
22
+ /**
23
+ * A round button that answers for the top card without a drag.
24
+ *
25
+ * A direction throws the card exactly as a release would — `onSwipe`, then
26
+ * `onIndexChange` — and `undo` brings the last one back. A direction's glyph is
27
+ * tinted with the colour of the stamp answering for that direction, so the
28
+ * button and the stamp read as one answer. `undo` is a quiet ghost button,
29
+ * disabled while there is nothing to bring back; the directions disable once
30
+ * the deck is empty.
31
+ */
32
+ export function StackCardAction({
33
+ action,
34
+ icon,
35
+ label,
36
+ onPress,
37
+ className,
38
+ testID,
39
+ }: StackCardActionProps): ReactElement {
40
+ const { swipe, undo, canUndo, index, count, isDisabled, labelFor, stampColors } =
41
+ useStackCardPart("StackCard.Action");
42
+ const isUndo = action === "undo";
43
+ const isActionDisabled = isDisabled || (isUndo ? !canUndo : index >= count);
44
+ const color = isUndo ? undefined : stampColors[action];
45
+
46
+ const handlePress = useCallback(() => {
47
+ onPress?.();
48
+ if (action === "undo") undo();
49
+ else swipe(action);
50
+ }, [action, onPress, swipe, undo]);
51
+
52
+ return (
53
+ <Button
54
+ accessibilityLabel={label ?? (action === "undo" ? "Undo" : labelFor(action))}
55
+ className={className}
56
+ isDisabled={isActionDisabled}
57
+ onPress={handlePress}
58
+ size="icon-lg"
59
+ testID={testID}
60
+ variant={isUndo ? "ghost" : "secondary"}
61
+ >
62
+ <Icon color={color} icon={icon} />
63
+ </Button>
64
+ );
65
+ }
66
+ StackCardAction.displayName = "DelacourUI.StackCard.Action";
@@ -0,0 +1,18 @@
1
+ import type { ReactElement } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { stackCardVariants } from "./stack-card.variants";
4
+
5
+ export type StackCardActionsProps = ViewProps & {
6
+ className?: string;
7
+ };
8
+
9
+ /**
10
+ * A centred row of `StackCard.Action` buttons under the pile.
11
+ *
12
+ * Wherever it is written among the deck's children, the deck renders it below
13
+ * the pile, which takes the height left over.
14
+ */
15
+ export function StackCardActions({ className, ...props }: StackCardActionsProps): ReactElement {
16
+ return <View className={stackCardVariants().actions({ className })} {...props} />;
17
+ }
18
+ StackCardActions.displayName = "DelacourUI.StackCard.Actions";
@@ -0,0 +1,71 @@
1
+ import { type ReactElement, useCallback, useMemo } from "react";
2
+ import { type AccessibilityActionEvent, type AccessibilityActionInfo, View, type ViewProps } from "react-native";
3
+ import { useStackCardPart, useStackCardSlot } from "./stack-card.context";
4
+ import { STACK_CARD_DIRECTIONS, type StackCardDirection, stackCardVariants } from "./stack-card.variants";
5
+
6
+ export type StackCardCardProps = ViewProps & {
7
+ className?: string;
8
+ };
9
+
10
+ const UNDO_ACTION = "undo";
11
+
12
+ /** The accessibility action name a direction is announced under. */
13
+ function actionName(direction: StackCardDirection): string {
14
+ return `swipe-${direction}`;
15
+ }
16
+
17
+ /**
18
+ * One card in the deck.
19
+ *
20
+ * A card's look — the card fill, a hairline and the card corner — absolutely
21
+ * filling the pile, so every card occupies one box and only a transform tells
22
+ * them apart. Its position, tilt and opacity are the deck's, applied by the slot
23
+ * around it; the card itself holds no gesture.
24
+ *
25
+ * The top card is one `accessible` element whose custom actions are the deck's
26
+ * allowed directions — named by `directionLabels`, or "Swipe left" and so on —
27
+ * plus "Undo" while there is a card to bring back. A screen reader swipes the
28
+ * deck from the actions rotor rather than by dragging. Because the card is one
29
+ * element, a control inside it is not separately reachable: put the deck's
30
+ * controls in `StackCard.Actions`, not on the card.
31
+ */
32
+ export function StackCardCard({ className, children, ...props }: StackCardCardProps): ReactElement {
33
+ const { directions, labelFor, swipe, undo, canUndo } = useStackCardPart("StackCard.Card");
34
+ const slot = useStackCardSlot();
35
+ const isTop = slot?.isTop ?? false;
36
+
37
+ const actions = useMemo<AccessibilityActionInfo[]>(() => {
38
+ const list: AccessibilityActionInfo[] = directions.map((direction) => ({
39
+ label: labelFor(direction),
40
+ name: actionName(direction),
41
+ }));
42
+ if (canUndo) list.push({ label: "Undo", name: UNDO_ACTION });
43
+ return list;
44
+ }, [canUndo, directions, labelFor]);
45
+
46
+ const handleAction = useCallback(
47
+ (event: AccessibilityActionEvent) => {
48
+ const name = event.nativeEvent.actionName;
49
+ if (name === UNDO_ACTION) {
50
+ undo();
51
+ return;
52
+ }
53
+ const direction = STACK_CARD_DIRECTIONS.find((candidate) => actionName(candidate) === name);
54
+ if (direction) swipe(direction);
55
+ },
56
+ [swipe, undo]
57
+ );
58
+
59
+ return (
60
+ <View
61
+ accessibilityActions={isTop ? actions : undefined}
62
+ accessible={isTop}
63
+ className={stackCardVariants().card({ className })}
64
+ onAccessibilityAction={isTop ? handleAction : undefined}
65
+ {...props}
66
+ >
67
+ {children}
68
+ </View>
69
+ );
70
+ }
71
+ StackCardCard.displayName = "DelacourUI.StackCard.Card";
@@ -0,0 +1,30 @@
1
+ import type { ReactElement } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { Text } from "../text";
4
+ import { stackCardVariants } from "./stack-card.variants";
5
+
6
+ export type StackCardEmptyProps = ViewProps & {
7
+ className?: string;
8
+ };
9
+
10
+ /**
11
+ * What the pile shows once every card has gone — centred where the cards were.
12
+ *
13
+ * Rendered by the deck only while its index has reached the card count, so an
14
+ * `EmptyState` or a refill button goes here rather than behind the cards. Bare
15
+ * text is wrapped in a muted paragraph.
16
+ */
17
+ export function StackCardEmpty({ className, children, ...props }: StackCardEmptyProps): ReactElement {
18
+ return (
19
+ <View className={stackCardVariants().empty({ className })} {...props}>
20
+ {typeof children === "string" ? (
21
+ <Text align="center" color="muted" variant="paragraph">
22
+ {children}
23
+ </Text>
24
+ ) : (
25
+ children
26
+ )}
27
+ </View>
28
+ );
29
+ }
30
+ StackCardEmpty.displayName = "DelacourUI.StackCard.Empty";