@momo-kits/foundation 0.169.9 → 0.169.10-beta.1
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 +17 -27
- package/Application/BottomSheet.tsx +27 -35
- package/Application/BottomTab/BottomTabBar.tsx +6 -6
- package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
- package/Application/BottomTab/TabScreen.tsx +10 -11
- package/Application/BottomTab/index.tsx +5 -5
- package/Application/BottomTab/usePressLift.ts +3 -3
- 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 +21 -54
- package/Application/Components/NavigationButton.tsx +9 -6
- package/Application/Components/SearchHeader.tsx +20 -7
- package/Application/Components/useToolkitActions.tsx +21 -22
- package/Application/ModalScreen.tsx +15 -19
- package/Application/StackScreen.tsx +18 -13
- package/Application/headerLayout.ts +165 -0
- package/Application/index.ts +11 -1
- package/Application/modalLayout.ts +0 -1
- package/Application/navigationOptions.tsx +6 -15
- package/Application/types.header.ts +7 -2
- package/Application/types.ts +7 -0
- package/Context/index.ts +1 -3
- package/Image/index.tsx +6 -6
- package/Input/Input.tsx +37 -40
- package/Input/InputDropDown.tsx +10 -13
- package/Input/InputMoney.tsx +17 -17
- package/Input/InputMoneyView.tsx +12 -12
- package/Input/InputOTP.tsx +34 -46
- package/Input/InputPhoneNumber.tsx +43 -48
- package/Input/InputSearch.tsx +39 -26
- package/Input/InputTextArea.tsx +23 -29
- package/Input/common.tsx +257 -0
- package/Input/index.tsx +7 -1
- package/Input/moneyUtils.ts +23 -0
- package/Input/utils.ts +7 -126
- package/Layout/FloatingButton.tsx +24 -38
- package/Layout/Screen.tsx +3 -0
- package/Layout/layoutMetrics.ts +34 -43
- package/Layout/screenHeader.tsx +55 -52
- package/Layout/screenTypes.ts +13 -4
- package/Pagination/PaginationScroll.tsx +3 -5
- package/Pagination/types.ts +5 -3
- package/Popup/PopupNotify.tsx +14 -17
- package/Popup/PopupPromotion.tsx +4 -5
- package/Popup/types.ts +4 -2
- package/Popup/utils.ts +4 -7
- package/Skeleton/index.tsx +12 -15
- package/package.json +1 -1
- package/Input/ErrorView.tsx +0 -59
- package/Input/FloatingView.tsx +0 -101
- package/Input/RenderTrailing.tsx +0 -79
- package/Input/constants.ts +0 -8
|
@@ -4,7 +4,6 @@ import { PopupNotify } from '../../Popup';
|
|
|
4
4
|
import { Tool, ToolGroup } from '../types';
|
|
5
5
|
|
|
6
6
|
const DID_SYNC_NEW_HOME = 'did_sync_new_home';
|
|
7
|
-
const LOADING_RESET_DELAY = 1000;
|
|
8
7
|
|
|
9
8
|
type ToolkitActionsParams = {
|
|
10
9
|
preventClose?: any;
|
|
@@ -50,8 +49,8 @@ const useToolkitActions = ({
|
|
|
50
49
|
const favoriteObserver = navigator?.maxApi?.observer?.(
|
|
51
50
|
DID_SYNC_NEW_HOME,
|
|
52
51
|
(updatedData: any) => {
|
|
53
|
-
const
|
|
54
|
-
setIsFavorite(
|
|
52
|
+
const status = updatedData?.items?.includes(context?.code);
|
|
53
|
+
setIsFavorite(status);
|
|
55
54
|
},
|
|
56
55
|
);
|
|
57
56
|
return () => {
|
|
@@ -60,7 +59,7 @@ const useToolkitActions = ({
|
|
|
60
59
|
}
|
|
61
60
|
}, [useShortcut, context, checkAppIsFavorite, navigator?.maxApi]);
|
|
62
61
|
|
|
63
|
-
const
|
|
62
|
+
const onDismiss = () => {
|
|
64
63
|
if (useCloseIcon) {
|
|
65
64
|
navigator?.maxApi?.dispatchFunction?.(
|
|
66
65
|
'dismiss',
|
|
@@ -72,7 +71,7 @@ const useToolkitActions = ({
|
|
|
72
71
|
}
|
|
73
72
|
};
|
|
74
73
|
|
|
75
|
-
const
|
|
74
|
+
const onClose = () => {
|
|
76
75
|
const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
|
|
77
76
|
context?.autoTracking?.({
|
|
78
77
|
...context,
|
|
@@ -90,24 +89,24 @@ const useToolkitActions = ({
|
|
|
90
89
|
title: preventClose.primary.title,
|
|
91
90
|
onPress: () => {
|
|
92
91
|
preventClose?.primary.onPress();
|
|
93
|
-
|
|
92
|
+
onDismiss();
|
|
94
93
|
},
|
|
95
94
|
}}
|
|
96
95
|
/>
|
|
97
96
|
),
|
|
98
97
|
});
|
|
99
98
|
} else {
|
|
100
|
-
|
|
99
|
+
onDismiss();
|
|
101
100
|
}
|
|
102
101
|
};
|
|
103
102
|
|
|
104
|
-
const
|
|
103
|
+
const onPressShortcut = () => {
|
|
105
104
|
const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
|
|
106
|
-
const
|
|
105
|
+
const expected = !isFavorite;
|
|
107
106
|
context?.autoTracking?.({
|
|
108
107
|
...context,
|
|
109
108
|
componentName: 'IconButton',
|
|
110
|
-
componentId: `${
|
|
109
|
+
componentId: `${expected ? 'pin' : 'unpin'}_shortcut`,
|
|
111
110
|
screenName: currentRoute?.params?.screen?.name ?? currentRoute?.name,
|
|
112
111
|
});
|
|
113
112
|
setIsLoading(true);
|
|
@@ -121,16 +120,16 @@ const useToolkitActions = ({
|
|
|
121
120
|
},
|
|
122
121
|
({ success }: { success: boolean }) => {
|
|
123
122
|
if (success) {
|
|
124
|
-
setIsFavorite(
|
|
123
|
+
setIsFavorite(expected);
|
|
125
124
|
}
|
|
126
125
|
},
|
|
127
126
|
);
|
|
128
127
|
setTimeout(() => {
|
|
129
128
|
setIsLoading(false);
|
|
130
|
-
},
|
|
129
|
+
}, 1000);
|
|
131
130
|
};
|
|
132
131
|
|
|
133
|
-
const
|
|
132
|
+
const onPressHelpCenter = () => {
|
|
134
133
|
const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
|
|
135
134
|
context?.autoTracking?.({
|
|
136
135
|
...context,
|
|
@@ -142,7 +141,7 @@ const useToolkitActions = ({
|
|
|
142
141
|
navigator?.maxApi?.dispatchFunction?.('showHelpCenter', context);
|
|
143
142
|
};
|
|
144
143
|
|
|
145
|
-
const
|
|
144
|
+
const onPressIconMore = () => {
|
|
146
145
|
const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
|
|
147
146
|
context?.autoTracking?.({
|
|
148
147
|
...context,
|
|
@@ -173,18 +172,18 @@ const useToolkitActions = ({
|
|
|
173
172
|
};
|
|
174
173
|
|
|
175
174
|
let iconShortcut = isFavorite ? 'pin_star_checked' : 'pin_star';
|
|
176
|
-
let shortcutOnPress =
|
|
175
|
+
let shortcutOnPress = onPressShortcut;
|
|
177
176
|
const totalTools = tools.reduce(
|
|
178
177
|
(count: number, group: ToolGroup) => count + group?.items?.length,
|
|
179
178
|
0,
|
|
180
179
|
);
|
|
181
|
-
const
|
|
182
|
-
const
|
|
180
|
+
const showIconMore = tools && totalTools > 1;
|
|
181
|
+
const isHaveOneTool = tools && totalTools === 1;
|
|
183
182
|
|
|
184
|
-
if (
|
|
183
|
+
if (showIconMore || useMore) {
|
|
185
184
|
iconShortcut = 'navigation_more_icon';
|
|
186
|
-
shortcutOnPress =
|
|
187
|
-
} else if (
|
|
185
|
+
shortcutOnPress = onPressIconMore;
|
|
186
|
+
} else if (isHaveOneTool) {
|
|
188
187
|
const singleToolGroup = tools.find(
|
|
189
188
|
(group: ToolGroup) => group?.items?.length === 1,
|
|
190
189
|
);
|
|
@@ -211,8 +210,8 @@ const useToolkitActions = ({
|
|
|
211
210
|
iconShortcut,
|
|
212
211
|
shortcutOnPress,
|
|
213
212
|
showBadge,
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
onPressHelpCenter,
|
|
214
|
+
onClose,
|
|
216
215
|
};
|
|
217
216
|
};
|
|
218
217
|
|
|
@@ -27,12 +27,6 @@ import { isIPhoneDuo } from './utils';
|
|
|
27
27
|
import { useModalLayout } from './modalLayout';
|
|
28
28
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
29
29
|
|
|
30
|
-
const DUO_ORIENTATIONS = ['portrait', 'landscape'];
|
|
31
|
-
const ENTER_DURATION = 250;
|
|
32
|
-
const EXIT_DURATION = 200;
|
|
33
|
-
const INITIAL_SCALE = 0.8;
|
|
34
|
-
const ENTER_EASING = Easing.bezier(0.2, 0.0, 0, 1.0);
|
|
35
|
-
|
|
36
30
|
/**
|
|
37
31
|
* Screen of the `Modal` route: a bottom sheet when pushed by
|
|
38
32
|
* `showBottomSheet`, otherwise a centered popup.
|
|
@@ -78,7 +72,7 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
78
72
|
}, [context, description, navigator?.maxApi, title]);
|
|
79
73
|
|
|
80
74
|
const opacity = useRef(new Animated.Value(0)).current;
|
|
81
|
-
const scale = useRef(new Animated.Value(
|
|
75
|
+
const scale = useRef(new Animated.Value(0.8)).current;
|
|
82
76
|
const scrimStyle = useMemo(
|
|
83
77
|
() => ({
|
|
84
78
|
opacity: opacity.interpolate({
|
|
@@ -104,13 +98,13 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
104
98
|
Animated.parallel([
|
|
105
99
|
Animated.timing(opacity, {
|
|
106
100
|
toValue: 1,
|
|
107
|
-
duration:
|
|
101
|
+
duration: 250,
|
|
108
102
|
useNativeDriver: true,
|
|
109
103
|
}),
|
|
110
104
|
Animated.timing(scale, {
|
|
111
105
|
toValue: 1,
|
|
112
|
-
duration:
|
|
113
|
-
easing:
|
|
106
|
+
duration: 250,
|
|
107
|
+
easing: Easing.bezier(0.2, 0.0, 0, 1.0),
|
|
114
108
|
useNativeDriver: true,
|
|
115
109
|
}),
|
|
116
110
|
]).start();
|
|
@@ -120,7 +114,7 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
120
114
|
};
|
|
121
115
|
}, [opacity, props.route.params, scale]);
|
|
122
116
|
|
|
123
|
-
const
|
|
117
|
+
const onDismiss = useCallback(
|
|
124
118
|
(callback = () => {}, preventClose = false) => {
|
|
125
119
|
if (preventClose) {
|
|
126
120
|
return;
|
|
@@ -128,12 +122,12 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
128
122
|
Animated.parallel([
|
|
129
123
|
Animated.timing(opacity, {
|
|
130
124
|
toValue: 0,
|
|
131
|
-
duration:
|
|
125
|
+
duration: 200,
|
|
132
126
|
useNativeDriver: true,
|
|
133
127
|
}),
|
|
134
128
|
Animated.timing(scale, {
|
|
135
|
-
toValue:
|
|
136
|
-
duration:
|
|
129
|
+
toValue: 0.8,
|
|
130
|
+
duration: 200,
|
|
137
131
|
easing: Easing.linear,
|
|
138
132
|
useNativeDriver: true,
|
|
139
133
|
}),
|
|
@@ -149,18 +143,18 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
149
143
|
const backHandler = BackHandler.addEventListener(
|
|
150
144
|
'hardwareBackPress',
|
|
151
145
|
() => {
|
|
152
|
-
|
|
146
|
+
onDismiss(undefined, barrierDismissible);
|
|
153
147
|
return true;
|
|
154
148
|
},
|
|
155
149
|
);
|
|
156
150
|
return () => backHandler.remove();
|
|
157
|
-
}, [barrierDismissible,
|
|
151
|
+
}, [barrierDismissible, onDismiss]);
|
|
158
152
|
|
|
159
153
|
return (
|
|
160
154
|
<Container
|
|
161
155
|
transparent
|
|
162
156
|
visible={true}
|
|
163
|
-
onRequestClose={
|
|
157
|
+
onRequestClose={onDismiss}
|
|
164
158
|
style={StyleSheet.absoluteFillObject}
|
|
165
159
|
isModalKit={true}
|
|
166
160
|
supportedOrientations={isDuo ? DUO_ORIENTATIONS : undefined}
|
|
@@ -171,7 +165,7 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
171
165
|
>
|
|
172
166
|
<Pressable
|
|
173
167
|
style={StyleSheet.absoluteFillObject}
|
|
174
|
-
onPress={() =>
|
|
168
|
+
onPress={() => onDismiss(undefined, barrierDismissible)}
|
|
175
169
|
>
|
|
176
170
|
<Animated.View style={[styles.overlayContent, scrimStyle]} />
|
|
177
171
|
</Pressable>
|
|
@@ -194,13 +188,15 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
194
188
|
modalStyle,
|
|
195
189
|
]}
|
|
196
190
|
>
|
|
197
|
-
<Component {...params} onRequestClose={
|
|
191
|
+
<Component {...params} onRequestClose={onDismiss} />
|
|
198
192
|
</Animated.View>
|
|
199
193
|
</KeyboardAvoidingView>
|
|
200
194
|
</Container>
|
|
201
195
|
);
|
|
202
196
|
};
|
|
203
197
|
|
|
198
|
+
const DUO_ORIENTATIONS = ['portrait', 'landscape'];
|
|
199
|
+
|
|
204
200
|
const styles = StyleSheet.create({
|
|
205
201
|
overlayContent: {
|
|
206
202
|
flex: 1,
|
|
@@ -8,10 +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
|
-
|
|
12
|
-
const TRACKING_TIMEOUT = 5000;
|
|
13
|
-
const ELEMENT_IDLE_TIMEOUT = 2000;
|
|
14
|
-
const LOADING_IDLE_TIMEOUT = 2000;
|
|
11
|
+
import { useHeaderFrameOptions } from './headerLayout';
|
|
15
12
|
|
|
16
13
|
const runAfterInteractions = InteractionManager.runAfterInteractions;
|
|
17
14
|
|
|
@@ -55,6 +52,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
55
52
|
const navigation = useRef(new Navigation(props.navigation, context)).current;
|
|
56
53
|
const heightHeader = useHeaderHeight();
|
|
57
54
|
const { isBackgroundToForeground } = useAppState();
|
|
55
|
+
const headerFrameOptions = useHeaderFrameOptions(options?.contentLayout);
|
|
58
56
|
|
|
59
57
|
const data = {
|
|
60
58
|
...initialParams,
|
|
@@ -73,9 +71,16 @@ const StackScreen: React.FC<any> = props => {
|
|
|
73
71
|
}
|
|
74
72
|
}, [navigation, options]);
|
|
75
73
|
|
|
74
|
+
// re-applied on fold, unfold and rotation, apart from the other options
|
|
75
|
+
useLayoutEffect(() => {
|
|
76
|
+
if (headerFrameOptions) {
|
|
77
|
+
navigation.instance.setOptions(headerFrameOptions);
|
|
78
|
+
}
|
|
79
|
+
}, [navigation, headerFrameOptions]);
|
|
80
|
+
|
|
76
81
|
useEffect(() => {
|
|
77
82
|
let focusScreen: any;
|
|
78
|
-
let
|
|
83
|
+
let onFocusApp: any;
|
|
79
84
|
if (['Invalid', 'screen'].includes(screenName)) {
|
|
80
85
|
navigator?.maxApi?.showPopup?.('notice', {
|
|
81
86
|
title: 'Invalid screen name',
|
|
@@ -89,19 +94,19 @@ const StackScreen: React.FC<any> = props => {
|
|
|
89
94
|
if (!bottomTab) {
|
|
90
95
|
focusScreen = props.navigation.addListener('focus', () => {
|
|
91
96
|
navigator?.maxApi?.getDataObserver?.('current_screen', (item: any) => {
|
|
92
|
-
|
|
97
|
+
onScreenNavigated(item?.screenName, screenName);
|
|
93
98
|
navigator?.maxApi?.setDataObserver?.('current_screen', {
|
|
94
99
|
screenName,
|
|
95
100
|
startTime,
|
|
96
101
|
});
|
|
97
102
|
});
|
|
98
103
|
});
|
|
99
|
-
|
|
104
|
+
onFocusApp = navigator?.maxApi?.listen?.('onFocusApp', () => {
|
|
100
105
|
if (props.navigation.isFocused()) {
|
|
101
106
|
navigator?.maxApi?.getDataObserver?.(
|
|
102
107
|
'current_screen',
|
|
103
108
|
(item: any) => {
|
|
104
|
-
|
|
109
|
+
onScreenNavigated(item?.screenName, screenName);
|
|
105
110
|
navigator?.maxApi?.setDataObserver?.('current_screen', {
|
|
106
111
|
screenName,
|
|
107
112
|
startTime,
|
|
@@ -130,7 +135,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
130
135
|
tracking.current.timeoutTracking = setTimeout(() => {
|
|
131
136
|
onScreenLoad();
|
|
132
137
|
onScreenInteraction();
|
|
133
|
-
},
|
|
138
|
+
}, 5000);
|
|
134
139
|
|
|
135
140
|
return () => {
|
|
136
141
|
onScreenLoad();
|
|
@@ -141,13 +146,13 @@ const StackScreen: React.FC<any> = props => {
|
|
|
141
146
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
142
147
|
clearTimeout(tracking.current.timeoutLoading);
|
|
143
148
|
focusScreen?.();
|
|
144
|
-
|
|
149
|
+
onFocusApp?.remove?.();
|
|
145
150
|
};
|
|
146
151
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
152
|
}, []);
|
|
148
153
|
|
|
149
154
|
/** Reports a navigation to this screen: `back` if it was shown before. */
|
|
150
|
-
const
|
|
155
|
+
const onScreenNavigated = (pre: string, current: string) => {
|
|
151
156
|
if (!isBackgroundToForeground) {
|
|
152
157
|
const item: any = {
|
|
153
158
|
preScreenName: pre,
|
|
@@ -238,7 +243,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
238
243
|
|
|
239
244
|
onScreenLoad();
|
|
240
245
|
onScreenInteraction();
|
|
241
|
-
},
|
|
246
|
+
}, 2000);
|
|
242
247
|
},
|
|
243
248
|
onLoading: (loading: boolean) => {
|
|
244
249
|
if (loading && tracking.current.timeStartLoading === 0) {
|
|
@@ -252,7 +257,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
252
257
|
clearTimeout(tracking.current.timeoutLoading);
|
|
253
258
|
tracking.current.timeoutLoading = setTimeout(() => {
|
|
254
259
|
onScreenLoading();
|
|
255
|
-
},
|
|
260
|
+
}, 2000);
|
|
256
261
|
},
|
|
257
262
|
onSetParams: (item: ScreenTrackingParams) => {
|
|
258
263
|
tracking.current.params = item;
|
|
@@ -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/Context/index.ts
CHANGED
|
@@ -5,11 +5,9 @@ import { defaultContext } from '../Consts';
|
|
|
5
5
|
const Context = createContext({});
|
|
6
6
|
const ApplicationContext = createContext(defaultContext);
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// Shared by the host app via `Platform`; standalone falls back to a local one.
|
|
9
9
|
const MiniAppContext = (Platform as any).MiniAppContext ?? Context;
|
|
10
|
-
/** Screen context shared by the host via `Platform`; local if standalone. */
|
|
11
10
|
const ScreenContext = (Platform as any).ScreenContext ?? Context;
|
|
12
|
-
/** Component context shared by the host via `Platform`; local if standalone. */
|
|
13
11
|
const ComponentContext = (Platform as any).ComponentContext ?? Context;
|
|
14
12
|
const SkeletonContext = createContext({ loading: false });
|
|
15
13
|
/**
|
package/Image/index.tsx
CHANGED
|
@@ -44,7 +44,7 @@ const Image: React.FC<ImageProps> = ({
|
|
|
44
44
|
const component: any = useContext(ComponentContext);
|
|
45
45
|
const context = useContext<any>(MiniAppContext);
|
|
46
46
|
const skeleton = useContext(SkeletonContext);
|
|
47
|
-
const
|
|
47
|
+
const error = useRef(false);
|
|
48
48
|
const [status, setStatus] = useState<Status>('success');
|
|
49
49
|
|
|
50
50
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
@@ -92,30 +92,30 @@ const Image: React.FC<ImageProps> = ({
|
|
|
92
92
|
showBaseLineDebug && styles.debugBaseLine,
|
|
93
93
|
]}
|
|
94
94
|
onProgress={event => {
|
|
95
|
-
|
|
95
|
+
error.current = false;
|
|
96
96
|
if (status !== 'loading' && loading) {
|
|
97
97
|
setStatus('loading');
|
|
98
98
|
}
|
|
99
99
|
onProgress?.(event);
|
|
100
100
|
}}
|
|
101
101
|
onLoadStart={() => {
|
|
102
|
-
|
|
102
|
+
error.current = false;
|
|
103
103
|
onLoadStart?.();
|
|
104
104
|
}}
|
|
105
105
|
onLoad={event => {
|
|
106
|
-
|
|
106
|
+
error.current = false;
|
|
107
107
|
onLoad?.(event);
|
|
108
108
|
}}
|
|
109
109
|
onLoadEnd={() => {
|
|
110
110
|
let current: Status = 'success';
|
|
111
|
-
if (
|
|
111
|
+
if (error.current) {
|
|
112
112
|
current = 'error';
|
|
113
113
|
}
|
|
114
114
|
setStatus(prevState => (prevState !== current ? current : prevState));
|
|
115
115
|
onLoadEnd?.();
|
|
116
116
|
}}
|
|
117
117
|
onError={() => {
|
|
118
|
-
|
|
118
|
+
error.current = true;
|
|
119
119
|
onError?.();
|
|
120
120
|
}}
|
|
121
121
|
>
|