@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,41 +1,15 @@
1
1
  import { StyleSheet } from 'react-native';
2
- import { Colors, Spacing } from '../../Consts';
2
+ import { Spacing } from '../../Consts';
3
3
  import { DuoMetrics } from '../../Layout/utils';
4
+ import { CTA_SHADOW, TabBarMetrics } from './metrics';
4
5
 
5
- export const ITEM_SIZE = DuoMetrics.controlSize;
6
- export const ICON_SIZE = 28;
7
- export const QR_ICON_SIZE = 24;
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: ITEM_SIZE,
50
- height: ITEM_SIZE,
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: ITEM_SIZE,
57
- paddingVertical: RAIL_PADDING,
58
- gap: RAIL_GAP,
35
+ width: TabBarMetrics.railContainerWidth,
36
+ paddingHorizontal: TabBarMetrics.railPaddingHorizontal,
37
+ paddingBottom: TabBarMetrics.railPaddingBottom,
59
38
  },
60
- railBubble: {
61
- position: 'absolute',
62
- top: RAIL_PADDING - (RAIL_BUBBLE_HEIGHT - ITEM_SIZE) / 2,
63
- left: (ITEM_SIZE - RAIL_BUBBLE_WIDTH) / 2,
64
- width: RAIL_BUBBLE_WIDTH,
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: ITEM_SIZE,
69
- height: ITEM_SIZE,
46
+ width: RAIL_ITEM_WIDTH,
47
+ height: RAIL_ITEM_HEIGHT,
70
48
  alignItems: 'center',
71
49
  justifyContent: 'center',
72
50
  },
73
- railQr: {
74
- width: ITEM_SIZE,
75
- height: ITEM_SIZE,
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
- floating: {
81
- position: 'absolute',
82
- flexDirection: 'row',
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
- itemLayer: {
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
- // pink fills the whole pinned platter
112
- floatingQr: {
113
- width: FLOATING_HEIGHT,
114
- height: FLOATING_HEIGHT,
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 React, { useContext, useMemo, useState } from 'react';
2
- import { LayoutChangeEvent, TouchableOpacity, View } from 'react-native';
3
- import { GestureDetector } from 'react-native-gesture-handler';
4
- import Animated from 'react-native-reanimated';
5
- import { ApplicationContext } from '../../Context';
6
- import { DuoMetrics, useScreenLayout } from '../../Layout/utils';
7
- import { useLiquidSelection } from './hooks';
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
- BarMaterial,
11
- getTabItems,
12
- LiquidTabItem,
13
- Props,
14
- QrIcon,
15
- SelectionMaterial,
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 Duo floating tab bar, 0 elsewhere
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 (!isIPhoneDuo || sideBar) {
24
+ if (isIPhoneDuo && sideBar) {
34
25
  return 0;
35
26
  }
36
- return FLOATING_HEIGHT + getFloatingTabBarBottom(insets.bottom);
27
+ return TabBarMetrics.height + getSystemIndicatorHeight(insets.bottom);
37
28
  };
38
29
 
39
30
  /**
40
- * floating labelled bar for the unfolded portrait display, QR pinned in its own
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<Props> = props => {
44
- const { theme } = useContext(ApplicationContext);
33
+ const DuoFloatingTabBar: React.FC<DuoTabBarProps> = props => {
45
34
  const { insets } = useScreenLayout();
46
- const { state, floatingButton, activeTintColor, inactiveTintColor } = props;
47
- const items = getTabItems(props);
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
- <Animated.View
91
- style={floatingBarStyle}
92
- accessibilityLabel={'bottom_tab_bar'}
93
- >
94
- <BarMaterial radius={FLOATING_HEIGHT / 2} />
95
- <GestureDetector gesture={selection.gesture}>
96
- <View style={styles.floatingRow} onLayout={onRowLayout}>
97
- {rowWidth > 0 && (
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 a floating bar when unfolded portrait
59
+ * iPhone Duo tab bar: rail in the side bar, or the horizontal bar when unfolded portrait
141
60
  */
142
- const DuoTabBar: React.FC<Props> = props => {
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 type { Route } from '@react-navigation/native';
2
- import React from 'react';
3
- import {
4
- type StyleProp,
5
- StyleSheet,
6
- type TextStyle,
7
- View,
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 { Badge, BadgeDot } from '../../Badge';
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
- route: Route<string>;
16
+ focused: boolean;
17
+ // '' shows a dot, anything else a label badge
16
18
  badge?: string | number;
17
- badgeStyle?: StyleProp<TextStyle> | any;
18
- activeOpacity: number;
19
- inactiveOpacity: number;
20
- activeTintColor?: string;
21
- inactiveTintColor?: string;
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
- * Icon sizes taken from Apple HIG
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 ICON_SIZE_WIDE = 35;
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
- route: _,
116
+ focused,
39
117
  badge,
40
- badgeStyle,
41
- activeOpacity,
42
- inactiveOpacity,
118
+ vertical,
43
119
  activeTintColor,
44
120
  inactiveTintColor,
45
121
  renderIcon,
46
- style,
47
122
  }: Props) {
48
- const iconSize = 25;
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 == null) {
52
- return;
136
+ if (badge === undefined || badge === null) {
137
+ return null;
53
138
  }
54
139
  if (badge === '') {
55
- return <BadgeDot size={'small'} style={[styles.badge, badgeStyle]} />;
140
+ return <BadgeDot size={'small'} style={styles.dot} />;
56
141
  }
57
- return <Badge style={[styles.badge, badgeStyle]} label={badge} />;
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={[styles.wrapperUikit, style]}>
64
- <View
65
- style={[
66
- styles.icon,
67
- {
68
- opacity: activeOpacity,
69
- // Workaround for react-native >= 0.54 layout bug
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
- // We render the icon twice at the same position on top of each other:
95
- // active and inactive one, so we can fade between them:
96
- // Cover the whole iconContainer:
97
- position: 'absolute',
98
- alignSelf: 'center',
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
- wrapperUikit: {
105
- width: ICON_SIZE_WIDE,
106
- height: ICON_SIZE_WIDE,
171
+ dot: {
172
+ position: 'absolute',
173
+ left: 16,
174
+ top: -Spacing.XXS,
107
175
  },
108
- badge: {
176
+ badgeLeading: {
109
177
  position: 'absolute',
110
- end: -3,
111
- top: -3,
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
  });