@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,113 +1,201 @@
1
- import type { Route } from '@react-navigation/native';
2
- import React from 'react';
3
- import {
4
- type StyleProp,
5
- StyleSheet,
6
- type TextStyle,
7
- View,
8
- type ViewStyle,
9
- } from 'react-native';
1
+ import React, { useContext, useEffect } from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withTiming,
7
+ } from 'react-native-reanimated';
10
8
 
11
- import { Colors } from '../../Consts';
12
- import { Badge, BadgeDot } from '../../Badge';
9
+ import { Colors, Spacing } from '../../Consts';
10
+ import { ApplicationContext, MiniAppContext } from '../../Context';
11
+ import { BadgeDot } from '../../Badge';
12
+ import { Text } from '../../Text';
13
+ import { TabBarMetrics, TabBarMotion } from './metrics';
13
14
 
14
15
  type Props = {
15
- route: Route<string>;
16
+ focused: boolean;
17
+ // '' shows a dot, anything else a label badge
16
18
  badge?: string | number;
17
- badgeStyle?: StyleProp<TextStyle> | any;
18
- activeOpacity: number;
19
- inactiveOpacity: number;
20
- activeTintColor?: string;
21
- inactiveTintColor?: string;
22
- renderIcon: (props: {
19
+ // rail items center the label badge over the icon
20
+ vertical?: boolean;
21
+ activeTintColor: string;
22
+ inactiveTintColor: string;
23
+ renderIcon?: (props: {
23
24
  focused: boolean;
24
25
  color: string;
25
26
  size: number;
26
27
  }) => React.ReactNode;
27
- allowFontScaling?: boolean;
28
- style: StyleProp<ViewStyle>;
29
28
  };
30
29
 
31
30
  /**
32
- * Icon sizes taken from Apple HIG
33
- * https://developer.apple.com/design/human-interface-guidelines/tab-bars
31
+ * badge value shown on a tab: undefined hides it, '' is a dot, numbers cap at 99+
34
32
  */
35
- const ICON_SIZE_WIDE = 35;
33
+ export const getTabBadge = (
34
+ badgeLabel?: string | number,
35
+ showDot?: boolean,
36
+ ) => {
37
+ if (badgeLabel === undefined || badgeLabel === null) {
38
+ return showDot ? '' : undefined;
39
+ }
40
+ const text = `${badgeLabel}`;
41
+ if (text === '') {
42
+ return '';
43
+ }
44
+ if (/^\d+$/.test(text)) {
45
+ const value = Number(text);
46
+ if (value === 0) {
47
+ return showDot ? '' : undefined;
48
+ }
49
+ return value > 99 ? '99+' : text;
50
+ }
51
+ return text;
52
+ };
53
+
54
+ /**
55
+ * text at its design size: like the system tab bar it doesn't grow with the OS font scale or
56
+ * the wide Duo displays, which Text otherwise scales up to 5pt
57
+ */
58
+ export const FixedScaleText: React.FC<{ children: React.ReactNode }> = ({
59
+ children,
60
+ }) => {
61
+ const context = useContext<any>(MiniAppContext);
62
+ return (
63
+ <MiniAppContext.Provider
64
+ value={{
65
+ ...context,
66
+ fontScale: { useOSScaleRate: false, userScaleRate: 1 },
67
+ }}
68
+ >
69
+ {children}
70
+ </MiniAppContext.Provider>
71
+ );
72
+ };
36
73
 
74
+ /**
75
+ * numbers use the error color, text the warning color (Figma Badge round numeric / text)
76
+ */
77
+ const TabBadge: React.FC<{ label: string; vertical?: boolean }> = ({
78
+ label,
79
+ vertical,
80
+ }) => {
81
+ const { theme } = useContext(ApplicationContext);
82
+ const isNumber = /^\d+\+?$/.test(label);
83
+ const badge = (
84
+ <View
85
+ style={[
86
+ styles.badge,
87
+ {
88
+ backgroundColor: isNumber
89
+ ? theme.colors.error.primary
90
+ : theme.colors.warning.primary,
91
+ borderColor: theme.colors.background.surface,
92
+ },
93
+ ]}
94
+ >
95
+ <FixedScaleText>
96
+ <Text
97
+ numberOfLines={1}
98
+ typography={'action_xxs_bold'}
99
+ color={Colors.black_01}
100
+ >
101
+ {label}
102
+ </Text>
103
+ </FixedScaleText>
104
+ </View>
105
+ );
106
+ if (vertical) {
107
+ return <View style={styles.badgeCentered}>{badge}</View>;
108
+ }
109
+ return <View style={styles.badgeLeading}>{badge}</View>;
110
+ };
111
+
112
+ /**
113
+ * 24 icon; active and inactive icons are stacked and cross-faded on focus
114
+ */
37
115
  export function TabBarIcon({
38
- route: _,
116
+ focused,
39
117
  badge,
40
- badgeStyle,
41
- activeOpacity,
42
- inactiveOpacity,
118
+ vertical,
43
119
  activeTintColor,
44
120
  inactiveTintColor,
45
121
  renderIcon,
46
- style,
47
122
  }: Props) {
48
- const iconSize = 25;
123
+ const size = TabBarMetrics.iconSize;
124
+ const progress = useSharedValue(focused ? 1 : 0);
125
+
126
+ useEffect(() => {
127
+ progress.value = withTiming(focused ? 1 : 0, TabBarMotion.tint);
128
+ }, [focused, progress]);
129
+
130
+ const activeStyle = useAnimatedStyle(() => ({ opacity: progress.value }));
131
+ const inactiveStyle = useAnimatedStyle(() => ({
132
+ opacity: 1 - progress.value,
133
+ }));
49
134
 
50
135
  const renderBadge = () => {
51
- if (badge == null) {
52
- return;
136
+ if (badge === undefined || badge === null) {
137
+ return null;
53
138
  }
54
139
  if (badge === '') {
55
- return <BadgeDot size={'small'} style={[styles.badge, badgeStyle]} />;
140
+ return <BadgeDot size={'small'} style={styles.dot} />;
56
141
  }
57
- return <Badge style={[styles.badge, badgeStyle]} label={badge} />;
142
+ return <TabBadge label={`${badge}`} vertical={vertical} />;
58
143
  };
59
144
 
60
- // We render the icon twice at the same position on top of each other:
61
- // active and inactive one, so we can fade between them.
62
145
  return (
63
- <View style={[styles.wrapperUikit, style]}>
64
- <View
65
- style={[
66
- styles.icon,
67
- {
68
- opacity: activeOpacity,
69
- // Workaround for react-native >= 0.54 layout bug
70
- minWidth: iconSize,
71
- },
72
- ]}
73
- >
74
- {renderIcon({
75
- focused: true,
76
- size: iconSize,
77
- color: activeTintColor ?? Colors.black_20,
78
- })}
79
- </View>
80
- <View style={[styles.icon, { opacity: inactiveOpacity }]}>
81
- {renderIcon({
82
- focused: false,
83
- size: iconSize,
84
- color: inactiveTintColor ?? Colors.black_04,
85
- })}
86
- </View>
146
+ <View style={styles.icon}>
147
+ <Animated.View style={[styles.layer, activeStyle]}>
148
+ {renderIcon?.({ focused: true, size, color: activeTintColor })}
149
+ </Animated.View>
150
+ <Animated.View style={[styles.layer, inactiveStyle]}>
151
+ {renderIcon?.({ focused: false, size, color: inactiveTintColor })}
152
+ </Animated.View>
87
153
  {renderBadge()}
88
154
  </View>
89
155
  );
90
156
  }
91
157
 
158
+ const BADGE_HEIGHT = 16;
159
+ const BADGE_BORDER = 1;
160
+
92
161
  const styles = StyleSheet.create({
93
162
  icon: {
94
- // We render the icon twice at the same position on top of each other:
95
- // active and inactive one, so we can fade between them:
96
- // Cover the whole iconContainer:
97
- position: 'absolute',
98
- alignSelf: 'center',
163
+ width: TabBarMetrics.iconSize,
164
+ height: TabBarMetrics.iconSize,
165
+ },
166
+ layer: {
167
+ ...StyleSheet.absoluteFillObject,
99
168
  alignItems: 'center',
100
169
  justifyContent: 'center',
101
- height: '100%',
102
- width: '100%',
103
170
  },
104
- wrapperUikit: {
105
- width: ICON_SIZE_WIDE,
106
- height: ICON_SIZE_WIDE,
171
+ dot: {
172
+ position: 'absolute',
173
+ left: 16,
174
+ top: -Spacing.XXS,
107
175
  },
108
- badge: {
176
+ badgeLeading: {
109
177
  position: 'absolute',
110
- end: -3,
111
- top: -3,
178
+ left: Spacing.M,
179
+ top: -7,
180
+ width: TabBarMetrics.badgeMaxWidth,
181
+ alignItems: 'flex-start',
182
+ },
183
+ badgeCentered: {
184
+ position: 'absolute',
185
+ left: (TabBarMetrics.iconSize - TabBarMetrics.badgeMaxWidth) / 2,
186
+ top: -10,
187
+ width: TabBarMetrics.badgeMaxWidth,
188
+ alignItems: 'center',
189
+ },
190
+ badge: {
191
+ height: BADGE_HEIGHT,
192
+ minWidth: BADGE_HEIGHT,
193
+ maxWidth: TabBarMetrics.badgeMaxWidth,
194
+ // the border is drawn inside the 4 padding, like the Figma inside stroke
195
+ paddingHorizontal: Spacing.XS - BADGE_BORDER,
196
+ borderRadius: BADGE_HEIGHT / 2,
197
+ borderWidth: BADGE_BORDER,
198
+ alignItems: 'center',
199
+ justifyContent: 'center',
112
200
  },
113
201
  });
@@ -0,0 +1,164 @@
1
+ import {
2
+ NavigationContainer,
3
+ NavigationIndependentTree,
4
+ useFocusEffect,
5
+ } from '@react-navigation/native';
6
+ import { createStackNavigator } from '@react-navigation/stack';
7
+ import React, { useContext, useEffect, useMemo, useState } from 'react';
8
+ import { View } from 'react-native';
9
+ import StackScreen from '../StackScreen';
10
+ import { getOptions, getStackOptions } from '../navigationOptions';
11
+ import { useAppState } from '../hooks';
12
+ import { ApplicationContext, MiniAppContext } from '../../Context';
13
+ import { Styles } from '../../Consts';
14
+ import { useScreenLayout } from '../../Layout';
15
+ import { getHeaderRowOptions } from '../../Layout/utils';
16
+ import { BottomTabInsetContext } from './BottomTabInset';
17
+
18
+ const Stack = createStackNavigator();
19
+
20
+ /**
21
+ * Content of one tab: its own stack, padded clear of the floating tab bar
22
+ * until a Screen inside claims that inset, plus screen_navigated tracking.
23
+ */
24
+ const TabScreen: React.FC<any> = ({ route, navigation }) => {
25
+ const { navigator } = useContext(ApplicationContext);
26
+ const context = useContext<any>(MiniAppContext);
27
+ const { nested, options, screen, initialParams } = route?.params;
28
+ const headerStyleOptions = getHeaderRowOptions(useScreenLayout());
29
+
30
+ const screenName = screen?.name || screen?.type?.name || 'Invalid';
31
+ const { isBackgroundToForeground } = useAppState();
32
+ // the floating tab bar covers the bottom of the tab: pad the content unless a Screen in it
33
+ // keeps clear of the bar itself (scroll content running under the bar)
34
+ const { inset: tabBarInset } = useContext(BottomTabInsetContext);
35
+ const [insetClaimed, setInsetClaimed] = useState(false);
36
+ const tabInsetValue = useMemo(
37
+ () => ({ inset: tabBarInset, claim: () => setInsetClaimed(true) }),
38
+ [tabBarInset],
39
+ );
40
+ const tabInsetStyle = { paddingBottom: insetClaimed ? 0 : tabBarInset };
41
+
42
+ const onScreenNavigated = (pre: string, current: string) => {
43
+ if (!isBackgroundToForeground) {
44
+ const data: any = {
45
+ preScreenName: pre,
46
+ screenName: current,
47
+ componentName: 'Screen',
48
+ state: 'navigated',
49
+ action: 'push',
50
+ };
51
+
52
+ context?.autoTracking?.({
53
+ ...context,
54
+ ...data,
55
+ });
56
+
57
+ /**
58
+ * debug toast
59
+ */
60
+ navigator?.maxApi?.showToastDebug?.({
61
+ appId: context.appId,
62
+ message: `${screenName} screen_navigated`,
63
+ type: 'ERROR',
64
+ });
65
+ }
66
+ };
67
+
68
+ useFocusEffect(
69
+ React.useCallback(
70
+ () => {
71
+ setTimeout(() => {
72
+ navigator?.maxApi?.getDataObserver?.(
73
+ 'current_screen',
74
+ (data: any) => {
75
+ onScreenNavigated(data?.screenName, screenName);
76
+ navigator?.maxApi?.setObserver?.('current_screen', {
77
+ screenName,
78
+ });
79
+ },
80
+ );
81
+ }, 100);
82
+ },
83
+ // eslint-disable-next-line react-hooks/exhaustive-deps
84
+ [],
85
+ ),
86
+ );
87
+
88
+ useEffect(
89
+ () => {
90
+ const onFocusApp = navigator?.maxApi?.listen?.('onFocusApp', () => {
91
+ if (navigation.isFocused()) {
92
+ navigator?.maxApi?.getDataObserver?.(
93
+ 'current_screen',
94
+ (data: any) => {
95
+ onScreenNavigated(data?.screenName, screenName);
96
+ navigator?.maxApi?.setObserver?.('current_screen', {
97
+ screenName,
98
+ });
99
+ },
100
+ );
101
+ }
102
+ });
103
+
104
+ return () => {
105
+ onFocusApp?.remove?.();
106
+ };
107
+ },
108
+ // eslint-disable-next-line react-hooks/exhaustive-deps
109
+ [],
110
+ );
111
+
112
+ let stackOptions = {};
113
+ if (options) {
114
+ stackOptions = getOptions(options);
115
+ }
116
+
117
+ if (nested) {
118
+ return (
119
+ <BottomTabInsetContext.Provider value={tabInsetValue}>
120
+ <View style={[Styles.flex, tabInsetStyle]}>
121
+ <Stack.Navigator screenOptions={headerStyleOptions}>
122
+ <Stack.Screen
123
+ name="Stack"
124
+ component={StackScreen}
125
+ initialParams={{
126
+ screen,
127
+ options: stackOptions,
128
+ initialParams,
129
+ bottomTab: 'nested',
130
+ }}
131
+ options={{ ...getStackOptions() }}
132
+ />
133
+ </Stack.Navigator>
134
+ </View>
135
+ </BottomTabInsetContext.Provider>
136
+ );
137
+ }
138
+
139
+ return (
140
+ <BottomTabInsetContext.Provider value={tabInsetValue}>
141
+ <View style={[Styles.flex, tabInsetStyle]}>
142
+ <NavigationIndependentTree>
143
+ <NavigationContainer>
144
+ <Stack.Navigator screenOptions={headerStyleOptions}>
145
+ <Stack.Screen
146
+ name="Stack"
147
+ component={StackScreen}
148
+ initialParams={{
149
+ screen,
150
+ options: stackOptions,
151
+ initialParams,
152
+ bottomTab: 'default',
153
+ }}
154
+ options={{ ...getStackOptions(), ...options }}
155
+ />
156
+ </Stack.Navigator>
157
+ </NavigationContainer>
158
+ </NavigationIndependentTree>
159
+ </View>
160
+ </BottomTabInsetContext.Provider>
161
+ );
162
+ };
163
+
164
+ export default TabScreen;