@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.
- package/Application/Components/HeaderAnimated.tsx +38 -13
- package/Application/Components/HeaderBackground.tsx +16 -3
- package/Application/Components/HeaderExtendHeader.tsx +41 -28
- package/Application/Components/HeaderLeft.tsx +4 -18
- package/Application/Components/HeaderRight.styles.ts +15 -13
- package/Application/Components/HeaderTitle.shared.ts +10 -11
- package/Application/Components/HeaderToolkitAction.tsx +8 -38
- package/Application/Components/NavigationButton.tsx +9 -6
- package/Application/Components/SearchHeader.tsx +20 -7
- package/Application/StackScreen.tsx +9 -0
- package/Application/headerLayout.ts +165 -0
- package/Application/index.ts +11 -1
- package/Application/navigationOptions.tsx +6 -15
- package/Application/types.header.ts +7 -2
- package/Application/types.ts +7 -0
- package/Input/InputSearch.tsx +16 -2
- package/Input/index.tsx +7 -1
- package/Layout/Screen.tsx +3 -0
- package/Layout/screenHeader.tsx +55 -52
- package/Layout/screenTypes.ts +10 -2
- package/package.json +1 -1
|
@@ -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
|
-
{
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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
|
|
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={
|
|
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 {
|
|
20
|
+
import { HeaderMetrics } from '../../Layout/utils';
|
|
21
|
+
import {
|
|
22
|
+
HeaderBarMetrics,
|
|
23
|
+
type HeaderContentLayout,
|
|
24
|
+
useHeaderLayout,
|
|
25
|
+
} from '../headerLayout';
|
|
21
26
|
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const TOOLKIT_WIDTH =
|
|
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
|
-
|
|
60
|
-
const
|
|
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 :
|
|
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
|
|
79
|
+
const searchLeft = leftPosition + clearanceLeft + frameShift;
|
|
80
|
+
const collapsedLeft = layout.insetLeft + searchLeft;
|
|
74
81
|
const collapsedRight = contentRight - clearanceRight - headerRightWidth;
|
|
75
|
-
const searchWidth =
|
|
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={
|
|
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={
|
|
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 {
|
|
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
|
|
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:
|
|
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:
|
|
7
|
-
width:
|
|
8
|
-
borderRadius:
|
|
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:
|
|
51
|
+
paddingRight: sidePadding,
|
|
49
52
|
},
|
|
50
53
|
toolkitContainer: {
|
|
51
|
-
height:
|
|
52
|
-
borderRadius:
|
|
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
|
-
|
|
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:
|
|
141
|
+
width: pillWidth,
|
|
140
142
|
},
|
|
141
143
|
toolkitNarrow: {
|
|
142
|
-
width:
|
|
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
|
|
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
|
|
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
|
|
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 -
|
|
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
|
|
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={
|
|
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
|
|
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
|
|
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={
|
|
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:
|
|
51
|
+
width: buttonSize,
|
|
49
52
|
},
|
|
50
53
|
navigationButton: {
|
|
51
|
-
height:
|
|
52
|
-
width:
|
|
53
|
-
borderRadius:
|
|
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 {
|
|
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
|
-
|
|
32
|
-
|
|
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
|
|
36
|
-
const
|
|
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
|
-
|
|
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
|
+
};
|
package/Application/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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 |
|
|
113
|
-
leftPosition?: 12 |
|
|
117
|
+
headerRightWidth?: 0 | 84 | 124 | number;
|
|
118
|
+
leftPosition?: 12 | 52 | number;
|
|
114
119
|
renderButtons?: () => ReactNode;
|
|
115
120
|
hiddenBack?: boolean;
|
|
116
121
|
}
|
package/Application/types.ts
CHANGED
|
@@ -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
|
}
|
package/Input/InputSearch.tsx
CHANGED
|
@@ -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={[
|
|
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={
|
|
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
|
};
|
package/Layout/screenHeader.tsx
CHANGED
|
@@ -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({
|
|
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
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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 =
|
|
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,
|
package/Layout/screenTypes.ts
CHANGED
|
@@ -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 (
|
|
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 |
|
|
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.
|