@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,41 +1,15 @@
|
|
|
1
1
|
import { StyleSheet } from 'react-native';
|
|
2
|
-
import {
|
|
2
|
+
import { Spacing } from '../../Consts';
|
|
3
3
|
import { DuoMetrics } from '../../Layout/utils';
|
|
4
|
+
import { CTA_SHADOW, TabBarMetrics } from './metrics';
|
|
4
5
|
|
|
5
|
-
export const
|
|
6
|
-
export const
|
|
7
|
-
export const
|
|
8
|
-
export const RAIL_PADDING = 6;
|
|
9
|
-
export const RAIL_GAP = Spacing.XXS;
|
|
10
|
-
export const RAIL_PITCH = ITEM_SIZE + RAIL_GAP;
|
|
11
|
-
// selection bubble overflows the 48 rail cell by 5 above and below (Figma MT2)
|
|
12
|
-
export const RAIL_BUBBLE_WIDTH = 44;
|
|
13
|
-
export const RAIL_BUBBLE_HEIGHT = 58;
|
|
14
|
-
export const FLOATING_HEIGHT = DuoMetrics.floatingTabBarHeight;
|
|
15
|
-
export const FLOATING_PADDING = Spacing.XS;
|
|
16
|
-
// the system bar sizes its pill to the tabs instead of stretching it across the display
|
|
17
|
-
export const FLOATING_ITEM_WIDTH = 80;
|
|
18
|
-
export const FLOATING_BUBBLE_RADIUS =
|
|
19
|
-
(FLOATING_HEIGHT - 2 * FLOATING_PADDING) / 2;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* distance from the screen bottom to the floating tab bar, overlapping the home
|
|
23
|
-
* indicator inset like the system tab bar
|
|
24
|
-
*/
|
|
25
|
-
export const getFloatingTabBarBottom = (bottomInset: number) =>
|
|
26
|
-
Math.max(bottomInset - DuoMetrics.floatingTabBarInsetOverlap, Spacing.S);
|
|
6
|
+
export const RAIL_ITEM_WIDTH = TabBarMetrics.railItemWidth;
|
|
7
|
+
export const RAIL_ITEM_HEIGHT = TabBarMetrics.railItemHeight;
|
|
8
|
+
export const RAIL_COMPACT_SIZE = DuoMetrics.controlSize;
|
|
27
9
|
|
|
28
10
|
export const styles = StyleSheet.create({
|
|
29
|
-
barMaterial: {
|
|
30
|
-
borderWidth: 1,
|
|
31
|
-
},
|
|
32
|
-
icon: {
|
|
33
|
-
width: ICON_SIZE,
|
|
34
|
-
height: ICON_SIZE,
|
|
35
|
-
},
|
|
36
11
|
railColumn: {
|
|
37
12
|
position: 'absolute',
|
|
38
|
-
gap: Spacing.M,
|
|
39
13
|
},
|
|
40
14
|
railColumnRight: {
|
|
41
15
|
alignItems: 'flex-start',
|
|
@@ -45,79 +19,63 @@ export const styles = StyleSheet.create({
|
|
|
45
19
|
alignItems: 'flex-end',
|
|
46
20
|
paddingRight: Spacing.M,
|
|
47
21
|
},
|
|
22
|
+
// as wide as the header buttons; the wider rail and center action overflow it evenly
|
|
23
|
+
railLane: {
|
|
24
|
+
width: DuoMetrics.controlSize,
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
gap: TabBarMetrics.railCtaGap,
|
|
27
|
+
},
|
|
48
28
|
railCompact: {
|
|
49
|
-
width:
|
|
50
|
-
height:
|
|
51
|
-
borderRadius: ITEM_SIZE / 2,
|
|
29
|
+
width: RAIL_COMPACT_SIZE,
|
|
30
|
+
height: RAIL_COMPACT_SIZE,
|
|
52
31
|
alignItems: 'center',
|
|
53
32
|
justifyContent: 'center',
|
|
54
33
|
},
|
|
55
34
|
rail: {
|
|
56
|
-
width:
|
|
57
|
-
|
|
58
|
-
|
|
35
|
+
width: TabBarMetrics.railContainerWidth,
|
|
36
|
+
paddingHorizontal: TabBarMetrics.railPaddingHorizontal,
|
|
37
|
+
paddingBottom: TabBarMetrics.railPaddingBottom,
|
|
59
38
|
},
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
height: RAIL_BUBBLE_HEIGHT,
|
|
39
|
+
railIndicator: {
|
|
40
|
+
top: Spacing.XS,
|
|
41
|
+
left: TabBarMetrics.railPaddingHorizontal,
|
|
42
|
+
width: RAIL_ITEM_WIDTH,
|
|
43
|
+
height: TabBarMetrics.railIndicatorHeight,
|
|
66
44
|
},
|
|
67
45
|
railItem: {
|
|
68
|
-
width:
|
|
69
|
-
height:
|
|
46
|
+
width: RAIL_ITEM_WIDTH,
|
|
47
|
+
height: RAIL_ITEM_HEIGHT,
|
|
70
48
|
alignItems: 'center',
|
|
71
49
|
justifyContent: 'center',
|
|
72
50
|
},
|
|
73
|
-
|
|
74
|
-
width:
|
|
75
|
-
height:
|
|
76
|
-
borderRadius: ITEM_SIZE / 2,
|
|
51
|
+
railCta: {
|
|
52
|
+
width: TabBarMetrics.railCtaWidth,
|
|
53
|
+
height: TabBarMetrics.railCtaHeight,
|
|
77
54
|
alignItems: 'center',
|
|
78
55
|
justifyContent: 'center',
|
|
56
|
+
gap: TabBarMetrics.railCtaLabelGap,
|
|
79
57
|
},
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
justifyContent: 'center',
|
|
84
|
-
gap: Spacing.S,
|
|
85
|
-
},
|
|
86
|
-
floatingBar: {
|
|
87
|
-
flexShrink: 1,
|
|
88
|
-
height: FLOATING_HEIGHT,
|
|
89
|
-
padding: FLOATING_PADDING,
|
|
90
|
-
},
|
|
91
|
-
floatingRow: {
|
|
92
|
-
flex: 1,
|
|
93
|
-
flexDirection: 'row',
|
|
94
|
-
},
|
|
95
|
-
floatingBubble: {
|
|
96
|
-
position: 'absolute',
|
|
97
|
-
top: 0,
|
|
98
|
-
bottom: 0,
|
|
99
|
-
left: 0,
|
|
100
|
-
},
|
|
101
|
-
floatingItem: {
|
|
102
|
-
flex: 1,
|
|
58
|
+
// custom label content over the label text, centered and unclipped
|
|
59
|
+
railCtaLabelOverlay: {
|
|
60
|
+
...StyleSheet.absoluteFillObject,
|
|
103
61
|
alignItems: 'center',
|
|
104
62
|
justifyContent: 'center',
|
|
105
63
|
},
|
|
106
|
-
|
|
64
|
+
railCtaLabel: {
|
|
65
|
+
paddingHorizontal: Spacing.XS,
|
|
66
|
+
},
|
|
67
|
+
railCtaButton: {
|
|
68
|
+
boxShadow: CTA_SHADOW,
|
|
69
|
+
width: TabBarMetrics.railCtaSize,
|
|
70
|
+
height: TabBarMetrics.railCtaSize,
|
|
71
|
+
borderRadius: TabBarMetrics.railCtaSize / 2,
|
|
107
72
|
alignItems: 'center',
|
|
108
73
|
justifyContent: 'center',
|
|
109
|
-
gap: Spacing.XXS,
|
|
110
74
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
borderRadius: FLOATING_HEIGHT / 2,
|
|
75
|
+
floating: {
|
|
76
|
+
position: 'absolute',
|
|
77
|
+
left: 0,
|
|
78
|
+
right: 0,
|
|
116
79
|
alignItems: 'center',
|
|
117
|
-
justifyContent: 'center',
|
|
118
|
-
shadowColor: Colors.black_20,
|
|
119
|
-
shadowOffset: { width: 0, height: 6 },
|
|
120
|
-
shadowOpacity: 0.16,
|
|
121
|
-
shadowRadius: 16,
|
|
122
80
|
},
|
|
123
81
|
});
|
|
@@ -1,145 +1,64 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useWindowDimensions, View } from 'react-native';
|
|
4
|
+
import { useScreenLayout } from '../../Layout/utils';
|
|
5
|
+
import { FloatingButtonProps } from '../types';
|
|
6
|
+
import { getSystemIndicatorHeight, HorizontalTabBar } from './BottomTabBar';
|
|
7
|
+
import { getHorizontalBarWidth, TabBarMetrics } from './metrics';
|
|
8
|
+
import { getTabItems } from './tabItems';
|
|
8
9
|
import { DuoRailTabBar } from './DuoRailTabBar';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
TabLabel,
|
|
17
|
-
} from './DuoTabBar.parts';
|
|
18
|
-
import {
|
|
19
|
-
FLOATING_BUBBLE_RADIUS,
|
|
20
|
-
FLOATING_HEIGHT,
|
|
21
|
-
FLOATING_ITEM_WIDTH,
|
|
22
|
-
FLOATING_PADDING,
|
|
23
|
-
getFloatingTabBarBottom,
|
|
24
|
-
QR_ICON_SIZE,
|
|
25
|
-
styles,
|
|
26
|
-
} from './DuoTabBar.styles';
|
|
10
|
+
import { styles } from './DuoTabBar.styles';
|
|
11
|
+
|
|
12
|
+
type DuoTabBarProps = BottomTabBarProps & {
|
|
13
|
+
activeTintColor: string;
|
|
14
|
+
inactiveTintColor: string;
|
|
15
|
+
floatingButton?: FloatingButtonProps;
|
|
16
|
+
};
|
|
27
17
|
|
|
28
18
|
/**
|
|
29
|
-
* space tab content leaves under the
|
|
19
|
+
* space tab content leaves under the floating horizontal tab bar (phone and unfolded Duo);
|
|
20
|
+
* 0 when the Duo rail sits in the side bar
|
|
30
21
|
*/
|
|
31
22
|
const useDuoTabBarInset = () => {
|
|
32
23
|
const { isIPhoneDuo, sideBar, insets } = useScreenLayout();
|
|
33
|
-
if (
|
|
24
|
+
if (isIPhoneDuo && sideBar) {
|
|
34
25
|
return 0;
|
|
35
26
|
}
|
|
36
|
-
return
|
|
27
|
+
return TabBarMetrics.height + getSystemIndicatorHeight(insets.bottom);
|
|
37
28
|
};
|
|
38
29
|
|
|
39
30
|
/**
|
|
40
|
-
*
|
|
41
|
-
* platter on the trailing side
|
|
31
|
+
* horizontal navigation bar of the unfolded display: 65% of the short side, centered
|
|
42
32
|
*/
|
|
43
|
-
const DuoFloatingTabBar: React.FC<
|
|
44
|
-
const { theme } = useContext(ApplicationContext);
|
|
33
|
+
const DuoFloatingTabBar: React.FC<DuoTabBarProps> = props => {
|
|
45
34
|
const { insets } = useScreenLayout();
|
|
46
|
-
const {
|
|
47
|
-
const
|
|
48
|
-
const [rowWidth, setRowWidth] = useState(0);
|
|
49
|
-
const cellWidth = rowWidth / state.routes.length;
|
|
50
|
-
|
|
51
|
-
const selection = useLiquidSelection({
|
|
52
|
-
count: items.length,
|
|
53
|
-
pitch: cellWidth,
|
|
54
|
-
firstCenter: cellWidth / 2,
|
|
55
|
-
axis: 'x',
|
|
56
|
-
index: state.index,
|
|
57
|
-
onSelect: itemIndex => items[itemIndex]?.onPress(),
|
|
58
|
-
});
|
|
59
|
-
const { barStyle, bubbleStyle } = selection;
|
|
60
|
-
const tabCount = state.routes.length;
|
|
61
|
-
// stable style arrays, see Rail
|
|
62
|
-
const floatingBarStyle = useMemo(
|
|
63
|
-
() => [
|
|
64
|
-
styles.floatingBar,
|
|
65
|
-
{ width: tabCount * FLOATING_ITEM_WIDTH + 2 * FLOATING_PADDING },
|
|
66
|
-
barStyle,
|
|
67
|
-
],
|
|
68
|
-
[tabCount, barStyle],
|
|
69
|
-
);
|
|
70
|
-
const floatingBubbleStyle = useMemo(
|
|
71
|
-
() => [styles.floatingBubble, { width: cellWidth }, bubbleStyle],
|
|
72
|
-
[cellWidth, bubbleStyle],
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
const onRowLayout = (e: LayoutChangeEvent) =>
|
|
76
|
-
setRowWidth(e.nativeEvent.layout.width);
|
|
35
|
+
const { width, height } = useWindowDimensions();
|
|
36
|
+
const { floatingButton, activeTintColor, inactiveTintColor } = props;
|
|
77
37
|
|
|
78
38
|
return (
|
|
79
39
|
<View
|
|
80
40
|
pointerEvents={'box-none'}
|
|
81
41
|
style={[
|
|
82
42
|
styles.floating,
|
|
83
|
-
{
|
|
84
|
-
left: insets.left + DuoMetrics.floatingTabBarMargin,
|
|
85
|
-
right: insets.right + DuoMetrics.floatingTabBarMargin,
|
|
86
|
-
bottom: getFloatingTabBarBottom(insets.bottom),
|
|
87
|
-
},
|
|
43
|
+
{ bottom: getSystemIndicatorHeight(insets.bottom) },
|
|
88
44
|
]}
|
|
89
45
|
>
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<Animated.View pointerEvents={'none'} style={floatingBubbleStyle}>
|
|
99
|
-
<SelectionMaterial radius={FLOATING_BUBBLE_RADIUS} />
|
|
100
|
-
</Animated.View>
|
|
101
|
-
)}
|
|
102
|
-
{items.map((item, index) => (
|
|
103
|
-
<LiquidTabItem
|
|
104
|
-
key={item.route.key}
|
|
105
|
-
item={item}
|
|
106
|
-
itemIndex={index}
|
|
107
|
-
selection={selection}
|
|
108
|
-
onSelect={item.onPress}
|
|
109
|
-
renderContent={active => (
|
|
110
|
-
<>
|
|
111
|
-
{item.renderIcon(active)}
|
|
112
|
-
<TabLabel
|
|
113
|
-
label={item.label}
|
|
114
|
-
typography={'label_xs_medium'}
|
|
115
|
-
color={active ? activeTintColor : inactiveTintColor}
|
|
116
|
-
/>
|
|
117
|
-
</>
|
|
118
|
-
)}
|
|
119
|
-
style={styles.floatingItem}
|
|
120
|
-
/>
|
|
121
|
-
))}
|
|
122
|
-
</View>
|
|
123
|
-
</GestureDetector>
|
|
124
|
-
</Animated.View>
|
|
125
|
-
{floatingButton && (
|
|
126
|
-
<TouchableOpacity
|
|
127
|
-
accessibilityRole={'button'}
|
|
128
|
-
accessibilityLabel={floatingButton.label}
|
|
129
|
-
onPress={floatingButton.onPress}
|
|
130
|
-
style={[styles.floatingQr, { backgroundColor: theme.colors.primary }]}
|
|
131
|
-
>
|
|
132
|
-
<QrIcon icon={floatingButton.icon} size={QR_ICON_SIZE} />
|
|
133
|
-
</TouchableOpacity>
|
|
134
|
-
)}
|
|
46
|
+
<HorizontalTabBar
|
|
47
|
+
fixedFontScale
|
|
48
|
+
items={getTabItems(props)}
|
|
49
|
+
width={getHorizontalBarWidth(width, height, insets.left, insets.right)}
|
|
50
|
+
floatingButton={floatingButton}
|
|
51
|
+
activeTintColor={activeTintColor}
|
|
52
|
+
inactiveTintColor={inactiveTintColor}
|
|
53
|
+
/>
|
|
135
54
|
</View>
|
|
136
55
|
);
|
|
137
56
|
};
|
|
138
57
|
|
|
139
58
|
/**
|
|
140
|
-
* iPhone Duo tab bar: rail in the side bar, or
|
|
59
|
+
* iPhone Duo tab bar: rail in the side bar, or the horizontal bar when unfolded portrait
|
|
141
60
|
*/
|
|
142
|
-
const DuoTabBar: React.FC<
|
|
61
|
+
const DuoTabBar: React.FC<DuoTabBarProps> = props => {
|
|
143
62
|
const { sideBar } = useScreenLayout();
|
|
144
63
|
return sideBar ? (
|
|
145
64
|
<DuoRailTabBar {...props} />
|
|
@@ -148,4 +67,5 @@ const DuoTabBar: React.FC<Props> = props => {
|
|
|
148
67
|
);
|
|
149
68
|
};
|
|
150
69
|
|
|
70
|
+
export type { DuoTabBarProps };
|
|
151
71
|
export { DuoTabBar, useDuoTabBarInset };
|
|
@@ -1,113 +1,201 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type ViewStyle,
|
|
9
|
-
} from 'react-native';
|
|
1
|
+
import React, { useContext, useEffect } from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withTiming,
|
|
7
|
+
} from 'react-native-reanimated';
|
|
10
8
|
|
|
11
|
-
import { Colors } from '../../Consts';
|
|
12
|
-
import {
|
|
9
|
+
import { Colors, Spacing } from '../../Consts';
|
|
10
|
+
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
11
|
+
import { BadgeDot } from '../../Badge';
|
|
12
|
+
import { Text } from '../../Text';
|
|
13
|
+
import { TabBarMetrics, TabBarMotion } from './metrics';
|
|
13
14
|
|
|
14
15
|
type Props = {
|
|
15
|
-
|
|
16
|
+
focused: boolean;
|
|
17
|
+
// '' shows a dot, anything else a label badge
|
|
16
18
|
badge?: string | number;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
renderIcon: (props: {
|
|
19
|
+
// rail items center the label badge over the icon
|
|
20
|
+
vertical?: boolean;
|
|
21
|
+
activeTintColor: string;
|
|
22
|
+
inactiveTintColor: string;
|
|
23
|
+
renderIcon?: (props: {
|
|
23
24
|
focused: boolean;
|
|
24
25
|
color: string;
|
|
25
26
|
size: number;
|
|
26
27
|
}) => React.ReactNode;
|
|
27
|
-
allowFontScaling?: boolean;
|
|
28
|
-
style: StyleProp<ViewStyle>;
|
|
29
28
|
};
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
|
-
*
|
|
33
|
-
* https://developer.apple.com/design/human-interface-guidelines/tab-bars
|
|
31
|
+
* badge value shown on a tab: undefined hides it, '' is a dot, numbers cap at 99+
|
|
34
32
|
*/
|
|
35
|
-
const
|
|
33
|
+
export const getTabBadge = (
|
|
34
|
+
badgeLabel?: string | number,
|
|
35
|
+
showDot?: boolean,
|
|
36
|
+
) => {
|
|
37
|
+
if (badgeLabel === undefined || badgeLabel === null) {
|
|
38
|
+
return showDot ? '' : undefined;
|
|
39
|
+
}
|
|
40
|
+
const text = `${badgeLabel}`;
|
|
41
|
+
if (text === '') {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
if (/^\d+$/.test(text)) {
|
|
45
|
+
const value = Number(text);
|
|
46
|
+
if (value === 0) {
|
|
47
|
+
return showDot ? '' : undefined;
|
|
48
|
+
}
|
|
49
|
+
return value > 99 ? '99+' : text;
|
|
50
|
+
}
|
|
51
|
+
return text;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* text at its design size: like the system tab bar it doesn't grow with the OS font scale or
|
|
56
|
+
* the wide Duo displays, which Text otherwise scales up to 5pt
|
|
57
|
+
*/
|
|
58
|
+
export const FixedScaleText: React.FC<{ children: React.ReactNode }> = ({
|
|
59
|
+
children,
|
|
60
|
+
}) => {
|
|
61
|
+
const context = useContext<any>(MiniAppContext);
|
|
62
|
+
return (
|
|
63
|
+
<MiniAppContext.Provider
|
|
64
|
+
value={{
|
|
65
|
+
...context,
|
|
66
|
+
fontScale: { useOSScaleRate: false, userScaleRate: 1 },
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</MiniAppContext.Provider>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
36
73
|
|
|
74
|
+
/**
|
|
75
|
+
* numbers use the error color, text the warning color (Figma Badge round numeric / text)
|
|
76
|
+
*/
|
|
77
|
+
const TabBadge: React.FC<{ label: string; vertical?: boolean }> = ({
|
|
78
|
+
label,
|
|
79
|
+
vertical,
|
|
80
|
+
}) => {
|
|
81
|
+
const { theme } = useContext(ApplicationContext);
|
|
82
|
+
const isNumber = /^\d+\+?$/.test(label);
|
|
83
|
+
const badge = (
|
|
84
|
+
<View
|
|
85
|
+
style={[
|
|
86
|
+
styles.badge,
|
|
87
|
+
{
|
|
88
|
+
backgroundColor: isNumber
|
|
89
|
+
? theme.colors.error.primary
|
|
90
|
+
: theme.colors.warning.primary,
|
|
91
|
+
borderColor: theme.colors.background.surface,
|
|
92
|
+
},
|
|
93
|
+
]}
|
|
94
|
+
>
|
|
95
|
+
<FixedScaleText>
|
|
96
|
+
<Text
|
|
97
|
+
numberOfLines={1}
|
|
98
|
+
typography={'action_xxs_bold'}
|
|
99
|
+
color={Colors.black_01}
|
|
100
|
+
>
|
|
101
|
+
{label}
|
|
102
|
+
</Text>
|
|
103
|
+
</FixedScaleText>
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
106
|
+
if (vertical) {
|
|
107
|
+
return <View style={styles.badgeCentered}>{badge}</View>;
|
|
108
|
+
}
|
|
109
|
+
return <View style={styles.badgeLeading}>{badge}</View>;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 24 icon; active and inactive icons are stacked and cross-faded on focus
|
|
114
|
+
*/
|
|
37
115
|
export function TabBarIcon({
|
|
38
|
-
|
|
116
|
+
focused,
|
|
39
117
|
badge,
|
|
40
|
-
|
|
41
|
-
activeOpacity,
|
|
42
|
-
inactiveOpacity,
|
|
118
|
+
vertical,
|
|
43
119
|
activeTintColor,
|
|
44
120
|
inactiveTintColor,
|
|
45
121
|
renderIcon,
|
|
46
|
-
style,
|
|
47
122
|
}: Props) {
|
|
48
|
-
const
|
|
123
|
+
const size = TabBarMetrics.iconSize;
|
|
124
|
+
const progress = useSharedValue(focused ? 1 : 0);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
progress.value = withTiming(focused ? 1 : 0, TabBarMotion.tint);
|
|
128
|
+
}, [focused, progress]);
|
|
129
|
+
|
|
130
|
+
const activeStyle = useAnimatedStyle(() => ({ opacity: progress.value }));
|
|
131
|
+
const inactiveStyle = useAnimatedStyle(() => ({
|
|
132
|
+
opacity: 1 - progress.value,
|
|
133
|
+
}));
|
|
49
134
|
|
|
50
135
|
const renderBadge = () => {
|
|
51
|
-
if (badge
|
|
52
|
-
return;
|
|
136
|
+
if (badge === undefined || badge === null) {
|
|
137
|
+
return null;
|
|
53
138
|
}
|
|
54
139
|
if (badge === '') {
|
|
55
|
-
return <BadgeDot size={'small'} style={
|
|
140
|
+
return <BadgeDot size={'small'} style={styles.dot} />;
|
|
56
141
|
}
|
|
57
|
-
return <
|
|
142
|
+
return <TabBadge label={`${badge}`} vertical={vertical} />;
|
|
58
143
|
};
|
|
59
144
|
|
|
60
|
-
// We render the icon twice at the same position on top of each other:
|
|
61
|
-
// active and inactive one, so we can fade between them.
|
|
62
145
|
return (
|
|
63
|
-
<View style={
|
|
64
|
-
<View
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
minWidth: iconSize,
|
|
71
|
-
},
|
|
72
|
-
]}
|
|
73
|
-
>
|
|
74
|
-
{renderIcon({
|
|
75
|
-
focused: true,
|
|
76
|
-
size: iconSize,
|
|
77
|
-
color: activeTintColor ?? Colors.black_20,
|
|
78
|
-
})}
|
|
79
|
-
</View>
|
|
80
|
-
<View style={[styles.icon, { opacity: inactiveOpacity }]}>
|
|
81
|
-
{renderIcon({
|
|
82
|
-
focused: false,
|
|
83
|
-
size: iconSize,
|
|
84
|
-
color: inactiveTintColor ?? Colors.black_04,
|
|
85
|
-
})}
|
|
86
|
-
</View>
|
|
146
|
+
<View style={styles.icon}>
|
|
147
|
+
<Animated.View style={[styles.layer, activeStyle]}>
|
|
148
|
+
{renderIcon?.({ focused: true, size, color: activeTintColor })}
|
|
149
|
+
</Animated.View>
|
|
150
|
+
<Animated.View style={[styles.layer, inactiveStyle]}>
|
|
151
|
+
{renderIcon?.({ focused: false, size, color: inactiveTintColor })}
|
|
152
|
+
</Animated.View>
|
|
87
153
|
{renderBadge()}
|
|
88
154
|
</View>
|
|
89
155
|
);
|
|
90
156
|
}
|
|
91
157
|
|
|
158
|
+
const BADGE_HEIGHT = 16;
|
|
159
|
+
const BADGE_BORDER = 1;
|
|
160
|
+
|
|
92
161
|
const styles = StyleSheet.create({
|
|
93
162
|
icon: {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
163
|
+
width: TabBarMetrics.iconSize,
|
|
164
|
+
height: TabBarMetrics.iconSize,
|
|
165
|
+
},
|
|
166
|
+
layer: {
|
|
167
|
+
...StyleSheet.absoluteFillObject,
|
|
99
168
|
alignItems: 'center',
|
|
100
169
|
justifyContent: 'center',
|
|
101
|
-
height: '100%',
|
|
102
|
-
width: '100%',
|
|
103
170
|
},
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
171
|
+
dot: {
|
|
172
|
+
position: 'absolute',
|
|
173
|
+
left: 16,
|
|
174
|
+
top: -Spacing.XXS,
|
|
107
175
|
},
|
|
108
|
-
|
|
176
|
+
badgeLeading: {
|
|
109
177
|
position: 'absolute',
|
|
110
|
-
|
|
111
|
-
top: -
|
|
178
|
+
left: Spacing.M,
|
|
179
|
+
top: -7,
|
|
180
|
+
width: TabBarMetrics.badgeMaxWidth,
|
|
181
|
+
alignItems: 'flex-start',
|
|
182
|
+
},
|
|
183
|
+
badgeCentered: {
|
|
184
|
+
position: 'absolute',
|
|
185
|
+
left: (TabBarMetrics.iconSize - TabBarMetrics.badgeMaxWidth) / 2,
|
|
186
|
+
top: -10,
|
|
187
|
+
width: TabBarMetrics.badgeMaxWidth,
|
|
188
|
+
alignItems: 'center',
|
|
189
|
+
},
|
|
190
|
+
badge: {
|
|
191
|
+
height: BADGE_HEIGHT,
|
|
192
|
+
minWidth: BADGE_HEIGHT,
|
|
193
|
+
maxWidth: TabBarMetrics.badgeMaxWidth,
|
|
194
|
+
// the border is drawn inside the 4 padding, like the Figma inside stroke
|
|
195
|
+
paddingHorizontal: Spacing.XS - BADGE_BORDER,
|
|
196
|
+
borderRadius: BADGE_HEIGHT / 2,
|
|
197
|
+
borderWidth: BADGE_BORDER,
|
|
198
|
+
alignItems: 'center',
|
|
199
|
+
justifyContent: 'center',
|
|
112
200
|
},
|
|
113
201
|
});
|