@momo-kits/foundation 0.169.9 → 0.169.10

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.
@@ -1,4 +1,5 @@
1
- import { StyleSheet } from 'react-native';
1
+ import { StyleSheet, View } from 'react-native';
2
+ import LinearGradient from 'react-native-linear-gradient';
2
3
  import Animated, {
3
4
  Extrapolation,
4
5
  interpolate,
@@ -9,6 +10,7 @@ import React from 'react';
9
10
  import { Image } from '../../Image';
10
11
  import { Colors } from '../../Consts';
11
12
  import { useNormalizedSharedValue } from '../hooks';
13
+ import { HeaderBarMetrics, useHeaderLayout } from '../headerLayout';
12
14
 
13
15
  const ASPECT_RATIOS: Record<
14
16
  NonNullable<HeaderAnimatedProps['aspectRatio']>,
@@ -21,16 +23,20 @@ const ASPECT_RATIOS: Record<
21
23
 
22
24
  /**
23
25
  * Parallax header image (or `children`): stretches when pulled down and fades out
24
- * as the content scrolls up.
26
+ * as the content scrolls up. Wide windows and the iPhone Duo inner display keep
27
+ * it at phone size, centered.
25
28
  */
26
29
  const HeaderAnimated: React.FC<HeaderAnimatedProps> = ({
27
30
  animatedValue,
28
31
  image,
29
32
  useScale = true,
30
33
  aspectRatio = '16/9',
34
+ bannerBackgroundColor,
31
35
  children,
32
36
  }) => {
33
37
  const sharedValue = useNormalizedSharedValue(animatedValue);
38
+ const { isBannerCentered, bannerWidth } = useHeaderLayout();
39
+ const ratio = ASPECT_RATIOS[aspectRatio];
34
40
  const animatedStyle = useAnimatedStyle(() => {
35
41
  let scale = 1;
36
42
  if (useScale) {
@@ -59,22 +65,36 @@ const HeaderAnimated: React.FC<HeaderAnimatedProps> = ({
59
65
  };
60
66
  });
61
67
 
68
+ const content = children ?? (
69
+ <Image
70
+ source={{
71
+ uri: image,
72
+ }}
73
+ style={styles.full}
74
+ />
75
+ );
76
+
77
+ if (!isBannerCentered) {
78
+ return (
79
+ <Animated.View
80
+ style={[styles.container, { aspectRatio: ratio }, animatedStyle]}
81
+ >
82
+ {content}
83
+ </Animated.View>
84
+ );
85
+ }
86
+
62
87
  return (
63
88
  <Animated.View
64
- style={[
65
- styles.container,
66
- { aspectRatio: ASPECT_RATIOS[aspectRatio] },
67
- animatedStyle,
68
- ]}
89
+ style={[styles.container, { height: bannerWidth / ratio }, animatedStyle]}
69
90
  >
70
- {children ?? (
71
- <Image
72
- source={{
73
- uri: image,
74
- }}
75
- style={styles.full}
91
+ {!!bannerBackgroundColor && (
92
+ <LinearGradient
93
+ colors={[bannerBackgroundColor, bannerBackgroundColor + '00']}
94
+ style={StyleSheet.absoluteFill}
76
95
  />
77
96
  )}
97
+ <View style={styles.phoneBanner}>{content}</View>
78
98
  </Animated.View>
79
99
  );
80
100
  };
@@ -90,6 +110,11 @@ const styles = StyleSheet.create({
90
110
  alignSelf: 'center',
91
111
  },
92
112
  full: { width: '100%', height: '100%' },
113
+ phoneBanner: {
114
+ width: HeaderBarMetrics.bannerWidth,
115
+ height: '100%',
116
+ alignSelf: 'center',
117
+ },
93
118
  });
94
119
 
95
120
  export { HeaderAnimated };
@@ -8,10 +8,12 @@ import { Colors, Styles } from '../../Consts';
8
8
  import { Image } from '../../Image';
9
9
  import BackgroundImageView from './BackgroundImageView';
10
10
  import { useNormalizedSharedValue } from '../hooks';
11
+ import { HeaderBarMetrics, useHeaderLayout } from '../headerLayout';
11
12
 
12
13
  /**
13
14
  * Header background: surface and image that fade in on scroll, with an optional
14
- * gradient (custom or theme) on top. Outlined when `showBaseLineDebug` is on.
15
+ * gradient (custom or theme) on top, at phone height on wide windows. Outlined
16
+ * when `showBaseLineDebug` is on.
15
17
  */
16
18
  const HeaderBackground: React.FC<HeaderBackgroundProps> = ({
17
19
  animatedValue,
@@ -25,6 +27,7 @@ const HeaderBackground: React.FC<HeaderBackgroundProps> = ({
25
27
  const gradientColor = customGradientColor ?? theme.colors.gradient;
26
28
  const headerImage = headerBackground ?? theme.assets?.headerBackground;
27
29
  const sharedValue = useNormalizedSharedValue(animatedValue);
30
+ const { isWide } = useHeaderLayout();
28
31
 
29
32
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
30
33
 
@@ -38,14 +41,19 @@ const HeaderBackground: React.FC<HeaderBackgroundProps> = ({
38
41
  />
39
42
  <View style={styles.gradientContainer}>
40
43
  {useGradient && !!gradientColor && (
41
- <Animated.View style={styles.extendedHeader}>
44
+ <Animated.View
45
+ style={[styles.extendedHeader, isWide && styles.phoneHeight]}
46
+ >
42
47
  <LinearGradient
43
48
  colors={[gradientColor, gradientColor + '00']}
44
49
  style={StyleSheet.absoluteFill}
45
50
  >
46
51
  {!!headerImage && (
47
52
  <Image
48
- style={styles.headerBackground}
53
+ style={[
54
+ styles.headerBackground,
55
+ isWide && styles.phoneHeight,
56
+ ]}
49
57
  source={{ uri: headerImage }}
50
58
  loading={false}
51
59
  />
@@ -75,6 +83,11 @@ const styles = StyleSheet.create({
75
83
  position: 'absolute',
76
84
  aspectRatio: 375 / 154,
77
85
  },
86
+ // wide windows keep the phone gradient height instead of scaling with the width
87
+ phoneHeight: {
88
+ aspectRatio: undefined,
89
+ height: HeaderBarMetrics.gradientHeight,
90
+ },
78
91
  debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
79
92
  });
80
93
 
@@ -17,11 +17,16 @@ import BackgroundImageView from './BackgroundImageView';
17
17
  import { type AnimatedCompatValue } from '../utils';
18
18
  import { useNormalizedSharedValue } from '../hooks';
19
19
  import { useScreenLayout } from '../../Layout';
20
- import { DuoMetrics, HeaderMetrics } from '../../Layout/utils';
20
+ import { HeaderMetrics } from '../../Layout/utils';
21
+ import {
22
+ HeaderBarMetrics,
23
+ type HeaderContentLayout,
24
+ useHeaderLayout,
25
+ } from '../headerLayout';
21
26
 
22
- const SCREEN_PADDING = 12;
23
- const BACK_WIDTH = 28;
24
- const TOOLKIT_WIDTH = 73;
27
+ const { buttonSize, pillWidth, sidePadding, titleGap } = HeaderBarMetrics;
28
+ const SCREEN_PADDING = sidePadding;
29
+ const TOOLKIT_WIDTH = pillWidth + sidePadding;
25
30
 
26
31
  /**
27
32
  * Extended header background: a gradient that collapses on scroll and, with
@@ -38,6 +43,7 @@ const HeaderExtendHeader: React.FC<{
38
43
  useShadowHeader?: boolean;
39
44
  gradientColor?: string;
40
45
  headerBackground?: string;
46
+ contentLayout?: HeaderContentLayout;
41
47
  }> = ({
42
48
  headerType = 'default',
43
49
  animatedValue,
@@ -48,6 +54,7 @@ const HeaderExtendHeader: React.FC<{
48
54
  useShadowHeader: useShadowHeaderProp = true,
49
55
  gradientColor: customGradientColor,
50
56
  headerBackground: customBackground,
57
+ contentLayout,
51
58
  }) => {
52
59
  const { theme } = useContext(ApplicationContext);
53
60
  const context = useContext<any>(MiniAppContext);
@@ -55,29 +62,32 @@ const HeaderExtendHeader: React.FC<{
55
62
  const gradientColor = customGradientColor ?? theme.colors.gradient;
56
63
  const headerBackground = customBackground ?? theme.assets?.headerBackground;
57
64
  const layout = useScreenLayout();
65
+ const headerLayout = useHeaderLayout(contentLayout);
66
+ // the stretch frame pulls the search in from both bar edges
67
+ const frameShift = headerLayout.frameInset - sidePadding;
58
68
 
59
- // the iPhone Duo status bar row keeps side bar sized back and toolkit
60
- const isStatusBarRow = !!layout.statusBarClearance;
61
- const duoToolkitWidth = DuoMetrics.itemPitch * 2 + SCREEN_PADDING;
62
- const backWidth = isStatusBarRow ? DuoMetrics.controlSize : BACK_WIDTH;
63
- const toolkitWidth = isStatusBarRow ? duoToolkitWidth : TOOLKIT_WIDTH;
64
- const defaultLeft = layout.sideBar ? SCREEN_PADDING : backWidth + 20;
69
+ const backLeft = buttonSize + sidePadding + titleGap;
70
+ const defaultLeft = layout.sideBar ? SCREEN_PADDING : backLeft;
65
71
  const leftPosition = inputSearchProps?.leftPosition || defaultLeft;
66
- const defaultRightWidth = layout.sideBar ? 0 : toolkitWidth;
72
+ const defaultRightWidth = layout.sideBar ? 0 : TOOLKIT_WIDTH;
67
73
  const headerRightWidth = customHeaderRightWidth ?? defaultRightWidth;
68
- const contentLeft = layout.insetLeft + SCREEN_PADDING;
69
- const contentRight = layout.insetLeft + layout.contentWidth;
74
+ const contentLeft = layout.insetLeft + SCREEN_PADDING + frameShift;
75
+ const contentRight = layout.insetLeft + layout.contentWidth - frameShift;
70
76
  // collapsed on the status bar row, the search stays between its clusters
71
77
  const clearanceLeft = layout.statusBarClearance?.left ?? 0;
72
78
  const clearanceRight = layout.statusBarClearance?.right ?? 0;
73
- const collapsedLeft = layout.insetLeft + leftPosition + clearanceLeft;
79
+ const searchLeft = leftPosition + clearanceLeft + frameShift;
80
+ const collapsedLeft = layout.insetLeft + searchLeft;
74
81
  const collapsedRight = contentRight - clearanceRight - headerRightWidth;
75
- const searchWidth = layout.contentWidth - SCREEN_PADDING * 2;
82
+ const searchWidth = contentRight - contentLeft - SCREEN_PADDING;
76
83
  const collapsedSearchWidth = collapsedRight - collapsedLeft - SCREEN_PADDING;
77
84
  // a row taller than barHeight (iPhone Duo) centers the collapsed search on the title
78
85
  const titleRowHeight = layout.headerHeight - layout.headerStatusBarHeight;
79
86
  const collapsedRaise = (titleRowHeight - HeaderMetrics.barHeight) / 2;
80
87
 
88
+ // the iPhone Duo side bar keeps the content search size
89
+ const searchSize = layout.sideBar ? 'default' : 'header';
90
+
81
91
  let useShadowHeader = useShadowHeaderProp;
82
92
  if (inputSearchProps && Platform.OS === 'android') {
83
93
  useShadowHeader = false;
@@ -169,6 +179,7 @@ const HeaderExtendHeader: React.FC<{
169
179
  <Animated.View
170
180
  style={[
171
181
  styles.extendedHeader,
182
+ headerLayout.isWide && styles.phoneHeight,
172
183
  // fade out within the header box, as DuoScrollGradient does
173
184
  !!layout.sideBar && styles.fillHeaderBox,
174
185
  gradientOpacityStyle,
@@ -180,7 +191,10 @@ const HeaderExtendHeader: React.FC<{
180
191
  >
181
192
  {!!theme.assets?.headerBackground && (
182
193
  <Image
183
- style={styles.headerBackground}
194
+ style={[
195
+ styles.headerBackground,
196
+ headerLayout.isWide && styles.phoneHeight,
197
+ ]}
184
198
  source={{ uri: theme.assets?.headerBackground }}
185
199
  loading={false}
186
200
  />
@@ -189,9 +203,7 @@ const HeaderExtendHeader: React.FC<{
189
203
  </Animated.View>
190
204
  )}
191
205
  </Animated.View>
192
- <Animated.View
193
- style={[styles.searchContainer, heightStyle]}
194
- >
206
+ <Animated.View style={[styles.searchContainer, heightStyle]}>
195
207
  <Animated.View
196
208
  style={[{ marginVertical: Spacing.S }, searchTranslateStyle]}
197
209
  >
@@ -201,6 +213,7 @@ const HeaderExtendHeader: React.FC<{
201
213
  <InputSearch
202
214
  {...inputSearchProps}
203
215
  ref={inputSearchRef}
216
+ size={searchSize}
204
217
  backgroundColor={'transparent'}
205
218
  showButtonText={false}
206
219
  />
@@ -236,7 +249,10 @@ const HeaderExtendHeader: React.FC<{
236
249
  >
237
250
  {!!headerBackground && (
238
251
  <Image
239
- style={styles.headerBackground}
252
+ style={[
253
+ styles.headerBackground,
254
+ headerLayout.isWide && styles.phoneHeight,
255
+ ]}
240
256
  source={{ uri: headerBackground }}
241
257
  loading={false}
242
258
  />
@@ -264,6 +280,11 @@ const styles = StyleSheet.create({
264
280
  position: 'absolute',
265
281
  width: '100%',
266
282
  },
283
+ // wide windows keep the phone gradient height instead of scaling with the width
284
+ phoneHeight: {
285
+ aspectRatio: undefined,
286
+ height: HeaderBarMetrics.gradientHeight,
287
+ },
267
288
  fillHeaderBox: {
268
289
  aspectRatio: undefined,
269
290
  height: '100%',
@@ -273,14 +294,6 @@ const styles = StyleSheet.create({
273
294
  position: 'absolute',
274
295
  overflow: 'hidden',
275
296
  },
276
- verifiedDot: {
277
- width: 4,
278
- height: 4,
279
- borderRadius: 2,
280
- backgroundColor: Colors.green_03,
281
- position: 'absolute',
282
- alignSelf: 'center',
283
- },
284
297
  debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
285
298
  container: {
286
299
  zIndex: 0,
@@ -6,12 +6,13 @@ import { PopupNotify } from '../../Popup';
6
6
  import { NavigationButton } from './NavigationButton';
7
7
  import { SideBarButton } from './SideBarButton';
8
8
  import Animated from 'react-native-reanimated';
9
- import { useScreenLayout, useSideBarItemStyle } from '../../Layout/utils';
9
+ import { useSideBarItemStyle } from '../../Layout/utils';
10
10
  import { useSideBarCounterStyle } from './DuoQuickReturnHeader';
11
+ import { HeaderBarMetrics } from '../headerLayout';
11
12
 
12
13
  /**
13
14
  * Header back button, also bound to the Android hardware back. `preventBack`
14
- * confirms in a popup first; the iPhone Duo layouts use the 48pt side bar button.
15
+ * confirms in a popup first; the iPhone Duo side bar uses the 48pt side bar button.
15
16
  */
16
17
  const HeaderLeft: React.FC<HeaderBackProps> = ({
17
18
  tintColor,
@@ -24,7 +25,6 @@ const HeaderLeft: React.FC<HeaderBackProps> = ({
24
25
  const { navigator } = useContext(ApplicationContext);
25
26
  const sideBarStyle = useSideBarItemStyle('left', 0);
26
27
  const counterStyle = useSideBarCounterStyle();
27
- const { statusBarClearance } = useScreenLayout();
28
28
 
29
29
  const goBack = useCallback(() => {
30
30
  const canGoBack = navigator?.ref?.current?.canGoBack?.();
@@ -97,20 +97,6 @@ const HeaderLeft: React.FC<HeaderBackProps> = ({
97
97
  );
98
98
  }
99
99
 
100
- // on the status bar row the controls keep the side bar size
101
- if (statusBarClearance) {
102
- return (
103
- <View style={styles.headerLeft}>
104
- <SideBarButton
105
- accessibilityLabel="btn_navigation_back"
106
- icon="arrow-back"
107
- tintColor={tintColor}
108
- onPress={goBackSafe}
109
- />
110
- </View>
111
- );
112
- }
113
-
114
100
  return (
115
101
  <View style={styles.headerLeft}>
116
102
  <NavigationButton
@@ -125,7 +111,7 @@ const HeaderLeft: React.FC<HeaderBackProps> = ({
125
111
 
126
112
  const styles = StyleSheet.create({
127
113
  headerLeft: {
128
- marginLeft: 12,
114
+ marginLeft: HeaderBarMetrics.sidePadding,
129
115
  },
130
116
  });
131
117
 
@@ -1,11 +1,14 @@
1
1
  import { Platform, StyleSheet } from 'react-native';
2
2
  import { Colors, Spacing } from '../../Consts';
3
+ import { HeaderBarMetrics } from '../headerLayout';
4
+
5
+ const { buttonSize, pillWidth, pillHeight, sidePadding } = HeaderBarMetrics;
3
6
 
4
7
  export const styles = StyleSheet.create({
5
8
  navigationButton: {
6
- height: 28,
7
- width: 28,
8
- borderRadius: 14,
9
+ height: buttonSize,
10
+ width: buttonSize,
11
+ borderRadius: buttonSize / 2,
9
12
  justifyContent: 'center',
10
13
  alignItems: 'center',
11
14
  backgroundColor: Colors.black_01 + '99',
@@ -45,11 +48,11 @@ export const styles = StyleSheet.create({
45
48
  flexDirection: 'row',
46
49
  justifyContent: 'center',
47
50
  alignItems: 'center',
48
- paddingRight: Spacing.M,
51
+ paddingRight: sidePadding,
49
52
  },
50
53
  toolkitContainer: {
51
- height: 28,
52
- borderRadius: 14,
54
+ height: pillHeight,
55
+ borderRadius: pillHeight / 2,
53
56
  justifyContent: 'center',
54
57
  alignItems: 'center',
55
58
  flexDirection: 'row',
@@ -59,7 +62,10 @@ export const styles = StyleSheet.create({
59
62
  marginLeft: Spacing.S,
60
63
  },
61
64
  toolkitButton: {
62
- padding: 4,
65
+ flex: 1,
66
+ height: '100%',
67
+ justifyContent: 'center',
68
+ alignItems: 'center',
63
69
  },
64
70
  divider: {
65
71
  width: 0.5,
@@ -68,10 +74,6 @@ export const styles = StyleSheet.create({
68
74
  sideBarColumn: {
69
75
  gap: Spacing.M,
70
76
  },
71
- sideBarRow: {
72
- flexDirection: 'row',
73
- gap: Spacing.M,
74
- },
75
77
  headerLeft: {
76
78
  marginLeft: 12,
77
79
  },
@@ -136,9 +138,9 @@ export const styles = StyleSheet.create({
136
138
  marginRight: Spacing.M,
137
139
  },
138
140
  toolkitWide: {
139
- width: 65,
141
+ width: pillWidth,
140
142
  },
141
143
  toolkitNarrow: {
142
- width: 28,
144
+ width: buttonSize,
143
145
  },
144
146
  });
@@ -3,6 +3,9 @@ import { MiniAppContext } from '../../Context';
3
3
  import { Spacing } from '../../Consts';
4
4
  import { isWhiteListDesignSystem } from '../utils';
5
5
  import { useScreenLayout } from '../../Layout';
6
+ import { HeaderBarMetrics } from '../headerLayout';
7
+
8
+ const { sidePadding, buttonSize, titleGap, clusterWidth } = HeaderBarMetrics;
6
9
 
7
10
  /**
8
11
  * Header title font: MoMoTrustDisplay for design-system whitelisted mini apps,
@@ -14,32 +17,28 @@ const useHeaderFontFamily = (): string => {
14
17
  };
15
18
 
16
19
  /**
17
- * back button and toolkit sharing the navigation bar with the title
18
- */
19
- const HEADER_ACTIONS_WIDTH = 172;
20
-
21
- /**
22
- * extra width the 48pt back and toolkit take on the iPhone Duo status bar row
20
+ * side paddings, back button, title gaps and right cluster sharing the
21
+ * navigation bar with the title
23
22
  */
24
- const STATUS_BAR_ROW_ACTIONS_EXTRA = 55;
23
+ const HEADER_ACTIONS_WIDTH =
24
+ sidePadding * 2 + buttonSize + titleGap * 2 + clusterWidth;
25
25
 
26
26
  /**
27
27
  * Max header title width. The iPhone Duo side bar takes back and toolkit out of
28
- * the bar, so the title gets the content width; otherwise it shares the bar with them.
28
+ * the bar, so the title gets the content width; otherwise it shares the bar with
29
+ * them, minus the status bar clusters on the iPhone Duo inner portrait row.
29
30
  */
30
31
  const useTitleMaxWidth = () => {
31
32
  const layout = useScreenLayout();
32
33
  const { width, contentWidth, sideBar, statusBarClearance } = layout;
33
34
  let clearance = 0;
34
- let rowActionsExtra = 0;
35
35
  if (statusBarClearance) {
36
36
  clearance = statusBarClearance.left + statusBarClearance.right;
37
- rowActionsExtra = STATUS_BAR_ROW_ACTIONS_EXTRA;
38
37
  }
39
38
  if (sideBar) {
40
39
  return contentWidth - Spacing.M * 2 - clearance;
41
40
  }
42
- return width - HEADER_ACTIONS_WIDTH - rowActionsExtra - clearance;
41
+ return width - HEADER_ACTIONS_WIDTH - clearance;
43
42
  };
44
43
 
45
44
  export { useHeaderFontFamily, HEADER_ACTIONS_WIDTH, useTitleMaxWidth };
@@ -4,15 +4,15 @@ import { NavigationButton } from './NavigationButton';
4
4
  import { MiniAppContext } from '../../Context';
5
5
  import { Colors, Spacing, Styles } from '../../Consts';
6
6
  import { Icon } from '../../Icon';
7
- import { DuoMetrics, useScreenLayout } from '../../Layout/utils';
7
+ import { DuoMetrics } from '../../Layout/utils';
8
8
  import { SideBarButton, SideBarGroup, SideBarGroupItem } from './SideBarButton';
9
9
  import { useDuoBarCompression } from '../BottomTab/DuoBarCompression';
10
10
  import { styles } from './HeaderRight.styles';
11
11
  import { useToolkitActions } from './useToolkitActions';
12
+ import { HeaderBarMetrics } from '../headerLayout';
12
13
 
13
14
  const CLOSE_ICON = '16_navigation_close_circle';
14
15
  const HOME_ICON = '16_basic_home';
15
- const TOOLKIT_ICON_SIZE = 20;
16
16
  // the capsule's two buttons extend their touch area outward only
17
17
  const HELP_CENTER_HIT_SLOP = { top: 7, bottom: 7, left: 7, right: 0 };
18
18
  const CLOSE_HIT_SLOP = { top: 7, bottom: 7, left: 0, right: 7 };
@@ -35,7 +35,6 @@ const HeaderToolkitAction: React.FC<any> = ({
35
35
  const compression = useDuoBarCompression();
36
36
  const { toolbarCompressed, toolbarHidden } = compression;
37
37
  const { expandToolbar, setToolbarHeight } = compression;
38
- const { statusBarClearance } = useScreenLayout();
39
38
  const {
40
39
  isLoading,
41
40
  iconShortcut,
@@ -135,39 +134,6 @@ const HeaderToolkitAction: React.FC<any> = ({
135
134
  );
136
135
  }
137
136
 
138
- // on the status bar row the controls keep the side bar size
139
- if (statusBarClearance) {
140
- return (
141
- <View style={styles.sideBarRow}>
142
- {useShortcut && (
143
- <SideBarButton
144
- disabled={isLoading}
145
- icon={iconShortcut}
146
- onPress={shortcutOnPress}
147
- tintColor={tintColor}
148
- showBadge={showBadge}
149
- />
150
- )}
151
- <SideBarGroup tintColor={tintColor} horizontal>
152
- {hasHelpCenter && (
153
- <SideBarGroupItem
154
- accessibilityLabel={'btn_navigation_help_center'}
155
- icon={'help_center'}
156
- tintColor={tintColor}
157
- onPress={handlePressHelpCenter}
158
- />
159
- )}
160
- <SideBarGroupItem
161
- accessibilityLabel={'btn_navigation_close'}
162
- icon={closeIcon}
163
- tintColor={tintColor}
164
- onPress={handleClose}
165
- />
166
- </SideBarGroup>
167
- </View>
168
- );
169
- }
170
-
171
137
  return (
172
138
  <View style={Styles.row}>
173
139
  {useShortcut && (
@@ -191,7 +157,7 @@ const HeaderToolkitAction: React.FC<any> = ({
191
157
  <Icon
192
158
  color={tintColor}
193
159
  source={'help_center'}
194
- size={TOOLKIT_ICON_SIZE}
160
+ size={HeaderBarMetrics.iconSize}
195
161
  />
196
162
  </TouchableOpacity>
197
163
  <View style={[styles.divider, { backgroundColor: tintColor }]} />
@@ -203,7 +169,11 @@ const HeaderToolkitAction: React.FC<any> = ({
203
169
  style={styles.toolkitButton}
204
170
  hitSlop={CLOSE_HIT_SLOP}
205
171
  >
206
- <Icon color={tintColor} source={closeIcon} size={TOOLKIT_ICON_SIZE} />
172
+ <Icon
173
+ color={tintColor}
174
+ source={closeIcon}
175
+ size={HeaderBarMetrics.iconSize}
176
+ />
207
177
  </TouchableOpacity>
208
178
  </View>
209
179
  </View>
@@ -5,9 +5,12 @@ import { StyleSheet, TouchableOpacity, View, ViewStyle } from 'react-native';
5
5
  import { Colors, Spacing } from '../../Consts';
6
6
  import { Icon } from '../../Icon';
7
7
  import { BadgeDot } from '../../Badge';
8
+ import { HeaderBarMetrics } from '../headerLayout';
9
+
10
+ const { buttonSize, iconSize } = HeaderBarMetrics;
8
11
 
9
12
  /**
10
- * Round 28pt icon button of the header (back, shortcut) with an optional badge
13
+ * Round 32pt icon button of the header (back, shortcut) with an optional badge
11
14
  * dot; switches to a dark fill under a white tint so the icon stays readable.
12
15
  */
13
16
  const NavigationButton: React.FC<NavigationButtonProps> = ({
@@ -35,7 +38,7 @@ const NavigationButton: React.FC<NavigationButtonProps> = ({
35
38
  <Icon
36
39
  source={icon}
37
40
  color={tintColor ?? theme.colors.text.default}
38
- size={20}
41
+ size={iconSize}
39
42
  />
40
43
  </TouchableOpacity>
41
44
  {showBadge && <BadgeDot size={'small'} style={styles.badgeDot} />}
@@ -45,12 +48,12 @@ const NavigationButton: React.FC<NavigationButtonProps> = ({
45
48
 
46
49
  const styles = StyleSheet.create({
47
50
  container: {
48
- width: 28,
51
+ width: buttonSize,
49
52
  },
50
53
  navigationButton: {
51
- height: 28,
52
- width: 28,
53
- borderRadius: 14,
54
+ height: buttonSize,
55
+ width: buttonSize,
56
+ borderRadius: buttonSize / 2,
54
57
  justifyContent: 'center',
55
58
  alignItems: 'center',
56
59
  backgroundColor: Colors.black_01 + '99',
@@ -1,5 +1,5 @@
1
1
  import { Colors, Radius, Spacing, Styles } from '../../Consts';
2
- import { InputRef, InputSearch } from '../../Input';
2
+ import { InputRef, InputSearch, type InputSearchProps } from '../../Input';
3
3
  import { StyleSheet, TouchableOpacity, View } from 'react-native';
4
4
  import Animated, { useAnimatedStyle } from 'react-native-reanimated';
5
5
  import React, { useContext } from 'react';
@@ -8,7 +8,13 @@ import { ApplicationContext, MiniAppContext } from '../../Context';
8
8
  import { Text } from '../../Text';
9
9
  import { useNormalizedSharedValue } from '../hooks';
10
10
  import { useScreenLayout } from '../../Layout';
11
- import { DuoMetrics } from '../../Layout/utils';
11
+ import { HeaderBarMetrics } from '../headerLayout';
12
+
13
+ const { buttonSize, pillWidth, sidePadding, titleGap } = HeaderBarMetrics;
14
+ // help center | home pill and the bar padding right of it
15
+ const TOOLKIT_WIDTH = pillWidth + sidePadding;
16
+ // the iPhone Duo side bar keeps its right header width
17
+ const DUO_TOOLKIT_WIDTH = 73;
12
18
 
13
19
  /**
14
20
  * Header search bar whose fill turns from surface to default once scrolled, with
@@ -28,15 +34,21 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
28
34
  const { theme, navigator } = useContext(ApplicationContext);
29
35
  const context = useContext<any>(MiniAppContext);
30
36
  const layout = useScreenLayout();
31
- // the iPhone Duo status bar row keeps a side bar sized back button
32
- const BACK_WIDTH = layout.statusBarClearance ? DuoMetrics.controlSize : 28;
37
+ let defaultRightWidth = TOOLKIT_WIDTH;
38
+ let searchSize: InputSearchProps['size'] = 'header';
39
+ if (layout.sideBar) {
40
+ defaultRightWidth = DUO_TOOLKIT_WIDTH;
41
+ searchSize = 'default';
42
+ }
33
43
 
34
44
  const sharedValue = useNormalizedSharedValue(animatedValue);
35
- const leftPosition = props?.leftPosition ?? BACK_WIDTH + 20;
36
- const headerRightWidth = props?.headerRightWidth ?? 73;
45
+ const defaultLeft = buttonSize + sidePadding + titleGap;
46
+ const leftPosition = props?.leftPosition ?? defaultLeft;
47
+ const headerRightWidth = props?.headerRightWidth ?? defaultRightWidth;
37
48
 
38
49
  // iPhone Duo side bar holds back: the search shares the content width with the buttons only
39
- let searchMaxWidth = layout.width - leftPosition - 12 - headerRightWidth;
50
+ const rowWidth = layout.width - leftPosition - sidePadding;
51
+ let searchMaxWidth = rowWidth - headerRightWidth;
40
52
  if (layout.sideBar) {
41
53
  searchMaxWidth = layout.contentWidth - Spacing.M * 2 - headerRightWidth;
42
54
  }
@@ -105,6 +117,7 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
105
117
  <InputSearch
106
118
  ref={ref}
107
119
  {...props}
120
+ size={searchSize}
108
121
  backgroundColor={'transparent'}
109
122
  showButtonText={false}
110
123
  />
@@ -8,6 +8,7 @@ import { SwipeBackHaptic } from './Components/SwipeBackHaptic';
8
8
  import { useAppState } from './hooks';
9
9
  import { createScreenTracking } from './StackScreen.tracking';
10
10
  import { TooltipPortalHost, TooltipPortalProvider } from './TooltipPortal';
11
+ import { useHeaderFrameOptions } from './headerLayout';
11
12
 
12
13
  const TRACKING_TIMEOUT = 5000;
13
14
  const ELEMENT_IDLE_TIMEOUT = 2000;
@@ -55,6 +56,7 @@ const StackScreen: React.FC<any> = props => {
55
56
  const navigation = useRef(new Navigation(props.navigation, context)).current;
56
57
  const heightHeader = useHeaderHeight();
57
58
  const { isBackgroundToForeground } = useAppState();
59
+ const headerFrameOptions = useHeaderFrameOptions(options?.contentLayout);
58
60
 
59
61
  const data = {
60
62
  ...initialParams,
@@ -73,6 +75,13 @@ const StackScreen: React.FC<any> = props => {
73
75
  }
74
76
  }, [navigation, options]);
75
77
 
78
+ // re-applied on fold, unfold and rotation, apart from the other options
79
+ useLayoutEffect(() => {
80
+ if (headerFrameOptions) {
81
+ navigation.instance.setOptions(headerFrameOptions);
82
+ }
83
+ }, [navigation, headerFrameOptions]);
84
+
76
85
  useEffect(() => {
77
86
  let focusScreen: any;
78
87
  let focusAppListener: { remove?: () => void } | undefined;
@@ -0,0 +1,165 @@
1
+ import { useMemo } from 'react';
2
+ import { useWindowDimensions, type ViewStyle } from 'react-native';
3
+ import type { StackNavigationOptions } from '@react-navigation/stack';
4
+ import { Spacing } from '../Consts';
5
+ import { getHeaderMargin } from '../Layout/utils';
6
+ import { FOLDABLE_MIN_WIDTH } from './modalLayout';
7
+ import { isIPhoneDuo } from './utils';
8
+
9
+ // Top navigation spec, shared with the Compose / SwiftUI kits
10
+ const HEADER_FRAME_MAX_WIDTH = 600;
11
+
12
+ /**
13
+ * Top navigation geometry on every device
14
+ * - buttonSize / iconSize: round back and shortcut buttons, icons inside them and the pill
15
+ * - pillWidth / pillHeight: help center | home capsule
16
+ * - clusterWidth: shortcut, gap and pill on the right of the bar
17
+ * - searchHeight: search field inside the header
18
+ * - titleGap: space between back and the title, and the title and the right cluster
19
+ * - sidePadding: bar edge to back and to the right cluster
20
+ * - gradientHeight: header gradient, kept at the phone height on wide windows
21
+ * - bannerWidth: animated banner image, kept at the phone width on wide windows
22
+ */
23
+ const HeaderBarMetrics = {
24
+ buttonSize: 32,
25
+ iconSize: 20,
26
+ pillWidth: 72,
27
+ pillHeight: 32,
28
+ clusterWidth: 112,
29
+ searchHeight: 32,
30
+ titleGap: Spacing.S,
31
+ sidePadding: Spacing.M,
32
+ gradientHeight: 154,
33
+ bannerWidth: 375,
34
+ };
35
+
36
+ /**
37
+ * `grid` keeps the header row at the bar edges, `stretch` centers it in a 600pt
38
+ * frame on wide windows. Background and status bar always span the window.
39
+ */
40
+ type HeaderContentLayout = 'grid' | 'stretch';
41
+
42
+ type HeaderLayout = typeof HeaderBarMetrics & {
43
+ /**
44
+ * Window whose short side is at least 600 (Galaxy Z Fold / Pixel Fold inner
45
+ * display, tablets), iPhone Duo excluded. Phones, landscape included, are not.
46
+ */
47
+ isWide: boolean;
48
+ /**
49
+ * Banner kept at `bannerWidth`, centered: wide windows plus the iPhone Duo
50
+ * inner display, portrait and landscape.
51
+ */
52
+ isBannerCentered: boolean;
53
+ /**
54
+ * Window edge to back and to the right cluster: `sidePadding`, or centering the
55
+ * row in the 600pt frame with `stretch` on a wide window.
56
+ */
57
+ frameInset: number;
58
+ /**
59
+ * Header row width between the two `frameInset`s.
60
+ */
61
+ frameWidth: number;
62
+ };
63
+
64
+ type HeaderLayoutInput = {
65
+ width: number;
66
+ height: number;
67
+ isDuo: boolean;
68
+ contentLayout?: HeaderContentLayout;
69
+ };
70
+
71
+ /**
72
+ * Header geometry for a window, pure so it can be tested without a device.
73
+ */
74
+ const getHeaderLayout = ({
75
+ width,
76
+ height,
77
+ isDuo,
78
+ contentLayout = 'grid',
79
+ }: HeaderLayoutInput): HeaderLayout => {
80
+ const { sidePadding } = HeaderBarMetrics;
81
+ const isLargeWindow = Math.min(width, height) >= FOLDABLE_MIN_WIDTH;
82
+ const isWide = !isDuo && isLargeWindow;
83
+ let frameInset = sidePadding;
84
+ if (isWide && contentLayout === 'stretch') {
85
+ const centered = (width - HEADER_FRAME_MAX_WIDTH) / 2;
86
+ frameInset = Math.max(sidePadding, centered);
87
+ }
88
+ return {
89
+ ...HeaderBarMetrics,
90
+ isWide,
91
+ isBannerCentered: isLargeWindow,
92
+ frameInset,
93
+ frameWidth: width - frameInset * 2,
94
+ };
95
+ };
96
+
97
+ /**
98
+ * Widest a title can be between back and the right cluster of the header row.
99
+ */
100
+ const getTitleBudget = (layout: HeaderLayout) => {
101
+ const { buttonSize, titleGap, clusterWidth } = layout;
102
+ return layout.frameWidth - buttonSize - titleGap * 2 - clusterWidth;
103
+ };
104
+
105
+ /**
106
+ * Header geometry for the current window; follows fold, unfold and rotation.
107
+ */
108
+ const useHeaderLayout = (contentLayout?: HeaderContentLayout): HeaderLayout => {
109
+ const { width, height } = useWindowDimensions();
110
+ const isDuo = isIPhoneDuo();
111
+ return useMemo(() => {
112
+ return getHeaderLayout({ width, height, isDuo, contentLayout });
113
+ }, [width, height, isDuo, contentLayout]);
114
+ };
115
+
116
+ const TITLE_CONTAINER_RESET: ViewStyle = {
117
+ marginLeft: 0,
118
+ marginEnd: 0,
119
+ marginStart: 0,
120
+ marginHorizontal: 0,
121
+ marginVertical: 0,
122
+ margin: 0,
123
+ };
124
+
125
+ /**
126
+ * React Navigation container styles that keep back, title and the right cluster
127
+ * in the `stretch` frame. `undefined` for `grid`, and on iPhone Duo, which keeps
128
+ * its own header row.
129
+ */
130
+ const useHeaderFrameOptions = (
131
+ contentLayout?: HeaderContentLayout,
132
+ ): StackNavigationOptions | undefined => {
133
+ const layout = useHeaderLayout(contentLayout);
134
+ const { width } = useWindowDimensions();
135
+ const isDuo = isIPhoneDuo();
136
+ return useMemo(() => {
137
+ if (contentLayout !== 'stretch' || isDuo) {
138
+ return undefined;
139
+ }
140
+ // the header content row already sits this far in on large iOS windows
141
+ const shift = layout.frameInset - layout.sidePadding;
142
+ const containerShift = Math.max(0, shift - getHeaderMargin(width));
143
+ return {
144
+ headerLeftContainerStyle: { paddingLeft: containerShift },
145
+ headerRightContainerStyle: { paddingRight: containerShift },
146
+ headerTitleContainerStyle: {
147
+ ...TITLE_CONTAINER_RESET,
148
+ left: layout.titleGap,
149
+ maxWidth: getTitleBudget(layout),
150
+ },
151
+ };
152
+ }, [contentLayout, isDuo, layout, width]);
153
+ };
154
+
155
+ export type { HeaderContentLayout, HeaderLayout };
156
+
157
+ export {
158
+ HeaderBarMetrics,
159
+ HEADER_FRAME_MAX_WIDTH,
160
+ TITLE_CONTAINER_RESET,
161
+ getHeaderLayout,
162
+ getTitleBudget,
163
+ useHeaderLayout,
164
+ useHeaderFrameOptions,
165
+ };
@@ -26,6 +26,13 @@ import {
26
26
  type ModalLayout,
27
27
  type ModalRect,
28
28
  } from './modalLayout';
29
+ import {
30
+ HeaderBarMetrics,
31
+ getHeaderLayout,
32
+ useHeaderLayout,
33
+ type HeaderContentLayout,
34
+ type HeaderLayout,
35
+ } from './headerLayout';
29
36
 
30
37
  export {
31
38
  NavigationContainer,
@@ -41,6 +48,9 @@ export {
41
48
  useTooltipPortal,
42
49
  getModalLayout,
43
50
  useModalLayout,
51
+ HeaderBarMetrics,
52
+ getHeaderLayout,
53
+ useHeaderLayout,
44
54
  };
45
55
 
46
- export type { ModalLayout, ModalRect };
56
+ export type { ModalLayout, ModalRect, HeaderContentLayout, HeaderLayout };
@@ -8,11 +8,12 @@ import {
8
8
  TransitionPresets,
9
9
  } from '@react-navigation/stack';
10
10
  import type { SharedValue } from 'react-native-reanimated';
11
- import { Colors, Spacing } from '../Consts';
11
+ import { Colors } from '../Consts';
12
12
  import { HeaderBackground } from './Components/HeaderBackground';
13
13
  import { HeaderLeft } from './Components/HeaderLeft';
14
14
  import { HeaderRight } from './Components/HeaderRight';
15
15
  import { HeaderTitle, TitleCustom } from './Components/HeaderTitle';
16
+ import { HeaderBarMetrics, TITLE_CONTAINER_RESET } from './headerLayout';
16
17
  import type { HeaderTitleProps, NavigationOptions } from './types';
17
18
 
18
19
  const CARD_SHADOW_OPACITY = 0.3;
@@ -55,13 +56,8 @@ const getStackOptions = (): StackNavigationOptions => {
55
56
  headerTitleAlign: 'left',
56
57
  headerTitle: props => <HeaderTitle {...props} />,
57
58
  headerTitleContainerStyle: {
58
- marginLeft: 0,
59
- marginEnd: 0,
60
- marginStart: 0,
61
- marginHorizontal: 0,
62
- marginVertical: 0,
63
- margin: 0,
64
- left: Spacing.S,
59
+ ...TITLE_CONTAINER_RESET,
60
+ left: HeaderBarMetrics.titleGap,
65
61
  },
66
62
  headerBackground: (props: any) => <HeaderBackground {...props} />,
67
63
  headerLeft: (props: any) => <HeaderLeft {...props} floating />,
@@ -172,13 +168,8 @@ const getOptions = (
172
168
  if (params.hiddenBack) {
173
169
  headerLeft = null;
174
170
  options.headerTitleContainerStyle = {
175
- marginLeft: 0,
176
- marginEnd: 0,
177
- marginStart: 0,
178
- marginHorizontal: 0,
179
- marginVertical: 0,
180
- margin: 0,
181
- left: Spacing.M,
171
+ ...TITLE_CONTAINER_RESET,
172
+ left: HeaderBarMetrics.sidePadding,
182
173
  };
183
174
  }
184
175
  options = {
@@ -98,6 +98,11 @@ export interface HeaderAnimatedProps extends ViewProps {
98
98
  image: string;
99
99
  useScale?: boolean;
100
100
  aspectRatio?: '16/9' | '3/2' | '1/1';
101
+ /**
102
+ * Optional. On wide windows the image keeps its phone width, centered; this
103
+ * color fills the sides and fades to transparent downward. No fill when unset.
104
+ */
105
+ bannerBackgroundColor?: string;
101
106
  }
102
107
 
103
108
  export type AnimatedHeader = {
@@ -109,8 +114,8 @@ export type AnimatedHeader = {
109
114
  export interface SearchHeaderProps extends InputSearchProps {
110
115
  ref?: React.RefObject<InputRef>;
111
116
  animatedValue?: Animated.Value | SharedValue<number>;
112
- headerRightWidth?: 0 | 74 | 110 | number;
113
- leftPosition?: 12 | 48 | number;
117
+ headerRightWidth?: 0 | 84 | 124 | number;
118
+ leftPosition?: 12 | 52 | number;
114
119
  renderButtons?: () => ReactNode;
115
120
  hiddenBack?: boolean;
116
121
  }
@@ -9,6 +9,7 @@ import type {
9
9
  OnBoarding,
10
10
  } from './types.header';
11
11
  import type { LocalizeProps, Theme } from './types.theme';
12
+ import type { HeaderContentLayout } from './headerLayout';
12
13
 
13
14
  export * from './types.theme';
14
15
  export * from './types.header';
@@ -128,4 +129,10 @@ export interface NavigationOptions
128
129
  hiddenBack?: boolean;
129
130
  headerTitle?: HeaderTitleProps | string;
130
131
  headerRight?: OnBoarding | HeaderRightToolkit | any;
132
+ /**
133
+ * Optional. `stretch` centers back, title and the right cluster in a 600pt
134
+ * frame on wide windows (foldable inner display, tablet); the background stays
135
+ * full width. Defaults to `grid`, the header row at the window edges.
136
+ */
137
+ contentLayout?: HeaderContentLayout;
131
138
  }
@@ -13,6 +13,7 @@ import {
13
13
  View,
14
14
  } from 'react-native';
15
15
  import { useComponentId } from '../Application';
16
+ import { HeaderBarMetrics } from '../Application/headerLayout';
16
17
  import {
17
18
  ApplicationContext,
18
19
  ComponentContext,
@@ -52,6 +53,7 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
52
53
  backgroundColor,
53
54
  onPress,
54
55
  typingData,
56
+ size = 'default',
55
57
  ...props
56
58
  },
57
59
  ref,
@@ -62,6 +64,13 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
62
64
  const searchFontFamily = exportFontFamily('regular');
63
65
 
64
66
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
67
+ const isHeaderSize = size === 'header';
68
+ const inputHeightStyle = isHeaderSize && {
69
+ height: HeaderBarMetrics.searchHeight,
70
+ };
71
+ const searchIconSize = isHeaderSize
72
+ ? HeaderBarMetrics.iconSize
73
+ : SEARCH_ICON_SIZE;
65
74
 
66
75
  const [isFocused, setIsFocused] = useState(false);
67
76
  const [internalText, setInternalText] = useState(defaultValue || '');
@@ -117,7 +126,11 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
117
126
  if (typingData && typingData?.length > 0) {
118
127
  return (
119
128
  <TouchableOpacity
120
- style={[styles.searchInput, styles.searchInputTyping]}
129
+ style={[
130
+ styles.searchInput,
131
+ styles.searchInputTyping,
132
+ inputHeightStyle,
133
+ ]}
121
134
  onPress={onPress}
122
135
  activeOpacity={onPress ? PRESSED_OPACITY : 1}
123
136
  >
@@ -135,6 +148,7 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
135
148
  ref={inputRef}
136
149
  style={[
137
150
  styles.searchInput,
151
+ inputHeightStyle,
138
152
  {
139
153
  fontSize: scaledFontSize,
140
154
  color: theme.colors.text.default,
@@ -228,7 +242,7 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
228
242
  >
229
243
  <Icon
230
244
  source={'navigation_search'}
231
- size={SEARCH_ICON_SIZE}
245
+ size={searchIconSize}
232
246
  color={theme.colors.text.hint}
233
247
  />
234
248
  {renderInputView()}
package/Input/index.tsx CHANGED
@@ -194,6 +194,12 @@ export interface InputSearchProps extends TextInputProps {
194
194
  */
195
195
  typingData?: string[];
196
196
 
197
+ /**
198
+ * Optional. `header` is the 32pt field with a 20pt search icon that sits in the
199
+ * navigation header. Defaults to `default`, the 36pt field for content.
200
+ */
201
+ size?: 'default' | 'header';
202
+
197
203
  /**
198
204
  * Optional. Called when the field is pressed while showing `typingData`.
199
205
  */
@@ -367,4 +373,4 @@ export {
367
373
  InputPhoneNumber,
368
374
  };
369
375
 
370
- export {checkValidPhoneNumber};
376
+ export { checkValidPhoneNumber };
package/Layout/Screen.tsx CHANGED
@@ -67,6 +67,7 @@ const Screen = forwardRef(
67
67
  headerBackground,
68
68
  gradientColor,
69
69
  trackingParams,
70
+ contentLayout,
70
71
  }: ScreenProps,
71
72
  ref: any,
72
73
  ) => {
@@ -144,6 +145,7 @@ const Screen = forwardRef(
144
145
  gradientColor,
145
146
  headerBackground,
146
147
  layoutOffset,
148
+ contentLayout,
147
149
  });
148
150
 
149
151
  // mutates the content node so callers keep its methods plus the header setters
@@ -223,6 +225,7 @@ const Screen = forwardRef(
223
225
  useShadowHeader={useShadowHeader}
224
226
  gradientColor={gradientColor}
225
227
  headerBackground={headerBackground}
228
+ contentLayout={contentLayout}
226
229
  />
227
230
  );
228
231
  };
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useRef } from 'react';
2
2
  import { StatusBar, StatusBarStyle } from 'react-native';
3
3
  import { StackNavigationOptions } from '@react-navigation/stack';
4
+ import type { SharedValue } from 'react-native-reanimated';
4
5
  import Navigation from '../Application/Navigation';
5
6
  import { AnimatedHeader, SearchHeaderProps } from '../Application/types';
6
7
  import { exportHeaderTitle } from '../Application/navigationOptions';
@@ -12,6 +13,11 @@ import {
12
13
  SearchHeader,
13
14
  } from '../Application';
14
15
  import { DuoGlassBackground } from '../Application/Components/DuoGlassBackground';
16
+ import {
17
+ type HeaderContentLayout,
18
+ useHeaderFrameOptions,
19
+ useHeaderLayout,
20
+ } from '../Application/headerLayout';
15
21
  import { getHeaderMargin } from './utils';
16
22
  import { ANIMATED_BANNER_HEIGHT } from './screenTypes';
17
23
  import { HeaderType } from './types';
@@ -29,6 +35,31 @@ type ScreenHeaderParams = {
29
35
  gradientColor?: string;
30
36
  headerBackground?: string;
31
37
  layoutOffset: number;
38
+ contentLayout?: HeaderContentLayout;
39
+ };
40
+
41
+ /**
42
+ * Transparent header for an `inputSearchProps` Screen: its title fades out over the
43
+ * first 50pt of scroll while the search collapses into the bar.
44
+ */
45
+ const getSearchTitleOptions = (
46
+ sharedValue: SharedValue<number>,
47
+ ): StackNavigationOptions => {
48
+ return {
49
+ headerShown: true,
50
+ headerTransparent: true,
51
+ headerBackground: () => null,
52
+ headerTitle: (props: any) => (
53
+ <HeaderTitle
54
+ {...props}
55
+ interpolate={{
56
+ inputRange: [0, 50],
57
+ outputRange: [1, 0],
58
+ }}
59
+ animatedValue={sharedValue}
60
+ />
61
+ ),
62
+ };
32
63
  };
33
64
 
34
65
  /**
@@ -48,9 +79,12 @@ export const useScreenHeader = ({
48
79
  gradientColor,
49
80
  headerBackground,
50
81
  layoutOffset,
82
+ contentLayout,
51
83
  }: ScreenHeaderParams) => {
52
84
  const currentTint = useRef<string | undefined>(undefined);
53
85
  const appliedHeaderMode = useRef<string | undefined>(undefined);
86
+ const headerLayout = useHeaderLayout(contentLayout);
87
+ const frameOptions = useHeaderFrameOptions(contentLayout);
54
88
 
55
89
  // keepTitle leaves a title the screen set itself when the same type is re-applied
56
90
  const setHeaderType = useCallback(
@@ -79,21 +113,7 @@ export const useScreenHeader = ({
79
113
  headerTitle: (props: any) => <HeaderTitle {...props} />,
80
114
  };
81
115
  if (inputSearchProps) {
82
- options = {
83
- headerShown: true,
84
- headerTransparent: true,
85
- headerBackground: () => null,
86
- headerTitle: (props: any) => (
87
- <HeaderTitle
88
- {...props}
89
- interpolate={{
90
- inputRange: [0, 50],
91
- outputRange: [1, 0],
92
- }}
93
- animatedValue={sharedValue}
94
- />
95
- ),
96
- };
116
+ options = getSearchTitleOptions(sharedValue);
97
117
  }
98
118
  break;
99
119
 
@@ -127,21 +147,7 @@ export const useScreenHeader = ({
127
147
  headerTitle: (props: any) => <HeaderTitle {...props} />,
128
148
  };
129
149
  if (inputSearchProps) {
130
- options = {
131
- headerShown: true,
132
- headerTransparent: true,
133
- headerBackground: () => null,
134
- headerTitle: (props: any) => (
135
- <HeaderTitle
136
- {...props}
137
- interpolate={{
138
- inputRange: [0, 50],
139
- outputRange: [1, 0],
140
- }}
141
- animatedValue={sharedValue}
142
- />
143
- ),
144
- };
150
+ options = getSearchTitleOptions(sharedValue);
145
151
  }
146
152
  }
147
153
 
@@ -149,7 +155,11 @@ export const useScreenHeader = ({
149
155
  delete options.headerTitle;
150
156
  }
151
157
 
152
- navigation?.instance?.setOptions({ header: undefined, ...options });
158
+ navigation?.instance?.setOptions({
159
+ header: undefined,
160
+ ...frameOptions,
161
+ ...options,
162
+ });
153
163
  },
154
164
  [
155
165
  gradientColor,
@@ -160,6 +170,7 @@ export const useScreenHeader = ({
160
170
  useShadowHeader,
161
171
  hasSideBar,
162
172
  quickReturnOptions,
173
+ frameOptions,
163
174
  ],
164
175
  );
165
176
 
@@ -207,7 +218,7 @@ export const useScreenHeader = ({
207
218
  };
208
219
  }
209
220
 
210
- navigation?.instance?.setOptions(options);
221
+ navigation?.instance?.setOptions({ ...frameOptions, ...options });
211
222
  },
212
223
  [
213
224
  gradientColor,
@@ -218,32 +229,23 @@ export const useScreenHeader = ({
218
229
  hasSideBar,
219
230
  layoutOffset,
220
231
  quickReturnOptions,
232
+ frameOptions,
221
233
  ],
222
234
  );
223
235
 
224
236
  const setAnimatedSearch = useCallback(() => {
225
- const options: StackNavigationOptions = {
226
- headerShown: true,
227
- headerTransparent: true,
228
- headerBackground: () => null,
229
- headerTitle: (props: any) => (
230
- <HeaderTitle
231
- {...props}
232
- interpolate={{
233
- inputRange: [0, 50],
234
- outputRange: [1, 0],
235
- }}
236
- animatedValue={sharedValue}
237
- />
238
- ),
239
- };
240
-
241
- navigation?.instance?.setOptions({ header: undefined, ...options });
242
- }, [navigation?.instance, sharedValue]);
237
+ const options = getSearchTitleOptions(sharedValue);
238
+ navigation?.instance?.setOptions({
239
+ header: undefined,
240
+ ...frameOptions,
241
+ ...options,
242
+ });
243
+ }, [navigation?.instance, sharedValue, frameOptions]);
243
244
 
244
- // the search field spans the content, clear of the side bar and status bar clusters
245
+ // the search field spans the content (or the stretch frame), clear of the side bar
246
+ // and status bar clusters
245
247
  const setSearchHeader = (params: SearchHeaderProps) => {
246
- let searchWidth = layout.width - 24;
248
+ let searchWidth = headerLayout.frameWidth;
247
249
  let searchLeft = Spacing.M;
248
250
  if (layout.sideBar) {
249
251
  searchWidth = layout.contentWidth - Spacing.M * 2;
@@ -254,6 +256,7 @@ export const useScreenHeader = ({
254
256
  searchWidth -= clearance.left + clearance.right;
255
257
  }
256
258
  const options: StackNavigationOptions = {
259
+ ...frameOptions,
257
260
  headerRight: undefined,
258
261
  headerTitleContainerStyle: {
259
262
  width: searchWidth,
@@ -9,6 +9,7 @@ import {
9
9
  import { FloatingButtonProps } from './FloatingButton';
10
10
  import { HeaderType } from './types';
11
11
  import { InputRef } from '../Input';
12
+ import type { HeaderContentLayout } from '../Application/headerLayout';
12
13
 
13
14
  export const ANIMATED_BANNER_HEIGHT = 210;
14
15
 
@@ -76,10 +77,17 @@ export interface ScreenProps extends ViewProps {
76
77
  layoutOffset?: -8 | -24 | -56 | number;
77
78
 
78
79
  /**
79
- * Optional. Width of the right header. Defaults to the toolkit width (73), or 0 when
80
+ * Optional. Width of the right header. Defaults to the toolkit width (84), or 0 when
80
81
  * iPhone Duo floats the toolkit into its side bar.
81
82
  */
82
- headerRightWidth?: 0 | 73 | 110 | number;
83
+ headerRightWidth?: 0 | 84 | 124 | number;
84
+
85
+ /**
86
+ * Optional. `stretch` centers back, title or search and the right cluster of the
87
+ * header in a 600pt frame on wide windows (foldable inner display, tablet); the
88
+ * background and status bar stay full width. Defaults to `grid`.
89
+ */
90
+ contentLayout?: HeaderContentLayout;
83
91
 
84
92
  /**
85
93
  * Optional. Props for the floating action button.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.169.9",
3
+ "version": "0.169.10",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},