@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
@@ -0,0 +1,213 @@
1
+ import { type ReactElement, type ReactNode, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
2
+ import { AccessibilityInfo } from "react-native";
3
+ import { scheduleOnUI } from "react-native-worklets";
4
+ import { useControllableState } from "../../hooks/use-controllable-state";
5
+ import { useOptionalOverlay } from "../overlay/overlay.context";
6
+ import { useAnchorMeasure } from "../popover/use-anchor-measure";
7
+ import { playHaptic } from "../pressable";
8
+ import { TooltipContext, type TooltipContextValue } from "./tooltip.context";
9
+ import {
10
+ resolveTooltipDuration,
11
+ shouldTooltipActivate,
12
+ TOOLTIP_DEFAULTS,
13
+ type TooltipGesture,
14
+ type TooltipOpenOn,
15
+ } from "./tooltip.variants";
16
+ import { TooltipArrow } from "./tooltip-arrow";
17
+ import { TooltipContent } from "./tooltip-content";
18
+ import { TooltipDescription } from "./tooltip-description";
19
+ import { TooltipText } from "./tooltip-text";
20
+ import { TooltipTitle } from "./tooltip-title";
21
+ import { TooltipTrigger } from "./tooltip-trigger";
22
+
23
+ export type TooltipProps = {
24
+ isOpen?: boolean;
25
+ /** Default false. */
26
+ defaultOpen?: boolean;
27
+ onOpenChange?: (isOpen: boolean) => void;
28
+ /** `"longPress"` leaves the trigger's tap alone; `"press"` is for a trigger with no tap of its own. Default `"longPress"`. */
29
+ openOn?: TooltipOpenOn;
30
+ /** Auto-hide this many ms after the entrance settles. 0 = until an outside tap or the trigger again. Default 1500. */
31
+ duration?: number;
32
+ /** The text a screen reader reads for the trigger — the tooltip's words without opening it. */
33
+ label?: string;
34
+ children: ReactNode;
35
+ };
36
+
37
+ /** The one tooltip on screen. Opening another closes it. */
38
+ let current: { id: string; close: () => void } | null = null;
39
+
40
+ /** Whether VoiceOver or TalkBack is running, kept current. */
41
+ function useScreenReaderEnabled(): boolean {
42
+ const [isEnabled, setEnabled] = useState(false);
43
+ useEffect(() => {
44
+ let isMounted = true;
45
+ AccessibilityInfo.isScreenReaderEnabled().then((value) => {
46
+ if (isMounted) setEnabled(value);
47
+ });
48
+ const subscription = AccessibilityInfo.addEventListener("screenReaderChanged", setEnabled);
49
+ return () => {
50
+ isMounted = false;
51
+ subscription.remove();
52
+ };
53
+ }, []);
54
+ return isEnabled;
55
+ }
56
+
57
+ function TooltipRoot({
58
+ isOpen: isOpenProp,
59
+ defaultOpen = false,
60
+ onOpenChange,
61
+ openOn = TOOLTIP_DEFAULTS.openOn,
62
+ duration: durationProp,
63
+ label,
64
+ children,
65
+ }: TooltipProps): ReactElement {
66
+ const id = useId();
67
+ const [isOpen, setOpen] = useControllableState({
68
+ value: isOpenProp,
69
+ defaultValue: defaultOpen,
70
+ onChange: onOpenChange,
71
+ });
72
+ const isScreenReaderEnabled = useScreenReaderEnabled();
73
+ const duration = resolveTooltipDuration(durationProp, { isScreenReaderEnabled });
74
+ const trigger = useAnchorMeasure({ isEnabled: isOpen });
75
+ const subscribeTouchStart = useOptionalOverlay()?.subscribeTouchStart;
76
+
77
+ // Set by the trigger's own `onTouchStart`, which bubbles to it before the
78
+ // provider hears the same touch and closes the tooltip — so the activation
79
+ // that follows knows this touch began on an open tooltip and leaves it shut.
80
+ const wasOpenAtTriggerTouch = useRef(false);
81
+ const isTouchInsideContent = useRef(false);
82
+
83
+ const close = useCallback(() => setOpen(false), [setOpen]);
84
+
85
+ const activate = useCallback(
86
+ (gesture: TooltipGesture) => {
87
+ if (!shouldTooltipActivate({ openOn, gesture, isScreenReaderEnabled })) return;
88
+ const wasOpen = wasOpenAtTriggerTouch.current || isOpen;
89
+ wasOpenAtTriggerTouch.current = false;
90
+ if (wasOpen) {
91
+ setOpen(false);
92
+ return;
93
+ }
94
+ if (gesture === "longPress") scheduleOnUI(playHaptic, "selection");
95
+ setOpen(true);
96
+ },
97
+ [openOn, isScreenReaderEnabled, isOpen, setOpen]
98
+ );
99
+
100
+ const onTriggerTouchStart = useCallback(() => {
101
+ wasOpenAtTriggerTouch.current = isOpen;
102
+ }, [isOpen]);
103
+
104
+ const onContentTouchStart = useCallback(() => {
105
+ isTouchInsideContent.current = true;
106
+ }, []);
107
+
108
+ useEffect(() => {
109
+ if (!isOpen || subscribeTouchStart === undefined) return;
110
+ return subscribeTouchStart(() => {
111
+ if (isTouchInsideContent.current) {
112
+ isTouchInsideContent.current = false;
113
+ return;
114
+ }
115
+ setOpen(false);
116
+ });
117
+ }, [isOpen, subscribeTouchStart, setOpen]);
118
+
119
+ useEffect(() => {
120
+ if (!isOpen) return;
121
+ if (current !== null && current.id !== id) current.close();
122
+ current = { id, close };
123
+ return () => {
124
+ if (current?.id === id) current = null;
125
+ };
126
+ }, [isOpen, id, close]);
127
+
128
+ const value = useMemo<TooltipContextValue>(
129
+ () => ({
130
+ isOpen,
131
+ setOpen,
132
+ close,
133
+ openOn,
134
+ label,
135
+ duration,
136
+ activate,
137
+ onTriggerTouchStart,
138
+ onContentTouchStart,
139
+ anchorRect: trigger.rect,
140
+ triggerRef: trigger.ref,
141
+ }),
142
+ [
143
+ isOpen,
144
+ setOpen,
145
+ close,
146
+ openOn,
147
+ label,
148
+ duration,
149
+ activate,
150
+ onTriggerTouchStart,
151
+ onContentTouchStart,
152
+ trigger.rect,
153
+ trigger.ref,
154
+ ]
155
+ );
156
+
157
+ return <TooltipContext.Provider value={value}>{children}</TooltipContext.Provider>;
158
+ }
159
+
160
+ /**
161
+ * A short label naming the control under the finger — what an icon-only
162
+ * button does, a shortcut, a one-line hint.
163
+ *
164
+ * Mobile has no hover, so it opens on a **long press** by default, with a
165
+ * selection haptic, and the control's own tap still does what it did;
166
+ * `openOn="press"` opens it on a tap instead, for a trigger with no tap of its
167
+ * own — an info glyph. It hides itself `duration` ms after it appears, on the
168
+ * trigger again, or on a tap anywhere else — and that tap still lands on what
169
+ * it was aimed at. Opening one tooltip closes any other.
170
+ *
171
+ * It is not interactive: anything with a button in it is a `Popover`. The
172
+ * panel is hidden from assistive technology; `label` reaches the trigger as
173
+ * its accessibility label, or its hint when it already has a label, so
174
+ * VoiceOver reads the words without anything opening.
175
+ *
176
+ * It draws in the overlay layer, so it needs `OverlayProvider` at the app root
177
+ * — which is also what hears the outside tap.
178
+ *
179
+ * @example
180
+ * <Tooltip label="Share">
181
+ * <Tooltip.Trigger asChild>
182
+ * <Button size="icon-md" variant="ghost"><Icon icon={IconShare} /></Button>
183
+ * </Tooltip.Trigger>
184
+ * <Tooltip.Content>
185
+ * <Tooltip.Arrow />
186
+ * <Tooltip.Text>Share</Tooltip.Text>
187
+ * </Tooltip.Content>
188
+ * </Tooltip>
189
+ *
190
+ * @example
191
+ * <Tooltip openOn="press" duration={0}>
192
+ * <Tooltip.Trigger accessibilityLabel="About sync"><Icon icon={IconCircleInfo} /></Tooltip.Trigger>
193
+ * <Tooltip.Content variant="surface">
194
+ * <Tooltip.Title>Sync</Tooltip.Title>
195
+ * <Tooltip.Description>Changes reach your other devices within a minute.</Tooltip.Description>
196
+ * </Tooltip.Content>
197
+ * </Tooltip>
198
+ */
199
+ export const Tooltip = Object.assign(TooltipRoot, {
200
+ /** The control the tooltip names — `Pressable`, or `asChild` to donate the gesture. */
201
+ Trigger: TooltipTrigger,
202
+ /** The panel — teleported, measured, placed and animated; never interactive. */
203
+ Content: TooltipContent,
204
+ /** The arrow pointing from the panel at the trigger. */
205
+ Arrow: TooltipArrow,
206
+ /** The one-line label. */
207
+ Text: TooltipText,
208
+ /** A heading, for the surface variant. */
209
+ Title: TooltipTitle,
210
+ /** Supporting copy under the title, for the surface variant. */
211
+ Description: TooltipDescription,
212
+ displayName: "DelacourUI.Tooltip",
213
+ });
@@ -0,0 +1,231 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createElement } from "react";
3
+ import { declaredTokens } from "../../styles/theme-tokens.test";
4
+ import {
5
+ readableTextOf,
6
+ resolveTooltipAccessibility,
7
+ resolveTooltipDuration,
8
+ shouldTooltipActivate,
9
+ TOOLTIP_DEFAULTS,
10
+ TOOLTIP_DURATION,
11
+ TOOLTIP_ENTER_DISTANCE,
12
+ TOOLTIP_VARIANTS,
13
+ tooltipVariants,
14
+ } from "./tooltip.variants";
15
+
16
+ const LIGHT = declaredTokens("light");
17
+ const DARK = declaredTokens("dark");
18
+
19
+ /** `border-t` and friends set a width, not a colour, and name no token. */
20
+ const STRUCTURAL_BORDER_SUFFIXES = new Set(["t", "b", "l", "r", "x", "y", "s", "e"]);
21
+
22
+ /** Tailwind's own size steps share the `text-` prefix with colours and name no token. */
23
+ const TEXT_SIZES = new Set(["xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl"]);
24
+
25
+ /** Every theme token a class string paints with, with any `/alpha` suffix dropped. */
26
+ function colorTokens(cls: string): string[] {
27
+ const tokens: string[] = [];
28
+ for (const [, utility, token] of cls.matchAll(/\b(bg|border|text)-([a-z][\w-]*)(?:\/\d+)?\b/g)) {
29
+ if (utility === "border" && STRUCTURAL_BORDER_SUFFIXES.has(token)) continue;
30
+ if (utility === "text" && TEXT_SIZES.has(token)) continue;
31
+ tokens.push(token);
32
+ }
33
+ return tokens;
34
+ }
35
+
36
+ /** The slots this component declares, pinned so a new one has to be added before the sweeps can miss it. */
37
+ const SLOT_NAMES = ["content", "arrow", "text", "title", "description"] as const;
38
+
39
+ describe("tooltipVariants — slots", () => {
40
+ test("declares every slot in both variants", () => {
41
+ for (const variant of TOOLTIP_VARIANTS) {
42
+ const slots = tooltipVariants({ variant });
43
+ for (const name of SLOT_NAMES) expect(typeof slots[name]).toBe("function");
44
+ }
45
+ });
46
+
47
+ test("every token a slot paints with exists in both themes", () => {
48
+ for (const variant of TOOLTIP_VARIANTS) {
49
+ const slots = tooltipVariants({ variant });
50
+ for (const name of SLOT_NAMES) {
51
+ for (const token of colorTokens(slots[name]() ?? "")) {
52
+ expect(LIGHT.has(token)).toBe(true);
53
+ expect(DARK.has(token)).toBe(true);
54
+ }
55
+ }
56
+ }
57
+ });
58
+
59
+ test("the default variant is inverted", () => {
60
+ expect(tooltipVariants().content()).toBe(tooltipVariants({ variant: "inverted" }).content());
61
+ });
62
+
63
+ test("a caller's className wins on the panel", () => {
64
+ const content = tooltipVariants().content({ className: "px-4" });
65
+ expect(content).toContain("px-4");
66
+ expect(content).not.toMatch(/\bpx-2\b/);
67
+ });
68
+ });
69
+
70
+ describe("tooltipVariants — inverted", () => {
71
+ const slots = tooltipVariants({ variant: "inverted" });
72
+
73
+ test("the panel is the foreground colour, a small corner and compact padding, with no border", () => {
74
+ const content = slots.content();
75
+ expect(content).toContain("bg-foreground");
76
+ expect(content).toContain("rounded-md");
77
+ expect(content).toMatch(/\bpx-2\b/);
78
+ expect(content).toMatch(/\bpy-1\b/);
79
+ expect(content).not.toMatch(/\bborder\b/);
80
+ });
81
+
82
+ test("the arrow wears the panel's fill and no border", () => {
83
+ const arrow = slots.arrow();
84
+ expect(arrow).toContain("bg-foreground");
85
+ expect(arrow).not.toMatch(/\bborder-border\b/);
86
+ });
87
+
88
+ test("every piece of text is drawn in the background colour, so it reads on the inverted fill", () => {
89
+ expect(slots.text()).toContain("text-background");
90
+ expect(slots.title()).toContain("text-background");
91
+ expect(colorTokens(slots.description())).toEqual(["background"]);
92
+ });
93
+ });
94
+
95
+ describe("tooltipVariants — surface", () => {
96
+ const slots = tooltipVariants({ variant: "surface" });
97
+
98
+ test("the panel is a popover surface with a hairline, the card corner and room for two lines", () => {
99
+ const content = slots.content();
100
+ expect(content).toContain("bg-popover");
101
+ expect(content).toContain("border");
102
+ expect(content).toContain("border-border");
103
+ expect(content).toContain("rounded-lg");
104
+ expect(content).toMatch(/\bgap-\d/);
105
+ });
106
+
107
+ test("the arrow is the panel's fill with the panel's border on two edges", () => {
108
+ const arrow = slots.arrow();
109
+ expect(arrow).toContain("bg-popover");
110
+ expect(arrow).toContain("border-border");
111
+ expect(arrow).toContain("border-b");
112
+ expect(arrow).toContain("border-r");
113
+ });
114
+
115
+ test("text and title sit on the popover foreground; the description is muted", () => {
116
+ expect(slots.text()).toContain("text-popover-foreground");
117
+ expect(slots.title()).toContain("text-popover-foreground");
118
+ expect(slots.description()).toContain("text-muted-foreground");
119
+ });
120
+ });
121
+
122
+ describe("resolveTooltipDuration", () => {
123
+ test("passes a positive duration through", () => {
124
+ expect(resolveTooltipDuration(800, { isScreenReaderEnabled: false })).toBe(800);
125
+ });
126
+
127
+ test("0 means stay until dismissed", () => {
128
+ expect(resolveTooltipDuration(0, { isScreenReaderEnabled: false })).toBe(0);
129
+ });
130
+
131
+ test("an omitted duration is the default", () => {
132
+ expect(resolveTooltipDuration(undefined, { isScreenReaderEnabled: false })).toBe(TOOLTIP_DURATION);
133
+ });
134
+
135
+ test("a negative or non-finite duration falls back to the default rather than hiding at once", () => {
136
+ expect(resolveTooltipDuration(-1, { isScreenReaderEnabled: false })).toBe(TOOLTIP_DURATION);
137
+ expect(resolveTooltipDuration(Number.NaN, { isScreenReaderEnabled: false })).toBe(TOOLTIP_DURATION);
138
+ expect(resolveTooltipDuration(Number.POSITIVE_INFINITY, { isScreenReaderEnabled: false })).toBe(TOOLTIP_DURATION);
139
+ });
140
+
141
+ test("with a screen reader on it never times out — whoever opened it reads at their own pace", () => {
142
+ expect(resolveTooltipDuration(800, { isScreenReaderEnabled: true })).toBe(0);
143
+ expect(resolveTooltipDuration(undefined, { isScreenReaderEnabled: true })).toBe(0);
144
+ });
145
+ });
146
+
147
+ describe("shouldTooltipActivate", () => {
148
+ test("a long-press tooltip opens on a long press and ignores the tap", () => {
149
+ expect(shouldTooltipActivate({ openOn: "longPress", gesture: "longPress", isScreenReaderEnabled: false })).toBe(
150
+ true
151
+ );
152
+ expect(shouldTooltipActivate({ openOn: "longPress", gesture: "press", isScreenReaderEnabled: false })).toBe(false);
153
+ });
154
+
155
+ test("a press tooltip opens on a tap and ignores the long press", () => {
156
+ expect(shouldTooltipActivate({ openOn: "press", gesture: "press", isScreenReaderEnabled: false })).toBe(true);
157
+ expect(shouldTooltipActivate({ openOn: "press", gesture: "longPress", isScreenReaderEnabled: false })).toBe(false);
158
+ });
159
+
160
+ test("with a screen reader on, a long press never opens it — the label already reached the trigger", () => {
161
+ expect(shouldTooltipActivate({ openOn: "longPress", gesture: "longPress", isScreenReaderEnabled: true })).toBe(
162
+ false
163
+ );
164
+ });
165
+
166
+ test("with a screen reader on, a press tooltip still opens, for a partially sighted user", () => {
167
+ expect(shouldTooltipActivate({ openOn: "press", gesture: "press", isScreenReaderEnabled: true })).toBe(true);
168
+ });
169
+ });
170
+
171
+ describe("resolveTooltipAccessibility", () => {
172
+ test("no label, nothing to add", () => {
173
+ expect(resolveTooltipAccessibility({})).toEqual({});
174
+ expect(resolveTooltipAccessibility({ label: "", triggerLabel: "Share" })).toEqual({});
175
+ });
176
+
177
+ test("a trigger with no label of its own is named by the tooltip", () => {
178
+ expect(resolveTooltipAccessibility({ label: "Share" })).toEqual({ accessibilityLabel: "Share" });
179
+ expect(resolveTooltipAccessibility({ label: "Share", triggerLabel: "" })).toEqual({ accessibilityLabel: "Share" });
180
+ });
181
+
182
+ test("a trigger that already has a label keeps it, and the tooltip becomes its hint", () => {
183
+ expect(resolveTooltipAccessibility({ label: "Copies a link to the clipboard", triggerLabel: "Share" })).toEqual({
184
+ accessibilityHint: "Copies a link to the clipboard",
185
+ });
186
+ });
187
+
188
+ test("a label identical to the trigger's is not read twice", () => {
189
+ expect(resolveTooltipAccessibility({ label: "Share", triggerLabel: "Share" })).toEqual({});
190
+ });
191
+ });
192
+
193
+ describe("readableTextOf", () => {
194
+ test("reads a bare string or number", () => {
195
+ expect(readableTextOf("Sync now")).toBe("Sync now");
196
+ expect(readableTextOf(3)).toBe("3");
197
+ });
198
+
199
+ test("reads text nested in elements, the way a screen reader names a control from its content", () => {
200
+ const child = createElement("Label", null, "Sync ", createElement("Strong", null, "now"));
201
+ expect(readableTextOf(child)).toBe("Sync now");
202
+ });
203
+
204
+ test("an icon-only child has no text", () => {
205
+ expect(readableTextOf(createElement("Icon", { icon: "share" }))).toBeUndefined();
206
+ expect(readableTextOf(undefined)).toBeUndefined();
207
+ });
208
+
209
+ test("whitespace alone is no text", () => {
210
+ expect(readableTextOf(" ")).toBeUndefined();
211
+ });
212
+ });
213
+
214
+ describe("constants", () => {
215
+ test("defaults match the documented API", () => {
216
+ expect(TOOLTIP_DEFAULTS).toEqual({
217
+ placement: "top",
218
+ align: "center",
219
+ offset: 6,
220
+ alignOffset: 0,
221
+ width: "content-fit",
222
+ variant: "inverted",
223
+ openOn: "longPress",
224
+ });
225
+ expect(TOOLTIP_DURATION).toBe(1500);
226
+ });
227
+
228
+ test("the entrance is a short slide from the resolved side", () => {
229
+ expect(TOOLTIP_ENTER_DISTANCE).toBe(4);
230
+ });
231
+ });
@@ -0,0 +1,177 @@
1
+ import { Children, isValidElement, type ReactNode } from "react";
2
+ import type { VariantProps } from "tailwind-variants";
3
+ import { tv } from "../../lib/tv";
4
+ import type { PopoverAlign, PopoverPlacement, PopoverWidth } from "../popover/popover.position";
5
+
6
+ /** The two looks: a dark chip that reads over anything, or a popover card for a title and a line. */
7
+ export const TOOLTIP_VARIANTS = ["inverted", "surface"] as const;
8
+ export type TooltipVariant = (typeof TOOLTIP_VARIANTS)[number];
9
+
10
+ /** What opens it: a long press leaves the trigger's own tap alone; a press is for a trigger with no tap of its own. */
11
+ export type TooltipOpenOn = "longPress" | "press";
12
+
13
+ /** The gesture that reached the trigger. */
14
+ export type TooltipGesture = "longPress" | "press";
15
+
16
+ /**
17
+ * What `Tooltip` and `Tooltip.Content` do when a prop is left out.
18
+ *
19
+ * Above the trigger and centred on it, 6pt clear — closer than a popover's 8,
20
+ * because a one-line label belongs to its control — inverted, as wide as its
21
+ * words, opened by a long press.
22
+ */
23
+ export const TOOLTIP_DEFAULTS = {
24
+ placement: "top",
25
+ align: "center",
26
+ offset: 6,
27
+ alignOffset: 0,
28
+ width: "content-fit",
29
+ variant: "inverted",
30
+ openOn: "longPress",
31
+ } as const satisfies {
32
+ placement: PopoverPlacement;
33
+ align: PopoverAlign;
34
+ offset: number;
35
+ alignOffset: number;
36
+ width: PopoverWidth;
37
+ variant: TooltipVariant;
38
+ openOn: TooltipOpenOn;
39
+ };
40
+
41
+ /** How long a tooltip stays after its entrance settles, in ms. */
42
+ export const TOOLTIP_DURATION = 1500;
43
+
44
+ /** How far the panel travels on its way in, toward its resolved side. No scale — a label just appears. */
45
+ export const TOOLTIP_ENTER_DISTANCE = 4;
46
+
47
+ /**
48
+ * How long the tooltip stays, in ms — `0` for until it is dismissed.
49
+ *
50
+ * A negative or non-finite value is a mistake, not a request to vanish at
51
+ * once, so it falls back to the default. With a screen reader on it never
52
+ * times out: whoever opened it — a press tooltip, for someone using zoom
53
+ * alongside VoiceOver — reads at their own pace, and dismisses it with the
54
+ * trigger or a tap elsewhere.
55
+ */
56
+ export function resolveTooltipDuration(
57
+ duration: number | undefined,
58
+ { isScreenReaderEnabled }: { isScreenReaderEnabled: boolean }
59
+ ): number {
60
+ if (isScreenReaderEnabled) return 0;
61
+ if (duration === undefined || !Number.isFinite(duration) || duration < 0) return TOOLTIP_DURATION;
62
+ return duration;
63
+ }
64
+
65
+ /**
66
+ * Whether a gesture on the trigger toggles the tooltip.
67
+ *
68
+ * Only the gesture `openOn` names does. With a screen reader on, a long press
69
+ * never opens it: the tooltip's words already reached the trigger as its label
70
+ * or hint, and VoiceOver's double-tap-and-hold is an action, not a request to
71
+ * see a label that is hidden from it anyway. A press tooltip still opens, for a
72
+ * partially sighted user who reads the screen as well as hearing it.
73
+ */
74
+ export function shouldTooltipActivate({
75
+ openOn,
76
+ gesture,
77
+ isScreenReaderEnabled,
78
+ }: {
79
+ openOn: TooltipOpenOn;
80
+ gesture: TooltipGesture;
81
+ isScreenReaderEnabled: boolean;
82
+ }): boolean {
83
+ if (gesture !== openOn) return false;
84
+ return !(isScreenReaderEnabled && gesture === "longPress");
85
+ }
86
+
87
+ /**
88
+ * The text a screen reader would name a control by when it has no
89
+ * `accessibilityLabel` — every string and number in its children, at any depth.
90
+ *
91
+ * `resolveTooltipAccessibility` needs it: a `<Button>Sync now</Button>` already
92
+ * has a name, and treating it as unnamed would replace "Sync now" with the
93
+ * tooltip's words instead of adding them as a hint.
94
+ */
95
+ export function readableTextOf(node: ReactNode): string | undefined {
96
+ const parts: string[] = [];
97
+ const visit = (child: ReactNode): void => {
98
+ if (typeof child === "string" || typeof child === "number") {
99
+ parts.push(String(child));
100
+ return;
101
+ }
102
+ if (isValidElement<{ children?: ReactNode }>(child)) Children.forEach(child.props.children, visit);
103
+ };
104
+ Children.forEach(node, visit);
105
+ const text = parts.join("").trim();
106
+ return text === "" ? undefined : text;
107
+ }
108
+
109
+ export type TooltipAccessibility = { accessibilityLabel?: string; accessibilityHint?: string };
110
+
111
+ /**
112
+ * What the trigger tells assistive technology, so VoiceOver says the
113
+ * tooltip's words without anything opening.
114
+ *
115
+ * A trigger with no label of its own — an icon button — is named by the
116
+ * tooltip. One that has a label keeps it, and the tooltip becomes its hint;
117
+ * unless the two say the same thing, which would only be read twice.
118
+ */
119
+ export function resolveTooltipAccessibility({
120
+ label,
121
+ triggerLabel,
122
+ }: {
123
+ label?: string;
124
+ triggerLabel?: string;
125
+ }): TooltipAccessibility {
126
+ if (label === undefined || label === "") return {};
127
+ if (triggerLabel === undefined || triggerLabel === "") return { accessibilityLabel: label };
128
+ if (triggerLabel === label) return {};
129
+ return { accessibilityHint: label };
130
+ }
131
+
132
+ /**
133
+ * Styling for every part of a tooltip.
134
+ *
135
+ * `inverted` is the default: the foreground colour as the fill and the
136
+ * background colour as the ink, so it reads over anything in either theme,
137
+ * with a small corner and padding sized for one caption-sized line. `surface`
138
+ * is the popover card — fill, hairline and card corner — with room for a title
139
+ * over a description.
140
+ *
141
+ * The arrow is drawn by Popover's `AnchoredArrow` with its own paint stripped,
142
+ * so the slot here carries all of it: the inverted chip has no border, so its
143
+ * arrow is fill alone; the surface arrow wears the border on its two outer
144
+ * edges, the ones that show past the panel.
145
+ */
146
+ export const tooltipVariants = tv({
147
+ slots: {
148
+ content: "",
149
+ arrow: "",
150
+ text: "",
151
+ title: "",
152
+ description: "",
153
+ },
154
+ variants: {
155
+ variant: {
156
+ inverted: {
157
+ content: "gap-0.5 rounded-md bg-foreground px-2 py-1",
158
+ arrow: "bg-foreground",
159
+ text: "text-background",
160
+ title: "text-background",
161
+ description: "text-background/80",
162
+ },
163
+ surface: {
164
+ content: "gap-1 rounded-lg border border-border bg-popover px-3 py-2",
165
+ arrow: "border-b border-r border-border bg-popover",
166
+ text: "text-popover-foreground",
167
+ title: "text-popover-foreground",
168
+ description: "text-muted-foreground",
169
+ },
170
+ },
171
+ },
172
+ defaultVariants: {
173
+ variant: "inverted",
174
+ },
175
+ });
176
+
177
+ export type TooltipVariantProps = VariantProps<typeof tooltipVariants>;
@@ -0,0 +1,31 @@
1
+ import { createContext, type ReactElement, type ReactNode, useContext } from "react";
2
+ import { useReducedMotion } from "react-native-reanimated";
3
+ import { calmMotion } from "../lib/calm-motion";
4
+
5
+ const CalmMotionContext = createContext(false);
6
+
7
+ export type CalmMotionProviderProps = {
8
+ /** Hold every decorative loop beneath this still, whatever the OS setting. */
9
+ isMotionCalm: boolean;
10
+ children: ReactNode;
11
+ };
12
+
13
+ /**
14
+ * Asks every decorative loop beneath it to hold still — for an E2E build, whose test runner waits
15
+ * for the screen to stop moving before each gesture. `DelacourProvider` mounts it from its own
16
+ * `isMotionCalm`; mount it by hand only in an app that composes its providers itself.
17
+ *
18
+ * Optional: without one, {@link useCalmMotion} follows the OS reduce-motion setting alone.
19
+ */
20
+ export function CalmMotionProvider({ isMotionCalm, children }: CalmMotionProviderProps): ReactElement {
21
+ return <CalmMotionContext.Provider value={isMotionCalm}>{children}</CalmMotionContext.Provider>;
22
+ }
23
+
24
+ /**
25
+ * Whether decorative motion should hold still, for this device and this app: the one place a
26
+ * component asks whether to loop. See `calmMotion` for what counts as decorative.
27
+ */
28
+ export function useCalmMotion(): boolean {
29
+ return calmMotion({ isMotionCalm: useContext(CalmMotionContext), isReduceMotion: useReducedMotion() });
30
+ }
31
+ CalmMotionProvider.displayName = "DelacourUI.CalmMotionProvider";
@@ -0,0 +1,17 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { calmMotion } from "./calm-motion";
3
+
4
+ describe("calmMotion", () => {
5
+ test("moves when neither the OS nor the app asks for stillness", () => {
6
+ expect(calmMotion({ isReduceMotion: false, isMotionCalm: false })).toBe(false);
7
+ });
8
+
9
+ test("holds still under the OS reduce-motion setting", () => {
10
+ expect(calmMotion({ isReduceMotion: true, isMotionCalm: false })).toBe(true);
11
+ });
12
+
13
+ test("holds still when the app asks, whatever the OS says", () => {
14
+ expect(calmMotion({ isReduceMotion: false, isMotionCalm: true })).toBe(true);
15
+ expect(calmMotion({ isReduceMotion: true, isMotionCalm: true })).toBe(true);
16
+ });
17
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Whether decorative motion holds still: under the OS reduce-motion setting, or whenever the app
3
+ * asks — `DelacourProvider`'s `isMotionCalm`.
4
+ *
5
+ * The app's half is for automation, not accessibility. A UI test runner that waits for the screen
6
+ * to stop moving before every gesture (Argent waits up to 3 s, Detox and Maestro idle-wait the
7
+ * same way) pays the whole wait on any screen holding a loop that never ends — a shimmering
8
+ * skeleton turns every tap there into a timeout. An E2E build passes `isMotionCalm` and the loops
9
+ * hold still.
10
+ *
11
+ * Only motion that is decoration asks. Motion that *is* the behaviour — a spinner saying work is
12
+ * in flight, a progress bar a reader reads — keeps moving: stilling it would change what the
13
+ * screen says.
14
+ */
15
+ export function calmMotion({
16
+ isReduceMotion,
17
+ isMotionCalm,
18
+ }: {
19
+ isReduceMotion: boolean;
20
+ isMotionCalm: boolean;
21
+ }): boolean {
22
+ return isReduceMotion || isMotionCalm;
23
+ }