@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,120 @@
1
+ # Skeleton
2
+
3
+ A placeholder standing in for content that is still loading: a tinted shape
4
+ with a glint sweeping across it, or a pulse, on the UI thread. Compound root plus
5
+ `Skeleton.Group`, which synchronises the skeletons inside it, and
6
+ `Skeleton.Lines`, a paragraph of placeholder lines.
7
+
8
+ `import { Skeleton } from "@delacour/react-native-ui/skeleton";`
9
+
10
+ ## Files
11
+
12
+ | File | What it holds |
13
+ | --- | --- |
14
+ | `index.ts` | → `@delacour/react-native-ui/skeleton` |
15
+ | `skeleton.tsx` | Root, `Skeleton.Lines` and the `Object.assign` compound surface |
16
+ | `skeleton-group.tsx` | `Skeleton.Group` — the shared clock and loading flag |
17
+ | `skeleton-shimmer.tsx` | The sweeping band, an internal leaf |
18
+ | `skeleton-clock.ts` | `useSkeletonClock`, the 0 → 1 UI-thread loop both animations read |
19
+ | `skeleton.context.tsx` | `SkeletonGroupProvider`, `useSkeletonGroup()` |
20
+ | `skeleton.variants.ts` | Pure `tv()` slots + resolvers + worklets, no RN imports |
21
+ | `skeleton.variants.test.ts` | |
22
+
23
+ ## Design
24
+
25
+ - **Three shapes, and a shape is a corner plus a default size.** `rect` is a
26
+ card-shaped `rounded-lg` (see **Sizing** in the package doc for why a card is
27
+ that step), `line` is a line of text at `rounded-sm`, `circle` is an avatar at
28
+ `rounded-full`. A childless skeleton also takes a default size — `h-24 w-full`,
29
+ `h-3.5 w-full`, `size-10` — that a caller's `className` overrides, since
30
+ tailwind-merge sees both in one chain. The defaults exist so an unsized
31
+ skeleton is visible rather than a zero-height nothing.
32
+ - **Children are the content, and they size the placeholder.** A skeleton that
33
+ wraps children renders them underneath at opacity 0 while loading, so the grey
34
+ shape is exactly the size the content will be and **nothing moves when it
35
+ lands**. The shape then contributes only its corner: a default height would
36
+ fight the content, then clip it once shown. Flip `isLoading` off and the fill
37
+ and the clip drop and the content fades in over `SKELETON_REVEAL_MS`. The
38
+ children are never unmounted between the two states, so their own state
39
+ survives the swap. `pointerEvents` is off on them while they are invisible —
40
+ an invisible button that still takes a tap is a trap.
41
+ - **One clock drives both animations.** `useSkeletonClock` loops 0 → 1 every
42
+ `SKELETON_CYCLE_MS`. A shimmer reads it as the band's position
43
+ (`skeletonShimmerOffset`), a pulse as a point on a cosine
44
+ (`skeletonPulseOpacity`). That is what lets a `Skeleton.Group` hold a single
45
+ clock for every skeleton inside it without caring which each one draws.
46
+ - **`Skeleton.Group` exists to keep a region in step.** Separate skeletons
47
+ start their clocks when they mount, so a list whose rows mount a frame apart
48
+ shimmers as a ripple of unrelated glints. Inside a group they read the
49
+ group's clock and move together, and one `isLoading` reveals the whole
50
+ region. A skeleton's own `isLoading` and `animation` still win over the
51
+ group's; it starts a clock of its own only when there is no running group
52
+ clock to share.
53
+ - **The fill is a tint, `bg-muted-foreground/20`, not `bg-muted`.** `muted`
54
+ sits within 0.015 lightness of the light page, and a placeholder drawn in it
55
+ was verified on the simulator to all but vanish there. A fifth of the muted
56
+ foreground lands a clear step off whatever it sits on — page or card, light or
57
+ dark — because it is translucent, and it still leaves headroom above it for
58
+ the glint. `SKELETON_FILL_CLASS` names it and a test pins it to a tint.
59
+ - **The glint is painted in `elevated`, not white.** `elevated` is the raised
60
+ surface in both themes — white in light, a lifted grey in dark — so it sits
61
+ above the tinted fill either way. White would glare on dark; a translucent
62
+ black would darken rather than lighten in light mode. Following the token
63
+ also means a pasted palette carries the glint with it. The band is
64
+ a react-native-svg gradient, transparent at both ends, with low quarter stops
65
+ so it reads as a soft glint rather than a ridge.
66
+ - **The band sweeps from wholly off one edge to wholly off the other.**
67
+ `skeletonShimmerOffset(0)` is `-bandWidth`, `(1)` is the placeholder width, so
68
+ the band enters from nothing and leaves to nothing and the wrap back to 0 has
69
+ no visible jump. Its width is a share of the placeholder, bounded both ways —
70
+ a chip still gets a visible band and a full-bleed card does not get a wash.
71
+ The band is sized by Yoga from its class (`w-3/5 min-w-12 max-w-60`), and
72
+ both widths are read with `measure` inside the animated style, so the skeleton
73
+ never re-renders to animate and each frame writes only `transform` and
74
+ `opacity`. Until both are measured the band is transparent.
75
+ - **The gradient is a unit viewBox stretched over an absolutely filled `Svg`.**
76
+ Found on the simulator: an `Svg` at `width="100%"` kept its first size when
77
+ the band resized, so when raising `Skeleton.Lines`' count turned the old last
78
+ line into a full one, that line's glint sat off-centre and swept visibly out
79
+ of step with the rest. `StyleSheet.absoluteFill` plus
80
+ `preserveAspectRatio="none"` makes the native view follow the band.
81
+ - **Calm motion stills the skeleton, it does not hide it.**
82
+ `resolveSkeletonAnimation` returns `none` while `useCalmMotion()` is true — the
83
+ OS reduce-motion setting, or `DelacourProvider`'s `isMotionCalm` — and the
84
+ clock is never started. The second half is for E2E builds: a test runner that
85
+ waits for the screen to settle before each gesture pays its whole timeout on a
86
+ screen holding a shimmer that never ends. The shape is the message: a placeholder that stops
87
+ moving still reads as content on its way. The clock's own timing sets
88
+ `ReduceMotion.Never` because the decision has already been made one level up —
89
+ under the default `System` policy `withTiming` completes instantly and
90
+ `withRepeat(-1)` would spin a zero-length animation forever, the trap
91
+ [Spinner](../spinner/AGENTS.md) documents too.
92
+ - **The pulse owns `opacity`.** Its animated style goes after the caller's
93
+ `style`, so an `opacity` passed there has no effect while it pulses. The floor
94
+ is `SKELETON_PULSE_MIN_OPACITY`, well above zero — a placeholder that blinks
95
+ out reads as a failing layout, not a loading one.
96
+ - **Hidden from assistive technology unless labelled.** An unlabelled grey box
97
+ announces nothing worth hearing, and a screen of them announces it many times
98
+ over, so the default is `accessibilityElementsHidden` plus
99
+ `importantForAccessibility="no-hide-descendants"`. A `label` turns the
100
+ skeleton into one busy `progressbar` status. Label the one that stands for the
101
+ region — or the `Skeleton.Group`, or the `Skeleton.Lines` paragraph — never
102
+ each line. Once loaded the skeleton contributes no accessibility props at all
103
+ and the content speaks for itself. `resolveSkeletonAccessibility` returns a
104
+ discriminated union (`hidden` / `status` / `content`) so the three cases are
105
+ pinned in `bun test`.
106
+ - **An unlabelled group takes no role.** It may hold real content beside its
107
+ placeholders, and hiding the group would hide that too; each skeleton already
108
+ hides itself.
109
+ - **`Skeleton.Lines` lives in the root file.** It renders `SkeletonRoot`
110
+ directly, and a file of its own would have to import `./skeleton` and close a
111
+ cycle (package rule 3). Only the last line is shortened, and only with two or
112
+ more — a single line is a title whose length the caller already knows. Its
113
+ line keys are positional because lines never reorder. **Outside a group it is
114
+ a `Skeleton.Group` of its own.** Found on the simulator: with a clock per line,
115
+ a line added by raising `lines` started from zero and glinted out of step with
116
+ the rest. Inside a group it is a plain container, since the group's clock
117
+ already serves every line.
118
+ - **A number is not a class here either.** An arbitrary height like `h-[200px]`
119
+ only exists once Tailwind has scanned it; pass a computed dimension through
120
+ `style`, which the root spreads before the pulse.
@@ -0,0 +1,15 @@
1
+ export { Skeleton, type SkeletonLinesProps, type SkeletonProps } from "./skeleton";
2
+ export { type SkeletonGroupContextValue, useSkeletonGroup } from "./skeleton.context";
3
+ export {
4
+ resolveSkeletonAccessibility,
5
+ resolveSkeletonAnimation,
6
+ resolveSkeletonLineWidths,
7
+ SKELETON_ANIMATIONS,
8
+ SKELETON_CYCLE_MS,
9
+ SKELETON_SHAPES,
10
+ type SkeletonAccessibility,
11
+ type SkeletonAnimation,
12
+ type SkeletonShape,
13
+ skeletonVariants,
14
+ } from "./skeleton.variants";
15
+ export type { SkeletonGroupProps } from "./skeleton-group";
@@ -0,0 +1,48 @@
1
+ import { useEffect } from "react";
2
+ import {
3
+ cancelAnimation,
4
+ Easing,
5
+ ReduceMotion,
6
+ type SharedValue,
7
+ useSharedValue,
8
+ withRepeat,
9
+ withTiming,
10
+ } from "react-native-reanimated";
11
+ import { SKELETON_CYCLE_MS } from "./skeleton.variants";
12
+
13
+ /**
14
+ * A 0 → 1 clock that loops on the UI thread while `isRunning`.
15
+ *
16
+ * Both animations read the same clock — a shimmer as the band's position, a
17
+ * pulse as a point on a cosine — which is what lets a `Skeleton.Group` hold one
18
+ * for every placeholder inside it, whatever each of them draws.
19
+ *
20
+ * The timing sets `ReduceMotion.Never` deliberately. Under the default
21
+ * `System` policy `withTiming` completes instantly while the OS setting is on,
22
+ * so `withRepeat(-1)` would spin a zero-length animation forever. Reduce-motion
23
+ * is honoured one level up instead, where `resolveSkeletonAnimation` stills the
24
+ * skeleton and this clock is never started.
25
+ */
26
+ export function useSkeletonClock(isRunning: boolean): SharedValue<number> {
27
+ const progress = useSharedValue(0);
28
+
29
+ useEffect(() => {
30
+ if (!isRunning) {
31
+ cancelAnimation(progress);
32
+ progress.value = 0;
33
+ return;
34
+ }
35
+
36
+ progress.value = 0;
37
+ progress.value = withRepeat(
38
+ withTiming(1, { duration: SKELETON_CYCLE_MS, easing: Easing.linear, reduceMotion: ReduceMotion.Never }),
39
+ -1,
40
+ false
41
+ );
42
+
43
+ // Without this the repeat outlives the unmount when the content lands.
44
+ return () => cancelAnimation(progress);
45
+ }, [isRunning, progress]);
46
+
47
+ return progress;
48
+ }
@@ -0,0 +1,64 @@
1
+ import { type ReactElement, type ReactNode, useMemo } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { useCalmMotion } from "../../hooks/use-calm-motion";
4
+ import { cn } from "../../lib/cn";
5
+ import { type SkeletonGroupContextValue, SkeletonGroupProvider } from "./skeleton.context";
6
+ import { resolveSkeletonAccessibility, resolveSkeletonAnimation, type SkeletonAnimation } from "./skeleton.variants";
7
+ import { useSkeletonClock } from "./skeleton-clock";
8
+
9
+ export type SkeletonGroupProps = Omit<ViewProps, "children"> & {
10
+ /** Whether the group's content is still loading. Every skeleton inside follows it unless told otherwise. Defaults to `true`. */
11
+ isLoading?: boolean;
12
+ /** The animation every skeleton inside runs unless it names its own. Defaults to `shimmer`. */
13
+ animation?: SkeletonAnimation;
14
+ /**
15
+ * What is loading, for a screen reader. Announced once for the whole group, as
16
+ * a busy status, while it loads. Leave the skeletons inside unlabelled.
17
+ */
18
+ label?: string;
19
+ className?: string;
20
+ children?: ReactNode;
21
+ };
22
+
23
+ /**
24
+ * Holds one clock for every skeleton inside it, and one loading flag.
25
+ *
26
+ * Separate placeholders each start their own clock when they mount, so a list
27
+ * whose rows arrive a frame apart shimmers out of step — a ripple of unrelated
28
+ * glints instead of one sweep. Inside a group they all read this clock, so they
29
+ * move together, and flipping one `isLoading` reveals the whole region at once.
30
+ *
31
+ * It lays nothing out; it is a plain `View` for the caller to arrange.
32
+ */
33
+ export function SkeletonGroup({
34
+ isLoading = true,
35
+ animation,
36
+ label,
37
+ className,
38
+ children,
39
+ ...props
40
+ }: SkeletonGroupProps): ReactElement {
41
+ const isCalm = useCalmMotion();
42
+ const resolvedAnimation = resolveSkeletonAnimation(animation, isCalm);
43
+ const isRunning = isLoading && resolvedAnimation !== "none";
44
+ const progress = useSkeletonClock(isRunning);
45
+
46
+ const context = useMemo<SkeletonGroupContextValue>(
47
+ () => ({ animation: resolvedAnimation, isLoading, isRunning, progress }),
48
+ [resolvedAnimation, isLoading, isRunning, progress]
49
+ );
50
+
51
+ // Only a labelled group takes a role. An unlabelled one may hold real
52
+ // content beside its placeholders, and hiding it would hide that too — each
53
+ // skeleton already hides itself.
54
+ const accessibility = label ? resolveSkeletonAccessibility({ isLoading, label }).props : undefined;
55
+
56
+ return (
57
+ <SkeletonGroupProvider value={context}>
58
+ <View className={cn(className)} {...accessibility} {...props}>
59
+ {children}
60
+ </View>
61
+ </SkeletonGroupProvider>
62
+ );
63
+ }
64
+ SkeletonGroup.displayName = "DelacourUI.Skeleton.Group";
@@ -0,0 +1,80 @@
1
+ import { type ReactElement, useId } from "react";
2
+ import { StyleSheet } from "react-native";
3
+ import Animated, {
4
+ type AnimatedRef,
5
+ measure,
6
+ type SharedValue,
7
+ useAnimatedRef,
8
+ useAnimatedStyle,
9
+ } from "react-native-reanimated";
10
+ import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
11
+ import { useThemeColor } from "../../hooks/use-theme-color";
12
+ import { SKELETON_SHIMMER_STOPS, skeletonShimmerOffset, skeletonVariants } from "./skeleton.variants";
13
+
14
+ /** The token the band is painted in — a surface that rises above the placeholder fill in both themes. */
15
+ const SHIMMER_TOKEN = "elevated";
16
+
17
+ /**
18
+ * The glint that sweeps across a loading skeleton.
19
+ *
20
+ * A band as tall as the skeleton and 60% as wide — bounded to 48–240pt by its
21
+ * class — carried from off one edge to off the other by the clock. The root clips
22
+ * it, so a circle's glint stays inside the circle.
23
+ *
24
+ * Painted in `elevated` rather than white or a translucent black. That token is
25
+ * the raised surface in both themes — white in light, a lifted grey in dark —
26
+ * so it lands above the skeleton's tinted fill either way, reads as a
27
+ * highlight, and follows a pasted palette with no support from here.
28
+ *
29
+ * Everything runs on the UI thread, so a skeleton never re-renders to animate.
30
+ */
31
+ export function SkeletonShimmer({
32
+ progress,
33
+ container,
34
+ }: {
35
+ progress: SharedValue<number>;
36
+ container: AnimatedRef<Animated.View>;
37
+ }): ReactElement {
38
+ const color = useThemeColor(SHIMMER_TOKEN);
39
+ const band = useAnimatedRef<Animated.View>();
40
+
41
+ // Gradient ids are global to the document, and React 19's `«r1»`-style ids
42
+ // carry delimiters that are invalid inside `url(#…)`.
43
+ const id = `skeleton-shimmer-${useId().replace(/[^a-zA-Z0-9]/g, "")}`;
44
+
45
+ // Both widths are measured on the UI thread, so the sweep always uses the
46
+ // layout as it is now and nothing round-trips through JavaScript. Only
47
+ // `transform` and `opacity` are written, so every frame takes Reanimated's
48
+ // fast path and never commits layout; Yoga sizes the band from its class.
49
+ const motionStyle = useAnimatedStyle(() => {
50
+ const outer = measure(container);
51
+ const inner = measure(band);
52
+ if (!outer || !inner) return { opacity: 0, transform: [{ translateX: 0 }] };
53
+
54
+ return {
55
+ opacity: 1,
56
+ transform: [{ translateX: skeletonShimmerOffset(progress.value, outer.width, inner.width) }],
57
+ };
58
+ });
59
+
60
+ // The gradient is drawn in a unit viewBox stretched over an absolutely
61
+ // filled Svg, never at `width="100%"`. On the simulator a percentage-sized
62
+ // Svg kept its first size when the band resized — the last line of a
63
+ // paragraph becoming a full one — so its bright centre sat off the band's
64
+ // centre and the line glinted out of step with its neighbours.
65
+ return (
66
+ <Animated.View className={skeletonVariants().band()} pointerEvents="none" ref={band} style={motionStyle}>
67
+ <Svg preserveAspectRatio="none" style={StyleSheet.absoluteFill} viewBox="0 0 1 1">
68
+ <Defs>
69
+ <LinearGradient id={id} x1="0" x2="1" y1="0" y2="0">
70
+ {SKELETON_SHIMMER_STOPS.map((stop) => (
71
+ <Stop key={stop.offset} offset={stop.offset} stopColor={color} stopOpacity={stop.opacity} />
72
+ ))}
73
+ </LinearGradient>
74
+ </Defs>
75
+ <Rect fill={`url(#${id})`} height={1} width={1} x={0} y={0} />
76
+ </Svg>
77
+ </Animated.View>
78
+ );
79
+ }
80
+ SkeletonShimmer.displayName = "DelacourUI.Skeleton.Shimmer";
@@ -0,0 +1,45 @@
1
+ import { createContext, type ReactElement, type ReactNode, use } from "react";
2
+ import type { SharedValue } from "react-native-reanimated";
3
+ import type { SkeletonAnimation } from "./skeleton.variants";
4
+
5
+ export type SkeletonGroupContextValue = {
6
+ /** The group's clock, 0 → 1 once per cycle. Only meaningful while `isRunning`. */
7
+ progress: SharedValue<number>;
8
+ /** Whether the clock is ticking — the group is loading and has an animation to run. */
9
+ isRunning: boolean;
10
+ /** The animation the group resolved, reduce-motion already applied. */
11
+ animation: SkeletonAnimation;
12
+ /** Whether the group's content is still loading. */
13
+ isLoading: boolean;
14
+ };
15
+
16
+ const SkeletonGroupContext = createContext<SkeletonGroupContextValue | null>(null);
17
+
18
+ /**
19
+ * Supplies a `Skeleton.Group`'s clock and loading state to every skeleton in
20
+ * its subtree.
21
+ *
22
+ * Lives in its own module, importing nothing but React and types, so the root
23
+ * and the group can both read it without importing each other. See AGENTS.md
24
+ * rule 3.
25
+ */
26
+ export function SkeletonGroupProvider({
27
+ value,
28
+ children,
29
+ }: {
30
+ value: SkeletonGroupContextValue;
31
+ children: ReactNode;
32
+ }): ReactElement {
33
+ return <SkeletonGroupContext value={value}>{children}</SkeletonGroupContext>;
34
+ }
35
+ SkeletonGroupProvider.displayName = "DelacourUI.Skeleton.GroupProvider";
36
+
37
+ /**
38
+ * The enclosing `Skeleton.Group`, or null outside one.
39
+ *
40
+ * Optional by design: a skeleton is complete on its own, and a group only
41
+ * synchronises the ones it holds.
42
+ */
43
+ export function useSkeletonGroup(): SkeletonGroupContextValue | null {
44
+ return use(SkeletonGroupContext);
45
+ }
@@ -0,0 +1,227 @@
1
+ import { type ReactElement, type ReactNode, useEffect } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import Animated, { useAnimatedRef, useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
4
+ import { useCalmMotion } from "../../hooks/use-calm-motion";
5
+ import { cn } from "../../lib/cn";
6
+ import { useSkeletonGroup } from "./skeleton.context";
7
+ import {
8
+ resolveSkeletonAccessibility,
9
+ resolveSkeletonAnimation,
10
+ resolveSkeletonLineWidths,
11
+ SKELETON_REVEAL_MS,
12
+ type SkeletonAnimation,
13
+ type SkeletonShape,
14
+ skeletonPulseOpacity,
15
+ skeletonVariants,
16
+ } from "./skeleton.variants";
17
+ import { useSkeletonClock } from "./skeleton-clock";
18
+ import { SkeletonGroup } from "./skeleton-group";
19
+ import { SkeletonShimmer } from "./skeleton-shimmer";
20
+
21
+ export type SkeletonProps = Omit<ViewProps, "children"> & {
22
+ /** `rect` (a card or an image), `line` (a line of text) or `circle` (an avatar). Defaults to `rect`. */
23
+ shape?: SkeletonShape;
24
+ /**
25
+ * `shimmer` sweeps a glint across the placeholder, `pulse` breathes its
26
+ * opacity, `none` holds it still. Inherited from an enclosing `Skeleton.Group`,
27
+ * then `shimmer`. Every animation stills under the OS reduce-motion setting, and under `DelacourProvider`'s `isMotionCalm`.
28
+ */
29
+ animation?: SkeletonAnimation;
30
+ /**
31
+ * Whether the content is still loading. Inherited from an enclosing
32
+ * `Skeleton.Group`, then `true`. Flipped off, the placeholder gives way to its
33
+ * children, which fade in.
34
+ */
35
+ isLoading?: boolean;
36
+ /**
37
+ * What is loading, for a screen reader. Setting it makes this skeleton
38
+ * announce as a busy status; leaving it unset keeps the placeholder out of the
39
+ * accessibility tree. Label the one skeleton standing for a region, not all of them.
40
+ */
41
+ label?: string;
42
+ className?: string;
43
+ /**
44
+ * The real content. Rendered invisibly underneath while loading, so the
45
+ * placeholder takes its exact size and nothing moves when it lands.
46
+ */
47
+ children?: ReactNode;
48
+ };
49
+
50
+ function SkeletonRoot({
51
+ shape = "rect",
52
+ animation,
53
+ isLoading,
54
+ label,
55
+ className,
56
+ style,
57
+ children,
58
+ ...props
59
+ }: SkeletonProps): ReactElement {
60
+ const group = useSkeletonGroup();
61
+ const isCalm = useCalmMotion();
62
+
63
+ const loading = isLoading ?? group?.isLoading ?? true;
64
+ const resolvedAnimation = resolveSkeletonAnimation(animation ?? group?.animation, isCalm);
65
+ const isAnimating = loading && resolvedAnimation !== "none";
66
+
67
+ // One clock drives either animation, so a group's clock serves every
68
+ // skeleton inside it whatever each of them draws. A skeleton starts its own
69
+ // only when there is no running group clock to share.
70
+ const groupClock = group?.isRunning ? group.progress : undefined;
71
+ const ownClock = useSkeletonClock(isAnimating && groupClock === undefined);
72
+ const progress = groupClock ?? ownClock;
73
+
74
+ const root = useAnimatedRef<Animated.View>();
75
+ const reveal = useSharedValue(loading ? 0 : 1);
76
+
77
+ useEffect(() => {
78
+ // Hidden at once when loading starts again; faded in when it stops. The
79
+ // default reduce-motion policy makes the fade instant under that setting.
80
+ reveal.value = loading ? 0 : withTiming(1, { duration: SKELETON_REVEAL_MS });
81
+ }, [loading, reveal]);
82
+
83
+ const isPulsing = isAnimating && resolvedAnimation === "pulse";
84
+
85
+ const pulseStyle = useAnimatedStyle(() => ({
86
+ opacity: isPulsing ? skeletonPulseOpacity(progress.value) : 1,
87
+ }));
88
+
89
+ const revealStyle = useAnimatedStyle(() => ({ opacity: reveal.value }));
90
+
91
+ const hasChildren = children !== undefined && children !== null && children !== false;
92
+ const slots = skeletonVariants({ isEmpty: !hasChildren, isLoading: loading, shape });
93
+ const accessibility = resolveSkeletonAccessibility({ isLoading: loading, label });
94
+
95
+ // The pulse owns opacity, so its style goes after the caller's.
96
+ return (
97
+ <Animated.View
98
+ className={slots.root({ className })}
99
+ ref={root}
100
+ style={[style, pulseStyle]}
101
+ {...accessibility.props}
102
+ {...props}
103
+ >
104
+ {hasChildren ? (
105
+ <Animated.View className={slots.content()} pointerEvents={loading ? "none" : "auto"} style={revealStyle}>
106
+ {children}
107
+ </Animated.View>
108
+ ) : null}
109
+ {isAnimating && resolvedAnimation === "shimmer" ? <SkeletonShimmer container={root} progress={progress} /> : null}
110
+ </Animated.View>
111
+ );
112
+ }
113
+
114
+ export type SkeletonLinesProps = Omit<SkeletonProps, "shape" | "children" | "style"> & {
115
+ /** How many lines to draw. Defaults to 3. */
116
+ lines?: number;
117
+ /** How far across the last line runs, 0–1. Only applies with two or more lines. Defaults to 0.6. */
118
+ lastLineWidth?: number;
119
+ /** Classes for each line — a height to match the text it stands in for, say. */
120
+ lineClassName?: string;
121
+ /** The paragraph itself, shown in place of the lines once loaded. */
122
+ children?: ReactNode;
123
+ };
124
+
125
+ /**
126
+ * A paragraph of placeholder lines, the last one shortened the way prose ends.
127
+ *
128
+ * Lives in the root file because it composes `SkeletonRoot` directly — a file of
129
+ * its own would have to import `./skeleton` and close a cycle. Its lines read
130
+ * the enclosing group's clock, or one the paragraph holds itself when there is
131
+ * no group, so they always glint in step. The label goes on the paragraph,
132
+ * never on each line.
133
+ */
134
+ function SkeletonLines({
135
+ lines = 3,
136
+ lastLineWidth,
137
+ lineClassName,
138
+ animation,
139
+ isLoading,
140
+ label,
141
+ className,
142
+ children,
143
+ ...props
144
+ }: SkeletonLinesProps): ReactElement {
145
+ const group = useSkeletonGroup();
146
+ const loading = isLoading ?? group?.isLoading ?? true;
147
+
148
+ if (!loading) {
149
+ return (
150
+ <View className={className} {...props}>
151
+ {children}
152
+ </View>
153
+ );
154
+ }
155
+
156
+ const widths = resolveSkeletonLineWidths(lines, lastLineWidth);
157
+ const containerClassName = cn("w-full gap-2", className);
158
+
159
+ // Lines are positional and never reorder, so the index is their identity.
160
+ const content = widths.map((width, index) => (
161
+ <SkeletonRoot
162
+ animation={animation}
163
+ className={lineClassName}
164
+ isLoading
165
+ key={`line-${index.toString()}`}
166
+ shape="line"
167
+ style={{ width }}
168
+ />
169
+ ));
170
+
171
+ // Inside a group the lines already share its clock, so the paragraph is a
172
+ // plain container. Alone, it becomes a group of its own — otherwise a line
173
+ // added later starts a clock of its own and glints out of step with the rest.
174
+ if (group) {
175
+ const accessibility = resolveSkeletonAccessibility({ isLoading: true, label });
176
+ return (
177
+ <View className={containerClassName} {...accessibility.props} {...props}>
178
+ {content}
179
+ </View>
180
+ );
181
+ }
182
+
183
+ return (
184
+ <SkeletonGroup animation={animation} className={containerClassName} label={label} {...props}>
185
+ {content}
186
+ </SkeletonGroup>
187
+ );
188
+ }
189
+ SkeletonLines.displayName = "DelacourUI.Skeleton.Lines";
190
+
191
+ /**
192
+ * A placeholder standing in for content that is still loading.
193
+ *
194
+ * Drawn as a tinted shape — `rect`, `line` or `circle` — with a glint of
195
+ * `elevated` sweeping across it on the UI thread, or a pulse of its opacity.
196
+ * Under the OS reduce-motion setting it holds still: the shape is the message.
197
+ *
198
+ * Give it the size of the content it replaces, or pass that content as children
199
+ * and let it take the size itself — the content is rendered invisibly
200
+ * underneath, so nothing moves when `isLoading` flips off and it fades in.
201
+ *
202
+ * Hidden from screen readers unless given a `label`; label the one skeleton
203
+ * that stands for a region and leave the rest silent.
204
+ *
205
+ * @example
206
+ * <View className="flex-row items-center gap-3">
207
+ * <Skeleton shape="circle" />
208
+ * <Skeleton.Lines className="flex-1" lines={2} />
209
+ * </View>
210
+ *
211
+ * @example
212
+ * <Skeleton isLoading={!user} shape="circle">
213
+ * <Avatar user={user} />
214
+ * </Skeleton>
215
+ *
216
+ * @example
217
+ * <Skeleton.Group isLoading={isFetching} label="Loading inbox">
218
+ * {rows.map((row) => <Row key={row.id} row={row} />)}
219
+ * </Skeleton.Group>
220
+ */
221
+ export const Skeleton = Object.assign(SkeletonRoot, {
222
+ /** One clock and one loading flag for every skeleton inside — they shimmer in step and reveal together. */
223
+ Group: SkeletonGroup,
224
+ /** A paragraph of placeholder lines, the last one shortened. */
225
+ Lines: SkeletonLines,
226
+ displayName: "DelacourUI.Skeleton",
227
+ });