@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.
- package/Application/BottomSheet.hooks.ts +17 -37
- package/Application/BottomSheet.parts.tsx +81 -165
- package/Application/BottomSheet.styles.ts +7 -23
- package/Application/BottomSheet.tsx +15 -67
- package/Application/BottomTab/ActiveIndicator.tsx +90 -0
- package/Application/BottomTab/BottomTabBar.styles.ts +48 -43
- package/Application/BottomTab/BottomTabBar.tsx +229 -223
- package/Application/BottomTab/BottomTabInset.tsx +14 -0
- package/Application/BottomTab/CurvedContainer.tsx +153 -0
- package/Application/BottomTab/CustomBottomTabItem.tsx +132 -108
- package/Application/BottomTab/DuoBarCompression.tsx +4 -12
- package/Application/BottomTab/DuoRailTabBar.tsx +174 -96
- package/Application/BottomTab/DuoTabBar.styles.ts +42 -84
- package/Application/BottomTab/DuoTabBar.tsx +35 -115
- package/Application/BottomTab/TabBarIcon.tsx +160 -72
- package/Application/BottomTab/TabScreen.tsx +48 -61
- package/Application/BottomTab/index.tsx +85 -125
- package/Application/BottomTab/metrics.ts +164 -0
- package/Application/BottomTab/tabItems.ts +46 -0
- package/Application/BottomTab/usePressLift.ts +32 -0
- package/Application/ModalIconButton.tsx +66 -0
- package/Application/ModalScreen.tsx +32 -15
- package/Application/__tests__/modalLayout.test.ts +128 -0
- package/Application/index.ts +10 -0
- package/Application/modalLayout.ts +177 -0
- package/Application/types.tabs.ts +5 -0
- package/Application/utils.ts +0 -36
- package/Layout/ReservedRegions.tsx +21 -4
- package/Layout/Screen.tsx +11 -2
- package/Layout/index.ts +2 -0
- package/Popup/PopupNotify.tsx +38 -68
- package/Popup/PopupPromotion.tsx +5 -4
- package/Popup/utils.ts +1 -26
- package/package.json +2 -1
- package/Application/BottomTab/DuoTabBar.parts.tsx +0 -202
- package/Application/BottomTab/badge.ts +0 -41
- package/Application/BottomTab/hooks.ts +0 -358
- package/Application/BottomTab/index.styles.ts +0 -30
|
@@ -1,270 +1,276 @@
|
|
|
1
1
|
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
Platform,
|
|
6
|
-
TouchableOpacity,
|
|
7
|
-
useWindowDimensions,
|
|
8
|
-
View,
|
|
9
|
-
} from 'react-native';
|
|
2
|
+
import React, { useContext, useState } from 'react';
|
|
3
|
+
import { Platform, Pressable, useWindowDimensions, View } from 'react-native';
|
|
10
4
|
import Animated, {
|
|
11
5
|
useAnimatedStyle,
|
|
12
6
|
useSharedValue,
|
|
13
7
|
withTiming,
|
|
14
8
|
} from 'react-native-reanimated';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
9
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
10
|
+
import { Colors, Spacing } from '../../Consts';
|
|
17
11
|
import { Icon } from '../../Icon';
|
|
18
12
|
import { Text } from '../../Text';
|
|
19
13
|
import { FloatingButtonProps } from '../types';
|
|
20
14
|
import CustomBottomTabItem from './CustomBottomTabItem';
|
|
21
15
|
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
16
|
+
import { CurvedContainer } from './CurvedContainer';
|
|
17
|
+
import { ActiveIndicator } from './ActiveIndicator';
|
|
18
|
+
import { getHorizontalBarWidth, TabBarMetrics, TabBarMotion } from './metrics';
|
|
19
|
+
import { getTabItems, TabItem } from './tabItems';
|
|
20
|
+
import { FixedScaleText } from './TabBarIcon';
|
|
22
21
|
import { styles } from './BottomTabBar.styles';
|
|
23
22
|
|
|
24
23
|
type Props = BottomTabBarProps & {
|
|
25
|
-
activeTintColor
|
|
26
|
-
inactiveTintColor
|
|
24
|
+
activeTintColor: string;
|
|
25
|
+
inactiveTintColor: string;
|
|
27
26
|
floatingButton?: FloatingButtonProps;
|
|
28
|
-
activeBackgroundColor?: string;
|
|
29
|
-
allowFontScaling?: boolean;
|
|
30
|
-
inactiveBackgroundColor?: string;
|
|
31
|
-
labelStyle?: any;
|
|
32
|
-
iconStyle?: any;
|
|
33
|
-
showLabel?: boolean;
|
|
34
|
-
style?: any;
|
|
35
|
-
tabStyle?: any;
|
|
36
|
-
badgeStyle?: any;
|
|
37
27
|
};
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
/**
|
|
30
|
+
* the center action needs a tab on each side: Special type takes 2 or 4 tabs
|
|
31
|
+
*/
|
|
32
|
+
const hasCenterAction = (floatingButton: unknown, tabCount: number) =>
|
|
33
|
+
!!floatingButton && tabCount % 2 === 0;
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
iconStyle,
|
|
54
|
-
showLabel,
|
|
55
|
-
style,
|
|
56
|
-
floatingButton,
|
|
57
|
-
}: Props) {
|
|
58
|
-
const { theme } = useContext(ApplicationContext);
|
|
59
|
-
const { routes } = state;
|
|
60
|
-
const widthDevice = useWindowDimensions()?.width;
|
|
61
|
-
const insets = useSafeAreaInsets();
|
|
62
|
-
const useFloating = floatingButton && state.routes.length % 2 === 0;
|
|
63
|
-
const itemWidth =
|
|
64
|
-
(widthDevice - insets.left - insets.right) /
|
|
65
|
-
(state.routes.length + (useFloating ? 1 : 0));
|
|
66
|
-
// const buildLink = useLinkBuilder();
|
|
67
|
-
const indicatorAnimated = useSharedValue(0);
|
|
35
|
+
/**
|
|
36
|
+
* gap under the container: the home indicator / gesture bar area
|
|
37
|
+
*/
|
|
38
|
+
const getSystemIndicatorHeight = (bottomInset: number) =>
|
|
39
|
+
Math.max(
|
|
40
|
+
Platform.OS === 'ios'
|
|
41
|
+
? Math.min(bottomInset, TabBarMetrics.systemIndicatorHeight)
|
|
42
|
+
: bottomInset,
|
|
43
|
+
Spacing.S,
|
|
44
|
+
);
|
|
68
45
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
46
|
+
/**
|
|
47
|
+
* center action press: the button shrinks while held; the label stays plain text
|
|
48
|
+
*/
|
|
49
|
+
const useCenterActionPress = () => {
|
|
50
|
+
const scale = useSharedValue(1);
|
|
51
|
+
const buttonStyle = useAnimatedStyle(() => ({
|
|
52
|
+
transform: [{ scale: scale.value }],
|
|
72
53
|
}));
|
|
73
|
-
const
|
|
74
|
-
|
|
54
|
+
const onPressIn = () => {
|
|
55
|
+
scale.value = withTiming(
|
|
56
|
+
TabBarMotion.ctaPressedScale,
|
|
57
|
+
TabBarMotion.ctaPress,
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
const onPressOut = () => {
|
|
61
|
+
scale.value = withTiming(1, TabBarMotion.ctaPress);
|
|
62
|
+
};
|
|
63
|
+
return { buttonStyle, onPressIn, onPressOut };
|
|
64
|
+
};
|
|
75
65
|
|
|
66
|
+
/**
|
|
67
|
+
* center action (.Nav/CTA): button rising out of the notch, label under it
|
|
68
|
+
*/
|
|
69
|
+
const CenterAction: React.FC<{
|
|
70
|
+
floatingButton: FloatingButtonProps;
|
|
71
|
+
fixedFontScale?: boolean;
|
|
72
|
+
}> = ({ floatingButton, fixedFontScale }) => {
|
|
73
|
+
const { theme } = useContext(ApplicationContext);
|
|
76
74
|
const context = useContext<any>(MiniAppContext);
|
|
77
75
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const onPress = () => {
|
|
89
|
-
const event = navigation.emit({
|
|
90
|
-
type: 'tabPress',
|
|
91
|
-
target: route.key,
|
|
92
|
-
canPreventDefault: true,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
if (!focused && !event.defaultPrevented) {
|
|
96
|
-
navigation.dispatch({
|
|
97
|
-
...CommonActions.navigate(route.name),
|
|
98
|
-
target: state.key,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const onLongPress = () => {
|
|
104
|
-
navigation.emit({
|
|
105
|
-
type: 'tabLongPress',
|
|
106
|
-
target: route.key,
|
|
107
|
-
});
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
return (
|
|
111
|
-
<CustomBottomTabItem
|
|
112
|
-
key={route.key}
|
|
113
|
-
route={route}
|
|
114
|
-
label={options.tabBarLabel || options.title || route.name}
|
|
115
|
-
focused={focused}
|
|
116
|
-
onPress={onPress}
|
|
117
|
-
onLongPress={onLongPress}
|
|
118
|
-
activeTintColor={activeTintColor ?? theme.colors.primary}
|
|
119
|
-
inactiveTintColor={inactiveTintColor ?? theme.colors.text.default}
|
|
120
|
-
activeBackgroundColor={activeBackgroundColor}
|
|
121
|
-
inactiveBackgroundColor={inactiveBackgroundColor}
|
|
122
|
-
icon={options.tabBarIcon}
|
|
123
|
-
badge={options.tabBarBadge}
|
|
124
|
-
showLabel={showLabel}
|
|
125
|
-
labelStyle={labelStyle}
|
|
126
|
-
iconStyle={iconStyle}
|
|
127
|
-
style={options.tabBarStyle}
|
|
128
|
-
badgeStyle={options.tabBarBadgeStyle}
|
|
129
|
-
allowFontScaling={allowFontScaling}
|
|
130
|
-
/>
|
|
131
|
-
);
|
|
132
|
-
});
|
|
133
|
-
},
|
|
134
|
-
[
|
|
135
|
-
focusedTab,
|
|
136
|
-
descriptors,
|
|
137
|
-
activeTintColor,
|
|
138
|
-
theme.colors.primary,
|
|
139
|
-
theme.colors.text.default,
|
|
140
|
-
inactiveTintColor,
|
|
141
|
-
activeBackgroundColor,
|
|
142
|
-
inactiveBackgroundColor,
|
|
143
|
-
showLabel,
|
|
144
|
-
labelStyle,
|
|
145
|
-
iconStyle,
|
|
146
|
-
allowFontScaling,
|
|
147
|
-
navigation,
|
|
148
|
-
state.key,
|
|
149
|
-
],
|
|
76
|
+
const { icon, label, onPress, labelContent } = floatingButton;
|
|
77
|
+
const { buttonStyle, onPressIn, onPressOut } = useCenterActionPress();
|
|
78
|
+
const labelText = (
|
|
79
|
+
<Text
|
|
80
|
+
numberOfLines={1}
|
|
81
|
+
typography={'label_s_medium'}
|
|
82
|
+
color={theme.colors.text.secondary}
|
|
83
|
+
>
|
|
84
|
+
{label}
|
|
85
|
+
</Text>
|
|
150
86
|
);
|
|
151
87
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
88
|
+
return (
|
|
89
|
+
<View style={[styles.ctaSlot, showBaseLineDebug && styles.debugBaseLine]}>
|
|
90
|
+
<Pressable
|
|
91
|
+
accessibilityRole={'button'}
|
|
92
|
+
accessibilityLabel={label}
|
|
93
|
+
onPress={onPress}
|
|
94
|
+
onPressIn={onPressIn}
|
|
95
|
+
onPressOut={onPressOut}
|
|
96
|
+
style={styles.cta}
|
|
97
|
+
>
|
|
98
|
+
<Animated.View
|
|
99
|
+
style={[
|
|
100
|
+
styles.ctaButton,
|
|
101
|
+
{ backgroundColor: theme.colors.primary },
|
|
102
|
+
buttonStyle,
|
|
103
|
+
]}
|
|
168
104
|
>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
activeTintColor,
|
|
196
|
-
floatingButton?.icon,
|
|
197
|
-
floatingButton?.label,
|
|
198
|
-
floatingButton?.onPress,
|
|
199
|
-
showBaseLineDebug,
|
|
200
|
-
theme.colors.primary,
|
|
201
|
-
]);
|
|
105
|
+
{typeof icon === 'string' ? (
|
|
106
|
+
<Icon
|
|
107
|
+
source={icon}
|
|
108
|
+
size={TabBarMetrics.ctaIconSize}
|
|
109
|
+
color={Colors.black_01}
|
|
110
|
+
/>
|
|
111
|
+
) : (
|
|
112
|
+
icon
|
|
113
|
+
)}
|
|
114
|
+
</Animated.View>
|
|
115
|
+
<View style={styles.ctaLabel}>
|
|
116
|
+
{fixedFontScale ? (
|
|
117
|
+
<FixedScaleText>{labelText}</FixedScaleText>
|
|
118
|
+
) : (
|
|
119
|
+
labelText
|
|
120
|
+
)}
|
|
121
|
+
{labelContent && (
|
|
122
|
+
<View pointerEvents={'none'} style={styles.ctaLabelOverlay}>
|
|
123
|
+
{labelContent}
|
|
124
|
+
</View>
|
|
125
|
+
)}
|
|
126
|
+
</View>
|
|
127
|
+
</Pressable>
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
202
131
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
132
|
+
/**
|
|
133
|
+
* horizontal navigation bar: floating container, items split evenly, Special type puts
|
|
134
|
+
* the center action in a notch in the middle
|
|
135
|
+
*/
|
|
136
|
+
const HorizontalTabBar: React.FC<{
|
|
137
|
+
items: TabItem[];
|
|
138
|
+
width: number;
|
|
139
|
+
activeTintColor: string;
|
|
140
|
+
inactiveTintColor: string;
|
|
141
|
+
floatingButton?: FloatingButtonProps;
|
|
142
|
+
fixedFontScale?: boolean;
|
|
143
|
+
}> = ({
|
|
144
|
+
items,
|
|
145
|
+
width,
|
|
146
|
+
activeTintColor,
|
|
147
|
+
inactiveTintColor,
|
|
148
|
+
floatingButton,
|
|
149
|
+
fixedFontScale,
|
|
150
|
+
}) => {
|
|
151
|
+
const withCta = hasCenterAction(floatingButton, items.length);
|
|
152
|
+
const [layouts, setLayouts] = useState<
|
|
153
|
+
Record<string, { x: number; y: number; width: number }>
|
|
154
|
+
>({});
|
|
155
|
+
const selected = layouts[items.find(item => item.focused)?.route.key ?? ''];
|
|
156
|
+
|
|
157
|
+
const renderItems = (data: TabItem[]) =>
|
|
158
|
+
data.map(item => (
|
|
159
|
+
<CustomBottomTabItem
|
|
160
|
+
key={item.route.key}
|
|
161
|
+
onLayout={e => {
|
|
162
|
+
const { x, y, width: itemWidth } = e.nativeEvent.layout;
|
|
163
|
+
setLayouts(prev => ({
|
|
164
|
+
...prev,
|
|
165
|
+
[item.route.key]: { x, y, width: itemWidth },
|
|
166
|
+
}));
|
|
167
|
+
}}
|
|
168
|
+
label={item.label}
|
|
169
|
+
focused={item.focused}
|
|
170
|
+
icon={item.icon}
|
|
171
|
+
badge={item.badge}
|
|
172
|
+
onPress={item.onPress}
|
|
173
|
+
onLongPress={item.onLongPress}
|
|
174
|
+
activeTintColor={activeTintColor}
|
|
175
|
+
inactiveTintColor={inactiveTintColor}
|
|
176
|
+
fixedFontScale={fixedFontScale}
|
|
177
|
+
/>
|
|
178
|
+
));
|
|
219
179
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const renderTabBar = () => {
|
|
224
|
-
if (useFloating) {
|
|
225
|
-
const middleIndex = routes.length / 2;
|
|
226
|
-
const leftArr = routes.slice(0, middleIndex);
|
|
227
|
-
const rightArr = routes.slice(middleIndex);
|
|
180
|
+
const renderContent = () => {
|
|
181
|
+
if (withCta && floatingButton) {
|
|
182
|
+
const middle = items.length / 2;
|
|
228
183
|
return (
|
|
229
184
|
<>
|
|
230
|
-
{
|
|
231
|
-
|
|
232
|
-
|
|
185
|
+
{renderItems(items.slice(0, middle))}
|
|
186
|
+
<CenterAction
|
|
187
|
+
floatingButton={floatingButton}
|
|
188
|
+
fixedFontScale={fixedFontScale}
|
|
189
|
+
/>
|
|
190
|
+
{renderItems(items.slice(middle))}
|
|
233
191
|
</>
|
|
234
192
|
);
|
|
235
193
|
}
|
|
236
|
-
return
|
|
194
|
+
return renderItems(items);
|
|
237
195
|
};
|
|
238
196
|
|
|
197
|
+
const customContainer = withCta && floatingButton?.container;
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<View
|
|
201
|
+
style={[styles.container, { width }]}
|
|
202
|
+
accessibilityLabel={'bottom_tab_bar'}
|
|
203
|
+
>
|
|
204
|
+
{customContainer || (
|
|
205
|
+
<CurvedContainer
|
|
206
|
+
width={width}
|
|
207
|
+
height={TabBarMetrics.height}
|
|
208
|
+
radius={TabBarMetrics.height / 2}
|
|
209
|
+
notchCenterX={withCta ? width / 2 : undefined}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
<View style={styles.content}>
|
|
213
|
+
<ActiveIndicator
|
|
214
|
+
axis={'x'}
|
|
215
|
+
offset={selected?.x}
|
|
216
|
+
extent={selected?.width}
|
|
217
|
+
radius={TabBarMetrics.indicatorRadius}
|
|
218
|
+
style={[
|
|
219
|
+
styles.indicator,
|
|
220
|
+
{ top: selected?.y ?? TabBarMetrics.paddingVertical },
|
|
221
|
+
]}
|
|
222
|
+
/>
|
|
223
|
+
{renderContent()}
|
|
224
|
+
</View>
|
|
225
|
+
</View>
|
|
226
|
+
);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* phone tab bar: the horizontal navigation bar floating over the tab content above the home
|
|
231
|
+
* indicator, so the content shows around it and through the notch. The transparent strip
|
|
232
|
+
* above it keeps the rising center action touchable
|
|
233
|
+
*/
|
|
234
|
+
export default function BottomTabBar(props: Props) {
|
|
235
|
+
const { state, floatingButton, activeTintColor, inactiveTintColor } = props;
|
|
236
|
+
const window = useWindowDimensions();
|
|
237
|
+
const insets = useSafeAreaInsets();
|
|
238
|
+
const withCta = hasCenterAction(floatingButton, state.routes.length);
|
|
239
|
+
const width = getHorizontalBarWidth(
|
|
240
|
+
window.width,
|
|
241
|
+
window.height,
|
|
242
|
+
insets.left,
|
|
243
|
+
insets.right,
|
|
244
|
+
);
|
|
245
|
+
|
|
239
246
|
return (
|
|
240
247
|
<View
|
|
248
|
+
pointerEvents={'box-none'}
|
|
241
249
|
style={[
|
|
242
|
-
|
|
250
|
+
styles.bar,
|
|
251
|
+
withCta && styles.barCtaRise,
|
|
243
252
|
{
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
paddingBottom: paddingBottom,
|
|
253
|
+
paddingBottom: getSystemIndicatorHeight(insets.bottom),
|
|
254
|
+
// centered between the side safe areas
|
|
249
255
|
paddingLeft: insets.left,
|
|
250
256
|
paddingRight: insets.right,
|
|
251
257
|
},
|
|
252
|
-
style,
|
|
253
258
|
]}
|
|
254
|
-
pointerEvents={'auto'}
|
|
255
259
|
>
|
|
256
|
-
<
|
|
257
|
-
{
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
]}
|
|
264
|
-
/>
|
|
265
|
-
</Animated.View>
|
|
266
|
-
{renderTabBar()}
|
|
267
|
-
</View>
|
|
260
|
+
<HorizontalTabBar
|
|
261
|
+
items={getTabItems(props)}
|
|
262
|
+
width={width}
|
|
263
|
+
floatingButton={floatingButton}
|
|
264
|
+
activeTintColor={activeTintColor}
|
|
265
|
+
inactiveTintColor={inactiveTintColor}
|
|
266
|
+
/>
|
|
268
267
|
</View>
|
|
269
268
|
);
|
|
270
269
|
}
|
|
270
|
+
|
|
271
|
+
export {
|
|
272
|
+
HorizontalTabBar,
|
|
273
|
+
getSystemIndicatorHeight,
|
|
274
|
+
hasCenterAction,
|
|
275
|
+
useCenterActionPress,
|
|
276
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
type BottomTabInset = {
|
|
4
|
+
// space the floating tab bar takes over the bottom of a tab's content (bar + home
|
|
5
|
+
// indicator area, plus the rising center action when shown); 0 on the iPhone Duo rail
|
|
6
|
+
inset: number;
|
|
7
|
+
// a Screen in the tab keeps its content clear of the bar itself (inside its scroll view,
|
|
8
|
+
// or above the bar when static), so the tab stops padding its content
|
|
9
|
+
claim?: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const BottomTabInsetContext = createContext<BottomTabInset>({ inset: 0 });
|
|
13
|
+
|
|
14
|
+
export { BottomTabInsetContext };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import Svg, { Defs, FeGaussianBlur, Filter, Path } from 'react-native-svg';
|
|
4
|
+
import { Colors } from '../../Consts';
|
|
5
|
+
import { ApplicationContext } from '../../Context';
|
|
6
|
+
import { NOTCH_CURVES, NOTCH_HALF_WIDTH } from './metrics';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
radius: number;
|
|
12
|
+
// x of the center action, the top edge is cut around it
|
|
13
|
+
notchCenterX?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// room for the floating shadow around the shape: the key shadow reaches 12 + 3 × 16 below it
|
|
17
|
+
const SHADOW_PADDING = 64;
|
|
18
|
+
// Figma Shadow/floating: 12% blur 24, plus 4% blur 32 spread -16 offset 12; svg blur takes
|
|
19
|
+
// half the CSS blur radius, the spread shrinks the shape the key shadow is cast from
|
|
20
|
+
const AMBIENT_BLUR = 12;
|
|
21
|
+
const KEY_BLUR = 16;
|
|
22
|
+
const KEY_SPREAD = 16;
|
|
23
|
+
const KEY_OFFSET_Y = 12;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* rounded container path placed at (`x`, `y`) on the canvas, with the notch dipped into the
|
|
27
|
+
* top edge around `notchCenterX`
|
|
28
|
+
*/
|
|
29
|
+
const getContainerPath = (
|
|
30
|
+
width: number,
|
|
31
|
+
height: number,
|
|
32
|
+
radius: number,
|
|
33
|
+
x: number,
|
|
34
|
+
y: number,
|
|
35
|
+
notchCenterX?: number,
|
|
36
|
+
) => {
|
|
37
|
+
const r = Math.max(Math.min(radius, height / 2, width / 2), 0);
|
|
38
|
+
const left = x;
|
|
39
|
+
const top = y;
|
|
40
|
+
const right = x + width;
|
|
41
|
+
const bottom = y + height;
|
|
42
|
+
let notch = '';
|
|
43
|
+
if (notchCenterX !== undefined) {
|
|
44
|
+
const cx = x + notchCenterX;
|
|
45
|
+
const point = ([dx, dy]: [number, number]) => `${cx + dx} ${top + dy}`;
|
|
46
|
+
notch =
|
|
47
|
+
` L ${cx - NOTCH_HALF_WIDTH} ${top}` +
|
|
48
|
+
NOTCH_CURVES.map(
|
|
49
|
+
([c1, c2, end]) => ` C ${point(c1)} ${point(c2)} ${point(end)}`,
|
|
50
|
+
).join('');
|
|
51
|
+
}
|
|
52
|
+
return (
|
|
53
|
+
`M ${left + r} ${top}` +
|
|
54
|
+
notch +
|
|
55
|
+
` L ${right - r} ${top}` +
|
|
56
|
+
` A ${r} ${r} 0 0 1 ${right} ${top + r}` +
|
|
57
|
+
` L ${right} ${bottom - r}` +
|
|
58
|
+
` A ${r} ${r} 0 0 1 ${right - r} ${bottom}` +
|
|
59
|
+
` L ${left + r} ${bottom}` +
|
|
60
|
+
` A ${r} ${r} 0 0 1 ${left} ${bottom - r}` +
|
|
61
|
+
` L ${left} ${top + r}` +
|
|
62
|
+
` A ${r} ${r} 0 0 1 ${left + r} ${top} Z`
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* navigation bar container: surface fill with the floating shadow, drawn as a shape so
|
|
68
|
+
* the shadow follows the notch
|
|
69
|
+
*/
|
|
70
|
+
const CurvedContainer: React.FC<Props> = ({
|
|
71
|
+
width,
|
|
72
|
+
height,
|
|
73
|
+
radius,
|
|
74
|
+
notchCenterX,
|
|
75
|
+
}) => {
|
|
76
|
+
const { theme } = useContext(ApplicationContext);
|
|
77
|
+
if (width <= 0 || height <= 0) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const canvasWidth = width + 2 * SHADOW_PADDING;
|
|
81
|
+
const canvasHeight = height + 2 * SHADOW_PADDING;
|
|
82
|
+
const path = getContainerPath(
|
|
83
|
+
width,
|
|
84
|
+
height,
|
|
85
|
+
radius,
|
|
86
|
+
SHADOW_PADDING,
|
|
87
|
+
SHADOW_PADDING,
|
|
88
|
+
notchCenterX,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<Svg
|
|
93
|
+
pointerEvents={'none'}
|
|
94
|
+
width={canvasWidth}
|
|
95
|
+
height={canvasHeight}
|
|
96
|
+
style={styles.canvas}
|
|
97
|
+
>
|
|
98
|
+
<Defs>
|
|
99
|
+
<Filter
|
|
100
|
+
id={'ambientShadow'}
|
|
101
|
+
filterUnits={'userSpaceOnUse'}
|
|
102
|
+
x={0}
|
|
103
|
+
y={0}
|
|
104
|
+
width={canvasWidth}
|
|
105
|
+
height={canvasHeight}
|
|
106
|
+
>
|
|
107
|
+
<FeGaussianBlur in={'SourceAlpha'} stdDeviation={AMBIENT_BLUR} />
|
|
108
|
+
</Filter>
|
|
109
|
+
<Filter
|
|
110
|
+
id={'keyShadow'}
|
|
111
|
+
filterUnits={'userSpaceOnUse'}
|
|
112
|
+
x={0}
|
|
113
|
+
y={0}
|
|
114
|
+
width={canvasWidth}
|
|
115
|
+
height={canvasHeight}
|
|
116
|
+
>
|
|
117
|
+
<FeGaussianBlur in={'SourceAlpha'} stdDeviation={KEY_BLUR} />
|
|
118
|
+
</Filter>
|
|
119
|
+
</Defs>
|
|
120
|
+
<Path
|
|
121
|
+
d={path}
|
|
122
|
+
fill={Colors.black_20}
|
|
123
|
+
fillOpacity={0.12}
|
|
124
|
+
filter={'url(#ambientShadow)'}
|
|
125
|
+
/>
|
|
126
|
+
{width > 2 * KEY_SPREAD && height > 2 * KEY_SPREAD && (
|
|
127
|
+
<Path
|
|
128
|
+
d={getContainerPath(
|
|
129
|
+
width - 2 * KEY_SPREAD,
|
|
130
|
+
height - 2 * KEY_SPREAD,
|
|
131
|
+
radius - KEY_SPREAD,
|
|
132
|
+
SHADOW_PADDING + KEY_SPREAD,
|
|
133
|
+
SHADOW_PADDING + KEY_SPREAD + KEY_OFFSET_Y,
|
|
134
|
+
)}
|
|
135
|
+
fill={Colors.black_20}
|
|
136
|
+
fillOpacity={0.04}
|
|
137
|
+
filter={'url(#keyShadow)'}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
<Path d={path} fill={theme.colors.background.surface} />
|
|
141
|
+
</Svg>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const styles = StyleSheet.create({
|
|
146
|
+
canvas: {
|
|
147
|
+
position: 'absolute',
|
|
148
|
+
left: -SHADOW_PADDING,
|
|
149
|
+
top: -SHADOW_PADDING,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
export { CurvedContainer };
|