@momo-kits/foundation 0.168.1-rn862.1 → 0.169.2

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 (109) hide show
  1. package/Application/BottomSheet.hooks.ts +195 -0
  2. package/Application/BottomSheet.parts.tsx +147 -0
  3. package/Application/BottomSheet.styles.ts +43 -0
  4. package/Application/BottomSheet.tsx +76 -279
  5. package/Application/BottomTab/ActiveIndicator.tsx +90 -0
  6. package/Application/BottomTab/BottomTabBar.styles.ts +65 -0
  7. package/Application/BottomTab/BottomTabBar.tsx +229 -276
  8. package/Application/BottomTab/BottomTabInset.tsx +14 -0
  9. package/Application/BottomTab/CurvedContainer.tsx +153 -0
  10. package/Application/BottomTab/CustomBottomTabItem.tsx +132 -108
  11. package/Application/BottomTab/DuoBarCompression.tsx +4 -12
  12. package/Application/BottomTab/DuoRailTabBar.tsx +227 -0
  13. package/Application/BottomTab/DuoTabBar.styles.ts +81 -0
  14. package/Application/BottomTab/DuoTabBar.tsx +29 -495
  15. package/Application/BottomTab/TabBarIcon.tsx +160 -72
  16. package/Application/BottomTab/TabScreen.tsx +164 -0
  17. package/Application/BottomTab/index.tsx +84 -351
  18. package/Application/BottomTab/metrics.ts +164 -0
  19. package/Application/BottomTab/tabItems.ts +46 -0
  20. package/Application/BottomTab/usePressLift.ts +32 -0
  21. package/Application/Components/DuoGlassBackground.tsx +7 -9
  22. package/Application/Components/HeaderAnimated.tsx +1 -1
  23. package/Application/Components/HeaderBackground.tsx +1 -1
  24. package/Application/Components/HeaderExtendHeader.tsx +17 -11
  25. package/Application/Components/HeaderRight.styles.ts +144 -0
  26. package/Application/Components/HeaderRight.tsx +6 -528
  27. package/Application/Components/HeaderTitle.shared.ts +43 -0
  28. package/Application/Components/HeaderTitle.styles.ts +125 -0
  29. package/Application/Components/HeaderTitle.tsx +12 -576
  30. package/Application/Components/HeaderToolkitAction.tsx +208 -0
  31. package/Application/Components/NavigationButton.tsx +9 -6
  32. package/Application/Components/SearchHeader.tsx +1 -1
  33. package/Application/Components/SwipeBackHaptic.tsx +95 -0
  34. package/Application/Components/TitleJourney.tsx +110 -0
  35. package/Application/Components/TitleLocation.tsx +73 -0
  36. package/Application/Components/TitleUser.tsx +304 -0
  37. package/Application/Components/useToolkitActions.tsx +233 -0
  38. package/Application/ModalIconButton.tsx +66 -0
  39. package/Application/ModalScreen.tsx +39 -18
  40. package/Application/Navigation.ts +1 -1
  41. package/Application/NavigationContainer.tsx +7 -8
  42. package/Application/StackScreen.tracking.ts +158 -0
  43. package/Application/StackScreen.tsx +10 -137
  44. package/Application/WidgetContainer.tsx +10 -3
  45. package/Application/__tests__/modalLayout.test.ts +128 -0
  46. package/Application/hooks.ts +137 -0
  47. package/Application/index.ts +13 -1
  48. package/Application/modalLayout.ts +177 -0
  49. package/Application/navigationOptions.tsx +217 -0
  50. package/Application/types.header.ts +116 -0
  51. package/Application/types.tabs.ts +48 -0
  52. package/Application/types.theme.ts +87 -0
  53. package/Application/types.ts +12 -244
  54. package/Application/utils.ts +107 -0
  55. package/Badge/BadgeRibbon.tsx +3 -3
  56. package/Badge/styles.ts +2 -0
  57. package/Button/index.tsx +16 -136
  58. package/Button/styles.ts +1 -1
  59. package/Button/utils.ts +139 -0
  60. package/CheckBox/index.tsx +1 -6
  61. package/Divider/DashDivider.tsx +10 -11
  62. package/Divider/index.tsx +18 -8
  63. package/Image/index.tsx +1 -46
  64. package/Input/Input.tsx +7 -8
  65. package/Input/InputDropDown.tsx +3 -4
  66. package/Input/InputMoney.tsx +38 -164
  67. package/Input/InputMoneyView.tsx +187 -0
  68. package/Input/InputOTP.tsx +1 -1
  69. package/Input/InputPhoneNumber.tsx +5 -1
  70. package/Input/InputSearch.tsx +2 -108
  71. package/Input/InputTextArea.tsx +1 -5
  72. package/Input/SearchTextTyping.tsx +104 -0
  73. package/Input/common.tsx +4 -4
  74. package/Input/moneyUtils.ts +17 -0
  75. package/Input/styles.ts +26 -2
  76. package/Layout/FloatingButton.tsx +22 -21
  77. package/Layout/ReservedRegions.tsx +21 -4
  78. package/Layout/Screen.tsx +74 -670
  79. package/Layout/Section.tsx +1 -2
  80. package/Layout/index.ts +10 -4
  81. package/Layout/layoutMetrics.ts +58 -0
  82. package/Layout/layoutTypes.ts +86 -0
  83. package/Layout/screenContent.tsx +90 -0
  84. package/Layout/screenFooter.tsx +53 -0
  85. package/Layout/screenHeader.tsx +315 -0
  86. package/Layout/screenQuickReturn.tsx +67 -0
  87. package/Layout/screenScroll.ts +110 -0
  88. package/Layout/screenTypes.ts +134 -0
  89. package/Layout/styles.ts +2 -0
  90. package/Layout/utils.ts +10 -144
  91. package/Loader/DotLoader.tsx +7 -3
  92. package/Loader/ProgressBar.tsx +2 -6
  93. package/Loader/Spinner.tsx +7 -3
  94. package/Loader/styles.ts +4 -0
  95. package/Popup/PopupNotify.tsx +70 -64
  96. package/Popup/PopupPromotion.tsx +5 -4
  97. package/Popup/types.ts +7 -0
  98. package/Popup/utils.ts +46 -24
  99. package/Radio/index.tsx +2 -10
  100. package/Radio/styles.ts +6 -0
  101. package/Skeleton/index.tsx +1 -9
  102. package/Skeleton/styles.ts +1 -0
  103. package/Tag/index.tsx +3 -3
  104. package/Text/index.tsx +11 -2
  105. package/Title/index.tsx +2 -9
  106. package/Title/styles.ts +6 -0
  107. package/package.json +21 -22
  108. package/Application/device.ts +0 -29
  109. package/Application/utils.tsx +0 -497
@@ -1,199 +1,59 @@
1
- import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
2
1
  import {
3
- NavigationContainer,
4
- NavigationIndependentTree,
5
- useFocusEffect,
6
- } from '@react-navigation/native';
7
- import { createStackNavigator } from '@react-navigation/stack';
8
- import React, { useContext, useEffect, useLayoutEffect } from 'react';
9
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
2
+ BottomTabNavigationOptions,
3
+ createBottomTabNavigator,
4
+ } from '@react-navigation/bottom-tabs';
5
+ import { useFocusEffect } from '@react-navigation/native';
6
+ import React, { useContext, useLayoutEffect } from 'react';
10
7
  import {
8
+ Easing,
11
9
  NativeModules,
12
10
  Platform,
13
11
  StatusBar,
14
- StyleSheet,
15
12
  useWindowDimensions,
16
- View,
17
13
  } from 'react-native';
18
- import Animated, {
19
- useAnimatedStyle,
20
- useSharedValue,
21
- withTiming,
22
- } from 'react-native-reanimated';
23
- import { Colors } from '../../Consts';
24
14
  import { Icon } from '../../Icon';
25
- import { exportFontFamily, Text } from '../../Text';
26
- import StackScreen from '../StackScreen';
27
15
  import { BottomTabProps } from '../types';
28
- import { getOptions, getStackOptions, useAppState } from '../utils';
29
- import BottomTabBar from './BottomTabBar';
30
- import { DuoTabBar } from './DuoTabBar';
16
+ import BottomTabBar, { hasCenterAction } from './BottomTabBar';
17
+ import { TabBarMetrics } from './metrics';
18
+ import { getTabBadge } from './TabBarIcon';
19
+ import { DuoTabBar, useDuoTabBarInset } from './DuoTabBar';
20
+ import { BottomTabInsetContext } from './BottomTabInset';
31
21
  import { DuoBarCompressionProvider } from './DuoBarCompression';
32
- import { ApplicationContext, MiniAppContext } from '../../Context';
22
+ import TabScreen from './TabScreen';
23
+ import { ApplicationContext } from '../../Context';
33
24
  import { useScreenLayout } from '../../Layout';
34
- import { getHeaderRowOptions } from '../../Layout/utils';
35
25
 
36
26
  const Tab = createBottomTabNavigator();
37
- const Stack = createStackNavigator();
38
27
 
39
- const TabScreen: React.FC<any> = ({ route, navigation }) => {
40
- const { navigator } = useContext(ApplicationContext);
41
- const context = useContext<any>(MiniAppContext);
42
- const { nested, options, screen, initialParams } = route?.params;
43
- const headerStyleOptions = getHeaderRowOptions(useScreenLayout());
44
-
45
- const opacityValue = 0.3;
46
- const scaleValue = 0.97;
47
- const opacity = useSharedValue(opacityValue);
48
- const scale = useSharedValue(scaleValue);
49
- const screenName = screen?.name || screen?.type?.name || 'Invalid';
50
- const { isBackgroundToForeground } = useAppState();
51
-
52
- const tabAnimatedStyle = useAnimatedStyle(() => ({
53
- opacity: opacity.value,
54
- transform: [{ scale: scale.value }],
55
- }));
56
-
57
- const onScreenNavigated = (pre: string, current: string) => {
58
- if (!isBackgroundToForeground) {
59
- const data: any = {
60
- preScreenName: pre,
61
- screenName: current,
62
- componentName: 'Screen',
63
- state: 'navigated',
64
- action: 'push',
65
- };
66
-
67
- context?.autoTracking?.({
68
- ...context,
69
- ...data,
70
- });
71
-
72
- /**
73
- * debug toast
74
- */
75
- navigator?.maxApi?.showToastDebug?.({
76
- appId: context.appId,
77
- message: `${screenName} screen_navigated`,
78
- type: 'ERROR',
79
- });
80
- }
81
- };
82
-
83
- useFocusEffect(
84
- React.useCallback(
85
- () => {
86
- opacity.value = withTiming(1, { duration: 200 });
87
- scale.value = withTiming(1, { duration: 200 });
88
-
89
- setTimeout(() => {
90
- navigator?.maxApi?.getDataObserver?.(
91
- 'current_screen',
92
- (data: any) => {
93
- onScreenNavigated(data?.screenName, screenName);
94
- navigator?.maxApi?.setObserver?.('current_screen', {
95
- screenName,
96
- });
97
- },
98
- );
99
- }, 100);
100
-
101
- return () => {
102
- if (navigation.getState().index !== route?.params.index) {
103
- opacity.value = withTiming(opacityValue, { duration: 200 });
104
- scale.value = withTiming(scaleValue, { duration: 200 });
105
- }
106
- };
107
- },
108
- // eslint-disable-next-line react-hooks/exhaustive-deps
109
- [],
110
- ),
111
- );
112
-
113
- useEffect(
114
- () => {
115
- const onFocusApp = navigator?.maxApi?.listen?.('onFocusApp', () => {
116
- if (navigation.isFocused()) {
117
- navigator?.maxApi?.getDataObserver?.(
118
- 'current_screen',
119
- (data: any) => {
120
- onScreenNavigated(data?.screenName, screenName);
121
- navigator?.maxApi?.setObserver?.('current_screen', {
122
- screenName,
123
- });
124
- },
125
- );
126
- }
127
- });
128
-
129
- return () => {
130
- onFocusApp?.remove?.();
131
- };
132
- },
133
- // eslint-disable-next-line react-hooks/exhaustive-deps
134
- [],
135
- );
136
-
137
- let stackOptions = {};
138
- if (options) {
139
- stackOptions = getOptions(options);
140
- }
141
-
142
- if (nested) {
143
- return (
144
- <Animated.View style={[styles.container, tabAnimatedStyle]}>
145
- <Stack.Navigator
146
- screenOptions={headerStyleOptions}
147
- >
148
- <Stack.Screen
149
- name="Stack"
150
- component={StackScreen}
151
- initialParams={{
152
- screen,
153
- options: stackOptions,
154
- initialParams,
155
- bottomTab: 'nested',
156
- }}
157
- options={{ ...getStackOptions() }}
158
- />
159
- </Stack.Navigator>
160
- </Animated.View>
161
- );
162
- }
28
+ // the tab screens slide in index order over 280ms, CSS `ease`
29
+ const TAB_SWITCH_SPEC = {
30
+ animation: 'timing' as const,
31
+ config: { duration: 280, easing: Easing.bezier(0.25, 0.1, 0.25, 1) },
32
+ };
163
33
 
164
- return (
165
- <Animated.View
166
- style={[
167
- styles.container,
34
+ /**
35
+ * scene progress is -1 for tabs before the active one, 1 for tabs after it: they wait a full
36
+ * width off screen on their side
37
+ */
38
+ const getTabSlide =
39
+ (width: number): BottomTabNavigationOptions['sceneStyleInterpolator'] =>
40
+ ({ current }) => ({
41
+ sceneStyle: {
42
+ transform: [
168
43
  {
169
- opacity,
170
- transform: [{ scale }],
44
+ translateX: current.progress.interpolate({
45
+ inputRange: [-1, 0, 1],
46
+ outputRange: [-width, 0, width],
47
+ }),
171
48
  },
172
- ]}
173
- >
174
- <NavigationIndependentTree>
175
- <NavigationContainer>
176
- <Stack.Navigator
177
- screenOptions={headerStyleOptions}
178
- >
179
- <Stack.Screen
180
- name="Stack"
181
- component={StackScreen}
182
- initialParams={{
183
- screen,
184
- options: stackOptions,
185
- initialParams,
186
- bottomTab: 'default',
187
- }}
188
- options={{ ...getStackOptions(), ...options }}
189
- />
190
- </Stack.Navigator>
191
- </NavigationContainer>
192
- </NavigationIndependentTree>
193
- </Animated.View>
194
- );
195
- };
49
+ ],
50
+ },
51
+ });
196
52
 
53
+ /**
54
+ * App tab navigator: the floating bar on phones, the rail or floating bar on
55
+ * iPhone Duo, tabs sliding in index order. Shares the bar inset with its tabs.
56
+ */
197
57
  const BottomTab: React.FC<BottomTabProps> = ({
198
58
  nested,
199
59
  tabs,
@@ -205,17 +65,15 @@ const BottomTab: React.FC<BottomTabProps> = ({
205
65
  }) => {
206
66
  const { theme, navigator } = useContext(ApplicationContext);
207
67
  const { isIPhoneDuo } = useScreenLayout();
208
- const dimensions = useWindowDimensions();
209
- const insets = useSafeAreaInsets();
68
+ const { width: windowWidth } = useWindowDimensions();
69
+ const barInset = useDuoTabBarInset();
70
+ // the center action's button rises above the bar, tab content leaves room for it too
71
+ const tabBarInset =
72
+ barInset && hasCenterAction(floatingButton, tabs.length)
73
+ ? barInset + TabBarMetrics.ctaLift
74
+ : barInset;
210
75
  const initialIndex = tabs.findIndex(i => i.name === initialRouteName);
211
- const indicatorAnimated = useSharedValue(0);
212
76
  const activeIndex = React.useRef(initialIndex > 0 ? initialIndex : 0);
213
- const itemWidth = dimensions.width / tabs.length;
214
-
215
- const indicatorStyle = useAnimatedStyle(() => ({
216
- width: itemWidth,
217
- transform: [{ translateX: indicatorAnimated.value }],
218
- }));
219
77
 
220
78
  useLayoutEffect(() => {
221
79
  navigation?.setOptions({
@@ -225,12 +83,6 @@ const BottomTab: React.FC<BottomTabProps> = ({
225
83
  });
226
84
  }, [navigation]);
227
85
 
228
- useEffect(() => {
229
- indicatorAnimated.value = withTiming(itemWidth * activeIndex.current, {
230
- duration: 200,
231
- });
232
- }, [itemWidth, indicatorAnimated]);
233
-
234
86
  // Focus, not mount: pushing a screen from a tab leaves BottomTab mounted but blurred, and
235
87
  // onFocus only fires on tab changes.
236
88
  useFocusEffect(
@@ -245,16 +97,15 @@ const BottomTab: React.FC<BottomTabProps> = ({
245
97
 
246
98
  const onFocus = (e: any) => {
247
99
  activeIndex.current = tabs.findIndex(i => e.target.includes(i.name));
248
- indicatorAnimated.value = withTiming(itemWidth * activeIndex.current, {
249
- duration: 200,
250
- });
251
100
 
252
101
  const isFirstTab = activeIndex.current === 0;
253
102
  const gestureEnabled = Platform.OS === 'ios' && isFirstTab;
254
103
  navigation?.setOptions({
255
104
  gestureEnabled: gestureEnabled,
256
105
  });
257
- NativeModules?.MiniAppModule?.setShouldUseFloatingArrowBack?.(!gestureEnabled);
106
+ NativeModules?.MiniAppModule?.setShouldUseFloatingArrowBack?.(
107
+ !gestureEnabled,
108
+ );
258
109
  };
259
110
 
260
111
  const handler: {
@@ -277,102 +128,43 @@ const BottomTab: React.FC<BottomTabProps> = ({
277
128
  },
278
129
  };
279
130
 
280
- const isIOS = Platform.OS === 'ios';
281
- const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
282
-
283
131
  return (
284
132
  <DuoBarCompressionProvider
285
133
  behavior={compressionBehavior}
286
134
  tabCount={tabs.length}
287
135
  hasQr={!!floatingButton}
288
136
  >
289
- <Tab.Navigator
290
- screenOptions={{
291
- headerShown: false,
292
- headerTransparent: true,
293
- headerBackground: undefined,
294
- tabBarLabelPosition: 'below-icon',
295
- tabBarBackground: () => (
296
- <View
297
- style={[
298
- styles.container,
299
- {
300
- backgroundColor: theme.colors.background.surface,
301
- },
302
- ]}
303
- >
304
- <Animated.View
305
- style={[styles.indicatorContainer, indicatorStyle]}
306
- >
307
- <View
308
- style={[
309
- styles.indicator,
310
- { backgroundColor: theme.colors.primary },
311
- ]}
312
- />
313
- </Animated.View>
314
- </View>
315
- ),
316
- }}
317
- tabBar={props =>
318
- isIPhoneDuo ? (
319
- <DuoTabBar
320
- {...props}
321
- floatingButton={floatingButton}
322
- inactiveTintColor={theme.colors.text.hint}
323
- activeTintColor={theme.colors.primary}
324
- />
325
- ) : (
326
- <BottomTabBar
327
- {...props}
328
- floatingButton={floatingButton}
329
- inactiveTintColor={theme.colors.text.hint}
330
- activeTintColor={theme.colors.primary}
331
- style={[{ height: 64 + bottomInset }]}
332
- />
333
- )
334
- }
335
- initialRouteName={initialRouteName}
336
- >
337
- {tabs.map((item, index) => {
338
- const isNum = !isNaN(parseInt(`${item?.badgeLabel}`, 10));
339
- const isDot =
340
- typeof item?.badgeLabel === 'string' && !item?.badgeLabel;
341
- const isNumZero = isNum && Number(item.badgeLabel) === 0;
342
-
343
- let badgeLabel = item?.badgeLabel;
344
- if (isNumZero) {
345
- badgeLabel = undefined;
137
+ <BottomTabInsetContext.Provider value={{ inset: tabBarInset }}>
138
+ <Tab.Navigator
139
+ screenOptions={{
140
+ headerShown: false,
141
+ headerTransparent: true,
142
+ headerBackground: undefined,
143
+ tabBarLabelPosition: 'below-icon',
144
+ animation: 'shift',
145
+ transitionSpec: TAB_SWITCH_SPEC,
146
+ sceneStyleInterpolator: getTabSlide(windowWidth),
147
+ }}
148
+ tabBar={props =>
149
+ isIPhoneDuo ? (
150
+ <DuoTabBar
151
+ {...props}
152
+ floatingButton={floatingButton}
153
+ inactiveTintColor={theme.colors.text.secondary}
154
+ activeTintColor={theme.colors.primary}
155
+ />
156
+ ) : (
157
+ <BottomTabBar
158
+ {...props}
159
+ floatingButton={floatingButton}
160
+ inactiveTintColor={theme.colors.text.secondary}
161
+ activeTintColor={theme.colors.primary}
162
+ />
163
+ )
346
164
  }
347
-
348
- let badgeStyle: {
349
- marginRight: number;
350
- top: number;
351
- backgroundColor: string;
352
- width?: number;
353
- } = {
354
- marginRight: -8,
355
- top: -4,
356
- backgroundColor: Colors.orange_03,
357
- };
358
-
359
- if (isNum) {
360
- badgeStyle = {
361
- marginRight: -4,
362
- top: -4,
363
- backgroundColor: Colors.red_03,
364
- };
365
- }
366
- if (isDot) {
367
- badgeStyle = {
368
- marginRight: -2,
369
- top: -2,
370
- backgroundColor: Colors.orange_03,
371
- width: 12,
372
- };
373
- }
374
-
375
- return (
165
+ initialRouteName={initialRouteName}
166
+ >
167
+ {tabs.map((item, index) => (
376
168
  <Tab.Screen
377
169
  key={`BottomTab-${item.name}`}
378
170
  name={item.name}
@@ -380,78 +172,19 @@ const BottomTab: React.FC<BottomTabProps> = ({
380
172
  initialParams={{ ...item, nested, index }}
381
173
  listeners={handler}
382
174
  options={{
383
- tabBarLabel: ({ focused, color }) => {
384
- return (
385
- <Text
386
- numberOfLines={1}
387
- typography={
388
- focused ? 'label_xs_medium' : 'description_xs_regular'
389
- }
390
- color={color}
391
- >
392
- {item.label}
393
- </Text>
394
- );
395
- },
396
175
  title: item.label,
397
- tabBarStyle: [
398
- {
399
- height: 64,
400
- },
401
- styles.tabStyle,
402
- ],
403
- tabBarBadge: badgeLabel,
404
- tabBarBadgeStyle: [
405
- {
406
- borderColor: 'white',
407
- borderWidth: 2,
408
- fontSize: 10,
409
- lineHeight: 14,
410
- fontWeight: 'bold',
411
- alignSelf: 'center',
412
- fontFamily: exportFontFamily('bold'),
413
- },
414
- badgeStyle,
415
- ],
416
- tabBarIcon: ({ color }) => (
417
- <Icon source={item.icon} color={color} size={28} />
176
+ tabBarBadge: getTabBadge(item.badgeLabel, item.showDot),
177
+ tabBarIcon: ({ color, size }) => (
178
+ <Icon source={item.icon} color={color} size={size} />
418
179
  ),
419
180
  ...item,
420
181
  }}
421
182
  />
422
- );
423
- })}
424
- </Tab.Navigator>
183
+ ))}
184
+ </Tab.Navigator>
185
+ </BottomTabInsetContext.Provider>
425
186
  </DuoBarCompressionProvider>
426
187
  );
427
188
  };
428
189
 
429
- const styles = StyleSheet.create({
430
- container: { flex: 1 },
431
- indicatorContainer: { position: 'absolute', top: 0 },
432
- indicator: {
433
- height: 2,
434
- width: 48,
435
- alignSelf: 'center',
436
- borderBottomLeftRadius: 2,
437
- borderBottomRightRadius: 2,
438
- },
439
- tabBarStyle: {
440
- ...Platform.select({
441
- ios: {
442
- shadowColor: Colors.black_10,
443
- shadowOffset: { width: 0, height: 4 },
444
- shadowOpacity: 1,
445
- shadowRadius: 4,
446
- },
447
- android: {
448
- elevation: 4,
449
- },
450
- }),
451
- },
452
- tabStyle: {
453
- paddingVertical: 4,
454
- },
455
- });
456
-
457
190
  export default BottomTab;
@@ -0,0 +1,164 @@
1
+ import { BoxShadowValue } from 'react-native';
2
+ import { Easing } from 'react-native-reanimated';
3
+ import { Colors, Radius, Spacing } from '../../Consts';
4
+
5
+ /**
6
+ * Navigation bar measurements (Figma "Specs Navigation", DS-984), kept in sync with
7
+ * momo-native-kits `TabBarMetrics`
8
+ * - horizontal: floating container with items split evenly, center action in a notch
9
+ * - vertical: iPhone Duo rail, center action under the container
10
+ */
11
+ const TabBarMetrics = {
12
+ height: 68,
13
+ margin: Spacing.S,
14
+ paddingVertical: Spacing.S,
15
+ paddingHorizontal: Spacing.M,
16
+ itemGap: Spacing.XS,
17
+ iconSize: 24,
18
+ // active indicator behind the icon: 24 icon + XS·M padding
19
+ indicatorPaddingVertical: Spacing.XS,
20
+ indicatorPaddingHorizontal: Spacing.M,
21
+ indicatorRadius: Radius.XL,
22
+ indicatorHeight: 32,
23
+ // home indicator / gesture bar area under the container
24
+ systemIndicatorHeight: 24,
25
+ // unfolded foldables (short side from 600, the smallest in the spec) use 65% of the short
26
+ // side, centered; phones and folded outer displays fill the window
27
+ foldableMinShortSide: 600,
28
+ foldableWidthRatio: 0.65,
29
+ ctaSlotWidth: 92,
30
+ ctaSize: 48,
31
+ // the icon sits 10 inside the 48 button (prototype .cta: 28 icon), 12 inside the 52 rail one
32
+ ctaIconSize: 28,
33
+ // the button rises 22 above the container top edge
34
+ ctaLift: 22,
35
+ ctaLabelGap: Spacing.M,
36
+ ctaLabelHeight: 18,
37
+ railContainerWidth: 56,
38
+ railPaddingHorizontal: Spacing.XS,
39
+ railPaddingBottom: Spacing.S,
40
+ railItemWidth: 48,
41
+ railItemHeight: 62,
42
+ railIndicatorHeight: 40,
43
+ railIndicatorPaddingVertical: Spacing.S,
44
+ railIndicatorRadius: 20,
45
+ railCtaGap: Spacing.L,
46
+ railCtaSize: 52,
47
+ railCtaLabelGap: Spacing.XS,
48
+ railCtaWidth: 68,
49
+ railCtaHeight: 70,
50
+ badgeMaxWidth: 58,
51
+ };
52
+
53
+ /**
54
+ * motion of the navigation bar (MoMo Navigation Prototype):
55
+ * - one active indicator slides to the selected item, overshooting a little
56
+ * - icons cross-fade and the label color follows
57
+ * - the center action shrinks while pressed
58
+ */
59
+ const TabBarMotion = {
60
+ indicator: { duration: 550, easing: Easing.bezier(0.34, 1.4, 0.5, 1) },
61
+ tint: { duration: 250 },
62
+ ctaPress: { duration: 150 },
63
+ ctaPressedScale: 0.92,
64
+ };
65
+
66
+ /**
67
+ * Figma Effect/Popover on the center action button: 8% y8 blur 24 spread -12, 4% y12 blur 32
68
+ * spread -16
69
+ */
70
+ const CTA_SHADOW: BoxShadowValue[] = [
71
+ {
72
+ offsetX: 0,
73
+ offsetY: 8,
74
+ blurRadius: 24,
75
+ spreadDistance: -12,
76
+ color: `${Colors.black_20}14`,
77
+ },
78
+ {
79
+ offsetX: 0,
80
+ offsetY: 12,
81
+ blurRadius: 32,
82
+ spreadDistance: -16,
83
+ color: `${Colors.black_20}0A`,
84
+ },
85
+ ];
86
+
87
+ /**
88
+ * inner light and tint of the active indicator: white 12% from below, primary 12% from above
89
+ */
90
+ const getIndicatorShadow = (primary: string): BoxShadowValue[] => [
91
+ {
92
+ offsetX: 0,
93
+ offsetY: -2,
94
+ blurRadius: 2,
95
+ color: `${Colors.black_01}1F`,
96
+ inset: true,
97
+ },
98
+ { offsetX: 0, offsetY: 1, blurRadius: 2, color: `${primary}1F`, inset: true },
99
+ ];
100
+
101
+ /**
102
+ * notch dipped into the container top edge under the center action (Figma "Backgroud" path):
103
+ * starts NOTCH_HALF_WIDTH left of the center, then cubic segments as [control1, control2, end],
104
+ * each point [dx from the center, dy below the top edge]
105
+ */
106
+ const NOTCH_HALF_WIDTH = 50.758;
107
+ const NOTCH_CURVES: [number, number][][] = [
108
+ [
109
+ [-39.749, 0],
110
+ [-31.231, 9.765],
111
+ [-24.563, 18.526],
112
+ ],
113
+ [
114
+ [-18.928, 25.93],
115
+ [-10.022, 30.709],
116
+ [0, 30.709],
117
+ ],
118
+ [
119
+ [10.022, 30.709],
120
+ [18.928, 25.93],
121
+ [24.563, 18.526],
122
+ ],
123
+ [
124
+ [31.231, 9.765],
125
+ [39.749, 0],
126
+ [NOTCH_HALF_WIDTH, 0],
127
+ ],
128
+ ];
129
+
130
+ /**
131
+ * width of the horizontal bar: 65% of the short side on an unfolded foldable, otherwise the
132
+ * window minus the side safe areas and margins
133
+ */
134
+ const getHorizontalBarWidth = (
135
+ width: number,
136
+ height: number,
137
+ insetLeft: number,
138
+ insetRight: number,
139
+ ) => {
140
+ const shortSide = Math.min(width, height);
141
+ if (shortSide >= TabBarMetrics.foldableMinShortSide) {
142
+ return shortSide * TabBarMetrics.foldableWidthRatio;
143
+ }
144
+ return width - insetLeft - insetRight - 2 * TabBarMetrics.margin;
145
+ };
146
+
147
+ /**
148
+ * height of the vertical rail container, plus the center action under it
149
+ */
150
+ const getRailHeight = (tabCount: number, hasCta: boolean) =>
151
+ tabCount * TabBarMetrics.railItemHeight +
152
+ TabBarMetrics.railPaddingBottom +
153
+ (hasCta ? TabBarMetrics.railCtaGap + TabBarMetrics.railCtaHeight : 0);
154
+
155
+ export {
156
+ TabBarMetrics,
157
+ TabBarMotion,
158
+ getHorizontalBarWidth,
159
+ getRailHeight,
160
+ getIndicatorShadow,
161
+ CTA_SHADOW,
162
+ NOTCH_CURVES,
163
+ NOTCH_HALF_WIDTH,
164
+ };