@momo-kits/foundation 0.161.2-test.1 → 0.162.1-beta.2

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.
@@ -37,10 +37,8 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
37
37
  const action = useRef<undefined | string>(undefined);
38
38
  const insets = useSafeAreaInsets();
39
39
  const heightHeader = useHeaderHeight();
40
- // AI-GENERATED START: iOS caps at 21pt (home indicator), Android uses full nav bar height
41
40
  const bottomInset = Platform.OS === 'ios' ? Math.min(insets.bottom, 21) : insets.bottom;
42
41
  const keyboardOffset = heightHeader - bottomInset;
43
- // AI-GENERATED END
44
42
 
45
43
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
46
44
 
@@ -296,9 +296,7 @@ const BottomTab: React.FC<BottomTabProps> = ({
296
296
  activeTintColor={theme.colors.primary}
297
297
  style={[
298
298
  {
299
- // AI-GENERATED START: iOS caps at 21pt (home indicator), Android uses full nav bar height
300
299
  height: 64 + (Platform.OS === 'ios' ? Math.min(insets.bottom, 21) : insets.bottom),
301
- // AI-GENERATED END
302
300
  },
303
301
  ]}
304
302
  />
@@ -8,6 +8,7 @@ import { type HeaderAnimatedProps } from '../types';
8
8
  import React from 'react';
9
9
  import { Image } from '../../Image';
10
10
  import { Colors } from '../../Consts';
11
+ import { useNormalizedSharedValue } from '../utils';
11
12
 
12
13
  const HeaderAnimated: React.FC<HeaderAnimatedProps> = ({
13
14
  animatedValue,
@@ -15,23 +16,19 @@ const HeaderAnimated: React.FC<HeaderAnimatedProps> = ({
15
16
  useScale = true,
16
17
  children,
17
18
  }) => {
19
+ const sharedValue = useNormalizedSharedValue(animatedValue);
18
20
  const animatedStyle = useAnimatedStyle(() => {
19
21
  const scale = useScale
20
- ? interpolate(
21
- animatedValue.value,
22
- [-300, 0, 300],
23
- [4, 1, 1],
24
- Extrapolation.CLAMP,
25
- )
22
+ ? interpolate(sharedValue.value, [-300, 0, 300], [4, 1, 1], Extrapolation.CLAMP)
26
23
  : 1;
27
24
  const opacity = interpolate(
28
- animatedValue.value,
25
+ sharedValue.value,
29
26
  [0, 150, 300],
30
27
  [1, 0.5, 0],
31
28
  Extrapolation.CLAMP,
32
29
  );
33
30
  const translateY = interpolate(
34
- animatedValue.value,
31
+ sharedValue.value,
35
32
  [-300, 0],
36
33
  [-80, -2],
37
34
  Extrapolation.CLAMP,
@@ -1,12 +1,13 @@
1
1
  import LinearGradient from 'react-native-linear-gradient';
2
2
  import { StyleSheet, View } from 'react-native';
3
3
  import React, { useContext } from 'react';
4
- import Animated, { useSharedValue } from 'react-native-reanimated';
4
+ import Animated from 'react-native-reanimated';
5
5
  import { HeaderBackgroundProps } from '../types';
6
6
  import { ApplicationContext, MiniAppContext } from '../../Context';
7
7
  import { Colors, Styles } from '../../Consts';
8
8
  import { Image } from '../../Image';
9
9
  import BackgroundImageView from './BackgroundImageView';
10
+ import { useNormalizedSharedValue } from '../utils';
10
11
 
11
12
  const HeaderBackground: React.FC<HeaderBackgroundProps> = ({
12
13
  animatedValue,
@@ -19,8 +20,7 @@ const HeaderBackground: React.FC<HeaderBackgroundProps> = ({
19
20
  const context = useContext<any>(MiniAppContext);
20
21
  const gradientColor = customGradientColor ?? theme.colors.gradient;
21
22
  const headerImage = headerBackground ?? theme.assets?.headerBackground;
22
- const fallback = useSharedValue(0);
23
- const sv = animatedValue ?? fallback;
23
+ const sharedValue = useNormalizedSharedValue(animatedValue);
24
24
 
25
25
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
26
26
 
@@ -29,7 +29,7 @@ const HeaderBackground: React.FC<HeaderBackgroundProps> = ({
29
29
  <BackgroundImageView
30
30
  useShadowHeader={useShadowHeader}
31
31
  heightHeader={'100%'}
32
- animatedValue={sv}
32
+ animatedValue={sharedValue}
33
33
  headerBackground={headerImage}
34
34
  />
35
35
  <View style={styles.gradientContainer}>
@@ -5,8 +5,6 @@ import Animated, {
5
5
  Extrapolation,
6
6
  interpolate,
7
7
  useAnimatedStyle,
8
- useSharedValue,
9
- type SharedValue,
10
8
  } from 'react-native-reanimated';
11
9
  import { ApplicationContext, MiniAppContext } from '../../Context';
12
10
  import { HeaderType } from '../../Layout/types';
@@ -16,6 +14,10 @@ import { Colors, Radius, Spacing } from '../../Consts';
16
14
  import { Image } from '../../Image';
17
15
  import { SearchHeaderProps } from '../types';
18
16
  import BackgroundImageView from './BackgroundImageView';
17
+ import {
18
+ AnimatedCompatValue,
19
+ useNormalizedSharedValue,
20
+ } from '../utils';
19
21
 
20
22
  const SCREEN_PADDING = 12;
21
23
  const BACK_WIDTH = 28;
@@ -24,7 +26,7 @@ const { width: screenWidth } = Dimensions.get('window');
24
26
 
25
27
  const HeaderExtendHeader: React.FC<{
26
28
  headerType?: HeaderType;
27
- animatedValue: SharedValue<number>;
29
+ animatedValue: AnimatedCompatValue;
28
30
  heightHeader: number;
29
31
  headerRightWidth: number;
30
32
  inputSearchProps?: SearchHeaderProps;
@@ -46,8 +48,7 @@ const HeaderExtendHeader: React.FC<{
46
48
  }) => {
47
49
  const { theme } = useContext(ApplicationContext);
48
50
  const context = useContext<any>(MiniAppContext);
49
- const fallback = useSharedValue(0);
50
- const sv = animatedValue ?? fallback;
51
+ const sharedValue = useNormalizedSharedValue(animatedValue);
51
52
  const gradientColor = customGradientColor ?? theme.colors.gradient;
52
53
  const headerBackground = customBackground ?? theme.assets?.headerBackground;
53
54
  const leftPosition = inputSearchProps?.leftPosition || BACK_WIDTH + 20;
@@ -61,7 +62,7 @@ const HeaderExtendHeader: React.FC<{
61
62
 
62
63
  const heightStyle = useAnimatedStyle(() => ({
63
64
  height: interpolate(
64
- sv.value,
65
+ sharedValue.value,
65
66
  [0, 100],
66
67
  [heightHeader + 52, heightHeader],
67
68
  Extrapolation.CLAMP,
@@ -69,14 +70,14 @@ const HeaderExtendHeader: React.FC<{
69
70
  }));
70
71
 
71
72
  const gradientOpacityStyle = useAnimatedStyle(() => ({
72
- opacity: interpolate(sv.value, [0, 52], [1, 0], Extrapolation.CLAMP),
73
+ opacity: interpolate(sharedValue.value, [0, 52], [1, 0], Extrapolation.CLAMP),
73
74
  }));
74
75
 
75
76
  const searchTranslateStyle = useAnimatedStyle(() => ({
76
77
  transform: [
77
78
  {
78
79
  translateX: interpolate(
79
- sv.value,
80
+ sharedValue.value,
80
81
  [0, 100],
81
82
  [SCREEN_PADDING, leftPosition],
82
83
  Extrapolation.CLAMP,
@@ -84,7 +85,7 @@ const HeaderExtendHeader: React.FC<{
84
85
  },
85
86
  ],
86
87
  width: interpolate(
87
- sv.value,
88
+ sharedValue.value,
88
89
  [0, 100],
89
90
  [
90
91
  screenWidth - SCREEN_PADDING * 2,
@@ -95,7 +96,7 @@ const HeaderExtendHeader: React.FC<{
95
96
  }));
96
97
 
97
98
  const searchBackgroundStyle = useAnimatedStyle(() => {
98
- const t = Math.min(Math.max(sv.value / 100, 0), 1);
99
+ const t = Math.min(Math.max(sharedValue.value / 100, 0), 1);
99
100
  return { backgroundColor: t === 0
100
101
  ? theme.colors.background.surface
101
102
  : theme.colors.background.default };
@@ -103,7 +104,7 @@ const HeaderExtendHeader: React.FC<{
103
104
 
104
105
  const extendedHeightStyle = useAnimatedStyle(() => ({
105
106
  height: interpolate(
106
- sv.value,
107
+ sharedValue.value,
107
108
  [0, 100],
108
109
  [heightHeader + 52, heightHeader],
109
110
  Extrapolation.CLAMP,
@@ -117,7 +118,7 @@ const HeaderExtendHeader: React.FC<{
117
118
  <BackgroundImageView
118
119
  useShadowHeader={useShadowHeader}
119
120
  heightHeader={heightHeader}
120
- animatedValue={sv}
121
+ animatedValue={sharedValue}
121
122
  headerBackground={headerBackground}
122
123
  />
123
124
  <Animated.View
@@ -185,7 +186,7 @@ const HeaderExtendHeader: React.FC<{
185
186
  <BackgroundImageView
186
187
  useShadowHeader={useShadowHeader}
187
188
  heightHeader={heightHeader}
188
- animatedValue={sv}
189
+ animatedValue={sharedValue}
189
190
  headerBackground={headerBackground}
190
191
  />
191
192
  {!!gradientColor && (
@@ -86,7 +86,7 @@ const HeaderLeft: React.FC<HeaderBackProps> = ({
86
86
 
87
87
  const styles = StyleSheet.create({
88
88
  headerLeft: {
89
- marginLeft: 8,
89
+ marginLeft: 12,
90
90
  },
91
91
  });
92
92
 
@@ -9,7 +9,6 @@ import Animated, {
9
9
  Extrapolation,
10
10
  interpolate,
11
11
  useAnimatedStyle,
12
- type SharedValue,
13
12
  } from 'react-native-reanimated';
14
13
  import { ApplicationContext, MiniAppContext } from '../../Context';
15
14
  import { exportFontFamily, Text, useScaleSize } from '../../Text';
@@ -22,6 +21,10 @@ import {
22
21
  import { Image } from '../../Image';
23
22
  import { Icon } from '../../Icon';
24
23
  import { Skeleton } from '../../Skeleton';
24
+ import {
25
+ AnimatedCompatValue,
26
+ useNormalizedSharedValue,
27
+ } from '../utils';
25
28
 
26
29
  type HeaderTitleInterpolate = {
27
30
  inputRange: number[];
@@ -29,7 +32,7 @@ type HeaderTitleInterpolate = {
29
32
  };
30
33
 
31
34
  type HeaderTitleExtraProps = {
32
- animatedValue?: SharedValue<number>;
35
+ animatedValue?: AnimatedCompatValue;
33
36
  interpolate?: HeaderTitleInterpolate;
34
37
  tintColor?: string;
35
38
  children?: React.ReactNode;
@@ -47,15 +50,16 @@ const HeaderTitle: React.FC<HeaderTitleExtraProps & { [key: string]: any }> = pr
47
50
  inputRange: [0, 200],
48
51
  outputRange: [0, 1],
49
52
  };
50
- const animatedValue = props.animatedValue;
53
+ const hasAnimated = props.animatedValue != null;
54
+ const sharedValue = useNormalizedSharedValue(props.animatedValue);
51
55
 
52
56
  const animatedStyle = useAnimatedStyle(() => {
53
- if (!animatedValue) {
57
+ if (!hasAnimated) {
54
58
  return { opacity: 1 };
55
59
  }
56
60
  return {
57
61
  opacity: interpolate(
58
- animatedValue.value,
62
+ sharedValue.value,
59
63
  interpolateConfig.inputRange,
60
64
  interpolateConfig.outputRange,
61
65
  Extrapolation.CLAMP,
@@ -6,14 +6,12 @@ import {
6
6
  TouchableOpacity,
7
7
  View,
8
8
  } from 'react-native';
9
- import Animated, {
10
- useAnimatedStyle,
11
- useSharedValue,
12
- } from 'react-native-reanimated';
9
+ import Animated, { useAnimatedStyle } from 'react-native-reanimated';
13
10
  import React, { useContext } from 'react';
14
11
  import { SearchHeaderProps } from '../types';
15
12
  import { ApplicationContext, MiniAppContext } from '../../Context';
16
13
  import { Text } from '../../Text';
14
+ import { useNormalizedSharedValue } from '../utils';
17
15
 
18
16
  const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
19
17
  (
@@ -31,13 +29,12 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
31
29
  const BACK_WIDTH = 28;
32
30
  const { width: screenWidth } = Dimensions.get('window');
33
31
 
34
- const fallback = useSharedValue(0);
35
- const sv = animatedValue ?? fallback;
32
+ const sharedValue = useNormalizedSharedValue(animatedValue);
36
33
  const leftPosition = props?.leftPosition ?? BACK_WIDTH + 20;
37
34
  const headerRightWidth = props?.headerRightWidth ?? 73;
38
35
 
39
36
  const backgroundStyle = useAnimatedStyle(() => {
40
- const t = Math.min(Math.max(sv.value / 100, 0), 1);
37
+ const t = Math.min(Math.max(sharedValue.value / 100, 0), 1);
41
38
  return {
42
39
  backgroundColor: t === 0
43
40
  ? theme.colors.background.surface
@@ -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,29 @@ import { HeaderBackground } from './Components/HeaderBackground';
29
29
 
30
30
  const Stack = createStackNavigator();
31
31
 
32
+ const FONT_SCALE_OBSERVER_KEY = 'font_scale_config';
33
+
34
+ // AI-GENERATED START: parse the font_scale_config first-frame seed read straight from async storage (string | {response} | object)
35
+ const parseSeedFontScaleConfig = (raw: any): FontScaleConfig | undefined => {
36
+ let data = raw;
37
+ if (typeof data === 'string') {
38
+ try {
39
+ data = JSON.parse(data);
40
+ } catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ data = data?.response ?? data;
45
+ if (!data || (data.useOSFontScale === undefined && data.userScaleRate === undefined)) {
46
+ return undefined;
47
+ }
48
+ return {
49
+ useOSFontScale: data.useOSFontScale,
50
+ userScaleRate: data.userScaleRate,
51
+ };
52
+ };
53
+ // AI-GENERATED END: parse the font_scale_config first-frame seed read straight from async storage (string | {response} | object)
54
+
32
55
  const NavigationContainer: React.FC<NavigationContainerProps> = ({
33
56
  screen,
34
57
  theme = defaultTheme,
@@ -45,6 +68,11 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
45
68
  }) => {
46
69
  const context = useContext<any>(MiniAppContext);
47
70
  const [currentContext, setCurrentContext] = useState<any>({});
71
+ // AI-GENERATED START: self-seed first-frame font scale by reading font_scale_config straight from async storage
72
+ const [observerFontScaleConfig, setObserverFontScaleConfig] = useState<
73
+ FontScaleConfig | undefined
74
+ >(() => parseSeedFontScaleConfig(maxApi?.getItem?.(FONT_SCALE_OBSERVER_KEY)) ?? context?.fontScaleConfig);
75
+ // AI-GENERATED END: self-seed first-frame font scale by reading font_scale_config straight from async storage
48
76
 
49
77
  const mergedContext = {
50
78
  ...context,
@@ -55,10 +83,24 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
55
83
  },
56
84
  };
57
85
 
86
+ useEffect(() => {
87
+ const sub = maxApi?.observer?.(
88
+ FONT_SCALE_OBSERVER_KEY,
89
+ (data: FontScaleConfig) => {
90
+ setObserverFontScaleConfig(data ?? undefined);
91
+ },
92
+ );
93
+ return () => {
94
+ sub?.remove?.();
95
+ };
96
+ }, [maxApi]);
97
+
58
98
  return (
59
99
  <SafeAreaProvider>
60
100
  <MiniAppContext.Provider value={mergedContext}>
61
- <ScaleSizeProvider scaleSizeMaxRate={mergedContext?.scaleSizeMaxRate}>
101
+ <ScaleSizeProvider
102
+ fontScaleConfig={observerFontScaleConfig}
103
+ >
62
104
  <Navigation
63
105
  screen={screen}
64
106
  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,6 +2,7 @@ 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,
@@ -9,6 +10,7 @@ import {
9
10
  import type { SharedValue } from 'react-native-reanimated';
10
11
  import { PopupNotifyProps } from '../Popup/types';
11
12
  import { InputRef, InputSearchProps } from '../Input';
13
+ import { FontScaleConfig } from '../Context';
12
14
  import Navigation from './Navigation';
13
15
 
14
16
  export type NavigationProps = {
@@ -108,7 +110,7 @@ export type Theme = {
108
110
  };
109
111
 
110
112
  export type ScaleSizeProviderProps = {
111
- scaleSizeMaxRate?: number;
113
+ fontScaleConfig?: FontScaleConfig;
112
114
  children: ViewProps['children'];
113
115
  };
114
116
 
@@ -258,7 +260,7 @@ export interface HeaderBackProps extends NavigationButtonProps {
258
260
  }
259
261
 
260
262
  export type HeaderBackgroundProps = {
261
- animatedValue?: SharedValue<number>;
263
+ animatedValue?: Animated.Value | SharedValue<number>;
262
264
  useGradient?: boolean;
263
265
  useShadowHeader?: boolean;
264
266
  backgroundColor?: string;
@@ -298,7 +300,7 @@ export type TitleJourneyProps = {
298
300
  };
299
301
 
300
302
  export interface HeaderAnimatedProps extends ViewProps {
301
- animatedValue: SharedValue<number>;
303
+ animatedValue: Animated.Value | SharedValue<number>;
302
304
  image: string;
303
305
  useScale?: boolean;
304
306
  }
@@ -342,7 +344,7 @@ export type AnimatedHeader = {
342
344
 
343
345
  export interface SearchHeaderProps extends InputSearchProps {
344
346
  ref?: React.RefObject<InputRef>;
345
- animatedValue?: SharedValue<number>;
347
+ animatedValue?: Animated.Value | SharedValue<number>;
346
348
  headerRightWidth?: 0 | 74 | 110 | number;
347
349
  leftPosition?: 12 | 48 | number;
348
350
  renderButtons?: () => ReactNode;
@@ -5,8 +5,9 @@ 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';
8
+ import { Animated, AppState, Platform } from 'react-native';
9
9
  import type { SharedValue } from 'react-native-reanimated';
10
+ import { useSharedValue } from 'react-native-reanimated';
10
11
  import {
11
12
  MiniAppContext,
12
13
  ScreenContext,
@@ -229,6 +230,43 @@ const useAppState = () => {
229
230
  };
230
231
  };
231
232
 
233
+ /**
234
+ * Union of the two animation runtimes an internal foundation component might
235
+ * receive. Public Screen props still accept React Native Animated.Value only;
236
+ * this type keeps lower-level header/floating components compatible while they
237
+ * render with reanimated.
238
+ */
239
+ export type AnimatedCompatValue = Animated.Value | SharedValue<number>;
240
+
241
+ const isRNAnimatedValue = (v: unknown): v is Animated.Value =>
242
+ !!v &&
243
+ typeof (v as Animated.Value).setValue === 'function' &&
244
+ typeof (v as Animated.Value).interpolate === 'function';
245
+
246
+ const isSharedValue = (v: unknown): v is SharedValue<number> =>
247
+ !!v && !isRNAnimatedValue(v) && 'value' in (v as object);
248
+
249
+ const useNormalizedSharedValue = (
250
+ input?: AnimatedCompatValue,
251
+ ): SharedValue<number> => {
252
+ const fallback = useSharedValue(0);
253
+
254
+ useEffect(() => {
255
+ if (!isRNAnimatedValue(input)) {
256
+ return;
257
+ }
258
+ const id = input.addListener(({ value }) => {
259
+ fallback.value = value;
260
+ });
261
+ return () => input.removeListener(id);
262
+ }, [input, fallback]);
263
+
264
+ if (isSharedValue(input)) {
265
+ return input;
266
+ }
267
+ return fallback;
268
+ };
269
+
232
270
  export {
233
271
  getStackOptions,
234
272
  getModalOptions,
@@ -237,4 +275,5 @@ export {
237
275
  setAutomationID,
238
276
  useComponentId,
239
277
  useAppState,
278
+ useNormalizedSharedValue,
240
279
  };
package/Context/index.ts CHANGED
@@ -9,9 +9,12 @@ const MiniAppContext = (Platform as any).MiniAppContext ?? Context;
9
9
  const ScreenContext = (Platform as any).ScreenContext ?? Context;
10
10
  const ComponentContext = (Platform as any).ComponentContext ?? Context;
11
11
  const SkeletonContext = createContext({ loading: false });
12
- const ScaleSizeContext = createContext<{ scaleSizeMaxRate?: number }>({
13
- scaleSizeMaxRate: undefined,
14
- });
12
+ // Single source of truth: holds both the OS flag and the user-adjusted rate.
13
+ export type FontScaleConfig = {
14
+ useOSFontScale?: boolean;
15
+ userScaleRate?: number;
16
+ };
17
+ const ScaleSizeContext = createContext<FontScaleConfig>({});
15
18
  const TrackingScopeContext = createContext<{ scopeName?: string }>({
16
19
  scopeName: undefined,
17
20
  });
@@ -11,11 +11,14 @@ import Animated, {
11
11
  useSharedValue,
12
12
  withTiming,
13
13
  runOnJS,
14
- type SharedValue,
15
14
  } from 'react-native-reanimated';
16
15
  import { ApplicationContext } from '../Context';
17
16
  import { Icon } from '../Icon';
18
17
  import { useScaleSize } from '../Text';
18
+ import {
19
+ AnimatedCompatValue,
20
+ useNormalizedSharedValue,
21
+ } from '../Application/utils';
19
22
 
20
23
  export interface FloatingButtonProps {
21
24
  label?: string;
@@ -24,7 +27,7 @@ export interface FloatingButtonProps {
24
27
  icon?: string;
25
28
  iconColor?: string;
26
29
  size?: 'small' | 'large';
27
- animatedValue?: SharedValue<number>;
30
+ animatedValue?: AnimatedCompatValue;
28
31
  bottom?: number;
29
32
  renderComponent?: () => React.ReactNode;
30
33
  }
@@ -41,6 +44,8 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
41
44
  bottom = 12,
42
45
  }: FloatingButtonProps) => {
43
46
  const { theme } = useContext(ApplicationContext);
47
+ const enabled = animatedValue != null;
48
+ const sharedValue = useNormalizedSharedValue(animatedValue);
44
49
  const scaledFontSize = useScaleSize(16);
45
50
  const scaledLineHeight = useScaleSize(22);
46
51
  const maxWidth = useSharedValue(0);
@@ -57,10 +62,10 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
57
62
  }, [label, opacityAnimated]);
58
63
 
59
64
  useAnimatedReaction(
60
- () => animatedValue?.value ?? 0,
65
+ () => sharedValue.value,
61
66
  (value) => {
62
67
  'worklet';
63
- if (!label || !animatedValue) return;
68
+ if (!label || !enabled) return;
64
69
  if (value !== lastOffset.value && value > 0) {
65
70
  const direction = value > lastOffset.value ? 'down' : 'up';
66
71
  lastOffset.value = value;
@@ -88,7 +93,7 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
88
93
  }
89
94
  }
90
95
  },
91
- [label, animatedValue, minWidth],
96
+ [label, enabled, minWidth],
92
97
  );
93
98
 
94
99
  const containerStyle = useAnimatedStyle(() => ({
package/Layout/Screen.tsx CHANGED
@@ -12,6 +12,7 @@ import React, {
12
12
  useRef,
13
13
  } from 'react';
14
14
  import {
15
+ Animated,
15
16
  KeyboardAvoidingView,
16
17
  NativeScrollEvent,
17
18
  NativeSyntheticEvent,
@@ -24,12 +25,9 @@ import {
24
25
  View,
25
26
  ViewProps,
26
27
  } from 'react-native';
27
- import Animated, {
28
- runOnJS,
29
- useAnimatedScrollHandler,
28
+ import {
30
29
  useSharedValue,
31
30
  withTiming,
32
- type SharedValue,
33
31
  } from 'react-native-reanimated';
34
32
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
35
33
  import { ApplicationContext, ScreenContext } from '../Context';
@@ -142,9 +140,10 @@ export interface ScreenProps extends ViewProps {
142
140
  inputSearchRef?: Ref<InputRef>;
143
141
 
144
142
  /**
145
- * Optional. Animated value for header.
143
+ * Optional. React Native animated value for header.
144
+ * Internally, Screen mirrors it into a reanimated shared value.
146
145
  */
147
- animatedValue?: SharedValue<number>;
146
+ animatedValue?: Animated.Value;
148
147
 
149
148
  /**
150
149
  * Optional. If `true`, use shadow header.
@@ -201,20 +200,47 @@ const Screen = forwardRef(
201
200
  const screen: any = useContext(ScreenContext);
202
201
  const insets = useSafeAreaInsets();
203
202
  const heightHeader = useHeaderHeight();
204
- const internalAnimatedValue = useSharedValue(0);
205
- const animatedValue = customAnimatedValue ?? internalAnimatedValue;
203
+
204
+ /**
205
+ * Dual animation source:
206
+ * - `animatedValue` is a react-native `Animated.Value` driven natively by the
207
+ * scroll event. It is what legacy consumers receive (`animatedValue`).
208
+ * - `sharedValue` is a reanimated `SharedValue` consumed by every internal
209
+ * foundation component (and offered to modern consumers). It is kept in
210
+ * sync from the scroll `listener` (see below), which fires on the JS
211
+ * thread even when the scroll uses the native driver.
212
+ * If a legacy value is driven outside of Screen, its listener also
213
+ * mirrors JS-driven updates into the shared value.
214
+ * Consumers pass only `Animated.Value`; the reanimated shared value stays
215
+ * an internal implementation detail.
216
+ */
217
+ const animatedValue = useRef<Animated.Value>(
218
+ customAnimatedValue ?? new Animated.Value(0),
219
+ );
220
+ const internalShared = useSharedValue(0);
221
+ const sharedValue = internalShared;
222
+
223
+ useEffect(() => {
224
+ if (!customAnimatedValue) {
225
+ return;
226
+ }
227
+
228
+ const listenerId = customAnimatedValue.addListener(({ value }) => {
229
+ internalShared.value = value;
230
+ });
231
+
232
+ return () => {
233
+ customAnimatedValue.removeListener(listenerId);
234
+ };
235
+ }, [customAnimatedValue, internalShared]);
236
+
206
237
  const currentTint = useRef<string | undefined>(undefined);
207
238
  const isTab = navigation?.instance?.getState?.()?.type === 'tab';
208
239
 
209
- let handleScroll: any;
240
+ let handleScroll;
210
241
  let Component: any = View;
211
242
 
212
- // AI-GENERATED START
213
- // On iOS the home indicator is ~34pt but visual safe area needs only ~21pt cap.
214
- // On Android 15+ edge-to-edge the nav bar can be 48dp — no cap needed.
215
243
  const bottomInset = Platform.OS === 'ios' ? Math.min(insets.bottom, 21) : insets.bottom;
216
- // AI-GENERATED END
217
-
218
244
  let keyboardOffset = heightHeader - bottomInset;
219
245
  if (headerType === 'extended' || animatedHeader || inputSearchProps) {
220
246
  keyboardOffset = -bottomInset;
@@ -260,7 +286,7 @@ const Screen = forwardRef(
260
286
  inputRange: [0, 50],
261
287
  outputRange: [1, 0],
262
288
  }}
263
- animatedValue={animatedValue}
289
+ animatedValue={sharedValue}
264
290
  />
265
291
  ),
266
292
  };
@@ -275,7 +301,7 @@ const Screen = forwardRef(
275
301
  headerBackground: (props: any) => (
276
302
  <HeaderBackground
277
303
  {...props}
278
- animatedValue={animatedValue}
304
+ animatedValue={sharedValue}
279
305
  useShadowHeader={useShadowHeader}
280
306
  headerBackground={headerBackground}
281
307
  gradientColor={gradientColor}
@@ -295,7 +321,7 @@ const Screen = forwardRef(
295
321
  inputRange: [0, 50],
296
322
  outputRange: [1, 0],
297
323
  }}
298
- animatedValue={animatedValue}
324
+ animatedValue={sharedValue}
299
325
  />
300
326
  ),
301
327
  };
@@ -309,6 +335,7 @@ const Screen = forwardRef(
309
335
  headerBackground,
310
336
  inputSearchProps,
311
337
  navigation?.instance,
338
+ sharedValue,
312
339
  useShadowHeader,
313
340
  ],
314
341
  );
@@ -330,7 +357,7 @@ const Screen = forwardRef(
330
357
  headerBackground: (props: any) => (
331
358
  <HeaderBackground
332
359
  {...props}
333
- animatedValue={animatedValue}
360
+ animatedValue={sharedValue}
334
361
  useGradient={false}
335
362
  useShadowHeader={useShadowHeader}
336
363
  headerBackground={headerBackground}
@@ -351,7 +378,13 @@ const Screen = forwardRef(
351
378
 
352
379
  navigation?.instance?.setOptions(options);
353
380
  },
354
- [gradientColor, headerBackground, navigation?.instance, useShadowHeader],
381
+ [
382
+ gradientColor,
383
+ headerBackground,
384
+ navigation?.instance,
385
+ sharedValue,
386
+ useShadowHeader,
387
+ ],
355
388
  );
356
389
 
357
390
  /**
@@ -369,13 +402,13 @@ const Screen = forwardRef(
369
402
  inputRange: [0, 50],
370
403
  outputRange: [1, 0],
371
404
  }}
372
- animatedValue={animatedValue}
405
+ animatedValue={sharedValue}
373
406
  />
374
407
  ),
375
408
  };
376
409
 
377
410
  navigation?.instance?.setOptions(options);
378
- }, [navigation?.instance]);
411
+ }, [navigation?.instance, sharedValue]);
379
412
 
380
413
  /**
381
414
  * export search header
@@ -398,7 +431,7 @@ const Screen = forwardRef(
398
431
  headerLeft: (props: any) =>
399
432
  params?.hiddenBack ? null : <HeaderLeft {...props} />,
400
433
  headerTitle: () => (
401
- <SearchHeader {...params} animatedValue={animatedValue} />
434
+ <SearchHeader {...params} animatedValue={sharedValue} />
402
435
  ),
403
436
  };
404
437
 
@@ -449,51 +482,47 @@ const Screen = forwardRef(
449
482
  });
450
483
  });
451
484
 
452
- const onTintColorChange = useCallback(
453
- (offsetY: number) => {
454
- if (!animatedHeader) return;
455
- let color = animatedHeader?.headerTintColor ?? Colors.black_17;
456
- if (offsetY > 50) {
457
- color = Colors.black_17;
458
- }
459
- if (color !== currentTint.current) {
460
- currentTint.current = color;
461
- navigation?.setOptions({
462
- headerTintColor: color,
463
- });
464
- let barStyle: StatusBarStyle = 'dark-content';
465
- if (currentTint.current === Colors.black_01) {
466
- barStyle = 'light-content';
467
- }
468
- StatusBar.setBarStyle(barStyle, true);
469
- }
470
- },
471
- [animatedHeader, navigation],
472
- );
473
-
474
- const emitOnScroll = useCallback(
475
- (offsetY: number) => {
476
- scrollViewProps?.onScroll?.({
477
- nativeEvent: { contentOffset: { x: 0, y: offsetY } },
478
- } as NativeSyntheticEvent<NativeScrollEvent>);
479
- },
480
- [scrollViewProps],
481
- );
482
-
483
- const scrollHandler = useAnimatedScrollHandler({
484
- onScroll: (event) => {
485
- animatedValue.value = event.contentOffset.y;
486
- runOnJS(emitOnScroll)(event.contentOffset.y);
487
- runOnJS(onTintColorChange)(event.contentOffset.y);
488
- },
489
- });
490
-
491
485
  /**
492
486
  * animated when use scroll && animated value
493
487
  */
494
488
  if (scrollable) {
495
489
  Component = Animated.ScrollView;
496
- handleScroll = scrollHandler;
490
+ handleScroll = Animated.event(
491
+ [
492
+ {
493
+ nativeEvent: {
494
+ contentOffset: { y: animatedValue.current as Animated.Value },
495
+ },
496
+ },
497
+ ],
498
+ {
499
+ useNativeDriver: true,
500
+ listener: (e: NativeSyntheticEvent<NativeScrollEvent>) => {
501
+ const offsetY = e.nativeEvent.contentOffset.y;
502
+ // keep the reanimated source in sync for the internal components.
503
+ sharedValue.value = offsetY;
504
+ scrollViewProps?.onScroll?.(e);
505
+ if (animatedHeader) {
506
+ let color = animatedHeader?.headerTintColor ?? Colors.black_17;
507
+ if (offsetY > 50) {
508
+ color = Colors.black_17;
509
+ }
510
+ if (color !== currentTint.current) {
511
+ currentTint.current = color;
512
+ navigation?.setOptions({
513
+ headerTintColor: color,
514
+ });
515
+
516
+ let barStyle: StatusBarStyle = 'dark-content';
517
+ if (currentTint.current === Colors.black_01) {
518
+ barStyle = 'light-content';
519
+ }
520
+ StatusBar.setBarStyle(barStyle, true);
521
+ }
522
+ }
523
+ },
524
+ },
525
+ );
497
526
  }
498
527
 
499
528
  /**
@@ -503,7 +532,12 @@ const Screen = forwardRef(
503
532
  const handleScrollEnd = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
504
533
  const offsetY = e.nativeEvent.contentOffset.y;
505
534
  if (inputSearchProps && offsetY < 100 && offsetY > 0) {
506
- animatedValue.value = withTiming(0, { duration: 300 });
535
+ Animated.timing(animatedValue.current, {
536
+ toValue: 0,
537
+ useNativeDriver: true,
538
+ duration: 300,
539
+ }).start();
540
+ sharedValue.value = withTiming(0, { duration: 300 });
507
541
  ref?.scrollTo?.({ y: 0, animated: true });
508
542
  }
509
543
  scrollViewProps?.onScrollEndDrag?.(e);
@@ -519,7 +553,10 @@ const Screen = forwardRef(
519
553
  style={[styles.screenBanner, { maxHeight: 210 + layoutOffset }]}
520
554
  >
521
555
  {animatedHeader?.component({
522
- animatedValue: animatedValue,
556
+ // legacy consumers read `animatedValue` (Animated.Value),
557
+ // modern consumers read `sharedValue` (SharedValue).
558
+ animatedValue: animatedValue.current,
559
+ sharedValue: sharedValue,
523
560
  })}
524
561
  </View>
525
562
  );
@@ -586,7 +623,7 @@ const Screen = forwardRef(
586
623
  headerType={headerType}
587
624
  heightHeader={heightHeader}
588
625
  headerRightWidth={headerRightWidth}
589
- animatedValue={animatedValue}
626
+ animatedValue={sharedValue}
590
627
  inputSearchProps={inputSearchProps}
591
628
  navigation={navigation}
592
629
  inputSearchRef={inputSearchRef}
@@ -613,7 +650,14 @@ const Screen = forwardRef(
613
650
  onScroll={handleScroll}
614
651
  onScrollEndDrag={handleScrollEnd}
615
652
  scrollEventThrottle={16}
616
- style={Styles.flex}
653
+ style={[
654
+ Styles.flex,
655
+ !scrollable && !Footer && !isTab && { paddingBottom: bottomInset },
656
+ ]}
657
+ contentContainerStyle={[
658
+ scrollable && !Footer && !isTab && { paddingBottom: bottomInset },
659
+ scrollViewProps?.contentContainerStyle,
660
+ ]}
617
661
  >
618
662
  {renderAnimatedHeader()}
619
663
 
@@ -624,7 +668,7 @@ const Screen = forwardRef(
624
668
  <View>
625
669
  <FloatingButton
626
670
  {...floatingButtonProps}
627
- animatedValue={animatedValue}
671
+ animatedValue={sharedValue}
628
672
  bottom={
629
673
  Footer || isTab ? 12 : bottomInset + Spacing.S
630
674
  }
package/Text/utils.ts CHANGED
@@ -7,13 +7,18 @@ const DEFAULT_SCREEN_SIZE = 375;
7
7
  const MAX_FONT_SCALE = 1.5;
8
8
  const MAX_DEVICE_SCALE = 5;
9
9
 
10
- const useScaleSize = (size: number, scaleRate?: number) => {
11
- const { scaleSizeMaxRate = MAX_FONT_SCALE } = useContext(ScaleSizeContext);
10
+ const useScaleSize = (size: number, userScaleRate?: number) => {
11
+ const { useOSFontScale = true, userScaleRate: ctxRate } =
12
+ useContext(ScaleSizeContext);
12
13
  const fontScale = PixelRatio.getFontScale();
13
14
  const { width } = useWindowDimensions();
14
15
  const deviceScale = width / DEFAULT_SCREEN_SIZE;
15
16
 
16
- const maxScaleRate = scaleRate ?? scaleSizeMaxRate;
17
+ const customRate = userScaleRate ?? ctxRate ?? 1;
18
+
19
+ if (!useOSFontScale) {
20
+ return customRate > 1 ? customRate * size : size;
21
+ }
17
22
 
18
23
  let fontSizeDeviceScale = size;
19
24
  let fontSizeOSScale = size;
@@ -27,8 +32,8 @@ const useScaleSize = (size: number, scaleRate?: number) => {
27
32
 
28
33
  if (fontScale > 1) {
29
34
  fontSizeOSScale = Math.min(
30
- fontSizeOSScale * fontScale,
31
- fontSizeOSScale * maxScaleRate,
35
+ fontScale * fontSizeOSScale,
36
+ fontSizeOSScale * MAX_FONT_SCALE,
32
37
  );
33
38
  }
34
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.161.2-test.1",
3
+ "version": "0.162.1-beta.2",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},