@momo-kits/foundation 0.167.1-beta.7 → 0.167.1-duo-test.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 (47) hide show
  1. package/Application/BottomSheet.tsx +115 -14
  2. package/Application/BottomTab/BottomTabBar.tsx +6 -3
  3. package/Application/BottomTab/DuoBarCompression.tsx +135 -0
  4. package/Application/BottomTab/DuoTabBar.tsx +462 -0
  5. package/Application/BottomTab/index.tsx +150 -134
  6. package/Application/Components/DuoGlassBackground.tsx +179 -0
  7. package/Application/Components/DuoQuickReturnHeader.tsx +42 -0
  8. package/Application/Components/HeaderExtendHeader.tsx +75 -28
  9. package/Application/Components/HeaderLeft.tsx +38 -0
  10. package/Application/Components/HeaderRight.tsx +142 -9
  11. package/Application/Components/HeaderTitle.tsx +61 -21
  12. package/Application/Components/OptionalGlassView.ts +11 -0
  13. package/Application/Components/OptionalScrollObserver.ts +12 -0
  14. package/Application/Components/SearchHeader.tsx +22 -15
  15. package/Application/Components/SideBarButton.tsx +139 -0
  16. package/Application/ModalScreen.tsx +16 -2
  17. package/Application/NavigationContainer.tsx +36 -50
  18. package/Application/Navigator.ts +1 -3
  19. package/Application/StackScreen.tsx +63 -9
  20. package/Application/WidgetContainer.tsx +9 -15
  21. package/Application/device.ts +29 -0
  22. package/Application/types.ts +27 -1
  23. package/Application/utils.tsx +255 -7
  24. package/Consts/index.ts +0 -1
  25. package/Consts/theme.ts +3 -4
  26. package/Input/Input.tsx +3 -4
  27. package/Input/InputMoney.tsx +31 -51
  28. package/Input/InputPhoneNumber.tsx +3 -4
  29. package/Input/InputSearch.tsx +3 -4
  30. package/Input/InputTextArea.tsx +3 -4
  31. package/Layout/Card.tsx +8 -26
  32. package/Layout/FloatingButton.tsx +3 -3
  33. package/Layout/Item.tsx +1 -10
  34. package/Layout/ReservedRegions.tsx +47 -0
  35. package/Layout/Screen.tsx +199 -52
  36. package/Layout/Section.tsx +5 -18
  37. package/Layout/index.ts +4 -4
  38. package/Layout/styles.ts +5 -0
  39. package/Layout/utils.ts +465 -5
  40. package/Popup/PopupNotify.tsx +33 -22
  41. package/Popup/PopupPromotion.tsx +20 -8
  42. package/Popup/utils.ts +32 -0
  43. package/Text/index.tsx +5 -15
  44. package/Text/utils.ts +1 -2
  45. package/package.json +3 -2
  46. package/Consts/designSystemFont.ts +0 -25
  47. 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);
@@ -280,138 +278,156 @@ const BottomTab: React.FC<BottomTabProps> = ({
280
278
  };
281
279
 
282
280
  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}
281
+ <DuoBarCompressionProvider
282
+ behavior={compressionBehavior}
283
+ tabCount={tabs.length}
284
+ hasQr={!!floatingButton}
327
285
  >
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;
286
+ <Tab.Navigator
287
+ screenOptions={{
288
+ headerShown: false,
289
+ headerTransparent: true,
290
+ headerBackground: undefined,
291
+ tabBarLabelPosition: 'below-icon',
292
+ tabBarBackground: () => (
293
+ <View
294
+ style={[
295
+ styles.container,
296
+ {
297
+ backgroundColor: theme.colors.background.surface,
298
+ },
299
+ ]}
300
+ >
301
+ <Animated.View
302
+ style={[styles.indicatorContainer, indicatorStyle]}
303
+ >
304
+ <View
305
+ style={[
306
+ styles.indicator,
307
+ { backgroundColor: theme.colors.primary },
308
+ ]}
309
+ />
310
+ </Animated.View>
311
+ </View>
312
+ ),
313
+ }}
314
+ tabBar={props =>
315
+ isIPhoneDuo ? (
316
+ <DuoTabBar
317
+ {...props}
318
+ floatingButton={floatingButton}
319
+ inactiveTintColor={theme.colors.text.default}
320
+ activeTintColor={theme.colors.primary}
321
+ />
322
+ ) : (
323
+ <BottomTabBar
324
+ {...props}
325
+ floatingButton={floatingButton}
326
+ inactiveTintColor={theme.colors.text.hint}
327
+ activeTintColor={theme.colors.primary}
328
+ style={[
329
+ {
330
+ height:
331
+ 64 +
332
+ (Platform.OS === 'ios'
333
+ ? Math.min(insets.bottom, 21)
334
+ : insets.bottom),
335
+ },
336
+ ]}
337
+ />
338
+ )
336
339
  }
340
+ initialRouteName={initialRouteName}
341
+ >
342
+ {tabs.map((item, index) => {
343
+ const isNum = !isNaN(parseInt(`${item?.badgeLabel}`, 10));
344
+ const isDot =
345
+ typeof item?.badgeLabel === 'string' && !item?.badgeLabel;
346
+ const isNumZero = isNum && Number(item.badgeLabel) === 0;
337
347
 
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
- };
348
+ let badgeLabel = item?.badgeLabel;
349
+ if (isNumZero) {
350
+ badgeLabel = undefined;
351
+ }
348
352
 
349
- if (isNum) {
350
- badgeStyle = {
351
- marginRight: -4,
353
+ let badgeStyle: {
354
+ marginRight: number;
355
+ top: number;
356
+ backgroundColor: string;
357
+ width?: number;
358
+ } = {
359
+ marginRight: -8,
352
360
  top: -4,
353
- backgroundColor: Colors.red_03,
354
- };
355
- }
356
- if (isDot) {
357
- badgeStyle = {
358
- marginRight: -2,
359
- top: -2,
360
361
  backgroundColor: Colors.orange_03,
361
- width: 12,
362
362
  };
363
- }
364
363
 
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'),
364
+ if (isNum) {
365
+ badgeStyle = {
366
+ marginRight: -4,
367
+ top: -4,
368
+ backgroundColor: Colors.red_03,
369
+ };
370
+ }
371
+ if (isDot) {
372
+ badgeStyle = {
373
+ marginRight: -2,
374
+ top: -2,
375
+ backgroundColor: Colors.orange_03,
376
+ width: 12,
377
+ };
378
+ }
379
+
380
+ return (
381
+ <Tab.Screen
382
+ key={`BottomTab-${item.name}`}
383
+ name={item.name}
384
+ component={TabScreen}
385
+ initialParams={{ ...item, nested, index }}
386
+ listeners={handler}
387
+ options={{
388
+ tabBarLabel: ({ focused, color }) => {
389
+ return (
390
+ <Text
391
+ numberOfLines={1}
392
+ typography={
393
+ focused ? 'label_xs_medium' : 'description_xs_regular'
394
+ }
395
+ color={color}
396
+ >
397
+ {item.label}
398
+ </Text>
399
+ );
403
400
  },
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>
401
+ title: item.label,
402
+ tabBarStyle: [
403
+ {
404
+ height: 64,
405
+ },
406
+ styles.tabStyle,
407
+ ],
408
+ tabBarBadge: badgeLabel,
409
+ tabBarBadgeStyle: [
410
+ {
411
+ borderColor: 'white',
412
+ borderWidth: 2,
413
+ fontSize: 10,
414
+ lineHeight: 14,
415
+ fontWeight: 'bold',
416
+ alignSelf: 'center',
417
+ fontFamily: exportFontFamily('bold'),
418
+ },
419
+ badgeStyle,
420
+ ],
421
+ tabBarIcon: ({ color }) => (
422
+ <Icon source={item.icon} color={color} size={28} />
423
+ ),
424
+ ...item,
425
+ }}
426
+ />
427
+ );
428
+ })}
429
+ </Tab.Navigator>
430
+ </DuoBarCompressionProvider>
415
431
  );
416
432
  };
417
433
 
@@ -0,0 +1,179 @@
1
+ import React, { useContext, useState } 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
+ runOnJS,
8
+ useAnimatedReaction,
9
+ useAnimatedStyle,
10
+ } from 'react-native-reanimated';
11
+ import { ApplicationContext } from '../../Context';
12
+ import { Shadow } from '../../Consts';
13
+ import { Image } from '../../Image';
14
+ import { AnimatedCompatValue, useNormalizedSharedValue } from '../utils';
15
+ import { GlassView } from './OptionalGlassView';
16
+
17
+ // extended headers run their gradient this far below the header, as off Duo
18
+ const EXTENDED_GRADIENT_EXTRA = 52;
19
+ // scroll distance over which the extended gradient collapses to the header
20
+ const EXTENDED_COLLAPSE_DISTANCE = 100;
21
+ // scroll distance over which the bar material fades in
22
+ const FADE_DISTANCE = 16;
23
+ const TRANSLUCENT_ALPHA = 'CC';
24
+
25
+ /**
26
+ * iPhone Duo header background: glass running edge to edge, behind the status bar
27
+ * column too, shown once content scrolls under it
28
+ */
29
+ const DuoGlassBackground: React.FC<{
30
+ animatedValue?: AnimatedCompatValue;
31
+ // scroll offset where the bar starts to show, e.g. below an animated banner
32
+ fadeStart?: number;
33
+ }> = ({ animatedValue, fadeStart = 0 }) => {
34
+ const { theme } = useContext(ApplicationContext);
35
+ const sharedValue = useNormalizedSharedValue(animatedValue);
36
+ const [scrolled, setScrolled] = useState(false);
37
+
38
+ useAnimatedReaction(
39
+ () => sharedValue.value > fadeStart,
40
+ (isScrolled, wasScrolled) => {
41
+ if (GlassView && isScrolled !== wasScrolled) {
42
+ runOnJS(setScrolled)(isScrolled);
43
+ }
44
+ },
45
+ [fadeStart],
46
+ );
47
+
48
+ const fadeStyle = useAnimatedStyle(() => ({
49
+ opacity: interpolate(
50
+ sharedValue.value,
51
+ [fadeStart, fadeStart + FADE_DISTANCE],
52
+ [0, 1],
53
+ Extrapolation.CLAMP,
54
+ ),
55
+ }));
56
+
57
+ return (
58
+ <View pointerEvents={'none'} style={StyleSheet.absoluteFill}>
59
+ {GlassView ? (
60
+ // UIVisualEffectView breaks under alpha, so glass toggles its effect instead
61
+ <GlassView
62
+ effect={scrolled ? 'glass' : 'none'}
63
+ useShadow
64
+ style={StyleSheet.absoluteFill}
65
+ />
66
+ ) : (
67
+ <Animated.View
68
+ style={[
69
+ StyleSheet.absoluteFill,
70
+ styles.translucentBar,
71
+ {
72
+ backgroundColor:
73
+ theme.colors.background.surface + TRANSLUCENT_ALPHA,
74
+ borderBottomColor: theme.colors.border.default,
75
+ },
76
+ fadeStyle,
77
+ ]}
78
+ />
79
+ )}
80
+ </View>
81
+ );
82
+ };
83
+
84
+ /**
85
+ * pink header gradient drawn under the content, fading out on scroll as main's extended
86
+ * header does: header-high for default headers, 52 taller (collapsing) for extended
87
+ */
88
+ const DuoScrollGradient: React.FC<{
89
+ animatedValue?: AnimatedCompatValue;
90
+ gradientColor?: string;
91
+ headerBackground?: string;
92
+ heightHeader: number;
93
+ extended?: boolean;
94
+ // quick return header: the gradient leaves with the content instead of fading
95
+ followScroll?: boolean;
96
+ }> = ({
97
+ animatedValue,
98
+ gradientColor,
99
+ headerBackground,
100
+ heightHeader,
101
+ extended,
102
+ followScroll,
103
+ }) => {
104
+ const { theme } = useContext(ApplicationContext);
105
+ const sharedValue = useNormalizedSharedValue(animatedValue);
106
+ const color = gradientColor ?? theme.colors.gradient;
107
+ const image = headerBackground ?? theme.assets?.headerBackground;
108
+
109
+ const scrollStyle = useAnimatedStyle(() => {
110
+ if (followScroll) {
111
+ return {
112
+ height: heightHeader + (extended ? EXTENDED_GRADIENT_EXTRA : 0),
113
+ transform: [{ translateY: -Math.max(sharedValue.value, 0) }],
114
+ };
115
+ }
116
+ return {
117
+ height: extended
118
+ ? interpolate(
119
+ sharedValue.value,
120
+ [0, EXTENDED_COLLAPSE_DISTANCE],
121
+ [heightHeader + EXTENDED_GRADIENT_EXTRA, heightHeader],
122
+ Extrapolation.CLAMP,
123
+ )
124
+ : heightHeader,
125
+ opacity: interpolate(
126
+ sharedValue.value,
127
+ [0, EXTENDED_GRADIENT_EXTRA],
128
+ [1, 0],
129
+ Extrapolation.CLAMP,
130
+ ),
131
+ };
132
+ });
133
+
134
+ if (!color) {
135
+ return null;
136
+ }
137
+
138
+ return (
139
+ <Animated.View
140
+ pointerEvents={'none'}
141
+ style={[styles.gradient, scrollStyle]}
142
+ >
143
+ <LinearGradient
144
+ colors={[color, color + '00']}
145
+ style={StyleSheet.absoluteFill}
146
+ >
147
+ {!!image && (
148
+ <Image
149
+ style={styles.gradientFill}
150
+ source={{ uri: image }}
151
+ loading={false}
152
+ />
153
+ )}
154
+ </LinearGradient>
155
+ </Animated.View>
156
+ );
157
+ };
158
+
159
+ const styles = StyleSheet.create({
160
+ translucentBar: {
161
+ borderBottomWidth: StyleSheet.hairlineWidth,
162
+ ...Shadow.Light,
163
+ },
164
+ gradient: {
165
+ position: 'absolute',
166
+ top: 0,
167
+ left: 0,
168
+ right: 0,
169
+ overflow: 'hidden',
170
+ },
171
+ // banner image keeps its 375:154 shape, the gradient box clips it
172
+ gradientFill: {
173
+ width: '100%',
174
+ position: 'absolute',
175
+ aspectRatio: 375 / 154,
176
+ },
177
+ });
178
+
179
+ 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 };