@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.
Files changed (63) hide show
  1. package/Application/BottomSheet.hooks.ts +27 -17
  2. package/Application/BottomSheet.tsx +35 -27
  3. package/Application/BottomTab/ActiveIndicator.tsx +5 -1
  4. package/Application/BottomTab/BottomTabBar.styles.ts +18 -1
  5. package/Application/BottomTab/BottomTabBar.tsx +14 -16
  6. package/Application/BottomTab/CurvedContainer.tsx +8 -2
  7. package/Application/BottomTab/CustomBottomTabItem.tsx +2 -2
  8. package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
  9. package/Application/BottomTab/DuoTabBar.tsx +15 -3
  10. package/Application/BottomTab/TabScreen.tsx +11 -10
  11. package/Application/BottomTab/index.tsx +5 -5
  12. package/Application/BottomTab/metrics.ts +23 -3
  13. package/Application/BottomTab/usePressLift.ts +3 -3
  14. package/Application/Components/HeaderAnimated.tsx +13 -38
  15. package/Application/Components/HeaderBackground.tsx +3 -16
  16. package/Application/Components/HeaderExtendHeader.tsx +28 -41
  17. package/Application/Components/HeaderLeft.tsx +18 -4
  18. package/Application/Components/HeaderRight.styles.ts +13 -15
  19. package/Application/Components/HeaderTitle.shared.ts +11 -10
  20. package/Application/Components/HeaderToolkitAction.tsx +54 -21
  21. package/Application/Components/NavigationButton.tsx +6 -9
  22. package/Application/Components/SearchHeader.tsx +7 -20
  23. package/Application/Components/useToolkitActions.tsx +22 -21
  24. package/Application/ModalScreen.tsx +19 -15
  25. package/Application/StackScreen.tsx +13 -18
  26. package/Application/index.ts +1 -11
  27. package/Application/modalLayout.ts +1 -0
  28. package/Application/navigationOptions.tsx +15 -6
  29. package/Application/types.header.ts +2 -7
  30. package/Application/types.tabs.ts +0 -1
  31. package/Application/types.ts +0 -7
  32. package/Context/index.ts +3 -1
  33. package/Image/index.tsx +6 -6
  34. package/Input/ErrorView.tsx +59 -0
  35. package/Input/FloatingView.tsx +101 -0
  36. package/Input/Input.tsx +40 -37
  37. package/Input/InputDropDown.tsx +13 -10
  38. package/Input/InputMoney.tsx +17 -17
  39. package/Input/InputMoneyView.tsx +12 -12
  40. package/Input/InputOTP.tsx +46 -34
  41. package/Input/InputPhoneNumber.tsx +48 -43
  42. package/Input/InputSearch.tsx +26 -39
  43. package/Input/InputTextArea.tsx +29 -23
  44. package/Input/RenderTrailing.tsx +79 -0
  45. package/Input/constants.ts +8 -0
  46. package/Input/index.tsx +1 -7
  47. package/Input/utils.ts +126 -7
  48. package/Layout/FloatingButton.tsx +38 -24
  49. package/Layout/Screen.tsx +0 -3
  50. package/Layout/layoutMetrics.ts +43 -34
  51. package/Layout/screenHeader.tsx +52 -55
  52. package/Layout/screenTypes.ts +4 -13
  53. package/Pagination/PaginationScroll.tsx +5 -3
  54. package/Pagination/types.ts +3 -5
  55. package/Popup/PopupNotify.tsx +17 -14
  56. package/Popup/PopupPromotion.tsx +5 -4
  57. package/Popup/types.ts +2 -4
  58. package/Popup/utils.ts +7 -4
  59. package/Skeleton/index.tsx +15 -12
  60. package/package.json +1 -1
  61. package/Application/headerLayout.ts +0 -165
  62. package/Input/common.tsx +0 -257
  63. package/Input/moneyUtils.ts +0 -23
@@ -1,5 +1,5 @@
1
1
  import { Colors, Radius, Spacing, Styles } from '../../Consts';
2
- import { InputRef, InputSearch, type InputSearchProps } from '../../Input';
2
+ import { InputRef, InputSearch } from '../../Input';
3
3
  import { StyleSheet, TouchableOpacity, View } from 'react-native';
4
4
  import Animated, { useAnimatedStyle } from 'react-native-reanimated';
5
5
  import React, { useContext } from 'react';
@@ -8,13 +8,7 @@ import { ApplicationContext, MiniAppContext } from '../../Context';
8
8
  import { Text } from '../../Text';
9
9
  import { useNormalizedSharedValue } from '../hooks';
10
10
  import { useScreenLayout } from '../../Layout';
11
- import { HeaderBarMetrics } from '../headerLayout';
12
-
13
- const { buttonSize, pillWidth, sidePadding, titleGap } = HeaderBarMetrics;
14
- // help center | home pill and the bar padding right of it
15
- const TOOLKIT_WIDTH = pillWidth + sidePadding;
16
- // the iPhone Duo side bar keeps its right header width
17
- const DUO_TOOLKIT_WIDTH = 73;
11
+ import { DuoMetrics } from '../../Layout/utils';
18
12
 
19
13
  /**
20
14
  * Header search bar whose fill turns from surface to default once scrolled, with
@@ -34,21 +28,15 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
34
28
  const { theme, navigator } = useContext(ApplicationContext);
35
29
  const context = useContext<any>(MiniAppContext);
36
30
  const layout = useScreenLayout();
37
- let defaultRightWidth = TOOLKIT_WIDTH;
38
- let searchSize: InputSearchProps['size'] = 'header';
39
- if (layout.sideBar) {
40
- defaultRightWidth = DUO_TOOLKIT_WIDTH;
41
- searchSize = 'default';
42
- }
31
+ // the iPhone Duo status bar row keeps a side bar sized back button
32
+ const BACK_WIDTH = layout.statusBarClearance ? DuoMetrics.controlSize : 28;
43
33
 
44
34
  const sharedValue = useNormalizedSharedValue(animatedValue);
45
- const defaultLeft = buttonSize + sidePadding + titleGap;
46
- const leftPosition = props?.leftPosition ?? defaultLeft;
47
- const headerRightWidth = props?.headerRightWidth ?? defaultRightWidth;
35
+ const leftPosition = props?.leftPosition ?? BACK_WIDTH + 20;
36
+ const headerRightWidth = props?.headerRightWidth ?? 73;
48
37
 
49
38
  // iPhone Duo side bar holds back: the search shares the content width with the buttons only
50
- const rowWidth = layout.width - leftPosition - sidePadding;
51
- let searchMaxWidth = rowWidth - headerRightWidth;
39
+ let searchMaxWidth = layout.width - leftPosition - 12 - headerRightWidth;
52
40
  if (layout.sideBar) {
53
41
  searchMaxWidth = layout.contentWidth - Spacing.M * 2 - headerRightWidth;
54
42
  }
@@ -117,7 +105,6 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
117
105
  <InputSearch
118
106
  ref={ref}
119
107
  {...props}
120
- size={searchSize}
121
108
  backgroundColor={'transparent'}
122
109
  showButtonText={false}
123
110
  />
@@ -4,6 +4,7 @@ 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;
7
8
 
8
9
  type ToolkitActionsParams = {
9
10
  preventClose?: any;
@@ -49,8 +50,8 @@ const useToolkitActions = ({
49
50
  const favoriteObserver = navigator?.maxApi?.observer?.(
50
51
  DID_SYNC_NEW_HOME,
51
52
  (updatedData: any) => {
52
- const status = updatedData?.items?.includes(context?.code);
53
- setIsFavorite(status);
53
+ const isInFavorites = updatedData?.items?.includes(context?.code);
54
+ setIsFavorite(isInFavorites);
54
55
  },
55
56
  );
56
57
  return () => {
@@ -59,7 +60,7 @@ const useToolkitActions = ({
59
60
  }
60
61
  }, [useShortcut, context, checkAppIsFavorite, navigator?.maxApi]);
61
62
 
62
- const onDismiss = () => {
63
+ const handleDismiss = () => {
63
64
  if (useCloseIcon) {
64
65
  navigator?.maxApi?.dispatchFunction?.(
65
66
  'dismiss',
@@ -71,7 +72,7 @@ const useToolkitActions = ({
71
72
  }
72
73
  };
73
74
 
74
- const onClose = () => {
75
+ const handleClose = () => {
75
76
  const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
76
77
  context?.autoTracking?.({
77
78
  ...context,
@@ -89,24 +90,24 @@ const useToolkitActions = ({
89
90
  title: preventClose.primary.title,
90
91
  onPress: () => {
91
92
  preventClose?.primary.onPress();
92
- onDismiss();
93
+ handleDismiss();
93
94
  },
94
95
  }}
95
96
  />
96
97
  ),
97
98
  });
98
99
  } else {
99
- onDismiss();
100
+ handleDismiss();
100
101
  }
101
102
  };
102
103
 
103
- const onPressShortcut = () => {
104
+ const handlePressShortcut = () => {
104
105
  const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
105
- const expected = !isFavorite;
106
+ const shouldBeFavorite = !isFavorite;
106
107
  context?.autoTracking?.({
107
108
  ...context,
108
109
  componentName: 'IconButton',
109
- componentId: `${expected ? 'pin' : 'unpin'}_shortcut`,
110
+ componentId: `${shouldBeFavorite ? 'pin' : 'unpin'}_shortcut`,
110
111
  screenName: currentRoute?.params?.screen?.name ?? currentRoute?.name,
111
112
  });
112
113
  setIsLoading(true);
@@ -120,16 +121,16 @@ const useToolkitActions = ({
120
121
  },
121
122
  ({ success }: { success: boolean }) => {
122
123
  if (success) {
123
- setIsFavorite(expected);
124
+ setIsFavorite(shouldBeFavorite);
124
125
  }
125
126
  },
126
127
  );
127
128
  setTimeout(() => {
128
129
  setIsLoading(false);
129
- }, 1000);
130
+ }, LOADING_RESET_DELAY);
130
131
  };
131
132
 
132
- const onPressHelpCenter = () => {
133
+ const handlePressHelpCenter = () => {
133
134
  const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
134
135
  context?.autoTracking?.({
135
136
  ...context,
@@ -141,7 +142,7 @@ const useToolkitActions = ({
141
142
  navigator?.maxApi?.dispatchFunction?.('showHelpCenter', context);
142
143
  };
143
144
 
144
- const onPressIconMore = () => {
145
+ const handlePressIconMore = () => {
145
146
  const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
146
147
  context?.autoTracking?.({
147
148
  ...context,
@@ -172,18 +173,18 @@ const useToolkitActions = ({
172
173
  };
173
174
 
174
175
  let iconShortcut = isFavorite ? 'pin_star_checked' : 'pin_star';
175
- let shortcutOnPress = onPressShortcut;
176
+ let shortcutOnPress = handlePressShortcut;
176
177
  const totalTools = tools.reduce(
177
178
  (count: number, group: ToolGroup) => count + group?.items?.length,
178
179
  0,
179
180
  );
180
- const showIconMore = tools && totalTools > 1;
181
- const isHaveOneTool = tools && totalTools === 1;
181
+ const hasManyTools = tools && totalTools > 1;
182
+ const hasOneTool = tools && totalTools === 1;
182
183
 
183
- if (showIconMore || useMore) {
184
+ if (hasManyTools || useMore) {
184
185
  iconShortcut = 'navigation_more_icon';
185
- shortcutOnPress = onPressIconMore;
186
- } else if (isHaveOneTool) {
186
+ shortcutOnPress = handlePressIconMore;
187
+ } else if (hasOneTool) {
187
188
  const singleToolGroup = tools.find(
188
189
  (group: ToolGroup) => group?.items?.length === 1,
189
190
  );
@@ -210,8 +211,8 @@ const useToolkitActions = ({
210
211
  iconShortcut,
211
212
  shortcutOnPress,
212
213
  showBadge,
213
- onPressHelpCenter,
214
- onClose,
214
+ handlePressHelpCenter,
215
+ handleClose,
215
216
  };
216
217
  };
217
218
 
@@ -27,6 +27,12 @@ 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
+
30
36
  /**
31
37
  * Screen of the `Modal` route: a bottom sheet when pushed by
32
38
  * `showBottomSheet`, otherwise a centered popup.
@@ -72,7 +78,7 @@ const Modal: React.FC<ModalParams> = props => {
72
78
  }, [context, description, navigator?.maxApi, title]);
73
79
 
74
80
  const opacity = useRef(new Animated.Value(0)).current;
75
- const scale = useRef(new Animated.Value(0.8)).current;
81
+ const scale = useRef(new Animated.Value(INITIAL_SCALE)).current;
76
82
  const scrimStyle = useMemo(
77
83
  () => ({
78
84
  opacity: opacity.interpolate({
@@ -98,13 +104,13 @@ const Modal: React.FC<ModalParams> = props => {
98
104
  Animated.parallel([
99
105
  Animated.timing(opacity, {
100
106
  toValue: 1,
101
- duration: 250,
107
+ duration: ENTER_DURATION,
102
108
  useNativeDriver: true,
103
109
  }),
104
110
  Animated.timing(scale, {
105
111
  toValue: 1,
106
- duration: 250,
107
- easing: Easing.bezier(0.2, 0.0, 0, 1.0),
112
+ duration: ENTER_DURATION,
113
+ easing: ENTER_EASING,
108
114
  useNativeDriver: true,
109
115
  }),
110
116
  ]).start();
@@ -114,7 +120,7 @@ const Modal: React.FC<ModalParams> = props => {
114
120
  };
115
121
  }, [opacity, props.route.params, scale]);
116
122
 
117
- const onDismiss = useCallback(
123
+ const handleDismiss = useCallback(
118
124
  (callback = () => {}, preventClose = false) => {
119
125
  if (preventClose) {
120
126
  return;
@@ -122,12 +128,12 @@ const Modal: React.FC<ModalParams> = props => {
122
128
  Animated.parallel([
123
129
  Animated.timing(opacity, {
124
130
  toValue: 0,
125
- duration: 200,
131
+ duration: EXIT_DURATION,
126
132
  useNativeDriver: true,
127
133
  }),
128
134
  Animated.timing(scale, {
129
- toValue: 0.8,
130
- duration: 200,
135
+ toValue: INITIAL_SCALE,
136
+ duration: EXIT_DURATION,
131
137
  easing: Easing.linear,
132
138
  useNativeDriver: true,
133
139
  }),
@@ -143,18 +149,18 @@ const Modal: React.FC<ModalParams> = props => {
143
149
  const backHandler = BackHandler.addEventListener(
144
150
  'hardwareBackPress',
145
151
  () => {
146
- onDismiss(undefined, barrierDismissible);
152
+ handleDismiss(undefined, barrierDismissible);
147
153
  return true;
148
154
  },
149
155
  );
150
156
  return () => backHandler.remove();
151
- }, [barrierDismissible, onDismiss]);
157
+ }, [barrierDismissible, handleDismiss]);
152
158
 
153
159
  return (
154
160
  <Container
155
161
  transparent
156
162
  visible={true}
157
- onRequestClose={onDismiss}
163
+ onRequestClose={handleDismiss}
158
164
  style={StyleSheet.absoluteFillObject}
159
165
  isModalKit={true}
160
166
  supportedOrientations={isDuo ? DUO_ORIENTATIONS : undefined}
@@ -165,7 +171,7 @@ const Modal: React.FC<ModalParams> = props => {
165
171
  >
166
172
  <Pressable
167
173
  style={StyleSheet.absoluteFillObject}
168
- onPress={() => onDismiss(undefined, barrierDismissible)}
174
+ onPress={() => handleDismiss(undefined, barrierDismissible)}
169
175
  >
170
176
  <Animated.View style={[styles.overlayContent, scrimStyle]} />
171
177
  </Pressable>
@@ -188,15 +194,13 @@ const Modal: React.FC<ModalParams> = props => {
188
194
  modalStyle,
189
195
  ]}
190
196
  >
191
- <Component {...params} onRequestClose={onDismiss} />
197
+ <Component {...params} onRequestClose={handleDismiss} />
192
198
  </Animated.View>
193
199
  </KeyboardAvoidingView>
194
200
  </Container>
195
201
  );
196
202
  };
197
203
 
198
- const DUO_ORIENTATIONS = ['portrait', 'landscape'];
199
-
200
204
  const styles = StyleSheet.create({
201
205
  overlayContent: {
202
206
  flex: 1,
@@ -8,7 +8,10 @@ import { SwipeBackHaptic } from './Components/SwipeBackHaptic';
8
8
  import { useAppState } from './hooks';
9
9
  import { createScreenTracking } from './StackScreen.tracking';
10
10
  import { TooltipPortalHost, TooltipPortalProvider } from './TooltipPortal';
11
- import { useHeaderFrameOptions } from './headerLayout';
11
+
12
+ const TRACKING_TIMEOUT = 5000;
13
+ const ELEMENT_IDLE_TIMEOUT = 2000;
14
+ const LOADING_IDLE_TIMEOUT = 2000;
12
15
 
13
16
  const runAfterInteractions = InteractionManager.runAfterInteractions;
14
17
 
@@ -52,7 +55,6 @@ const StackScreen: React.FC<any> = props => {
52
55
  const navigation = useRef(new Navigation(props.navigation, context)).current;
53
56
  const heightHeader = useHeaderHeight();
54
57
  const { isBackgroundToForeground } = useAppState();
55
- const headerFrameOptions = useHeaderFrameOptions(options?.contentLayout);
56
58
 
57
59
  const data = {
58
60
  ...initialParams,
@@ -71,16 +73,9 @@ const StackScreen: React.FC<any> = props => {
71
73
  }
72
74
  }, [navigation, options]);
73
75
 
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
-
81
76
  useEffect(() => {
82
77
  let focusScreen: any;
83
- let onFocusApp: any;
78
+ let focusAppListener: { remove?: () => void } | undefined;
84
79
  if (['Invalid', 'screen'].includes(screenName)) {
85
80
  navigator?.maxApi?.showPopup?.('notice', {
86
81
  title: 'Invalid screen name',
@@ -94,19 +89,19 @@ const StackScreen: React.FC<any> = props => {
94
89
  if (!bottomTab) {
95
90
  focusScreen = props.navigation.addListener('focus', () => {
96
91
  navigator?.maxApi?.getDataObserver?.('current_screen', (item: any) => {
97
- onScreenNavigated(item?.screenName, screenName);
92
+ handleScreenNavigated(item?.screenName, screenName);
98
93
  navigator?.maxApi?.setDataObserver?.('current_screen', {
99
94
  screenName,
100
95
  startTime,
101
96
  });
102
97
  });
103
98
  });
104
- onFocusApp = navigator?.maxApi?.listen?.('onFocusApp', () => {
99
+ focusAppListener = navigator?.maxApi?.listen?.('onFocusApp', () => {
105
100
  if (props.navigation.isFocused()) {
106
101
  navigator?.maxApi?.getDataObserver?.(
107
102
  'current_screen',
108
103
  (item: any) => {
109
- onScreenNavigated(item?.screenName, screenName);
104
+ handleScreenNavigated(item?.screenName, screenName);
110
105
  navigator?.maxApi?.setDataObserver?.('current_screen', {
111
106
  screenName,
112
107
  startTime,
@@ -135,7 +130,7 @@ const StackScreen: React.FC<any> = props => {
135
130
  tracking.current.timeoutTracking = setTimeout(() => {
136
131
  onScreenLoad();
137
132
  onScreenInteraction();
138
- }, 5000);
133
+ }, TRACKING_TIMEOUT);
139
134
 
140
135
  return () => {
141
136
  onScreenLoad();
@@ -146,13 +141,13 @@ const StackScreen: React.FC<any> = props => {
146
141
  // eslint-disable-next-line react-hooks/exhaustive-deps
147
142
  clearTimeout(tracking.current.timeoutLoading);
148
143
  focusScreen?.();
149
- onFocusApp?.remove?.();
144
+ focusAppListener?.remove?.();
150
145
  };
151
146
  // eslint-disable-next-line react-hooks/exhaustive-deps
152
147
  }, []);
153
148
 
154
149
  /** Reports a navigation to this screen: `back` if it was shown before. */
155
- const onScreenNavigated = (pre: string, current: string) => {
150
+ const handleScreenNavigated = (pre: string, current: string) => {
156
151
  if (!isBackgroundToForeground) {
157
152
  const item: any = {
158
153
  preScreenName: pre,
@@ -243,7 +238,7 @@ const StackScreen: React.FC<any> = props => {
243
238
 
244
239
  onScreenLoad();
245
240
  onScreenInteraction();
246
- }, 2000);
241
+ }, ELEMENT_IDLE_TIMEOUT);
247
242
  },
248
243
  onLoading: (loading: boolean) => {
249
244
  if (loading && tracking.current.timeStartLoading === 0) {
@@ -257,7 +252,7 @@ const StackScreen: React.FC<any> = props => {
257
252
  clearTimeout(tracking.current.timeoutLoading);
258
253
  tracking.current.timeoutLoading = setTimeout(() => {
259
254
  onScreenLoading();
260
- }, 2000);
255
+ }, LOADING_IDLE_TIMEOUT);
261
256
  },
262
257
  onSetParams: (item: ScreenTrackingParams) => {
263
258
  tracking.current.params = item;
@@ -26,13 +26,6 @@ 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';
36
29
 
37
30
  export {
38
31
  NavigationContainer,
@@ -48,9 +41,6 @@ export {
48
41
  useTooltipPortal,
49
42
  getModalLayout,
50
43
  useModalLayout,
51
- HeaderBarMetrics,
52
- getHeaderLayout,
53
- useHeaderLayout,
54
44
  };
55
45
 
56
- export type { ModalLayout, ModalRect, HeaderContentLayout, HeaderLayout };
46
+ export type { ModalLayout, ModalRect };
@@ -73,6 +73,7 @@ type ModalLayoutInput = {
73
73
  hinge?: ReservedRegion;
74
74
  };
75
75
 
76
+ /** `value` limited to the `[min, max]` range. */
76
77
  const clamp = (value: number, min: number, max: number) => {
77
78
  return Math.min(Math.max(value, min), max);
78
79
  };
@@ -8,12 +8,11 @@ import {
8
8
  TransitionPresets,
9
9
  } from '@react-navigation/stack';
10
10
  import type { SharedValue } from 'react-native-reanimated';
11
- import { Colors } from '../Consts';
11
+ import { Colors, Spacing } 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';
17
16
  import type { HeaderTitleProps, NavigationOptions } from './types';
18
17
 
19
18
  const CARD_SHADOW_OPACITY = 0.3;
@@ -56,8 +55,13 @@ const getStackOptions = (): StackNavigationOptions => {
56
55
  headerTitleAlign: 'left',
57
56
  headerTitle: props => <HeaderTitle {...props} />,
58
57
  headerTitleContainerStyle: {
59
- ...TITLE_CONTAINER_RESET,
60
- left: HeaderBarMetrics.titleGap,
58
+ marginLeft: 0,
59
+ marginEnd: 0,
60
+ marginStart: 0,
61
+ marginHorizontal: 0,
62
+ marginVertical: 0,
63
+ margin: 0,
64
+ left: Spacing.S,
61
65
  },
62
66
  headerBackground: (props: any) => <HeaderBackground {...props} />,
63
67
  headerLeft: (props: any) => <HeaderLeft {...props} floating />,
@@ -168,8 +172,13 @@ const getOptions = (
168
172
  if (params.hiddenBack) {
169
173
  headerLeft = null;
170
174
  options.headerTitleContainerStyle = {
171
- ...TITLE_CONTAINER_RESET,
172
- left: HeaderBarMetrics.sidePadding,
175
+ marginLeft: 0,
176
+ marginEnd: 0,
177
+ marginStart: 0,
178
+ marginHorizontal: 0,
179
+ marginVertical: 0,
180
+ margin: 0,
181
+ left: Spacing.M,
173
182
  };
174
183
  }
175
184
  options = {
@@ -98,11 +98,6 @@ 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;
106
101
  }
107
102
 
108
103
  export type AnimatedHeader = {
@@ -114,8 +109,8 @@ export type AnimatedHeader = {
114
109
  export interface SearchHeaderProps extends InputSearchProps {
115
110
  ref?: React.RefObject<InputRef>;
116
111
  animatedValue?: Animated.Value | SharedValue<number>;
117
- headerRightWidth?: 0 | 84 | 124 | number;
118
- leftPosition?: 12 | 52 | number;
112
+ headerRightWidth?: 0 | 74 | 110 | number;
113
+ leftPosition?: 12 | 48 | number;
119
114
  renderButtons?: () => ReactNode;
120
115
  hiddenBack?: boolean;
121
116
  }
@@ -36,7 +36,6 @@ export type BottomTabProps = {
36
36
  };
37
37
 
38
38
  export type FloatingButtonProps = {
39
- container?: React.ReactNode;
40
39
  icon: string | React.ReactNode;
41
40
  label: string;
42
41
  onPress: () => void;
@@ -9,7 +9,6 @@ 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';
13
12
 
14
13
  export * from './types.theme';
15
14
  export * from './types.header';
@@ -129,10 +128,4 @@ export interface NavigationOptions
129
128
  hiddenBack?: boolean;
130
129
  headerTitle?: HeaderTitleProps | string;
131
130
  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;
138
131
  }
package/Context/index.ts CHANGED
@@ -5,9 +5,11 @@ import { defaultContext } from '../Consts';
5
5
  const Context = createContext({});
6
6
  const ApplicationContext = createContext(defaultContext);
7
7
 
8
- // Shared by the host app via `Platform`; standalone falls back to a local one.
8
+ /** Mini app context shared by the host via `Platform`; local if standalone. */
9
9
  const MiniAppContext = (Platform as any).MiniAppContext ?? Context;
10
+ /** Screen context shared by the host via `Platform`; local if standalone. */
10
11
  const ScreenContext = (Platform as any).ScreenContext ?? Context;
12
+ /** Component context shared by the host via `Platform`; local if standalone. */
11
13
  const ComponentContext = (Platform as any).ComponentContext ?? Context;
12
14
  const SkeletonContext = createContext({ loading: false });
13
15
  /**
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 error = useRef(false);
47
+ const hasErrorRef = 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
- error.current = false;
95
+ hasErrorRef.current = false;
96
96
  if (status !== 'loading' && loading) {
97
97
  setStatus('loading');
98
98
  }
99
99
  onProgress?.(event);
100
100
  }}
101
101
  onLoadStart={() => {
102
- error.current = false;
102
+ hasErrorRef.current = false;
103
103
  onLoadStart?.();
104
104
  }}
105
105
  onLoad={event => {
106
- error.current = false;
106
+ hasErrorRef.current = false;
107
107
  onLoad?.(event);
108
108
  }}
109
109
  onLoadEnd={() => {
110
110
  let current: Status = 'success';
111
- if (error.current) {
111
+ if (hasErrorRef.current) {
112
112
  current = 'error';
113
113
  }
114
114
  setStatus(prevState => (prevState !== current ? current : prevState));
115
115
  onLoadEnd?.();
116
116
  }}
117
117
  onError={() => {
118
- error.current = true;
118
+ hasErrorRef.current = true;
119
119
  onError?.();
120
120
  }}
121
121
  >
@@ -0,0 +1,59 @@
1
+ import React, { type FC, useContext } from 'react';
2
+ import { View } from 'react-native';
3
+ import { Styles } from '../Consts';
4
+ import { ApplicationContext } from '../Context';
5
+ import { Icon } from '../Icon';
6
+ import { Text, useScaleSize } from '../Text';
7
+ import styles from './styles';
8
+
9
+ const ICON_SIZE = 16;
10
+
11
+ type ErrorViewProps = {
12
+ errorMessage?: string;
13
+ errorSpacing?: boolean;
14
+ hintText?: string;
15
+ componentId?: string;
16
+ };
17
+
18
+ /**
19
+ * Error or hint row under an input (error wins); with neither, `errorSpacing`
20
+ * keeps an empty row so the layout doesn't jump when an error appears.
21
+ */
22
+ const ErrorView: FC<ErrorViewProps> = ({
23
+ errorMessage,
24
+ errorSpacing,
25
+ hintText,
26
+ componentId,
27
+ }) => {
28
+ const { theme } = useContext(ApplicationContext);
29
+ const errorColor = theme.colors.error.primary;
30
+ const hintColor = theme.colors.text.hint;
31
+ const hintTextDefault = hintText ?? 'Không thể chỉnh sửa';
32
+ const scaledIconSize = useScaleSize(ICON_SIZE);
33
+
34
+ if (errorMessage || hintText) {
35
+ const messageColor = errorMessage ? errorColor : hintColor;
36
+ const messageType = errorMessage ? 'error' : 'hint';
37
+ return (
38
+ <View style={styles.errorView}>
39
+ <View style={styles.errorIcon}>
40
+ <Icon color={messageColor} source="ic_error" size={scaledIconSize} />
41
+ </View>
42
+ <Text
43
+ accessibilityLabel={`${componentId}|${messageType}-text`}
44
+ style={Styles.flex}
45
+ color={messageColor}
46
+ typography={'description_default_regular'}
47
+ >
48
+ {errorMessage ?? hintTextDefault}
49
+ </Text>
50
+ </View>
51
+ );
52
+ }
53
+ if (errorSpacing) {
54
+ return <View style={[styles.errorView, styles.errorSpacing]} />;
55
+ }
56
+ return <View />;
57
+ };
58
+
59
+ export { ErrorView };