@momo-kits/foundation 0.167.1-beta.7 → 0.167.1-duo.12

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 (46) 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 +155 -134
  6. package/Application/Components/DuoGlassBackground.tsx +179 -0
  7. package/Application/Components/DuoQuickReturnHeader.tsx +42 -0
  8. package/Application/Components/HeaderExtendHeader.tsx +65 -28
  9. package/Application/Components/HeaderLeft.tsx +23 -0
  10. package/Application/Components/HeaderRight.tsx +104 -9
  11. package/Application/Components/HeaderTitle.tsx +47 -21
  12. package/Application/Components/OptionalGlassView.ts +11 -0
  13. package/Application/Components/SearchHeader.tsx +15 -14
  14. package/Application/Components/SideBarButton.tsx +128 -0
  15. package/Application/ModalScreen.tsx +16 -2
  16. package/Application/NavigationContainer.tsx +36 -48
  17. package/Application/Navigator.ts +1 -3
  18. package/Application/StackScreen.tsx +2 -5
  19. package/Application/WidgetContainer.tsx +9 -15
  20. package/Application/device.ts +29 -0
  21. package/Application/types.ts +10 -1
  22. package/Application/utils.tsx +10 -3
  23. package/Consts/index.ts +0 -1
  24. package/Consts/theme.ts +3 -4
  25. package/Input/Input.tsx +3 -4
  26. package/Input/InputMoney.tsx +31 -51
  27. package/Input/InputPhoneNumber.tsx +3 -4
  28. package/Input/InputSearch.tsx +3 -4
  29. package/Input/InputTextArea.tsx +3 -4
  30. package/Layout/Card.tsx +8 -26
  31. package/Layout/FloatingButton.tsx +3 -3
  32. package/Layout/Item.tsx +1 -10
  33. package/Layout/ReservedRegions.tsx +47 -0
  34. package/Layout/Screen.tsx +240 -48
  35. package/Layout/Section.tsx +5 -18
  36. package/Layout/index.ts +4 -4
  37. package/Layout/styles.ts +5 -0
  38. package/Layout/utils.ts +375 -5
  39. package/Popup/PopupNotify.tsx +33 -22
  40. package/Popup/PopupPromotion.tsx +20 -8
  41. package/Popup/utils.ts +32 -0
  42. package/Text/index.tsx +5 -15
  43. package/Text/utils.ts +1 -2
  44. package/package.json +3 -2
  45. package/Consts/designSystemFont.ts +0 -25
  46. package/Layout/GridSystem.tsx +0 -137
@@ -9,11 +9,9 @@ import React, {
9
9
  useState,
10
10
  } from 'react';
11
11
  import {
12
- NativeSyntheticEvent,
13
- Platform,
14
12
  TextInput,
15
- TextInputFocusEventData,
16
- TextInputSelectionChangeEventData,
13
+ TextInputFocusEvent,
14
+ TextInputSelectionChangeEvent,
17
15
  TouchableOpacity,
18
16
  View,
19
17
  } from 'react-native';
@@ -57,13 +55,10 @@ const caretFromDigitIndex = (display: string, digitIndex: number) => {
57
55
  return 0;
58
56
  };
59
57
 
60
- // Finds which range of `old` was replaced to produce `next`. `selection` is
61
- // the pre-edit selection on iOS and already the post-edit caret on Android,
62
- // where onSelectionChange fires before onChangeText.
63
58
  const detectEdit = (old: string, next: string, selection: Selection): Edit => {
64
59
  const { start, end } = selection;
65
60
  const removed = old.length - next.length;
66
- const preEdit: Edit[] = [
61
+ const candidates: Edit[] = [
67
62
  {
68
63
  start,
69
64
  end,
@@ -72,26 +67,6 @@ const detectEdit = (old: string, next: string, selection: Selection): Edit => {
72
67
  { start: start - removed, end: start, inserted: '' },
73
68
  { start, end: start + removed, inserted: '' },
74
69
  ];
75
- const postEdit: Edit[] = [];
76
- const tail = next.length - start;
77
- if (start === end && tail <= old.length) {
78
- let prefix = 0;
79
- while (
80
- prefix < Math.min(start, old.length - tail) &&
81
- old[prefix] === next[prefix]
82
- ) {
83
- prefix += 1;
84
- }
85
- postEdit.push({
86
- start: prefix,
87
- end: old.length - tail,
88
- inserted: next.slice(prefix, start),
89
- });
90
- }
91
- const candidates =
92
- Platform.OS === 'android'
93
- ? [...postEdit, ...preEdit]
94
- : [...preEdit, ...postEdit];
95
70
  const match = candidates.find(
96
71
  e =>
97
72
  e.start >= 0 &&
@@ -196,9 +171,8 @@ const InputMoney = forwardRef(
196
171
  const selectionRef = useRef<Selection>({ start: 0, end: 0 });
197
172
  // Caret to push to native once the committed text is rendered.
198
173
  const pendingCaretRef = useRef<number | null>(null);
199
- // Caret the native input reports for the raw, unformatted text. On iOS that
200
- // event lands after onChangeText and must not override the formatted caret.
201
- const rawCaretRef = useRef<number | null>(null);
174
+ const pendingSelectionRef = useRef<Selection | null>(null);
175
+ const editingRef = useRef(false);
202
176
  const inputRef = useRef<TextInput>(null);
203
177
  const componentName = 'InputMoney';
204
178
 
@@ -265,10 +239,13 @@ const InputMoney = forwardRef(
265
239
  const _onChangeText = (text: string) => {
266
240
  let digits = '';
267
241
  let caretDigit = 0;
268
- rawCaretRef.current = 0;
242
+ pendingSelectionRef.current = null;
243
+ editingRef.current = true;
244
+ setTimeout(() => {
245
+ editingRef.current = false;
246
+ });
269
247
  if (text) {
270
248
  const edit = detectEdit(displayRef.current, text, selectionRef.current);
271
- rawCaretRef.current = edit.start + edit.inserted.length;
272
249
  ({ digits, caretDigit } = applyEdit(
273
250
  displayRef.current,
274
251
  edit,
@@ -281,31 +258,34 @@ const InputMoney = forwardRef(
281
258
  };
282
259
 
283
260
  // Keeps the caret out of the currency suffix.
284
- const _onSelectionChange = (
285
- e: NativeSyntheticEvent<TextInputSelectionChangeEventData>,
286
- ) => {
261
+ const _onSelectionChange = (e: TextInputSelectionChangeEvent) => {
287
262
  props.onSelectionChange?.(e);
288
- const { start, end } = e.nativeEvent.selection;
289
- const rawCaret = rawCaretRef.current;
290
- rawCaretRef.current = null;
291
- if (start === end && start === rawCaret) {
292
- return;
293
- }
294
- const display = displayRef.current;
295
- const max = display ? display.length - currency.length : 0;
296
- const clamped = { start: Math.min(start, max), end: Math.min(end, max) };
297
- selectionRef.current = clamped;
298
- if (clamped.start !== start || clamped.end !== end) {
299
- inputRef.current?.setSelection(clamped.start, clamped.end);
300
- }
263
+ if (editingRef.current) return;
264
+ const selection = e.nativeEvent.selection;
265
+ pendingSelectionRef.current = selection;
266
+ setTimeout(() => {
267
+ if (pendingSelectionRef.current !== selection) return;
268
+ pendingSelectionRef.current = null;
269
+ const { start, end } = selection;
270
+ const display = displayRef.current;
271
+ const max = display ? display.length - currency.length : 0;
272
+ const clamped = {
273
+ start: Math.min(start, max),
274
+ end: Math.min(end, max),
275
+ };
276
+ selectionRef.current = clamped;
277
+ if (clamped.start !== start || clamped.end !== end) {
278
+ inputRef.current?.setSelection(clamped.start, clamped.end);
279
+ }
280
+ });
301
281
  };
302
282
 
303
- const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
283
+ const _onFocus = (e: TextInputFocusEvent) => {
304
284
  setFocused(true);
305
285
  onFocus?.(e);
306
286
  };
307
287
 
308
- const _onBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
288
+ const _onBlur = (e: TextInputFocusEvent) => {
309
289
  setFocused(false);
310
290
  onBlur?.(e);
311
291
  };
@@ -6,10 +6,9 @@ import React, {
6
6
  useState,
7
7
  } from 'react';
8
8
  import {
9
- NativeSyntheticEvent,
10
9
  StyleSheet,
11
10
  TextInput,
12
- TextInputFocusEventData,
11
+ TextInputFocusEvent,
13
12
  TouchableOpacity,
14
13
  View,
15
14
  } from 'react-native';
@@ -93,12 +92,12 @@ const InputPhoneNumber = forwardRef(
93
92
  onChangeText?.(text);
94
93
  };
95
94
 
96
- const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
95
+ const _onFocus = (e: TextInputFocusEvent) => {
97
96
  setFocused(true);
98
97
  onFocus?.(e);
99
98
  };
100
99
 
101
- const _onBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
100
+ const _onBlur = (e: TextInputFocusEvent) => {
102
101
  setFocused(false);
103
102
  onBlur?.(e);
104
103
  };
@@ -10,9 +10,8 @@ import React, {
10
10
  useState,
11
11
  } from 'react';
12
12
  import {
13
- NativeSyntheticEvent,
14
13
  TextInput,
15
- TextInputFocusEventData,
14
+ TextInputFocusEvent,
16
15
  TouchableOpacity,
17
16
  View,
18
17
  } from 'react-native';
@@ -166,12 +165,12 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
166
165
  props.accessibilityLabel,
167
166
  );
168
167
 
169
- const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
168
+ const _onFocus = (e: TextInputFocusEvent) => {
170
169
  setFocused(true);
171
170
  onFocus?.(e);
172
171
  };
173
172
 
174
- const _onBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
173
+ const _onBlur = (e: TextInputFocusEvent) => {
175
174
  setFocused(false);
176
175
  onBlur?.(e);
177
176
  };
@@ -7,9 +7,8 @@ import React, {
7
7
  useState,
8
8
  } from 'react';
9
9
  import {
10
- NativeSyntheticEvent,
11
10
  TextInput,
12
- TextInputFocusEventData,
11
+ TextInputFocusEvent,
13
12
  TouchableOpacity,
14
13
  View,
15
14
  } from 'react-native';
@@ -79,12 +78,12 @@ const InputTextArea = forwardRef(
79
78
  setInputValue(text);
80
79
  };
81
80
 
82
- const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
81
+ const _onFocus = (e: TextInputFocusEvent) => {
83
82
  setFocused(true);
84
83
  onFocus?.(e);
85
84
  };
86
85
 
87
- const _onBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
86
+ const _onBlur = (e: TextInputFocusEvent) => {
88
87
  setFocused(false);
89
88
  onBlur?.(e);
90
89
  };
package/Layout/Card.tsx CHANGED
@@ -1,12 +1,12 @@
1
1
  import React, { useContext } from 'react';
2
- import { Dimensions, View, ViewProps } from 'react-native';
3
- import { useGridSystem } from './utils';
2
+ import { View, ViewProps } from 'react-native';
3
+ import { useGridSystem, useScreenLayout } from './utils';
4
4
  import { GridContextProps } from './types';
5
- import { GridContext, GridSystem } from './index';
5
+ import { GridContext } from './index';
6
6
  import { Image } from '../Image';
7
- import { ApplicationContext, MiniAppContext } from '../Context';
7
+ import { ApplicationContext } from '../Context';
8
8
  import styles from './styles';
9
- import { Colors, Radius, Shadow, Spacing } from '../Consts';
9
+ import { Radius, Shadow, Spacing } from '../Consts';
10
10
 
11
11
  export interface CardProps extends ViewProps {
12
12
  /**
@@ -27,12 +27,13 @@ const Card: React.FC<CardProps> = ({
27
27
  useShadow = false,
28
28
  ...props
29
29
  }) => {
30
- const { showGrid, theme } = useContext(ApplicationContext);
30
+ const { theme } = useContext(ApplicationContext);
31
31
  const { numberOfColumns } = useGridSystem();
32
+ const { contentWidth } = useScreenLayout();
32
33
 
33
34
  const gutterSize = 8;
34
35
  const margin = 12;
35
- const widthSection = Dimensions.get('window').width - margin * 2;
36
+ const widthSection = contentWidth - margin * 2;
36
37
  let sizePerSpan =
37
38
  (widthSection - margin * 2 - gutterSize * (numberOfColumns - 1)) /
38
39
  numberOfColumns -
@@ -47,23 +48,6 @@ const Card: React.FC<CardProps> = ({
47
48
  },
48
49
  };
49
50
 
50
- /**
51
- * render overlay only dev mode
52
- */
53
- const renderOverlay = () => {
54
- return (
55
- <>
56
- <GridSystem
57
- width={widthSection}
58
- gutterSize={Spacing.S}
59
- screenGrid={false}
60
- gridColor={Colors.red_01}
61
- />
62
- <View pointerEvents={'none'} style={styles.boxGridCard} />
63
- </>
64
- );
65
- };
66
-
67
51
  return (
68
52
  <GridContext.Provider value={gridContext}>
69
53
  <View
@@ -95,9 +79,7 @@ const Card: React.FC<CardProps> = ({
95
79
  ]}
96
80
  />
97
81
  )}
98
- {/* Render only allowed children as valid React elements */}
99
82
  {children}
100
- {showGrid && renderOverlay()}
101
83
  </View>
102
84
  </GridContext.Provider>
103
85
  );
@@ -10,8 +10,8 @@ import Animated, {
10
10
  useAnimatedStyle,
11
11
  useSharedValue,
12
12
  withTiming,
13
- runOnJS,
14
13
  } from 'react-native-reanimated';
14
+ import { scheduleOnRN } from 'react-native-worklets';
15
15
  import { ApplicationContext } from '../Context';
16
16
  import { Icon } from '../Icon';
17
17
  import { useScaleSize, exportFontFamily } from '../Text';
@@ -78,7 +78,7 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
78
78
  minWidth,
79
79
  { duration: 100 },
80
80
  finished => {
81
- if (finished) runOnJS(setShowText)(false);
81
+ if (finished) scheduleOnRN(setShowText, false);
82
82
  },
83
83
  );
84
84
  } else {
@@ -87,7 +87,7 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
87
87
  maxWidth.value,
88
88
  { duration: 100 },
89
89
  finished => {
90
- if (finished) runOnJS(setShowText)(true);
90
+ if (finished) scheduleOnRN(setShowText, true);
91
91
  },
92
92
  );
93
93
  }
package/Layout/Item.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import React, { useContext } from 'react';
2
2
  import { View, ViewProps } from 'react-native';
3
3
  import { GridContext } from './index';
4
- import { ApplicationContext, MiniAppContext } from '../Context';
4
+ import { MiniAppContext } from '../Context';
5
5
  import styles from './styles';
6
6
  import { SpanNumber } from './types';
7
7
 
@@ -23,19 +23,11 @@ const Item: React.FC<ItemProps> = ({
23
23
  children,
24
24
  style,
25
25
  }) => {
26
- const { showGrid } = useContext(ApplicationContext);
27
26
  const context = useContext<any>(MiniAppContext);
28
27
  const grid = useContext(GridContext);
29
28
 
30
29
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
31
30
 
32
- /**
33
- * render overlay view only dev mode
34
- */
35
- const renderOverlay = () => {
36
- return <View pointerEvents={'none'} style={styles.boxGrid} />;
37
- };
38
-
39
31
  return (
40
32
  <View
41
33
  style={[
@@ -49,7 +41,6 @@ const Item: React.FC<ItemProps> = ({
49
41
  ]}
50
42
  >
51
43
  {children}
52
- {showGrid && renderOverlay()}
53
44
  </View>
54
45
  );
55
46
  };
@@ -0,0 +1,47 @@
1
+ import React, { createContext, ReactNode, useState } from 'react';
2
+ import { Platform, StyleSheet } from 'react-native';
3
+
4
+ export type ReservedRegion = {
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ };
10
+
11
+ // native probe is optional so apps without @momo-kits/reserved-regions still bundle
12
+ let ReservedRegionsView: React.ComponentType<any> | undefined;
13
+ try {
14
+ ReservedRegionsView =
15
+ require('@momo-kits/reserved-regions').ReservedRegionsView;
16
+ } catch {
17
+ ReservedRegionsView = undefined;
18
+ }
19
+
20
+ /**
21
+ * screen regions the system currently occludes, in window coordinates
22
+ */
23
+ const ReservedRegionsContext = createContext<ReservedRegion[]>([]);
24
+
25
+ /**
26
+ * tracks the system occlusion regions (iOS 27.1+), e.g. the iPhone Duo status bar
27
+ * cluster growing for a call or a Live Activity
28
+ */
29
+ const ReservedRegionsProvider: React.FC<{ children: ReactNode }> = ({
30
+ children,
31
+ }) => {
32
+ const [regions, setRegions] = useState<ReservedRegion[]>([]);
33
+
34
+ return (
35
+ <ReservedRegionsContext.Provider value={regions}>
36
+ {children}
37
+ {Platform.OS === 'ios' && !!ReservedRegionsView && (
38
+ <ReservedRegionsView
39
+ style={StyleSheet.absoluteFill}
40
+ onRegionsChange={setRegions}
41
+ />
42
+ )}
43
+ </ReservedRegionsContext.Provider>
44
+ );
45
+ };
46
+
47
+ export { ReservedRegionsContext, ReservedRegionsProvider };