react-native-terra-ui 0.7.2 → 0.8.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 (252) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/lib/module/components/avatar/Avatar.js +20 -20
  4. package/lib/module/components/avatar/Avatar.js.map +1 -1
  5. package/lib/module/components/button/Button.js +11 -11
  6. package/lib/module/components/button/Button.js.map +1 -1
  7. package/lib/module/components/chip/Chip.js +32 -32
  8. package/lib/module/components/chip/Chip.js.map +1 -1
  9. package/lib/module/components/empty-state/EmptyState.js +3 -3
  10. package/lib/module/components/empty-state/EmptyState.js.map +1 -1
  11. package/lib/module/components/header/variants/LargeTitleHeader/index.js +30 -32
  12. package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
  13. package/lib/module/components/header/variants/TitleHeader/index.js +26 -18
  14. package/lib/module/components/header/variants/TitleHeader/index.js.map +1 -1
  15. package/lib/module/components/icon/Icon.js +3 -3
  16. package/lib/module/components/page-indicator/hooks/use-page-indicator-colors.js +2 -2
  17. package/lib/module/components/screen/Screen.js +63 -34
  18. package/lib/module/components/screen/Screen.js.map +1 -1
  19. package/lib/module/components/screen/ScreenContext.js +28 -9
  20. package/lib/module/components/screen/ScreenContext.js.map +1 -1
  21. package/lib/module/components/screen/ScreenFlatList.js +4 -162
  22. package/lib/module/components/screen/ScreenFlatList.js.map +1 -1
  23. package/lib/module/components/screen/ScreenList.js +81 -97
  24. package/lib/module/components/screen/ScreenList.js.map +1 -1
  25. package/lib/module/components/screen/ScreenScrollView.js +64 -115
  26. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  27. package/lib/module/components/screen/hooks/use-screen-ref.js +18 -0
  28. package/lib/module/components/screen/hooks/use-screen-ref.js.map +1 -0
  29. package/lib/module/components/screen/hooks/use-screen-scroll-handler.js +19 -0
  30. package/lib/module/components/screen/hooks/use-screen-scroll-handler.js.map +1 -0
  31. package/lib/module/components/screen/index.js.map +1 -1
  32. package/lib/module/components/screen/parts/ScreenFrame.js +47 -0
  33. package/lib/module/components/screen/parts/ScreenFrame.js.map +1 -0
  34. package/lib/module/components/screen/parts/ScreenHeaderAppearance.js +43 -0
  35. package/lib/module/components/screen/parts/ScreenHeaderAppearance.js.map +1 -0
  36. package/lib/module/components/screen/parts/ScreenPlaceholder.js +11 -13
  37. package/lib/module/components/screen/parts/ScreenPlaceholder.js.map +1 -1
  38. package/lib/module/components/screen/parts/ScreenSafeArea.android.js +27 -0
  39. package/lib/module/components/screen/parts/ScreenSafeArea.android.js.map +1 -0
  40. package/lib/module/components/screen/parts/ScreenSafeArea.js +26 -0
  41. package/lib/module/components/screen/parts/ScreenSafeArea.js.map +1 -0
  42. package/lib/module/components/screen/utils.js +18 -9
  43. package/lib/module/components/screen/utils.js.map +1 -1
  44. package/lib/module/components/spinner/Spinner.js +2 -2
  45. package/lib/module/components/text/Text.js +1 -1
  46. package/lib/module/components/toast/Toast.js +13 -13
  47. package/lib/module/components/toast/Toast.js.map +1 -1
  48. package/lib/module/components/toolbar/Toolbar.js +2 -2
  49. package/lib/module/context/ThemeProvider.js +22 -39
  50. package/lib/module/context/ThemeProvider.js.map +1 -1
  51. package/lib/module/context/index.js +1 -1
  52. package/lib/module/context/index.js.map +1 -1
  53. package/lib/module/theme/breakpoints.js +1 -1
  54. package/lib/module/theme/define-config.js +21 -0
  55. package/lib/module/theme/define-config.js.map +1 -0
  56. package/lib/module/theme/diagnostics.js +13 -0
  57. package/lib/module/theme/diagnostics.js.map +1 -0
  58. package/lib/module/theme/errors.js +15 -0
  59. package/lib/module/theme/errors.js.map +1 -0
  60. package/lib/module/theme/index.js +7 -1
  61. package/lib/module/theme/index.js.map +1 -1
  62. package/lib/module/theme/legacy-tokens.js +119 -0
  63. package/lib/module/theme/legacy-tokens.js.map +1 -0
  64. package/lib/module/theme/registry.js +122 -130
  65. package/lib/module/theme/registry.js.map +1 -1
  66. package/lib/module/theme/resolve-config.js +346 -0
  67. package/lib/module/theme/resolve-config.js.map +1 -0
  68. package/lib/module/theme/runtime.js +57 -12
  69. package/lib/module/theme/runtime.js.map +1 -1
  70. package/lib/module/theme/screen-margin.js +6 -14
  71. package/lib/module/theme/screen-margin.js.map +1 -1
  72. package/lib/module/theme/screen-padding.js +17 -0
  73. package/lib/module/theme/screen-padding.js.map +1 -0
  74. package/lib/module/theme/selection-store.js +75 -0
  75. package/lib/module/theme/selection-store.js.map +1 -0
  76. package/lib/module/theme/theme.js +81 -36
  77. package/lib/module/theme/theme.js.map +1 -1
  78. package/lib/module/theme/tokens/canonical-color-keys.js +102 -0
  79. package/lib/module/theme/tokens/canonical-color-keys.js.map +1 -0
  80. package/lib/module/theme/tokens/dark.js +72 -88
  81. package/lib/module/theme/tokens/dark.js.map +1 -1
  82. package/lib/module/theme/tokens/light.js +69 -83
  83. package/lib/module/theme/tokens/light.js.map +1 -1
  84. package/lib/module/theme/tokens/primitives.js +14 -14
  85. package/lib/module/theme/tokens/primitives.js.map +1 -1
  86. package/lib/module/theme/tokens/validate.js +238 -0
  87. package/lib/module/theme/tokens/validate.js.map +1 -0
  88. package/lib/module/theme/unistyles-adapter.js +77 -0
  89. package/lib/module/theme/unistyles-adapter.js.map +1 -0
  90. package/lib/module/utils/accent-generator.js +476 -0
  91. package/lib/module/utils/accent-generator.js.map +1 -0
  92. package/lib/module/utils/accent-utils.js +52 -35
  93. package/lib/module/utils/accent-utils.js.map +1 -1
  94. package/lib/module/utils/color-utils.js +224 -7
  95. package/lib/module/utils/color-utils.js.map +1 -1
  96. package/lib/module/utils/deep-merge.js +1 -1
  97. package/lib/module/utils/deep-merge.js.map +1 -1
  98. package/lib/module/utils/resolve-theme-color.js +2 -2
  99. package/lib/typescript/src/components/chip/types.d.ts +1 -1
  100. package/lib/typescript/src/components/empty-state/EmptyState.d.ts +1 -1
  101. package/lib/typescript/src/components/empty-state/EmptyState.d.ts.map +1 -1
  102. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +6 -5
  103. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
  104. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts +11 -5
  105. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts.map +1 -1
  106. package/lib/typescript/src/components/icon/Icon.d.ts +2 -2
  107. package/lib/typescript/src/components/screen/Screen.d.ts +45 -16
  108. package/lib/typescript/src/components/screen/Screen.d.ts.map +1 -1
  109. package/lib/typescript/src/components/screen/ScreenContext.d.ts +42 -26
  110. package/lib/typescript/src/components/screen/ScreenContext.d.ts.map +1 -1
  111. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts +5 -46
  112. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts.map +1 -1
  113. package/lib/typescript/src/components/screen/ScreenList.d.ts +62 -50
  114. package/lib/typescript/src/components/screen/ScreenList.d.ts.map +1 -1
  115. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts +18 -58
  116. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  117. package/lib/typescript/src/components/screen/hooks/use-screen-ref.d.ts +5 -0
  118. package/lib/typescript/src/components/screen/hooks/use-screen-ref.d.ts.map +1 -0
  119. package/lib/typescript/src/components/screen/hooks/use-screen-scroll-handler.d.ts +53 -0
  120. package/lib/typescript/src/components/screen/hooks/use-screen-scroll-handler.d.ts.map +1 -0
  121. package/lib/typescript/src/components/screen/index.d.ts +2 -2
  122. package/lib/typescript/src/components/screen/index.d.ts.map +1 -1
  123. package/lib/typescript/src/components/screen/parts/ScreenFrame.d.ts +6 -0
  124. package/lib/typescript/src/components/screen/parts/ScreenFrame.d.ts.map +1 -0
  125. package/lib/typescript/src/components/screen/parts/ScreenHeaderAppearance.d.ts +21 -0
  126. package/lib/typescript/src/components/screen/parts/ScreenHeaderAppearance.d.ts.map +1 -0
  127. package/lib/typescript/src/components/screen/parts/ScreenPlaceholder.d.ts +6 -8
  128. package/lib/typescript/src/components/screen/parts/ScreenPlaceholder.d.ts.map +1 -1
  129. package/lib/typescript/src/components/screen/parts/ScreenSafeArea.android.d.ts +7 -0
  130. package/lib/typescript/src/components/screen/parts/ScreenSafeArea.android.d.ts.map +1 -0
  131. package/lib/typescript/src/components/screen/parts/ScreenSafeArea.d.ts +6 -0
  132. package/lib/typescript/src/components/screen/parts/ScreenSafeArea.d.ts.map +1 -0
  133. package/lib/typescript/src/components/screen/types.d.ts +27 -32
  134. package/lib/typescript/src/components/screen/types.d.ts.map +1 -1
  135. package/lib/typescript/src/components/screen/utils.d.ts +9 -8
  136. package/lib/typescript/src/components/screen/utils.d.ts.map +1 -1
  137. package/lib/typescript/src/components/text/Text.d.ts +1 -1
  138. package/lib/typescript/src/components/text/Text.d.ts.map +1 -1
  139. package/lib/typescript/src/components/toolbar/Toolbar.d.ts +2 -2
  140. package/lib/typescript/src/context/ThemeProvider.d.ts +29 -12
  141. package/lib/typescript/src/context/ThemeProvider.d.ts.map +1 -1
  142. package/lib/typescript/src/context/index.d.ts +1 -1
  143. package/lib/typescript/src/context/index.d.ts.map +1 -1
  144. package/lib/typescript/src/theme/breakpoints.d.ts +1 -1
  145. package/lib/typescript/src/theme/define-config.d.ts +23 -0
  146. package/lib/typescript/src/theme/define-config.d.ts.map +1 -0
  147. package/lib/typescript/src/theme/diagnostics.d.ts +4 -0
  148. package/lib/typescript/src/theme/diagnostics.d.ts.map +1 -0
  149. package/lib/typescript/src/theme/errors.d.ts +10 -0
  150. package/lib/typescript/src/theme/errors.d.ts.map +1 -0
  151. package/lib/typescript/src/theme/index.d.ts +10 -2
  152. package/lib/typescript/src/theme/index.d.ts.map +1 -1
  153. package/lib/typescript/src/theme/legacy-tokens.d.ts +187 -0
  154. package/lib/typescript/src/theme/legacy-tokens.d.ts.map +1 -0
  155. package/lib/typescript/src/theme/registry.d.ts +12 -17
  156. package/lib/typescript/src/theme/registry.d.ts.map +1 -1
  157. package/lib/typescript/src/theme/resolve-config.d.ts +99 -0
  158. package/lib/typescript/src/theme/resolve-config.d.ts.map +1 -0
  159. package/lib/typescript/src/theme/runtime.d.ts +25 -4
  160. package/lib/typescript/src/theme/runtime.d.ts.map +1 -1
  161. package/lib/typescript/src/theme/screen-margin.d.ts +6 -14
  162. package/lib/typescript/src/theme/screen-margin.d.ts.map +1 -1
  163. package/lib/typescript/src/theme/screen-padding.d.ts +15 -0
  164. package/lib/typescript/src/theme/screen-padding.d.ts.map +1 -0
  165. package/lib/typescript/src/theme/selection-store.d.ts +41 -0
  166. package/lib/typescript/src/theme/selection-store.d.ts.map +1 -0
  167. package/lib/typescript/src/theme/theme.d.ts +54 -1
  168. package/lib/typescript/src/theme/theme.d.ts.map +1 -1
  169. package/lib/typescript/src/theme/tokens/canonical-color-keys.d.ts +95 -0
  170. package/lib/typescript/src/theme/tokens/canonical-color-keys.d.ts.map +1 -0
  171. package/lib/typescript/src/theme/tokens/dark.d.ts +56 -63
  172. package/lib/typescript/src/theme/tokens/dark.d.ts.map +1 -1
  173. package/lib/typescript/src/theme/tokens/light.d.ts +56 -71
  174. package/lib/typescript/src/theme/tokens/light.d.ts.map +1 -1
  175. package/lib/typescript/src/theme/tokens/primitives.d.ts +11 -10
  176. package/lib/typescript/src/theme/tokens/primitives.d.ts.map +1 -1
  177. package/lib/typescript/src/theme/tokens/validate.d.ts +34 -0
  178. package/lib/typescript/src/theme/tokens/validate.d.ts.map +1 -0
  179. package/lib/typescript/src/theme/types.d.ts +166 -86
  180. package/lib/typescript/src/theme/types.d.ts.map +1 -1
  181. package/lib/typescript/src/theme/unistyles-adapter.d.ts +33 -0
  182. package/lib/typescript/src/theme/unistyles-adapter.d.ts.map +1 -0
  183. package/lib/typescript/src/utils/accent-generator.d.ts +103 -0
  184. package/lib/typescript/src/utils/accent-generator.d.ts.map +1 -0
  185. package/lib/typescript/src/utils/accent-utils.d.ts +15 -2
  186. package/lib/typescript/src/utils/accent-utils.d.ts.map +1 -1
  187. package/lib/typescript/src/utils/color-utils.d.ts +59 -2
  188. package/lib/typescript/src/utils/color-utils.d.ts.map +1 -1
  189. package/lib/typescript/src/utils/deep-merge.d.ts +1 -0
  190. package/lib/typescript/src/utils/deep-merge.d.ts.map +1 -1
  191. package/lib/typescript/src/utils/resolve-theme-color.d.ts +2 -2
  192. package/package.json +1 -1
  193. package/src/components/avatar/Avatar.tsx +20 -20
  194. package/src/components/button/Button.tsx +11 -11
  195. package/src/components/chip/Chip.tsx +32 -32
  196. package/src/components/chip/types.ts +1 -1
  197. package/src/components/empty-state/EmptyState.tsx +4 -4
  198. package/src/components/header/variants/LargeTitleHeader/index.tsx +31 -30
  199. package/src/components/header/variants/TitleHeader/index.tsx +33 -22
  200. package/src/components/icon/Icon.tsx +4 -4
  201. package/src/components/page-indicator/hooks/use-page-indicator-colors.ts +2 -2
  202. package/src/components/screen/Screen.tsx +89 -43
  203. package/src/components/screen/ScreenContext.tsx +48 -28
  204. package/src/components/screen/ScreenFlatList.tsx +5 -191
  205. package/src/components/screen/ScreenList.tsx +197 -175
  206. package/src/components/screen/ScreenScrollView.tsx +92 -167
  207. package/src/components/screen/hooks/use-screen-ref.ts +24 -0
  208. package/src/components/screen/hooks/use-screen-scroll-handler.ts +31 -0
  209. package/src/components/screen/index.ts +8 -2
  210. package/src/components/screen/parts/ScreenFrame.tsx +49 -0
  211. package/src/components/screen/parts/ScreenHeaderAppearance.tsx +52 -0
  212. package/src/components/screen/parts/ScreenPlaceholder.tsx +16 -16
  213. package/src/components/screen/parts/ScreenSafeArea.android.tsx +22 -0
  214. package/src/components/screen/parts/ScreenSafeArea.tsx +20 -0
  215. package/src/components/screen/types.ts +29 -32
  216. package/src/components/screen/utils.ts +24 -11
  217. package/src/components/spinner/Spinner.tsx +2 -2
  218. package/src/components/text/Text.tsx +2 -2
  219. package/src/components/toast/Toast.tsx +13 -13
  220. package/src/components/toolbar/Toolbar.tsx +4 -4
  221. package/src/context/ThemeProvider.tsx +47 -51
  222. package/src/context/index.ts +1 -1
  223. package/src/theme/breakpoints.ts +1 -1
  224. package/src/theme/define-config.ts +27 -0
  225. package/src/theme/diagnostics.ts +11 -0
  226. package/src/theme/errors.ts +14 -0
  227. package/src/theme/index.ts +40 -1
  228. package/src/theme/legacy-tokens.ts +227 -0
  229. package/src/theme/registry.ts +119 -137
  230. package/src/theme/resolve-config.ts +406 -0
  231. package/src/theme/runtime.ts +52 -14
  232. package/src/theme/screen-margin.ts +6 -25
  233. package/src/theme/screen-padding.ts +26 -0
  234. package/src/theme/selection-store.ts +76 -0
  235. package/src/theme/theme.ts +104 -39
  236. package/src/theme/tokens/canonical-color-keys.ts +107 -0
  237. package/src/theme/tokens/dark.ts +91 -98
  238. package/src/theme/tokens/light.ts +88 -93
  239. package/src/theme/tokens/primitives.ts +14 -14
  240. package/src/theme/tokens/validate.ts +271 -0
  241. package/src/theme/types.ts +184 -95
  242. package/src/theme/unistyles-adapter.ts +75 -0
  243. package/src/utils/accent-generator.ts +530 -0
  244. package/src/utils/accent-utils.ts +66 -39
  245. package/src/utils/color-utils.ts +225 -8
  246. package/src/utils/deep-merge.ts +1 -1
  247. package/src/utils/resolve-theme-color.ts +2 -2
  248. package/lib/module/components/screen/parts/BottomSafeArea.js +0 -62
  249. package/lib/module/components/screen/parts/BottomSafeArea.js.map +0 -1
  250. package/lib/typescript/src/components/screen/parts/BottomSafeArea.d.ts +0 -30
  251. package/lib/typescript/src/components/screen/parts/BottomSafeArea.d.ts.map +0 -1
  252. package/src/components/screen/parts/BottomSafeArea.tsx +0 -59
@@ -1,193 +1,118 @@
1
- import { Children, type ReactNode } from "react";
2
- import { type NativeScrollEvent, type NativeSyntheticEvent, type ScrollViewProps, View } from "react-native";
1
+ import { Children, type ComponentRef, forwardRef, type ReactNode } from "react";
2
+ import { type ScrollView, type ScrollViewProps, type StyleProp, View, type ViewStyle } from "react-native";
3
3
 
4
- import Animated, {
5
- type ScrollHandlerProcessed,
6
- useAnimatedScrollHandler,
7
- useComposedEventHandler,
8
- } from "react-native-reanimated";
4
+ import Animated from "react-native-reanimated";
9
5
  import { StyleSheet } from "react-native-unistyles";
10
- import { scheduleOnRN } from "react-native-worklets";
11
6
 
12
- import { resolveScreenMargin } from "#theme/screen-margin";
7
+ import { resolveScreenPadding } from "#theme/screen-padding";
13
8
  import { PortalHost } from "../portal";
14
- import { BottomSafeArea } from "./parts/BottomSafeArea";
9
+ import { useScreenRef } from "./hooks/use-screen-ref";
10
+ import { useScreenScrollHandler } from "./hooks/use-screen-scroll-handler";
15
11
  import { renderScreenPlaceholder } from "./parts/ScreenPlaceholder";
16
12
  import { SCREEN_HEADER_PORTAL_HOST, useScreen } from "./ScreenContext";
17
- import type { ScreenMargins, ScreenScrollableProps } from "./types";
18
- import { hasXMargin, hasYMargin } from "./utils";
13
+ import type { ScreenContentPadding, ScreenScrollableProps } from "./types";
14
+ import { hasHorizontalPadding, hasVerticalPadding } from "./utils";
19
15
 
16
+ /** Native ScrollView props plus Screen layout, placeholders, and worklet support. */
20
17
  export interface ScreenScrollViewProps extends Omit<ScrollViewProps, "horizontal">, ScreenScrollableProps {
18
+ /** Content replaced while loading or empty. */
21
19
  children?: ReactNode;
22
- /**
23
- * Reanimated scroll handler for custom scroll-driven animation, composed
24
- * alongside the screen's own scroll tracking and any JS `onScroll`
25
- * callback. Runs on the UI thread — for simple JS-side callbacks, use
26
- * `onScroll` instead; it doesn't require writing a worklet.
27
- */
28
- scrollHandler?: ScrollHandlerProcessed<Record<string, unknown>>;
20
+ /** Style the inner content View, excluding the header portal. */
21
+ contentStyle?: StyleProp<ViewStyle>;
22
+ /** Override empty-child detection, for example when a child component renders null. */
23
+ isEmpty?: boolean;
29
24
  }
30
25
 
31
26
  /**
32
- * Scrollable content container for a `Screen`. Binds the screen's shared scroll
33
- * ref, mirrors offset into `scrollY`, hosts the portal target at the top (where
34
- * header content is injected), and applies token-driven content padding.
27
+ * Scrollable content with a header portal and shared scroll tracking.
28
+ * Must be inside Screen. The ref exposes native ScrollView methods.
35
29
  *
36
- * For simple side effects (analytics, infinite-scroll triggers), pass a plain
37
- * `onScroll` callback. For scroll-driven animation, pass your own
38
- * `useAnimatedScrollHandler` via `scrollHandler` instead — it runs on the UI
39
- * thread alongside the screen's own handler, with no JS-thread round trip.
30
+ * `contentContainerStyle` styles the native container, including header content.
31
+ * Use `contentStyle` for the inner content only. `onScroll` receives nativeEvent
32
+ * data on JS, without SyntheticEvent methods; `scrollHandler` runs on the UI thread.
40
33
  *
41
34
  * @example
42
35
  * ```tsx
43
- * <Screen.ScrollView>
44
- * <Text>Scrollable content</Text>
45
- * </Screen.ScrollView>
46
- * ```
47
- *
48
- * @example Loading placeholder instead of a per-screen spinner branch
49
- * ```tsx
50
- * const { data, loading } = useQuery();
51
- *
52
- * <Screen.ScrollView loading={loading}>
53
- * <Text>{data?.title}</Text>
54
- * </Screen.ScrollView>
55
- * ```
56
- *
57
- * @example Empty placeholder when there is nothing to render
58
- * ```tsx
59
- * <Screen.ScrollView EmptyComponent={<EmptyState icon="status.info" title="Nothing yet" fill />}>
60
- * {items.map((item) => <Row key={item.id} item={item} />)}
61
- * </Screen.ScrollView>
62
- * ```
63
- *
64
- * @example Simple JS-side scroll callback
65
- * ```tsx
66
- * <Screen.ScrollView onScroll={(e) => console.log(e.nativeEvent.contentOffset.y)}>
67
- * <Text>Scrollable content</Text>
68
- * </Screen.ScrollView>
69
- * ```
70
- *
71
- * @example Custom scroll-driven animation
72
- * ```tsx
73
- * const headerOpacity = useSharedValue(1);
74
- * const scrollHandler = useAnimatedScrollHandler({
75
- * onScroll: (event) => {
76
- * headerOpacity.value = interpolate(event.contentOffset.y, [0, 100], [1, 0]);
77
- * // Need a JS-side side effect too? Call scheduleOnRN(fn, ...args) here.
78
- * },
79
- * });
80
- *
81
- * <Screen.ScrollView scrollHandler={scrollHandler}>
82
- * <Text>Scrollable content</Text>
83
- * </Screen.ScrollView>
36
+ * <Screen safeArea="auto" contentPadding="x">
37
+ * <Screen.ScrollView isLoading={isLoading} emptyContent={<Text>Nothing yet</Text>}>
38
+ * {items.map(item => <Row key={item.id} item={item} />)}
39
+ * </Screen.ScrollView>
40
+ * </Screen>
84
41
  * ```
85
42
  */
86
- export function ScreenScrollView({
87
- children,
88
- style,
89
- contentContainerStyle,
90
- scrollHandler: externalScrollHandler,
91
- onScroll: onScrollProp,
92
- horizontal = false,
93
- loading = false,
94
- LoadingComponent,
95
- EmptyComponent,
96
- margins,
97
- ...rest
98
- }: ScreenScrollViewProps) {
99
- const {
100
- scrollRef,
101
- scrollHandler: screenScrollHandler,
102
- margins: screenMargins,
103
- hasHeader,
104
- background,
105
- } = useScreen();
106
-
107
- const resolvedMargins = margins ?? screenMargins;
108
- // `toArray` drops the nullish/boolean children a conditional render leaves
109
- // behind, so `{items.map(...)}` over an empty array counts as empty.
110
- const placeholder = renderScreenPlaceholder({
111
- loading,
112
- LoadingComponent,
113
- EmptyComponent,
114
- isEmpty: Children.toArray(children).length === 0,
115
- });
116
-
117
- const jsScrollHandler = useAnimatedScrollHandler({
118
- onScroll: (event) => {
119
- if (onScrollProp) {
120
- scheduleOnRN(onScrollProp, {
121
- nativeEvent: event,
122
- } as unknown as NativeSyntheticEvent<NativeScrollEvent>);
123
- }
43
+ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, ScreenScrollViewProps>(
44
+ function ScreenScrollView(
45
+ {
46
+ children,
47
+ style,
48
+ contentContainerStyle,
49
+ contentStyle,
50
+ scrollHandler: externalScrollHandler,
51
+ onScroll,
52
+ horizontal = false,
53
+ isLoading,
54
+ loading = false,
55
+ loadingContent,
56
+ LoadingComponent,
57
+ emptyContent,
58
+ EmptyComponent,
59
+ contentPadding,
60
+ margins,
61
+ extraBottomPadding = 0,
62
+ isEmpty,
63
+ ...rest
124
64
  },
125
- });
126
-
127
- const handlers: ScrollHandlerProcessed<Record<string, unknown>>[] = [
128
- screenScrollHandler,
129
- ...(externalScrollHandler ? [externalScrollHandler] : []),
130
- ...(onScrollProp ? [jsScrollHandler] : []),
131
- ];
132
-
133
- const composedHandler = useComposedEventHandler(handlers);
65
+ ref
66
+ ) {
67
+ const { scrollRef, scrollHandler, contentPadding: screenPadding, hasHeader, background } = useScreen();
68
+ const resolvedPadding = contentPadding ?? margins ?? screenPadding;
69
+ const mergedRef = useScreenRef(scrollRef, ref);
70
+ const placeholder = renderScreenPlaceholder({
71
+ isLoading: isLoading ?? loading,
72
+ loadingContent: loadingContent !== undefined ? loadingContent : (LoadingComponent ?? undefined),
73
+ emptyContent: emptyContent !== undefined ? emptyContent : EmptyComponent,
74
+ isEmpty: isEmpty ?? Children.toArray(children).length === 0,
75
+ });
76
+ const composedScrollHandler = useScreenScrollHandler(scrollHandler, onScroll, externalScrollHandler);
134
77
 
135
- return (
136
- <Animated.ScrollView
137
- ref={scrollRef}
138
- style={[{ backgroundColor: background }, style]}
139
- horizontal={horizontal}
140
- contentInsetAdjustmentBehavior={hasHeader ? "never" : undefined}
141
- showsVerticalScrollIndicator={false}
142
- scrollEventThrottle={16}
143
- contentContainerStyle={styles.scrollContent(hasHeader, resolvedMargins)}
144
- // snapToOffsets={headerSnapOffsets}
145
- {...rest}
146
- onScroll={composedHandler}
147
- >
148
- {/* <View style={styles.safeArea} /> */}
149
- <View>
150
- <PortalHost name={SCREEN_HEADER_PORTAL_HOST} />
151
- </View>
152
- <View style={[styles.content(horizontal, resolvedMargins), contentContainerStyle]}>
153
- {placeholder ?? children}
154
- </View>
155
- {/* Outside the padded content view so the horizontal margins do not
156
- apply to it, and last so it sits below everything. A horizontal
157
- scroller has no bottom edge to clear. */}
158
- {horizontal ? null : <BottomSafeArea />}
159
- </Animated.ScrollView>
160
- );
161
- }
162
-
163
- const styles = StyleSheet.create((theme, rt) => ({
164
- // `resolveScreenMargin` is called *inside* each dynamic function, not
165
- // hoisted above `return` — the Babel plugin only attributes a runtime
166
- // dependency (here, breakpoint) to a style key when the `rt` read it
167
- // derives from sits inside that key's own value. Hoisting it to the
168
- // factory's top level silently drops the dependency, so the margin would
169
- // freeze at whatever breakpoint was active on mount and never update on
170
- // rotation.
171
- scrollContent: (hasHeader: boolean, margins: ScreenMargins) => {
172
- const { y: marginY } = resolveScreenMargin(theme, rt.breakpoint);
173
- return {
174
- flexGrow: 1,
175
- // The header inset and the safe area are structural — they apply
176
- // whether or not margins are on; the y margin stacks on top of them.
177
- paddingTop:
178
- (hasHeader ? theme.layout.header.height + rt.insets.top : 0) + (hasYMargin(margins) ? marginY : 0),
179
- // The safe area is `BottomSafeArea`'s job; counting it here as well
180
- // would leave a gap the height of the gesture bar.
181
- paddingBottom: hasYMargin(margins) ? marginY : 0,
182
- };
183
- },
78
+ return (
79
+ <Animated.ScrollView
80
+ ref={mergedRef}
81
+ style={[{ backgroundColor: background }, style]}
82
+ horizontal={horizontal}
83
+ contentInsetAdjustmentBehavior="never"
84
+ automaticallyAdjustContentInsets={false}
85
+ automaticallyAdjustsScrollIndicatorInsets={false}
86
+ showsVerticalScrollIndicator={false}
87
+ scrollEventThrottle={16}
88
+ contentContainerStyle={[
89
+ styles.scrollContent(hasHeader, resolvedPadding, extraBottomPadding),
90
+ contentContainerStyle,
91
+ ]}
92
+ {...rest}
93
+ onScroll={composedScrollHandler}
94
+ >
95
+ <View style={styles.headerWrapper(resolvedPadding)}>
96
+ <PortalHost name={SCREEN_HEADER_PORTAL_HOST} />
97
+ </View>
98
+ <View style={[styles.content(horizontal), contentStyle]}>{placeholder ?? children}</View>
99
+ </Animated.ScrollView>
100
+ );
101
+ }
102
+ );
184
103
 
185
- content: (horizontal: boolean, margins: ScreenMargins) => {
186
- const { x: marginX } = resolveScreenMargin(theme, rt.breakpoint);
104
+ const styles = StyleSheet.create((theme, runtime) => ({
105
+ scrollContent: (hasHeader: boolean, padding: ScreenContentPadding, extraBottomPadding: number) => {
106
+ const { x, y } = resolveScreenPadding(theme, runtime.breakpoint);
187
107
  return {
188
108
  flexGrow: 1,
189
- paddingHorizontal: hasXMargin(margins) ? marginX : 0,
190
- flexDirection: horizontal ? "row" : "column",
109
+ paddingTop: (hasHeader ? theme.layout.header.height : 0) + (hasVerticalPadding(padding) ? y : 0),
110
+ paddingHorizontal: hasHorizontalPadding(padding) ? x : 0,
111
+ paddingBottom: (hasVerticalPadding(padding) ? y : 0) + extraBottomPadding,
191
112
  };
192
113
  },
114
+ headerWrapper: (padding: ScreenContentPadding) => ({
115
+ marginHorizontal: hasHorizontalPadding(padding) ? -resolveScreenPadding(theme, runtime.breakpoint).x : 0,
116
+ }),
117
+ content: (horizontal: boolean) => ({ flexGrow: 1, flexDirection: horizontal ? "row" : "column" }),
193
118
  }));
@@ -0,0 +1,24 @@
1
+ import { type Ref, useCallback } from "react";
2
+
3
+ import type { ScreenScrollRef } from "../ScreenContext";
4
+
5
+ /** Bind the animated ref and the caller's native ref to the same instance. */
6
+ export function useScreenRef<T>(screenRef: ScreenScrollRef | undefined, ref: Ref<T> | undefined) {
7
+ return useCallback(
8
+ (instance: T | null) => {
9
+ if (typeof screenRef === "function") screenRef(instance as never);
10
+ else if (screenRef) (screenRef as { current: unknown }).current = instance;
11
+ if (typeof ref === "function") {
12
+ const cleanup = ref(instance);
13
+ if (cleanup)
14
+ return () => {
15
+ if (typeof screenRef === "function") screenRef(null);
16
+ else if (screenRef) (screenRef as { current: unknown }).current = null;
17
+ cleanup();
18
+ };
19
+ } else if (ref) ref.current = instance;
20
+ return undefined;
21
+ },
22
+ [screenRef, ref]
23
+ );
24
+ }
@@ -0,0 +1,31 @@
1
+ import type { NativeScrollEvent, NativeSyntheticEvent, ScrollViewProps } from "react-native";
2
+
3
+ import {
4
+ type ScrollHandlerProcessed,
5
+ useAnimatedScrollHandler,
6
+ useComposedEventHandler,
7
+ } from "react-native-reanimated";
8
+ import { scheduleOnRN } from "react-native-worklets";
9
+
10
+ /** Keeps scroll tracking on the UI thread and schedules optional JS callbacks on RN. */
11
+ export function useScreenScrollHandler(
12
+ screenScrollHandler: ScrollHandlerProcessed<Record<string, unknown>>,
13
+ onScroll?: ScrollViewProps["onScroll"],
14
+ externalScrollHandler?: ScrollHandlerProcessed<Record<string, unknown>>
15
+ ) {
16
+ const jsScrollHandler = useAnimatedScrollHandler({
17
+ onScroll: (event) => {
18
+ if (onScroll) {
19
+ scheduleOnRN(onScroll, {
20
+ nativeEvent: event,
21
+ } as unknown as NativeSyntheticEvent<NativeScrollEvent>);
22
+ }
23
+ },
24
+ });
25
+
26
+ return useComposedEventHandler([
27
+ screenScrollHandler,
28
+ externalScrollHandler ?? null,
29
+ onScroll ? jsScrollHandler : null,
30
+ ]);
31
+ }
@@ -4,8 +4,14 @@ export type { ScreenContextValue } from "./ScreenContext";
4
4
  export { useScreen } from "./ScreenContext";
5
5
  export type { ScreenFlatListProps } from "./ScreenFlatList";
6
6
  export { ScreenFlatList } from "./ScreenFlatList";
7
- export type { ScreenListProps } from "./ScreenList";
7
+ export type {
8
+ ScreenCustomListProps,
9
+ ScreenListAdapterProps,
10
+ ScreenListProps,
11
+ ScreenListRenderItemInfo,
12
+ ScreenListViewToken,
13
+ } from "./ScreenList";
8
14
  export { ScreenList } from "./ScreenList";
9
15
  export type { ScreenScrollViewProps } from "./ScreenScrollView";
10
16
  export { ScreenScrollView } from "./ScreenScrollView";
11
- export type { ScreenMargins, ScreenScrollableProps } from "./types";
17
+ export type { ScreenContentPadding, ScreenMargins, ScreenSafeArea, ScreenScrollableProps } from "./types";
@@ -0,0 +1,49 @@
1
+ import { type PropsWithChildren, useCallback, useContext, useState } from "react";
2
+ import { type LayoutChangeEvent, View } from "react-native";
3
+
4
+ import Animated from "react-native-reanimated";
5
+ import { StyleSheet } from "react-native-unistyles";
6
+
7
+ import type { ScreenSafeAreaEdges } from "../types";
8
+ import { ScreenHeaderAppearanceContext } from "./ScreenHeaderAppearance";
9
+ import { ScreenSafeArea } from "./ScreenSafeArea";
10
+
11
+ export function ScreenFrame({ children, edges }: PropsWithChildren<{ edges: ScreenSafeAreaEdges }>) {
12
+ const appearance = useContext(ScreenHeaderAppearanceContext);
13
+ const [topInset, setTopInset] = useState(0);
14
+ const onLayout = useCallback((event: LayoutChangeEvent) => {
15
+ // This is the actual native padding, including navigator and orientation changes.
16
+ setTopInset(event.nativeEvent.layout.y);
17
+ }, []);
18
+
19
+ return (
20
+ <>
21
+ <ScreenSafeArea edges={edges}>
22
+ <View style={styles.content} onLayout={onLayout}>
23
+ {children}
24
+ </View>
25
+ </ScreenSafeArea>
26
+ {edges.top && appearance?.background !== undefined && (
27
+ <Animated.View
28
+ pointerEvents="none"
29
+ style={[
30
+ styles.topSafeArea(topInset, appearance.background.color),
31
+ appearance.background.animatedStyle,
32
+ ]}
33
+ />
34
+ )}
35
+ </>
36
+ );
37
+ }
38
+
39
+ const styles = StyleSheet.create(() => ({
40
+ content: { flex: 1 },
41
+ topSafeArea: (height: number, backgroundColor: string) => ({
42
+ position: "absolute",
43
+ top: 0,
44
+ left: 0,
45
+ right: 0,
46
+ height,
47
+ backgroundColor,
48
+ }),
49
+ }));
@@ -0,0 +1,52 @@
1
+ import {
2
+ createContext,
3
+ type Dispatch,
4
+ type PropsWithChildren,
5
+ type SetStateAction,
6
+ useContext,
7
+ useLayoutEffect,
8
+ useMemo,
9
+ useState,
10
+ } from "react";
11
+
12
+ import type { useAnimatedStyle } from "react-native-reanimated";
13
+
14
+ type HeaderOpacityStyle = ReturnType<typeof useAnimatedStyle<{ opacity: number }>>;
15
+
16
+ interface HeaderBackground {
17
+ color: string;
18
+ animatedStyle?: HeaderOpacityStyle;
19
+ }
20
+
21
+ interface HeaderAppearance {
22
+ background: HeaderBackground | undefined;
23
+ setBackground: Dispatch<SetStateAction<HeaderBackground | undefined>>;
24
+ }
25
+
26
+ export const ScreenHeaderAppearanceContext = createContext<HeaderAppearance | null>(null);
27
+ const HeaderSlotContext = createContext<HeaderAppearance["setBackground"] | undefined>(undefined);
28
+
29
+ export function ScreenHeaderAppearanceProvider({ children }: PropsWithChildren) {
30
+ const [background, setBackground] = useState<HeaderBackground>();
31
+ const value = useMemo(() => ({ background, setBackground }), [background]);
32
+ return (
33
+ <ScreenHeaderAppearanceContext.Provider value={value}>{children}</ScreenHeaderAppearanceContext.Provider>
34
+ );
35
+ }
36
+
37
+ /** Only the designated header slot controls the screen's top safe-area color. */
38
+ export function ScreenHeaderAppearanceSlot({ children }: PropsWithChildren) {
39
+ const appearance = useContext(ScreenHeaderAppearanceContext);
40
+ return (
41
+ <HeaderSlotContext.Provider value={appearance?.setBackground}>{children}</HeaderSlotContext.Provider>
42
+ );
43
+ }
44
+
45
+ /** Share the overlay's animated style so safe-area fading stays on the UI thread. */
46
+ export function useHeaderBackground(color: string, animatedStyle?: HeaderOpacityStyle) {
47
+ const setBackground = useContext(HeaderSlotContext);
48
+ useLayoutEffect(() => {
49
+ setBackground?.({ color, animatedStyle });
50
+ return () => setBackground?.(undefined);
51
+ }, [color, animatedStyle, setBackground]);
52
+ }
@@ -7,36 +7,36 @@ import { Spinner } from "../../spinner";
7
7
  import type { ScreenScrollableProps } from "../types";
8
8
 
9
9
  /**
10
- * Centres a loading/empty placeholder in whatever space the content leaves.
11
- * Grows so the placeholder sits in the middle of the content area rather than
12
- * hugging the top, which needs the content container to grow as well.
10
+ * Centres a placeholder in the available space. The parent must also grow
11
+ * to fill the viewport.
13
12
  */
14
13
  export function ScreenPlaceholder({ children }: { children: ReactNode }) {
15
14
  return <View style={styles.placeholder}>{children}</View>;
16
15
  }
17
16
 
18
- interface PlaceholderInput
19
- extends Pick<ScreenScrollableProps, "loading" | "LoadingComponent" | "EmptyComponent"> {
17
+ interface ScreenPlaceholderOptions
18
+ extends Pick<ScreenScrollableProps, "isLoading" | "loadingContent" | "emptyContent"> {
20
19
  /** Whether the container has nothing to render (empty `data`, no children). */
21
20
  isEmpty: boolean;
22
21
  }
23
22
 
24
23
  /**
25
- * The placeholder a scroll container should show instead of its content, or
26
- * `null` to render the content. Loading wins over empty: while data is in
27
- * flight there is nothing to be empty about yet.
24
+ * Resolves the loading or empty placeholder, or null to keep the content.
25
+ * Loading takes precedence over the empty state.
28
26
  */
29
27
  export function renderScreenPlaceholder({
30
- loading,
31
- LoadingComponent,
32
- EmptyComponent,
28
+ isLoading,
29
+ loadingContent,
30
+ emptyContent,
33
31
  isEmpty,
34
- }: PlaceholderInput): ReactElement | null {
35
- if (loading) {
36
- return <ScreenPlaceholder>{LoadingComponent ?? <Spinner />}</ScreenPlaceholder>;
32
+ }: ScreenPlaceholderOptions): ReactElement | null {
33
+ if (isLoading) {
34
+ return (
35
+ <ScreenPlaceholder>{loadingContent === undefined ? <Spinner /> : loadingContent}</ScreenPlaceholder>
36
+ );
37
37
  }
38
- if (isEmpty && EmptyComponent) {
39
- return <ScreenPlaceholder>{EmptyComponent}</ScreenPlaceholder>;
38
+ if (isEmpty && emptyContent !== undefined) {
39
+ return <ScreenPlaceholder>{emptyContent}</ScreenPlaceholder>;
40
40
  }
41
41
  return null;
42
42
  }
@@ -0,0 +1,22 @@
1
+ import type { ComponentType, PropsWithChildren } from "react";
2
+ import type { ViewProps } from "react-native";
3
+
4
+ import { StyleSheet } from "react-native-unistyles";
5
+
6
+ import type { ScreenSafeAreaEdges } from "../types";
7
+
8
+ // Keep native codegen declarations outside the library's generated public types.
9
+ const { SafeAreaView } = require("react-native-screens/src/components/safe-area") as {
10
+ SafeAreaView: ComponentType<ViewProps & { edges: ScreenSafeAreaEdges; insetType: "all" }>;
11
+ };
12
+
13
+ /** Android combines system and native interface insets and consumes the selected edges. */
14
+ export function ScreenSafeArea({ children, edges }: PropsWithChildren<{ edges: ScreenSafeAreaEdges }>) {
15
+ return (
16
+ <SafeAreaView edges={edges} insetType="all" style={styles.fill}>
17
+ {children}
18
+ </SafeAreaView>
19
+ );
20
+ }
21
+
22
+ const styles = StyleSheet.create(() => ({ fill: { flex: 1 } }));
@@ -0,0 +1,20 @@
1
+ import type { PropsWithChildren } from "react";
2
+
3
+ import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
4
+ import { StyleSheet } from "react-native-unistyles";
5
+
6
+ import type { ScreenSafeAreaEdges } from "../types";
7
+
8
+ export function ScreenSafeArea({ children, edges }: PropsWithChildren<{ edges: ScreenSafeAreaEdges }>) {
9
+ const enabledEdges = (Object.keys(edges) as (keyof ScreenSafeAreaEdges)[]).filter((edge) => edges[edge]);
10
+ // A local provider measures remaining UIKit/browser insets, including native iOS bars.
11
+ return (
12
+ <SafeAreaProvider style={styles.fill}>
13
+ <SafeAreaView edges={enabledEdges} style={styles.fill}>
14
+ {children}
15
+ </SafeAreaView>
16
+ </SafeAreaProvider>
17
+ );
18
+ }
19
+
20
+ const styles = StyleSheet.create(() => ({ fill: { flex: 1 } }));
@@ -1,42 +1,39 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
- /**
4
- * Which axes of the `layout.screen.margin` token a scroll container pads its
5
- * content with.
6
- *
7
- * `'y'` pads the top and bottom of the content area. It stacks on top of the
8
- * structural insets — the header height and the safe area — which apply
9
- * whether or not margins are on.
10
- */
11
- export type ScreenMargins = "all" | "x" | "y" | "none";
3
+ import type { ScrollHandlerProcessed } from "react-native-reanimated";
12
4
 
13
- /**
14
- * Props shared by `Screen.ScrollView`, `Screen.FlatList` and `Screen.List`, so
15
- * a screen can swap one container for another without relearning its API.
16
- */
5
+ /** Axes that receive the theme's screen content padding. */
6
+ export type ScreenContentPadding = "all" | "x" | "y" | "none";
7
+ /** @deprecated Use ScreenContentPadding. */
8
+ export type ScreenMargins = ScreenContentPadding;
9
+ /** Edges protected by the Screen root. An empty array is equivalent to `"none"`. */
10
+ export type ScreenSafeArea = "auto" | "none" | readonly ("top" | "bottom" | "left" | "right")[];
11
+
12
+ /** Shared content layout and placeholders. Native props retain React Native names. */
17
13
  export interface ScreenScrollableProps {
18
- /** Lay content out along the x axis. Defaults to `false`. */
14
+ /** Scroll horizontally. @defaultValue false */
19
15
  horizontal?: boolean;
20
- /**
21
- * Renders a centred placeholder in the content area instead of the content —
22
- * spares every screen from hand-rolling a loading branch while its data
23
- * arrives. The `Screen.Header` still renders, so the chrome stays put.
24
- */
16
+ /** Replace content with a loading placeholder; `refreshing` keeps list rows visible. */
17
+ isLoading?: boolean;
18
+ /** Loading element. Undefined uses a Spinner; null renders a blank placeholder. */
19
+ loadingContent?: ReactNode;
20
+ /** Empty-state element. Lists otherwise use their native ListEmptyComponent. */
21
+ emptyContent?: ReactNode;
22
+ /** Override the root's themed content-padding axes. */
23
+ contentPadding?: ScreenContentPadding;
24
+ /** Additional bottom padding in layout points, added to themed padding. @defaultValue 0 */
25
+ extraBottomPadding?: number;
26
+ /** UI-thread worklet composed with Screen tracking. Native onScroll runs on the JS thread. */
27
+ scrollHandler?: ScrollHandlerProcessed<Record<string, unknown>>;
28
+ /** @deprecated Use isLoading. Ignored when isLoading is supplied. */
25
29
  loading?: boolean;
26
- /**
27
- * Node shown while `loading`. Defaults to a `Spinner`; it is centred in the
28
- * content area either way.
29
- */
30
+ /** @deprecated Pass an element through loadingContent. */
30
31
  LoadingComponent?: ReactNode;
31
- /**
32
- * Node shown, centred, when there is nothing to render and `loading` is
33
- * false — an empty `data` array on the list containers, no children on
34
- * `Screen.ScrollView`. Omit it to render nothing.
35
- */
32
+ /** @deprecated Pass an element through emptyContent. */
36
33
  EmptyComponent?: ReactNode;
37
- /**
38
- * Which axes get `layout.screen.margin` padding. Defaults to the enclosing
39
- * `Screen`'s `margins`.
40
- */
34
+ /** @deprecated Use contentPadding. */
41
35
  margins?: ScreenMargins;
42
36
  }
37
+
38
+ /** Internal edge flags shared by the platform safe-area wrappers. */
39
+ export type ScreenSafeAreaEdges = Record<"top" | "bottom" | "left" | "right", boolean>;