@momo-kits/foundation 0.169.9-beta.1 → 0.169.9
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/BottomSheet.hooks.ts +27 -17
- package/Application/BottomSheet.tsx +35 -27
- package/Application/BottomTab/ActiveIndicator.tsx +5 -1
- package/Application/BottomTab/BottomTabBar.styles.ts +18 -1
- package/Application/BottomTab/BottomTabBar.tsx +14 -16
- package/Application/BottomTab/CurvedContainer.tsx +8 -2
- package/Application/BottomTab/CustomBottomTabItem.tsx +2 -2
- package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
- package/Application/BottomTab/DuoTabBar.tsx +15 -3
- package/Application/BottomTab/TabScreen.tsx +11 -10
- package/Application/BottomTab/index.tsx +5 -5
- package/Application/BottomTab/metrics.ts +23 -3
- package/Application/BottomTab/usePressLift.ts +3 -3
- package/Application/Components/HeaderAnimated.tsx +13 -38
- package/Application/Components/HeaderBackground.tsx +3 -16
- package/Application/Components/HeaderExtendHeader.tsx +28 -41
- package/Application/Components/HeaderLeft.tsx +18 -4
- package/Application/Components/HeaderRight.styles.ts +13 -15
- package/Application/Components/HeaderTitle.shared.ts +11 -10
- package/Application/Components/HeaderToolkitAction.tsx +54 -21
- package/Application/Components/NavigationButton.tsx +6 -9
- package/Application/Components/SearchHeader.tsx +7 -20
- package/Application/Components/useToolkitActions.tsx +22 -21
- package/Application/ModalScreen.tsx +19 -15
- package/Application/StackScreen.tsx +13 -18
- package/Application/index.ts +1 -11
- package/Application/modalLayout.ts +1 -0
- package/Application/navigationOptions.tsx +15 -6
- package/Application/types.header.ts +2 -7
- package/Application/types.tabs.ts +0 -1
- package/Application/types.ts +0 -7
- package/Context/index.ts +3 -1
- package/Image/index.tsx +6 -6
- package/Input/ErrorView.tsx +59 -0
- package/Input/FloatingView.tsx +101 -0
- package/Input/Input.tsx +40 -37
- package/Input/InputDropDown.tsx +13 -10
- package/Input/InputMoney.tsx +17 -17
- package/Input/InputMoneyView.tsx +12 -12
- package/Input/InputOTP.tsx +46 -34
- package/Input/InputPhoneNumber.tsx +48 -43
- package/Input/InputSearch.tsx +26 -39
- package/Input/InputTextArea.tsx +29 -23
- package/Input/RenderTrailing.tsx +79 -0
- package/Input/constants.ts +8 -0
- package/Input/index.tsx +1 -7
- package/Input/utils.ts +126 -7
- package/Layout/FloatingButton.tsx +38 -24
- package/Layout/Screen.tsx +0 -3
- package/Layout/layoutMetrics.ts +43 -34
- package/Layout/screenHeader.tsx +52 -55
- package/Layout/screenTypes.ts +4 -13
- package/Pagination/PaginationScroll.tsx +5 -3
- package/Pagination/types.ts +3 -5
- package/Popup/PopupNotify.tsx +17 -14
- package/Popup/PopupPromotion.tsx +5 -4
- package/Popup/types.ts +2 -4
- package/Popup/utils.ts +7 -4
- package/Skeleton/index.tsx +15 -12
- package/package.json +1 -1
- package/Application/headerLayout.ts +0 -165
- package/Input/common.tsx +0 -257
- package/Input/moneyUtils.ts +0 -23
|
@@ -13,6 +13,8 @@ import { ApplicationContext, MiniAppContext } from '../Context';
|
|
|
13
13
|
|
|
14
14
|
const INDICATOR_WIDTH = 24;
|
|
15
15
|
const INDICATOR_CONTAINER_WIDTH = 72;
|
|
16
|
+
// one frame at 60 fps
|
|
17
|
+
const SCROLL_EVENT_THROTTLE = 16;
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Horizontal scroll view with a small indicator that tracks the scroll offset.
|
|
@@ -31,7 +33,7 @@ const PaginationScroll: FC<ScrollIndicatorProps> = ({
|
|
|
31
33
|
|
|
32
34
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
33
35
|
|
|
34
|
-
const
|
|
36
|
+
const handleScroll = useAnimatedScrollHandler({
|
|
35
37
|
onScroll: (event) => {
|
|
36
38
|
left.value = event.contentOffset.x;
|
|
37
39
|
},
|
|
@@ -54,8 +56,8 @@ const PaginationScroll: FC<ScrollIndicatorProps> = ({
|
|
|
54
56
|
return (
|
|
55
57
|
<Animated.ScrollView
|
|
56
58
|
ref={scrollViewRef as any}
|
|
57
|
-
onScroll={
|
|
58
|
-
scrollEventThrottle={
|
|
59
|
+
onScroll={handleScroll}
|
|
60
|
+
scrollEventThrottle={SCROLL_EVENT_THROTTLE}
|
|
59
61
|
alwaysBounceHorizontal={false}
|
|
60
62
|
showsHorizontalScrollIndicator={false}
|
|
61
63
|
horizontal
|
package/Pagination/types.ts
CHANGED
|
@@ -3,11 +3,9 @@ import {Ref} from 'react';
|
|
|
3
3
|
|
|
4
4
|
export type PaginationProps = {
|
|
5
5
|
/**
|
|
6
|
-
* Optional.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - 'number': Represents a style where pages are represented by numbers.
|
|
10
|
-
* - 'scroll': Rendered as 'default'; use `PaginationScroll` instead.
|
|
6
|
+
* Optional. Visual style: 'default' dots, 'black_white' dots on a translucent
|
|
7
|
+
* dark background, 'number' page numbers. 'scroll' renders as 'default'; use
|
|
8
|
+
* `PaginationScroll` instead.
|
|
11
9
|
*/
|
|
12
10
|
type?: 'default' | 'black_white' | 'number' | 'scroll';
|
|
13
11
|
|
package/Popup/PopupNotify.tsx
CHANGED
|
@@ -12,6 +12,9 @@ import { ModalIconButton } from '../Application/ModalIconButton';
|
|
|
12
12
|
import { splitHighlightText } from './utils';
|
|
13
13
|
|
|
14
14
|
const MAX_ROW_TITLE_LENGTH = 12;
|
|
15
|
+
const DESCRIPTION_MAX_HEIGHT = 172;
|
|
16
|
+
const MAX_DESCRIPTION_LINES = 8.5;
|
|
17
|
+
const IMAGE_ASPECT_RATIO = 1.7777;
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Notify popup body: optional image, title, description, error code and one or
|
|
@@ -32,10 +35,10 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
32
35
|
}) => {
|
|
33
36
|
const context = useContext<any>(MiniAppContext);
|
|
34
37
|
const { theme, navigator, translate } = useContext(ApplicationContext);
|
|
35
|
-
const [
|
|
38
|
+
const [isDescriptionScrollable, setIsDescriptionScrollable] = useState(false);
|
|
36
39
|
const { isFoldable, popup } = useModalLayout();
|
|
37
40
|
const { width: popupWidth, maxHeight, radius } = popup;
|
|
38
|
-
const scaledMaxHeight = useScaleSize(
|
|
41
|
+
const scaledMaxHeight = useScaleSize(DESCRIPTION_MAX_HEIGHT);
|
|
39
42
|
|
|
40
43
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
41
44
|
const closeAction = useRef('touch_outside');
|
|
@@ -45,12 +48,12 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
45
48
|
// foldables cap the popup height, the description takes what is left
|
|
46
49
|
Description = ScrollView;
|
|
47
50
|
descriptionStyle = styles.shrink;
|
|
48
|
-
} else if (
|
|
51
|
+
} else if (isDescriptionScrollable) {
|
|
49
52
|
Description = ScrollView;
|
|
50
53
|
descriptionStyle = { maxHeight: scaledMaxHeight };
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
const
|
|
56
|
+
const handleAction = (callback?: () => void) => {
|
|
54
57
|
if (typeof onRequestClose === 'function') {
|
|
55
58
|
onRequestClose?.(callback);
|
|
56
59
|
} else {
|
|
@@ -72,7 +75,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
72
75
|
size="medium"
|
|
73
76
|
onPress={() => {
|
|
74
77
|
closeAction.current = 'button_action';
|
|
75
|
-
|
|
78
|
+
handleAction(secondary?.onPress);
|
|
76
79
|
}}
|
|
77
80
|
accessibilityLabel={'btn_popup_cancel'}
|
|
78
81
|
/>
|
|
@@ -86,7 +89,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
86
89
|
size="medium"
|
|
87
90
|
onPress={() => {
|
|
88
91
|
closeAction.current = 'button_action';
|
|
89
|
-
|
|
92
|
+
handleAction(primary.onPress);
|
|
90
93
|
}}
|
|
91
94
|
accessibilityLabel={'btn_popup_allow'}
|
|
92
95
|
/>
|
|
@@ -103,7 +106,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
103
106
|
size="medium"
|
|
104
107
|
onPress={() => {
|
|
105
108
|
closeAction.current = 'button_action';
|
|
106
|
-
|
|
109
|
+
handleAction(primary.onPress);
|
|
107
110
|
}}
|
|
108
111
|
/>
|
|
109
112
|
{!!secondary?.title && (
|
|
@@ -116,7 +119,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
116
119
|
size="medium"
|
|
117
120
|
onPress={() => {
|
|
118
121
|
closeAction.current = 'button_action';
|
|
119
|
-
|
|
122
|
+
handleAction(secondary?.onPress);
|
|
120
123
|
}}
|
|
121
124
|
/>
|
|
122
125
|
</>
|
|
@@ -150,7 +153,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
150
153
|
return text;
|
|
151
154
|
}
|
|
152
155
|
return segments.map((segment, index) => {
|
|
153
|
-
if (segment.
|
|
156
|
+
if (segment.isHighlighted) {
|
|
154
157
|
return (
|
|
155
158
|
<Text key={index} typography={'header_s_semibold'}>
|
|
156
159
|
{segment.text}
|
|
@@ -172,7 +175,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
172
175
|
style={styles.iconClose}
|
|
173
176
|
onPress={() => {
|
|
174
177
|
closeAction.current = 'icon_close';
|
|
175
|
-
|
|
178
|
+
handleAction(onIconClose);
|
|
176
179
|
}}
|
|
177
180
|
/>
|
|
178
181
|
);
|
|
@@ -189,9 +192,9 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
189
192
|
accessibilityLabel={'description_popup_notify'}
|
|
190
193
|
typography={'body_default_regular'}
|
|
191
194
|
onTextLayout={({ nativeEvent: { lines } }) => {
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
+
const shouldScroll = lines.length > MAX_DESCRIPTION_LINES;
|
|
196
|
+
if (shouldScroll !== isDescriptionScrollable) {
|
|
197
|
+
setIsDescriptionScrollable(shouldScroll);
|
|
195
198
|
}
|
|
196
199
|
}}
|
|
197
200
|
>
|
|
@@ -260,7 +263,7 @@ const styles = StyleSheet.create({
|
|
|
260
263
|
// clips the image and content to the card radius
|
|
261
264
|
card: { overflow: 'hidden' },
|
|
262
265
|
image: {
|
|
263
|
-
aspectRatio:
|
|
266
|
+
aspectRatio: IMAGE_ASPECT_RATIO,
|
|
264
267
|
},
|
|
265
268
|
shrink: { flexShrink: 1 },
|
|
266
269
|
content: { padding: Spacing.XL },
|
package/Popup/PopupPromotion.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { useModalLayout } from '../Application/modalLayout';
|
|
|
9
9
|
|
|
10
10
|
const ASPECT_RATIO = 0.72;
|
|
11
11
|
const ICON_CLOSE_SIZE = 20;
|
|
12
|
+
const CLOSE_GLYPH_SIZE = 14;
|
|
12
13
|
// close button row under the image: marginTop + vertical padding + icon
|
|
13
14
|
const CLOSE_ROW_HEIGHT = Spacing.S * 3 + ICON_CLOSE_SIZE;
|
|
14
15
|
|
|
@@ -37,7 +38,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
|
|
|
37
38
|
|
|
38
39
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
39
40
|
|
|
40
|
-
const
|
|
41
|
+
const handleAction = (callback?: () => void) => {
|
|
41
42
|
if (typeof onRequestClose === 'function') {
|
|
42
43
|
onRequestClose?.(callback);
|
|
43
44
|
} else {
|
|
@@ -53,7 +54,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
|
|
|
53
54
|
accessibilityLabel={'ic_popup_close'}
|
|
54
55
|
onPress={() => {
|
|
55
56
|
closeAction.current = 'icon_close';
|
|
56
|
-
|
|
57
|
+
handleAction(onIconClose);
|
|
57
58
|
}}
|
|
58
59
|
style={styles.iconButton}
|
|
59
60
|
>
|
|
@@ -69,7 +70,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
|
|
|
69
70
|
<Icon
|
|
70
71
|
source="navigation_close"
|
|
71
72
|
color={theme.colors.background.surface}
|
|
72
|
-
size={
|
|
73
|
+
size={CLOSE_GLYPH_SIZE}
|
|
73
74
|
/>
|
|
74
75
|
</View>
|
|
75
76
|
</TouchableOpacity>
|
|
@@ -82,7 +83,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
|
|
|
82
83
|
accessibilityLabel={'popup_promotion'}
|
|
83
84
|
style={[{ width: imageWidth }, showBaseLineDebug && styles.debugBaseLine]}
|
|
84
85
|
>
|
|
85
|
-
<TouchableOpacity onPress={() =>
|
|
86
|
+
<TouchableOpacity onPress={() => handleAction(onActionClick)}>
|
|
86
87
|
<Image
|
|
87
88
|
source={{
|
|
88
89
|
uri: image,
|
package/Popup/types.ts
CHANGED
|
@@ -43,10 +43,8 @@ export type PopupNotifyProps = {
|
|
|
43
43
|
primary: PopupAction;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* `buttonDirection`: Optional.
|
|
47
|
-
*
|
|
48
|
-
* - 'column': Buttons are laid out vertically.
|
|
49
|
-
* - 'auto': The layout is automatically determined by title length.
|
|
46
|
+
* `buttonDirection`: Optional. Layout of the action buttons: 'row' horizontal,
|
|
47
|
+
* 'column' vertical, 'auto' column only when a title is too long for a row.
|
|
50
48
|
*/
|
|
51
49
|
buttonDirection?: PopupActionDirection;
|
|
52
50
|
|
package/Popup/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type HighlightSegment = { text: string;
|
|
1
|
+
type HighlightSegment = { text: string; isHighlighted: boolean };
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* True for cased-script letters, digits and `_`; caseless scripts (CJK, Thai)
|
|
@@ -46,12 +46,15 @@ const splitHighlightText = (
|
|
|
46
46
|
continue;
|
|
47
47
|
}
|
|
48
48
|
if (start > lastIndex) {
|
|
49
|
-
segments.push({
|
|
49
|
+
segments.push({
|
|
50
|
+
text: text.slice(lastIndex, start),
|
|
51
|
+
isHighlighted: false,
|
|
52
|
+
});
|
|
50
53
|
}
|
|
51
|
-
segments.push({ text: match[0],
|
|
54
|
+
segments.push({ text: match[0], isHighlighted: true });
|
|
52
55
|
lastIndex = end;
|
|
53
56
|
}
|
|
54
|
-
segments.push({ text: text.slice(lastIndex),
|
|
57
|
+
segments.push({ text: text.slice(lastIndex), isHighlighted: false });
|
|
55
58
|
return segments;
|
|
56
59
|
};
|
|
57
60
|
|
package/Skeleton/index.tsx
CHANGED
|
@@ -21,6 +21,12 @@ import { Colors, Styles } from '../Consts';
|
|
|
21
21
|
import styles from './styles';
|
|
22
22
|
import { ScreenContext, SkeletonContext } from '../Context';
|
|
23
23
|
|
|
24
|
+
const PRIMARY_COLOR = Colors.black_05;
|
|
25
|
+
const HIGHLIGHT_COLOR1 = Colors.black_05;
|
|
26
|
+
const HIGHLIGHT_COLOR2 = Colors.black_03;
|
|
27
|
+
const SHIMMER_DURATION = 1000;
|
|
28
|
+
const LAYOUT_ANIMATION_DURATION = 500;
|
|
29
|
+
|
|
24
30
|
/**
|
|
25
31
|
* Shimmering placeholder that fills its parent. Reports loading to the screen
|
|
26
32
|
* context while mounted.
|
|
@@ -28,9 +34,6 @@ import { ScreenContext, SkeletonContext } from '../Context';
|
|
|
28
34
|
const Skeleton: React.FC<SkeletonTypes> = ({ style }) => {
|
|
29
35
|
const screen = useContext<any>(ScreenContext);
|
|
30
36
|
const [width, setWidth] = useState(0);
|
|
31
|
-
const PRIMARY_COLOR = Colors.black_05;
|
|
32
|
-
const HIGHLIGHT_COLOR1 = Colors.black_05;
|
|
33
|
-
const HIGHLIGHT_COLOR2 = Colors.black_03;
|
|
34
37
|
const progress = useSharedValue(0);
|
|
35
38
|
|
|
36
39
|
const shimmerColors = [HIGHLIGHT_COLOR1, HIGHLIGHT_COLOR2, HIGHLIGHT_COLOR1];
|
|
@@ -39,7 +42,7 @@ const Skeleton: React.FC<SkeletonTypes> = ({ style }) => {
|
|
|
39
42
|
progress.value = 0;
|
|
40
43
|
progress.value = withRepeat(
|
|
41
44
|
withTiming(1, {
|
|
42
|
-
duration:
|
|
45
|
+
duration: SHIMMER_DURATION,
|
|
43
46
|
easing: Easing.linear,
|
|
44
47
|
}),
|
|
45
48
|
-1,
|
|
@@ -57,7 +60,7 @@ const Skeleton: React.FC<SkeletonTypes> = ({ style }) => {
|
|
|
57
60
|
return { transform: [{ translateX }] };
|
|
58
61
|
});
|
|
59
62
|
|
|
60
|
-
const
|
|
63
|
+
const handleLayout = (newWidth: number) => {
|
|
61
64
|
if (newWidth !== width) {
|
|
62
65
|
setWidth(newWidth);
|
|
63
66
|
}
|
|
@@ -66,7 +69,7 @@ const Skeleton: React.FC<SkeletonTypes> = ({ style }) => {
|
|
|
66
69
|
return (
|
|
67
70
|
<View style={[styles.container, style]}>
|
|
68
71
|
<View
|
|
69
|
-
onLayout={e =>
|
|
72
|
+
onLayout={e => handleLayout(e.nativeEvent.layout.width)}
|
|
70
73
|
style={[Styles.flex, { backgroundColor: PRIMARY_COLOR }]}
|
|
71
74
|
>
|
|
72
75
|
<Animated.View style={[styles.shimmer, shimmerStyle]}>
|
|
@@ -96,28 +99,28 @@ const SkeletonProvider: React.FC<{ loading: boolean; children: any }> = ({
|
|
|
96
99
|
loading = true,
|
|
97
100
|
children,
|
|
98
101
|
}) => {
|
|
99
|
-
const [
|
|
102
|
+
const [isLoadingShown, setIsLoadingShown] = useState(loading);
|
|
100
103
|
|
|
101
104
|
useEffect(() => {
|
|
102
|
-
if (
|
|
105
|
+
if (isLoadingShown !== loading) {
|
|
103
106
|
if (Platform.OS === 'android') {
|
|
104
107
|
if (UIManager.setLayoutAnimationEnabledExperimental) {
|
|
105
108
|
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
106
109
|
}
|
|
107
110
|
}
|
|
108
111
|
LayoutAnimation.configureNext({
|
|
109
|
-
duration:
|
|
112
|
+
duration: LAYOUT_ANIMATION_DURATION,
|
|
110
113
|
create: {
|
|
111
114
|
type: LayoutAnimation.Types.easeInEaseOut,
|
|
112
115
|
property: LayoutAnimation.Properties.opacity,
|
|
113
116
|
},
|
|
114
117
|
});
|
|
115
|
-
|
|
118
|
+
setIsLoadingShown(loading);
|
|
116
119
|
}
|
|
117
|
-
}, [loading,
|
|
120
|
+
}, [loading, isLoadingShown]);
|
|
118
121
|
|
|
119
122
|
return (
|
|
120
|
-
<SkeletonContext.Provider value={{ loading:
|
|
123
|
+
<SkeletonContext.Provider value={{ loading: isLoadingShown }}>
|
|
121
124
|
{children}
|
|
122
125
|
</SkeletonContext.Provider>
|
|
123
126
|
);
|
package/package.json
CHANGED
|
@@ -1,165 +0,0 @@
|
|
|
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
|
-
};
|