@momo-kits/foundation 0.169.1 → 0.169.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.
Files changed (38) hide show
  1. package/Application/BottomSheet.hooks.ts +17 -37
  2. package/Application/BottomSheet.parts.tsx +81 -165
  3. package/Application/BottomSheet.styles.ts +7 -23
  4. package/Application/BottomSheet.tsx +15 -67
  5. package/Application/BottomTab/ActiveIndicator.tsx +90 -0
  6. package/Application/BottomTab/BottomTabBar.styles.ts +48 -43
  7. package/Application/BottomTab/BottomTabBar.tsx +229 -223
  8. package/Application/BottomTab/BottomTabInset.tsx +14 -0
  9. package/Application/BottomTab/CurvedContainer.tsx +153 -0
  10. package/Application/BottomTab/CustomBottomTabItem.tsx +132 -108
  11. package/Application/BottomTab/DuoBarCompression.tsx +4 -12
  12. package/Application/BottomTab/DuoRailTabBar.tsx +174 -96
  13. package/Application/BottomTab/DuoTabBar.styles.ts +42 -84
  14. package/Application/BottomTab/DuoTabBar.tsx +35 -115
  15. package/Application/BottomTab/TabBarIcon.tsx +160 -72
  16. package/Application/BottomTab/TabScreen.tsx +48 -61
  17. package/Application/BottomTab/index.tsx +85 -125
  18. package/Application/BottomTab/metrics.ts +164 -0
  19. package/Application/BottomTab/tabItems.ts +46 -0
  20. package/Application/BottomTab/usePressLift.ts +32 -0
  21. package/Application/ModalIconButton.tsx +66 -0
  22. package/Application/ModalScreen.tsx +32 -15
  23. package/Application/__tests__/modalLayout.test.ts +128 -0
  24. package/Application/index.ts +10 -0
  25. package/Application/modalLayout.ts +177 -0
  26. package/Application/types.tabs.ts +5 -0
  27. package/Application/utils.ts +0 -36
  28. package/Layout/ReservedRegions.tsx +21 -4
  29. package/Layout/Screen.tsx +11 -2
  30. package/Layout/index.ts +2 -0
  31. package/Popup/PopupNotify.tsx +38 -68
  32. package/Popup/PopupPromotion.tsx +5 -4
  33. package/Popup/utils.ts +1 -26
  34. package/package.json +2 -1
  35. package/Application/BottomTab/DuoTabBar.parts.tsx +0 -202
  36. package/Application/BottomTab/badge.ts +0 -41
  37. package/Application/BottomTab/hooks.ts +0 -358
  38. package/Application/BottomTab/index.styles.ts +0 -30
@@ -1,19 +1,15 @@
1
1
  import React, { useContext, useRef, useState } from 'react';
2
- import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
2
+ import { ScrollView, StyleSheet, View } from 'react-native';
3
3
  import { ApplicationContext, MiniAppContext } from '../Context';
4
4
  import { Button } from '../Button';
5
- import { Colors, Radius, Spacing, Styles } from '../Consts';
6
- import { Icon } from '../Icon';
5
+ import { Colors, Spacing, Styles } from '../Consts';
7
6
  import { Image } from '../Image';
8
7
  import { Text, useScaleSize } from '../Text';
9
8
  import { PopupNotifyProps } from './types';
10
9
  import { scheduleOnRN } from 'react-native-worklets';
11
- import {
12
- getPopupRadius,
13
- splitHighlightText,
14
- usePopupMaxHeight,
15
- usePopupWidth,
16
- } from './utils';
10
+ import { useModalLayout } from '../Application/modalLayout';
11
+ import { ModalIconButton } from '../Application/ModalIconButton';
12
+ import { splitHighlightText } from './utils';
17
13
 
18
14
  const PopupNotify: React.FC<PopupNotifyProps> = ({
19
15
  image,
@@ -30,16 +26,19 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
30
26
  const context = useContext<any>(MiniAppContext);
31
27
  const { theme, navigator, translate } = useContext(ApplicationContext);
32
28
  const [scrollContent, setScrollContent] = useState(false);
33
- const popupWidth = usePopupWidth();
34
- const maxHeight = usePopupMaxHeight();
35
- const radius = getPopupRadius();
29
+ const { isFoldable, popup } = useModalLayout();
30
+ const { width: popupWidth, maxHeight, radius } = popup;
36
31
  const scaledMaxHeight = useScaleSize(172);
37
32
 
38
33
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
39
34
  const closeAction = useRef('touch_outside');
40
35
  let descriptionStyle = {};
41
36
  let Description: any = View;
42
- if (scrollContent) {
37
+ if (isFoldable) {
38
+ // foldables cap the popup height, the description takes what is left
39
+ Description = ScrollView;
40
+ descriptionStyle = styles.shrink;
41
+ } else if (scrollContent) {
43
42
  Description = ScrollView;
44
43
  descriptionStyle = { maxHeight: scaledMaxHeight };
45
44
  }
@@ -165,34 +164,20 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
165
164
  * build close action
166
165
  */
167
166
  const buildCloseIcon = () => {
168
- if (typeof onIconClose === 'function') {
169
- return (
170
- <TouchableOpacity
171
- accessibilityLabel={'ic_popup_close'}
172
- style={styles.iconCloseContainer}
173
- onPress={() => {
174
- closeAction.current = 'icon_close';
175
- onAction(onIconClose);
176
- }}
177
- >
178
- <View
179
- style={[
180
- styles.iconClose,
181
- {
182
- backgroundColor: theme.colors.text.default,
183
- borderColor: theme.colors.background.surface,
184
- },
185
- ]}
186
- >
187
- <Icon
188
- source="navigation_close"
189
- color={theme.colors.background.surface}
190
- size={14}
191
- />
192
- </View>
193
- </TouchableOpacity>
194
- );
167
+ if (typeof onIconClose !== 'function') {
168
+ return null;
195
169
  }
170
+ return (
171
+ <ModalIconButton
172
+ icon="navigation_close"
173
+ accessibilityLabel={'ic_popup_close'}
174
+ style={styles.iconClose}
175
+ onPress={() => {
176
+ closeAction.current = 'icon_close';
177
+ onAction(onIconClose);
178
+ }}
179
+ />
180
+ );
196
181
  };
197
182
 
198
183
  const body = (
@@ -206,11 +191,17 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
206
191
  ]}
207
192
  />
208
193
  )}
209
- <View style={styles.content}>
194
+ <View style={[styles.content, isFoldable && styles.shrink]}>
210
195
  <Text
211
196
  typography={'header_default_bold'}
212
197
  numberOfLines={3}
213
198
  accessibilityLabel={'title_popup_notify'}
199
+ // keeps the title clear of the close button on a text only popup
200
+ style={
201
+ !image &&
202
+ typeof onIconClose === 'function' &&
203
+ styles.titleClearClose
204
+ }
214
205
  >
215
206
  {title}
216
207
  </Text>
@@ -262,20 +253,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
262
253
  showBaseLineDebug && styles.debugBaseLine,
263
254
  ]}
264
255
  >
265
- {maxHeight === undefined ? (
266
- body
267
- ) : (
268
- // scrolls when iPhone Duo landscape is too short; actions stay visible
269
- <ScrollView
270
- style={[
271
- styles.body,
272
- { borderTopLeftRadius: radius, borderTopRightRadius: radius },
273
- ]}
274
- bounces={false}
275
- >
276
- {body}
277
- </ScrollView>
278
- )}
256
+ {body}
279
257
  <View style={styles.contentAction}>{buildAction()}</View>
280
258
  {buildCloseIcon()}
281
259
  </View>
@@ -288,7 +266,7 @@ const styles = StyleSheet.create({
288
266
  aspectRatio: 1.7777,
289
267
  overflow: 'hidden',
290
268
  },
291
- body: { flexShrink: 1, overflow: 'hidden' },
269
+ shrink: { flexShrink: 1 },
292
270
  content: { padding: Spacing.XL },
293
271
  description: {
294
272
  marginTop: Spacing.S,
@@ -304,19 +282,11 @@ const styles = StyleSheet.create({
304
282
  width: Spacing.S,
305
283
  height: Spacing.S,
306
284
  },
307
- iconCloseContainer: {
308
- position: 'absolute',
309
- top: -Spacing.M,
310
- right: -Spacing.M,
311
- padding: Spacing.XS,
312
- },
285
+ titleClearClose: { paddingRight: Spacing.XL },
313
286
  iconClose: {
314
- width: 20,
315
- height: 20,
316
- alignItems: 'center',
317
- justifyContent: 'center',
318
- borderRadius: Radius.M,
319
- borderWidth: 2,
287
+ position: 'absolute',
288
+ top: Spacing.M,
289
+ right: Spacing.M,
320
290
  },
321
291
  });
322
292
 
@@ -5,7 +5,7 @@ import { ApplicationContext, MiniAppContext } from '../Context';
5
5
  import { Image } from '../Image';
6
6
  import { Colors, Radius, Spacing } from '../Consts';
7
7
  import { Icon } from '../Icon';
8
- import { usePopupMaxHeight, usePopupWidth } from './utils';
8
+ import { useModalLayout } from '../Application/modalLayout';
9
9
 
10
10
  const ASPECT_RATIO = 0.72;
11
11
  const ICON_CLOSE_SIZE = 20;
@@ -20,9 +20,10 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
20
20
  }) => {
21
21
  const context = useContext<any>(MiniAppContext);
22
22
  const { theme, navigator } = useContext(ApplicationContext);
23
- const popupWidth = usePopupWidth();
24
- const maxHeight = usePopupMaxHeight();
25
- // fit the portrait image by height when iPhone Duo leaves too little room
23
+ const {
24
+ popup: { width: popupWidth, maxHeight },
25
+ } = useModalLayout();
26
+ // fit the portrait image by height when a foldable leaves too little room
26
27
  let imageWidth = popupWidth;
27
28
  if (maxHeight !== undefined) {
28
29
  const heightFitWidth = (maxHeight - CLOSE_ROW_HEIGHT) * ASPECT_RATIO;
package/Popup/utils.ts CHANGED
@@ -1,30 +1,5 @@
1
- import { Dimensions, useWindowDimensions } from 'react-native';
2
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
3
- import { Radius, Spacing } from '../Consts';
4
- import { getDuoModalBounds, isIPhoneDuo } from '../Application/utils';
5
-
6
1
  type HighlightSegment = { text: string; highlight: boolean };
7
2
 
8
- const usePopupWidth = () => {
9
- const insets = useSafeAreaInsets();
10
- const { width, height } = useWindowDimensions();
11
- if (!isIPhoneDuo()) {
12
- return Dimensions.get('window').width - Spacing.XL * 2;
13
- }
14
- return getDuoModalBounds(insets, width, height).maxWidth;
15
- };
16
-
17
- const usePopupMaxHeight = () => {
18
- const insets = useSafeAreaInsets();
19
- const { width, height } = useWindowDimensions();
20
- if (!isIPhoneDuo()) {
21
- return undefined;
22
- }
23
- return getDuoModalBounds(insets, width, height).maxHeight;
24
- };
25
-
26
- const getPopupRadius = () => (isIPhoneDuo() ? Radius.XL : Radius.L);
27
-
28
3
  const isWordChar = (char?: string) => {
29
4
  if (!char) {
30
5
  return false;
@@ -76,4 +51,4 @@ const splitHighlightText = (
76
51
  return segments;
77
52
  };
78
53
 
79
- export { usePopupWidth, usePopupMaxHeight, getPopupRadius, splitHighlightText };
54
+ export { splitHighlightText };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.169.1",
3
+ "version": "0.169.2",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "peerDependencies": {
28
28
  "react-native": "*",
29
+ "react-native-svg": "*",
29
30
  "react-native-worklets": "0.6.1"
30
31
  },
31
32
  "devDependencies": {
@@ -1,202 +0,0 @@
1
- import { CommonActions, Route } from '@react-navigation/native';
2
- import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
3
- import React, { ReactNode, useContext, useMemo, useRef } from 'react';
4
- import {
5
- StyleProp,
6
- StyleSheet,
7
- TouchableOpacity,
8
- View,
9
- ViewStyle,
10
- } from 'react-native';
11
- import Animated from 'react-native-reanimated';
12
- import { Colors } from '../../Consts';
13
- import { ApplicationContext, MiniAppContext } from '../../Context';
14
- import { Icon } from '../../Icon';
15
- import { Text } from '../../Text';
16
- import { FloatingButtonProps } from '../types';
17
- import { TabBarIcon } from './TabBarIcon';
18
- import { LiquidSelection, useLiquidItem } from './hooks';
19
- import { styles } from './DuoTabBar.styles';
20
-
21
- export type Props = BottomTabBarProps & {
22
- activeTintColor: string;
23
- inactiveTintColor: string;
24
- floatingButton?: FloatingButtonProps;
25
- };
26
-
27
- export const BarMaterial: React.FC<{ radius: number }> = ({ radius }) => {
28
- const { theme } = useContext(ApplicationContext);
29
- return (
30
- <View
31
- pointerEvents={'none'}
32
- style={[
33
- StyleSheet.absoluteFill,
34
- styles.barMaterial,
35
- {
36
- borderRadius: radius,
37
- backgroundColor: theme.colors.background.surface,
38
- borderColor: theme.colors.border.default,
39
- },
40
- ]}
41
- />
42
- );
43
- };
44
-
45
- // Figma Home-2026 tab bar "Selection": brand pink at 8%
46
- const SELECTION_TINT_ALPHA = '14';
47
-
48
- /**
49
- * pink tinted capsule under the selected tab
50
- */
51
- export const SelectionMaterial: React.FC<{ radius: number }> = ({ radius }) => {
52
- const { theme } = useContext(ApplicationContext);
53
- return (
54
- <View
55
- pointerEvents={'none'}
56
- style={[
57
- StyleSheet.absoluteFill,
58
- {
59
- borderRadius: radius,
60
- backgroundColor: theme.colors.primary + SELECTION_TINT_ALPHA,
61
- },
62
- ]}
63
- />
64
- );
65
- };
66
-
67
- /**
68
- * tab label at its design size: like the system tab bar it doesn't grow with the wide
69
- * Duo displays, which Text otherwise scales up to 5pt
70
- */
71
- export const TabLabel: React.FC<{
72
- label: string;
73
- color: string;
74
- typography: 'label_xs_medium';
75
- }> = ({ label, color, typography }) => {
76
- const context = useContext<any>(MiniAppContext);
77
- return (
78
- <MiniAppContext.Provider
79
- value={{
80
- ...context,
81
- fontScale: { useOSScaleRate: false, userScaleRate: 1 },
82
- }}
83
- >
84
- <Text numberOfLines={1} typography={typography} color={color}>
85
- {label}
86
- </Text>
87
- </MiniAppContext.Provider>
88
- );
89
- };
90
-
91
- export const getTabItems = ({
92
- state,
93
- descriptors,
94
- navigation,
95
- activeTintColor,
96
- inactiveTintColor,
97
- }: Props) => {
98
- return state.routes.map((route: Route<string>, index: number) => {
99
- const { options } = descriptors[route.key];
100
- const focused = index === state.index;
101
- const label = options.title ?? route.name;
102
-
103
- const onPress = () => {
104
- const event = navigation.emit({
105
- type: 'tabPress',
106
- target: route.key,
107
- canPreventDefault: true,
108
- });
109
- if (!focused && !event.defaultPrevented) {
110
- navigation.dispatch({
111
- ...CommonActions.navigate(route.name),
112
- target: state.key,
113
- });
114
- }
115
- };
116
-
117
- const onLongPress = () => {
118
- navigation.emit({ type: 'tabLongPress', target: route.key });
119
- };
120
-
121
- const renderIcon = (active: boolean) =>
122
- options.tabBarIcon && (
123
- <TabBarIcon
124
- route={route}
125
- badge={options.tabBarBadge}
126
- badgeStyle={options.tabBarBadgeStyle}
127
- activeOpacity={active ? 1 : 0}
128
- inactiveOpacity={active ? 0 : 1}
129
- activeTintColor={activeTintColor}
130
- inactiveTintColor={inactiveTintColor}
131
- renderIcon={options.tabBarIcon}
132
- style={styles.icon}
133
- />
134
- );
135
-
136
- return { route, focused, label, onPress, onLongPress, renderIcon };
137
- });
138
- };
139
-
140
- export const QrIcon: React.FC<{
141
- icon: FloatingButtonProps['icon'];
142
- size: number;
143
- }> = ({ icon, size }) => {
144
- if (typeof icon === 'string') {
145
- return <Icon source={icon} size={size} color={Colors.black_01} />;
146
- }
147
- return <>{icon}</>;
148
- };
149
-
150
- export type TabItem = ReturnType<typeof getTabItems>[number];
151
-
152
- /**
153
- * tab item: a tap is handled here as before, a drag over the bar is handled by the bar's
154
- * gesture. The active layer fades in as the lens passes over it
155
- */
156
- export const LiquidTabItem: React.FC<{
157
- item: TabItem;
158
- itemIndex: number;
159
- selection: LiquidSelection;
160
- onSelect: () => void;
161
- renderContent: (active: boolean) => ReactNode;
162
- style: StyleProp<ViewStyle>;
163
- }> = ({ item, itemIndex, selection, onSelect, renderContent, style }) => {
164
- const { activeStyle, popStyle } = useLiquidItem(selection, itemIndex);
165
- const activeLayerStyle = useMemo(
166
- () => [StyleSheet.absoluteFill, styles.itemLayer, activeStyle],
167
- [activeStyle],
168
- );
169
- // like the system tab bar, a held tab is still picked when released
170
- const longPressed = useRef(false);
171
-
172
- return (
173
- <TouchableOpacity
174
- accessibilityRole={'tab'}
175
- accessibilityLabel={item.label}
176
- accessibilityState={{ selected: item.focused }}
177
- activeOpacity={1}
178
- onPressIn={() => {
179
- longPressed.current = false;
180
- selection.handlePressIn(itemIndex);
181
- }}
182
- onPressOut={() => {
183
- if (selection.handlePressOut() && longPressed.current) {
184
- onSelect();
185
- }
186
- }}
187
- onPress={onSelect}
188
- onLongPress={() => {
189
- longPressed.current = true;
190
- item.onLongPress();
191
- }}
192
- style={style}
193
- >
194
- <Animated.View style={popStyle}>
195
- <View style={styles.itemLayer}>{renderContent(false)}</View>
196
- <Animated.View style={activeLayerStyle}>
197
- {renderContent(true)}
198
- </Animated.View>
199
- </Animated.View>
200
- </TouchableOpacity>
201
- );
202
- };
@@ -1,41 +0,0 @@
1
- import { Colors } from '../../Consts';
2
-
3
- export const getBadgeProps = (itemBadgeLabel: string | number | undefined) => {
4
- const isNum = !isNaN(parseInt(`${itemBadgeLabel}`, 10));
5
- const isDot = typeof itemBadgeLabel === 'string' && !itemBadgeLabel;
6
- const isNumZero = isNum && Number(itemBadgeLabel) === 0;
7
-
8
- let badgeLabel = itemBadgeLabel;
9
- if (isNumZero) {
10
- badgeLabel = undefined;
11
- }
12
-
13
- let badgeStyle: {
14
- marginRight: number;
15
- top: number;
16
- backgroundColor: string;
17
- width?: number;
18
- } = {
19
- marginRight: -8,
20
- top: -4,
21
- backgroundColor: Colors.orange_03,
22
- };
23
-
24
- if (isNum) {
25
- badgeStyle = {
26
- marginRight: -4,
27
- top: -4,
28
- backgroundColor: Colors.red_03,
29
- };
30
- }
31
- if (isDot) {
32
- badgeStyle = {
33
- marginRight: -2,
34
- top: -2,
35
- backgroundColor: Colors.orange_03,
36
- width: 12,
37
- };
38
- }
39
-
40
- return { badgeLabel, badgeStyle };
41
- };