@momo-kits/foundation 0.167.1-duo.1 → 0.167.1-duo.13

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 (45) hide show
  1. package/Application/BottomSheet.tsx +115 -14
  2. package/Application/BottomTab/DuoBarCompression.tsx +135 -0
  3. package/Application/BottomTab/DuoTabBar.tsx +462 -0
  4. package/Application/BottomTab/index.tsx +150 -134
  5. package/Application/Components/DuoGlassBackground.tsx +179 -0
  6. package/Application/Components/DuoQuickReturnHeader.tsx +42 -0
  7. package/Application/Components/HeaderExtendHeader.tsx +75 -28
  8. package/Application/Components/HeaderLeft.tsx +38 -0
  9. package/Application/Components/HeaderRight.tsx +142 -9
  10. package/Application/Components/HeaderTitle.tsx +67 -17
  11. package/Application/Components/OptionalGlassView.ts +11 -0
  12. package/Application/Components/SearchHeader.tsx +22 -15
  13. package/Application/Components/SideBarButton.tsx +139 -0
  14. package/Application/ModalScreen.tsx +16 -2
  15. package/Application/NavigationContainer.tsx +36 -45
  16. package/Application/Navigator.ts +1 -3
  17. package/Application/StackScreen.tsx +2 -5
  18. package/Application/WidgetContainer.tsx +9 -7
  19. package/Application/device.ts +29 -0
  20. package/Application/types.ts +10 -1
  21. package/Application/utils.tsx +10 -3
  22. package/Consts/index.ts +0 -1
  23. package/Consts/theme.ts +5 -6
  24. package/Input/Input.tsx +3 -4
  25. package/Input/InputMoney.tsx +31 -51
  26. package/Input/InputPhoneNumber.tsx +3 -4
  27. package/Input/InputSearch.tsx +3 -4
  28. package/Input/InputTextArea.tsx +3 -4
  29. package/Layout/Card.tsx +8 -26
  30. package/Layout/FloatingButton.tsx +3 -3
  31. package/Layout/Item.tsx +1 -10
  32. package/Layout/ReservedRegions.tsx +47 -0
  33. package/Layout/Screen.tsx +244 -48
  34. package/Layout/Section.tsx +5 -18
  35. package/Layout/index.ts +4 -4
  36. package/Layout/styles.ts +5 -0
  37. package/Layout/utils.ts +465 -5
  38. package/Popup/PopupNotify.tsx +33 -22
  39. package/Popup/PopupPromotion.tsx +20 -8
  40. package/Popup/utils.ts +32 -0
  41. package/Text/index.tsx +2 -11
  42. package/Text/utils.ts +1 -2
  43. package/package.json +4 -3
  44. package/Consts/designSystemFont.ts +0 -38
  45. package/Layout/GridSystem.tsx +0 -137
@@ -1,14 +1,24 @@
1
- import React, { useCallback, useContext, useEffect, useRef } from 'react';
1
+ import React, {
2
+ useCallback,
3
+ useContext,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
2
8
  import {
3
9
  BackHandler,
4
10
  Dimensions,
11
+ Keyboard,
5
12
  KeyboardAvoidingView,
13
+ KeyboardEvent,
14
+ LayoutAnimation,
6
15
  Modal,
7
16
  PanResponder,
8
17
  Platform,
9
18
  Pressable,
10
19
  StyleSheet,
11
20
  TouchableOpacity,
21
+ useWindowDimensions,
12
22
  View,
13
23
  } from 'react-native';
14
24
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -22,22 +32,38 @@ import Animated, {
22
32
  Easing,
23
33
  Extrapolation,
24
34
  interpolate,
25
- runOnJS,
26
35
  useAnimatedStyle,
27
36
  useSharedValue,
28
37
  withSpring,
29
38
  withTiming,
30
39
  } from 'react-native-reanimated';
40
+ import { scheduleOnRN } from 'react-native-worklets';
31
41
  import layoutStyles from '../Layout/styles';
42
+ import { getDuoTopInset, isIPhoneDuo } from './device';
43
+
44
+ // iPhone Duo inner display: center the sheet instead of spanning the width
45
+ const DUO_REGULAR_MIN_WIDTH = 600;
32
46
 
33
47
  const BottomSheet: React.FC<BottomSheetParams> = props => {
34
48
  const { theme, navigator } = useContext(ApplicationContext);
35
49
  const context = useContext<any>(MiniAppContext);
36
- const heightDevice = Dimensions.get('screen').height;
50
+ const windowSize = useWindowDimensions();
51
+ const isDuo = isIPhoneDuo();
52
+ const isDuoCentered = isDuo && windowSize.width >= DUO_REGULAR_MIN_WIDTH;
53
+ const isPortrait = windowSize.height > windowSize.width;
54
+ // 'screen' is the outer display on iPhone Duo, even when unfolded
55
+ const heightDevice = isDuo
56
+ ? windowSize.height
57
+ : Dimensions.get('screen').height;
37
58
  const action = useRef<undefined | string>(undefined);
38
59
  const insets = useSafeAreaInsets();
39
60
  const heightHeader = useHeaderHeight();
40
- const bottomInset = Platform.OS === 'ios' ? Math.min(insets.bottom, 21) : insets.bottom;
61
+ const topGap = Math.max(
62
+ getDuoTopInset(insets, windowSize.width, windowSize.height),
63
+ Spacing.XL,
64
+ );
65
+ const bottomInset =
66
+ Platform.OS === 'ios' ? Math.min(insets.bottom, 21) : insets.bottom;
41
67
  const keyboardOffset = heightHeader - bottomInset;
42
68
 
43
69
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
@@ -58,6 +84,43 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
58
84
  }: BottomSheetParams = props.route.params;
59
85
 
60
86
  const translateY = useSharedValue(heightDevice);
87
+ const [keyboardLift, setKeyboardLift] = useState(0);
88
+
89
+ // KeyboardAvoidingView lifts the whole sheet past the top on iPhone Duo,
90
+ // so lift it here and shrink maxHeight by the same amount instead
91
+ useEffect(() => {
92
+ if (!isDuo || !useKeyboardAvoidingView) {
93
+ return;
94
+ }
95
+ const onKeyboardChange = (e: KeyboardEvent) => {
96
+ const overlap = windowSize.height - e.endCoordinates.screenY;
97
+ const lift = Math.max(
98
+ overlap - bottomInset - (keyboardVerticalOffset ?? 0),
99
+ 0,
100
+ );
101
+ if (e.duration) {
102
+ LayoutAnimation.configureNext({
103
+ duration: e.duration,
104
+ update: {
105
+ duration: e.duration,
106
+ type: LayoutAnimation.Types[e.easing] || 'keyboard',
107
+ },
108
+ });
109
+ }
110
+ setKeyboardLift(lift);
111
+ };
112
+ const subscriptions = [
113
+ Keyboard.addListener('keyboardWillChangeFrame', onKeyboardChange),
114
+ Keyboard.addListener('keyboardWillHide', onKeyboardChange),
115
+ ];
116
+ return () => subscriptions.forEach(sub => sub.remove());
117
+ }, [
118
+ bottomInset,
119
+ isDuo,
120
+ keyboardVerticalOffset,
121
+ useKeyboardAvoidingView,
122
+ windowSize.height,
123
+ ]);
61
124
 
62
125
  const openAnimation = useCallback(() => {
63
126
  translateY.value = withTiming(0, {
@@ -74,7 +137,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
74
137
  duration: 200,
75
138
  easing: Easing.bezier(0.3, 0.0, 0.8, 0.15),
76
139
  },
77
- () => runOnJS(callback)(),
140
+ () => scheduleOnRN(callback),
78
141
  );
79
142
  },
80
143
  [heightDevice, translateY],
@@ -137,9 +200,9 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
137
200
  }
138
201
  closeAnimation(() => {
139
202
  navigator?.pop();
140
- runOnJS(() => {
203
+ scheduleOnRN(() => {
141
204
  callback?.();
142
- })();
205
+ });
143
206
  });
144
207
  },
145
208
  [barrierDismissible, closeAnimation, navigator],
@@ -248,6 +311,23 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
248
311
  };
249
312
  });
250
313
 
314
+ // landscape side buttons span the full height, so inset the content; in
315
+ // portrait the side status bar only covers the top corner
316
+ const sideInsetStyle = isDuo &&
317
+ !isDuoCentered &&
318
+ !isPortrait && {
319
+ paddingLeft: insets.left,
320
+ paddingRight: insets.right,
321
+ };
322
+
323
+ const screen = (
324
+ <Screen
325
+ {...props}
326
+ {...props.route.params}
327
+ onRequestClose={onRequestClose}
328
+ />
329
+ );
330
+
251
331
  return (
252
332
  <Container
253
333
  transparent
@@ -264,7 +344,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
264
344
  android: undefined,
265
345
  })}
266
346
  keyboardVerticalOffset={keyboardVerticalOffset ?? keyboardOffset}
267
- enabled={useKeyboardAvoidingView}
347
+ enabled={useKeyboardAvoidingView && !isDuo}
268
348
  contentContainerStyle={styles.container}
269
349
  style={styles.container}
270
350
  >
@@ -287,15 +367,34 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
287
367
  borderTopRightRadius: Radius.M,
288
368
  overflow: 'hidden',
289
369
  },
370
+ isDuo && {
371
+ maxHeight: windowSize.height - topGap - keyboardLift,
372
+ marginBottom: keyboardLift,
373
+ },
374
+ isDuoCentered && {
375
+ // keeps the portrait width in landscape, like system sheets
376
+ width: Math.min(
377
+ Math.min(windowSize.width, windowSize.height) - Spacing.L * 2,
378
+ windowSize.width -
379
+ Math.max(insets.left, insets.right, Spacing.L) * 2,
380
+ ),
381
+ alignSelf: 'center',
382
+ },
290
383
  showBaseLineDebug && styles.debugBaseLine,
291
384
  ]}
292
385
  >
293
- {renderHeader()}
294
- <Screen
295
- {...props}
296
- {...props.route.params}
297
- onRequestClose={onRequestClose}
298
- />
386
+ {isDuo ? (
387
+ // content shrinks so a tall sheet keeps its header and footer
388
+ <View style={[styles.shrink, sideInsetStyle]}>
389
+ {renderHeader()}
390
+ <View style={styles.shrink}>{screen}</View>
391
+ </View>
392
+ ) : (
393
+ <>
394
+ {renderHeader()}
395
+ {screen}
396
+ </>
397
+ )}
299
398
  {footerComponent && (
300
399
  <View
301
400
  style={[
@@ -304,6 +403,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
304
403
  paddingBottom: Spacing.S,
305
404
  backgroundColor: theme.colors.background.surface,
306
405
  },
406
+ sideInsetStyle,
307
407
  ]}
308
408
  >
309
409
  {footerComponent}
@@ -353,6 +453,7 @@ const styles = StyleSheet.create({
353
453
  justifyContent: 'center',
354
454
  marginHorizontal: Spacing.M,
355
455
  },
456
+ shrink: { flexShrink: 1 },
356
457
  debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
357
458
  });
358
459
 
@@ -0,0 +1,135 @@
1
+ import React, {
2
+ createContext,
3
+ ReactNode,
4
+ useContext,
5
+ useMemo,
6
+ useState,
7
+ } from 'react';
8
+ import { Spacing } from '../../Consts';
9
+ import { useScreenLayout } from '../../Layout';
10
+ import { DuoMetrics } from '../../Layout/utils';
11
+
12
+ export type DuoBarCompressionBehavior = 'toolbar' | 'tabBar';
13
+
14
+ type DuoBarCompression = {
15
+ toolbarCompressed: boolean;
16
+ tabBarCompressed: boolean;
17
+ // the open rail needs the toolbar button slot too: only Back stays
18
+ toolbarHidden?: boolean;
19
+ // tapping the compressed bar expands it and compresses the other one
20
+ expandToolbar?: () => void;
21
+ expandTabBar?: () => void;
22
+ // a rail opened from the compressed state closes once a tab is picked
23
+ onTabSelected?: () => void;
24
+ // header buttons report their full height so the tab bar knows what is left
25
+ setToolbarHeight?: (height: number) => void;
26
+ };
27
+
28
+ const RAIL_PADDING = 6;
29
+ const RAIL_GAP = Spacing.XXS;
30
+ // space kept between the top and the bottom groups (HIG Vertical controls)
31
+ const GROUP_GAP = Spacing.M;
32
+ // Back, then the right toolkit: help + home capsule
33
+ const DEFAULT_TOOLBAR_HEIGHT =
34
+ DuoMetrics.controlSize + Spacing.M + 2 * DuoMetrics.itemPitch;
35
+ // Back, then the single button the toolbar folds into
36
+ const COMPRESSED_TOOLBAR_HEIGHT = 2 * DuoMetrics.controlSize + Spacing.M;
37
+ // Back alone, while a rail opened from the compressed state takes the toolbar slot
38
+ const BACK_ONLY_HEIGHT = DuoMetrics.controlSize;
39
+
40
+ const NO_COMPRESSION: DuoBarCompression = {
41
+ toolbarCompressed: false,
42
+ tabBarCompressed: false,
43
+ };
44
+
45
+ const DuoBarCompressionContext =
46
+ createContext<DuoBarCompression>(NO_COMPRESSION);
47
+
48
+ /**
49
+ * height of the vertical tab rail, plus the QR button under it
50
+ */
51
+ const getRailHeight = (tabCount: number, hasQr: boolean) =>
52
+ 2 * RAIL_PADDING +
53
+ tabCount * DuoMetrics.controlSize +
54
+ Math.max(tabCount - 1, 0) * RAIL_GAP +
55
+ (hasQr ? Spacing.M + DuoMetrics.controlSize : 0);
56
+
57
+ /**
58
+ * keeps the side bar's toolbar and tab bar clear of each other (HIG, Designing for
59
+ * iPhone Duo: Vertical controls):
60
+ * - both fit: both stay whole
61
+ * - one fits whole beside the other compressed: one is compressed, tapping it swaps them
62
+ * - otherwise both rest compressed; tapping one opens it for a moment (the rail closes
63
+ * again once a tab is picked)
64
+ */
65
+ const DuoBarCompressionProvider: React.FC<{
66
+ behavior?: DuoBarCompressionBehavior;
67
+ tabCount: number;
68
+ hasQr: boolean;
69
+ children: ReactNode;
70
+ }> = ({ behavior = 'toolbar', tabCount, hasQr, children }) => {
71
+ const { height, sideBar } = useScreenLayout();
72
+ const [toolbarHeight, setToolbarHeight] = useState(DEFAULT_TOOLBAR_HEIGHT);
73
+ // 'toolbar' compression keeps the tab bar whole until the toolbar is tapped
74
+ const [expanded, setExpanded] = useState<'toolbar' | 'tabBar'>(
75
+ behavior === 'toolbar' ? 'tabBar' : 'toolbar',
76
+ );
77
+ // bar opened for a moment while both rest compressed
78
+ const [opened, setOpened] = useState<'toolbar' | 'tabBar'>();
79
+
80
+ const value = useMemo(() => {
81
+ if (!sideBar) {
82
+ return NO_COMPRESSION;
83
+ }
84
+ const room = height - sideBar.bottom - sideBar.top - GROUP_GAP;
85
+ const tabBar = getRailHeight(tabCount, hasQr);
86
+ const compressedTabBar = getRailHeight(1, hasQr) - 2 * RAIL_PADDING;
87
+ const fits = (toolbar: number, tabs: number) => toolbar + tabs <= room;
88
+
89
+ if (fits(toolbarHeight, tabBar)) {
90
+ return { ...NO_COMPRESSION, setToolbarHeight };
91
+ }
92
+ const canSwap =
93
+ fits(COMPRESSED_TOOLBAR_HEIGHT, tabBar) &&
94
+ fits(toolbarHeight, compressedTabBar);
95
+ if (canSwap) {
96
+ return {
97
+ toolbarCompressed: expanded === 'tabBar',
98
+ tabBarCompressed: expanded === 'toolbar',
99
+ expandToolbar: () => setExpanded('toolbar'),
100
+ expandTabBar: () => setExpanded('tabBar'),
101
+ setToolbarHeight,
102
+ };
103
+ }
104
+
105
+ const open = {
106
+ expandToolbar: () => setOpened('toolbar'),
107
+ expandTabBar: () => setOpened('tabBar'),
108
+ setToolbarHeight,
109
+ };
110
+ if (opened === 'tabBar' && fits(BACK_ONLY_HEIGHT, tabBar)) {
111
+ return {
112
+ ...open,
113
+ toolbarCompressed: true,
114
+ toolbarHidden: true,
115
+ tabBarCompressed: false,
116
+ onTabSelected: () => setOpened(undefined),
117
+ };
118
+ }
119
+ return {
120
+ ...open,
121
+ toolbarCompressed: opened !== 'toolbar',
122
+ tabBarCompressed: true,
123
+ };
124
+ }, [expanded, opened, hasQr, height, sideBar, tabCount, toolbarHeight]);
125
+
126
+ return (
127
+ <DuoBarCompressionContext.Provider value={value}>
128
+ {children}
129
+ </DuoBarCompressionContext.Provider>
130
+ );
131
+ };
132
+
133
+ const useDuoBarCompression = () => useContext(DuoBarCompressionContext);
134
+
135
+ export { DuoBarCompressionProvider, useDuoBarCompression };