@momo-kits/foundation 0.161.2-test.1 → 0.162.2-test.7

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.
@@ -1,14 +1,7 @@
1
- import React, { Ref, useContext } from 'react';
1
+ import React, { Ref, useContext, useEffect, useRef } from 'react';
2
2
  import LinearGradient from 'react-native-linear-gradient';
3
- import { Dimensions, Platform, StyleSheet, View } from 'react-native';
4
- import Animated, {
5
- Extrapolation,
6
- interpolate,
7
- useAnimatedStyle,
8
- useSharedValue,
9
- type SharedValue,
10
- } from 'react-native-reanimated';
11
3
  import { ApplicationContext, MiniAppContext } from '../../Context';
4
+ import { Animated, Dimensions, Platform, StyleSheet, View } from 'react-native';
12
5
  import { HeaderType } from '../../Layout/types';
13
6
  import { InputRef, InputSearch } from '../../Input';
14
7
  import Navigation from '../Navigation';
@@ -21,10 +14,15 @@ const SCREEN_PADDING = 12;
21
14
  const BACK_WIDTH = 28;
22
15
 
23
16
  const { width: screenWidth } = Dimensions.get('window');
17
+ const LinearGradientAnimated = Animated.createAnimatedComponent(LinearGradient);
24
18
 
19
+ /**
20
+ * Header extended with background image
21
+ * @constructor
22
+ */
25
23
  const HeaderExtendHeader: React.FC<{
26
24
  headerType?: HeaderType;
27
- animatedValue: SharedValue<number>;
25
+ animatedValue: Animated.Value;
28
26
  heightHeader: number;
29
27
  headerRightWidth: number;
30
28
  inputSearchProps?: SearchHeaderProps;
@@ -40,132 +38,118 @@ const HeaderExtendHeader: React.FC<{
40
38
  headerRightWidth = 73,
41
39
  inputSearchProps,
42
40
  inputSearchRef,
43
- useShadowHeader: useShadowHeaderProp = true,
41
+ useShadowHeader = true,
44
42
  gradientColor: customGradientColor,
45
43
  headerBackground: customBackground,
46
44
  }) => {
47
45
  const { theme } = useContext(ApplicationContext);
48
46
  const context = useContext<any>(MiniAppContext);
49
- const fallback = useSharedValue(0);
50
- const sv = animatedValue ?? fallback;
47
+ const animated = useRef(new Animated.Value(0));
51
48
  const gradientColor = customGradientColor ?? theme.colors.gradient;
52
49
  const headerBackground = customBackground ?? theme.assets?.headerBackground;
53
50
  const leftPosition = inputSearchProps?.leftPosition || BACK_WIDTH + 20;
54
51
 
55
- let useShadowHeader = useShadowHeaderProp;
56
- if (inputSearchProps && Platform.OS === 'android') {
57
- useShadowHeader = false;
58
- }
59
-
60
52
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
61
53
 
62
- const heightStyle = useAnimatedStyle(() => ({
63
- height: interpolate(
64
- sv.value,
65
- [0, 100],
66
- [heightHeader + 52, heightHeader],
67
- Extrapolation.CLAMP,
68
- ),
69
- }));
54
+ const opacityBackground = animatedValue?.interpolate({
55
+ inputRange: [0, 52],
56
+ outputRange: [0, 1],
57
+ extrapolate: 'clamp',
58
+ });
59
+ const opacityGradient = animatedValue?.interpolate({
60
+ inputRange: [0, 52],
61
+ outputRange: [1, 0],
62
+ extrapolate: 'clamp',
63
+ });
70
64
 
71
- const gradientOpacityStyle = useAnimatedStyle(() => ({
72
- opacity: interpolate(sv.value, [0, 52], [1, 0], Extrapolation.CLAMP),
73
- }));
65
+ useEffect(() => {
66
+ const listener = animatedValue.addListener(({ value }) => {
67
+ animated.current.setValue(value);
68
+ });
69
+ return () => {
70
+ animatedValue?.removeListener(listener);
71
+ };
72
+ }, [animatedValue]);
74
73
 
75
- const searchTranslateStyle = useAnimatedStyle(() => ({
76
- transform: [
77
- {
78
- translateX: interpolate(
79
- sv.value,
80
- [0, 100],
81
- [SCREEN_PADDING, leftPosition],
82
- Extrapolation.CLAMP,
83
- ),
84
- },
85
- ],
86
- width: interpolate(
87
- sv.value,
88
- [0, 100],
89
- [
90
- screenWidth - SCREEN_PADDING * 2,
91
- screenWidth - leftPosition - 12 - headerRightWidth,
92
- ],
93
- Extrapolation.CLAMP,
94
- ),
95
- }));
74
+ const height = animated.current.interpolate({
75
+ inputRange: [0, 100],
76
+ outputRange: [heightHeader + 52, heightHeader],
77
+ extrapolate: 'clamp',
78
+ });
96
79
 
97
- const searchBackgroundStyle = useAnimatedStyle(() => {
98
- const t = Math.min(Math.max(sv.value / 100, 0), 1);
99
- return { backgroundColor: t === 0
100
- ? theme.colors.background.surface
101
- : theme.colors.background.default };
80
+ const translateX = animated.current.interpolate({
81
+ inputRange: [0, 100],
82
+ outputRange: [SCREEN_PADDING, leftPosition],
83
+ extrapolate: 'clamp',
102
84
  });
103
85
 
104
- const extendedHeightStyle = useAnimatedStyle(() => ({
105
- height: interpolate(
106
- sv.value,
107
- [0, 100],
108
- [heightHeader + 52, heightHeader],
109
- Extrapolation.CLAMP,
110
- ),
111
- }));
86
+ const backgroundColor = animated.current.interpolate({
87
+ inputRange: [0, 100],
88
+ outputRange: [
89
+ theme.colors.background.surface,
90
+ theme.colors.background.default,
91
+ ],
92
+ extrapolate: 'clamp',
93
+ });
94
+
95
+ if (inputSearchProps && Platform.OS === 'android') {
96
+ useShadowHeader = false;
97
+ }
112
98
 
113
99
  if (inputSearchProps) {
114
100
  return (
115
101
  <View style={[{ zIndex: 0 }, showBaseLineDebug && styles.debugBaseLine]}>
116
- <Animated.View style={heightStyle} />
102
+ <Animated.View style={{ height: height }} />
117
103
  <BackgroundImageView
118
104
  useShadowHeader={useShadowHeader}
119
105
  heightHeader={heightHeader}
120
- animatedValue={sv}
106
+ opacityBackground={opacityBackground}
121
107
  headerBackground={headerBackground}
122
108
  />
123
109
  <Animated.View
124
110
  style={[
125
111
  styles.headerBox,
126
- headerType === 'extended'
127
- ? heightStyle
128
- : { height: heightHeader },
112
+ { height: headerType === 'extended' ? height : heightHeader },
129
113
  ]}
130
114
  >
131
115
  {!!gradientColor && (
132
- <Animated.View
133
- style={[styles.extendedHeader, gradientOpacityStyle]}
116
+ <LinearGradientAnimated
117
+ colors={[gradientColor, gradientColor + '00']}
118
+ style={[styles.extendedHeader, { opacity: opacityGradient }]}
134
119
  >
135
- <LinearGradient
136
- colors={[gradientColor, gradientColor + '00']}
137
- style={StyleSheet.absoluteFill}
138
- >
139
- {!!theme.assets?.headerBackground && (
140
- <Image
141
- style={styles.headerBackground}
142
- source={{ uri: theme.assets?.headerBackground }}
143
- loading={false}
144
- />
145
- )}
146
- </LinearGradient>
147
- </Animated.View>
120
+ {!!theme.assets?.headerBackground && (
121
+ <Image
122
+ style={styles.headerBackground}
123
+ source={{ uri: theme.assets?.headerBackground }}
124
+ loading={false}
125
+ />
126
+ )}
127
+ </LinearGradientAnimated>
148
128
  )}
149
129
  </Animated.View>
150
130
  <Animated.View
151
- style={[
152
- {
153
- justifyContent: 'flex-end',
154
- position: 'absolute',
155
- zIndex: 2,
156
- },
157
- heightStyle,
158
- ]}
131
+ style={{
132
+ justifyContent: 'flex-end',
133
+ height,
134
+ position: 'absolute',
135
+ zIndex: 2,
136
+ }}
159
137
  >
160
138
  <Animated.View
161
- style={[{ marginVertical: Spacing.S }, searchTranslateStyle]}
139
+ style={{
140
+ transform: [{ translateX }],
141
+ marginVertical: Spacing.S,
142
+ width: animated.current.interpolate({
143
+ inputRange: [0, 100],
144
+ outputRange: [
145
+ screenWidth - SCREEN_PADDING * 2,
146
+ screenWidth - leftPosition - 12 - headerRightWidth,
147
+ ],
148
+ extrapolate: 'clamp',
149
+ }),
150
+ }}
162
151
  >
163
- <Animated.View
164
- style={[
165
- { borderRadius: Radius.XL },
166
- searchBackgroundStyle,
167
- ]}
168
- >
152
+ <Animated.View style={{ backgroundColor, borderRadius: Radius.XL }}>
169
153
  <InputSearch
170
154
  {...inputSearchProps}
171
155
  ref={inputSearchRef}
@@ -185,26 +169,22 @@ const HeaderExtendHeader: React.FC<{
185
169
  <BackgroundImageView
186
170
  useShadowHeader={useShadowHeader}
187
171
  heightHeader={heightHeader}
188
- animatedValue={sv}
172
+ opacityBackground={opacityBackground}
189
173
  headerBackground={headerBackground}
190
174
  />
191
175
  {!!gradientColor && (
192
- <Animated.View
193
- style={[styles.extendedHeader, gradientOpacityStyle, extendedHeightStyle]}
176
+ <LinearGradientAnimated
177
+ colors={[gradientColor, gradientColor + '00']}
178
+ style={[styles.extendedHeader, { opacity: opacityGradient }]}
194
179
  >
195
- <LinearGradient
196
- colors={[gradientColor, gradientColor + '00']}
197
- style={StyleSheet.absoluteFill}
198
- >
199
- {!!headerBackground && (
200
- <Image
201
- style={styles.headerBackground}
202
- source={{ uri: headerBackground }}
203
- loading={false}
204
- />
205
- )}
206
- </LinearGradient>
207
- </Animated.View>
180
+ {!!headerBackground && (
181
+ <Image
182
+ style={styles.headerBackground}
183
+ source={{ uri: headerBackground }}
184
+ loading={false}
185
+ />
186
+ )}
187
+ </LinearGradientAnimated>
208
188
  )}
209
189
  <View style={{ height: heightHeader }} />
210
190
  </View>
@@ -1,16 +1,11 @@
1
1
  import React, { useContext } from 'react';
2
2
  import {
3
+ Animated,
3
4
  Dimensions,
4
5
  StyleSheet,
5
6
  TouchableOpacity,
6
7
  View,
7
8
  } from 'react-native';
8
- import Animated, {
9
- Extrapolation,
10
- interpolate,
11
- useAnimatedStyle,
12
- type SharedValue,
13
- } from 'react-native-reanimated';
14
9
  import { ApplicationContext, MiniAppContext } from '../../Context';
15
10
  import { exportFontFamily, Text, useScaleSize } from '../../Text';
16
11
  import { Colors, Radius, Spacing, Styles } from '../../Consts';
@@ -23,45 +18,21 @@ import { Image } from '../../Image';
23
18
  import { Icon } from '../../Icon';
24
19
  import { Skeleton } from '../../Skeleton';
25
20
 
26
- type HeaderTitleInterpolate = {
27
- inputRange: number[];
28
- outputRange: number[];
29
- };
30
-
31
- type HeaderTitleExtraProps = {
32
- animatedValue?: SharedValue<number>;
33
- interpolate?: HeaderTitleInterpolate;
34
- tintColor?: string;
35
- children?: React.ReactNode;
36
- };
37
-
38
21
  /**
39
22
  * default header title used for nav
40
23
  */
41
- const HeaderTitle: React.FC<HeaderTitleExtraProps & { [key: string]: any }> = props => {
24
+ const HeaderTitle: React.FC<any> = props => {
42
25
  const context = useContext<any>(MiniAppContext);
43
26
 
44
27
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
45
28
 
46
- const interpolateConfig = props.interpolate ?? {
47
- inputRange: [0, 200],
48
- outputRange: [0, 1],
49
- };
50
- const animatedValue = props.animatedValue;
51
-
52
- const animatedStyle = useAnimatedStyle(() => {
53
- if (!animatedValue) {
54
- return { opacity: 1 };
55
- }
56
- return {
57
- opacity: interpolate(
58
- animatedValue.value,
59
- interpolateConfig.inputRange,
60
- interpolateConfig.outputRange,
61
- Extrapolation.CLAMP,
62
- ),
63
- };
64
- });
29
+ const opacity = props.animatedValue?.interpolate(
30
+ props.interpolate ?? {
31
+ inputRange: [0, 200],
32
+ outputRange: [0, 1],
33
+ extrapolate: 'clamp',
34
+ },
35
+ );
65
36
 
66
37
  return (
67
38
  <View
@@ -86,9 +57,9 @@ const HeaderTitle: React.FC<HeaderTitleExtraProps & { [key: string]: any }> = pr
86
57
  },
87
58
  {
88
59
  fontFamily: exportFontFamily('bold', 'action_xs_bold'),
60
+ opacity,
89
61
  color: props.tintColor,
90
62
  },
91
- animatedStyle,
92
63
  ]}
93
64
  numberOfLines={1}
94
65
  />
@@ -1,16 +1,13 @@
1
1
  import { Colors, Radius, Spacing, Styles } from '../../Consts';
2
2
  import { InputRef, InputSearch } from '../../Input';
3
3
  import {
4
+ Animated,
4
5
  Dimensions,
5
6
  StyleSheet,
6
7
  TouchableOpacity,
7
8
  View,
8
9
  } from 'react-native';
9
- import Animated, {
10
- useAnimatedStyle,
11
- useSharedValue,
12
- } from 'react-native-reanimated';
13
- import React, { useContext } from 'react';
10
+ import React, { useContext, useEffect, useRef } from 'react';
14
11
  import { SearchHeaderProps } from '../types';
15
12
  import { ApplicationContext, MiniAppContext } from '../../Context';
16
13
  import { Text } from '../../Text';
@@ -31,18 +28,28 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
31
28
  const BACK_WIDTH = 28;
32
29
  const { width: screenWidth } = Dimensions.get('window');
33
30
 
34
- const fallback = useSharedValue(0);
35
- const sv = animatedValue ?? fallback;
31
+ const animated = useRef(new Animated.Value(0));
36
32
  const leftPosition = props?.leftPosition ?? BACK_WIDTH + 20;
37
33
  const headerRightWidth = props?.headerRightWidth ?? 73;
38
34
 
39
- const backgroundStyle = useAnimatedStyle(() => {
40
- const t = Math.min(Math.max(sv.value / 100, 0), 1);
41
- return {
42
- backgroundColor: t === 0
43
- ? theme.colors.background.surface
44
- : theme.colors.background.default,
35
+ useEffect(() => {
36
+ const listener = animatedValue?.addListener(({ value }) => {
37
+ animated.current.setValue(value);
38
+ });
39
+ return () => {
40
+ if (listener) {
41
+ animatedValue?.removeListener(listener);
42
+ }
45
43
  };
44
+ }, [animatedValue]);
45
+
46
+ const backgroundColor = animated.current?.interpolate({
47
+ inputRange: [0, 100],
48
+ outputRange: [
49
+ theme.colors.background.surface,
50
+ theme.colors.background.default,
51
+ ],
52
+ extrapolate: 'clamp',
46
53
  });
47
54
 
48
55
  const goBack = () => {
@@ -84,9 +91,7 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
84
91
  },
85
92
  ]}
86
93
  >
87
- <Animated.View
88
- style={[{ borderRadius: Radius.XL }, backgroundStyle]}
89
- >
94
+ <Animated.View style={{ backgroundColor, borderRadius: Radius.XL }}>
90
95
  <InputSearch
91
96
  ref={ref}
92
97
  {...props}
@@ -19,7 +19,7 @@ import Navigator from './Navigator';
19
19
  import ScaleSizeProvider from './ScaleSizeProvider';
20
20
  import { getModalOptions, getStackOptions } from './utils';
21
21
  import { NavigationContainerProps } from './types';
22
- import { ApplicationContext, MiniAppContext } from '../Context';
22
+ import { ApplicationContext, FontScaleConfig, MiniAppContext } from '../Context';
23
23
  import Localize from './Localize';
24
24
  import { Colors, defaultTheme } from '../Consts';
25
25
  import { HeaderLeft } from './Components/HeaderLeft';
@@ -29,6 +29,9 @@ import { HeaderBackground } from './Components/HeaderBackground';
29
29
 
30
30
  const Stack = createStackNavigator();
31
31
 
32
+ // Observer storage key the host app writes FontScaleConfig to. Lowercased natively.
33
+ const FONT_SCALE_OBSERVER_KEY = 'font_scale_config';
34
+
32
35
  const NavigationContainer: React.FC<NavigationContainerProps> = ({
33
36
  screen,
34
37
  theme = defaultTheme,
@@ -45,6 +48,10 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
45
48
  }) => {
46
49
  const context = useContext<any>(MiniAppContext);
47
50
  const [currentContext, setCurrentContext] = useState<any>({});
51
+ // FontScaleConfig the host writes to observer storage (single source of truth when present).
52
+ const [observerFontScaleConfig, setObserverFontScaleConfig] = useState<
53
+ FontScaleConfig | undefined
54
+ >(undefined);
48
55
 
49
56
  const mergedContext = {
50
57
  ...context,
@@ -55,10 +62,28 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
55
62
  },
56
63
  };
57
64
 
65
+ useEffect(() => {
66
+ if (!maxApi) return;
67
+ // The initial value is seeded by the host into MiniAppContext (mergedContext.fontScaleConfig)
68
+ // before the miniapp mounts, so the first frame is already scaled (no flash). Here we only
69
+ // subscribe to later host writes so a rate change applies live while the miniapp is open.
70
+ const sub = maxApi.observer?.(
71
+ FONT_SCALE_OBSERVER_KEY,
72
+ (data: FontScaleConfig) => {
73
+ setObserverFontScaleConfig(data ?? undefined);
74
+ },
75
+ );
76
+ return () => {
77
+ sub?.remove?.();
78
+ };
79
+ }, [maxApi]);
80
+
58
81
  return (
59
82
  <SafeAreaProvider>
60
83
  <MiniAppContext.Provider value={mergedContext}>
61
- <ScaleSizeProvider scaleSizeMaxRate={mergedContext?.scaleSizeMaxRate}>
84
+ <ScaleSizeProvider
85
+ fontScaleConfig={observerFontScaleConfig ?? mergedContext?.fontScaleConfig}
86
+ >
62
87
  <Navigation
63
88
  screen={screen}
64
89
  theme={theme}
@@ -1,13 +1,15 @@
1
- import React, { FC } from 'react';
1
+ import React, { FC, useContext } from 'react';
2
2
  import { ScaleSizeContext } from '../Context';
3
3
  import { ScaleSizeProviderProps } from './types';
4
4
 
5
5
  const ScaleSizeProvider: FC<ScaleSizeProviderProps> = ({
6
- scaleSizeMaxRate,
6
+ fontScaleConfig,
7
7
  children,
8
8
  }) => {
9
+ // Inherit the config from an upper layer (e.g. app root) when the host doesn't set it via MiniAppContext.
10
+ const parent = useContext(ScaleSizeContext);
9
11
  return (
10
- <ScaleSizeContext.Provider value={{ scaleSizeMaxRate }}>
12
+ <ScaleSizeContext.Provider value={fontScaleConfig ?? parent}>
11
13
  {children}
12
14
  </ScaleSizeContext.Provider>
13
15
  );
@@ -78,7 +78,7 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
78
78
  <SafeAreaProvider>
79
79
  <MiniAppContext.Provider value={mergedContext}>
80
80
  <ScaleSizeProvider
81
- scaleSizeMaxRate={mergedContext?.scaleSizeMaxRate}
81
+ fontScaleConfig={mergedContext?.fontScaleConfig}
82
82
  >
83
83
  <ApplicationContext.Provider
84
84
  value={{
@@ -2,13 +2,14 @@ import { EventArg } from '@react-navigation/core';
2
2
  import { StackNavigationOptions } from '@react-navigation/stack';
3
3
  import React, { ReactNode } from 'react';
4
4
  import {
5
+ Animated,
5
6
  TouchableOpacityProps,
6
7
  ViewProps,
7
8
  ViewStyle,
8
9
  } from 'react-native';
9
- import type { SharedValue } from 'react-native-reanimated';
10
10
  import { PopupNotifyProps } from '../Popup/types';
11
11
  import { InputRef, InputSearchProps } from '../Input';
12
+ import { FontScaleConfig } from '../Context';
12
13
  import Navigation from './Navigation';
13
14
 
14
15
  export type NavigationProps = {
@@ -108,7 +109,7 @@ export type Theme = {
108
109
  };
109
110
 
110
111
  export type ScaleSizeProviderProps = {
111
- scaleSizeMaxRate?: number;
112
+ fontScaleConfig?: FontScaleConfig;
112
113
  children: ViewProps['children'];
113
114
  };
114
115
 
@@ -258,7 +259,7 @@ export interface HeaderBackProps extends NavigationButtonProps {
258
259
  }
259
260
 
260
261
  export type HeaderBackgroundProps = {
261
- animatedValue?: SharedValue<number>;
262
+ animatedValue?: Animated.Value;
262
263
  useGradient?: boolean;
263
264
  useShadowHeader?: boolean;
264
265
  backgroundColor?: string;
@@ -298,7 +299,7 @@ export type TitleJourneyProps = {
298
299
  };
299
300
 
300
301
  export interface HeaderAnimatedProps extends ViewProps {
301
- animatedValue: SharedValue<number>;
302
+ animatedValue: Animated.Value;
302
303
  image: string;
303
304
  useScale?: boolean;
304
305
  }
@@ -342,7 +343,7 @@ export type AnimatedHeader = {
342
343
 
343
344
  export interface SearchHeaderProps extends InputSearchProps {
344
345
  ref?: React.RefObject<InputRef>;
345
- animatedValue?: SharedValue<number>;
346
+ animatedValue?: Animated.Value;
346
347
  headerRightWidth?: 0 | 74 | 110 | number;
347
348
  leftPosition?: 12 | 48 | number;
348
349
  renderButtons?: () => ReactNode;
@@ -5,8 +5,7 @@ import {
5
5
  } from '@react-navigation/stack';
6
6
  import type { HeaderTitleProps, NavigationOptions } from './types';
7
7
  import { Colors, Spacing } from '../Consts';
8
- import { AppState, Platform } from 'react-native';
9
- import type { SharedValue } from 'react-native-reanimated';
8
+ import { Animated, AppState, Platform } from 'react-native';
10
9
  import {
11
10
  MiniAppContext,
12
11
  ScreenContext,
@@ -61,7 +60,7 @@ const getModalOptions = (): StackNavigationOptions => {
61
60
  */
62
61
  const getOptions = (
63
62
  params: NavigationOptions,
64
- animatedValue?: SharedValue<number>,
63
+ animatedValue?: Animated.Value,
65
64
  ) => {
66
65
  let options: StackNavigationOptions = {};
67
66
 
@@ -132,7 +131,7 @@ const getOptions = (
132
131
 
133
132
  const exportHeaderTitle = (
134
133
  params: NavigationOptions,
135
- animatedValue?: SharedValue<number>,
134
+ animatedValue?: Animated.Value,
136
135
  ): StackNavigationOptions => {
137
136
  if (typeof params.headerTitle === 'object') {
138
137
  return {