@momo-kits/foundation 0.167.2-beta.1 → 0.168.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/Application/BottomSheet.tsx +113 -14
  2. package/Application/BottomTab/BottomTabBar.tsx +6 -3
  3. package/Application/BottomTab/CustomBottomTabItem.tsx +3 -5
  4. package/Application/BottomTab/DuoBarCompression.tsx +135 -0
  5. package/Application/BottomTab/DuoTabBar.tsx +537 -0
  6. package/Application/BottomTab/index.tsx +145 -134
  7. package/Application/Components/DuoGlassBackground.tsx +158 -0
  8. package/Application/Components/DuoQuickReturnHeader.tsx +42 -0
  9. package/Application/Components/HeaderAnimated.tsx +9 -3
  10. package/Application/Components/HeaderExtendHeader.tsx +70 -28
  11. package/Application/Components/HeaderLeft.tsx +38 -0
  12. package/Application/Components/HeaderRight.tsx +144 -9
  13. package/Application/Components/HeaderTitle.tsx +43 -7
  14. package/Application/Components/SearchHeader.tsx +20 -15
  15. package/Application/Components/SideBarButton.tsx +134 -0
  16. package/Application/ModalScreen.tsx +16 -2
  17. package/Application/NavigationContainer.tsx +22 -38
  18. package/Application/Navigator.ts +1 -3
  19. package/Application/StackScreen.tsx +2 -5
  20. package/Application/device.ts +29 -0
  21. package/Application/types.ts +10 -1
  22. package/Application/utils.tsx +10 -7
  23. package/Badge/Badge.tsx +2 -3
  24. package/Badge/BadgeDotAnimation.tsx +3 -3
  25. package/Button/index.tsx +11 -11
  26. package/Consts/theme.ts +3 -4
  27. package/Input/Input.tsx +3 -4
  28. package/Input/InputMoney.tsx +8 -12
  29. package/Input/InputOTP.tsx +9 -12
  30. package/Input/InputPhoneNumber.tsx +3 -4
  31. package/Input/InputSearch.tsx +17 -14
  32. package/Input/InputTextArea.tsx +3 -4
  33. package/Input/SystemTextInput.tsx +2 -3
  34. package/Input/TextTyping.tsx +11 -6
  35. package/Input/utils.ts +2 -3
  36. package/Layout/Card.tsx +8 -26
  37. package/Layout/FloatingButton.tsx +3 -3
  38. package/Layout/Item.tsx +1 -10
  39. package/Layout/ReservedRegions.tsx +78 -0
  40. package/Layout/Screen.tsx +242 -48
  41. package/Layout/Section.tsx +5 -18
  42. package/Layout/index.ts +4 -4
  43. package/Layout/styles.ts +5 -0
  44. package/Layout/utils.ts +477 -5
  45. package/Pagination/Dot.tsx +3 -6
  46. package/Popup/PopupNotify.tsx +33 -22
  47. package/Popup/PopupPromotion.tsx +21 -8
  48. package/Popup/utils.ts +32 -0
  49. package/Radio/index.tsx +2 -3
  50. package/Text/utils.ts +1 -2
  51. package/Title/index.tsx +8 -11
  52. package/package.json +5 -2
  53. package/Layout/GridSystem.tsx +0 -137
@@ -27,7 +27,11 @@ import StackScreen from '../StackScreen';
27
27
  import { BottomTabProps } from '../types';
28
28
  import { getOptions, getStackOptions, useAppState } from '../utils';
29
29
  import BottomTabBar from './BottomTabBar';
30
+ import { DuoTabBar } from './DuoTabBar';
31
+ import { DuoBarCompressionProvider } from './DuoBarCompression';
30
32
  import { ApplicationContext, MiniAppContext } from '../../Context';
33
+ import { useScreenLayout } from '../../Layout';
34
+ import { getHeaderRowOptions } from '../../Layout/utils';
31
35
 
32
36
  const Tab = createBottomTabNavigator();
33
37
  const Stack = createStackNavigator();
@@ -36,7 +40,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
36
40
  const { navigator } = useContext(ApplicationContext);
37
41
  const context = useContext<any>(MiniAppContext);
38
42
  const { nested, options, screen, initialParams } = route?.params;
39
- const insets = useSafeAreaInsets();
43
+ const headerStyleOptions = getHeaderRowOptions(useScreenLayout());
40
44
 
41
45
  const opacityValue = 0.3;
42
46
  const scaleValue = 0.97;
@@ -139,11 +143,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
139
143
  return (
140
144
  <Animated.View style={[styles.container, tabAnimatedStyle]}>
141
145
  <Stack.Navigator
142
- screenOptions={{
143
- headerStyle: {
144
- height: 52 + insets.top,
145
- },
146
- }}
146
+ screenOptions={headerStyleOptions}
147
147
  >
148
148
  <Stack.Screen
149
149
  name="Stack"
@@ -174,11 +174,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
174
174
  <NavigationIndependentTree>
175
175
  <NavigationContainer>
176
176
  <Stack.Navigator
177
- screenOptions={{
178
- headerStyle: {
179
- height: 52 + insets.top,
180
- },
181
- }}
177
+ screenOptions={headerStyleOptions}
182
178
  >
183
179
  <Stack.Screen
184
180
  name="Stack"
@@ -205,8 +201,10 @@ const BottomTab: React.FC<BottomTabProps> = ({
205
201
  navigation,
206
202
  initialRouteName,
207
203
  floatingButton,
204
+ compressionBehavior,
208
205
  }) => {
209
206
  const { theme, navigator } = useContext(ApplicationContext);
207
+ const { isIPhoneDuo } = useScreenLayout();
210
208
  const dimensions = useWindowDimensions();
211
209
  const insets = useSafeAreaInsets();
212
210
  const initialIndex = tabs.findIndex(i => i.name === initialRouteName);
@@ -279,139 +277,152 @@ const BottomTab: React.FC<BottomTabProps> = ({
279
277
  },
280
278
  };
281
279
 
280
+ const isIOS = Platform.OS === 'ios';
281
+ const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
282
+
282
283
  return (
283
- <Tab.Navigator
284
- screenOptions={{
285
- headerShown: false,
286
- headerTransparent: true,
287
- headerBackground: undefined,
288
- tabBarLabelPosition: 'below-icon',
289
- tabBarBackground: () => (
290
- <View
291
- style={[
292
- styles.container,
293
- {
294
- backgroundColor: theme.colors.background.surface,
295
- },
296
- ]}
297
- >
298
- <Animated.View style={[styles.indicatorContainer, indicatorStyle]}>
299
- <View
300
- style={[
301
- styles.indicator,
302
- { backgroundColor: theme.colors.primary },
303
- ]}
304
- />
305
- </Animated.View>
306
- </View>
307
- ),
308
- }}
309
- tabBar={props => (
310
- <BottomTabBar
311
- {...props}
312
- floatingButton={floatingButton}
313
- inactiveTintColor={theme.colors.text.hint}
314
- activeTintColor={theme.colors.primary}
315
- style={[
316
- {
317
- height:
318
- 64 +
319
- (Platform.OS === 'ios'
320
- ? Math.min(insets.bottom, 21)
321
- : insets.bottom),
322
- },
323
- ]}
324
- />
325
- )}
326
- initialRouteName={initialRouteName}
284
+ <DuoBarCompressionProvider
285
+ behavior={compressionBehavior}
286
+ tabCount={tabs.length}
287
+ hasQr={!!floatingButton}
327
288
  >
328
- {tabs.map((item, index) => {
329
- const isNum = !isNaN(parseInt(`${item?.badgeLabel}`, 10));
330
- const isDot = typeof item?.badgeLabel === 'string' && !item?.badgeLabel;
331
- const isNumZero = isNum && Number(item.badgeLabel) === 0;
332
-
333
- let badgeLabel = item?.badgeLabel;
334
- if (isNumZero) {
335
- badgeLabel = undefined;
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
+ )
336
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;
337
342
 
338
- let badgeStyle: {
339
- marginRight: number;
340
- top: number;
341
- backgroundColor: string;
342
- width?: number;
343
- } = {
344
- marginRight: -8,
345
- top: -4,
346
- backgroundColor: Colors.orange_03,
347
- };
343
+ let badgeLabel = item?.badgeLabel;
344
+ if (isNumZero) {
345
+ badgeLabel = undefined;
346
+ }
348
347
 
349
- if (isNum) {
350
- badgeStyle = {
351
- marginRight: -4,
348
+ let badgeStyle: {
349
+ marginRight: number;
350
+ top: number;
351
+ backgroundColor: string;
352
+ width?: number;
353
+ } = {
354
+ marginRight: -8,
352
355
  top: -4,
353
- backgroundColor: Colors.red_03,
354
- };
355
- }
356
- if (isDot) {
357
- badgeStyle = {
358
- marginRight: -2,
359
- top: -2,
360
356
  backgroundColor: Colors.orange_03,
361
- width: 12,
362
357
  };
363
- }
364
358
 
365
- return (
366
- <Tab.Screen
367
- key={`BottomTab-${item.name}`}
368
- name={item.name}
369
- component={TabScreen}
370
- initialParams={{ ...item, nested, index }}
371
- listeners={handler}
372
- options={{
373
- tabBarLabel: ({ focused, color }) => {
374
- return (
375
- <Text
376
- numberOfLines={1}
377
- typography={
378
- focused ? 'label_xs_medium' : 'description_xs_regular'
379
- }
380
- color={color}
381
- >
382
- {item.label}
383
- </Text>
384
- );
385
- },
386
- title: item.label,
387
- tabBarStyle: [
388
- {
389
- height: 64,
390
- },
391
- styles.tabStyle,
392
- ],
393
- tabBarBadge: badgeLabel,
394
- tabBarBadgeStyle: [
395
- {
396
- borderColor: 'white',
397
- borderWidth: 2,
398
- fontSize: 10,
399
- lineHeight: 14,
400
- fontWeight: 'bold',
401
- alignSelf: 'center',
402
- fontFamily: exportFontFamily('bold'),
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 (
376
+ <Tab.Screen
377
+ key={`BottomTab-${item.name}`}
378
+ name={item.name}
379
+ component={TabScreen}
380
+ initialParams={{ ...item, nested, index }}
381
+ listeners={handler}
382
+ 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
+ );
403
395
  },
404
- badgeStyle,
405
- ],
406
- tabBarIcon: ({ color }) => (
407
- <Icon source={item.icon} color={color} size={28} />
408
- ),
409
- ...item,
410
- }}
411
- />
412
- );
413
- })}
414
- </Tab.Navigator>
396
+ 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} />
418
+ ),
419
+ ...item,
420
+ }}
421
+ />
422
+ );
423
+ })}
424
+ </Tab.Navigator>
425
+ </DuoBarCompressionProvider>
415
426
  );
416
427
  };
417
428
 
@@ -0,0 +1,158 @@
1
+ import React, { useContext } from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import LinearGradient from 'react-native-linear-gradient';
4
+ import Animated, {
5
+ Extrapolation,
6
+ interpolate,
7
+ useAnimatedStyle,
8
+ } from 'react-native-reanimated';
9
+ import { ApplicationContext } from '../../Context';
10
+ import { Shadow } from '../../Consts';
11
+ import { Image } from '../../Image';
12
+ import { AnimatedCompatValue, useNormalizedSharedValue } from '../utils';
13
+
14
+ // extended headers run their gradient this far below the header, as off Duo
15
+ const EXTENDED_GRADIENT_EXTRA = 52;
16
+ // scroll distance over which the extended gradient collapses to the header
17
+ const EXTENDED_COLLAPSE_DISTANCE = 100;
18
+ // scroll distance over which the bar material fades in
19
+ const FADE_DISTANCE = 16;
20
+ const TRANSLUCENT_ALPHA = 'CC';
21
+
22
+ /**
23
+ * iPhone Duo header background: translucent bar running edge to edge, behind the
24
+ * status bar column too, shown once content scrolls under it
25
+ */
26
+ const DuoGlassBackground: React.FC<{
27
+ animatedValue?: AnimatedCompatValue;
28
+ // scroll offset where the bar starts to show, e.g. below an animated banner
29
+ fadeStart?: number;
30
+ }> = ({ animatedValue, fadeStart = 0 }) => {
31
+ const { theme } = useContext(ApplicationContext);
32
+ const sharedValue = useNormalizedSharedValue(animatedValue);
33
+
34
+ const fadeStyle = useAnimatedStyle(() => ({
35
+ opacity: interpolate(
36
+ sharedValue.value,
37
+ [fadeStart, fadeStart + FADE_DISTANCE],
38
+ [0, 1],
39
+ Extrapolation.CLAMP,
40
+ ),
41
+ }));
42
+
43
+ return (
44
+ <View pointerEvents={'none'} style={StyleSheet.absoluteFill}>
45
+ <Animated.View
46
+ style={[
47
+ StyleSheet.absoluteFill,
48
+ styles.translucentBar,
49
+ {
50
+ backgroundColor:
51
+ theme.colors.background.surface + TRANSLUCENT_ALPHA,
52
+ borderBottomColor: theme.colors.border.default,
53
+ },
54
+ fadeStyle,
55
+ ]}
56
+ />
57
+ </View>
58
+ );
59
+ };
60
+
61
+ /**
62
+ * pink header gradient drawn under the content, fading out on scroll as main's extended
63
+ * header does: header-high for default headers, 52 taller (collapsing) for extended
64
+ */
65
+ const DuoScrollGradient: React.FC<{
66
+ animatedValue?: AnimatedCompatValue;
67
+ gradientColor?: string;
68
+ headerBackground?: string;
69
+ heightHeader: number;
70
+ extended?: boolean;
71
+ // quick return header: the gradient leaves with the content instead of fading
72
+ followScroll?: boolean;
73
+ }> = ({
74
+ animatedValue,
75
+ gradientColor,
76
+ headerBackground,
77
+ heightHeader,
78
+ extended,
79
+ followScroll,
80
+ }) => {
81
+ const { theme } = useContext(ApplicationContext);
82
+ const sharedValue = useNormalizedSharedValue(animatedValue);
83
+ const color = gradientColor ?? theme.colors.gradient;
84
+ const image = headerBackground ?? theme.assets?.headerBackground;
85
+
86
+ const scrollStyle = useAnimatedStyle(() => {
87
+ if (followScroll) {
88
+ return {
89
+ height: heightHeader + (extended ? EXTENDED_GRADIENT_EXTRA : 0),
90
+ transform: [{ translateY: -Math.max(sharedValue.value, 0) }],
91
+ };
92
+ }
93
+ let height = heightHeader;
94
+ if (extended) {
95
+ height = interpolate(
96
+ sharedValue.value,
97
+ [0, EXTENDED_COLLAPSE_DISTANCE],
98
+ [heightHeader + EXTENDED_GRADIENT_EXTRA, heightHeader],
99
+ Extrapolation.CLAMP,
100
+ );
101
+ }
102
+ return {
103
+ height,
104
+ opacity: interpolate(
105
+ sharedValue.value,
106
+ [0, EXTENDED_GRADIENT_EXTRA],
107
+ [1, 0],
108
+ Extrapolation.CLAMP,
109
+ ),
110
+ };
111
+ });
112
+
113
+ if (!color) {
114
+ return null;
115
+ }
116
+
117
+ return (
118
+ <Animated.View
119
+ pointerEvents={'none'}
120
+ style={[styles.gradient, scrollStyle]}
121
+ >
122
+ <LinearGradient
123
+ colors={[color, color + '00']}
124
+ style={StyleSheet.absoluteFill}
125
+ >
126
+ {!!image && (
127
+ <Image
128
+ style={styles.gradientFill}
129
+ source={{ uri: image }}
130
+ loading={false}
131
+ />
132
+ )}
133
+ </LinearGradient>
134
+ </Animated.View>
135
+ );
136
+ };
137
+
138
+ const styles = StyleSheet.create({
139
+ translucentBar: {
140
+ borderBottomWidth: StyleSheet.hairlineWidth,
141
+ ...Shadow.Light,
142
+ },
143
+ gradient: {
144
+ position: 'absolute',
145
+ top: 0,
146
+ left: 0,
147
+ right: 0,
148
+ overflow: 'hidden',
149
+ },
150
+ // banner image keeps its 375:154 shape, the gradient box clips it
151
+ gradientFill: {
152
+ width: '100%',
153
+ position: 'absolute',
154
+ aspectRatio: 375 / 154,
155
+ },
156
+ });
157
+
158
+ export { DuoGlassBackground, DuoScrollGradient };
@@ -0,0 +1,42 @@
1
+ import React, { createContext, useContext } from 'react';
2
+ import { StackHeaderProps, Header } from '@react-navigation/stack';
3
+ import Animated, {
4
+ SharedValue,
5
+ useAnimatedStyle,
6
+ } from 'react-native-reanimated';
7
+
8
+ // how far the header is pushed above the top edge
9
+ const DuoHeaderOffsetContext = createContext<SharedValue<number> | undefined>(
10
+ undefined,
11
+ );
12
+
13
+ /**
14
+ * iPhone Duo header that leaves with the content and comes back when scrolling back
15
+ */
16
+ const DuoQuickReturnHeader: React.FC<
17
+ StackHeaderProps & { offset: SharedValue<number> }
18
+ > = ({ offset, ...props }) => {
19
+ const style = useAnimatedStyle(() => ({
20
+ transform: [{ translateY: -offset.value }],
21
+ }));
22
+
23
+ return (
24
+ <DuoHeaderOffsetContext.Provider value={offset}>
25
+ <Animated.View pointerEvents={'box-none'} style={style}>
26
+ <Header {...props} />
27
+ </Animated.View>
28
+ </DuoHeaderOffsetContext.Provider>
29
+ );
30
+ };
31
+
32
+ /**
33
+ * side bar controls render inside the header; this cancels its offset so they stay put
34
+ */
35
+ const useSideBarCounterStyle = () => {
36
+ const offset = useContext(DuoHeaderOffsetContext);
37
+ return useAnimatedStyle(() => ({
38
+ transform: [{ translateY: offset ? offset.value : 0 }],
39
+ }));
40
+ };
41
+
42
+ export { DuoQuickReturnHeader, useSideBarCounterStyle };
@@ -28,9 +28,15 @@ const HeaderAnimated: React.FC<HeaderAnimatedProps> = ({
28
28
  }) => {
29
29
  const sharedValue = useNormalizedSharedValue(animatedValue);
30
30
  const animatedStyle = useAnimatedStyle(() => {
31
- const scale = useScale
32
- ? interpolate(sharedValue.value, [-300, 0, 300], [4, 1, 1], Extrapolation.CLAMP)
33
- : 1;
31
+ let scale = 1;
32
+ if (useScale) {
33
+ scale = interpolate(
34
+ sharedValue.value,
35
+ [-300, 0, 300],
36
+ [4, 1, 1],
37
+ Extrapolation.CLAMP,
38
+ );
39
+ }
34
40
  const opacity = interpolate(
35
41
  sharedValue.value,
36
42
  [0, 150, 300],