react-native-terra-ui 0.1.0 → 0.2.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 (304) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/lib/module/components/avatar/Avatar.js +118 -35
  3. package/lib/module/components/avatar/Avatar.js.map +1 -1
  4. package/lib/module/components/avatar/index.js.map +1 -1
  5. package/lib/module/components/box/Box.js +15 -2
  6. package/lib/module/components/box/Box.js.map +1 -1
  7. package/lib/module/components/box/index.js.map +1 -1
  8. package/lib/module/components/box/utils.js +5 -2
  9. package/lib/module/components/box/utils.js.map +1 -1
  10. package/lib/module/components/button/Button.js +7 -14
  11. package/lib/module/components/button/Button.js.map +1 -1
  12. package/lib/module/components/button/index.js.map +1 -1
  13. package/lib/module/components/chip/Chip.js +372 -0
  14. package/lib/module/components/chip/Chip.js.map +1 -0
  15. package/lib/module/components/chip/index.js +4 -0
  16. package/lib/module/components/chip/index.js.map +1 -0
  17. package/lib/module/components/chip/types.js +2 -0
  18. package/lib/module/components/chip/types.js.map +1 -0
  19. package/lib/module/components/chip/utils.js +36 -0
  20. package/lib/module/components/chip/utils.js.map +1 -0
  21. package/lib/module/components/empty-state/EmptyState.js +200 -0
  22. package/lib/module/components/empty-state/EmptyState.js.map +1 -0
  23. package/lib/module/components/empty-state/index.js +4 -0
  24. package/lib/module/components/empty-state/index.js.map +1 -0
  25. package/lib/module/components/empty-state/types.js +2 -0
  26. package/lib/module/components/empty-state/types.js.map +1 -0
  27. package/lib/module/components/header/Header.js.map +1 -1
  28. package/lib/module/components/header/header-buttons.js +31 -36
  29. package/lib/module/components/header/header-buttons.js.map +1 -1
  30. package/lib/module/components/header/index.js +1 -1
  31. package/lib/module/components/header/index.js.map +1 -1
  32. package/lib/module/components/header/parts/HeaderGradientOverlay.js +38 -0
  33. package/lib/module/components/header/parts/HeaderGradientOverlay.js.map +1 -0
  34. package/lib/module/components/header/variants/LargeTitleHeader/index.js +71 -101
  35. package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
  36. package/lib/module/components/header/variants/TitleHeader/index.js +34 -65
  37. package/lib/module/components/header/variants/TitleHeader/index.js.map +1 -1
  38. package/lib/module/components/icon/Icon.js +21 -9
  39. package/lib/module/components/icon/Icon.js.map +1 -1
  40. package/lib/module/components/icon/index.js.map +1 -1
  41. package/lib/module/components/image/Image.js +54 -0
  42. package/lib/module/components/image/Image.js.map +1 -0
  43. package/lib/module/components/image/index.js +4 -0
  44. package/lib/module/components/image/index.js.map +1 -0
  45. package/lib/module/components/image/types.js +4 -0
  46. package/lib/module/components/image/types.js.map +1 -0
  47. package/lib/module/components/index.js +4 -0
  48. package/lib/module/components/index.js.map +1 -1
  49. package/lib/module/components/page-indicator/PageIndicator.js.map +1 -1
  50. package/lib/module/components/page-indicator/PageIndicatorRoot.js.map +1 -1
  51. package/lib/module/components/page-indicator/hooks/use-page-indicator-colors.js +1 -1
  52. package/lib/module/components/page-indicator/hooks/use-page-indicator-colors.js.map +1 -1
  53. package/lib/module/components/page-indicator/hooks/use-page-indicator-core.js.map +1 -1
  54. package/lib/module/components/page-indicator/hooks/use-page-indicator-progress.js.map +1 -1
  55. package/lib/module/components/page-indicator/index.js.map +1 -1
  56. package/lib/module/components/page-indicator/variants/DotIndicator/InactiveDot.js.map +1 -1
  57. package/lib/module/components/page-indicator/variants/DotIndicator/LoadingOrbit.js.map +1 -1
  58. package/lib/module/components/page-indicator/variants/DotIndicator/TravelingPill.js.map +1 -1
  59. package/lib/module/components/page-indicator/variants/DotIndicator/index.js.map +1 -1
  60. package/lib/module/components/page-indicator/variants/PillIndicator/PillIndicatorDot.js.map +1 -1
  61. package/lib/module/components/page-indicator/variants/PillIndicator/index.js.map +1 -1
  62. package/lib/module/components/portal/Portal.js +42 -0
  63. package/lib/module/components/portal/Portal.js.map +1 -0
  64. package/lib/module/components/portal/PortalContext.js +1 -16
  65. package/lib/module/components/portal/PortalContext.js.map +1 -1
  66. package/lib/module/components/portal/PortalHost.js +16 -14
  67. package/lib/module/components/portal/PortalHost.js.map +1 -1
  68. package/lib/module/components/portal/PortalProvider.js +51 -19
  69. package/lib/module/components/portal/PortalProvider.js.map +1 -1
  70. package/lib/module/components/portal/index.js +1 -1
  71. package/lib/module/components/portal/index.js.map +1 -1
  72. package/lib/module/components/screen/Screen.js +26 -29
  73. package/lib/module/components/screen/Screen.js.map +1 -1
  74. package/lib/module/components/screen/ScreenContext.js +7 -5
  75. package/lib/module/components/screen/ScreenContext.js.map +1 -1
  76. package/lib/module/components/screen/ScreenFlashList.js +144 -0
  77. package/lib/module/components/screen/ScreenFlashList.js.map +1 -0
  78. package/lib/module/components/screen/ScreenFlatList.js +88 -27
  79. package/lib/module/components/screen/ScreenFlatList.js.map +1 -1
  80. package/lib/module/components/screen/ScreenScrollView.js +101 -34
  81. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  82. package/lib/module/components/screen/index.js +1 -0
  83. package/lib/module/components/screen/index.js.map +1 -1
  84. package/lib/module/components/screen/parts/BottomSafeArea.js +62 -0
  85. package/lib/module/components/screen/parts/BottomSafeArea.js.map +1 -0
  86. package/lib/module/components/screen/parts/ScreenPlaceholder.js +50 -0
  87. package/lib/module/components/screen/parts/ScreenPlaceholder.js.map +1 -0
  88. package/lib/module/components/screen/types.js +4 -0
  89. package/lib/module/components/screen/types.js.map +1 -0
  90. package/lib/module/components/screen/utils.js +12 -9
  91. package/lib/module/components/screen/utils.js.map +1 -1
  92. package/lib/module/components/slot/Slot.js.map +1 -1
  93. package/lib/module/components/slot/index.js.map +1 -1
  94. package/lib/module/components/spinner/Spinner.js +1 -1
  95. package/lib/module/components/spinner/Spinner.js.map +1 -1
  96. package/lib/module/components/spinner/index.js.map +1 -1
  97. package/lib/module/components/surface/Surface.js +16 -9
  98. package/lib/module/components/surface/Surface.js.map +1 -1
  99. package/lib/module/components/surface/index.js.map +1 -1
  100. package/lib/module/components/text/Text.js +25 -7
  101. package/lib/module/components/text/Text.js.map +1 -1
  102. package/lib/module/components/text/index.js.map +1 -1
  103. package/lib/module/components/toast/Toast.js +8 -8
  104. package/lib/module/components/toast/Toast.js.map +1 -1
  105. package/lib/module/components/toast/ToastProvider.js.map +1 -1
  106. package/lib/module/components/toast/index.js.map +1 -1
  107. package/lib/module/components/toolbar/Toolbar.js +268 -0
  108. package/lib/module/components/toolbar/Toolbar.js.map +1 -0
  109. package/lib/module/components/toolbar/index.js +4 -0
  110. package/lib/module/components/toolbar/index.js.map +1 -0
  111. package/lib/module/context/ThemeProvider.js +5 -2
  112. package/lib/module/context/ThemeProvider.js.map +1 -1
  113. package/lib/module/context/index.js.map +1 -1
  114. package/lib/module/index.js.map +1 -1
  115. package/lib/module/{components/icon/utils.js → theme/defaults/icons.js} +1 -1
  116. package/lib/module/theme/defaults/icons.js.map +1 -0
  117. package/lib/module/theme/defaults/image.js +30 -0
  118. package/lib/module/theme/defaults/image.js.map +1 -0
  119. package/lib/module/theme/index.js +6 -6
  120. package/lib/module/theme/index.js.map +1 -1
  121. package/lib/module/theme/{configure.js → registry.js} +23 -30
  122. package/lib/module/theme/registry.js.map +1 -0
  123. package/lib/module/theme/runtime.js +1 -1
  124. package/lib/module/theme/runtime.js.map +1 -1
  125. package/lib/module/theme/theme.js +92 -11
  126. package/lib/module/theme/theme.js.map +1 -1
  127. package/lib/module/theme/tokens/dark.js +59 -68
  128. package/lib/module/theme/tokens/dark.js.map +1 -1
  129. package/lib/module/theme/tokens/light.js +49 -67
  130. package/lib/module/theme/tokens/light.js.map +1 -1
  131. package/lib/module/theme/tokens/primitives.js +241 -20
  132. package/lib/module/theme/tokens/primitives.js.map +1 -1
  133. package/lib/module/utils/accent-utils.js +4 -4
  134. package/lib/module/utils/accent-utils.js.map +1 -1
  135. package/lib/typescript/src/components/avatar/Avatar.d.ts.map +1 -1
  136. package/lib/typescript/src/components/avatar/types.d.ts +5 -5
  137. package/lib/typescript/src/components/avatar/types.d.ts.map +1 -1
  138. package/lib/typescript/src/components/box/Box.d.ts.map +1 -1
  139. package/lib/typescript/src/components/box/utils.d.ts +6 -1
  140. package/lib/typescript/src/components/box/utils.d.ts.map +1 -1
  141. package/lib/typescript/src/components/button/Button.d.ts +2 -2
  142. package/lib/typescript/src/components/button/Button.d.ts.map +1 -1
  143. package/lib/typescript/src/components/chip/Chip.d.ts +24 -0
  144. package/lib/typescript/src/components/chip/Chip.d.ts.map +1 -0
  145. package/lib/typescript/src/components/chip/index.d.ts +4 -0
  146. package/lib/typescript/src/components/chip/index.d.ts.map +1 -0
  147. package/lib/typescript/src/components/chip/types.d.ts +8 -0
  148. package/lib/typescript/src/components/chip/types.d.ts.map +1 -0
  149. package/lib/typescript/src/components/chip/utils.d.ts +8 -0
  150. package/lib/typescript/src/components/chip/utils.d.ts.map +1 -0
  151. package/lib/typescript/src/components/empty-state/EmptyState.d.ts +45 -0
  152. package/lib/typescript/src/components/empty-state/EmptyState.d.ts.map +1 -0
  153. package/lib/typescript/src/components/empty-state/index.d.ts +4 -0
  154. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -0
  155. package/lib/typescript/src/components/empty-state/types.d.ts +6 -0
  156. package/lib/typescript/src/components/empty-state/types.d.ts.map +1 -0
  157. package/lib/typescript/src/components/header/header-buttons.d.ts +27 -0
  158. package/lib/typescript/src/components/header/header-buttons.d.ts.map +1 -1
  159. package/lib/typescript/src/components/header/index.d.ts +1 -1
  160. package/lib/typescript/src/components/header/index.d.ts.map +1 -1
  161. package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts +7 -0
  162. package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts.map +1 -0
  163. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +1 -1
  164. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
  165. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts +19 -8
  166. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts.map +1 -1
  167. package/lib/typescript/src/components/icon/Icon.d.ts +1 -1
  168. package/lib/typescript/src/components/icon/Icon.d.ts.map +1 -1
  169. package/lib/typescript/src/components/image/Image.d.ts +16 -0
  170. package/lib/typescript/src/components/image/Image.d.ts.map +1 -0
  171. package/lib/typescript/src/components/image/index.d.ts +3 -0
  172. package/lib/typescript/src/components/image/index.d.ts.map +1 -0
  173. package/lib/typescript/src/components/image/types.d.ts +7 -0
  174. package/lib/typescript/src/components/image/types.d.ts.map +1 -0
  175. package/lib/typescript/src/components/index.d.ts +4 -0
  176. package/lib/typescript/src/components/index.d.ts.map +1 -1
  177. package/lib/typescript/src/components/page-indicator/PageIndicator.d.ts +1 -1
  178. package/lib/typescript/src/components/page-indicator/hooks/use-page-indicator-colors.d.ts +1 -1
  179. package/lib/typescript/src/components/page-indicator/variants/DotIndicator/index.d.ts +1 -1
  180. package/lib/typescript/src/components/page-indicator/variants/PillIndicator/index.d.ts +1 -1
  181. package/lib/typescript/src/components/portal/Portal.d.ts +26 -0
  182. package/lib/typescript/src/components/portal/Portal.d.ts.map +1 -0
  183. package/lib/typescript/src/components/portal/PortalContext.d.ts +11 -14
  184. package/lib/typescript/src/components/portal/PortalContext.d.ts.map +1 -1
  185. package/lib/typescript/src/components/portal/PortalHost.d.ts +9 -4
  186. package/lib/typescript/src/components/portal/PortalHost.d.ts.map +1 -1
  187. package/lib/typescript/src/components/portal/PortalProvider.d.ts +3 -3
  188. package/lib/typescript/src/components/portal/PortalProvider.d.ts.map +1 -1
  189. package/lib/typescript/src/components/portal/index.d.ts +4 -2
  190. package/lib/typescript/src/components/portal/index.d.ts.map +1 -1
  191. package/lib/typescript/src/components/screen/Screen.d.ts +19 -14
  192. package/lib/typescript/src/components/screen/Screen.d.ts.map +1 -1
  193. package/lib/typescript/src/components/screen/ScreenContext.d.ts +12 -9
  194. package/lib/typescript/src/components/screen/ScreenContext.d.ts.map +1 -1
  195. package/lib/typescript/src/components/screen/ScreenFlashList.d.ts +43 -0
  196. package/lib/typescript/src/components/screen/ScreenFlashList.d.ts.map +1 -0
  197. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts +17 -7
  198. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts.map +1 -1
  199. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts +51 -16
  200. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  201. package/lib/typescript/src/components/screen/index.d.ts +3 -0
  202. package/lib/typescript/src/components/screen/index.d.ts.map +1 -1
  203. package/lib/typescript/src/components/screen/parts/BottomSafeArea.d.ts +30 -0
  204. package/lib/typescript/src/components/screen/parts/BottomSafeArea.d.ts.map +1 -0
  205. package/lib/typescript/src/components/screen/parts/ScreenPlaceholder.d.ts +22 -0
  206. package/lib/typescript/src/components/screen/parts/ScreenPlaceholder.d.ts.map +1 -0
  207. package/lib/typescript/src/components/screen/types.d.ts +42 -0
  208. package/lib/typescript/src/components/screen/types.d.ts.map +1 -0
  209. package/lib/typescript/src/components/screen/utils.d.ts +9 -9
  210. package/lib/typescript/src/components/screen/utils.d.ts.map +1 -1
  211. package/lib/typescript/src/components/spinner/Spinner.d.ts +1 -1
  212. package/lib/typescript/src/components/surface/Surface.d.ts +8 -1
  213. package/lib/typescript/src/components/surface/Surface.d.ts.map +1 -1
  214. package/lib/typescript/src/components/text/Text.d.ts +1 -1
  215. package/lib/typescript/src/components/text/Text.d.ts.map +1 -1
  216. package/lib/typescript/src/components/toast/Toast.d.ts +1 -1
  217. package/lib/typescript/src/components/toolbar/Toolbar.d.ts +115 -0
  218. package/lib/typescript/src/components/toolbar/Toolbar.d.ts.map +1 -0
  219. package/lib/typescript/src/components/toolbar/index.d.ts +3 -0
  220. package/lib/typescript/src/components/toolbar/index.d.ts.map +1 -0
  221. package/lib/typescript/src/context/ThemeProvider.d.ts.map +1 -1
  222. package/lib/typescript/src/{components/icon/utils.d.ts → theme/defaults/icons.d.ts} +2 -2
  223. package/lib/typescript/src/theme/defaults/icons.d.ts.map +1 -0
  224. package/lib/typescript/src/theme/defaults/image.d.ts +4 -0
  225. package/lib/typescript/src/theme/defaults/image.d.ts.map +1 -0
  226. package/lib/typescript/src/theme/index.d.ts +7 -7
  227. package/lib/typescript/src/theme/index.d.ts.map +1 -1
  228. package/lib/typescript/src/theme/{configure.d.ts → registry.d.ts} +1 -1
  229. package/lib/typescript/src/theme/registry.d.ts.map +1 -0
  230. package/lib/typescript/src/theme/theme.d.ts +21 -1
  231. package/lib/typescript/src/theme/theme.d.ts.map +1 -1
  232. package/lib/typescript/src/theme/tokens/dark.d.ts +41 -64
  233. package/lib/typescript/src/theme/tokens/dark.d.ts.map +1 -1
  234. package/lib/typescript/src/theme/tokens/light.d.ts +44 -65
  235. package/lib/typescript/src/theme/tokens/light.d.ts.map +1 -1
  236. package/lib/typescript/src/theme/tokens/primitives.d.ts +228 -17
  237. package/lib/typescript/src/theme/tokens/primitives.d.ts.map +1 -1
  238. package/lib/typescript/src/theme/types.d.ts +66 -36
  239. package/lib/typescript/src/theme/types.d.ts.map +1 -1
  240. package/lib/typescript/src/utils/accent-utils.d.ts +1 -1
  241. package/lib/typescript/src/utils/resolve-theme-color.d.ts +1 -1
  242. package/lib/typescript/src/utils/typography.d.ts +1 -1
  243. package/package.json +28 -12
  244. package/skills/terra-ui/SKILL.md +193 -0
  245. package/skills/terra-ui/references/recipes.md +198 -0
  246. package/skills/terra-ui/references/troubleshooting.md +112 -0
  247. package/src/components/avatar/Avatar.tsx +128 -38
  248. package/src/components/avatar/types.ts +5 -5
  249. package/src/components/box/Box.tsx +13 -1
  250. package/src/components/box/utils.ts +10 -0
  251. package/src/components/button/Button.tsx +7 -15
  252. package/src/components/chip/Chip.tsx +444 -0
  253. package/src/components/chip/index.ts +3 -0
  254. package/src/components/chip/types.ts +15 -0
  255. package/src/components/chip/utils.ts +34 -0
  256. package/src/components/empty-state/EmptyState.tsx +209 -0
  257. package/src/components/empty-state/index.ts +3 -0
  258. package/src/components/empty-state/types.ts +5 -0
  259. package/src/components/header/header-buttons.tsx +33 -45
  260. package/src/components/header/index.ts +2 -0
  261. package/src/components/header/parts/HeaderGradientOverlay.tsx +29 -0
  262. package/src/components/header/variants/LargeTitleHeader/index.tsx +68 -102
  263. package/src/components/header/variants/TitleHeader/index.tsx +47 -56
  264. package/src/components/icon/Icon.tsx +20 -15
  265. package/src/components/image/Image.tsx +48 -0
  266. package/src/components/image/index.ts +2 -0
  267. package/src/components/image/types.ts +7 -0
  268. package/src/components/index.ts +4 -0
  269. package/src/components/portal/Portal.tsx +55 -0
  270. package/src/components/portal/PortalContext.tsx +14 -22
  271. package/src/components/portal/PortalHost.tsx +17 -10
  272. package/src/components/portal/PortalProvider.tsx +58 -21
  273. package/src/components/portal/index.ts +4 -2
  274. package/src/components/screen/Screen.tsx +45 -59
  275. package/src/components/screen/ScreenContext.tsx +19 -12
  276. package/src/components/screen/ScreenFlashList.tsx +196 -0
  277. package/src/components/screen/ScreenFlatList.tsx +100 -39
  278. package/src/components/screen/ScreenScrollView.tsx +114 -53
  279. package/src/components/screen/index.ts +3 -0
  280. package/src/components/screen/parts/BottomSafeArea.tsx +60 -0
  281. package/src/components/screen/parts/ScreenPlaceholder.tsx +55 -0
  282. package/src/components/screen/types.ts +43 -0
  283. package/src/components/screen/utils.ts +29 -15
  284. package/src/components/surface/Surface.tsx +17 -20
  285. package/src/components/text/Text.tsx +23 -5
  286. package/src/components/toast/Toast.tsx +8 -8
  287. package/src/components/toolbar/Toolbar.tsx +353 -0
  288. package/src/components/toolbar/index.ts +6 -0
  289. package/src/context/ThemeProvider.tsx +5 -4
  290. package/src/{components/icon/utils.tsx → theme/defaults/icons.tsx} +1 -1
  291. package/src/theme/defaults/image.tsx +34 -0
  292. package/src/theme/index.ts +1 -1
  293. package/src/theme/{configure.ts → registry.ts} +19 -34
  294. package/src/theme/runtime.ts +1 -1
  295. package/src/theme/theme.ts +127 -13
  296. package/src/theme/tokens/dark.ts +60 -69
  297. package/src/theme/tokens/light.ts +50 -68
  298. package/src/theme/tokens/primitives.ts +248 -20
  299. package/src/theme/types.ts +67 -35
  300. package/src/utils/accent-utils.ts +4 -4
  301. package/lib/module/components/icon/utils.js.map +0 -1
  302. package/lib/module/theme/configure.js.map +0 -1
  303. package/lib/typescript/src/components/icon/utils.d.ts.map +0 -1
  304. package/lib/typescript/src/theme/configure.d.ts.map +0 -1
@@ -0,0 +1,196 @@
1
+ import { FlashList, type FlashListProps } from '@shopify/flash-list';
2
+ import type { ComponentType } from 'react';
3
+ import {
4
+ type NativeScrollEvent,
5
+ type NativeSyntheticEvent,
6
+ View,
7
+ } from 'react-native';
8
+ import Animated, {
9
+ useAnimatedScrollHandler,
10
+ useComposedEventHandler,
11
+ } from 'react-native-reanimated';
12
+
13
+ import { StyleSheet } from 'react-native-unistyles';
14
+
15
+ import { PortalHost } from '../portal';
16
+ import { BottomSafeArea } from './parts/BottomSafeArea';
17
+ import { renderScreenPlaceholder } from './parts/ScreenPlaceholder';
18
+ import { SCREEN_HEADER_PORTAL_HOST, useScreen } from './ScreenContext';
19
+ import type { ScreenMargins, ScreenScrollableProps } from './types';
20
+ import { hasXMargin, hasYMargin, renderListHeader } from './utils';
21
+
22
+ const AnimatedFlashList = Animated.createAnimatedComponent(
23
+ FlashList as unknown as ComponentType<FlashListProps<unknown>>
24
+ );
25
+
26
+ export interface ScreenFlashListProps<T>
27
+ extends Omit<FlashListProps<T>, 'horizontal'>,
28
+ ScreenScrollableProps {
29
+ /**
30
+ * Extra bottom padding on the list content — clearance for a tab bar or home
31
+ * indicator. Defaults to the `layout.screen.margin.y` token when margins are
32
+ * enabled, otherwise `0`.
33
+ */
34
+ bottomInset?: number;
35
+ }
36
+
37
+ /**
38
+ * `FlashList` variant of `Screen.ScrollView`: mirrors scroll offset into the
39
+ * screen's shared `scrollY`, hosts the portal target above the list, and
40
+ * applies token-driven content padding.
41
+ *
42
+ * Requires `@shopify/flash-list` — it is a peer dependency of
43
+ * `react-native-terra-ui` and must be installed in the consuming app.
44
+ *
45
+ * Note: unlike `Screen.FlatList`, this does not bind the screen's shared
46
+ * imperative `scrollRef` — `FlashList`'s ref exposes a different (non
47
+ * Reanimated-compatible) imperative API.
48
+ *
49
+ * Shares `loading` / `LoadingComponent` / `EmptyComponent` / `margins` /
50
+ * `horizontal` with `Screen.ScrollView`, so a screen can swap one container for
51
+ * another without relearning its API.
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * <Screen.FlashList data={items} renderItem={({ item }) => <Row item={item} />} />
56
+ * ```
57
+ *
58
+ * @example Loading and empty placeholders
59
+ * ```tsx
60
+ * <Screen.FlashList
61
+ * data={items}
62
+ * loading={isLoading}
63
+ * EmptyComponent={<EmptyState icon="status.info" title="Nothing yet" fill />}
64
+ * renderItem={({ item }) => <Row item={item} />}
65
+ * />
66
+ * ```
67
+ */
68
+ export function ScreenFlashList<T>({
69
+ data,
70
+ style,
71
+ contentContainerStyle,
72
+ ListHeaderComponent,
73
+ ListFooterComponent,
74
+ ListEmptyComponent,
75
+ horizontal = false,
76
+ loading = false,
77
+ LoadingComponent,
78
+ EmptyComponent,
79
+ margins,
80
+ bottomInset,
81
+ onScroll,
82
+ ...rest
83
+ }: ScreenFlashListProps<T>) {
84
+ if (FlashList == null) {
85
+ throw new Error(
86
+ '[react-native-terra-ui] Screen.FlashList requires "@shopify/flash-list" to be installed in your app. Run `yarn add @shopify/flash-list` (or the npm/pnpm equivalent) and rebuild.'
87
+ );
88
+ }
89
+
90
+ const {
91
+ background,
92
+ scrollHandler,
93
+ headerSnapOffsets,
94
+ margins: screenMargins,
95
+ hasHeader,
96
+ } = useScreen();
97
+
98
+ const resolvedMargins = margins ?? screenMargins;
99
+ // The placeholder rides in on `ListEmptyComponent` rather than replacing the
100
+ // list, so the header portal and snap offsets stay put. While loading the
101
+ // list is fed an empty `data` so that slot is the one that renders.
102
+ const placeholder = renderScreenPlaceholder({
103
+ loading,
104
+ LoadingComponent,
105
+ EmptyComponent,
106
+ isEmpty: !data || data.length === 0,
107
+ });
108
+
109
+ const jsScrollHandler = useAnimatedScrollHandler(
110
+ (event) => {
111
+ if (onScroll)
112
+ onScroll(event as unknown as NativeSyntheticEvent<NativeScrollEvent>);
113
+ },
114
+ [onScroll]
115
+ );
116
+
117
+ const composedHandler = useComposedEventHandler([
118
+ scrollHandler,
119
+ jsScrollHandler,
120
+ ]);
121
+
122
+ // `AnimatedFlashList` is typed for `unknown` items, so the remaining props
123
+ // are cast back to its prop type — minus the ones set explicitly below,
124
+ // which the spread would otherwise re-supply.
125
+ const listProps = rest as Omit<FlashListProps<unknown>, 'data' | 'onScroll'>;
126
+
127
+ // A caller naming its own `bottomInset` has taken the bottom over entirely,
128
+ // and a horizontal list has no bottom edge to clear.
129
+ const needsBottomSafeArea = bottomInset === undefined && !horizontal;
130
+
131
+ const footerComponent = (
132
+ <>
133
+ {renderListHeader(ListFooterComponent)}
134
+ {needsBottomSafeArea ? <BottomSafeArea /> : null}
135
+ </>
136
+ );
137
+
138
+ const headerComponent = (
139
+ <View style={styles.headerWrapper(resolvedMargins)}>
140
+ <PortalHost name={SCREEN_HEADER_PORTAL_HOST} />
141
+ {renderListHeader(ListHeaderComponent)}
142
+ </View>
143
+ );
144
+
145
+ return (
146
+ <AnimatedFlashList
147
+ data={loading ? [] : (data as readonly unknown[] | null | undefined)}
148
+ style={[{ backgroundColor: background }, style]}
149
+ horizontal={horizontal}
150
+ contentInsetAdjustmentBehavior={hasHeader ? 'never' : undefined}
151
+ contentContainerStyle={[
152
+ styles.content(resolvedMargins, hasHeader, bottomInset),
153
+ contentContainerStyle,
154
+ ]}
155
+ ListHeaderComponent={headerComponent}
156
+ ListFooterComponent={footerComponent}
157
+ ListEmptyComponent={placeholder ?? ListEmptyComponent}
158
+ showsVerticalScrollIndicator={false}
159
+ scrollEventThrottle={16}
160
+ snapToOffsets={headerSnapOffsets}
161
+ {...listProps}
162
+ onScroll={composedHandler}
163
+ />
164
+ );
165
+ }
166
+
167
+ const styles = StyleSheet.create((theme, runtime) => ({
168
+ content: (
169
+ margins: ScreenMargins,
170
+ hasHeader: boolean,
171
+ bottomInset: number | undefined
172
+ ) => {
173
+ return {
174
+ flexGrow: 1,
175
+ // The header is absolutely positioned, so the list reserves its full
176
+ // height — bar plus top safe area. This assumes the scroll view does not
177
+ // also get UIKit's automatic inset adjustment: on a native tab screen
178
+ // that means `disableAutomaticContentInsets` on the tab, or the two
179
+ // insets are counted twice.
180
+ paddingTop:
181
+ (hasHeader ? theme.layout.header.height + runtime.insets.top : 0) +
182
+ (hasYMargin(margins) ? theme.layout.screen.margin.y : 0),
183
+ paddingHorizontal: hasXMargin(margins) ? theme.layout.screen.margin.x : 0,
184
+ // The safe area is `BottomSafeArea`'s job; counting it here as well
185
+ // would leave a gap the height of the gesture bar.
186
+ paddingBottom:
187
+ bottomInset ?? (hasYMargin(margins) ? theme.layout.screen.margin.y : 0),
188
+ };
189
+ },
190
+ safeArea: {
191
+ height: runtime.insets.top,
192
+ },
193
+ headerWrapper: (margins: ScreenMargins) => ({
194
+ marginHorizontal: hasXMargin(margins) ? -theme.layout.screen.margin.x : 0,
195
+ }),
196
+ }));
@@ -1,9 +1,3 @@
1
- import {
2
- createElement,
3
- type FunctionComponent,
4
- isValidElement,
5
- type ReactElement,
6
- } from 'react';
7
1
  import {
8
2
  type FlatListProps,
9
3
  type NativeScrollEvent,
@@ -16,20 +10,18 @@ import Animated, {
16
10
  useAnimatedScrollHandler,
17
11
  useComposedEventHandler,
18
12
  } from 'react-native-reanimated';
19
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
20
- import { useUnistyles } from 'react-native-unistyles';
13
+ import { StyleSheet } from 'react-native-unistyles';
21
14
 
22
15
  import { PortalHost } from '../portal';
23
- import { useScreen } from './ScreenContext';
24
- import { resolveScreenContentInsets } from './utils';
16
+ import { BottomSafeArea } from './parts/BottomSafeArea';
17
+ import { renderScreenPlaceholder } from './parts/ScreenPlaceholder';
18
+ import { SCREEN_HEADER_PORTAL_HOST, useScreen } from './ScreenContext';
19
+ import type { ScreenMargins, ScreenScrollableProps } from './types';
20
+ import { hasXMargin, hasYMargin, renderListHeader } from './utils';
25
21
 
26
22
  export interface ScreenFlatListProps<T>
27
- extends Omit<FlatListProps<T>, 'CellRendererComponent'> {
28
- /**
29
- * Apply `layout.screen.margin` padding. Defaults to the enclosing `Screen`'s
30
- * `margins` value.
31
- */
32
- margins?: boolean;
23
+ extends Omit<FlatListProps<T>, 'CellRendererComponent' | 'horizontal'>,
24
+ ScreenScrollableProps {
33
25
  /**
34
26
  * Extra bottom padding on the list content — clearance for a tab bar or home
35
27
  * indicator. Defaults to the `layout.screen.margin.y` token when margins are
@@ -38,36 +30,48 @@ export interface ScreenFlatListProps<T>
38
30
  bottomInset?: number;
39
31
  }
40
32
 
41
- function renderListHeader(
42
- ListHeaderComponent: FlatListProps<unknown>['ListHeaderComponent']
43
- ): ReactElement | null {
44
- if (!ListHeaderComponent) return null;
45
- if (isValidElement(ListHeaderComponent)) return ListHeaderComponent;
46
- return createElement(ListHeaderComponent as FunctionComponent);
47
- }
48
-
49
33
  /**
50
34
  * `FlatList` variant of `Screen.ScrollView`: binds the screen's shared scroll
51
35
  * ref, mirrors offset into `scrollY`, hosts the portal target above the list,
52
36
  * and applies token-driven content padding.
53
37
  *
38
+ * Shares `loading` / `LoadingComponent` / `EmptyComponent` / `margins` /
39
+ * `horizontal` with `Screen.ScrollView`, so a screen can swap one container for
40
+ * another without relearning its API.
41
+ *
54
42
  * @example
55
43
  * ```tsx
56
44
  * <Screen.FlatList data={items} renderItem={({ item }) => <Row item={item} />} />
57
45
  * ```
46
+ *
47
+ * @example Loading and empty placeholders
48
+ * ```tsx
49
+ * <Screen.FlatList
50
+ * data={items}
51
+ * loading={isLoading}
52
+ * EmptyComponent={<EmptyState icon="status.info" title="Nothing yet" fill />}
53
+ * renderItem={({ item }) => <Row item={item} />}
54
+ * />
55
+ * ```
58
56
  */
59
57
  export function ScreenFlatList<T>({
58
+ data,
60
59
  style,
61
60
  contentContainerStyle,
62
61
  ListHeaderComponent,
62
+ ListFooterComponent,
63
+ ListEmptyComponent,
64
+ horizontal = false,
65
+ loading = false,
66
+ LoadingComponent,
67
+ EmptyComponent,
63
68
  margins,
64
69
  bottomInset,
65
70
  onScroll,
66
71
  ...rest
67
72
  }: ScreenFlatListProps<T>) {
68
- const { theme } = useUnistyles();
69
- const { top } = useSafeAreaInsets();
70
73
  const {
74
+ background,
71
75
  scrollRef,
72
76
  scrollHandler,
73
77
  headerSnapOffsets,
@@ -75,15 +79,17 @@ export function ScreenFlatList<T>({
75
79
  hasHeader,
76
80
  } = useScreen();
77
81
 
78
- const margin = theme.layout.screen.margin;
79
- const marginsEnabled = margins ?? screenMargins;
80
- const compactHeaderHeight = hasHeader ? top + theme.layout.header.height : 0;
81
- const { contentPadding } = resolveScreenContentInsets(
82
- margin,
83
- marginsEnabled,
84
- bottomInset,
85
- compactHeaderHeight
86
- );
82
+ const resolvedMargins = margins ?? screenMargins;
83
+ // The placeholder rides in on `ListEmptyComponent` rather than replacing the
84
+ // list, so the header portal, scroll binding and snap offsets all stay put.
85
+ // While loading the list is fed an empty `data` so that slot is the one that
86
+ // renders.
87
+ const placeholder = renderScreenPlaceholder({
88
+ loading,
89
+ LoadingComponent,
90
+ EmptyComponent,
91
+ isEmpty: !data || data.length === 0,
92
+ });
87
93
 
88
94
  const jsScrollHandler = useAnimatedScrollHandler(
89
95
  (event) => {
@@ -98,9 +104,21 @@ export function ScreenFlatList<T>({
98
104
  jsScrollHandler,
99
105
  ]);
100
106
 
107
+ // A caller naming its own `bottomInset` has taken the bottom over entirely,
108
+ // and a horizontal list has no bottom edge to clear.
109
+ const needsBottomSafeArea = bottomInset === undefined && !horizontal;
110
+
111
+ const footerComponent = (
112
+ <>
113
+ {renderListHeader(ListFooterComponent)}
114
+ {needsBottomSafeArea ? <BottomSafeArea /> : null}
115
+ </>
116
+ );
117
+
101
118
  const headerComponent = (
102
- <View>
103
- <PortalHost />
119
+ <View style={styles.headerWrapper(resolvedMargins)}>
120
+ {hasHeader ? <View style={styles.header} /> : null}
121
+ <PortalHost name={SCREEN_HEADER_PORTAL_HOST} />
104
122
  {renderListHeader(ListHeaderComponent)}
105
123
  </View>
106
124
  );
@@ -108,9 +126,17 @@ export function ScreenFlatList<T>({
108
126
  return (
109
127
  <Animated.FlatList
110
128
  ref={scrollRef as unknown as AnimatedRef<Animated.FlatList<T>>}
111
- style={style}
112
- contentContainerStyle={[contentPadding, contentContainerStyle]}
129
+ data={loading ? [] : data}
130
+ style={[{ backgroundColor: background }, style]}
131
+ horizontal={horizontal}
132
+ contentInsetAdjustmentBehavior={hasHeader ? 'never' : undefined}
133
+ contentContainerStyle={[
134
+ styles.content(resolvedMargins, hasHeader, bottomInset),
135
+ contentContainerStyle,
136
+ ]}
113
137
  ListHeaderComponent={headerComponent}
138
+ ListFooterComponent={footerComponent}
139
+ ListEmptyComponent={placeholder ?? ListEmptyComponent}
114
140
  showsVerticalScrollIndicator={false}
115
141
  scrollEventThrottle={16}
116
142
  snapToOffsets={headerSnapOffsets}
@@ -119,3 +145,38 @@ export function ScreenFlatList<T>({
119
145
  />
120
146
  );
121
147
  }
148
+
149
+ const styles = StyleSheet.create((theme, runtime) => ({
150
+ scrollContent: {
151
+ flexGrow: 1,
152
+ },
153
+ content: (
154
+ margins: ScreenMargins,
155
+ hasHeader: boolean,
156
+ bottomInset: number | undefined
157
+ ) => ({
158
+ flexGrow: 1,
159
+ // The header is absolutely positioned, so the list reserves its full
160
+ // height — bar plus top safe area. This assumes the scroll view does not
161
+ // also get UIKit's automatic inset adjustment: on a native tab screen
162
+ // that means `disableAutomaticContentInsets` on the tab, or the two
163
+ // insets are counted twice.
164
+ paddingTop:
165
+ (hasHeader ? theme.layout.header.height + runtime.insets.top : 0) +
166
+ (hasYMargin(margins) ? theme.layout.screen.margin.y : 0),
167
+ paddingHorizontal: hasXMargin(margins) ? theme.layout.screen.margin.x : 0,
168
+ // The safe area is `BottomSafeArea`'s job; counting it here as well would
169
+ // leave a gap the height of the gesture bar.
170
+ paddingBottom:
171
+ bottomInset ?? (hasYMargin(margins) ? theme.layout.screen.margin.y : 0),
172
+ }),
173
+ safeArea: {
174
+ height: runtime.insets.top,
175
+ },
176
+ headerWrapper: (margins: ScreenMargins) => ({
177
+ marginHorizontal: hasXMargin(margins) ? -theme.layout.screen.margin.x : 0,
178
+ }),
179
+ header: {
180
+ height: theme.layout.header.height,
181
+ },
182
+ }));
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import { Children, type ReactNode } from 'react';
2
2
  import {
3
3
  type NativeScrollEvent,
4
4
  type NativeSyntheticEvent,
@@ -11,29 +11,24 @@ import Animated, {
11
11
  useAnimatedScrollHandler,
12
12
  useComposedEventHandler,
13
13
  } from 'react-native-reanimated';
14
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
15
- import { useUnistyles } from 'react-native-unistyles';
16
-
14
+ import { StyleSheet } from 'react-native-unistyles';
15
+ import { scheduleOnRN } from 'react-native-worklets';
17
16
  import { PortalHost } from '../portal';
18
- import { useScreen } from './ScreenContext';
19
- import { resolveScreenContentInsets } from './utils';
17
+ import { BottomSafeArea } from './parts/BottomSafeArea';
18
+ import { renderScreenPlaceholder } from './parts/ScreenPlaceholder';
19
+ import { SCREEN_HEADER_PORTAL_HOST, useScreen } from './ScreenContext';
20
+ import type { ScreenMargins, ScreenScrollableProps } from './types';
21
+ import { hasXMargin, hasYMargin } from './utils';
20
22
 
21
- export interface ScreenScrollViewProps extends ScrollViewProps {
23
+ export interface ScreenScrollViewProps
24
+ extends Omit<ScrollViewProps, 'horizontal'>,
25
+ ScreenScrollableProps {
22
26
  children?: ReactNode;
23
27
  /**
24
- * Apply `layout.screen.margin` padding. Defaults to the enclosing `Screen`'s
25
- * `margins` value.
26
- */
27
- hasMargins?: boolean;
28
- /**
29
- * Extra bottom padding on the scroll content — clearance for a tab bar or
30
- * home indicator. Defaults to the `layout.screen.margin.y` token when margins
31
- * are enabled, otherwise `0`.
32
- */
33
- bottomInset?: number;
34
- /**
35
- * Optional Reanimated scroll handler composed with the screen's own scroll
36
- * tracking and any JS `onScroll` callback.
28
+ * Reanimated scroll handler for custom scroll-driven animation, composed
29
+ * alongside the screen's own scroll tracking and any JS `onScroll`
30
+ * callback. Runs on the UI thread — for simple JS-side callbacks, use
31
+ * `onScroll` instead; it doesn't require writing a worklet.
37
32
  */
38
33
  scrollHandler?: ScrollHandlerProcessed<Record<string, unknown>>;
39
34
  }
@@ -43,45 +38,96 @@ export interface ScreenScrollViewProps extends ScrollViewProps {
43
38
  * ref, mirrors offset into `scrollY`, hosts the portal target at the top (where
44
39
  * header content is injected), and applies token-driven content padding.
45
40
  *
41
+ * For simple side effects (analytics, infinite-scroll triggers), pass a plain
42
+ * `onScroll` callback. For scroll-driven animation, pass your own
43
+ * `useAnimatedScrollHandler` via `scrollHandler` instead — it runs on the UI
44
+ * thread alongside the screen's own handler, with no JS-thread round trip.
45
+ *
46
46
  * @example
47
47
  * ```tsx
48
48
  * <Screen.ScrollView>
49
49
  * <Text>Scrollable content</Text>
50
50
  * </Screen.ScrollView>
51
51
  * ```
52
+ *
53
+ * @example Loading placeholder instead of a per-screen spinner branch
54
+ * ```tsx
55
+ * const { data, loading } = useQuery();
56
+ *
57
+ * <Screen.ScrollView loading={loading}>
58
+ * <Text>{data?.title}</Text>
59
+ * </Screen.ScrollView>
60
+ * ```
61
+ *
62
+ * @example Empty placeholder when there is nothing to render
63
+ * ```tsx
64
+ * <Screen.ScrollView EmptyComponent={<EmptyState icon="status.info" title="Nothing yet" fill />}>
65
+ * {items.map((item) => <Row key={item.id} item={item} />)}
66
+ * </Screen.ScrollView>
67
+ * ```
68
+ *
69
+ * @example Simple JS-side scroll callback
70
+ * ```tsx
71
+ * <Screen.ScrollView onScroll={(e) => console.log(e.nativeEvent.contentOffset.y)}>
72
+ * <Text>Scrollable content</Text>
73
+ * </Screen.ScrollView>
74
+ * ```
75
+ *
76
+ * @example Custom scroll-driven animation
77
+ * ```tsx
78
+ * const headerOpacity = useSharedValue(1);
79
+ * const scrollHandler = useAnimatedScrollHandler({
80
+ * onScroll: (event) => {
81
+ * headerOpacity.value = interpolate(event.contentOffset.y, [0, 100], [1, 0]);
82
+ * // Need a JS-side side effect too? Call scheduleOnRN(fn, ...args) here.
83
+ * },
84
+ * });
85
+ *
86
+ * <Screen.ScrollView scrollHandler={scrollHandler}>
87
+ * <Text>Scrollable content</Text>
88
+ * </Screen.ScrollView>
89
+ * ```
52
90
  */
53
91
  export function ScreenScrollView({
54
92
  children,
55
93
  style,
56
94
  contentContainerStyle,
57
- hasMargins,
58
- bottomInset,
59
95
  scrollHandler: externalScrollHandler,
60
96
  onScroll: onScrollProp,
61
97
  horizontal = false,
98
+ loading = false,
99
+ LoadingComponent,
100
+ EmptyComponent,
101
+ margins,
62
102
  ...rest
63
103
  }: ScreenScrollViewProps) {
64
- const { theme } = useUnistyles();
65
- const { top } = useSafeAreaInsets();
66
104
  const {
67
105
  scrollRef,
68
106
  scrollHandler: screenScrollHandler,
69
- headerSnapOffsets,
70
107
  margins: screenMargins,
71
108
  hasHeader,
109
+ background,
72
110
  } = useScreen();
73
111
 
74
- const jsScrollHandler = useAnimatedScrollHandler(
75
- onScrollProp
76
- ? {
77
- onScroll: (event) => {
78
- onScrollProp({
79
- nativeEvent: event,
80
- } as unknown as NativeSyntheticEvent<NativeScrollEvent>);
81
- },
82
- }
83
- : {}
84
- );
112
+ const resolvedMargins = margins ?? screenMargins;
113
+ // `toArray` drops the nullish/boolean children a conditional render leaves
114
+ // behind, so `{items.map(...)}` over an empty array counts as empty.
115
+ const placeholder = renderScreenPlaceholder({
116
+ loading,
117
+ LoadingComponent,
118
+ EmptyComponent,
119
+ isEmpty: Children.toArray(children).length === 0,
120
+ });
121
+
122
+ const jsScrollHandler = useAnimatedScrollHandler({
123
+ onScroll: (event) => {
124
+ if (onScrollProp) {
125
+ scheduleOnRN(onScrollProp, {
126
+ nativeEvent: event,
127
+ } as unknown as NativeSyntheticEvent<NativeScrollEvent>);
128
+ }
129
+ },
130
+ });
85
131
 
86
132
  const handlers: ScrollHandlerProcessed<Record<string, unknown>>[] = [
87
133
  screenScrollHandler,
@@ -91,40 +137,55 @@ export function ScreenScrollView({
91
137
 
92
138
  const composedHandler = useComposedEventHandler(handlers);
93
139
 
94
- const margin = theme.layout.screen.margin;
95
- const marginsEnabled = hasMargins ?? screenMargins;
96
- const compactHeaderHeight = hasHeader ? top + theme.layout.header.height : 0;
97
- const { contentPadding } = resolveScreenContentInsets(
98
- margin,
99
- marginsEnabled,
100
- bottomInset,
101
- compactHeaderHeight
102
- );
103
-
104
140
  return (
105
141
  <Animated.ScrollView
106
142
  ref={scrollRef}
107
- style={style}
143
+ style={[{ backgroundColor: background }, style]}
108
144
  horizontal={horizontal}
145
+ contentInsetAdjustmentBehavior={hasHeader ? 'never' : undefined}
109
146
  showsVerticalScrollIndicator={false}
110
147
  scrollEventThrottle={16}
111
- contentContainerStyle={contentPadding}
112
- snapToOffsets={headerSnapOffsets}
148
+ contentContainerStyle={styles.scrollContent(hasHeader, resolvedMargins)}
149
+ // snapToOffsets={headerSnapOffsets}
113
150
  {...rest}
114
151
  onScroll={composedHandler}
115
152
  >
153
+ {/* <View style={styles.safeArea} /> */}
116
154
  <View>
117
- <PortalHost />
155
+ <PortalHost name={SCREEN_HEADER_PORTAL_HOST} />
118
156
  </View>
119
157
  <View
120
158
  style={[
159
+ styles.content(horizontal, resolvedMargins),
121
160
  contentContainerStyle,
122
- { flexDirection: horizontal ? 'row' : 'column' },
123
161
  ]}
124
162
  >
125
- {/* content spacing */}
126
- {children}
163
+ {placeholder ?? children}
127
164
  </View>
165
+ {/* Outside the padded content view so the horizontal margins do not
166
+ apply to it, and last so it sits below everything. A horizontal
167
+ scroller has no bottom edge to clear. */}
168
+ {horizontal ? null : <BottomSafeArea />}
128
169
  </Animated.ScrollView>
129
170
  );
130
171
  }
172
+
173
+ const styles = StyleSheet.create((theme, rt) => ({
174
+ scrollContent: (hasHeader: boolean, margins: ScreenMargins) => ({
175
+ flexGrow: 1,
176
+ // The header inset and the safe area are structural — they apply whether
177
+ // or not margins are on; the y margin stacks on top of them.
178
+ paddingTop:
179
+ (hasHeader ? theme.layout.header.height + rt.insets.top : 0) +
180
+ (hasYMargin(margins) ? theme.layout.screen.margin.y : 0),
181
+ // The safe area is `BottomSafeArea`'s job; counting it here as well would
182
+ // leave a gap the height of the gesture bar.
183
+ paddingBottom: hasYMargin(margins) ? theme.layout.screen.margin.y : 0,
184
+ }),
185
+
186
+ content: (horizontal: boolean, margins: ScreenMargins) => ({
187
+ flexGrow: 1,
188
+ paddingHorizontal: hasXMargin(margins) ? theme.layout.screen.margin.x : 0,
189
+ flexDirection: horizontal ? 'row' : 'column',
190
+ }),
191
+ }));
@@ -2,7 +2,10 @@ export type { ScreenHeaderProps, ScreenProps } from './Screen';
2
2
  export { Screen } from './Screen';
3
3
  export type { ScreenContextValue } from './ScreenContext';
4
4
  export { useScreen } from './ScreenContext';
5
+ export type { ScreenFlashListProps } from './ScreenFlashList';
6
+ export { ScreenFlashList } from './ScreenFlashList';
5
7
  export type { ScreenFlatListProps } from './ScreenFlatList';
6
8
  export { ScreenFlatList } from './ScreenFlatList';
7
9
  export type { ScreenScrollViewProps } from './ScreenScrollView';
8
10
  export { ScreenScrollView } from './ScreenScrollView';
11
+ export type { ScreenMargins, ScreenScrollableProps } from './types';