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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. package/README.md +10 -6
  2. package/package.json +48 -4
  3. package/src/components/accordion/AGENTS.md +21 -9
  4. package/src/components/accordion/accordion-content.tsx +7 -4
  5. package/src/components/accordion/accordion-item.tsx +15 -13
  6. package/src/components/accordion/accordion.context.tsx +11 -5
  7. package/src/components/accordion/accordion.variants.test.ts +64 -3
  8. package/src/components/accordion/accordion.variants.ts +33 -9
  9. package/src/components/alert/AGENTS.md +102 -0
  10. package/src/components/alert/alert-action.tsx +17 -0
  11. package/src/components/alert/alert-close-button.tsx +50 -0
  12. package/src/components/alert/alert-content.tsx +15 -0
  13. package/src/components/alert/alert-description.tsx +16 -0
  14. package/src/components/alert/alert-glyphs.ts +25 -0
  15. package/src/components/alert/alert-indicator.tsx +40 -0
  16. package/src/components/alert/alert-title.tsx +18 -0
  17. package/src/components/alert/alert.context.tsx +69 -0
  18. package/src/components/alert/alert.tsx +160 -0
  19. package/src/components/alert/alert.types.ts +10 -0
  20. package/src/components/alert/alert.variants.test.ts +218 -0
  21. package/src/components/alert/alert.variants.ts +180 -0
  22. package/src/components/alert/index.ts +23 -0
  23. package/src/components/avatar/AGENTS.md +106 -0
  24. package/src/components/avatar/avatar-badge.tsx +51 -0
  25. package/src/components/avatar/avatar-group.tsx +139 -0
  26. package/src/components/avatar/avatar.context.tsx +93 -0
  27. package/src/components/avatar/avatar.tsx +201 -0
  28. package/src/components/avatar/avatar.utils.test.ts +174 -0
  29. package/src/components/avatar/avatar.utils.ts +141 -0
  30. package/src/components/avatar/avatar.variants.test.ts +251 -0
  31. package/src/components/avatar/avatar.variants.ts +270 -0
  32. package/src/components/avatar/index.ts +39 -0
  33. package/src/components/badge/AGENTS.md +5 -0
  34. package/src/components/badge/badge.tsx +6 -1
  35. package/src/components/bottom-sheet/AGENTS.md +186 -231
  36. package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
  37. package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
  38. package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -208
  39. package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
  40. package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
  41. package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
  42. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -122
  43. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
  44. package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
  45. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
  46. package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
  47. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -62
  48. package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
  49. package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
  50. package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
  51. package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
  52. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
  53. package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
  54. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -87
  55. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
  56. package/src/components/bottom-sheet/index.ts +39 -17
  57. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
  58. package/src/components/button/AGENTS.md +45 -11
  59. package/src/components/button/button-group.tsx +44 -8
  60. package/src/components/button/button.context.tsx +10 -0
  61. package/src/components/button/button.tsx +2 -1
  62. package/src/components/button/button.variants.test.ts +154 -0
  63. package/src/components/button/button.variants.ts +71 -6
  64. package/src/components/button/index.ts +2 -0
  65. package/src/components/calendar/AGENTS.md +142 -0
  66. package/src/components/calendar/calendar-caption.tsx +116 -0
  67. package/src/components/calendar/calendar-day.tsx +103 -0
  68. package/src/components/calendar/calendar-grid.tsx +183 -0
  69. package/src/components/calendar/calendar-header.tsx +34 -0
  70. package/src/components/calendar/calendar-nav.tsx +51 -0
  71. package/src/components/calendar/calendar-picker.tsx +128 -0
  72. package/src/components/calendar/calendar-weekdays.tsx +48 -0
  73. package/src/components/calendar/calendar.context.tsx +108 -0
  74. package/src/components/calendar/calendar.date.test.ts +456 -0
  75. package/src/components/calendar/calendar.date.ts +584 -0
  76. package/src/components/calendar/calendar.tsx +389 -0
  77. package/src/components/calendar/calendar.types.ts +87 -0
  78. package/src/components/calendar/calendar.variants.test.ts +363 -0
  79. package/src/components/calendar/calendar.variants.ts +426 -0
  80. package/src/components/calendar/index.ts +96 -0
  81. package/src/components/card/AGENTS.md +88 -0
  82. package/src/components/card/card-action.tsx +16 -0
  83. package/src/components/card/card-content.tsx +12 -0
  84. package/src/components/card/card-description.tsx +12 -0
  85. package/src/components/card/card-footer.tsx +30 -0
  86. package/src/components/card/card-header.tsx +45 -0
  87. package/src/components/card/card-title.tsx +25 -0
  88. package/src/components/card/card.context.tsx +64 -0
  89. package/src/components/card/card.tsx +94 -0
  90. package/src/components/card/card.types.ts +13 -0
  91. package/src/components/card/card.variants.test.ts +240 -0
  92. package/src/components/card/card.variants.ts +177 -0
  93. package/src/components/card/index.ts +15 -0
  94. package/src/components/chart/AGENTS.md +8 -0
  95. package/src/components/chart/chart.tsx +8 -1
  96. package/src/components/chip/AGENTS.md +113 -0
  97. package/src/components/chip/chip-close-button.tsx +48 -0
  98. package/src/components/chip/chip-end-content.tsx +15 -0
  99. package/src/components/chip/chip-label.tsx +21 -0
  100. package/src/components/chip/chip-start-content.tsx +16 -0
  101. package/src/components/chip/chip.context.tsx +66 -0
  102. package/src/components/chip/chip.tsx +268 -0
  103. package/src/components/chip/chip.types.ts +10 -0
  104. package/src/components/chip/chip.variants.test.ts +335 -0
  105. package/src/components/chip/chip.variants.ts +321 -0
  106. package/src/components/chip/index.ts +24 -0
  107. package/src/components/collapsible/AGENTS.md +97 -0
  108. package/src/components/collapsible/collapsible-content.tsx +97 -0
  109. package/src/components/collapsible/collapsible-description.tsx +17 -0
  110. package/src/components/collapsible/collapsible-indicator.tsx +92 -0
  111. package/src/components/collapsible/collapsible-title.tsx +17 -0
  112. package/src/components/collapsible/collapsible-trigger.tsx +133 -0
  113. package/src/components/collapsible/collapsible.context.tsx +91 -0
  114. package/src/components/collapsible/collapsible.tsx +167 -0
  115. package/src/components/collapsible/collapsible.types.ts +9 -0
  116. package/src/components/collapsible/collapsible.variants.test.ts +338 -0
  117. package/src/components/collapsible/collapsible.variants.ts +228 -0
  118. package/src/components/collapsible/index.ts +32 -0
  119. package/src/components/context-menu/AGENTS.md +103 -0
  120. package/src/components/context-menu/context-menu-content.tsx +64 -0
  121. package/src/components/context-menu/context-menu-item.tsx +22 -0
  122. package/src/components/context-menu/context-menu-preview.tsx +60 -0
  123. package/src/components/context-menu/context-menu-trigger.tsx +160 -0
  124. package/src/components/context-menu/context-menu.context.tsx +91 -0
  125. package/src/components/context-menu/context-menu.tsx +90 -0
  126. package/src/components/context-menu/context-menu.variants.test.ts +266 -0
  127. package/src/components/context-menu/context-menu.variants.ts +147 -0
  128. package/src/components/context-menu/index.ts +18 -0
  129. package/src/components/dialog/AGENTS.md +97 -0
  130. package/src/components/dialog/dialog-body.tsx +21 -0
  131. package/src/components/dialog/dialog-close.tsx +82 -0
  132. package/src/components/dialog/dialog-content.tsx +153 -0
  133. package/src/components/dialog/dialog-description.tsx +23 -0
  134. package/src/components/dialog/dialog-footer.tsx +32 -0
  135. package/src/components/dialog/dialog-header.tsx +20 -0
  136. package/src/components/dialog/dialog-title.tsx +42 -0
  137. package/src/components/dialog/dialog-trigger.tsx +54 -0
  138. package/src/components/dialog/dialog.context.tsx +90 -0
  139. package/src/components/dialog/dialog.tsx +119 -0
  140. package/src/components/dialog/dialog.variants.test.ts +224 -0
  141. package/src/components/dialog/dialog.variants.ts +173 -0
  142. package/src/components/dialog/index.ts +30 -0
  143. package/src/components/drawer/AGENTS.md +123 -0
  144. package/src/components/drawer/drawer-body.tsx +46 -0
  145. package/src/components/drawer/drawer-close.tsx +76 -0
  146. package/src/components/drawer/drawer-content.tsx +186 -0
  147. package/src/components/drawer/drawer-description.tsx +23 -0
  148. package/src/components/drawer/drawer-footer.tsx +21 -0
  149. package/src/components/drawer/drawer-header.tsx +42 -0
  150. package/src/components/drawer/drawer-title.tsx +35 -0
  151. package/src/components/drawer/drawer-trigger.tsx +54 -0
  152. package/src/components/drawer/drawer.context.tsx +114 -0
  153. package/src/components/drawer/drawer.tsx +119 -0
  154. package/src/components/drawer/drawer.variants.test.ts +392 -0
  155. package/src/components/drawer/drawer.variants.ts +244 -0
  156. package/src/components/drawer/index.ts +41 -0
  157. package/src/components/drawer/use-drawer-pan.ts +108 -0
  158. package/src/components/empty-state/AGENTS.md +90 -0
  159. package/src/components/empty-state/empty-state-content.tsx +17 -0
  160. package/src/components/empty-state/empty-state-description.tsx +12 -0
  161. package/src/components/empty-state/empty-state-header.tsx +17 -0
  162. package/src/components/empty-state/empty-state-media.tsx +56 -0
  163. package/src/components/empty-state/empty-state-title.tsx +18 -0
  164. package/src/components/empty-state/empty-state.context.tsx +65 -0
  165. package/src/components/empty-state/empty-state.tsx +84 -0
  166. package/src/components/empty-state/empty-state.types.ts +14 -0
  167. package/src/components/empty-state/empty-state.variants.test.ts +245 -0
  168. package/src/components/empty-state/empty-state.variants.ts +132 -0
  169. package/src/components/empty-state/index.ts +20 -0
  170. package/src/components/fab/AGENTS.md +119 -0
  171. package/src/components/fab/fab-action.tsx +118 -0
  172. package/src/components/fab/fab-group.tsx +214 -0
  173. package/src/components/fab/fab-label.tsx +18 -0
  174. package/src/components/fab/fab.context.tsx +125 -0
  175. package/src/components/fab/fab.tsx +156 -0
  176. package/src/components/fab/fab.types.ts +17 -0
  177. package/src/components/fab/fab.variants.test.ts +274 -0
  178. package/src/components/fab/fab.variants.ts +234 -0
  179. package/src/components/fab/index.ts +37 -0
  180. package/src/components/feedback/AGENTS.md +95 -0
  181. package/src/components/feedback/feedback-action.tsx +16 -0
  182. package/src/components/feedback/feedback-cancel.tsx +34 -0
  183. package/src/components/feedback/feedback-close.tsx +21 -0
  184. package/src/components/feedback/feedback-content.tsx +24 -0
  185. package/src/components/feedback/feedback-field.tsx +70 -0
  186. package/src/components/feedback/feedback-footer.tsx +21 -0
  187. package/src/components/feedback/feedback-panel.tsx +54 -0
  188. package/src/components/feedback/feedback-submit.tsx +74 -0
  189. package/src/components/feedback/feedback-title.tsx +34 -0
  190. package/src/components/feedback/feedback-trigger.tsx +20 -0
  191. package/src/components/feedback/feedback.context.tsx +73 -0
  192. package/src/components/feedback/feedback.tsx +135 -0
  193. package/src/components/feedback/feedback.variants.test.ts +185 -0
  194. package/src/components/feedback/feedback.variants.ts +128 -0
  195. package/src/components/feedback/index.ts +24 -0
  196. package/src/components/field/AGENTS.md +19 -0
  197. package/src/components/field/field-label.tsx +19 -5
  198. package/src/components/field/field.context.tsx +17 -0
  199. package/src/components/field/field.tsx +15 -2
  200. package/src/components/field/field.variants.test.ts +24 -0
  201. package/src/components/field/field.variants.ts +20 -0
  202. package/src/components/field/index.ts +1 -0
  203. package/src/components/item/AGENTS.md +92 -0
  204. package/src/components/item/index.ts +25 -0
  205. package/src/components/item/item-actions.tsx +28 -0
  206. package/src/components/item/item-content.tsx +15 -0
  207. package/src/components/item/item-description.tsx +12 -0
  208. package/src/components/item/item-footer.tsx +13 -0
  209. package/src/components/item/item-group.tsx +32 -0
  210. package/src/components/item/item-header.tsx +13 -0
  211. package/src/components/item/item-media.tsx +37 -0
  212. package/src/components/item/item-separator.tsx +13 -0
  213. package/src/components/item/item-title.tsx +17 -0
  214. package/src/components/item/item.context.tsx +66 -0
  215. package/src/components/item/item.tsx +218 -0
  216. package/src/components/item/item.types.ts +12 -0
  217. package/src/components/item/item.variants.test.ts +263 -0
  218. package/src/components/item/item.variants.ts +200 -0
  219. package/src/components/kpi/AGENTS.md +125 -0
  220. package/src/components/kpi/index.ts +45 -0
  221. package/src/components/kpi/kpi-action.tsx +12 -0
  222. package/src/components/kpi/kpi-content.tsx +35 -0
  223. package/src/components/kpi/kpi-footer.tsx +52 -0
  224. package/src/components/kpi/kpi-group.tsx +70 -0
  225. package/src/components/kpi/kpi-header.tsx +25 -0
  226. package/src/components/kpi/kpi-icon.tsx +28 -0
  227. package/src/components/kpi/kpi-sparkline.tsx +145 -0
  228. package/src/components/kpi/kpi-stat.tsx +20 -0
  229. package/src/components/kpi/kpi-title.tsx +19 -0
  230. package/src/components/kpi/kpi-trend.tsx +105 -0
  231. package/src/components/kpi/kpi-value.tsx +32 -0
  232. package/src/components/kpi/kpi.context.tsx +111 -0
  233. package/src/components/kpi/kpi.tsx +154 -0
  234. package/src/components/kpi/kpi.types.ts +9 -0
  235. package/src/components/kpi/kpi.variants.test.ts +428 -0
  236. package/src/components/kpi/kpi.variants.ts +347 -0
  237. package/src/components/label/AGENTS.md +89 -0
  238. package/src/components/label/index.ts +10 -0
  239. package/src/components/label/label.tsx +97 -0
  240. package/src/components/label/label.variants.test.ts +176 -0
  241. package/src/components/label/label.variants.ts +123 -0
  242. package/src/components/menu/AGENTS.md +176 -0
  243. package/src/components/menu/index.ts +44 -0
  244. package/src/components/menu/menu-background.tsx +17 -0
  245. package/src/components/menu/menu-checkbox-item.tsx +57 -0
  246. package/src/components/menu/menu-content.tsx +303 -0
  247. package/src/components/menu/menu-item.tsx +162 -0
  248. package/src/components/menu/menu-label.tsx +14 -0
  249. package/src/components/menu/menu-radio-group.tsx +50 -0
  250. package/src/components/menu/menu-radio-item.tsx +57 -0
  251. package/src/components/menu/menu-separator.tsx +11 -0
  252. package/src/components/menu/menu-sub-content.tsx +72 -0
  253. package/src/components/menu/menu-sub-trigger.tsx +50 -0
  254. package/src/components/menu/menu-sub.tsx +75 -0
  255. package/src/components/menu/menu-trigger.tsx +57 -0
  256. package/src/components/menu/menu.context.tsx +214 -0
  257. package/src/components/menu/menu.tsx +88 -0
  258. package/src/components/menu/menu.types.ts +33 -0
  259. package/src/components/menu/menu.variants.test.ts +400 -0
  260. package/src/components/menu/menu.variants.ts +353 -0
  261. package/src/components/meter/AGENTS.md +135 -0
  262. package/src/components/meter/index.ts +43 -0
  263. package/src/components/meter/meter-fill.tsx +19 -0
  264. package/src/components/meter/meter-header.tsx +17 -0
  265. package/src/components/meter/meter-label.tsx +18 -0
  266. package/src/components/meter/meter-output.tsx +42 -0
  267. package/src/components/meter/meter-segments.tsx +83 -0
  268. package/src/components/meter/meter-track.tsx +16 -0
  269. package/src/components/meter/meter.context.tsx +62 -0
  270. package/src/components/meter/meter.tsx +286 -0
  271. package/src/components/meter/meter.types.ts +30 -0
  272. package/src/components/meter/meter.variants.test.ts +430 -0
  273. package/src/components/meter/meter.variants.ts +347 -0
  274. package/src/components/overlay/AGENTS.md +89 -0
  275. package/src/components/overlay/index.ts +27 -0
  276. package/src/components/overlay/overlay-portal.tsx +84 -0
  277. package/src/components/overlay/overlay-presence.test.ts +46 -0
  278. package/src/components/overlay/overlay-presence.ts +49 -0
  279. package/src/components/overlay/overlay-registry.test.ts +96 -0
  280. package/src/components/overlay/overlay-registry.ts +99 -0
  281. package/src/components/overlay/overlay-scrim.tsx +49 -0
  282. package/src/components/overlay/overlay.context.tsx +48 -0
  283. package/src/components/overlay/overlay.tsx +118 -0
  284. package/src/components/overlay/overlay.variants.test.ts +37 -0
  285. package/src/components/overlay/overlay.variants.ts +33 -0
  286. package/src/components/overlay/use-overlay-back-handler.ts +34 -0
  287. package/src/components/overlay/use-overlay-presence.ts +87 -0
  288. package/src/components/popover/AGENTS.md +138 -0
  289. package/src/components/popover/index.ts +48 -0
  290. package/src/components/popover/popover-anchor.tsx +49 -0
  291. package/src/components/popover/popover-arrow.tsx +89 -0
  292. package/src/components/popover/popover-close.tsx +73 -0
  293. package/src/components/popover/popover-content.tsx +235 -0
  294. package/src/components/popover/popover-description.tsx +18 -0
  295. package/src/components/popover/popover-title.tsx +33 -0
  296. package/src/components/popover/popover-trigger.tsx +56 -0
  297. package/src/components/popover/popover.context.tsx +71 -0
  298. package/src/components/popover/popover.position.test.ts +354 -0
  299. package/src/components/popover/popover.position.ts +286 -0
  300. package/src/components/popover/popover.tsx +123 -0
  301. package/src/components/popover/popover.variants.test.ts +135 -0
  302. package/src/components/popover/popover.variants.ts +85 -0
  303. package/src/components/popover/use-anchor-measure.ts +76 -0
  304. package/src/components/popover/use-anchored-content.ts +219 -0
  305. package/src/components/pressable/AGENTS.md +11 -1
  306. package/src/components/pressable/pressable.tsx +7 -2
  307. package/src/components/pressable/pressable.variants.test.ts +29 -0
  308. package/src/components/pressable/pressable.variants.ts +17 -0
  309. package/src/components/progress/AGENTS.md +120 -0
  310. package/src/components/progress/index.ts +40 -0
  311. package/src/components/progress/progress-fill.tsx +152 -0
  312. package/src/components/progress/progress-header.tsx +19 -0
  313. package/src/components/progress/progress-label.tsx +27 -0
  314. package/src/components/progress/progress-output.tsx +47 -0
  315. package/src/components/progress/progress-track.tsx +38 -0
  316. package/src/components/progress/progress.context.tsx +79 -0
  317. package/src/components/progress/progress.tsx +171 -0
  318. package/src/components/progress/progress.types.ts +23 -0
  319. package/src/components/progress/progress.variants.test.ts +335 -0
  320. package/src/components/progress/progress.variants.ts +284 -0
  321. package/src/components/progress-button/AGENTS.md +116 -0
  322. package/src/components/progress-button/index.ts +36 -0
  323. package/src/components/progress-button/progress-button-done.tsx +33 -0
  324. package/src/components/progress-button/progress-button-label.tsx +27 -0
  325. package/src/components/progress-button/progress-button.context.tsx +93 -0
  326. package/src/components/progress-button/progress-button.tsx +414 -0
  327. package/src/components/progress-button/progress-button.variants.test.ts +314 -0
  328. package/src/components/progress-button/progress-button.variants.ts +236 -0
  329. package/src/components/provider/AGENTS.md +38 -25
  330. package/src/components/provider/provider.tsx +38 -11
  331. package/src/components/radio/AGENTS.md +8 -10
  332. package/src/components/radio/radio.variants.test.ts +5 -6
  333. package/src/components/radio/radio.variants.ts +6 -7
  334. package/src/components/rating/AGENTS.md +143 -0
  335. package/src/components/rating/index.ts +43 -0
  336. package/src/components/rating/rating-output.tsx +39 -0
  337. package/src/components/rating/rating-star.tsx +70 -0
  338. package/src/components/rating/rating-stars.tsx +219 -0
  339. package/src/components/rating/rating.context.tsx +79 -0
  340. package/src/components/rating/rating.tsx +163 -0
  341. package/src/components/rating/rating.variants.test.ts +347 -0
  342. package/src/components/rating/rating.variants.ts +354 -0
  343. package/src/components/selection-mode/AGENTS.md +155 -0
  344. package/src/components/selection-mode/index.ts +43 -0
  345. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  346. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  347. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  348. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  349. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  350. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  351. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  352. package/src/components/selection-mode/selection-mode.tsx +207 -0
  353. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  354. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  355. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  356. package/src/components/skeleton/AGENTS.md +120 -0
  357. package/src/components/skeleton/index.ts +15 -0
  358. package/src/components/skeleton/skeleton-clock.ts +48 -0
  359. package/src/components/skeleton/skeleton-group.tsx +64 -0
  360. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  361. package/src/components/skeleton/skeleton.context.tsx +45 -0
  362. package/src/components/skeleton/skeleton.tsx +227 -0
  363. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  364. package/src/components/skeleton/skeleton.variants.ts +222 -0
  365. package/src/components/slide-button/AGENTS.md +122 -0
  366. package/src/components/slide-button/index.ts +43 -0
  367. package/src/components/slide-button/slide-button-label.tsx +70 -0
  368. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  369. package/src/components/slide-button/slide-button.context.tsx +76 -0
  370. package/src/components/slide-button/slide-button.tsx +457 -0
  371. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  372. package/src/components/slide-button/slide-button.variants.ts +324 -0
  373. package/src/components/slider/index.ts +2 -0
  374. package/src/components/slider/slider-thumb.tsx +33 -1
  375. package/src/components/slider/slider.variants.test.ts +52 -0
  376. package/src/components/slider/slider.variants.ts +56 -2
  377. package/src/components/stack-card/AGENTS.md +169 -0
  378. package/src/components/stack-card/index.ts +37 -0
  379. package/src/components/stack-card/stack-card-action.tsx +66 -0
  380. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  381. package/src/components/stack-card/stack-card-card.tsx +71 -0
  382. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  383. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  384. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  385. package/src/components/stack-card/stack-card.context.tsx +117 -0
  386. package/src/components/stack-card/stack-card.tsx +557 -0
  387. package/src/components/stack-card/stack-card.types.ts +26 -0
  388. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  389. package/src/components/stack-card/stack-card.variants.ts +360 -0
  390. package/src/components/steps/AGENTS.md +153 -0
  391. package/src/components/steps/index.ts +45 -0
  392. package/src/components/steps/steps-connector.tsx +31 -0
  393. package/src/components/steps/steps-description.tsx +26 -0
  394. package/src/components/steps/steps-indicator.tsx +106 -0
  395. package/src/components/steps/steps-item.tsx +225 -0
  396. package/src/components/steps/steps-panel.tsx +41 -0
  397. package/src/components/steps/steps-title.tsx +32 -0
  398. package/src/components/steps/steps-track.tsx +59 -0
  399. package/src/components/steps/steps.context.tsx +123 -0
  400. package/src/components/steps/steps.tsx +166 -0
  401. package/src/components/steps/steps.variants.test.ts +388 -0
  402. package/src/components/steps/steps.variants.ts +468 -0
  403. package/src/components/surface/AGENTS.md +72 -0
  404. package/src/components/surface/index.ts +15 -0
  405. package/src/components/surface/surface.context.tsx +54 -0
  406. package/src/components/surface/surface.tsx +70 -0
  407. package/src/components/surface/surface.variants.test.ts +180 -0
  408. package/src/components/surface/surface.variants.ts +133 -0
  409. package/src/components/swipe/AGENTS.md +140 -0
  410. package/src/components/swipe/index.ts +31 -0
  411. package/src/components/swipe/swipe-action.tsx +99 -0
  412. package/src/components/swipe/swipe-end.tsx +14 -0
  413. package/src/components/swipe/swipe-group.tsx +55 -0
  414. package/src/components/swipe/swipe-panel.tsx +55 -0
  415. package/src/components/swipe/swipe-start.tsx +15 -0
  416. package/src/components/swipe/swipe.context.tsx +135 -0
  417. package/src/components/swipe/swipe.tsx +393 -0
  418. package/src/components/swipe/swipe.types.ts +7 -0
  419. package/src/components/swipe/swipe.variants.test.ts +337 -0
  420. package/src/components/swipe/swipe.variants.ts +290 -0
  421. package/src/components/switch/switch.variants.ts +2 -2
  422. package/src/components/tabs/AGENTS.md +4 -3
  423. package/src/components/tabs/tabs.variants.test.ts +1 -1
  424. package/src/components/tabs/tabs.variants.ts +4 -6
  425. package/src/components/textarea/AGENTS.md +86 -0
  426. package/src/components/textarea/index.ts +16 -0
  427. package/src/components/textarea/textarea.tsx +157 -0
  428. package/src/components/textarea/textarea.variants.test.ts +219 -0
  429. package/src/components/textarea/textarea.variants.ts +171 -0
  430. package/src/components/toast/AGENTS.md +140 -0
  431. package/src/components/toast/index.ts +74 -0
  432. package/src/components/toast/toast-action.tsx +59 -0
  433. package/src/components/toast/toast-api.ts +32 -0
  434. package/src/components/toast/toast-close.tsx +44 -0
  435. package/src/components/toast/toast-content.tsx +11 -0
  436. package/src/components/toast/toast-description.tsx +11 -0
  437. package/src/components/toast/toast-indicator.tsx +34 -0
  438. package/src/components/toast/toast-item.tsx +308 -0
  439. package/src/components/toast/toast-title.tsx +13 -0
  440. package/src/components/toast/toast-viewport.tsx +135 -0
  441. package/src/components/toast/toast.context.tsx +69 -0
  442. package/src/components/toast/toast.store.test.ts +259 -0
  443. package/src/components/toast/toast.store.ts +338 -0
  444. package/src/components/toast/toast.timer.test.ts +64 -0
  445. package/src/components/toast/toast.timer.ts +51 -0
  446. package/src/components/toast/toast.tsx +132 -0
  447. package/src/components/toast/toast.variants.test.ts +393 -0
  448. package/src/components/toast/toast.variants.ts +304 -0
  449. package/src/components/toggle-button/AGENTS.md +106 -0
  450. package/src/components/toggle-button/index.ts +30 -0
  451. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  452. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  453. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  454. package/src/components/toggle-button/toggle-button.tsx +172 -0
  455. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  456. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  457. package/src/components/tooltip/AGENTS.md +133 -0
  458. package/src/components/tooltip/index.ts +28 -0
  459. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  460. package/src/components/tooltip/tooltip-content.tsx +179 -0
  461. package/src/components/tooltip/tooltip-description.tsx +19 -0
  462. package/src/components/tooltip/tooltip-text.tsx +21 -0
  463. package/src/components/tooltip/tooltip-title.tsx +20 -0
  464. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  465. package/src/components/tooltip/tooltip.context.tsx +68 -0
  466. package/src/components/tooltip/tooltip.tsx +213 -0
  467. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  468. package/src/components/tooltip/tooltip.variants.ts +177 -0
  469. package/src/hooks/use-calm-motion.tsx +31 -0
  470. package/src/lib/calm-motion.test.ts +17 -0
  471. package/src/lib/calm-motion.ts +23 -0
  472. package/src/lib/cn.test.ts +14 -0
  473. package/src/lib/cn.ts +2 -2
  474. package/src/no-gorhom.test.ts +54 -0
  475. package/src/styles/geometry.test.ts +1 -0
  476. package/src/styles/theme.css +4 -0
  477. package/src/styles/tokens.css +11 -0
  478. package/src/styles/tokens.test.ts +22 -0
  479. package/src/styles/tokens.ts +20 -0
  480. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  481. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -0,0 +1,244 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+
4
+ /** The logical sides a drawer opens from. `start` and `end` follow the layout direction. */
5
+ export const DRAWER_SIDES = ["start", "end", "top", "bottom"] as const;
6
+ export type DrawerSide = (typeof DRAWER_SIDES)[number];
7
+
8
+ /** The physical screen edges a side resolves to — what the motion and the pan are measured against. */
9
+ export const DRAWER_EDGES = ["left", "right", "top", "bottom"] as const;
10
+ export type DrawerEdge = (typeof DRAWER_EDGES)[number];
11
+
12
+ /** The panel's extents, narrowest first. */
13
+ export const DRAWER_SIZES = ["sm", "md", "lg", "full"] as const;
14
+ export type DrawerSize = (typeof DRAWER_SIZES)[number];
15
+
16
+ /** What a released swipe does. */
17
+ export type DrawerRelease = "dismiss" | "restore";
18
+
19
+ /**
20
+ * Each size as a fraction of the window along the panel's axis, capped.
21
+ *
22
+ * A fraction so a phone always keeps a strip of the app in view to tap away
23
+ * on; a cap so a tablet's drawer stays a drawer and not a second screen.
24
+ * `full` keeps a 6% strip and has no cap.
25
+ */
26
+ export const DRAWER_SIZE_EXTENT = {
27
+ sm: { fraction: 0.62, max: 280 },
28
+ md: { fraction: 0.78, max: 320 },
29
+ lg: { fraction: 0.88, max: 400 },
30
+ full: { fraction: 0.94, max: Number.POSITIVE_INFINITY },
31
+ } as const satisfies Record<DrawerSize, { fraction: number; max: number }>;
32
+
33
+ /** A release past this fraction of the extent toward the edge dismisses. */
34
+ export const DRAWER_DISMISS_FRACTION = 0.4;
35
+
36
+ /** A release faster than this toward the edge dismisses from any distance, pt/s. */
37
+ export const DRAWER_FLING_VELOCITY = 800;
38
+
39
+ /** The asymptote a drag away from the edge approaches, in points. */
40
+ export const DRAWER_RUBBER_BAND = 40;
41
+
42
+ /**
43
+ * Travel along the panel's axis before the pan claims the touch, in points.
44
+ * Travel across it first fails the pan, so a vertical scroll in a start
45
+ * drawer's body never moves the drawer.
46
+ */
47
+ export const DRAWER_PAN_ACTIVE_OFFSET = 10;
48
+
49
+ /** The bounds on a release animation, ms — finite so an E2E settle wait ends. */
50
+ export const DRAWER_RELEASE_MS = { min: 80, max: 220 } as const;
51
+
52
+ /** The scrim's token — the foundation's, named here so the test can pin it. */
53
+ export const DRAWER_SCRIM_TOKEN = "overlay";
54
+
55
+ /** Slop around the header ✕ — a bare glyph has no capsule to bring it toward 44pt. */
56
+ export const DRAWER_CLOSE_HIT_SLOP = 8;
57
+
58
+ export const drawerVariants = tv({
59
+ slots: {
60
+ /** The foundation's scrim token. The fade is `Overlay.Scrim`'s own. */
61
+ scrim: "bg-overlay",
62
+ /**
63
+ * A window-sized frame the panel is positioned in, laid out left to
64
+ * right whatever the app's direction, so the panel's classes and its
65
+ * safe-area padding are physical — the same terms its motion and its pan
66
+ * are measured in. `inner` restores the app's direction for the content.
67
+ */
68
+ positioner: "absolute inset-0",
69
+ /**
70
+ * The panel. Docked flush to its edge with square corners there, and the
71
+ * card corner on the two corners facing the app. `popover` because a
72
+ * drawer is a layer over the app, as `Dialog` and `BottomSheet` are.
73
+ */
74
+ content: "absolute overflow-hidden bg-popover",
75
+ /** The column inside the safe-area padding, laid out in the app's direction. */
76
+ inner: "flex-1",
77
+ /** Title block and ✕ in a row; the ✕ sits at the trailing edge. */
78
+ header: "flex-row items-start gap-3 px-5 pt-5 pb-3",
79
+ /** The title and description column, taking the row's slack. */
80
+ heading: "flex-1 gap-1",
81
+ body: "flex-1",
82
+ /** The body's padding, on the scroll content so it scrolls with it. */
83
+ bodyContent: "gap-3 px-5 pb-5",
84
+ /** `mt-auto` so it sits at the panel's end even when no body takes the slack. */
85
+ footer: "mt-auto flex-row items-center justify-end gap-2 border-border border-t px-5 py-4",
86
+ close: "shrink-0",
87
+ },
88
+ variants: {
89
+ edge: {
90
+ left: { content: "inset-y-0 left-0 rounded-r-lg" },
91
+ right: { content: "inset-y-0 right-0 rounded-l-lg" },
92
+ top: { content: "inset-x-0 top-0 rounded-b-lg" },
93
+ bottom: { content: "inset-x-0 bottom-0 rounded-t-lg" },
94
+ },
95
+ },
96
+ defaultVariants: {
97
+ edge: "left",
98
+ },
99
+ });
100
+
101
+ /**
102
+ * There is no `title` or `description` slot. Both *are* text presets with no
103
+ * layout of their own — the ✕ sits in the flow, so the title needs no
104
+ * clearance — and `tv` emits `undefined` for an empty class string. The parts
105
+ * merge a caller's `className` with `cn()` instead, as `Dialog`'s description does.
106
+ */
107
+ export type DrawerVariantProps = VariantProps<typeof drawerVariants>;
108
+
109
+ /**
110
+ * The physical edge a side docks to. `start` is the left edge in a
111
+ * left-to-right layout and the right edge in a right-to-left one.
112
+ */
113
+ export function resolveDrawerEdge(side: DrawerSide, isRTL: boolean): DrawerEdge {
114
+ if (side === "start") return isRTL ? "right" : "left";
115
+ if (side === "end") return isRTL ? "left" : "right";
116
+ return side;
117
+ }
118
+
119
+ /**
120
+ * The panel's width (a left or right edge) or height (top or bottom), from the
121
+ * window's extent along the same axis. Whole points, never negative.
122
+ */
123
+ export function resolveDrawerExtent(size: DrawerSize, windowExtent: number): number {
124
+ if (windowExtent <= 0) return 0;
125
+ const { fraction, max } = DRAWER_SIZE_EXTENT[size];
126
+ return Math.round(Math.min(windowExtent * fraction, max));
127
+ }
128
+
129
+ /**
130
+ * The translate that puts the panel `progress` of the way in: 0 is the whole
131
+ * extent off its edge, 1 is docked.
132
+ *
133
+ * A worklet, read in the panel's animated style. Flat on purpose — a
134
+ * module-scope worklet never calls another function.
135
+ */
136
+ export function resolveDrawerOffset(
137
+ edge: DrawerEdge,
138
+ extent: number,
139
+ progress: number
140
+ ): { translateX: number; translateY: number } {
141
+ "worklet";
142
+ const hidden = extent * (1 - progress);
143
+ if (edge === "left") return { translateX: -hidden, translateY: 0 };
144
+ if (edge === "right") return { translateX: hidden, translateY: 0 };
145
+ if (edge === "top") return { translateX: 0, translateY: -hidden };
146
+ return { translateX: 0, translateY: hidden };
147
+ }
148
+
149
+ /**
150
+ * Where the panel sits for a finger's travel along its axis.
151
+ *
152
+ * Toward the docked edge — the way it would leave — the panel follows the
153
+ * finger one to one. Away from it, into the app, it rubber-bands toward
154
+ * `DRAWER_RUBBER_BAND` and never reaches it, so the panel gives a little and
155
+ * says it goes no further. The result has the translation's sign.
156
+ */
157
+ export function resolveDrawerDrag(edge: DrawerEdge, translation: number): number {
158
+ "worklet";
159
+ const sign = edge === "left" || edge === "top" ? -1 : 1;
160
+ const toward = translation * sign;
161
+ if (toward >= 0) return translation;
162
+ const away = -toward;
163
+ return -sign * (away / (1 + away / DRAWER_RUBBER_BAND));
164
+ }
165
+
166
+ /** How much of the extent a drag has carried the panel toward its edge, 0 to 1. */
167
+ export function resolveDrawerDragFraction(edge: DrawerEdge, translation: number, extent: number): number {
168
+ "worklet";
169
+ if (extent <= 0) return 0;
170
+ const sign = edge === "left" || edge === "top" ? -1 : 1;
171
+ return Math.min(1, Math.max(0, (translation * sign) / extent));
172
+ }
173
+
174
+ export type DrawerReleaseInput = {
175
+ edge: DrawerEdge;
176
+ /** The finger's travel along the panel's axis, physical sign. */
177
+ translation: number;
178
+ /** The release velocity along the same axis, pt/s, physical sign. */
179
+ velocity: number;
180
+ extent: number;
181
+ };
182
+
183
+ /**
184
+ * Whether a released swipe dismisses the drawer or puts it back.
185
+ *
186
+ * A fling away from the edge always restores — the user changed their mind
187
+ * mid-swipe. Otherwise a fling toward the edge faster than
188
+ * `DRAWER_FLING_VELOCITY`, or a drag past `DRAWER_DISMISS_FRACTION` of the
189
+ * extent, dismisses.
190
+ */
191
+ export function resolveDrawerRelease({ edge, translation, velocity, extent }: DrawerReleaseInput): DrawerRelease {
192
+ "worklet";
193
+ if (extent <= 0) return "restore";
194
+ const sign = edge === "left" || edge === "top" ? -1 : 1;
195
+ const toward = velocity * sign;
196
+ if (toward < -DRAWER_FLING_VELOCITY) return "restore";
197
+ if (toward > DRAWER_FLING_VELOCITY) return "dismiss";
198
+ return (translation * sign) / extent > DRAWER_DISMISS_FRACTION ? "dismiss" : "restore";
199
+ }
200
+
201
+ /**
202
+ * How long the panel takes to cover the rest of its way out after a dismissing
203
+ * release: the remaining distance at the release speed, held inside
204
+ * `DRAWER_RELEASE_MS` so a flick does not snap and a slow release does not drift.
205
+ */
206
+ export function resolveDrawerExitDuration({ remaining, velocity }: { remaining: number; velocity: number }): number {
207
+ "worklet";
208
+ const speed = Math.abs(velocity);
209
+ if (remaining <= 0) return DRAWER_RELEASE_MS.min;
210
+ if (speed === 0) return DRAWER_RELEASE_MS.max;
211
+ return Math.min(DRAWER_RELEASE_MS.max, Math.max(DRAWER_RELEASE_MS.min, (remaining / speed) * 1000));
212
+ }
213
+
214
+ export type DrawerInsets = { top: number; right: number; bottom: number; left: number };
215
+
216
+ /**
217
+ * The safe-area padding a panel docked to `edge` takes.
218
+ *
219
+ * Every side that meets a screen edge pads its inset — the docked side and the
220
+ * two beside it — so a start drawer clears the status bar and the home
221
+ * indicator. The side facing the app sits nowhere near a screen edge, and pads
222
+ * nothing.
223
+ */
224
+ export function resolveDrawerInsets(edge: DrawerEdge, insets: DrawerInsets): DrawerInsets {
225
+ return {
226
+ top: edge === "bottom" ? 0 : insets.top,
227
+ right: edge === "left" ? 0 : insets.right,
228
+ bottom: edge === "top" ? 0 : insets.bottom,
229
+ left: edge === "right" ? 0 : insets.left,
230
+ };
231
+ }
232
+
233
+ /**
234
+ * The panel's full width or height: the size's extent plus the inset on the
235
+ * docked edge.
236
+ *
237
+ * The size measures the content, not the safe-area band it sits under — a
238
+ * top drawer at `sm` would otherwise hand a third of its 280pt to the status
239
+ * bar and clip its rows. The motion uses this, so the panel starts fully off
240
+ * screen.
241
+ */
242
+ export function resolveDrawerFrameExtent(edge: DrawerEdge, extent: number, insets: DrawerInsets): number {
243
+ return extent + insets[edge];
244
+ }
@@ -0,0 +1,41 @@
1
+ export { Drawer, type DrawerProps } from "./drawer";
2
+ export { type DrawerContextValue, useDrawer, useDrawerContext } from "./drawer.context";
3
+ export {
4
+ DRAWER_CLOSE_HIT_SLOP,
5
+ DRAWER_DISMISS_FRACTION,
6
+ DRAWER_EDGES,
7
+ DRAWER_FLING_VELOCITY,
8
+ DRAWER_PAN_ACTIVE_OFFSET,
9
+ DRAWER_RELEASE_MS,
10
+ DRAWER_RUBBER_BAND,
11
+ DRAWER_SCRIM_TOKEN,
12
+ DRAWER_SIDES,
13
+ DRAWER_SIZE_EXTENT,
14
+ DRAWER_SIZES,
15
+ type DrawerEdge,
16
+ type DrawerInsets,
17
+ type DrawerRelease,
18
+ type DrawerReleaseInput,
19
+ type DrawerSide,
20
+ type DrawerSize,
21
+ type DrawerVariantProps,
22
+ drawerVariants,
23
+ resolveDrawerDrag,
24
+ resolveDrawerDragFraction,
25
+ resolveDrawerEdge,
26
+ resolveDrawerExitDuration,
27
+ resolveDrawerExtent,
28
+ resolveDrawerFrameExtent,
29
+ resolveDrawerInsets,
30
+ resolveDrawerOffset,
31
+ resolveDrawerRelease,
32
+ } from "./drawer.variants";
33
+ export type { DrawerBodyProps } from "./drawer-body";
34
+ export type { DrawerCloseProps } from "./drawer-close";
35
+ export type { DrawerContentProps } from "./drawer-content";
36
+ export type { DrawerDescriptionProps } from "./drawer-description";
37
+ export type { DrawerFooterProps } from "./drawer-footer";
38
+ export type { DrawerHeaderProps } from "./drawer-header";
39
+ export type { DrawerTitleProps } from "./drawer-title";
40
+ export type { DrawerTriggerProps } from "./drawer-trigger";
41
+ export type { UseDrawerPanOptions } from "./use-drawer-pan";
@@ -0,0 +1,108 @@
1
+ import { useMemo } from "react";
2
+ import { Gesture, type PanGesture } from "react-native-gesture-handler";
3
+ import { cancelAnimation, Easing, type SharedValue, useSharedValue, withTiming } from "react-native-reanimated";
4
+ import { scheduleOnRN } from "react-native-worklets";
5
+ import { OVERLAY_MOTION } from "../overlay/overlay.variants";
6
+ import { playHaptic } from "../pressable";
7
+ import {
8
+ DRAWER_DISMISS_FRACTION,
9
+ DRAWER_PAN_ACTIVE_OFFSET,
10
+ type DrawerEdge,
11
+ resolveDrawerDrag,
12
+ resolveDrawerDragFraction,
13
+ resolveDrawerExitDuration,
14
+ resolveDrawerRelease,
15
+ } from "./drawer.variants";
16
+
17
+ export type UseDrawerPanOptions = {
18
+ edge: DrawerEdge;
19
+ /** The panel's width or height along its axis. */
20
+ extent: number;
21
+ isEnabled: boolean;
22
+ /** The panel's displacement along its axis, physical sign — written by the pan, read by the panel's style. */
23
+ drag: SharedValue<number>;
24
+ /** Called once a dismissing release has carried the panel off its edge. */
25
+ onDismiss: () => void;
26
+ };
27
+
28
+ const RESTORE_EASING = Easing.out(Easing.cubic);
29
+ const PAN_RANGE: [number, number] = [-DRAWER_PAN_ACTIVE_OFFSET, DRAWER_PAN_ACTIVE_OFFSET];
30
+
31
+ /**
32
+ * The panel's swipe-to-dismiss.
33
+ *
34
+ * The pan claims the touch only after `DRAWER_PAN_ACTIVE_OFFSET` of travel
35
+ * along the panel's axis and fails on the same travel across it, so a tap
36
+ * reaches the row under it and a vertical scroll in a start or end drawer's
37
+ * body never moves the drawer.
38
+ *
39
+ * While it runs the panel follows the finger toward its edge and rubber-bands
40
+ * away from it (`resolveDrawerDrag`), and a `selection` haptic marks each
41
+ * crossing of the dismiss threshold — the moment letting go changes what
42
+ * happens. On release `resolveDrawerRelease` decides: a dismiss **continues
43
+ * from where the finger left the panel**, at the release speed, and closes the
44
+ * drawer when it lands; a restore eases back to docked.
45
+ *
46
+ * The handlers are built in this hook, so they may call the module-scope
47
+ * worklets in `drawer.variants` — those, being module-scope, call nothing.
48
+ * Per-gesture memory lives in shared values: each worklet gets its own copy of
49
+ * a captured `let`.
50
+ */
51
+ export function useDrawerPan({ edge, extent, isEnabled, drag, onDismiss }: UseDrawerPanOptions): PanGesture {
52
+ const isDragging = useSharedValue(false);
53
+ const isPastThreshold = useSharedValue(false);
54
+
55
+ return useMemo(() => {
56
+ const isHorizontal = edge === "left" || edge === "right";
57
+ const sign = edge === "left" || edge === "top" ? -1 : 1;
58
+ const pan = Gesture.Pan().enabled(isEnabled).shouldCancelWhenOutside(false);
59
+ if (isHorizontal) {
60
+ pan.activeOffsetX(PAN_RANGE).failOffsetY(PAN_RANGE);
61
+ } else {
62
+ pan.activeOffsetY(PAN_RANGE).failOffsetX(PAN_RANGE);
63
+ }
64
+
65
+ return pan
66
+ .onStart(() => {
67
+ "worklet";
68
+ cancelAnimation(drag);
69
+ isDragging.value = true;
70
+ isPastThreshold.value = false;
71
+ })
72
+ .onUpdate((event) => {
73
+ "worklet";
74
+ const translation = isHorizontal ? event.translationX : event.translationY;
75
+ drag.value = resolveDrawerDrag(edge, translation);
76
+
77
+ const isPast = resolveDrawerDragFraction(edge, translation, extent) > DRAWER_DISMISS_FRACTION;
78
+ if (isPast !== isPastThreshold.value) {
79
+ isPastThreshold.value = isPast;
80
+ playHaptic("selection");
81
+ }
82
+ })
83
+ .onFinalize((event) => {
84
+ "worklet";
85
+ // A touch that never activated — a tap, a scroll across the axis —
86
+ // must not touch `drag`: a dismiss may still be animating it.
87
+ if (!isDragging.value) return;
88
+ isDragging.value = false;
89
+
90
+ const translation = isHorizontal ? event.translationX : event.translationY;
91
+ const velocity = isHorizontal ? event.velocityX : event.velocityY;
92
+ const release = resolveDrawerRelease({ edge, extent, translation, velocity });
93
+
94
+ if (release === "dismiss") {
95
+ const duration = resolveDrawerExitDuration({
96
+ remaining: Math.max(0, extent - Math.abs(drag.value)),
97
+ velocity,
98
+ });
99
+ drag.value = withTiming(sign * extent, { duration, easing: Easing.linear }, (finished) => {
100
+ if (finished) scheduleOnRN(onDismiss);
101
+ });
102
+ return;
103
+ }
104
+
105
+ drag.value = withTiming(0, { duration: OVERLAY_MOTION.enterMs, easing: RESTORE_EASING });
106
+ });
107
+ }, [drag, edge, extent, isDragging, isEnabled, isPastThreshold, onDismiss]);
108
+ }
@@ -0,0 +1,90 @@
1
+ # EmptyState
2
+
3
+ A placeholder for a list or a screen with nothing in it yet. Compound root plus
4
+ five parts: `Header`, `Media`, `Title`, `Description`, `Content`.
5
+
6
+ `import { EmptyState } from "@delacour/react-native-ui/empty-state";`
7
+
8
+ ## Files
9
+
10
+ | File | What it holds |
11
+ | --- | --- |
12
+ | `index.ts` | → `@delacour/react-native-ui/empty-state` |
13
+ | `empty-state.tsx` | Root + the `Object.assign` compound surface |
14
+ | `empty-state-header.tsx` | `EmptyState.Header` |
15
+ | `empty-state-media.tsx` | `EmptyState.Media`, and the icon cascade it owns |
16
+ | `empty-state-title.tsx` | `EmptyState.Title` |
17
+ | `empty-state-description.tsx` | `EmptyState.Description` |
18
+ | `empty-state-content.tsx` | `EmptyState.Content` |
19
+ | `empty-state.context.tsx` | `EmptyStateProvider`, `useEmptyState()`, `useEmptyStateContext()`, `useEmptyStatePart()` |
20
+ | `empty-state.types.ts` | Prop types shared by two or more parts |
21
+ | `empty-state.variants.ts` | Pure `tv()` slots and the media colour map, no RN imports |
22
+ | `empty-state.variants.test.ts` | |
23
+
24
+ ## Anatomy
25
+
26
+ ```tsx
27
+ <EmptyState>
28
+ <EmptyState.Header>
29
+ <EmptyState.Media variant="icon">
30
+ <Icon icon={IconInbox} />
31
+ </EmptyState.Media>
32
+ <EmptyState.Title>No messages</EmptyState.Title>
33
+ <EmptyState.Description>New conversations will show up here.</EmptyState.Description>
34
+ </EmptyState.Header>
35
+ <EmptyState.Content>
36
+ <Button onPress={compose}>New message</Button>
37
+ </EmptyState.Content>
38
+ </EmptyState>
39
+ ```
40
+
41
+ ## Design
42
+
43
+ - **Variants**: `default`, `card`. **Sizes**: `sm`, `md`, `lg`. **Media
44
+ variants**: `default`, `icon`. Size drives the padding, both gaps, the media
45
+ box, the glyph inside it and both type scales — one axis rather than seven
46
+ numbers that drift apart. The tests assert every one of them ascends.
47
+ - **`default` grows; it never uses `flex-1`.** `flex-1` is a zero flex basis, so
48
+ inside a `ScrollView`'s content container, which has no height to grow into,
49
+ the block collapsed to nothing. `grow` keeps the content height as a floor and
50
+ fills whatever spare height a bounded parent has — which is what "fills its
51
+ container" has to mean in both places. A test holds the class.
52
+ - **`card` is dashed.** A dashed outline is the conventional mark of a slot
53
+ waiting for content, and it stops an embedded empty state reading as one
54
+ more populated card beside real ones. The corner is the card step,
55
+ `rounded-lg`, stepping to `rounded-md` at `sm` like `ListGroup` and
56
+ `Accordion`. The border is reserved on both variants (`border-transparent` on
57
+ `default`), so switching variant changes a colour and never the layout.
58
+ - **A size is padding, never a height.** The description respects OS font
59
+ scaling; a fixed height would clip it at a large accessibility step.
60
+ - **Icons are composed, never passed as props.** `Media` wraps its subtree in an
61
+ `IconDefaultsProvider` carrying the `mediaIcon` slot — a step on the shared
62
+ `--spacing-icon-*` scale — and `EMPTY_STATE_MEDIA_FOREGROUND_TOKEN[variant]`.
63
+ A bare glyph is `muted-foreground`, because the picture is decoration and must
64
+ not outweigh the title. Inside the tinted `icon` box it takes `foreground`:
65
+ `muted-foreground` on `bg-muted` is two greys a step apart.
66
+ - **The glyph uses the same three steps inside the box and out of it.** The
67
+ icon scale tops out at `icon-2xl`, so a bare glyph one step larger than the
68
+ boxed one would have left `md` and `lg` identical.
69
+ - **The media sits further from the title than the title from its
70
+ description.** The header's `gap` spaces the two lines; the media adds a
71
+ bottom margin on top of it, so text reads as one unit and the picture as
72
+ something above it.
73
+ - **`Media` is hidden from assistive technology by default.** The title already
74
+ says what the picture shows, and a screen reader saying "image" first only
75
+ delays it. Media that carries meaning of its own opts back in with
76
+ `accessibilityElementsHidden={false}` and `importantForAccessibility="auto"` —
77
+ both are spread after the defaults.
78
+ - **`Title` is announced as a header.** An empty screen's title is what the
79
+ screen *is*, and the heading rotor should land on it.
80
+ - **Actions are a wrapping row, not a column.** Two actions sit side by side and
81
+ fall to a second line only when they must. `className="flex-col"` stacks them.
82
+ Button size is the caller's choice — `Content` sets nothing on its children, so
83
+ a `Button` keeps every prop it already has.
84
+ - **Header and content cap at `max-w-sm`.** On a tablet or in landscape a
85
+ description stretched edge to edge is a line too long to read.
86
+ - **Colour goes on the text parts.** No `View` slot carries a `text-*` utility;
87
+ a React Native `View` cannot cascade colour to a `Text`. The tests assert it.
88
+ - **No state of its own.** An empty state is a presentation of *someone else's*
89
+ state — a list's length, a query's result. Loading and disabled belong to the
90
+ `Button`s inside it, which already have them.
@@ -0,0 +1,17 @@
1
+ import type { ReactElement } from "react";
2
+ import { View } from "react-native";
3
+ import { useEmptyStatePart } from "./empty-state.context";
4
+ import type { EmptyStateSlotProps } from "./empty-state.types";
5
+ import { emptyStateVariants } from "./empty-state.variants";
6
+
7
+ /**
8
+ * The action slot under the header — usually one or two `Button`s.
9
+ *
10
+ * A centred row that wraps, so two actions sit side by side and fall onto a
11
+ * second line only when they have to. Pass `className="flex-col"` to stack them.
12
+ */
13
+ export function EmptyStateContent({ className, ...props }: EmptyStateSlotProps): ReactElement {
14
+ const { size } = useEmptyStatePart("EmptyState.Content");
15
+ return <View className={emptyStateVariants({ size }).content({ className })} {...props} />;
16
+ }
17
+ EmptyStateContent.displayName = "DelacourUI.EmptyState.Content";
@@ -0,0 +1,12 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text } from "../text";
3
+ import { useEmptyStatePart } from "./empty-state.context";
4
+ import type { EmptyStateTextProps } from "./empty-state.types";
5
+ import { emptyStateVariants } from "./empty-state.variants";
6
+
7
+ /** The supporting line under the title — a step down in scale, on the muted token. */
8
+ export function EmptyStateDescription({ className, ...props }: EmptyStateTextProps): ReactElement {
9
+ const { size } = useEmptyStatePart("EmptyState.Description");
10
+ return <Text className={emptyStateVariants({ size }).description({ className })} {...props} />;
11
+ }
12
+ EmptyStateDescription.displayName = "DelacourUI.EmptyState.Description";
@@ -0,0 +1,17 @@
1
+ import type { ReactElement } from "react";
2
+ import { View } from "react-native";
3
+ import { useEmptyStatePart } from "./empty-state.context";
4
+ import type { EmptyStateSlotProps } from "./empty-state.types";
5
+ import { emptyStateVariants } from "./empty-state.variants";
6
+
7
+ /**
8
+ * The centred column holding the media, the title and the description.
9
+ *
10
+ * Its own gap spaces the title from the description; the media adds a margin
11
+ * of its own on top, so the picture sits apart from the two lines of text.
12
+ */
13
+ export function EmptyStateHeader({ className, ...props }: EmptyStateSlotProps): ReactElement {
14
+ const { size } = useEmptyStatePart("EmptyState.Header");
15
+ return <View className={emptyStateVariants({ size }).header({ className })} {...props} />;
16
+ }
17
+ EmptyStateHeader.displayName = "DelacourUI.EmptyState.Header";
@@ -0,0 +1,56 @@
1
+ import { type ReactElement, useMemo } from "react";
2
+ import { View } from "react-native";
3
+ import { IconDefaultsProvider } from "../icon";
4
+ import { useEmptyStatePart } from "./empty-state.context";
5
+ import type { EmptyStateSlotProps } from "./empty-state.types";
6
+ import {
7
+ EMPTY_STATE_MEDIA_FOREGROUND_TOKEN,
8
+ type EmptyStateMediaVariant,
9
+ emptyStateVariants,
10
+ } from "./empty-state.variants";
11
+
12
+ export type EmptyStateMediaProps = EmptyStateSlotProps & {
13
+ /**
14
+ * `icon` draws a tinted, rounded box sized for a glyph. `default` draws
15
+ * nothing behind its content, for an illustration or an image that sizes
16
+ * itself.
17
+ */
18
+ variant?: EmptyStateMediaVariant;
19
+ };
20
+
21
+ /**
22
+ * The picture above the title.
23
+ *
24
+ * Its subtree inherits an icon size on the shared scale and a colour token, so a
25
+ * bare `<Icon icon={IconInbox} />` comes out right with nothing said at the call
26
+ * site — the same cascade a `Button` gives its icons.
27
+ *
28
+ * Hidden from assistive technology by default: the title already says what the
29
+ * picture shows, and a screen reader announcing "image" first only delays it.
30
+ * Pass `accessibilityElementsHidden={false}` and
31
+ * `importantForAccessibility="auto"` for media that carries meaning of its own.
32
+ */
33
+ export function EmptyStateMedia({
34
+ variant = "default",
35
+ className,
36
+ children,
37
+ ...props
38
+ }: EmptyStateMediaProps): ReactElement {
39
+ const { size } = useEmptyStatePart("EmptyState.Media");
40
+ const slots = emptyStateVariants({ size, media: variant });
41
+ const iconClassName = slots.mediaIcon();
42
+ const color = EMPTY_STATE_MEDIA_FOREGROUND_TOKEN[variant];
43
+ const iconDefaults = useMemo(() => ({ className: iconClassName, color }), [iconClassName, color]);
44
+
45
+ return (
46
+ <View
47
+ accessibilityElementsHidden
48
+ className={slots.media({ className })}
49
+ importantForAccessibility="no-hide-descendants"
50
+ {...props}
51
+ >
52
+ <IconDefaultsProvider value={iconDefaults}>{children}</IconDefaultsProvider>
53
+ </View>
54
+ );
55
+ }
56
+ EmptyStateMedia.displayName = "DelacourUI.EmptyState.Media";
@@ -0,0 +1,18 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text } from "../text";
3
+ import { useEmptyStatePart } from "./empty-state.context";
4
+ import type { EmptyStateTextProps } from "./empty-state.types";
5
+ import { emptyStateVariants } from "./empty-state.variants";
6
+
7
+ /**
8
+ * The empty state's heading.
9
+ *
10
+ * Announced as a header, so a screen reader's heading rotor lands on it — the
11
+ * title is what an empty screen *is*. Carries its own colour and type scale
12
+ * from context: a React Native `View` does not cascade colour to a `Text`.
13
+ */
14
+ export function EmptyStateTitle({ className, ...props }: EmptyStateTextProps): ReactElement {
15
+ const { size } = useEmptyStatePart("EmptyState.Title");
16
+ return <Text accessibilityRole="header" className={emptyStateVariants({ size }).title({ className })} {...props} />;
17
+ }
18
+ EmptyStateTitle.displayName = "DelacourUI.EmptyState.Title";
@@ -0,0 +1,65 @@
1
+ import { createContext, type ReactElement, type ReactNode, use } from "react";
2
+ import type { EmptyStateSize, EmptyStateVariant } from "./empty-state.variants";
3
+
4
+ export type EmptyStateContextValue = {
5
+ /** Size of the empty state. */
6
+ size: EmptyStateSize;
7
+ /** How the empty state sits on the screen. */
8
+ variant: EmptyStateVariant;
9
+ };
10
+
11
+ const EmptyStateContext = createContext<EmptyStateContextValue | null>(null);
12
+
13
+ /**
14
+ * Supplies the enclosing empty state's size and variant to its subtree.
15
+ *
16
+ * Lives in its own module, importing nothing but `empty-state.variants`, so a
17
+ * part can read it without importing `./empty-state` and closing a cycle
18
+ * through the root. See the package AGENTS.md, rule 3.
19
+ */
20
+ export function EmptyStateProvider({
21
+ value,
22
+ children,
23
+ }: {
24
+ value: EmptyStateContextValue;
25
+ children: ReactNode;
26
+ }): ReactElement {
27
+ return <EmptyStateContext value={value}>{children}</EmptyStateContext>;
28
+ }
29
+ EmptyStateProvider.displayName = "DelacourUI.EmptyState.Provider";
30
+
31
+ /** The enclosing empty state's context, or null outside an `<EmptyState>`. */
32
+ export function useEmptyStateContext(): EmptyStateContextValue | null {
33
+ return use(EmptyStateContext);
34
+ }
35
+
36
+ /**
37
+ * Reads the enclosing empty state's size and variant.
38
+ *
39
+ * Lets a custom part — an illustration, a link row — scale with the block
40
+ * without the root passing props down. Throws outside an `<EmptyState>`; use
41
+ * {@link useEmptyStateContext} where the enclosing empty state is optional.
42
+ */
43
+ export function useEmptyState(): EmptyStateContextValue {
44
+ const context = useEmptyStateContext();
45
+ if (!context) {
46
+ throw new Error("useEmptyState must be called inside an <EmptyState>.");
47
+ }
48
+ return context;
49
+ }
50
+
51
+ /**
52
+ * The enclosing empty state's context, for a compound part that cannot work
53
+ * without one.
54
+ *
55
+ * Internal: deliberately not re-exported from `index.ts`. A caller outside the
56
+ * library wants {@link useEmptyState}, whose error names the hook rather than a
57
+ * part.
58
+ */
59
+ export function useEmptyStatePart(component: string): EmptyStateContextValue {
60
+ const context = useEmptyStateContext();
61
+ if (!context) {
62
+ throw new Error(`${component} must be rendered inside an <EmptyState>.`);
63
+ }
64
+ return context;
65
+ }