@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,56 @@
1
+ import type { ReactElement } from "react";
2
+ import { Slot } from "../../lib/slot";
3
+ import { Pressable, type PressableProps } from "../pressable";
4
+ import { usePopoverContext } from "./popover.context";
5
+
6
+ export type PopoverTriggerProps = PressableProps;
7
+
8
+ /**
9
+ * The control that opens the popover, and — unless a `Popover.Anchor` is
10
+ * written — the view the panel is anchored to.
11
+ *
12
+ * On its own it is this library's `Pressable`. **`asChild` donates the press**
13
+ * rather than wrapping the child, for `BottomSheet.Trigger`'s reason: two tap
14
+ * gestures in an ancestor/descendant pair give the touch to the descendant, so
15
+ * a `Button` inside a pressable trigger would win it and the popover would
16
+ * never open. The toggle is handed to the child's own `onPress`, chained ahead
17
+ * of one the child already has, and the measuring ref is composed onto the
18
+ * child's own — so the child has to be something built on `Pressable`.
19
+ *
20
+ * Tells assistive technology whether the panel is expanded.
21
+ *
22
+ * @example
23
+ * <Popover.Trigger asChild>
24
+ * <Button variant="secondary">Rename</Button>
25
+ * </Popover.Trigger>
26
+ */
27
+ export function PopoverTrigger({
28
+ asChild = false,
29
+ accessibilityState,
30
+ children,
31
+ onPress,
32
+ ...props
33
+ }: PopoverTriggerProps): ReactElement {
34
+ const { isOpen, setOpen, triggerRef } = usePopoverContext();
35
+
36
+ const toggle = () => {
37
+ setOpen(!isOpen);
38
+ onPress?.();
39
+ };
40
+ const state = { ...accessibilityState, expanded: isOpen };
41
+
42
+ if (asChild) {
43
+ return (
44
+ <Slot {...props} accessibilityState={state} onPress={toggle} ref={triggerRef}>
45
+ {children}
46
+ </Slot>
47
+ );
48
+ }
49
+
50
+ return (
51
+ <Pressable {...props} accessibilityState={state} onPress={toggle} ref={triggerRef}>
52
+ {children}
53
+ </Pressable>
54
+ );
55
+ }
56
+ PopoverTrigger.displayName = "DelacourUI.Popover.Trigger";
@@ -0,0 +1,71 @@
1
+ import { createContext, type RefObject, useContext } from "react";
2
+ import type { AnchoredSize, AnchorRect, PopoverPlacement } from "./popover.position";
3
+ import type { MeasurableNode } from "./use-anchor-measure";
4
+
5
+ /** What `Popover` shares with its parts. */
6
+ export type PopoverContextValue = {
7
+ isOpen: boolean;
8
+ setOpen: (isOpen: boolean) => void;
9
+ close: () => void;
10
+ isDismissible: boolean;
11
+ /** The side the panel ended up on, after any flip — the preferred side until it is placed. */
12
+ placement: PopoverPlacement;
13
+ /** `Popover.Content` reports where it was placed. */
14
+ onPlaced: (placement: PopoverPlacement) => void;
15
+ /** The anchor's frame in window coordinates, `null` until it is measured on open. */
16
+ anchorRect: AnchorRect | null;
17
+ /** The callback ref `Popover.Trigger` measures through. */
18
+ triggerRef: (node: MeasurableNode | null) => void;
19
+ /** The trigger's node — where accessibility focus returns on close. */
20
+ triggerNode: RefObject<MeasurableNode | null>;
21
+ /** The callback ref `Popover.Anchor` measures through. */
22
+ anchorRef: (node: MeasurableNode | null) => void;
23
+ /** `Popover.Anchor` registers while mounted, which takes the measuring from the trigger. */
24
+ registerAnchor: () => () => void;
25
+ };
26
+
27
+ export const PopoverContext = createContext<PopoverContextValue | null>(null);
28
+ PopoverContext.displayName = "DelacourUI.Popover.Context";
29
+
30
+ /**
31
+ * The popover a part sits in — open state, the resolved placement and the
32
+ * close. Throws outside a `<Popover>`.
33
+ *
34
+ * @example
35
+ * function Done() {
36
+ * const { close } = usePopover();
37
+ * return <Button onPress={close}>Done</Button>;
38
+ * }
39
+ */
40
+ export function usePopover(): Pick<PopoverContextValue, "isOpen" | "setOpen" | "close" | "placement"> {
41
+ const context = useContext(PopoverContext);
42
+ if (context === null) throw new Error("usePopover must be used inside a <Popover>.");
43
+ return context;
44
+ }
45
+
46
+ /** The full context, for the parts. Throws outside a `<Popover>`. */
47
+ export function usePopoverContext(): PopoverContextValue {
48
+ const context = useContext(PopoverContext);
49
+ if (context === null) throw new Error("Popover parts must be used inside a <Popover>.");
50
+ return context;
51
+ }
52
+
53
+ /** What `Popover.Content` shares with the parts drawn inside the panel. */
54
+ export type PopoverContentContextValue = {
55
+ placement: PopoverPlacement;
56
+ arrowOffset: number;
57
+ size: AnchoredSize;
58
+ isUnstyled: boolean;
59
+ /** The title's `nativeID`, which the panel is labelled by. */
60
+ titleId: string;
61
+ /** The callback ref `Popover.Title` hands its node to — where accessibility focus lands on open. */
62
+ titleRef: (node: MeasurableNode | null) => void;
63
+ };
64
+
65
+ export const PopoverContentContext = createContext<PopoverContentContextValue | null>(null);
66
+ PopoverContentContext.displayName = "DelacourUI.Popover.ContentContext";
67
+
68
+ /** The panel's placement, or `null` outside `Popover.Content`. */
69
+ export function useOptionalPopoverContent(): PopoverContentContextValue | null {
70
+ return useContext(PopoverContentContext);
71
+ }
@@ -0,0 +1,354 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ type AnchoredInput,
4
+ type PopoverPlacement,
5
+ resolveAnchoredPosition,
6
+ resolveArrowFrame,
7
+ resolveEnterTranslate,
8
+ resolvePopoverWidth,
9
+ resolveTransformOrigin,
10
+ } from "./popover.position";
11
+
12
+ /** A 400 × 800 window with a notch and a home indicator. */
13
+ const BOUNDS = { width: 400, height: 800, insets: { top: 50, right: 0, bottom: 30, left: 0 } };
14
+ const PAD = 8;
15
+
16
+ /** A 100 × 40 trigger in the middle of the screen. */
17
+ const MIDDLE = { x: 150, y: 380, width: 100, height: 40 };
18
+
19
+ function input(overrides: Partial<AnchoredInput> = {}): AnchoredInput {
20
+ return {
21
+ anchor: MIDDLE,
22
+ content: { width: 200, height: 100 },
23
+ bounds: BOUNDS,
24
+ placement: "bottom",
25
+ align: "center",
26
+ offset: 8,
27
+ alignOffset: 0,
28
+ collisionPadding: PAD,
29
+ arrowInset: 16,
30
+ isRTL: false,
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ describe("resolveAnchoredPosition — placements", () => {
36
+ test("bottom sits below the anchor by the offset, centred", () => {
37
+ const position = resolveAnchoredPosition(input());
38
+ expect(position.placement).toBe("bottom");
39
+ expect(position.y).toBe(380 + 40 + 8);
40
+ expect(position.x).toBe(150 + 50 - 100);
41
+ });
42
+
43
+ test("top sits above the anchor by the offset", () => {
44
+ const position = resolveAnchoredPosition(input({ placement: "top" }));
45
+ expect(position.placement).toBe("top");
46
+ expect(position.y).toBe(380 - 8 - 100);
47
+ expect(position.x).toBe(100);
48
+ });
49
+
50
+ test("right sits beside the anchor, centred vertically", () => {
51
+ const position = resolveAnchoredPosition(
52
+ input({
53
+ placement: "right",
54
+ anchor: { x: 20, y: 380, width: 40, height: 40 },
55
+ content: { width: 120, height: 60 },
56
+ })
57
+ );
58
+ expect(position.placement).toBe("right");
59
+ expect(position.x).toBe(20 + 40 + 8);
60
+ expect(position.y).toBe(380 + 20 - 30);
61
+ });
62
+
63
+ test("left sits before the anchor", () => {
64
+ const position = resolveAnchoredPosition(
65
+ input({
66
+ placement: "left",
67
+ anchor: { x: 340, y: 380, width: 40, height: 40 },
68
+ content: { width: 120, height: 60 },
69
+ })
70
+ );
71
+ expect(position.placement).toBe("left");
72
+ expect(position.x).toBe(340 - 8 - 120);
73
+ expect(position.y).toBe(370);
74
+ });
75
+ });
76
+
77
+ describe("resolveAnchoredPosition — align", () => {
78
+ test("start lines the panel's leading edge up with the anchor's", () => {
79
+ expect(resolveAnchoredPosition(input({ align: "start" })).x).toBe(150);
80
+ });
81
+
82
+ test("end lines the trailing edges up", () => {
83
+ expect(resolveAnchoredPosition(input({ align: "end" })).x).toBe(250 - 200);
84
+ });
85
+
86
+ test("center centres on the anchor", () => {
87
+ expect(resolveAnchoredPosition(input({ align: "center" })).x).toBe(100);
88
+ });
89
+
90
+ test("on a side placement start is the top edge and end the bottom", () => {
91
+ const side = { placement: "right" as const, anchor: { x: 20, y: 380, width: 40, height: 40 } };
92
+ expect(resolveAnchoredPosition(input({ ...side, align: "start" })).y).toBe(380);
93
+ expect(resolveAnchoredPosition(input({ ...side, align: "end" })).y).toBe(420 - 100);
94
+ });
95
+
96
+ test("start and end mirror under RTL on top and bottom", () => {
97
+ expect(resolveAnchoredPosition(input({ align: "start", isRTL: true })).x).toBe(250 - 200);
98
+ expect(resolveAnchoredPosition(input({ align: "end", isRTL: true })).x).toBe(150);
99
+ });
100
+
101
+ test("RTL does not mirror a side placement's align", () => {
102
+ const side = { placement: "right" as const, anchor: { x: 20, y: 380, width: 40, height: 40 }, isRTL: true };
103
+ expect(resolveAnchoredPosition(input({ ...side, align: "start" })).y).toBe(380);
104
+ });
105
+
106
+ test("alignOffset nudges inward from the aligned edge", () => {
107
+ expect(resolveAnchoredPosition(input({ align: "start", alignOffset: 10 })).x).toBe(160);
108
+ expect(resolveAnchoredPosition(input({ align: "end", alignOffset: 10 })).x).toBe(40);
109
+ expect(resolveAnchoredPosition(input({ align: "center", alignOffset: 10 })).x).toBe(110);
110
+ });
111
+
112
+ test("alignOffset follows the logical direction under RTL", () => {
113
+ expect(resolveAnchoredPosition(input({ align: "start", alignOffset: 10, isRTL: true })).x).toBe(40);
114
+ });
115
+ });
116
+
117
+ describe("resolveAnchoredPosition — flip", () => {
118
+ test("flips above when there is no room below and more above", () => {
119
+ const anchor = { x: 150, y: 700, width: 100, height: 40 };
120
+ const position = resolveAnchoredPosition(input({ anchor }));
121
+ expect(position.placement).toBe("top");
122
+ expect(position.y).toBe(700 - 8 - 100);
123
+ });
124
+
125
+ test("flips below when there is no room above", () => {
126
+ const anchor = { x: 150, y: 60, width: 100, height: 40 };
127
+ expect(resolveAnchoredPosition(input({ anchor, placement: "top" })).placement).toBe("bottom");
128
+ });
129
+
130
+ test("flips a side placement across the anchor", () => {
131
+ const anchor = { x: 340, y: 380, width: 40, height: 40 };
132
+ expect(resolveAnchoredPosition(input({ anchor, placement: "right" })).placement).toBe("left");
133
+ });
134
+
135
+ test("does not flip when the opposite side is worse", () => {
136
+ const anchor = { x: 150, y: 120, width: 100, height: 40 };
137
+ const tall = { width: 200, height: 700 };
138
+ expect(resolveAnchoredPosition(input({ anchor, content: tall })).placement).toBe("bottom");
139
+ });
140
+
141
+ test("does not flip when the preferred side has room", () => {
142
+ const anchor = { x: 150, y: 600, width: 100, height: 40 };
143
+ expect(resolveAnchoredPosition(input({ anchor })).placement).toBe("bottom");
144
+ });
145
+ });
146
+
147
+ describe("resolveAnchoredPosition — shift", () => {
148
+ test("shifts right to stay off the left edge", () => {
149
+ const anchor = { x: 0, y: 380, width: 40, height: 40 };
150
+ expect(resolveAnchoredPosition(input({ anchor })).x).toBe(PAD);
151
+ });
152
+
153
+ test("shifts left to stay off the right edge", () => {
154
+ const anchor = { x: 360, y: 380, width: 40, height: 40 };
155
+ expect(resolveAnchoredPosition(input({ anchor })).x).toBe(400 - PAD - 200);
156
+ });
157
+
158
+ test("respects horizontal safe-area insets", () => {
159
+ const bounds = { ...BOUNDS, insets: { ...BOUNDS.insets, left: 44 } };
160
+ const anchor = { x: 0, y: 380, width: 40, height: 40 };
161
+ expect(resolveAnchoredPosition(input({ anchor, bounds })).x).toBe(44 + PAD);
162
+ });
163
+
164
+ test("shifts a side placement down off the top edge, into the safe area", () => {
165
+ const anchor = { x: 20, y: 52, width: 40, height: 40 };
166
+ const position = resolveAnchoredPosition(input({ anchor, placement: "right" }));
167
+ expect(position.y).toBe(50 + PAD);
168
+ });
169
+
170
+ test("shifts a side placement up off the bottom edge", () => {
171
+ const anchor = { x: 20, y: 740, width: 40, height: 40 };
172
+ const position = resolveAnchoredPosition(input({ anchor, placement: "right" }));
173
+ expect(position.y).toBe(800 - 30 - PAD - 100);
174
+ });
175
+
176
+ test("a panel wider than the safe span pins to its leading edge", () => {
177
+ const position = resolveAnchoredPosition(input({ content: { width: 500, height: 100 } }));
178
+ expect(position.x).toBe(PAD);
179
+ });
180
+
181
+ test("never leaves the screen in any placement or corner", () => {
182
+ const corners = [
183
+ { x: 0, y: 50, width: 44, height: 44 },
184
+ { x: 356, y: 50, width: 44, height: 44 },
185
+ { x: 0, y: 726, width: 44, height: 44 },
186
+ { x: 356, y: 726, width: 44, height: 44 },
187
+ ];
188
+ const placements: PopoverPlacement[] = ["top", "bottom", "left", "right"];
189
+ for (const anchor of corners) {
190
+ for (const placement of placements) {
191
+ const position = resolveAnchoredPosition(input({ anchor, placement, content: { width: 220, height: 160 } }));
192
+ const height = Math.min(160, position.maxHeight);
193
+ expect(position.x).toBeGreaterThanOrEqual(PAD);
194
+ expect(position.x + 220).toBeLessThanOrEqual(400 - PAD);
195
+ expect(position.y).toBeGreaterThanOrEqual(50 + PAD);
196
+ expect(position.y + height).toBeLessThanOrEqual(800 - 30 - PAD);
197
+ }
198
+ }
199
+ });
200
+ });
201
+
202
+ describe("resolveAnchoredPosition — maxHeight", () => {
203
+ test("is the room on the resolved side when none is asked for", () => {
204
+ const position = resolveAnchoredPosition(input());
205
+ expect(position.maxHeight).toBe(800 - 30 - PAD - (380 + 40 + 8));
206
+ });
207
+
208
+ test("is the asked-for cap when it fits", () => {
209
+ expect(resolveAnchoredPosition(input({ maxHeight: 120 })).maxHeight).toBe(120);
210
+ });
211
+
212
+ test("clamps the asked-for cap to the room", () => {
213
+ const anchor = { x: 150, y: 600, width: 100, height: 40 };
214
+ const position = resolveAnchoredPosition(input({ anchor, maxHeight: 500, content: { width: 200, height: 80 } }));
215
+ expect(position.placement).toBe("bottom");
216
+ expect(position.maxHeight).toBe(800 - 30 - PAD - 648);
217
+ });
218
+
219
+ test("a too-tall panel on the side with more room is clamped to it and stays on screen", () => {
220
+ const anchor = { x: 150, y: 120, width: 100, height: 40 };
221
+ const position = resolveAnchoredPosition(input({ anchor, content: { width: 200, height: 900 } }));
222
+ expect(position.maxHeight).toBe(800 - 30 - PAD - 168);
223
+ expect(position.y).toBe(168);
224
+ });
225
+
226
+ test("a top panel taller than its room is clamped and still meets the anchor", () => {
227
+ const anchor = { x: 150, y: 700, width: 100, height: 40 };
228
+ const position = resolveAnchoredPosition(input({ anchor, content: { width: 200, height: 900 } }));
229
+ expect(position.placement).toBe("top");
230
+ expect(position.maxHeight).toBe(700 - 8 - (50 + PAD));
231
+ expect(position.y).toBe(50 + PAD);
232
+ });
233
+
234
+ test("a side placement is capped by the safe height", () => {
235
+ const position = resolveAnchoredPosition(
236
+ input({ placement: "right", anchor: { x: 20, y: 380, width: 40, height: 40 } })
237
+ );
238
+ expect(position.maxHeight).toBe(800 - 50 - 30 - 2 * PAD);
239
+ });
240
+
241
+ test("is never negative", () => {
242
+ const anchor = { x: 150, y: 900, width: 100, height: 40 };
243
+ expect(resolveAnchoredPosition(input({ anchor })).maxHeight).toBeGreaterThanOrEqual(0);
244
+ });
245
+ });
246
+
247
+ describe("resolveAnchoredPosition — arrow", () => {
248
+ test("points at the anchor's centre", () => {
249
+ const position = resolveAnchoredPosition(input({ align: "start" }));
250
+ expect(position.arrowOffset).toBe(200 - 150);
251
+ });
252
+
253
+ test("tracks the anchor after a shift", () => {
254
+ const anchor = { x: 330, y: 380, width: 40, height: 40 };
255
+ const position = resolveAnchoredPosition(input({ anchor }));
256
+ expect(position.x).toBe(400 - PAD - 200);
257
+ expect(position.x + position.arrowOffset).toBe(350);
258
+ });
259
+
260
+ test("is clamped clear of the leading corner", () => {
261
+ const anchor = { x: 0, y: 380, width: 10, height: 40 };
262
+ expect(resolveAnchoredPosition(input({ anchor })).arrowOffset).toBe(16);
263
+ });
264
+
265
+ test("is clamped clear of the trailing corner", () => {
266
+ const anchor = { x: 390, y: 380, width: 10, height: 40 };
267
+ expect(resolveAnchoredPosition(input({ anchor })).arrowOffset).toBe(200 - 16);
268
+ });
269
+
270
+ test("runs along the vertical edge on a side placement", () => {
271
+ const anchor = { x: 20, y: 80, width: 40, height: 40 };
272
+ const position = resolveAnchoredPosition(input({ anchor, placement: "right" }));
273
+ expect(position.y).toBe(50 + PAD);
274
+ expect(position.y + position.arrowOffset).toBe(100);
275
+ });
276
+
277
+ test("sits in the middle of a panel too small to clear both corners", () => {
278
+ const position = resolveAnchoredPosition(input({ content: { width: 20, height: 20 } }));
279
+ expect(position.arrowOffset).toBe(10);
280
+ });
281
+ });
282
+
283
+ describe("resolvePopoverWidth", () => {
284
+ const safe = { anchorWidth: 120, bounds: BOUNDS, collisionPadding: PAD };
285
+
286
+ test('"trigger" is the anchor width', () => {
287
+ expect(resolvePopoverWidth({ ...safe, width: "trigger" })).toEqual({ width: 120, maxWidth: 384 });
288
+ });
289
+
290
+ test('"content-fit" leaves the width to the content', () => {
291
+ expect(resolvePopoverWidth({ ...safe, width: "content-fit" })).toEqual({ width: undefined, maxWidth: 384 });
292
+ });
293
+
294
+ test('"full" is the safe span', () => {
295
+ expect(resolvePopoverWidth({ ...safe, width: "full" })).toEqual({ width: 384, maxWidth: 384 });
296
+ });
297
+
298
+ test("a number is taken as it is, capped to the safe span", () => {
299
+ expect(resolvePopoverWidth({ ...safe, width: 240 })).toEqual({ width: 240, maxWidth: 384 });
300
+ expect(resolvePopoverWidth({ ...safe, width: 900 })).toEqual({ width: 384, maxWidth: 384 });
301
+ });
302
+
303
+ test("minWidth raises a narrow trigger", () => {
304
+ expect(resolvePopoverWidth({ ...safe, width: "trigger", minWidth: 260 })).toEqual({ width: 260, maxWidth: 384 });
305
+ });
306
+
307
+ test("minWidth is a floor for a content-fit panel", () => {
308
+ expect(resolvePopoverWidth({ ...safe, width: "content-fit", minWidth: 200 })).toEqual({
309
+ width: undefined,
310
+ minWidth: 200,
311
+ maxWidth: 384,
312
+ });
313
+ });
314
+
315
+ test("the safe span subtracts horizontal insets", () => {
316
+ const bounds = { ...BOUNDS, insets: { ...BOUNDS.insets, left: 44, right: 44 } };
317
+ expect(resolvePopoverWidth({ ...safe, bounds, width: "full" }).width).toBe(400 - 88 - 2 * PAD);
318
+ });
319
+ });
320
+
321
+ describe("motion geometry", () => {
322
+ test("a panel below enters downward from the trigger", () => {
323
+ expect(resolveEnterTranslate("bottom", 6)).toEqual({ x: 0, y: -6 });
324
+ expect(resolveEnterTranslate("top", 6)).toEqual({ x: 0, y: 6 });
325
+ expect(resolveEnterTranslate("right", 6)).toEqual({ x: -6, y: 0 });
326
+ expect(resolveEnterTranslate("left", 6)).toEqual({ x: 6, y: 0 });
327
+ });
328
+
329
+ test("the transform origin is the arrow, on the edge facing the anchor", () => {
330
+ const size = { width: 200, height: 100 };
331
+ expect(resolveTransformOrigin("bottom", 40, size)).toEqual({ x: 40, y: 0 });
332
+ expect(resolveTransformOrigin("top", 40, size)).toEqual({ x: 40, y: 100 });
333
+ expect(resolveTransformOrigin("right", 30, size)).toEqual({ x: 0, y: 30 });
334
+ expect(resolveTransformOrigin("left", 30, size)).toEqual({ x: 200, y: 30 });
335
+ });
336
+ });
337
+
338
+ describe("resolveArrowFrame", () => {
339
+ const size = { width: 200, height: 100 };
340
+
341
+ test("sits half over the edge facing the anchor, centred on the offset", () => {
342
+ expect(resolveArrowFrame("bottom", 40, size, 10)).toEqual({ left: 35, top: -5, rotate: 225 });
343
+ expect(resolveArrowFrame("top", 40, size, 10)).toEqual({ left: 35, top: 95, rotate: 45 });
344
+ expect(resolveArrowFrame("right", 30, size, 10)).toEqual({ left: -5, top: 25, rotate: 135 });
345
+ expect(resolveArrowFrame("left", 30, size, 10)).toEqual({ left: 195, top: 25, rotate: 315 });
346
+ });
347
+
348
+ test("turns the bordered corner toward the anchor in every placement", () => {
349
+ const pointing = { bottom: 225, top: 45, right: 135, left: 315 } as const;
350
+ for (const placement of ["top", "bottom", "left", "right"] as const) {
351
+ expect(resolveArrowFrame(placement, 40, size, 10).rotate).toBe(pointing[placement]);
352
+ }
353
+ });
354
+ });