@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
@@ -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 isInFavorites = updatedData?.items?.includes(context?.code);
54
- setIsFavorite(isInFavorites);
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 handleDismiss = () => {
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 handleClose = () => {
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
- handleDismiss();
92
+ onDismiss();
94
93
  },
95
94
  }}
96
95
  />
97
96
  ),
98
97
  });
99
98
  } else {
100
- handleDismiss();
99
+ onDismiss();
101
100
  }
102
101
  };
103
102
 
104
- const handlePressShortcut = () => {
103
+ const onPressShortcut = () => {
105
104
  const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
106
- const shouldBeFavorite = !isFavorite;
105
+ const expected = !isFavorite;
107
106
  context?.autoTracking?.({
108
107
  ...context,
109
108
  componentName: 'IconButton',
110
- componentId: `${shouldBeFavorite ? 'pin' : 'unpin'}_shortcut`,
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(shouldBeFavorite);
123
+ setIsFavorite(expected);
125
124
  }
126
125
  },
127
126
  );
128
127
  setTimeout(() => {
129
128
  setIsLoading(false);
130
- }, LOADING_RESET_DELAY);
129
+ }, 1000);
131
130
  };
132
131
 
133
- const handlePressHelpCenter = () => {
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 handlePressIconMore = () => {
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 = handlePressShortcut;
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 hasManyTools = tools && totalTools > 1;
182
- const hasOneTool = tools && totalTools === 1;
180
+ const showIconMore = tools && totalTools > 1;
181
+ const isHaveOneTool = tools && totalTools === 1;
183
182
 
184
- if (hasManyTools || useMore) {
183
+ if (showIconMore || useMore) {
185
184
  iconShortcut = 'navigation_more_icon';
186
- shortcutOnPress = handlePressIconMore;
187
- } else if (hasOneTool) {
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
- handlePressHelpCenter,
215
- handleClose,
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(INITIAL_SCALE)).current;
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: ENTER_DURATION,
101
+ duration: 250,
108
102
  useNativeDriver: true,
109
103
  }),
110
104
  Animated.timing(scale, {
111
105
  toValue: 1,
112
- duration: ENTER_DURATION,
113
- easing: ENTER_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 handleDismiss = useCallback(
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: EXIT_DURATION,
125
+ duration: 200,
132
126
  useNativeDriver: true,
133
127
  }),
134
128
  Animated.timing(scale, {
135
- toValue: INITIAL_SCALE,
136
- duration: EXIT_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
- handleDismiss(undefined, barrierDismissible);
146
+ onDismiss(undefined, barrierDismissible);
153
147
  return true;
154
148
  },
155
149
  );
156
150
  return () => backHandler.remove();
157
- }, [barrierDismissible, handleDismiss]);
151
+ }, [barrierDismissible, onDismiss]);
158
152
 
159
153
  return (
160
154
  <Container
161
155
  transparent
162
156
  visible={true}
163
- onRequestClose={handleDismiss}
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={() => handleDismiss(undefined, barrierDismissible)}
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={handleDismiss} />
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 focusAppListener: { remove?: () => void } | undefined;
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
- handleScreenNavigated(item?.screenName, screenName);
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
- focusAppListener = navigator?.maxApi?.listen?.('onFocusApp', () => {
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
- handleScreenNavigated(item?.screenName, screenName);
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
- }, TRACKING_TIMEOUT);
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
- focusAppListener?.remove?.();
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 handleScreenNavigated = (pre: string, current: string) => {
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
- }, ELEMENT_IDLE_TIMEOUT);
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
- }, LOADING_IDLE_TIMEOUT);
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
+ };
@@ -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 };
@@ -73,7 +73,6 @@ type ModalLayoutInput = {
73
73
  hinge?: ReservedRegion;
74
74
  };
75
75
 
76
- /** `value` limited to the `[min, max]` range. */
77
76
  const clamp = (value: number, min: number, max: number) => {
78
77
  return Math.min(Math.max(value, min), max);
79
78
  };
@@ -8,11 +8,12 @@ import {
8
8
  TransitionPresets,
9
9
  } from '@react-navigation/stack';
10
10
  import type { SharedValue } from 'react-native-reanimated';
11
- import { Colors, Spacing } from '../Consts';
11
+ import { Colors } from '../Consts';
12
12
  import { HeaderBackground } from './Components/HeaderBackground';
13
13
  import { HeaderLeft } from './Components/HeaderLeft';
14
14
  import { HeaderRight } from './Components/HeaderRight';
15
15
  import { HeaderTitle, TitleCustom } from './Components/HeaderTitle';
16
+ import { HeaderBarMetrics, TITLE_CONTAINER_RESET } from './headerLayout';
16
17
  import type { HeaderTitleProps, NavigationOptions } from './types';
17
18
 
18
19
  const CARD_SHADOW_OPACITY = 0.3;
@@ -55,13 +56,8 @@ const getStackOptions = (): StackNavigationOptions => {
55
56
  headerTitleAlign: 'left',
56
57
  headerTitle: props => <HeaderTitle {...props} />,
57
58
  headerTitleContainerStyle: {
58
- marginLeft: 0,
59
- marginEnd: 0,
60
- marginStart: 0,
61
- marginHorizontal: 0,
62
- marginVertical: 0,
63
- margin: 0,
64
- left: Spacing.S,
59
+ ...TITLE_CONTAINER_RESET,
60
+ left: HeaderBarMetrics.titleGap,
65
61
  },
66
62
  headerBackground: (props: any) => <HeaderBackground {...props} />,
67
63
  headerLeft: (props: any) => <HeaderLeft {...props} floating />,
@@ -172,13 +168,8 @@ const getOptions = (
172
168
  if (params.hiddenBack) {
173
169
  headerLeft = null;
174
170
  options.headerTitleContainerStyle = {
175
- marginLeft: 0,
176
- marginEnd: 0,
177
- marginStart: 0,
178
- marginHorizontal: 0,
179
- marginVertical: 0,
180
- margin: 0,
181
- left: Spacing.M,
171
+ ...TITLE_CONTAINER_RESET,
172
+ left: HeaderBarMetrics.sidePadding,
182
173
  };
183
174
  }
184
175
  options = {
@@ -98,6 +98,11 @@ export interface HeaderAnimatedProps extends ViewProps {
98
98
  image: string;
99
99
  useScale?: boolean;
100
100
  aspectRatio?: '16/9' | '3/2' | '1/1';
101
+ /**
102
+ * Optional. On wide windows the image keeps its phone width, centered; this
103
+ * color fills the sides and fades to transparent downward. No fill when unset.
104
+ */
105
+ bannerBackgroundColor?: string;
101
106
  }
102
107
 
103
108
  export type AnimatedHeader = {
@@ -109,8 +114,8 @@ export type AnimatedHeader = {
109
114
  export interface SearchHeaderProps extends InputSearchProps {
110
115
  ref?: React.RefObject<InputRef>;
111
116
  animatedValue?: Animated.Value | SharedValue<number>;
112
- headerRightWidth?: 0 | 74 | 110 | number;
113
- leftPosition?: 12 | 48 | number;
117
+ headerRightWidth?: 0 | 84 | 124 | number;
118
+ leftPosition?: 12 | 52 | number;
114
119
  renderButtons?: () => ReactNode;
115
120
  hiddenBack?: boolean;
116
121
  }
@@ -9,6 +9,7 @@ import type {
9
9
  OnBoarding,
10
10
  } from './types.header';
11
11
  import type { LocalizeProps, Theme } from './types.theme';
12
+ import type { HeaderContentLayout } from './headerLayout';
12
13
 
13
14
  export * from './types.theme';
14
15
  export * from './types.header';
@@ -128,4 +129,10 @@ export interface NavigationOptions
128
129
  hiddenBack?: boolean;
129
130
  headerTitle?: HeaderTitleProps | string;
130
131
  headerRight?: OnBoarding | HeaderRightToolkit | any;
132
+ /**
133
+ * Optional. `stretch` centers back, title and the right cluster in a 600pt
134
+ * frame on wide windows (foldable inner display, tablet); the background stays
135
+ * full width. Defaults to `grid`, the header row at the window edges.
136
+ */
137
+ contentLayout?: HeaderContentLayout;
131
138
  }
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
- /** Mini app context shared by the host via `Platform`; local if standalone. */
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 hasErrorRef = useRef(false);
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
- hasErrorRef.current = false;
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
- hasErrorRef.current = false;
102
+ error.current = false;
103
103
  onLoadStart?.();
104
104
  }}
105
105
  onLoad={event => {
106
- hasErrorRef.current = false;
106
+ error.current = false;
107
107
  onLoad?.(event);
108
108
  }}
109
109
  onLoadEnd={() => {
110
110
  let current: Status = 'success';
111
- if (hasErrorRef.current) {
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
- hasErrorRef.current = true;
118
+ error.current = true;
119
119
  onError?.();
120
120
  }}
121
121
  >