react-native-terra-ui 0.1.1 → 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 (262) 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/box/Box.js +15 -2
  5. package/lib/module/components/box/Box.js.map +1 -1
  6. package/lib/module/components/box/utils.js +4 -1
  7. package/lib/module/components/box/utils.js.map +1 -1
  8. package/lib/module/components/button/Button.js +7 -14
  9. package/lib/module/components/button/Button.js.map +1 -1
  10. package/lib/module/components/chip/Chip.js +372 -0
  11. package/lib/module/components/chip/Chip.js.map +1 -0
  12. package/lib/module/components/chip/index.js +4 -0
  13. package/lib/module/components/chip/index.js.map +1 -0
  14. package/lib/module/components/chip/types.js +2 -0
  15. package/lib/module/components/chip/types.js.map +1 -0
  16. package/lib/module/components/chip/utils.js +36 -0
  17. package/lib/module/components/chip/utils.js.map +1 -0
  18. package/lib/module/components/empty-state/EmptyState.js +200 -0
  19. package/lib/module/components/empty-state/EmptyState.js.map +1 -0
  20. package/lib/module/components/empty-state/index.js +4 -0
  21. package/lib/module/components/empty-state/index.js.map +1 -0
  22. package/lib/module/components/empty-state/types.js +2 -0
  23. package/lib/module/components/empty-state/types.js.map +1 -0
  24. package/lib/module/components/header/header-buttons.js +31 -36
  25. package/lib/module/components/header/header-buttons.js.map +1 -1
  26. package/lib/module/components/header/index.js +1 -1
  27. package/lib/module/components/header/index.js.map +1 -1
  28. package/lib/module/components/header/parts/HeaderGradientOverlay.js +38 -0
  29. package/lib/module/components/header/parts/HeaderGradientOverlay.js.map +1 -0
  30. package/lib/module/components/header/variants/LargeTitleHeader/index.js +69 -99
  31. package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
  32. package/lib/module/components/header/variants/TitleHeader/index.js +32 -63
  33. package/lib/module/components/header/variants/TitleHeader/index.js.map +1 -1
  34. package/lib/module/components/icon/Icon.js +20 -8
  35. package/lib/module/components/icon/Icon.js.map +1 -1
  36. package/lib/module/components/image/Image.js +54 -0
  37. package/lib/module/components/image/Image.js.map +1 -0
  38. package/lib/module/components/image/index.js +4 -0
  39. package/lib/module/components/image/index.js.map +1 -0
  40. package/lib/module/components/image/types.js +4 -0
  41. package/lib/module/components/image/types.js.map +1 -0
  42. package/lib/module/components/index.js +4 -0
  43. package/lib/module/components/index.js.map +1 -1
  44. package/lib/module/components/portal/Portal.js +42 -0
  45. package/lib/module/components/portal/Portal.js.map +1 -0
  46. package/lib/module/components/portal/PortalContext.js +1 -16
  47. package/lib/module/components/portal/PortalContext.js.map +1 -1
  48. package/lib/module/components/portal/PortalHost.js +16 -14
  49. package/lib/module/components/portal/PortalHost.js.map +1 -1
  50. package/lib/module/components/portal/PortalProvider.js +51 -19
  51. package/lib/module/components/portal/PortalProvider.js.map +1 -1
  52. package/lib/module/components/portal/index.js +1 -1
  53. package/lib/module/components/portal/index.js.map +1 -1
  54. package/lib/module/components/screen/Screen.js +25 -28
  55. package/lib/module/components/screen/Screen.js.map +1 -1
  56. package/lib/module/components/screen/ScreenContext.js +7 -5
  57. package/lib/module/components/screen/ScreenContext.js.map +1 -1
  58. package/lib/module/components/screen/ScreenFlashList.js +144 -0
  59. package/lib/module/components/screen/ScreenFlashList.js.map +1 -0
  60. package/lib/module/components/screen/ScreenFlatList.js +88 -27
  61. package/lib/module/components/screen/ScreenFlatList.js.map +1 -1
  62. package/lib/module/components/screen/ScreenScrollView.js +101 -34
  63. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  64. package/lib/module/components/screen/index.js +1 -0
  65. package/lib/module/components/screen/index.js.map +1 -1
  66. package/lib/module/components/screen/parts/BottomSafeArea.js +62 -0
  67. package/lib/module/components/screen/parts/BottomSafeArea.js.map +1 -0
  68. package/lib/module/components/screen/parts/ScreenPlaceholder.js +50 -0
  69. package/lib/module/components/screen/parts/ScreenPlaceholder.js.map +1 -0
  70. package/lib/module/components/screen/types.js +4 -0
  71. package/lib/module/components/screen/types.js.map +1 -0
  72. package/lib/module/components/screen/utils.js +12 -9
  73. package/lib/module/components/screen/utils.js.map +1 -1
  74. package/lib/module/components/surface/Surface.js +16 -9
  75. package/lib/module/components/surface/Surface.js.map +1 -1
  76. package/lib/module/components/text/Text.js +23 -5
  77. package/lib/module/components/text/Text.js.map +1 -1
  78. package/lib/module/components/toast/Toast.js +8 -8
  79. package/lib/module/components/toast/Toast.js.map +1 -1
  80. package/lib/module/components/toolbar/Toolbar.js +268 -0
  81. package/lib/module/components/toolbar/Toolbar.js.map +1 -0
  82. package/lib/module/components/toolbar/index.js +4 -0
  83. package/lib/module/components/toolbar/index.js.map +1 -0
  84. package/lib/module/context/ThemeProvider.js +5 -2
  85. package/lib/module/context/ThemeProvider.js.map +1 -1
  86. package/lib/module/{components/icon/utils.js → theme/defaults/icons.js} +1 -1
  87. package/lib/module/theme/defaults/icons.js.map +1 -0
  88. package/lib/module/theme/defaults/image.js +30 -0
  89. package/lib/module/theme/defaults/image.js.map +1 -0
  90. package/lib/module/theme/index.js +1 -1
  91. package/lib/module/theme/{configure.js → registry.js} +21 -28
  92. package/lib/module/theme/registry.js.map +1 -0
  93. package/lib/module/theme/runtime.js +1 -1
  94. package/lib/module/theme/theme.js +91 -10
  95. package/lib/module/theme/theme.js.map +1 -1
  96. package/lib/module/theme/tokens/dark.js +59 -68
  97. package/lib/module/theme/tokens/dark.js.map +1 -1
  98. package/lib/module/theme/tokens/light.js +49 -67
  99. package/lib/module/theme/tokens/light.js.map +1 -1
  100. package/lib/module/theme/tokens/primitives.js +241 -20
  101. package/lib/module/theme/tokens/primitives.js.map +1 -1
  102. package/lib/module/utils/accent-utils.js +4 -4
  103. package/lib/module/utils/accent-utils.js.map +1 -1
  104. package/lib/typescript/src/components/avatar/Avatar.d.ts.map +1 -1
  105. package/lib/typescript/src/components/avatar/types.d.ts +4 -4
  106. package/lib/typescript/src/components/avatar/types.d.ts.map +1 -1
  107. package/lib/typescript/src/components/box/Box.d.ts.map +1 -1
  108. package/lib/typescript/src/components/box/utils.d.ts +6 -1
  109. package/lib/typescript/src/components/box/utils.d.ts.map +1 -1
  110. package/lib/typescript/src/components/button/Button.d.ts +1 -1
  111. package/lib/typescript/src/components/button/Button.d.ts.map +1 -1
  112. package/lib/typescript/src/components/chip/Chip.d.ts +24 -0
  113. package/lib/typescript/src/components/chip/Chip.d.ts.map +1 -0
  114. package/lib/typescript/src/components/chip/index.d.ts +4 -0
  115. package/lib/typescript/src/components/chip/index.d.ts.map +1 -0
  116. package/lib/typescript/src/components/chip/types.d.ts +8 -0
  117. package/lib/typescript/src/components/chip/types.d.ts.map +1 -0
  118. package/lib/typescript/src/components/chip/utils.d.ts +8 -0
  119. package/lib/typescript/src/components/chip/utils.d.ts.map +1 -0
  120. package/lib/typescript/src/components/empty-state/EmptyState.d.ts +45 -0
  121. package/lib/typescript/src/components/empty-state/EmptyState.d.ts.map +1 -0
  122. package/lib/typescript/src/components/empty-state/index.d.ts +4 -0
  123. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -0
  124. package/lib/typescript/src/components/empty-state/types.d.ts +6 -0
  125. package/lib/typescript/src/components/empty-state/types.d.ts.map +1 -0
  126. package/lib/typescript/src/components/header/header-buttons.d.ts +27 -0
  127. package/lib/typescript/src/components/header/header-buttons.d.ts.map +1 -1
  128. package/lib/typescript/src/components/header/index.d.ts +1 -1
  129. package/lib/typescript/src/components/header/index.d.ts.map +1 -1
  130. package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts +7 -0
  131. package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts.map +1 -0
  132. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +1 -1
  133. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
  134. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts +18 -7
  135. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts.map +1 -1
  136. package/lib/typescript/src/components/icon/Icon.d.ts.map +1 -1
  137. package/lib/typescript/src/components/image/Image.d.ts +16 -0
  138. package/lib/typescript/src/components/image/Image.d.ts.map +1 -0
  139. package/lib/typescript/src/components/image/index.d.ts +3 -0
  140. package/lib/typescript/src/components/image/index.d.ts.map +1 -0
  141. package/lib/typescript/src/components/image/types.d.ts +7 -0
  142. package/lib/typescript/src/components/image/types.d.ts.map +1 -0
  143. package/lib/typescript/src/components/index.d.ts +4 -0
  144. package/lib/typescript/src/components/index.d.ts.map +1 -1
  145. package/lib/typescript/src/components/portal/Portal.d.ts +26 -0
  146. package/lib/typescript/src/components/portal/Portal.d.ts.map +1 -0
  147. package/lib/typescript/src/components/portal/PortalContext.d.ts +11 -14
  148. package/lib/typescript/src/components/portal/PortalContext.d.ts.map +1 -1
  149. package/lib/typescript/src/components/portal/PortalHost.d.ts +9 -4
  150. package/lib/typescript/src/components/portal/PortalHost.d.ts.map +1 -1
  151. package/lib/typescript/src/components/portal/PortalProvider.d.ts +3 -3
  152. package/lib/typescript/src/components/portal/PortalProvider.d.ts.map +1 -1
  153. package/lib/typescript/src/components/portal/index.d.ts +4 -2
  154. package/lib/typescript/src/components/portal/index.d.ts.map +1 -1
  155. package/lib/typescript/src/components/screen/Screen.d.ts +18 -13
  156. package/lib/typescript/src/components/screen/Screen.d.ts.map +1 -1
  157. package/lib/typescript/src/components/screen/ScreenContext.d.ts +12 -9
  158. package/lib/typescript/src/components/screen/ScreenContext.d.ts.map +1 -1
  159. package/lib/typescript/src/components/screen/ScreenFlashList.d.ts +43 -0
  160. package/lib/typescript/src/components/screen/ScreenFlashList.d.ts.map +1 -0
  161. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts +17 -7
  162. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts.map +1 -1
  163. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts +51 -16
  164. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  165. package/lib/typescript/src/components/screen/index.d.ts +3 -0
  166. package/lib/typescript/src/components/screen/index.d.ts.map +1 -1
  167. package/lib/typescript/src/components/screen/parts/BottomSafeArea.d.ts +30 -0
  168. package/lib/typescript/src/components/screen/parts/BottomSafeArea.d.ts.map +1 -0
  169. package/lib/typescript/src/components/screen/parts/ScreenPlaceholder.d.ts +22 -0
  170. package/lib/typescript/src/components/screen/parts/ScreenPlaceholder.d.ts.map +1 -0
  171. package/lib/typescript/src/components/screen/types.d.ts +42 -0
  172. package/lib/typescript/src/components/screen/types.d.ts.map +1 -0
  173. package/lib/typescript/src/components/screen/utils.d.ts +9 -9
  174. package/lib/typescript/src/components/screen/utils.d.ts.map +1 -1
  175. package/lib/typescript/src/components/surface/Surface.d.ts +7 -0
  176. package/lib/typescript/src/components/surface/Surface.d.ts.map +1 -1
  177. package/lib/typescript/src/components/text/Text.d.ts.map +1 -1
  178. package/lib/typescript/src/components/toolbar/Toolbar.d.ts +115 -0
  179. package/lib/typescript/src/components/toolbar/Toolbar.d.ts.map +1 -0
  180. package/lib/typescript/src/components/toolbar/index.d.ts +3 -0
  181. package/lib/typescript/src/components/toolbar/index.d.ts.map +1 -0
  182. package/lib/typescript/src/context/ThemeProvider.d.ts.map +1 -1
  183. package/lib/typescript/src/{components/icon/utils.d.ts → theme/defaults/icons.d.ts} +2 -2
  184. package/lib/typescript/src/theme/defaults/icons.d.ts.map +1 -0
  185. package/lib/typescript/src/theme/defaults/image.d.ts +4 -0
  186. package/lib/typescript/src/theme/defaults/image.d.ts.map +1 -0
  187. package/lib/typescript/src/theme/index.d.ts +1 -1
  188. package/lib/typescript/src/theme/index.d.ts.map +1 -1
  189. package/lib/typescript/src/theme/{configure.d.ts → registry.d.ts} +1 -1
  190. package/lib/typescript/src/theme/registry.d.ts.map +1 -0
  191. package/lib/typescript/src/theme/theme.d.ts +21 -1
  192. package/lib/typescript/src/theme/theme.d.ts.map +1 -1
  193. package/lib/typescript/src/theme/tokens/dark.d.ts +41 -64
  194. package/lib/typescript/src/theme/tokens/dark.d.ts.map +1 -1
  195. package/lib/typescript/src/theme/tokens/light.d.ts +44 -65
  196. package/lib/typescript/src/theme/tokens/light.d.ts.map +1 -1
  197. package/lib/typescript/src/theme/tokens/primitives.d.ts +228 -17
  198. package/lib/typescript/src/theme/tokens/primitives.d.ts.map +1 -1
  199. package/lib/typescript/src/theme/types.d.ts +65 -35
  200. package/lib/typescript/src/theme/types.d.ts.map +1 -1
  201. package/package.json +23 -9
  202. package/skills/terra-ui/SKILL.md +193 -0
  203. package/skills/terra-ui/references/recipes.md +198 -0
  204. package/skills/terra-ui/references/troubleshooting.md +112 -0
  205. package/src/components/avatar/Avatar.tsx +128 -38
  206. package/src/components/avatar/types.ts +5 -5
  207. package/src/components/box/Box.tsx +13 -1
  208. package/src/components/box/utils.ts +10 -0
  209. package/src/components/button/Button.tsx +7 -15
  210. package/src/components/chip/Chip.tsx +444 -0
  211. package/src/components/chip/index.ts +3 -0
  212. package/src/components/chip/types.ts +15 -0
  213. package/src/components/chip/utils.ts +34 -0
  214. package/src/components/empty-state/EmptyState.tsx +209 -0
  215. package/src/components/empty-state/index.ts +3 -0
  216. package/src/components/empty-state/types.ts +5 -0
  217. package/src/components/header/header-buttons.tsx +33 -45
  218. package/src/components/header/index.ts +2 -0
  219. package/src/components/header/parts/HeaderGradientOverlay.tsx +29 -0
  220. package/src/components/header/variants/LargeTitleHeader/index.tsx +68 -102
  221. package/src/components/header/variants/TitleHeader/index.tsx +47 -56
  222. package/src/components/icon/Icon.tsx +20 -15
  223. package/src/components/image/Image.tsx +48 -0
  224. package/src/components/image/index.ts +2 -0
  225. package/src/components/image/types.ts +7 -0
  226. package/src/components/index.ts +4 -0
  227. package/src/components/portal/Portal.tsx +55 -0
  228. package/src/components/portal/PortalContext.tsx +14 -22
  229. package/src/components/portal/PortalHost.tsx +17 -10
  230. package/src/components/portal/PortalProvider.tsx +58 -21
  231. package/src/components/portal/index.ts +4 -2
  232. package/src/components/screen/Screen.tsx +45 -59
  233. package/src/components/screen/ScreenContext.tsx +19 -12
  234. package/src/components/screen/ScreenFlashList.tsx +196 -0
  235. package/src/components/screen/ScreenFlatList.tsx +100 -39
  236. package/src/components/screen/ScreenScrollView.tsx +114 -53
  237. package/src/components/screen/index.ts +3 -0
  238. package/src/components/screen/parts/BottomSafeArea.tsx +60 -0
  239. package/src/components/screen/parts/ScreenPlaceholder.tsx +55 -0
  240. package/src/components/screen/types.ts +43 -0
  241. package/src/components/screen/utils.ts +29 -15
  242. package/src/components/surface/Surface.tsx +17 -20
  243. package/src/components/text/Text.tsx +23 -5
  244. package/src/components/toast/Toast.tsx +8 -8
  245. package/src/components/toolbar/Toolbar.tsx +353 -0
  246. package/src/components/toolbar/index.ts +6 -0
  247. package/src/context/ThemeProvider.tsx +5 -4
  248. package/src/{components/icon/utils.tsx → theme/defaults/icons.tsx} +1 -1
  249. package/src/theme/defaults/image.tsx +34 -0
  250. package/src/theme/index.ts +1 -1
  251. package/src/theme/{configure.ts → registry.ts} +19 -34
  252. package/src/theme/runtime.ts +1 -1
  253. package/src/theme/theme.ts +127 -13
  254. package/src/theme/tokens/dark.ts +60 -69
  255. package/src/theme/tokens/light.ts +50 -68
  256. package/src/theme/tokens/primitives.ts +248 -20
  257. package/src/theme/types.ts +67 -35
  258. package/src/utils/accent-utils.ts +4 -4
  259. package/lib/module/components/icon/utils.js.map +0 -1
  260. package/lib/module/theme/configure.js.map +0 -1
  261. package/lib/typescript/src/components/icon/utils.d.ts.map +0 -1
  262. package/lib/typescript/src/theme/configure.d.ts.map +0 -1
@@ -0,0 +1,3 @@
1
+ export type { EmptyStateProps } from './EmptyState';
2
+ export { EmptyState } from './EmptyState';
3
+ export type { EmptyStateSize } from './types';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Overall scale of an empty state — drives the icon size, the typography of the
3
+ * title/description, and the spacing between them.
4
+ */
5
+ export type EmptyStateSize = 'sm' | 'md' | 'lg';
@@ -1,10 +1,34 @@
1
- import { Pressable } from 'react-native';
2
-
3
- import { StyleSheet, useUnistyles } from 'react-native-unistyles';
4
-
5
1
  import type { TerraSemanticIconName } from '#theme/types';
6
2
 
7
- import { Icon } from '../icon';
3
+ import { Toolbar, type ToolbarButtonProps } from '../toolbar';
4
+
5
+ /**
6
+ * @deprecated Use {@link Toolbar.Button}. `HeaderButton` is an alias kept for
7
+ * compatibility and renders identically; `Toolbar.Button` additionally goes
8
+ * transparent inside a `Toolbar.Group` so several actions can share one
9
+ * capsule.
10
+ */
11
+ export type HeaderButtonProps = ToolbarButtonProps;
12
+
13
+ /**
14
+ * @deprecated Use {@link Toolbar.Button}, which is the same component under a
15
+ * name that also covers toolbars outside a header.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <Header.Title
20
+ * title="Inbox"
21
+ * RightToolbar={
22
+ * <Toolbar.Button
23
+ * icon="status.info"
24
+ * accessibilityLabel="Info"
25
+ * onPress={showInfo}
26
+ * />
27
+ * }
28
+ * />
29
+ * ```
30
+ */
31
+ export const HeaderButton = Toolbar.Button;
8
32
 
9
33
  export type HeaderDismissAction = 'back' | 'close' | 'none';
10
34
 
@@ -32,21 +56,11 @@ const getDismissIconName = (
32
56
  ): TerraSemanticIconName =>
33
57
  dismissAction === 'close' ? 'navigation.close' : 'navigation.back';
34
58
 
35
- const warnUnavailableDismiss = (dismissAction: HeaderDismissAction): void => {
36
- if (__DEV__) {
37
- console.warn(
38
- `[react-native-terra-ui] Header.Title dismissAction="${dismissAction}" was pressed, but no dismiss handler or available navigation action was provided.`
39
- );
40
- }
41
- };
42
-
43
59
  export function HeaderDismissButton({
44
60
  dismissAction = 'none',
45
61
  navigation,
46
62
  onDismiss,
47
63
  }: HeaderDismissProps) {
48
- const { theme } = useUnistyles();
49
-
50
64
  if (dismissAction === 'none') return null;
51
65
 
52
66
  const handlePress = () => {
@@ -64,40 +78,14 @@ export function HeaderDismissButton({
64
78
  navigation.goBack();
65
79
  return;
66
80
  }
67
-
68
- warnUnavailableDismiss(dismissAction);
69
81
  };
70
82
 
71
83
  return (
72
- <Pressable
84
+ <Toolbar.Button
85
+ icon={getDismissIconName(dismissAction)}
86
+ iconSize={24}
73
87
  accessibilityLabel={getDismissAccessibilityLabel(dismissAction)}
74
- accessibilityRole="button"
75
- hitSlop={8}
76
88
  onPress={handlePress}
77
- style={({ pressed }) => [
78
- styles.button,
79
- pressed ? { opacity: theme.opacity.pressed } : null,
80
- ]}
81
- >
82
- <Icon
83
- size={26}
84
- color="text.default"
85
- name={getDismissIconName(dismissAction)}
86
- />
87
- </Pressable>
89
+ />
88
90
  );
89
91
  }
90
-
91
- const styles = StyleSheet.create((theme) => ({
92
- button: {
93
- width: 36,
94
- height: 36,
95
- borderRadius: theme.radius.full,
96
- backgroundColor:
97
- (theme.color as unknown as Record<string, string | undefined>)[
98
- 'surface.default'
99
- ] ?? '',
100
- alignItems: 'center',
101
- justifyContent: 'center',
102
- },
103
- }));
@@ -1,5 +1,7 @@
1
1
  export { Header } from './Header';
2
2
  export {
3
+ HeaderButton,
4
+ type HeaderButtonProps,
3
5
  type HeaderDismissAction,
4
6
  HeaderDismissButton,
5
7
  type HeaderDismissNavigation,
@@ -0,0 +1,29 @@
1
+ import { useId } from 'react';
2
+ import Svg, {
3
+ Defs,
4
+ Rect,
5
+ Stop,
6
+ LinearGradient as SvgLinearGradient,
7
+ } from 'react-native-svg';
8
+
9
+ export interface HeaderGradientOverlayProps {
10
+ /** Fill color, fading from fully opaque to 90% opacity along the gradient. */
11
+ color: string;
12
+ }
13
+
14
+ /** Vertical fade-out overlay behind header content, shared by header variants. */
15
+ export function HeaderGradientOverlay({ color }: HeaderGradientOverlayProps) {
16
+ const gradientId = useId();
17
+
18
+ return (
19
+ <Svg height="100%" width="100%">
20
+ <Defs>
21
+ <SvgLinearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
22
+ <Stop offset="0" stopColor={color} stopOpacity="1" />
23
+ <Stop offset="1" stopColor={color} stopOpacity="0.9" />
24
+ </SvgLinearGradient>
25
+ </Defs>
26
+ <Rect width="100%" height="100%" fill={`url(#${gradientId})`} />
27
+ </Svg>
28
+ );
29
+ }
@@ -1,26 +1,24 @@
1
- import { useCallback, useEffect, useId } from 'react';
2
- import { type LayoutChangeEvent, Text, View } from 'react-native';
1
+ import { useEffect } from 'react';
2
+ import { Text, View } from 'react-native';
3
3
 
4
4
  import Animated, {
5
5
  interpolate,
6
6
  useAnimatedStyle,
7
7
  useSharedValue,
8
8
  } from 'react-native-reanimated';
9
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
10
- import Svg, {
11
- Defs,
12
- Rect,
13
- Stop,
14
- LinearGradient as SvgLinearGradient,
15
- } from 'react-native-svg';
16
9
  import { StyleSheet, useUnistyles } from 'react-native-unistyles';
17
10
 
18
11
  import { resolveThemeColor } from '#utils/resolve-theme-color';
19
12
  import { FONT_WEIGHT_VALUE } from '#utils/typography';
20
13
 
21
- import { usePortal } from '../../../portal';
22
- import { useScreen } from '../../../screen/ScreenContext';
14
+ import { Portal } from '../../../portal';
15
+ import {
16
+ SCREEN_HEADER_PORTAL_HOST,
17
+ useScreen,
18
+ } from '../../../screen/ScreenContext';
19
+ import { Toolbar } from '../../../toolbar';
23
20
  import { HeaderDismissButton } from '../../header-buttons';
21
+ import { HeaderGradientOverlay } from '../../parts/HeaderGradientOverlay';
24
22
  import type { TitleHeaderProps } from '../TitleHeader';
25
23
 
26
24
  const AnimatedText = Animated.createAnimatedComponent(Text);
@@ -40,15 +38,16 @@ export interface LargeTitleHeaderProps extends TitleHeaderProps {
40
38
  function LargeTitlePortalContent({
41
39
  title,
42
40
  caption,
43
- onLayout,
44
- }: Pick<LargeTitleHeaderProps, 'title' | 'caption'> & {
45
- onLayout: (event: LayoutChangeEvent) => void;
46
- }) {
41
+ titleAlignment = 'center',
42
+ }: Pick<LargeTitleHeaderProps, 'title' | 'caption' | 'titleAlignment'>) {
47
43
  const { theme } = useUnistyles();
48
- const { scrollY, headerCollapseHeight } = useScreen();
44
+ const { scrollY } = useScreen();
45
+ styles.useVariants({ titleAlignment });
46
+
47
+ const headerCollapseHeight = theme.layout.header.height;
49
48
 
50
49
  const largeTitleAnimatedStyle = useAnimatedStyle(() => {
51
- const height = headerCollapseHeight.value;
50
+ const height = headerCollapseHeight;
52
51
  if (height <= 0) {
53
52
  return { opacity: 1, transform: [{ translateY: 0 }] };
54
53
  }
@@ -69,10 +68,7 @@ function LargeTitlePortalContent({
69
68
  });
70
69
 
71
70
  return (
72
- <Animated.View
73
- style={[styles.portalContent, largeTitleAnimatedStyle]}
74
- onLayout={onLayout}
75
- >
71
+ <Animated.View style={[styles.portalContent, largeTitleAnimatedStyle]}>
76
72
  <Text
77
73
  style={styles.largeTitle}
78
74
  maxFontSizeMultiplier={
@@ -114,8 +110,8 @@ function LargeTitlePortalContent({
114
110
  export function LargeTitleHeader({
115
111
  title,
116
112
  caption,
117
- LeftComponent,
118
- RightComponent,
113
+ LeftToolbar,
114
+ RightToolbar,
119
115
  dismissAction = 'none',
120
116
  navigation,
121
117
  onDismiss,
@@ -124,37 +120,27 @@ export function LargeTitleHeader({
124
120
  bg = 'surface.default',
125
121
  }: LargeTitleHeaderProps) {
126
122
  const { theme } = useUnistyles();
127
- const { top } = useSafeAreaInsets();
128
- const gradientId = useId();
123
+
129
124
  const bgColor =
130
125
  resolveThemeColor(bg, theme) ??
131
126
  (theme.color as unknown as Record<string, string | undefined>)[
132
127
  'surface.default'
133
128
  ] ??
134
129
  '';
130
+
135
131
  const isTransparent = bgColor === 'transparent';
136
132
  const gradientColor = isTransparent ? theme.color.background : bgColor;
137
- const { scrollY, headerCollapseHeight, setHeaderCollapseHeight } =
138
- useScreen();
139
- const { registerContent, unregisterContent } = usePortal();
140
- const portalId = useId();
133
+ const { scrollY } = useScreen();
141
134
  const isLargeTitleHiddenValue = useSharedValue(isLargeTitleHidden);
142
-
135
+ const headerCollapseHeight = theme.layout.header.height;
143
136
  styles.useVariants({ titleAlignment });
144
137
 
145
- const handlePortalLayout = useCallback(
146
- (event: LayoutChangeEvent) => {
147
- setHeaderCollapseHeight(event.nativeEvent.layout.height);
148
- },
149
- [setHeaderCollapseHeight]
150
- );
151
-
152
138
  // Animate the gradient overlay's opacity: 0 (expanded) → 1 (collapsed).
153
139
  const gradientOpacityStyle = useAnimatedStyle(() => {
154
140
  if (isLargeTitleHiddenValue.value) {
155
141
  return { opacity: 1 };
156
142
  }
157
- const height = headerCollapseHeight.value;
143
+ const height = headerCollapseHeight;
158
144
  if (height <= 0) {
159
145
  return { opacity: 0 };
160
146
  }
@@ -168,7 +154,7 @@ export function LargeTitleHeader({
168
154
  return { opacity: 1, transform: [{ translateY: 0 }] };
169
155
  }
170
156
 
171
- const height = headerCollapseHeight.value;
157
+ const height = headerCollapseHeight;
172
158
  if (height <= 0) {
173
159
  return {
174
160
  opacity: 0,
@@ -195,35 +181,10 @@ export function LargeTitleHeader({
195
181
  isLargeTitleHiddenValue.value = isLargeTitleHidden;
196
182
  }, [isLargeTitleHidden, isLargeTitleHiddenValue]);
197
183
 
198
- useEffect(() => {
199
- if (isLargeTitleHidden) {
200
- setHeaderCollapseHeight(0);
201
- unregisterContent(portalId);
202
- return;
203
- }
204
-
205
- setHeaderCollapseHeight(0);
206
-
207
- registerContent(
208
- portalId,
209
- <LargeTitlePortalContent
210
- title={title}
211
- caption={caption}
212
- onLayout={handlePortalLayout}
213
- />
214
- );
215
-
216
- return () => unregisterContent(portalId);
217
- }, [
218
- portalId,
219
- title,
220
- caption,
221
- isLargeTitleHidden,
222
- setHeaderCollapseHeight,
223
- handlePortalLayout,
224
- registerContent,
225
- unregisterContent,
226
- ]);
184
+ // // biome-ignore lint/correctness/useExhaustiveDependencies: reset stale collapse height whenever content that resizes the portal (title/caption/visibility) changes, ahead of remeasurement
185
+ // useEffect(() => {
186
+ // setHeaderCollapseHeight(0);
187
+ // }, [title, caption, isLargeTitleHidden, setHeaderCollapseHeight]);
227
188
 
228
189
  const leading =
229
190
  dismissAction === 'back' ? (
@@ -233,16 +194,16 @@ export function LargeTitleHeader({
233
194
  navigation={navigation}
234
195
  onDismiss={onDismiss}
235
196
  />
236
- {LeftComponent}
197
+ {LeftToolbar}
237
198
  </>
238
199
  ) : (
239
- LeftComponent
200
+ LeftToolbar
240
201
  );
241
202
 
242
203
  const trailing =
243
204
  dismissAction === 'close' ? (
244
205
  <>
245
- {RightComponent}
206
+ {RightToolbar}
246
207
  <HeaderDismissButton
247
208
  dismissAction={dismissAction}
248
209
  navigation={navigation}
@@ -250,33 +211,31 @@ export function LargeTitleHeader({
250
211
  />
251
212
  </>
252
213
  ) : (
253
- RightComponent
214
+ RightToolbar
254
215
  );
255
216
 
256
217
  return (
257
- <View style={styles.safeArea}>
218
+ <View style={styles.container}>
219
+ {!isLargeTitleHidden && (
220
+ <Portal hostName={SCREEN_HEADER_PORTAL_HOST}>
221
+ <LargeTitlePortalContent
222
+ title={title}
223
+ caption={caption}
224
+ titleAlignment={titleAlignment}
225
+ />
226
+ </Portal>
227
+ )}
258
228
  <Animated.View
259
- style={[
260
- { height: top, backgroundColor: gradientColor },
261
- gradientOpacityStyle,
262
- ]}
229
+ style={[styles.safeArea(gradientColor), gradientOpacityStyle]}
263
230
  />
264
231
  <View style={styles.bar}>
265
232
  <Animated.View
266
233
  style={[styles.gradient, gradientOpacityStyle]}
267
234
  pointerEvents="none"
268
235
  >
269
- <Svg height="100%" width="100%">
270
- <Defs>
271
- <SvgLinearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
272
- <Stop offset="0" stopColor={gradientColor} stopOpacity="1" />
273
- <Stop offset="1" stopColor={gradientColor} stopOpacity="0" />
274
- </SvgLinearGradient>
275
- </Defs>
276
- <Rect width="100%" height="100%" fill={`url(#${gradientId})`} />
277
- </Svg>
236
+ <HeaderGradientOverlay color={gradientColor} />
278
237
  </Animated.View>
279
- {leading != null && <View style={styles.slot}>{leading}</View>}
238
+ {leading != null && <Toolbar>{leading}</Toolbar>}
280
239
  <Animated.View
281
240
  style={[styles.titleContainer, compactTitleAnimatedStyle]}
282
241
  pointerEvents="none"
@@ -290,20 +249,26 @@ export function LargeTitleHeader({
290
249
  {title}
291
250
  </AnimatedText>
292
251
  </Animated.View>
293
- {trailing != null && <View style={styles.slotEnd}>{trailing}</View>}
252
+ {trailing != null && (
253
+ <Toolbar style={styles.trailing}>{trailing}</Toolbar>
254
+ )}
294
255
  </View>
295
256
  </View>
296
257
  );
297
258
  }
298
259
 
299
- const styles = StyleSheet.create((theme) => ({
300
- safeArea: {
260
+ const styles = StyleSheet.create((theme, runtime) => ({
261
+ container: {
301
262
  position: 'absolute',
302
263
  top: 0,
303
264
  left: 0,
304
265
  right: 0,
305
266
  zIndex: 1000,
306
267
  },
268
+ safeArea: (bgColor) => ({
269
+ height: runtime.insets.top,
270
+ backgroundColor: bgColor,
271
+ }),
307
272
  gradient: {
308
273
  position: 'absolute',
309
274
  top: 0,
@@ -315,24 +280,18 @@ const styles = StyleSheet.create((theme) => ({
315
280
  height: theme.layout.header.height,
316
281
  flexDirection: 'row',
317
282
  alignItems: 'center',
318
- justifyContent: 'space-between',
319
283
  paddingHorizontal: theme.layout.screen.margin.x,
320
284
  gap: theme.spacing['3'],
285
+ justifyContent: 'space-between',
321
286
  },
322
- slot: {
323
- flexDirection: 'row',
324
- alignItems: 'center',
325
- gap: theme.spacing['2'],
326
- },
327
- slotEnd: {
328
- flexDirection: 'row',
329
- alignItems: 'center',
287
+ trailing: {
330
288
  justifyContent: 'flex-end',
331
- gap: theme.spacing['2'],
332
289
  },
333
290
  portalContent: {
334
- marginTop: theme.spacing['1'],
335
- marginBottom: theme.spacing['3'],
291
+ minHeight: theme.layout.header.height,
292
+ // paddingTop: theme.spacing['1'],
293
+ paddingBottom: theme.spacing['3'],
294
+ paddingHorizontal: theme.layout.screen.margin.x,
336
295
  },
337
296
  largeTitle: {
338
297
  fontSize: theme.typography.variants['headline-lg'].fontSize,
@@ -353,6 +312,12 @@ const styles = StyleSheet.create((theme) => ({
353
312
  (theme.color as unknown as Record<string, string | undefined>)[
354
313
  'text.subtle'
355
314
  ] ?? '',
315
+ variants: {
316
+ titleAlignment: {
317
+ center: { textAlign: 'center' },
318
+ left: { textAlign: 'left' },
319
+ },
320
+ },
356
321
  },
357
322
  compactTitle: {
358
323
  fontSize: theme.typography.variants['title-md'].fontSize,
@@ -372,6 +337,7 @@ const styles = StyleSheet.create((theme) => ({
372
337
  },
373
338
  },
374
339
  titleContainer: {
340
+ flex: 1,
375
341
  variants: {
376
342
  titleAlignment: {
377
343
  center: {
@@ -1,30 +1,31 @@
1
- import { type ReactNode, useId } from 'react';
1
+ import type { ReactNode } from 'react';
2
2
  import { Text, View } from 'react-native';
3
3
 
4
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
- import Svg, {
6
- Defs,
7
- Rect,
8
- Stop,
9
- LinearGradient as SvgLinearGradient,
10
- } from 'react-native-svg';
11
4
  import { StyleSheet, useUnistyles } from 'react-native-unistyles';
12
5
 
13
6
  import type { ColorToken } from '#theme/types';
14
7
  import { resolveThemeColor } from '#utils/resolve-theme-color';
15
8
  import { FONT_WEIGHT_VALUE } from '#utils/typography';
16
9
 
10
+ import { Toolbar } from '../../../toolbar';
17
11
  import {
18
12
  HeaderDismissButton,
19
13
  type HeaderDismissProps,
20
14
  } from '../../header-buttons';
15
+ import { HeaderGradientOverlay } from '../../parts/HeaderGradientOverlay';
21
16
 
22
17
  export interface TitleHeaderProps extends HeaderDismissProps {
23
18
  title?: string;
24
- /** Content on the leading side (e.g. a back button). */
25
- LeftComponent?: ReactNode;
26
- /** Content on the trailing side (e.g. an action). */
27
- RightComponent?: ReactNode;
19
+ /**
20
+ * Leading-side actions, wrapped in a `Toolbar` row. Pass `Toolbar.Group` /
21
+ * `Toolbar.Button` children directly no `Toolbar` of your own.
22
+ */
23
+ LeftToolbar?: ReactNode;
24
+ /**
25
+ * Trailing-side actions, wrapped in a `Toolbar` row. Pass `Toolbar.Group` /
26
+ * `Toolbar.Button` children directly — no `Toolbar` of your own.
27
+ */
28
+ RightToolbar?: ReactNode;
28
29
  /** Title placement within the bar. Defaults to `'center'`. */
29
30
  titleAlignment?: 'center' | 'left';
30
31
  /** Background color token. Defaults to `surface.default`. */
@@ -39,14 +40,19 @@ export interface TitleHeaderProps extends HeaderDismissProps {
39
40
  * @example
40
41
  * ```tsx
41
42
  * <Screen.Header>
42
- * <Header.Title title="Stats" RightComponent={<Button>Edit</Button>} />
43
+ * <Header.Title
44
+ * title="Stats"
45
+ * RightToolbar={
46
+ * <Toolbar.Button icon="add" accessibilityLabel="Add" onPress={add} />
47
+ * }
48
+ * />
43
49
  * </Screen.Header>
44
50
  * ```
45
51
  */
46
52
  export function TitleHeader({
47
53
  title,
48
- LeftComponent,
49
- RightComponent,
54
+ LeftToolbar,
55
+ RightToolbar,
50
56
  dismissAction = 'none',
51
57
  navigation,
52
58
  onDismiss,
@@ -54,9 +60,13 @@ export function TitleHeader({
54
60
  bg = 'surface.default',
55
61
  }: TitleHeaderProps) {
56
62
  const { theme } = useUnistyles();
57
- const { top } = useSafeAreaInsets();
58
- const gradientId = useId();
59
- const bgColor = resolveThemeColor(bg, theme) as string;
63
+
64
+ const bgColor =
65
+ resolveThemeColor(bg, theme) ??
66
+ (theme.color as unknown as Record<string, string | undefined>)[
67
+ 'surface.default'
68
+ ] ??
69
+ '';
60
70
  const isTransparent = bgColor === 'transparent';
61
71
  const gradientColor = isTransparent
62
72
  ? theme.color['surface.default']
@@ -71,16 +81,16 @@ export function TitleHeader({
71
81
  navigation={navigation}
72
82
  onDismiss={onDismiss}
73
83
  />
74
- {LeftComponent}
84
+ {LeftToolbar}
75
85
  </>
76
86
  ) : (
77
- LeftComponent
87
+ LeftToolbar
78
88
  );
79
89
 
80
90
  const trailing =
81
91
  dismissAction === 'close' ? (
82
92
  <>
83
- {RightComponent}
93
+ {RightToolbar}
84
94
  <HeaderDismissButton
85
95
  dismissAction={dismissAction}
86
96
  navigation={navigation}
@@ -88,50 +98,39 @@ export function TitleHeader({
88
98
  />
89
99
  </>
90
100
  ) : (
91
- RightComponent
101
+ RightToolbar
92
102
  );
93
103
  return (
94
- <View style={styles.safeArea}>
95
- <View
96
- style={{
97
- height: top,
98
- backgroundColor: isTransparent ? 'transparent' : bgColor,
99
- }}
100
- />
104
+ <View style={styles.container}>
105
+ <View style={styles.safeArea(isTransparent, bgColor)} />
101
106
  <View style={styles.bar}>
102
107
  <View style={styles.gradient} pointerEvents="none">
103
- <Svg height="100%" width="100%">
104
- <Defs>
105
- <SvgLinearGradient id={gradientId} x1="0" y1="0.4" x2="0" y2="1">
106
- <Stop offset="0" stopColor={gradientColor} stopOpacity="0.9" />
107
- <Stop
108
- offset="0.9"
109
- stopColor={gradientColor}
110
- stopOpacity="0.1"
111
- />
112
- </SvgLinearGradient>
113
- </Defs>
114
- <Rect width="100%" height="100%" fill={`url(#${gradientId})`} />
115
- </Svg>
108
+ <HeaderGradientOverlay color={gradientColor} />
116
109
  </View>
117
- {leading != null && <View style={styles.slot}>{leading}</View>}
110
+ {leading != null && <Toolbar>{leading}</Toolbar>}
118
111
  <View style={styles.titleContainer} pointerEvents="none">
119
112
  {!!title && <Text style={styles.title}>{title}</Text>}
120
113
  </View>
121
- {trailing != null && <View style={styles.slotEnd}>{trailing}</View>}
114
+ {trailing != null && (
115
+ <Toolbar style={styles.trailing}>{trailing}</Toolbar>
116
+ )}
122
117
  </View>
123
118
  </View>
124
119
  );
125
120
  }
126
121
 
127
- const styles = StyleSheet.create((theme) => ({
128
- safeArea: {
122
+ const styles = StyleSheet.create((theme, runtime) => ({
123
+ container: {
129
124
  position: 'absolute',
130
125
  top: 0,
131
126
  left: 0,
132
127
  right: 0,
133
128
  zIndex: 1000,
134
129
  },
130
+ safeArea: (isTransparent, bgColor) => ({
131
+ height: runtime.insets.top,
132
+ backgroundColor: isTransparent ? 'transparent' : bgColor,
133
+ }),
135
134
  gradient: {
136
135
  position: 'absolute',
137
136
  top: 0,
@@ -143,20 +142,12 @@ const styles = StyleSheet.create((theme) => ({
143
142
  height: theme.layout.header.height,
144
143
  flexDirection: 'row',
145
144
  alignItems: 'center',
146
- justifyContent: 'space-between',
147
145
  paddingHorizontal: theme.layout.screen.margin.x,
148
146
  gap: theme.spacing['3'],
147
+ justifyContent: 'space-between',
149
148
  },
150
- slot: {
151
- flexDirection: 'row',
152
- alignItems: 'center',
153
- gap: theme.spacing['2'],
154
- },
155
- slotEnd: {
156
- flexDirection: 'row',
157
- alignItems: 'center',
149
+ trailing: {
158
150
  justifyContent: 'flex-end',
159
- gap: theme.spacing['2'],
160
151
  },
161
152
  titleContainer: {
162
153
  variants: {