@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.
Files changed (56) hide show
  1. package/Application/BottomSheet.hooks.ts +17 -27
  2. package/Application/BottomSheet.tsx +27 -35
  3. package/Application/BottomTab/BottomTabBar.tsx +6 -6
  4. package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
  5. package/Application/BottomTab/TabScreen.tsx +10 -11
  6. package/Application/BottomTab/index.tsx +5 -5
  7. package/Application/BottomTab/usePressLift.ts +3 -3
  8. package/Application/Components/HeaderAnimated.tsx +38 -13
  9. package/Application/Components/HeaderBackground.tsx +16 -3
  10. package/Application/Components/HeaderExtendHeader.tsx +41 -28
  11. package/Application/Components/HeaderLeft.tsx +4 -18
  12. package/Application/Components/HeaderRight.styles.ts +15 -13
  13. package/Application/Components/HeaderTitle.shared.ts +10 -11
  14. package/Application/Components/HeaderToolkitAction.tsx +21 -54
  15. package/Application/Components/NavigationButton.tsx +9 -6
  16. package/Application/Components/SearchHeader.tsx +20 -7
  17. package/Application/Components/useToolkitActions.tsx +21 -22
  18. package/Application/ModalScreen.tsx +15 -19
  19. package/Application/StackScreen.tsx +18 -13
  20. package/Application/headerLayout.ts +165 -0
  21. package/Application/index.ts +11 -1
  22. package/Application/modalLayout.ts +0 -1
  23. package/Application/navigationOptions.tsx +6 -15
  24. package/Application/types.header.ts +7 -2
  25. package/Application/types.ts +7 -0
  26. package/Context/index.ts +1 -3
  27. package/Image/index.tsx +6 -6
  28. package/Input/Input.tsx +37 -40
  29. package/Input/InputDropDown.tsx +10 -13
  30. package/Input/InputMoney.tsx +17 -17
  31. package/Input/InputMoneyView.tsx +12 -12
  32. package/Input/InputOTP.tsx +34 -46
  33. package/Input/InputPhoneNumber.tsx +43 -48
  34. package/Input/InputSearch.tsx +39 -26
  35. package/Input/InputTextArea.tsx +23 -29
  36. package/Input/common.tsx +257 -0
  37. package/Input/index.tsx +7 -1
  38. package/Input/moneyUtils.ts +23 -0
  39. package/Input/utils.ts +7 -126
  40. package/Layout/FloatingButton.tsx +24 -38
  41. package/Layout/Screen.tsx +3 -0
  42. package/Layout/layoutMetrics.ts +34 -43
  43. package/Layout/screenHeader.tsx +55 -52
  44. package/Layout/screenTypes.ts +13 -4
  45. package/Pagination/PaginationScroll.tsx +3 -5
  46. package/Pagination/types.ts +5 -3
  47. package/Popup/PopupNotify.tsx +14 -17
  48. package/Popup/PopupPromotion.tsx +4 -5
  49. package/Popup/types.ts +4 -2
  50. package/Popup/utils.ts +4 -7
  51. package/Skeleton/index.tsx +12 -15
  52. package/package.json +1 -1
  53. package/Input/ErrorView.tsx +0 -59
  54. package/Input/FloatingView.tsx +0 -101
  55. package/Input/RenderTrailing.tsx +0 -79
  56. package/Input/constants.ts +0 -8
@@ -21,8 +21,6 @@ import { BottomSheetParams, NavigatorProps } from './types';
21
21
  import { isIPhoneDuo } from './utils';
22
22
  import { useModalLayout } from './modalLayout';
23
23
 
24
- const MAX_IOS_BOTTOM_INSET = 21;
25
-
26
24
  /**
27
25
  * Sheet geometry for the current window: foldable placement and size from
28
26
  * `useModalLayout`, plus the insets and offsets the keyboard handling needs.
@@ -37,10 +35,7 @@ export const useBottomSheetLayout = () => {
37
35
  const insets = useSafeAreaInsets();
38
36
  const heightHeader = useHeaderHeight();
39
37
  const isIOS = Platform.OS === 'ios';
40
- let bottomInset = insets.bottom;
41
- if (isIOS) {
42
- bottomInset = Math.min(insets.bottom, MAX_IOS_BOTTOM_INSET);
43
- }
38
+ const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
44
39
  const keyboardOffset = heightHeader - bottomInset;
45
40
 
46
41
  return {
@@ -73,7 +68,7 @@ export const useDuoKeyboardLift = (
73
68
  if (!isDuo || !useKeyboardAvoidingView) {
74
69
  return;
75
70
  }
76
- const handleKeyboardChange = (e: KeyboardEvent) => {
71
+ const onKeyboardChange = (e: KeyboardEvent) => {
77
72
  const overlap = windowHeight - e.endCoordinates.screenY;
78
73
  const lift = Math.max(
79
74
  overlap - bottomInset - (keyboardVerticalOffset ?? 0),
@@ -91,8 +86,8 @@ export const useDuoKeyboardLift = (
91
86
  setKeyboardLift(lift);
92
87
  };
93
88
  const subscriptions = [
94
- Keyboard.addListener('keyboardWillChangeFrame', handleKeyboardChange),
95
- Keyboard.addListener('keyboardWillHide', handleKeyboardChange),
89
+ Keyboard.addListener('keyboardWillChangeFrame', onKeyboardChange),
90
+ Keyboard.addListener('keyboardWillHide', onKeyboardChange),
96
91
  ];
97
92
  return () => subscriptions.forEach(sub => sub.remove());
98
93
  }, [
@@ -154,7 +149,7 @@ export const useSheetDismiss = ({
154
149
  leftOptions,
155
150
  action,
156
151
  }: SheetDismissParams) => {
157
- const handleDismiss = useCallback(
152
+ const onDismiss = useCallback(
158
153
  (force = false, callback?: () => void) => {
159
154
  if (barrierDismissible && !force) {
160
155
  return;
@@ -169,37 +164,32 @@ export const useSheetDismiss = ({
169
164
  [barrierDismissible, closeAnimation, navigator],
170
165
  );
171
166
 
172
- const handleRequestClose = useCallback(
167
+ const onRequestClose = useCallback(
173
168
  (callback?: () => void) => {
174
- handleDismiss(true, callback);
169
+ onDismiss(true, callback);
175
170
  },
176
- [handleDismiss],
171
+ [onDismiss],
177
172
  );
178
173
 
179
174
  useEffect(() => {
180
175
  const backHandler = BackHandler.addEventListener(
181
176
  'hardwareBackPress',
182
177
  () => {
183
- handleDismiss();
178
+ onDismiss();
184
179
  return true;
185
180
  },
186
181
  );
187
182
  return () => backHandler.remove();
188
- }, [barrierDismissible, handleDismiss]);
183
+ }, [barrierDismissible, onDismiss]);
189
184
 
190
- const handlePressBack = useCallback(() => {
191
- handleDismiss(true, leftOptions?.onPressIconLeft);
192
- }, [leftOptions, handleDismiss]);
185
+ const onPressBack = useCallback(() => {
186
+ onDismiss(true, leftOptions?.onPressIconLeft);
187
+ }, [leftOptions, onDismiss]);
193
188
 
194
- const handlePressClose = useCallback(() => {
189
+ const onPressClose = useCallback(() => {
195
190
  action.current = 'icon_close';
196
- handleDismiss(true);
197
- }, [action, handleDismiss]);
191
+ onDismiss(true);
192
+ }, [action, onDismiss]);
198
193
 
199
- return {
200
- handleDismiss,
201
- handleRequestClose,
202
- handlePressBack,
203
- handlePressClose,
204
- };
194
+ return { onDismiss, onRequestClose, onPressBack, onPressClose };
205
195
  };
@@ -30,23 +30,6 @@ import {
30
30
  renderSheetHeader,
31
31
  } from './BottomSheet.parts';
32
32
 
33
- const OPEN_TIMING = {
34
- duration: 350,
35
- easing: Easing.bezier(0.05, 0.7, 0.1, 1),
36
- };
37
- const CLOSE_TIMING = {
38
- duration: 200,
39
- easing: Easing.bezier(0.3, 0.0, 0.8, 0.15),
40
- };
41
- const SETTLE_SPRING = {
42
- damping: 20,
43
- mass: 1,
44
- stiffness: 100,
45
- overshootClamping: false,
46
- energyThreshold: 1e-10,
47
- };
48
- const DISMISS_DRAG_DISTANCE = 100;
49
-
50
33
  /**
51
34
  * Bottom sheet route of the modal stack: slides the given screen up under a
52
35
  * drag handle and header, and reports how it was closed through `onDismiss`.
@@ -96,14 +79,22 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
96
79
  );
97
80
 
98
81
  const openAnimation = useCallback(() => {
99
- translateY.value = withTiming(0, OPEN_TIMING);
82
+ translateY.value = withTiming(0, {
83
+ duration: 350,
84
+ easing: Easing.bezier(0.05, 0.7, 0.1, 1),
85
+ });
100
86
  }, [translateY]);
101
87
 
102
88
  const closeAnimation = useCallback(
103
89
  (callback = () => {}) => {
104
- translateY.value = withTiming(heightDevice, CLOSE_TIMING, () => {
105
- return scheduleOnRN(callback);
106
- });
90
+ translateY.value = withTiming(
91
+ heightDevice,
92
+ {
93
+ duration: 200,
94
+ easing: Easing.bezier(0.3, 0.0, 0.8, 0.15),
95
+ },
96
+ () => scheduleOnRN(callback),
97
+ );
107
98
  },
108
99
  [heightDevice, translateY],
109
100
  );
@@ -120,11 +111,17 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
120
111
  }
121
112
  },
122
113
  onPanResponderRelease: (_, gestureState) => {
123
- if (gestureState.dy > DISMISS_DRAG_DISTANCE) {
114
+ if (gestureState.dy > 100) {
124
115
  action.current = 'gesture';
125
- handleDismiss(true);
116
+ onDismiss(true);
126
117
  } else {
127
- translateY.value = withSpring(0, SETTLE_SPRING);
118
+ translateY.value = withSpring(0, {
119
+ damping: 20,
120
+ mass: 1,
121
+ stiffness: 100,
122
+ overshootClamping: false,
123
+ energyThreshold: 1e-10,
124
+ });
128
125
  }
129
126
  },
130
127
  }),
@@ -154,12 +151,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
154
151
  leftOptions,
155
152
  action,
156
153
  });
157
- const {
158
- handleDismiss,
159
- handleRequestClose,
160
- handlePressBack,
161
- handlePressClose,
162
- } = handlers;
154
+ const { onDismiss, onRequestClose, onPressBack, onPressClose } = handlers;
163
155
 
164
156
  const showBack = !!leftOptions?.onPressIconLeft;
165
157
  const iconBack = leftOptions?.iconLeft ?? 'ic_back';
@@ -169,8 +161,8 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
169
161
  useDivider,
170
162
  theme,
171
163
  showBack,
172
- onPressBack: handlePressBack,
173
- onPressClose: handlePressClose,
164
+ onPressBack,
165
+ onPressClose,
174
166
  iconBack,
175
167
  options,
176
168
  leftOptions,
@@ -186,7 +178,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
186
178
  <Screen
187
179
  {...props}
188
180
  {...props.route.params}
189
- onRequestClose={handleRequestClose}
181
+ onRequestClose={onRequestClose}
190
182
  />
191
183
  );
192
184
 
@@ -213,7 +205,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
213
205
  transparent
214
206
  visible={true}
215
207
  onRequestClose={() => {
216
- handleDismiss();
208
+ onDismiss();
217
209
  }}
218
210
  style={StyleSheet.absoluteFillObject}
219
211
  isModalKit={true}
@@ -233,7 +225,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
233
225
  style={StyleSheet.absoluteFillObject}
234
226
  onPress={() => {
235
227
  action.current = 'touch';
236
- handleDismiss();
228
+ onDismiss();
237
229
  }}
238
230
  >
239
231
  <Animated.View style={[styles.overlay, animatedOverlayStyle]} />
@@ -54,16 +54,16 @@ const useCenterActionPress = () => {
54
54
  const buttonStyle = useAnimatedStyle(() => ({
55
55
  transform: [{ scale: scale.value }],
56
56
  }));
57
- const handlePressIn = () => {
57
+ const onPressIn = () => {
58
58
  scale.value = withTiming(
59
59
  TabBarMotion.ctaPressedScale,
60
60
  TabBarMotion.ctaPress,
61
61
  );
62
62
  };
63
- const handlePressOut = () => {
63
+ const onPressOut = () => {
64
64
  scale.value = withTiming(1, TabBarMotion.ctaPress);
65
65
  };
66
- return { buttonStyle, handlePressIn, handlePressOut };
66
+ return { buttonStyle, onPressIn, onPressOut };
67
67
  };
68
68
 
69
69
  /**
@@ -77,7 +77,7 @@ const CenterAction: React.FC<{ floatingButton: FloatingButtonProps }> = ({
77
77
  const context = useContext<any>(MiniAppContext);
78
78
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
79
79
  const { icon, label, onPress, labelContent } = floatingButton;
80
- const { buttonStyle, handlePressIn, handlePressOut } = useCenterActionPress();
80
+ const { buttonStyle, onPressIn, onPressOut } = useCenterActionPress();
81
81
  let iconNode = icon;
82
82
  if (typeof icon === 'string') {
83
83
  iconNode = (
@@ -95,8 +95,8 @@ const CenterAction: React.FC<{ floatingButton: FloatingButtonProps }> = ({
95
95
  accessibilityRole={'button'}
96
96
  accessibilityLabel={label}
97
97
  onPress={onPress}
98
- onPressIn={handlePressIn}
99
- onPressOut={handlePressOut}
98
+ onPressIn={onPressIn}
99
+ onPressOut={onPressOut}
100
100
  style={styles.cta}
101
101
  >
102
102
  <Animated.View
@@ -94,14 +94,14 @@ const RailCenterAction: React.FC<{ floatingButton: FloatingButtonProps }> = ({
94
94
  floatingButton,
95
95
  }) => {
96
96
  const { theme } = useContext(ApplicationContext);
97
- const { buttonStyle, handlePressIn, handlePressOut } = useCenterActionPress();
97
+ const { buttonStyle, onPressIn, onPressOut } = useCenterActionPress();
98
98
  return (
99
99
  <Pressable
100
100
  accessibilityRole={'button'}
101
101
  accessibilityLabel={floatingButton.label}
102
102
  onPress={floatingButton.onPress}
103
- onPressIn={handlePressIn}
104
- onPressOut={handlePressOut}
103
+ onPressIn={onPressIn}
104
+ onPressOut={onPressOut}
105
105
  style={styles.railCta}
106
106
  >
107
107
  <Animated.View
@@ -190,8 +190,8 @@ const DuoRailTabBar: React.FC<DuoTabBarProps> = props => {
190
190
  accessibilityLabel={current?.label}
191
191
  accessibilityHint={'bottom_tab_bar'}
192
192
  activeOpacity={1}
193
- onPressIn={compactLift.handlePressIn}
194
- onPressOut={compactLift.handlePressOut}
193
+ onPressIn={compactLift.onPressIn}
194
+ onPressOut={compactLift.onPressOut}
195
195
  onPress={expandTabBar}
196
196
  style={styles.railCompact}
197
197
  >
@@ -15,8 +15,6 @@ import { useScreenLayout } from '../../Layout';
15
15
  import { getHeaderRowOptions } from '../../Layout/utils';
16
16
  import { BottomTabInsetContext } from './BottomTabInset';
17
17
 
18
- const NAVIGATED_REPORT_DELAY = 100;
19
-
20
18
  const Stack = createStackNavigator();
21
19
 
22
20
  /**
@@ -31,15 +29,16 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
31
29
 
32
30
  const screenName = screen?.name || screen?.type?.name || 'Invalid';
33
31
  const { isBackgroundToForeground } = useAppState();
32
+ // Pad clear of the floating bar unless a Screen inside claims the inset to scroll under it.
34
33
  const { inset: tabBarInset } = useContext(BottomTabInsetContext);
35
- const [isInsetClaimed, setIsInsetClaimed] = useState(false);
34
+ const [insetClaimed, setInsetClaimed] = useState(false);
36
35
  const tabInsetValue = useMemo(
37
- () => ({ inset: tabBarInset, claim: () => setIsInsetClaimed(true) }),
36
+ () => ({ inset: tabBarInset, claim: () => setInsetClaimed(true) }),
38
37
  [tabBarInset],
39
38
  );
40
- const tabInsetStyle = { paddingBottom: isInsetClaimed ? 0 : tabBarInset };
39
+ const tabInsetStyle = { paddingBottom: insetClaimed ? 0 : tabBarInset };
41
40
 
42
- const handleScreenNavigated = (pre: string, current: string) => {
41
+ const onScreenNavigated = (pre: string, current: string) => {
43
42
  if (!isBackgroundToForeground) {
44
43
  const data: any = {
45
44
  preScreenName: pre,
@@ -69,13 +68,13 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
69
68
  navigator?.maxApi?.getDataObserver?.(
70
69
  'current_screen',
71
70
  (data: any) => {
72
- handleScreenNavigated(data?.screenName, screenName);
71
+ onScreenNavigated(data?.screenName, screenName);
73
72
  navigator?.maxApi?.setObserver?.('current_screen', {
74
73
  screenName,
75
74
  });
76
75
  },
77
76
  );
78
- }, NAVIGATED_REPORT_DELAY);
77
+ }, 100);
79
78
  },
80
79
  // eslint-disable-next-line react-hooks/exhaustive-deps
81
80
  [],
@@ -84,12 +83,12 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
84
83
 
85
84
  useEffect(
86
85
  () => {
87
- const focusAppListener = navigator?.maxApi?.listen?.('onFocusApp', () => {
86
+ const onFocusApp = navigator?.maxApi?.listen?.('onFocusApp', () => {
88
87
  if (navigation.isFocused()) {
89
88
  navigator?.maxApi?.getDataObserver?.(
90
89
  'current_screen',
91
90
  (data: any) => {
92
- handleScreenNavigated(data?.screenName, screenName);
91
+ onScreenNavigated(data?.screenName, screenName);
93
92
  navigator?.maxApi?.setObserver?.('current_screen', {
94
93
  screenName,
95
94
  });
@@ -99,7 +98,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
99
98
  });
100
99
 
101
100
  return () => {
102
- focusAppListener?.remove?.();
101
+ onFocusApp?.remove?.();
103
102
  };
104
103
  },
105
104
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -99,16 +99,16 @@ const BottomTab: React.FC<BottomTabProps> = ({
99
99
  }, []),
100
100
  );
101
101
 
102
- const handleFocus = (e: any) => {
102
+ const onFocus = (e: any) => {
103
103
  activeIndex.current = tabs.findIndex(i => e.target.includes(i.name));
104
104
 
105
105
  const isFirstTab = activeIndex.current === 0;
106
- const isGestureEnabled = Platform.OS === 'ios' && isFirstTab;
106
+ const gestureEnabled = Platform.OS === 'ios' && isFirstTab;
107
107
  navigation?.setOptions({
108
- gestureEnabled: isGestureEnabled,
108
+ gestureEnabled: gestureEnabled,
109
109
  });
110
110
  NativeModules?.MiniAppModule?.setShouldUseFloatingArrowBack?.(
111
- !isGestureEnabled,
111
+ !gestureEnabled,
112
112
  );
113
113
  };
114
114
 
@@ -123,7 +123,7 @@ const BottomTab: React.FC<BottomTabProps> = ({
123
123
  listeners?.tabPress?.(e);
124
124
  },
125
125
  focus: e => {
126
- handleFocus(e);
126
+ onFocus(e);
127
127
  StatusBar.setBarStyle('dark-content', true);
128
128
  listeners?.focus?.(e);
129
129
  },
@@ -18,15 +18,15 @@ const usePressLift = () => {
18
18
  const style = useAnimatedStyle(() => ({
19
19
  transform: [{ scale: 1 + lift.value * (PRESSED_SCALE - 1) }],
20
20
  }));
21
- const handlePressIn = () => {
21
+ const onPressIn = () => {
22
22
  if (!reduceMotion) {
23
23
  lift.value = withSpring(1, SPRING);
24
24
  }
25
25
  };
26
- const handlePressOut = () => {
26
+ const onPressOut = () => {
27
27
  lift.value = withSpring(0, SPRING);
28
28
  };
29
- return { style, handlePressIn, handlePressOut };
29
+ return { style, onPressIn, onPressOut };
30
30
  };
31
31
 
32
32
  export { usePressLift };
@@ -1,4 +1,5 @@
1
- import { StyleSheet } from 'react-native';
1
+ import { StyleSheet, View } from 'react-native';
2
+ import LinearGradient from 'react-native-linear-gradient';
2
3
  import Animated, {
3
4
  Extrapolation,
4
5
  interpolate,
@@ -9,6 +10,7 @@ import React from 'react';
9
10
  import { Image } from '../../Image';
10
11
  import { Colors } from '../../Consts';
11
12
  import { useNormalizedSharedValue } from '../hooks';
13
+ import { HeaderBarMetrics, useHeaderLayout } from '../headerLayout';
12
14
 
13
15
  const ASPECT_RATIOS: Record<
14
16
  NonNullable<HeaderAnimatedProps['aspectRatio']>,
@@ -21,16 +23,20 @@ const ASPECT_RATIOS: Record<
21
23
 
22
24
  /**
23
25
  * Parallax header image (or `children`): stretches when pulled down and fades out
24
- * as the content scrolls up.
26
+ * as the content scrolls up. Wide windows and the iPhone Duo inner display keep
27
+ * it at phone size, centered.
25
28
  */
26
29
  const HeaderAnimated: React.FC<HeaderAnimatedProps> = ({
27
30
  animatedValue,
28
31
  image,
29
32
  useScale = true,
30
33
  aspectRatio = '16/9',
34
+ bannerBackgroundColor,
31
35
  children,
32
36
  }) => {
33
37
  const sharedValue = useNormalizedSharedValue(animatedValue);
38
+ const { isBannerCentered, bannerWidth } = useHeaderLayout();
39
+ const ratio = ASPECT_RATIOS[aspectRatio];
34
40
  const animatedStyle = useAnimatedStyle(() => {
35
41
  let scale = 1;
36
42
  if (useScale) {
@@ -59,22 +65,36 @@ const HeaderAnimated: React.FC<HeaderAnimatedProps> = ({
59
65
  };
60
66
  });
61
67
 
68
+ const content = children ?? (
69
+ <Image
70
+ source={{
71
+ uri: image,
72
+ }}
73
+ style={styles.full}
74
+ />
75
+ );
76
+
77
+ if (!isBannerCentered) {
78
+ return (
79
+ <Animated.View
80
+ style={[styles.container, { aspectRatio: ratio }, animatedStyle]}
81
+ >
82
+ {content}
83
+ </Animated.View>
84
+ );
85
+ }
86
+
62
87
  return (
63
88
  <Animated.View
64
- style={[
65
- styles.container,
66
- { aspectRatio: ASPECT_RATIOS[aspectRatio] },
67
- animatedStyle,
68
- ]}
89
+ style={[styles.container, { height: bannerWidth / ratio }, animatedStyle]}
69
90
  >
70
- {children ?? (
71
- <Image
72
- source={{
73
- uri: image,
74
- }}
75
- style={styles.full}
91
+ {!!bannerBackgroundColor && (
92
+ <LinearGradient
93
+ colors={[bannerBackgroundColor, bannerBackgroundColor + '00']}
94
+ style={StyleSheet.absoluteFill}
76
95
  />
77
96
  )}
97
+ <View style={styles.phoneBanner}>{content}</View>
78
98
  </Animated.View>
79
99
  );
80
100
  };
@@ -90,6 +110,11 @@ const styles = StyleSheet.create({
90
110
  alignSelf: 'center',
91
111
  },
92
112
  full: { width: '100%', height: '100%' },
113
+ phoneBanner: {
114
+ width: HeaderBarMetrics.bannerWidth,
115
+ height: '100%',
116
+ alignSelf: 'center',
117
+ },
93
118
  });
94
119
 
95
120
  export { HeaderAnimated };
@@ -8,10 +8,12 @@ import { Colors, Styles } from '../../Consts';
8
8
  import { Image } from '../../Image';
9
9
  import BackgroundImageView from './BackgroundImageView';
10
10
  import { useNormalizedSharedValue } from '../hooks';
11
+ import { HeaderBarMetrics, useHeaderLayout } from '../headerLayout';
11
12
 
12
13
  /**
13
14
  * Header background: surface and image that fade in on scroll, with an optional
14
- * gradient (custom or theme) on top. Outlined when `showBaseLineDebug` is on.
15
+ * gradient (custom or theme) on top, at phone height on wide windows. Outlined
16
+ * when `showBaseLineDebug` is on.
15
17
  */
16
18
  const HeaderBackground: React.FC<HeaderBackgroundProps> = ({
17
19
  animatedValue,
@@ -25,6 +27,7 @@ const HeaderBackground: React.FC<HeaderBackgroundProps> = ({
25
27
  const gradientColor = customGradientColor ?? theme.colors.gradient;
26
28
  const headerImage = headerBackground ?? theme.assets?.headerBackground;
27
29
  const sharedValue = useNormalizedSharedValue(animatedValue);
30
+ const { isWide } = useHeaderLayout();
28
31
 
29
32
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
30
33
 
@@ -38,14 +41,19 @@ const HeaderBackground: React.FC<HeaderBackgroundProps> = ({
38
41
  />
39
42
  <View style={styles.gradientContainer}>
40
43
  {useGradient && !!gradientColor && (
41
- <Animated.View style={styles.extendedHeader}>
44
+ <Animated.View
45
+ style={[styles.extendedHeader, isWide && styles.phoneHeight]}
46
+ >
42
47
  <LinearGradient
43
48
  colors={[gradientColor, gradientColor + '00']}
44
49
  style={StyleSheet.absoluteFill}
45
50
  >
46
51
  {!!headerImage && (
47
52
  <Image
48
- style={styles.headerBackground}
53
+ style={[
54
+ styles.headerBackground,
55
+ isWide && styles.phoneHeight,
56
+ ]}
49
57
  source={{ uri: headerImage }}
50
58
  loading={false}
51
59
  />
@@ -75,6 +83,11 @@ const styles = StyleSheet.create({
75
83
  position: 'absolute',
76
84
  aspectRatio: 375 / 154,
77
85
  },
86
+ // wide windows keep the phone gradient height instead of scaling with the width
87
+ phoneHeight: {
88
+ aspectRatio: undefined,
89
+ height: HeaderBarMetrics.gradientHeight,
90
+ },
78
91
  debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
79
92
  });
80
93