@momo-kits/foundation 0.169.1 → 0.169.3-beta.1

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,270 +1,276 @@
1
1
  import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
2
- import { CommonActions, Route } from '@react-navigation/native';
3
- import React, { useCallback, useContext } from 'react';
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 { EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context';
16
- import { Styles } from '../../Consts';
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?: string;
26
- inactiveTintColor?: string;
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
- const getPaddingBottom = (insets: EdgeInsets) => {
40
- return Math.max(insets.bottom - Platform.select({ ios: 4, default: 0 }), 0);
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
- export default function BottomTabBar({
44
- state,
45
- navigation,
46
- descriptors,
47
- activeBackgroundColor,
48
- activeTintColor,
49
- allowFontScaling,
50
- inactiveBackgroundColor,
51
- inactiveTintColor,
52
- labelStyle,
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
- const indicatorStyle = useAnimatedStyle(() => ({
70
- width: itemWidth,
71
- transform: [{ translateX: indicatorAnimated.value }],
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 paddingBottom = getPaddingBottom(insets);
74
- const focusedTab = state?.routes[state.index]?.name || routes[0]?.params;
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
- * render tab item
81
- */
82
- const renderTabItem = useCallback(
83
- (data: Route<string, any>[]) => {
84
- return data?.map(route => {
85
- const focused = route?.params?.name === focusedTab;
86
- const { options } = descriptors[route.key];
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
- * render floating button
154
- */
155
- const renderFloatingButton = useCallback(() => {
156
- const renderFloatingIcon = () => {
157
- if (typeof floatingButton?.icon === 'string') {
158
- return <Icon source={floatingButton?.icon} size={32} color={'white'} />;
159
- }
160
- return floatingButton?.icon;
161
- };
162
-
163
- return (
164
- <View style={[Styles.flex, showBaseLineDebug && styles.debugBaseLine]}>
165
- <TouchableOpacity
166
- onPress={floatingButton?.onPress}
167
- style={styles.floatingContainer}
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
- <View
170
- style={{
171
- ...styles.floatingContent,
172
- backgroundColor: activeTintColor ?? theme.colors.primary,
173
- }}
174
- >
175
- {renderFloatingIcon()}
176
- </View>
177
- <View
178
- style={[
179
- styles.floatingLabel,
180
- { backgroundColor: activeTintColor ?? theme.colors.primary },
181
- ]}
182
- >
183
- <Text
184
- numberOfLines={1}
185
- typography={'description_xs_regular'}
186
- color={'white'}
187
- >
188
- {floatingButton?.label}
189
- </Text>
190
- </View>
191
- </TouchableOpacity>
192
- </View>
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
- * animated indicator
205
- */
206
- React.useEffect(() => {
207
- let index = state.index;
208
- if (useFloating && index > (state.routes.length - 1) / 2) {
209
- index += 1;
210
- }
211
- indicatorAnimated.value = withTiming(itemWidth * index, { duration: 200 });
212
- }, [
213
- state.index,
214
- itemWidth,
215
- indicatorAnimated,
216
- useFloating,
217
- state.routes.length,
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
- * render tab bar
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
- {renderTabItem(leftArr)}
231
- {renderFloatingButton()}
232
- {renderTabItem(rightArr)}
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 renderTabItem(routes);
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
- floatingButton ? styles.tabBarFloatingButton : styles.tabBar,
250
+ styles.bar,
251
+ withCta && styles.barCtaRise,
243
252
  {
244
- backgroundColor: theme.colors.background.surface,
245
- borderTopColor: theme.colors.border.default,
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
- <View style={styles.content} accessibilityLabel={'bottom_tab_bar'}>
257
- {useFloating && floatingButton?.container}
258
- <Animated.View style={[styles.indicatorContainer, indicatorStyle]}>
259
- <View
260
- style={[
261
- styles.indicator,
262
- { backgroundColor: activeTintColor ?? theme.colors.primary },
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 };