@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
@@ -21,6 +21,8 @@ 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
+
24
26
  /**
25
27
  * Sheet geometry for the current window: foldable placement and size from
26
28
  * `useModalLayout`, plus the insets and offsets the keyboard handling needs.
@@ -35,7 +37,10 @@ export const useBottomSheetLayout = () => {
35
37
  const insets = useSafeAreaInsets();
36
38
  const heightHeader = useHeaderHeight();
37
39
  const isIOS = Platform.OS === 'ios';
38
- const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
40
+ let bottomInset = insets.bottom;
41
+ if (isIOS) {
42
+ bottomInset = Math.min(insets.bottom, MAX_IOS_BOTTOM_INSET);
43
+ }
39
44
  const keyboardOffset = heightHeader - bottomInset;
40
45
 
41
46
  return {
@@ -68,7 +73,7 @@ export const useDuoKeyboardLift = (
68
73
  if (!isDuo || !useKeyboardAvoidingView) {
69
74
  return;
70
75
  }
71
- const onKeyboardChange = (e: KeyboardEvent) => {
76
+ const handleKeyboardChange = (e: KeyboardEvent) => {
72
77
  const overlap = windowHeight - e.endCoordinates.screenY;
73
78
  const lift = Math.max(
74
79
  overlap - bottomInset - (keyboardVerticalOffset ?? 0),
@@ -86,8 +91,8 @@ export const useDuoKeyboardLift = (
86
91
  setKeyboardLift(lift);
87
92
  };
88
93
  const subscriptions = [
89
- Keyboard.addListener('keyboardWillChangeFrame', onKeyboardChange),
90
- Keyboard.addListener('keyboardWillHide', onKeyboardChange),
94
+ Keyboard.addListener('keyboardWillChangeFrame', handleKeyboardChange),
95
+ Keyboard.addListener('keyboardWillHide', handleKeyboardChange),
91
96
  ];
92
97
  return () => subscriptions.forEach(sub => sub.remove());
93
98
  }, [
@@ -149,7 +154,7 @@ export const useSheetDismiss = ({
149
154
  leftOptions,
150
155
  action,
151
156
  }: SheetDismissParams) => {
152
- const onDismiss = useCallback(
157
+ const handleDismiss = useCallback(
153
158
  (force = false, callback?: () => void) => {
154
159
  if (barrierDismissible && !force) {
155
160
  return;
@@ -164,32 +169,37 @@ export const useSheetDismiss = ({
164
169
  [barrierDismissible, closeAnimation, navigator],
165
170
  );
166
171
 
167
- const onRequestClose = useCallback(
172
+ const handleRequestClose = useCallback(
168
173
  (callback?: () => void) => {
169
- onDismiss(true, callback);
174
+ handleDismiss(true, callback);
170
175
  },
171
- [onDismiss],
176
+ [handleDismiss],
172
177
  );
173
178
 
174
179
  useEffect(() => {
175
180
  const backHandler = BackHandler.addEventListener(
176
181
  'hardwareBackPress',
177
182
  () => {
178
- onDismiss();
183
+ handleDismiss();
179
184
  return true;
180
185
  },
181
186
  );
182
187
  return () => backHandler.remove();
183
- }, [barrierDismissible, onDismiss]);
188
+ }, [barrierDismissible, handleDismiss]);
184
189
 
185
- const onPressBack = useCallback(() => {
186
- onDismiss(true, leftOptions?.onPressIconLeft);
187
- }, [leftOptions, onDismiss]);
190
+ const handlePressBack = useCallback(() => {
191
+ handleDismiss(true, leftOptions?.onPressIconLeft);
192
+ }, [leftOptions, handleDismiss]);
188
193
 
189
- const onPressClose = useCallback(() => {
194
+ const handlePressClose = useCallback(() => {
190
195
  action.current = 'icon_close';
191
- onDismiss(true);
192
- }, [action, onDismiss]);
196
+ handleDismiss(true);
197
+ }, [action, handleDismiss]);
193
198
 
194
- return { onDismiss, onRequestClose, onPressBack, onPressClose };
199
+ return {
200
+ handleDismiss,
201
+ handleRequestClose,
202
+ handlePressBack,
203
+ handlePressClose,
204
+ };
195
205
  };
@@ -30,6 +30,23 @@ 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
+
33
50
  /**
34
51
  * Bottom sheet route of the modal stack: slides the given screen up under a
35
52
  * drag handle and header, and reports how it was closed through `onDismiss`.
@@ -79,22 +96,14 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
79
96
  );
80
97
 
81
98
  const openAnimation = useCallback(() => {
82
- translateY.value = withTiming(0, {
83
- duration: 350,
84
- easing: Easing.bezier(0.05, 0.7, 0.1, 1),
85
- });
99
+ translateY.value = withTiming(0, OPEN_TIMING);
86
100
  }, [translateY]);
87
101
 
88
102
  const closeAnimation = useCallback(
89
103
  (callback = () => {}) => {
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
- );
104
+ translateY.value = withTiming(heightDevice, CLOSE_TIMING, () => {
105
+ return scheduleOnRN(callback);
106
+ });
98
107
  },
99
108
  [heightDevice, translateY],
100
109
  );
@@ -111,17 +120,11 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
111
120
  }
112
121
  },
113
122
  onPanResponderRelease: (_, gestureState) => {
114
- if (gestureState.dy > 100) {
123
+ if (gestureState.dy > DISMISS_DRAG_DISTANCE) {
115
124
  action.current = 'gesture';
116
- onDismiss(true);
125
+ handleDismiss(true);
117
126
  } else {
118
- translateY.value = withSpring(0, {
119
- damping: 20,
120
- mass: 1,
121
- stiffness: 100,
122
- overshootClamping: false,
123
- energyThreshold: 1e-10,
124
- });
127
+ translateY.value = withSpring(0, SETTLE_SPRING);
125
128
  }
126
129
  },
127
130
  }),
@@ -151,7 +154,12 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
151
154
  leftOptions,
152
155
  action,
153
156
  });
154
- const { onDismiss, onRequestClose, onPressBack, onPressClose } = handlers;
157
+ const {
158
+ handleDismiss,
159
+ handleRequestClose,
160
+ handlePressBack,
161
+ handlePressClose,
162
+ } = handlers;
155
163
 
156
164
  const showBack = !!leftOptions?.onPressIconLeft;
157
165
  const iconBack = leftOptions?.iconLeft ?? 'ic_back';
@@ -161,8 +169,8 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
161
169
  useDivider,
162
170
  theme,
163
171
  showBack,
164
- onPressBack,
165
- onPressClose,
172
+ onPressBack: handlePressBack,
173
+ onPressClose: handlePressClose,
166
174
  iconBack,
167
175
  options,
168
176
  leftOptions,
@@ -178,7 +186,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
178
186
  <Screen
179
187
  {...props}
180
188
  {...props.route.params}
181
- onRequestClose={onRequestClose}
189
+ onRequestClose={handleRequestClose}
182
190
  />
183
191
  );
184
192
 
@@ -205,7 +213,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
205
213
  transparent
206
214
  visible={true}
207
215
  onRequestClose={() => {
208
- onDismiss();
216
+ handleDismiss();
209
217
  }}
210
218
  style={StyleSheet.absoluteFillObject}
211
219
  isModalKit={true}
@@ -225,7 +233,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
225
233
  style={StyleSheet.absoluteFillObject}
226
234
  onPress={() => {
227
235
  action.current = 'touch';
228
- onDismiss();
236
+ handleDismiss();
229
237
  }}
230
238
  >
231
239
  <Animated.View style={[styles.overlay, animatedOverlayStyle]} />
@@ -5,6 +5,7 @@ import Animated, {
5
5
  useSharedValue,
6
6
  withTiming,
7
7
  } from 'react-native-reanimated';
8
+ import { defaultTheme } from '../../Consts';
8
9
  import { ApplicationContext } from '../../Context';
9
10
  import { getIndicatorShadow, TabBarMotion } from './metrics';
10
11
 
@@ -70,7 +71,10 @@ const ActiveIndicator: React.FC<Props> = ({
70
71
  styles.indicator,
71
72
  {
72
73
  borderRadius: radius,
73
- backgroundColor: theme.colors.background.tonal,
74
+ // a mini app's own theme may lack `tonal`; without the fallback the indicator is transparent
75
+ backgroundColor:
76
+ theme.colors.background.tonal ??
77
+ defaultTheme.colors.background.tonal,
74
78
  boxShadow: getIndicatorShadow(theme.colors.primary),
75
79
  },
76
80
  style,
@@ -2,6 +2,20 @@ import { StyleSheet } from 'react-native';
2
2
  import { Colors, Spacing } from '../../Consts';
3
3
  import { CTA_SHADOW, TabBarMetrics } from './metrics';
4
4
 
5
+ const ITEM_HEIGHT =
6
+ TabBarMetrics.indicatorHeight +
7
+ TabBarMetrics.itemIconLabelGap +
8
+ TabBarMetrics.labelHeight;
9
+ const CONTENT_HEIGHT = TabBarMetrics.height - 2 * TabBarMetrics.paddingVertical;
10
+ // the items are centered in the content, so their label starts this far below the content top
11
+ const ITEM_LABEL_TOP =
12
+ (CONTENT_HEIGHT - ITEM_HEIGHT) / 2 +
13
+ TabBarMetrics.indicatorHeight +
14
+ TabBarMetrics.itemIconLabelGap;
15
+ // the center action column is placed so its label starts on the same line as the tab labels
16
+ const CTA_TOP =
17
+ ITEM_LABEL_TOP - TabBarMetrics.ctaLabelGap - TabBarMetrics.ctaSize;
18
+
5
19
  export const styles = StyleSheet.create({
6
20
  bar: {
7
21
  position: 'absolute',
@@ -37,7 +51,7 @@ export const styles = StyleSheet.create({
37
51
  position: 'absolute',
38
52
  left: 0,
39
53
  right: 0,
40
- top: -TabBarMetrics.ctaLift - TabBarMetrics.paddingVertical,
54
+ top: CTA_TOP,
41
55
  alignItems: 'center',
42
56
  gap: TabBarMetrics.ctaLabelGap,
43
57
  },
@@ -46,6 +60,9 @@ export const styles = StyleSheet.create({
46
60
  width: TabBarMetrics.ctaSize,
47
61
  height: TabBarMetrics.ctaSize,
48
62
  borderRadius: TabBarMetrics.ctaSize / 2,
63
+ // Figma Icon Button border: white 60% hairline
64
+ borderWidth: 1,
65
+ borderColor: `rgba(255, 255, 255, ${TabBarMetrics.ctaBorderOpacity})`,
49
66
  alignItems: 'center',
50
67
  justifyContent: 'center',
51
68
  },
@@ -54,16 +54,16 @@ const useCenterActionPress = () => {
54
54
  const buttonStyle = useAnimatedStyle(() => ({
55
55
  transform: [{ scale: scale.value }],
56
56
  }));
57
- const onPressIn = () => {
57
+ const handlePressIn = () => {
58
58
  scale.value = withTiming(
59
59
  TabBarMotion.ctaPressedScale,
60
60
  TabBarMotion.ctaPress,
61
61
  );
62
62
  };
63
- const onPressOut = () => {
63
+ const handlePressOut = () => {
64
64
  scale.value = withTiming(1, TabBarMotion.ctaPress);
65
65
  };
66
- return { buttonStyle, onPressIn, onPressOut };
66
+ return { buttonStyle, handlePressIn, handlePressOut };
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, onPressIn, onPressOut } = useCenterActionPress();
80
+ const { buttonStyle, handlePressIn, handlePressOut } = 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={onPressIn}
99
- onPressOut={onPressOut}
98
+ onPressIn={handlePressIn}
99
+ onPressOut={handlePressOut}
100
100
  style={styles.cta}
101
101
  >
102
102
  <Animated.View
@@ -181,21 +181,17 @@ const HorizontalTabBar: React.FC<{
181
181
  return renderItems(items);
182
182
  };
183
183
 
184
- const customContainer = withCta && floatingButton?.container;
185
-
186
184
  return (
187
185
  <View
188
186
  style={[styles.container, { width }]}
189
187
  accessibilityLabel={'bottom_tab_bar'}
190
188
  >
191
- {customContainer || (
192
- <CurvedContainer
193
- width={width}
194
- height={TabBarMetrics.height}
195
- radius={TabBarMetrics.height / 2}
196
- notchCenterX={withCta ? width / 2 : undefined}
197
- />
198
- )}
189
+ <CurvedContainer
190
+ width={width}
191
+ height={TabBarMetrics.height}
192
+ radius={TabBarMetrics.height / 2}
193
+ notchCenterX={withCta ? width / 2 : undefined}
194
+ />
199
195
  <View style={styles.content}>
200
196
  <ActiveIndicator
201
197
  axis={'x'}
@@ -228,6 +224,8 @@ export default function BottomTabBar(props: Props) {
228
224
  window.height,
229
225
  insets.left,
230
226
  insets.right,
227
+ state.routes.length,
228
+ withCta,
231
229
  );
232
230
 
233
231
  return (
@@ -3,7 +3,7 @@ import { StyleSheet } from 'react-native';
3
3
  import Svg, { Defs, FeGaussianBlur, Filter, Path } from 'react-native-svg';
4
4
  import { Colors } from '../../Consts';
5
5
  import { ApplicationContext } from '../../Context';
6
- import { NOTCH_CURVES, NOTCH_HALF_WIDTH } from './metrics';
6
+ import { NOTCH_CURVES, NOTCH_HALF_WIDTH, TabBarMetrics } from './metrics';
7
7
 
8
8
  type Props = {
9
9
  width: number;
@@ -139,7 +139,13 @@ const CurvedContainer: React.FC<Props> = ({
139
139
  filter={'url(#keyShadow)'}
140
140
  />
141
141
  )}
142
- <Path d={path} fill={theme.colors.background.surface} />
142
+ <Path
143
+ d={path}
144
+ fill={theme.colors.background.surface}
145
+ fillOpacity={TabBarMetrics.backgroundOpacity}
146
+ stroke={theme.colors.border.default}
147
+ strokeWidth={1}
148
+ />
143
149
  </Svg>
144
150
  );
145
151
  };
@@ -13,7 +13,7 @@ import Animated, {
13
13
  import { TabBarIcon } from './TabBarIcon';
14
14
  import { Text } from '../../Text';
15
15
  import { MiniAppContext } from '../../Context';
16
- import { Colors, Spacing } from '../../Consts';
16
+ import { Colors } from '../../Consts';
17
17
  import { TabBarMetrics, TabBarMotion } from './metrics';
18
18
 
19
19
  type IconRenderer = (props: {
@@ -143,7 +143,7 @@ const styles = StyleSheet.create({
143
143
  minWidth: 1,
144
144
  alignItems: 'center',
145
145
  justifyContent: 'center',
146
- gap: Spacing.XXS,
146
+ gap: TabBarMetrics.itemIconLabelGap,
147
147
  },
148
148
  slot: {
149
149
  height: TabBarMetrics.indicatorHeight,
@@ -94,14 +94,14 @@ const RailCenterAction: React.FC<{ floatingButton: FloatingButtonProps }> = ({
94
94
  floatingButton,
95
95
  }) => {
96
96
  const { theme } = useContext(ApplicationContext);
97
- const { buttonStyle, onPressIn, onPressOut } = useCenterActionPress();
97
+ const { buttonStyle, handlePressIn, handlePressOut } = useCenterActionPress();
98
98
  return (
99
99
  <Pressable
100
100
  accessibilityRole={'button'}
101
101
  accessibilityLabel={floatingButton.label}
102
102
  onPress={floatingButton.onPress}
103
- onPressIn={onPressIn}
104
- onPressOut={onPressOut}
103
+ onPressIn={handlePressIn}
104
+ onPressOut={handlePressOut}
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.onPressIn}
194
- onPressOut={compactLift.onPressOut}
193
+ onPressIn={compactLift.handlePressIn}
194
+ onPressOut={compactLift.handlePressOut}
195
195
  onPress={expandTabBar}
196
196
  style={styles.railCompact}
197
197
  >
@@ -3,7 +3,11 @@ import React from 'react';
3
3
  import { useWindowDimensions, View } from 'react-native';
4
4
  import { useScreenLayout } from '../../Layout/utils';
5
5
  import { FloatingButtonProps } from '../types';
6
- import { getSystemIndicatorHeight, HorizontalTabBar } from './BottomTabBar';
6
+ import {
7
+ getSystemIndicatorHeight,
8
+ hasCenterAction,
9
+ HorizontalTabBar,
10
+ } from './BottomTabBar';
7
11
  import { getHorizontalBarWidth, TabBarMetrics } from './metrics';
8
12
  import { getTabItems } from './tabItems';
9
13
  import { DuoRailTabBar } from './DuoRailTabBar';
@@ -34,6 +38,7 @@ const DuoFloatingTabBar: React.FC<DuoTabBarProps> = props => {
34
38
  const { insets } = useScreenLayout();
35
39
  const { width, height } = useWindowDimensions();
36
40
  const { floatingButton, activeTintColor, inactiveTintColor } = props;
41
+ const tabItems = getTabItems(props);
37
42
 
38
43
  return (
39
44
  <View
@@ -44,8 +49,15 @@ const DuoFloatingTabBar: React.FC<DuoTabBarProps> = props => {
44
49
  ]}
45
50
  >
46
51
  <HorizontalTabBar
47
- items={getTabItems(props)}
48
- width={getHorizontalBarWidth(width, height, insets.left, insets.right)}
52
+ items={tabItems}
53
+ width={getHorizontalBarWidth(
54
+ width,
55
+ height,
56
+ insets.left,
57
+ insets.right,
58
+ tabItems.length,
59
+ hasCenterAction(floatingButton, tabItems.length),
60
+ )}
49
61
  floatingButton={floatingButton}
50
62
  activeTintColor={activeTintColor}
51
63
  inactiveTintColor={inactiveTintColor}
@@ -15,6 +15,8 @@ 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
+
18
20
  const Stack = createStackNavigator();
19
21
 
20
22
  /**
@@ -29,16 +31,15 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
29
31
 
30
32
  const screenName = screen?.name || screen?.type?.name || 'Invalid';
31
33
  const { isBackgroundToForeground } = useAppState();
32
- // Pad clear of the floating bar unless a Screen inside claims the inset to scroll under it.
33
34
  const { inset: tabBarInset } = useContext(BottomTabInsetContext);
34
- const [insetClaimed, setInsetClaimed] = useState(false);
35
+ const [isInsetClaimed, setIsInsetClaimed] = useState(false);
35
36
  const tabInsetValue = useMemo(
36
- () => ({ inset: tabBarInset, claim: () => setInsetClaimed(true) }),
37
+ () => ({ inset: tabBarInset, claim: () => setIsInsetClaimed(true) }),
37
38
  [tabBarInset],
38
39
  );
39
- const tabInsetStyle = { paddingBottom: insetClaimed ? 0 : tabBarInset };
40
+ const tabInsetStyle = { paddingBottom: isInsetClaimed ? 0 : tabBarInset };
40
41
 
41
- const onScreenNavigated = (pre: string, current: string) => {
42
+ const handleScreenNavigated = (pre: string, current: string) => {
42
43
  if (!isBackgroundToForeground) {
43
44
  const data: any = {
44
45
  preScreenName: pre,
@@ -68,13 +69,13 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
68
69
  navigator?.maxApi?.getDataObserver?.(
69
70
  'current_screen',
70
71
  (data: any) => {
71
- onScreenNavigated(data?.screenName, screenName);
72
+ handleScreenNavigated(data?.screenName, screenName);
72
73
  navigator?.maxApi?.setObserver?.('current_screen', {
73
74
  screenName,
74
75
  });
75
76
  },
76
77
  );
77
- }, 100);
78
+ }, NAVIGATED_REPORT_DELAY);
78
79
  },
79
80
  // eslint-disable-next-line react-hooks/exhaustive-deps
80
81
  [],
@@ -83,12 +84,12 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
83
84
 
84
85
  useEffect(
85
86
  () => {
86
- const onFocusApp = navigator?.maxApi?.listen?.('onFocusApp', () => {
87
+ const focusAppListener = navigator?.maxApi?.listen?.('onFocusApp', () => {
87
88
  if (navigation.isFocused()) {
88
89
  navigator?.maxApi?.getDataObserver?.(
89
90
  'current_screen',
90
91
  (data: any) => {
91
- onScreenNavigated(data?.screenName, screenName);
92
+ handleScreenNavigated(data?.screenName, screenName);
92
93
  navigator?.maxApi?.setObserver?.('current_screen', {
93
94
  screenName,
94
95
  });
@@ -98,7 +99,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
98
99
  });
99
100
 
100
101
  return () => {
101
- onFocusApp?.remove?.();
102
+ focusAppListener?.remove?.();
102
103
  };
103
104
  },
104
105
  // 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 onFocus = (e: any) => {
102
+ const handleFocus = (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 gestureEnabled = Platform.OS === 'ios' && isFirstTab;
106
+ const isGestureEnabled = Platform.OS === 'ios' && isFirstTab;
107
107
  navigation?.setOptions({
108
- gestureEnabled: gestureEnabled,
108
+ gestureEnabled: isGestureEnabled,
109
109
  });
110
110
  NativeModules?.MiniAppModule?.setShouldUseFloatingArrowBack?.(
111
- !gestureEnabled,
111
+ !isGestureEnabled,
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
- onFocus(e);
126
+ handleFocus(e);
127
127
  StatusBar.setBarStyle('dark-content', true);
128
128
  listeners?.focus?.(e);
129
129
  },
@@ -8,11 +8,18 @@ import { Colors, Radius, Spacing } from '../../Consts';
8
8
  * iPhone Duo rail (center action under the container).
9
9
  */
10
10
  const TabBarMetrics = {
11
- height: 68,
11
+ height: 60,
12
12
  margin: Spacing.S,
13
- paddingVertical: Spacing.S,
13
+ paddingVertical: Spacing.XS,
14
14
  paddingHorizontal: Spacing.M,
15
- itemGap: Spacing.XS,
15
+ itemGap: 1,
16
+ // gap between the icon slot and the label inside a tab (Figma: none)
17
+ itemIconLabelGap: 0,
18
+ // .Nav/Background: no backdrop blur, so use the Figma Blur=Off spec (opacity 95%, no blur) plus the 1px
19
+ // border — nearly solid, matching the frosted sample; surface keeps dark-mode support
20
+ backgroundOpacity: 0.95,
21
+ // white 60% hairline around the center action button (Figma Icon Button border)
22
+ ctaBorderOpacity: 0.6,
16
23
  iconSize: 24,
17
24
  // active indicator behind the icon: 24 icon + XS·M padding
18
25
  indicatorPaddingVertical: Spacing.XS,
@@ -25,6 +32,9 @@ const TabBarMetrics = {
25
32
  systemIndicatorHeight: 24,
26
33
  foldableMinShortSide: 600,
27
34
  foldableWidthRatio: 0.65,
35
+ // a 2-tab bar doesn't stretch to the full width: each item is this fixed width and the bar is
36
+ // sized to its content, centered (Figma Navigation · 2 Tab: item 100, bar ~225)
37
+ twoTabItemWidth: 100,
28
38
  ctaSlotWidth: 92,
29
39
  ctaSize: 48,
30
40
  ctaIconSize: 28,
@@ -132,7 +142,17 @@ const getHorizontalBarWidth = (
132
142
  height: number,
133
143
  insetLeft: number,
134
144
  insetRight: number,
145
+ itemCount: number,
146
+ hasCta: boolean,
135
147
  ) => {
148
+ // 2 tabs (no center action) stay content-sized and centered instead of filling the window
149
+ if (itemCount === 2 && !hasCta) {
150
+ return (
151
+ TabBarMetrics.twoTabItemWidth * 2 +
152
+ TabBarMetrics.paddingHorizontal * 2 +
153
+ TabBarMetrics.itemGap
154
+ );
155
+ }
136
156
  const shortSide = Math.min(width, height);
137
157
  if (shortSide >= TabBarMetrics.foldableMinShortSide) {
138
158
  return shortSide * TabBarMetrics.foldableWidthRatio;
@@ -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 onPressIn = () => {
21
+ const handlePressIn = () => {
22
22
  if (!reduceMotion) {
23
23
  lift.value = withSpring(1, SPRING);
24
24
  }
25
25
  };
26
- const onPressOut = () => {
26
+ const handlePressOut = () => {
27
27
  lift.value = withSpring(0, SPRING);
28
28
  };
29
- return { style, onPressIn, onPressOut };
29
+ return { style, handlePressIn, handlePressOut };
30
30
  };
31
31
 
32
32
  export { usePressLift };