@momo-kits/foundation 0.168.1-rn862.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 +195 -0
- package/Application/BottomSheet.parts.tsx +147 -0
- package/Application/BottomSheet.styles.ts +43 -0
- package/Application/BottomSheet.tsx +76 -279
- package/Application/BottomTab/ActiveIndicator.tsx +90 -0
- package/Application/BottomTab/BottomTabBar.styles.ts +65 -0
- package/Application/BottomTab/BottomTabBar.tsx +229 -276
- 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 +227 -0
- package/Application/BottomTab/DuoTabBar.styles.ts +81 -0
- package/Application/BottomTab/DuoTabBar.tsx +29 -495
- package/Application/BottomTab/TabBarIcon.tsx +160 -72
- package/Application/BottomTab/TabScreen.tsx +164 -0
- package/Application/BottomTab/index.tsx +84 -351
- package/Application/BottomTab/metrics.ts +164 -0
- package/Application/BottomTab/tabItems.ts +46 -0
- package/Application/BottomTab/usePressLift.ts +32 -0
- package/Application/Components/DuoGlassBackground.tsx +7 -9
- package/Application/Components/HeaderAnimated.tsx +1 -1
- package/Application/Components/HeaderBackground.tsx +1 -1
- package/Application/Components/HeaderExtendHeader.tsx +17 -11
- package/Application/Components/HeaderRight.styles.ts +144 -0
- package/Application/Components/HeaderRight.tsx +6 -528
- package/Application/Components/HeaderTitle.shared.ts +43 -0
- package/Application/Components/HeaderTitle.styles.ts +125 -0
- package/Application/Components/HeaderTitle.tsx +12 -576
- package/Application/Components/HeaderToolkitAction.tsx +208 -0
- package/Application/Components/NavigationButton.tsx +9 -6
- package/Application/Components/SearchHeader.tsx +1 -1
- package/Application/Components/SwipeBackHaptic.tsx +95 -0
- package/Application/Components/TitleJourney.tsx +110 -0
- package/Application/Components/TitleLocation.tsx +73 -0
- package/Application/Components/TitleUser.tsx +304 -0
- package/Application/Components/useToolkitActions.tsx +233 -0
- package/Application/ModalIconButton.tsx +66 -0
- package/Application/ModalScreen.tsx +39 -18
- package/Application/Navigation.ts +1 -1
- package/Application/NavigationContainer.tsx +7 -8
- package/Application/StackScreen.tracking.ts +158 -0
- package/Application/StackScreen.tsx +10 -137
- package/Application/WidgetContainer.tsx +10 -3
- package/Application/__tests__/modalLayout.test.ts +128 -0
- package/Application/hooks.ts +137 -0
- package/Application/index.ts +13 -1
- package/Application/modalLayout.ts +177 -0
- package/Application/navigationOptions.tsx +217 -0
- package/Application/types.header.ts +116 -0
- package/Application/types.tabs.ts +48 -0
- package/Application/types.theme.ts +87 -0
- package/Application/types.ts +12 -244
- package/Application/utils.ts +107 -0
- package/Badge/BadgeRibbon.tsx +3 -3
- package/Badge/styles.ts +2 -0
- package/Button/index.tsx +16 -136
- package/Button/styles.ts +1 -1
- package/Button/utils.ts +139 -0
- package/CheckBox/index.tsx +1 -6
- package/Divider/DashDivider.tsx +10 -11
- package/Divider/index.tsx +18 -8
- package/Image/index.tsx +1 -46
- package/Input/Input.tsx +7 -8
- package/Input/InputDropDown.tsx +3 -4
- package/Input/InputMoney.tsx +38 -164
- package/Input/InputMoneyView.tsx +187 -0
- package/Input/InputOTP.tsx +1 -1
- package/Input/InputPhoneNumber.tsx +5 -1
- package/Input/InputSearch.tsx +2 -108
- package/Input/InputTextArea.tsx +1 -5
- package/Input/SearchTextTyping.tsx +104 -0
- package/Input/common.tsx +4 -4
- package/Input/moneyUtils.ts +17 -0
- package/Input/styles.ts +26 -2
- package/Layout/FloatingButton.tsx +22 -21
- package/Layout/ReservedRegions.tsx +21 -4
- package/Layout/Screen.tsx +74 -670
- package/Layout/Section.tsx +1 -2
- package/Layout/index.ts +10 -4
- package/Layout/layoutMetrics.ts +58 -0
- package/Layout/layoutTypes.ts +86 -0
- package/Layout/screenContent.tsx +90 -0
- package/Layout/screenFooter.tsx +53 -0
- package/Layout/screenHeader.tsx +315 -0
- package/Layout/screenQuickReturn.tsx +67 -0
- package/Layout/screenScroll.ts +110 -0
- package/Layout/screenTypes.ts +134 -0
- package/Layout/styles.ts +2 -0
- package/Layout/utils.ts +10 -144
- package/Loader/DotLoader.tsx +7 -3
- package/Loader/ProgressBar.tsx +2 -6
- package/Loader/Spinner.tsx +7 -3
- package/Loader/styles.ts +4 -0
- package/Popup/PopupNotify.tsx +70 -64
- package/Popup/PopupPromotion.tsx +5 -4
- package/Popup/types.ts +7 -0
- package/Popup/utils.ts +46 -24
- package/Radio/index.tsx +2 -10
- package/Radio/styles.ts +6 -0
- package/Skeleton/index.tsx +1 -9
- package/Skeleton/styles.ts +1 -0
- package/Tag/index.tsx +3 -3
- package/Text/index.tsx +11 -2
- package/Title/index.tsx +2 -9
- package/Title/styles.ts +6 -0
- package/package.json +21 -22
- package/Application/device.ts +0 -29
- package/Application/utils.tsx +0 -497
|
@@ -1,438 +1,64 @@
|
|
|
1
1
|
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
LayoutChangeEvent,
|
|
6
|
-
StyleSheet,
|
|
7
|
-
TouchableOpacity,
|
|
8
|
-
View,
|
|
9
|
-
} from 'react-native';
|
|
10
|
-
import Animated, {
|
|
11
|
-
useAnimatedStyle,
|
|
12
|
-
useSharedValue,
|
|
13
|
-
withSpring,
|
|
14
|
-
} from 'react-native-reanimated';
|
|
15
|
-
import { Colors, Radius, Spacing } from '../../Consts';
|
|
16
|
-
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
17
|
-
import { Icon } from '../../Icon';
|
|
18
|
-
import { DuoMetrics, useScreenLayout } from '../../Layout/utils';
|
|
19
|
-
import { Text } from '../../Text';
|
|
20
|
-
import { useDuoBarCompression } from './DuoBarCompression';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useWindowDimensions, View } from 'react-native';
|
|
4
|
+
import { useScreenLayout } from '../../Layout/utils';
|
|
21
5
|
import { FloatingButtonProps } from '../types';
|
|
22
|
-
import {
|
|
6
|
+
import { getSystemIndicatorHeight, HorizontalTabBar } from './BottomTabBar';
|
|
7
|
+
import { getHorizontalBarWidth, TabBarMetrics } from './metrics';
|
|
8
|
+
import { getTabItems } from './tabItems';
|
|
9
|
+
import { DuoRailTabBar } from './DuoRailTabBar';
|
|
10
|
+
import { styles } from './DuoTabBar.styles';
|
|
23
11
|
|
|
24
|
-
type
|
|
12
|
+
type DuoTabBarProps = BottomTabBarProps & {
|
|
25
13
|
activeTintColor: string;
|
|
26
14
|
inactiveTintColor: string;
|
|
27
15
|
floatingButton?: FloatingButtonProps;
|
|
28
16
|
};
|
|
29
17
|
|
|
30
|
-
const ITEM_SIZE = DuoMetrics.controlSize;
|
|
31
|
-
const ICON_SIZE = 28;
|
|
32
|
-
const QR_ICON_SIZE = 24;
|
|
33
|
-
const RAIL_PADDING = 6;
|
|
34
|
-
const RAIL_GAP = Spacing.XXS;
|
|
35
|
-
const RAIL_PITCH = ITEM_SIZE + RAIL_GAP;
|
|
36
|
-
// selection bubble overflows the 48 rail cell by 5 above and below (Figma MT2)
|
|
37
|
-
const RAIL_BUBBLE_WIDTH = 44;
|
|
38
|
-
const RAIL_BUBBLE_HEIGHT = 58;
|
|
39
|
-
const FLOATING_HEIGHT = DuoMetrics.floatingTabBarHeight;
|
|
40
|
-
const FLOATING_PADDING = Spacing.XS;
|
|
41
|
-
// the system bar sizes its pill to the tabs instead of stretching it across the display
|
|
42
|
-
const FLOATING_ITEM_WIDTH = 80;
|
|
43
|
-
// while pressed the selection bubble grows into a lens past the pill's edges, wider and taller (80x54 to about 96x70)
|
|
44
|
-
const LENS_SCALE_X = 1.2;
|
|
45
|
-
const LENS_SCALE_Y = 1.3;
|
|
46
|
-
const FLOATING_BUBBLE_RADIUS = (FLOATING_HEIGHT - 2 * FLOATING_PADDING) / 2;
|
|
47
|
-
const SPRING = { damping: 18, stiffness: 220, mass: 0.8 };
|
|
48
|
-
|
|
49
18
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*/
|
|
53
|
-
const getFloatingTabBarBottom = (bottomInset: number) =>
|
|
54
|
-
Math.max(bottomInset - DuoMetrics.floatingTabBarInsetOverlap, Spacing.S);
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* 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
|
|
58
21
|
*/
|
|
59
22
|
const useDuoTabBarInset = () => {
|
|
60
23
|
const { isIPhoneDuo, sideBar, insets } = useScreenLayout();
|
|
61
|
-
if (
|
|
24
|
+
if (isIPhoneDuo && sideBar) {
|
|
62
25
|
return 0;
|
|
63
26
|
}
|
|
64
|
-
return
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const BarMaterial: React.FC<{ radius: number }> = ({ radius }) => {
|
|
68
|
-
const { theme } = useContext(ApplicationContext);
|
|
69
|
-
return (
|
|
70
|
-
<View
|
|
71
|
-
pointerEvents={'none'}
|
|
72
|
-
style={[
|
|
73
|
-
StyleSheet.absoluteFill,
|
|
74
|
-
styles.barMaterial,
|
|
75
|
-
{
|
|
76
|
-
borderRadius: radius,
|
|
77
|
-
backgroundColor: theme.colors.background.surface,
|
|
78
|
-
borderColor: theme.colors.border.default,
|
|
79
|
-
},
|
|
80
|
-
]}
|
|
81
|
-
/>
|
|
82
|
-
);
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// Figma Home-2026 tab bar "Selection": brand pink at 8%
|
|
86
|
-
const SELECTION_TINT_ALPHA = '14';
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* pink tinted capsule under the selected tab
|
|
90
|
-
*/
|
|
91
|
-
const SelectionMaterial: React.FC<{ radius: number }> = ({ radius }) => {
|
|
92
|
-
const { theme } = useContext(ApplicationContext);
|
|
93
|
-
return (
|
|
94
|
-
<View
|
|
95
|
-
pointerEvents={'none'}
|
|
96
|
-
style={[
|
|
97
|
-
StyleSheet.absoluteFill,
|
|
98
|
-
{
|
|
99
|
-
borderRadius: radius,
|
|
100
|
-
backgroundColor: theme.colors.primary + SELECTION_TINT_ALPHA,
|
|
101
|
-
},
|
|
102
|
-
]}
|
|
103
|
-
/>
|
|
104
|
-
);
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* tab label at its design size: like the system tab bar it doesn't grow with the wide
|
|
109
|
-
* Duo displays, which Text otherwise scales up to 5pt
|
|
110
|
-
*/
|
|
111
|
-
const TabLabel: React.FC<{
|
|
112
|
-
label: string;
|
|
113
|
-
color: string;
|
|
114
|
-
typography: 'label_xs_medium';
|
|
115
|
-
}> = ({ label, color, typography }) => {
|
|
116
|
-
const context = useContext<any>(MiniAppContext);
|
|
117
|
-
return (
|
|
118
|
-
<MiniAppContext.Provider
|
|
119
|
-
value={{
|
|
120
|
-
...context,
|
|
121
|
-
fontScale: { useOSScaleRate: false, userScaleRate: 1 },
|
|
122
|
-
}}
|
|
123
|
-
>
|
|
124
|
-
<Text
|
|
125
|
-
numberOfLines={1}
|
|
126
|
-
typography={typography}
|
|
127
|
-
color={color}
|
|
128
|
-
>
|
|
129
|
-
{label}
|
|
130
|
-
</Text>
|
|
131
|
-
</MiniAppContext.Provider>
|
|
132
|
-
);
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const getTabItems = ({
|
|
136
|
-
state,
|
|
137
|
-
descriptors,
|
|
138
|
-
navigation,
|
|
139
|
-
activeTintColor,
|
|
140
|
-
inactiveTintColor,
|
|
141
|
-
}: Props) => {
|
|
142
|
-
return state.routes.map((route: Route<string>, index: number) => {
|
|
143
|
-
const { options } = descriptors[route.key];
|
|
144
|
-
const focused = index === state.index;
|
|
145
|
-
const label = options.title ?? route.name;
|
|
146
|
-
|
|
147
|
-
const onPress = () => {
|
|
148
|
-
const event = navigation.emit({
|
|
149
|
-
type: 'tabPress',
|
|
150
|
-
target: route.key,
|
|
151
|
-
canPreventDefault: true,
|
|
152
|
-
});
|
|
153
|
-
if (!focused && !event.defaultPrevented) {
|
|
154
|
-
navigation.dispatch({
|
|
155
|
-
...CommonActions.navigate(route.name),
|
|
156
|
-
target: state.key,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const onLongPress = () => {
|
|
162
|
-
navigation.emit({ type: 'tabLongPress', target: route.key });
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const icon = options.tabBarIcon && (
|
|
166
|
-
<TabBarIcon
|
|
167
|
-
route={route}
|
|
168
|
-
badge={options.tabBarBadge}
|
|
169
|
-
badgeStyle={options.tabBarBadgeStyle}
|
|
170
|
-
activeOpacity={focused ? 1 : 0}
|
|
171
|
-
inactiveOpacity={focused ? 0 : 1}
|
|
172
|
-
activeTintColor={activeTintColor}
|
|
173
|
-
inactiveTintColor={inactiveTintColor}
|
|
174
|
-
renderIcon={options.tabBarIcon}
|
|
175
|
-
style={styles.icon}
|
|
176
|
-
/>
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
return { route, focused, label, onPress, onLongPress, icon };
|
|
180
|
-
});
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const QrIcon: React.FC<{ icon: FloatingButtonProps['icon']; size: number }> = ({
|
|
184
|
-
icon,
|
|
185
|
-
size,
|
|
186
|
-
}) => {
|
|
187
|
-
if (typeof icon === 'string') {
|
|
188
|
-
return <Icon source={icon} size={size} color={Colors.black_01} />;
|
|
189
|
-
}
|
|
190
|
-
return <>{icon}</>;
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
type TabItem = ReturnType<typeof getTabItems>[number];
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* column of icon-only tab items with the sliding selection bubble
|
|
197
|
-
*/
|
|
198
|
-
const Rail: React.FC<{
|
|
199
|
-
items: TabItem[];
|
|
200
|
-
index: number;
|
|
201
|
-
onItemPress?: () => void;
|
|
202
|
-
}> = ({ items, index, onItemPress }) => {
|
|
203
|
-
const bubbleOffset = useSharedValue(index * RAIL_PITCH);
|
|
204
|
-
const lens = useSharedValue(0);
|
|
205
|
-
|
|
206
|
-
useEffect(() => {
|
|
207
|
-
bubbleOffset.value = withSpring(index * RAIL_PITCH, SPRING);
|
|
208
|
-
}, [index, bubbleOffset]);
|
|
209
|
-
|
|
210
|
-
const bubbleStyle = useAnimatedStyle(() => ({
|
|
211
|
-
transform: [
|
|
212
|
-
{ translateY: bubbleOffset.value },
|
|
213
|
-
{ scaleX: 1 + lens.value * (LENS_SCALE_X - 1) },
|
|
214
|
-
{ scaleY: 1 + lens.value * (LENS_SCALE_Y - 1) },
|
|
215
|
-
],
|
|
216
|
-
}));
|
|
217
|
-
|
|
218
|
-
return (
|
|
219
|
-
<View style={styles.rail} accessibilityLabel={'bottom_tab_bar'}>
|
|
220
|
-
<BarMaterial radius={ITEM_SIZE / 2} />
|
|
221
|
-
<Animated.View
|
|
222
|
-
pointerEvents={'none'}
|
|
223
|
-
style={[styles.railBubble, bubbleStyle]}
|
|
224
|
-
>
|
|
225
|
-
<SelectionMaterial radius={RAIL_BUBBLE_WIDTH / 2} />
|
|
226
|
-
</Animated.View>
|
|
227
|
-
{items.map((item, itemIndex) => (
|
|
228
|
-
<TouchableOpacity
|
|
229
|
-
key={item.route.key}
|
|
230
|
-
accessibilityRole={'tab'}
|
|
231
|
-
accessibilityLabel={item.label}
|
|
232
|
-
accessibilityState={{ selected: item.focused }}
|
|
233
|
-
activeOpacity={1}
|
|
234
|
-
onPressIn={() => {
|
|
235
|
-
bubbleOffset.value = withSpring(itemIndex * RAIL_PITCH, SPRING);
|
|
236
|
-
lens.value = withSpring(1, SPRING);
|
|
237
|
-
}}
|
|
238
|
-
onPressOut={() => {
|
|
239
|
-
bubbleOffset.value = withSpring(index * RAIL_PITCH, SPRING);
|
|
240
|
-
lens.value = withSpring(0, SPRING);
|
|
241
|
-
}}
|
|
242
|
-
onPress={() => {
|
|
243
|
-
item.onPress();
|
|
244
|
-
onItemPress?.();
|
|
245
|
-
}}
|
|
246
|
-
onLongPress={item.onLongPress}
|
|
247
|
-
style={styles.railItem}
|
|
248
|
-
>
|
|
249
|
-
{item.icon}
|
|
250
|
-
</TouchableOpacity>
|
|
251
|
-
))}
|
|
252
|
-
</View>
|
|
253
|
-
);
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* vertical bar anchored to the bottom of the Duo side bar, QR button below it (Figma MT1-C/E).
|
|
258
|
-
* When the side bar is short, it can shrink to one button showing the current tab
|
|
259
|
-
* (HIG "Tab bar compressed"); tapping it expands the rail and compresses the toolbar
|
|
260
|
-
*/
|
|
261
|
-
const DuoRailTabBar: React.FC<Props> = props => {
|
|
262
|
-
const { theme } = useContext(ApplicationContext);
|
|
263
|
-
const { sideBar } = useScreenLayout();
|
|
264
|
-
const { tabBarCompressed, expandTabBar, onTabSelected } =
|
|
265
|
-
useDuoBarCompression();
|
|
266
|
-
const { state, floatingButton } = props;
|
|
267
|
-
const items = getTabItems(props);
|
|
268
|
-
const current = items[state.index];
|
|
269
|
-
|
|
270
|
-
const columnStyle = sideBar && [
|
|
271
|
-
styles.railColumn,
|
|
272
|
-
{
|
|
273
|
-
[sideBar.edge]: 0,
|
|
274
|
-
width: sideBar.width,
|
|
275
|
-
bottom: sideBar.bottom,
|
|
276
|
-
},
|
|
277
|
-
sideBar.edge === 'right' ? styles.railColumnRight : styles.railColumnLeft,
|
|
278
|
-
];
|
|
279
|
-
|
|
280
|
-
const qr = floatingButton && (
|
|
281
|
-
<TouchableOpacity
|
|
282
|
-
accessibilityRole={'button'}
|
|
283
|
-
accessibilityLabel={floatingButton.label}
|
|
284
|
-
onPress={floatingButton.onPress}
|
|
285
|
-
style={[styles.railQr, { backgroundColor: theme.colors.primary }]}
|
|
286
|
-
>
|
|
287
|
-
<QrIcon icon={floatingButton.icon} size={QR_ICON_SIZE} />
|
|
288
|
-
</TouchableOpacity>
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
if (!sideBar) {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (!tabBarCompressed) {
|
|
296
|
-
return (
|
|
297
|
-
<View pointerEvents={'box-none'} style={columnStyle}>
|
|
298
|
-
<Rail items={items} index={state.index} onItemPress={onTabSelected} />
|
|
299
|
-
{qr}
|
|
300
|
-
</View>
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return (
|
|
305
|
-
<View pointerEvents={'box-none'} style={columnStyle}>
|
|
306
|
-
<TouchableOpacity
|
|
307
|
-
accessibilityRole={'button'}
|
|
308
|
-
accessibilityLabel={current?.label}
|
|
309
|
-
accessibilityHint={'bottom_tab_bar'}
|
|
310
|
-
onPress={expandTabBar}
|
|
311
|
-
style={styles.railCompact}
|
|
312
|
-
>
|
|
313
|
-
<BarMaterial radius={Radius.XL} />
|
|
314
|
-
{current?.icon}
|
|
315
|
-
</TouchableOpacity>
|
|
316
|
-
{qr}
|
|
317
|
-
</View>
|
|
318
|
-
);
|
|
27
|
+
return TabBarMetrics.height + getSystemIndicatorHeight(insets.bottom);
|
|
319
28
|
};
|
|
320
29
|
|
|
321
30
|
/**
|
|
322
|
-
*
|
|
323
|
-
* platter on the trailing side
|
|
31
|
+
* horizontal navigation bar of the unfolded display: 65% of the short side, centered
|
|
324
32
|
*/
|
|
325
|
-
const DuoFloatingTabBar: React.FC<
|
|
326
|
-
const { theme } = useContext(ApplicationContext);
|
|
33
|
+
const DuoFloatingTabBar: React.FC<DuoTabBarProps> = props => {
|
|
327
34
|
const { insets } = useScreenLayout();
|
|
328
|
-
const {
|
|
329
|
-
const
|
|
330
|
-
const [rowWidth, setRowWidth] = useState(0);
|
|
331
|
-
const cellWidth = rowWidth / state.routes.length;
|
|
332
|
-
|
|
333
|
-
const bubbleOffset = useSharedValue(0);
|
|
334
|
-
const lens = useSharedValue(0);
|
|
335
|
-
const measured = useRef(false);
|
|
336
|
-
|
|
337
|
-
useEffect(() => {
|
|
338
|
-
const target = state.index * cellWidth;
|
|
339
|
-
// snap on first layout so the bubble doesn't slide in from the left edge
|
|
340
|
-
bubbleOffset.value = measured.current ? withSpring(target, SPRING) : target;
|
|
341
|
-
measured.current = cellWidth > 0;
|
|
342
|
-
}, [state.index, cellWidth, bubbleOffset]);
|
|
343
|
-
|
|
344
|
-
const bubbleStyle = useAnimatedStyle(() => ({
|
|
345
|
-
transform: [
|
|
346
|
-
{ translateX: bubbleOffset.value },
|
|
347
|
-
{ scaleX: 1 + lens.value * (LENS_SCALE_X - 1) },
|
|
348
|
-
{ scaleY: 1 + lens.value * (LENS_SCALE_Y - 1) },
|
|
349
|
-
],
|
|
350
|
-
}));
|
|
351
|
-
|
|
352
|
-
const onRowLayout = (e: LayoutChangeEvent) =>
|
|
353
|
-
setRowWidth(e.nativeEvent.layout.width);
|
|
35
|
+
const { width, height } = useWindowDimensions();
|
|
36
|
+
const { floatingButton, activeTintColor, inactiveTintColor } = props;
|
|
354
37
|
|
|
355
38
|
return (
|
|
356
39
|
<View
|
|
357
40
|
pointerEvents={'box-none'}
|
|
358
41
|
style={[
|
|
359
42
|
styles.floating,
|
|
360
|
-
{
|
|
361
|
-
left: insets.left + DuoMetrics.floatingTabBarMargin,
|
|
362
|
-
right: insets.right + DuoMetrics.floatingTabBarMargin,
|
|
363
|
-
bottom: getFloatingTabBarBottom(insets.bottom),
|
|
364
|
-
},
|
|
43
|
+
{ bottom: getSystemIndicatorHeight(insets.bottom) },
|
|
365
44
|
]}
|
|
366
45
|
>
|
|
367
|
-
<
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
accessibilityLabel={'bottom_tab_bar'}
|
|
376
|
-
>
|
|
377
|
-
<BarMaterial radius={FLOATING_HEIGHT / 2} />
|
|
378
|
-
<View style={styles.floatingRow} onLayout={onRowLayout}>
|
|
379
|
-
{rowWidth > 0 && (
|
|
380
|
-
<Animated.View
|
|
381
|
-
pointerEvents={'none'}
|
|
382
|
-
style={[styles.floatingBubble, { width: cellWidth }, bubbleStyle]}
|
|
383
|
-
>
|
|
384
|
-
<SelectionMaterial radius={FLOATING_BUBBLE_RADIUS} />
|
|
385
|
-
</Animated.View>
|
|
386
|
-
)}
|
|
387
|
-
{items.map((item, index) => (
|
|
388
|
-
<TouchableOpacity
|
|
389
|
-
key={item.route.key}
|
|
390
|
-
accessibilityRole={'tab'}
|
|
391
|
-
accessibilityLabel={item.label}
|
|
392
|
-
accessibilityState={{ selected: item.focused }}
|
|
393
|
-
activeOpacity={1}
|
|
394
|
-
onPressIn={() => {
|
|
395
|
-
// the lens jumps to the pressed tab, not only the selected one
|
|
396
|
-
bubbleOffset.value = withSpring(index * cellWidth, SPRING);
|
|
397
|
-
lens.value = withSpring(1, SPRING);
|
|
398
|
-
}}
|
|
399
|
-
onPressOut={() => {
|
|
400
|
-
// onPress follows synchronously, a tab switch re-targets the bubble
|
|
401
|
-
bubbleOffset.value = withSpring(state.index * cellWidth, SPRING);
|
|
402
|
-
lens.value = withSpring(0, SPRING);
|
|
403
|
-
}}
|
|
404
|
-
onPress={item.onPress}
|
|
405
|
-
onLongPress={item.onLongPress}
|
|
406
|
-
style={styles.floatingItem}
|
|
407
|
-
>
|
|
408
|
-
{item.icon}
|
|
409
|
-
<TabLabel
|
|
410
|
-
label={item.label}
|
|
411
|
-
typography={'label_xs_medium'}
|
|
412
|
-
color={item.focused ? activeTintColor : inactiveTintColor}
|
|
413
|
-
/>
|
|
414
|
-
</TouchableOpacity>
|
|
415
|
-
))}
|
|
416
|
-
</View>
|
|
417
|
-
</View>
|
|
418
|
-
{floatingButton && (
|
|
419
|
-
<TouchableOpacity
|
|
420
|
-
accessibilityRole={'button'}
|
|
421
|
-
accessibilityLabel={floatingButton.label}
|
|
422
|
-
onPress={floatingButton.onPress}
|
|
423
|
-
style={[styles.floatingQr, { backgroundColor: theme.colors.primary }]}
|
|
424
|
-
>
|
|
425
|
-
<QrIcon icon={floatingButton.icon} size={QR_ICON_SIZE} />
|
|
426
|
-
</TouchableOpacity>
|
|
427
|
-
)}
|
|
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
|
+
/>
|
|
428
54
|
</View>
|
|
429
55
|
);
|
|
430
56
|
};
|
|
431
57
|
|
|
432
58
|
/**
|
|
433
|
-
* 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
|
|
434
60
|
*/
|
|
435
|
-
const DuoTabBar: React.FC<
|
|
61
|
+
const DuoTabBar: React.FC<DuoTabBarProps> = props => {
|
|
436
62
|
const { sideBar } = useScreenLayout();
|
|
437
63
|
return sideBar ? (
|
|
438
64
|
<DuoRailTabBar {...props} />
|
|
@@ -441,97 +67,5 @@ const DuoTabBar: React.FC<Props> = props => {
|
|
|
441
67
|
);
|
|
442
68
|
};
|
|
443
69
|
|
|
444
|
-
|
|
445
|
-
barMaterial: {
|
|
446
|
-
borderWidth: 1,
|
|
447
|
-
},
|
|
448
|
-
icon: {
|
|
449
|
-
width: ICON_SIZE,
|
|
450
|
-
height: ICON_SIZE,
|
|
451
|
-
},
|
|
452
|
-
railColumn: {
|
|
453
|
-
position: 'absolute',
|
|
454
|
-
gap: Spacing.M,
|
|
455
|
-
},
|
|
456
|
-
railColumnRight: {
|
|
457
|
-
alignItems: 'flex-start',
|
|
458
|
-
paddingLeft: Spacing.M,
|
|
459
|
-
},
|
|
460
|
-
railColumnLeft: {
|
|
461
|
-
alignItems: 'flex-end',
|
|
462
|
-
paddingRight: Spacing.M,
|
|
463
|
-
},
|
|
464
|
-
railCompact: {
|
|
465
|
-
width: ITEM_SIZE,
|
|
466
|
-
height: ITEM_SIZE,
|
|
467
|
-
borderRadius: ITEM_SIZE / 2,
|
|
468
|
-
alignItems: 'center',
|
|
469
|
-
justifyContent: 'center',
|
|
470
|
-
},
|
|
471
|
-
rail: {
|
|
472
|
-
width: ITEM_SIZE,
|
|
473
|
-
paddingVertical: RAIL_PADDING,
|
|
474
|
-
gap: RAIL_GAP,
|
|
475
|
-
},
|
|
476
|
-
railBubble: {
|
|
477
|
-
position: 'absolute',
|
|
478
|
-
top: RAIL_PADDING - (RAIL_BUBBLE_HEIGHT - ITEM_SIZE) / 2,
|
|
479
|
-
left: (ITEM_SIZE - RAIL_BUBBLE_WIDTH) / 2,
|
|
480
|
-
width: RAIL_BUBBLE_WIDTH,
|
|
481
|
-
height: RAIL_BUBBLE_HEIGHT,
|
|
482
|
-
},
|
|
483
|
-
railItem: {
|
|
484
|
-
width: ITEM_SIZE,
|
|
485
|
-
height: ITEM_SIZE,
|
|
486
|
-
alignItems: 'center',
|
|
487
|
-
justifyContent: 'center',
|
|
488
|
-
},
|
|
489
|
-
railQr: {
|
|
490
|
-
width: ITEM_SIZE,
|
|
491
|
-
height: ITEM_SIZE,
|
|
492
|
-
borderRadius: ITEM_SIZE / 2,
|
|
493
|
-
alignItems: 'center',
|
|
494
|
-
justifyContent: 'center',
|
|
495
|
-
},
|
|
496
|
-
floating: {
|
|
497
|
-
position: 'absolute',
|
|
498
|
-
flexDirection: 'row',
|
|
499
|
-
justifyContent: 'center',
|
|
500
|
-
gap: Spacing.S,
|
|
501
|
-
},
|
|
502
|
-
floatingBar: {
|
|
503
|
-
flexShrink: 1,
|
|
504
|
-
height: FLOATING_HEIGHT,
|
|
505
|
-
padding: FLOATING_PADDING,
|
|
506
|
-
},
|
|
507
|
-
floatingRow: {
|
|
508
|
-
flex: 1,
|
|
509
|
-
flexDirection: 'row',
|
|
510
|
-
},
|
|
511
|
-
floatingBubble: {
|
|
512
|
-
position: 'absolute',
|
|
513
|
-
top: 0,
|
|
514
|
-
bottom: 0,
|
|
515
|
-
left: 0,
|
|
516
|
-
},
|
|
517
|
-
floatingItem: {
|
|
518
|
-
flex: 1,
|
|
519
|
-
alignItems: 'center',
|
|
520
|
-
justifyContent: 'center',
|
|
521
|
-
gap: Spacing.XXS,
|
|
522
|
-
},
|
|
523
|
-
// pink fills the whole pinned platter
|
|
524
|
-
floatingQr: {
|
|
525
|
-
width: FLOATING_HEIGHT,
|
|
526
|
-
height: FLOATING_HEIGHT,
|
|
527
|
-
borderRadius: FLOATING_HEIGHT / 2,
|
|
528
|
-
alignItems: 'center',
|
|
529
|
-
justifyContent: 'center',
|
|
530
|
-
shadowColor: Colors.black_20,
|
|
531
|
-
shadowOffset: { width: 0, height: 6 },
|
|
532
|
-
shadowOpacity: 0.16,
|
|
533
|
-
shadowRadius: 16,
|
|
534
|
-
},
|
|
535
|
-
});
|
|
536
|
-
|
|
70
|
+
export type { DuoTabBarProps };
|
|
537
71
|
export { DuoTabBar, useDuoTabBarInset };
|