@momo-kits/foundation 0.167.1-beta.7 → 0.167.1-duo-test.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.
- package/Application/BottomSheet.tsx +115 -14
- package/Application/BottomTab/BottomTabBar.tsx +6 -3
- package/Application/BottomTab/DuoBarCompression.tsx +135 -0
- package/Application/BottomTab/DuoTabBar.tsx +462 -0
- package/Application/BottomTab/index.tsx +150 -134
- package/Application/Components/DuoGlassBackground.tsx +179 -0
- package/Application/Components/DuoQuickReturnHeader.tsx +42 -0
- package/Application/Components/HeaderExtendHeader.tsx +75 -28
- package/Application/Components/HeaderLeft.tsx +38 -0
- package/Application/Components/HeaderRight.tsx +142 -9
- package/Application/Components/HeaderTitle.tsx +61 -21
- package/Application/Components/OptionalGlassView.ts +11 -0
- package/Application/Components/OptionalScrollObserver.ts +12 -0
- package/Application/Components/SearchHeader.tsx +22 -15
- package/Application/Components/SideBarButton.tsx +139 -0
- package/Application/ModalScreen.tsx +16 -2
- package/Application/NavigationContainer.tsx +36 -50
- package/Application/Navigator.ts +1 -3
- package/Application/StackScreen.tsx +63 -9
- package/Application/WidgetContainer.tsx +9 -15
- package/Application/device.ts +29 -0
- package/Application/types.ts +27 -1
- package/Application/utils.tsx +255 -7
- package/Consts/index.ts +0 -1
- package/Consts/theme.ts +3 -4
- package/Input/Input.tsx +3 -4
- package/Input/InputMoney.tsx +31 -51
- package/Input/InputPhoneNumber.tsx +3 -4
- package/Input/InputSearch.tsx +3 -4
- package/Input/InputTextArea.tsx +3 -4
- package/Layout/Card.tsx +8 -26
- package/Layout/FloatingButton.tsx +3 -3
- package/Layout/Item.tsx +1 -10
- package/Layout/ReservedRegions.tsx +47 -0
- package/Layout/Screen.tsx +199 -52
- package/Layout/Section.tsx +5 -18
- package/Layout/index.ts +4 -4
- package/Layout/styles.ts +5 -0
- package/Layout/utils.ts +465 -5
- package/Popup/PopupNotify.tsx +33 -22
- package/Popup/PopupPromotion.tsx +20 -8
- package/Popup/utils.ts +32 -0
- package/Text/index.tsx +5 -15
- package/Text/utils.ts +1 -2
- package/package.json +3 -2
- package/Consts/designSystemFont.ts +0 -25
- package/Layout/GridSystem.tsx +0 -137
|
@@ -27,7 +27,11 @@ import StackScreen from '../StackScreen';
|
|
|
27
27
|
import { BottomTabProps } from '../types';
|
|
28
28
|
import { getOptions, getStackOptions, useAppState } from '../utils';
|
|
29
29
|
import BottomTabBar from './BottomTabBar';
|
|
30
|
+
import { DuoTabBar } from './DuoTabBar';
|
|
31
|
+
import { DuoBarCompressionProvider } from './DuoBarCompression';
|
|
30
32
|
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
33
|
+
import { useScreenLayout } from '../../Layout';
|
|
34
|
+
import { getHeaderRowOptions } from '../../Layout/utils';
|
|
31
35
|
|
|
32
36
|
const Tab = createBottomTabNavigator();
|
|
33
37
|
const Stack = createStackNavigator();
|
|
@@ -36,7 +40,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
36
40
|
const { navigator } = useContext(ApplicationContext);
|
|
37
41
|
const context = useContext<any>(MiniAppContext);
|
|
38
42
|
const { nested, options, screen, initialParams } = route?.params;
|
|
39
|
-
const
|
|
43
|
+
const headerStyleOptions = getHeaderRowOptions(useScreenLayout());
|
|
40
44
|
|
|
41
45
|
const opacityValue = 0.3;
|
|
42
46
|
const scaleValue = 0.97;
|
|
@@ -139,11 +143,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
139
143
|
return (
|
|
140
144
|
<Animated.View style={[styles.container, tabAnimatedStyle]}>
|
|
141
145
|
<Stack.Navigator
|
|
142
|
-
screenOptions={
|
|
143
|
-
headerStyle: {
|
|
144
|
-
height: 52 + insets.top,
|
|
145
|
-
},
|
|
146
|
-
}}
|
|
146
|
+
screenOptions={headerStyleOptions}
|
|
147
147
|
>
|
|
148
148
|
<Stack.Screen
|
|
149
149
|
name="Stack"
|
|
@@ -174,11 +174,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
174
174
|
<NavigationIndependentTree>
|
|
175
175
|
<NavigationContainer>
|
|
176
176
|
<Stack.Navigator
|
|
177
|
-
screenOptions={
|
|
178
|
-
headerStyle: {
|
|
179
|
-
height: 52 + insets.top,
|
|
180
|
-
},
|
|
181
|
-
}}
|
|
177
|
+
screenOptions={headerStyleOptions}
|
|
182
178
|
>
|
|
183
179
|
<Stack.Screen
|
|
184
180
|
name="Stack"
|
|
@@ -205,8 +201,10 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
205
201
|
navigation,
|
|
206
202
|
initialRouteName,
|
|
207
203
|
floatingButton,
|
|
204
|
+
compressionBehavior,
|
|
208
205
|
}) => {
|
|
209
206
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
207
|
+
const { isIPhoneDuo } = useScreenLayout();
|
|
210
208
|
const dimensions = useWindowDimensions();
|
|
211
209
|
const insets = useSafeAreaInsets();
|
|
212
210
|
const initialIndex = tabs.findIndex(i => i.name === initialRouteName);
|
|
@@ -280,138 +278,156 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
280
278
|
};
|
|
281
279
|
|
|
282
280
|
return (
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
headerBackground: undefined,
|
|
288
|
-
tabBarLabelPosition: 'below-icon',
|
|
289
|
-
tabBarBackground: () => (
|
|
290
|
-
<View
|
|
291
|
-
style={[
|
|
292
|
-
styles.container,
|
|
293
|
-
{
|
|
294
|
-
backgroundColor: theme.colors.background.surface,
|
|
295
|
-
},
|
|
296
|
-
]}
|
|
297
|
-
>
|
|
298
|
-
<Animated.View style={[styles.indicatorContainer, indicatorStyle]}>
|
|
299
|
-
<View
|
|
300
|
-
style={[
|
|
301
|
-
styles.indicator,
|
|
302
|
-
{ backgroundColor: theme.colors.primary },
|
|
303
|
-
]}
|
|
304
|
-
/>
|
|
305
|
-
</Animated.View>
|
|
306
|
-
</View>
|
|
307
|
-
),
|
|
308
|
-
}}
|
|
309
|
-
tabBar={props => (
|
|
310
|
-
<BottomTabBar
|
|
311
|
-
{...props}
|
|
312
|
-
floatingButton={floatingButton}
|
|
313
|
-
inactiveTintColor={theme.colors.text.hint}
|
|
314
|
-
activeTintColor={theme.colors.primary}
|
|
315
|
-
style={[
|
|
316
|
-
{
|
|
317
|
-
height:
|
|
318
|
-
64 +
|
|
319
|
-
(Platform.OS === 'ios'
|
|
320
|
-
? Math.min(insets.bottom, 21)
|
|
321
|
-
: insets.bottom),
|
|
322
|
-
},
|
|
323
|
-
]}
|
|
324
|
-
/>
|
|
325
|
-
)}
|
|
326
|
-
initialRouteName={initialRouteName}
|
|
281
|
+
<DuoBarCompressionProvider
|
|
282
|
+
behavior={compressionBehavior}
|
|
283
|
+
tabCount={tabs.length}
|
|
284
|
+
hasQr={!!floatingButton}
|
|
327
285
|
>
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
286
|
+
<Tab.Navigator
|
|
287
|
+
screenOptions={{
|
|
288
|
+
headerShown: false,
|
|
289
|
+
headerTransparent: true,
|
|
290
|
+
headerBackground: undefined,
|
|
291
|
+
tabBarLabelPosition: 'below-icon',
|
|
292
|
+
tabBarBackground: () => (
|
|
293
|
+
<View
|
|
294
|
+
style={[
|
|
295
|
+
styles.container,
|
|
296
|
+
{
|
|
297
|
+
backgroundColor: theme.colors.background.surface,
|
|
298
|
+
},
|
|
299
|
+
]}
|
|
300
|
+
>
|
|
301
|
+
<Animated.View
|
|
302
|
+
style={[styles.indicatorContainer, indicatorStyle]}
|
|
303
|
+
>
|
|
304
|
+
<View
|
|
305
|
+
style={[
|
|
306
|
+
styles.indicator,
|
|
307
|
+
{ backgroundColor: theme.colors.primary },
|
|
308
|
+
]}
|
|
309
|
+
/>
|
|
310
|
+
</Animated.View>
|
|
311
|
+
</View>
|
|
312
|
+
),
|
|
313
|
+
}}
|
|
314
|
+
tabBar={props =>
|
|
315
|
+
isIPhoneDuo ? (
|
|
316
|
+
<DuoTabBar
|
|
317
|
+
{...props}
|
|
318
|
+
floatingButton={floatingButton}
|
|
319
|
+
inactiveTintColor={theme.colors.text.default}
|
|
320
|
+
activeTintColor={theme.colors.primary}
|
|
321
|
+
/>
|
|
322
|
+
) : (
|
|
323
|
+
<BottomTabBar
|
|
324
|
+
{...props}
|
|
325
|
+
floatingButton={floatingButton}
|
|
326
|
+
inactiveTintColor={theme.colors.text.hint}
|
|
327
|
+
activeTintColor={theme.colors.primary}
|
|
328
|
+
style={[
|
|
329
|
+
{
|
|
330
|
+
height:
|
|
331
|
+
64 +
|
|
332
|
+
(Platform.OS === 'ios'
|
|
333
|
+
? Math.min(insets.bottom, 21)
|
|
334
|
+
: insets.bottom),
|
|
335
|
+
},
|
|
336
|
+
]}
|
|
337
|
+
/>
|
|
338
|
+
)
|
|
336
339
|
}
|
|
340
|
+
initialRouteName={initialRouteName}
|
|
341
|
+
>
|
|
342
|
+
{tabs.map((item, index) => {
|
|
343
|
+
const isNum = !isNaN(parseInt(`${item?.badgeLabel}`, 10));
|
|
344
|
+
const isDot =
|
|
345
|
+
typeof item?.badgeLabel === 'string' && !item?.badgeLabel;
|
|
346
|
+
const isNumZero = isNum && Number(item.badgeLabel) === 0;
|
|
337
347
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
width?: number;
|
|
343
|
-
} = {
|
|
344
|
-
marginRight: -8,
|
|
345
|
-
top: -4,
|
|
346
|
-
backgroundColor: Colors.orange_03,
|
|
347
|
-
};
|
|
348
|
+
let badgeLabel = item?.badgeLabel;
|
|
349
|
+
if (isNumZero) {
|
|
350
|
+
badgeLabel = undefined;
|
|
351
|
+
}
|
|
348
352
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
353
|
+
let badgeStyle: {
|
|
354
|
+
marginRight: number;
|
|
355
|
+
top: number;
|
|
356
|
+
backgroundColor: string;
|
|
357
|
+
width?: number;
|
|
358
|
+
} = {
|
|
359
|
+
marginRight: -8,
|
|
352
360
|
top: -4,
|
|
353
|
-
backgroundColor: Colors.red_03,
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
if (isDot) {
|
|
357
|
-
badgeStyle = {
|
|
358
|
-
marginRight: -2,
|
|
359
|
-
top: -2,
|
|
360
361
|
backgroundColor: Colors.orange_03,
|
|
361
|
-
width: 12,
|
|
362
362
|
};
|
|
363
|
-
}
|
|
364
363
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
alignSelf: 'center',
|
|
402
|
-
fontFamily: exportFontFamily('bold'),
|
|
364
|
+
if (isNum) {
|
|
365
|
+
badgeStyle = {
|
|
366
|
+
marginRight: -4,
|
|
367
|
+
top: -4,
|
|
368
|
+
backgroundColor: Colors.red_03,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (isDot) {
|
|
372
|
+
badgeStyle = {
|
|
373
|
+
marginRight: -2,
|
|
374
|
+
top: -2,
|
|
375
|
+
backgroundColor: Colors.orange_03,
|
|
376
|
+
width: 12,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<Tab.Screen
|
|
382
|
+
key={`BottomTab-${item.name}`}
|
|
383
|
+
name={item.name}
|
|
384
|
+
component={TabScreen}
|
|
385
|
+
initialParams={{ ...item, nested, index }}
|
|
386
|
+
listeners={handler}
|
|
387
|
+
options={{
|
|
388
|
+
tabBarLabel: ({ focused, color }) => {
|
|
389
|
+
return (
|
|
390
|
+
<Text
|
|
391
|
+
numberOfLines={1}
|
|
392
|
+
typography={
|
|
393
|
+
focused ? 'label_xs_medium' : 'description_xs_regular'
|
|
394
|
+
}
|
|
395
|
+
color={color}
|
|
396
|
+
>
|
|
397
|
+
{item.label}
|
|
398
|
+
</Text>
|
|
399
|
+
);
|
|
403
400
|
},
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
401
|
+
title: item.label,
|
|
402
|
+
tabBarStyle: [
|
|
403
|
+
{
|
|
404
|
+
height: 64,
|
|
405
|
+
},
|
|
406
|
+
styles.tabStyle,
|
|
407
|
+
],
|
|
408
|
+
tabBarBadge: badgeLabel,
|
|
409
|
+
tabBarBadgeStyle: [
|
|
410
|
+
{
|
|
411
|
+
borderColor: 'white',
|
|
412
|
+
borderWidth: 2,
|
|
413
|
+
fontSize: 10,
|
|
414
|
+
lineHeight: 14,
|
|
415
|
+
fontWeight: 'bold',
|
|
416
|
+
alignSelf: 'center',
|
|
417
|
+
fontFamily: exportFontFamily('bold'),
|
|
418
|
+
},
|
|
419
|
+
badgeStyle,
|
|
420
|
+
],
|
|
421
|
+
tabBarIcon: ({ color }) => (
|
|
422
|
+
<Icon source={item.icon} color={color} size={28} />
|
|
423
|
+
),
|
|
424
|
+
...item,
|
|
425
|
+
}}
|
|
426
|
+
/>
|
|
427
|
+
);
|
|
428
|
+
})}
|
|
429
|
+
</Tab.Navigator>
|
|
430
|
+
</DuoBarCompressionProvider>
|
|
415
431
|
);
|
|
416
432
|
};
|
|
417
433
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import React, { useContext, useState } from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import LinearGradient from 'react-native-linear-gradient';
|
|
4
|
+
import Animated, {
|
|
5
|
+
Extrapolation,
|
|
6
|
+
interpolate,
|
|
7
|
+
runOnJS,
|
|
8
|
+
useAnimatedReaction,
|
|
9
|
+
useAnimatedStyle,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
import { ApplicationContext } from '../../Context';
|
|
12
|
+
import { Shadow } from '../../Consts';
|
|
13
|
+
import { Image } from '../../Image';
|
|
14
|
+
import { AnimatedCompatValue, useNormalizedSharedValue } from '../utils';
|
|
15
|
+
import { GlassView } from './OptionalGlassView';
|
|
16
|
+
|
|
17
|
+
// extended headers run their gradient this far below the header, as off Duo
|
|
18
|
+
const EXTENDED_GRADIENT_EXTRA = 52;
|
|
19
|
+
// scroll distance over which the extended gradient collapses to the header
|
|
20
|
+
const EXTENDED_COLLAPSE_DISTANCE = 100;
|
|
21
|
+
// scroll distance over which the bar material fades in
|
|
22
|
+
const FADE_DISTANCE = 16;
|
|
23
|
+
const TRANSLUCENT_ALPHA = 'CC';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* iPhone Duo header background: glass running edge to edge, behind the status bar
|
|
27
|
+
* column too, shown once content scrolls under it
|
|
28
|
+
*/
|
|
29
|
+
const DuoGlassBackground: React.FC<{
|
|
30
|
+
animatedValue?: AnimatedCompatValue;
|
|
31
|
+
// scroll offset where the bar starts to show, e.g. below an animated banner
|
|
32
|
+
fadeStart?: number;
|
|
33
|
+
}> = ({ animatedValue, fadeStart = 0 }) => {
|
|
34
|
+
const { theme } = useContext(ApplicationContext);
|
|
35
|
+
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
36
|
+
const [scrolled, setScrolled] = useState(false);
|
|
37
|
+
|
|
38
|
+
useAnimatedReaction(
|
|
39
|
+
() => sharedValue.value > fadeStart,
|
|
40
|
+
(isScrolled, wasScrolled) => {
|
|
41
|
+
if (GlassView && isScrolled !== wasScrolled) {
|
|
42
|
+
runOnJS(setScrolled)(isScrolled);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
[fadeStart],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const fadeStyle = useAnimatedStyle(() => ({
|
|
49
|
+
opacity: interpolate(
|
|
50
|
+
sharedValue.value,
|
|
51
|
+
[fadeStart, fadeStart + FADE_DISTANCE],
|
|
52
|
+
[0, 1],
|
|
53
|
+
Extrapolation.CLAMP,
|
|
54
|
+
),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<View pointerEvents={'none'} style={StyleSheet.absoluteFill}>
|
|
59
|
+
{GlassView ? (
|
|
60
|
+
// UIVisualEffectView breaks under alpha, so glass toggles its effect instead
|
|
61
|
+
<GlassView
|
|
62
|
+
effect={scrolled ? 'glass' : 'none'}
|
|
63
|
+
useShadow
|
|
64
|
+
style={StyleSheet.absoluteFill}
|
|
65
|
+
/>
|
|
66
|
+
) : (
|
|
67
|
+
<Animated.View
|
|
68
|
+
style={[
|
|
69
|
+
StyleSheet.absoluteFill,
|
|
70
|
+
styles.translucentBar,
|
|
71
|
+
{
|
|
72
|
+
backgroundColor:
|
|
73
|
+
theme.colors.background.surface + TRANSLUCENT_ALPHA,
|
|
74
|
+
borderBottomColor: theme.colors.border.default,
|
|
75
|
+
},
|
|
76
|
+
fadeStyle,
|
|
77
|
+
]}
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* pink header gradient drawn under the content, fading out on scroll as main's extended
|
|
86
|
+
* header does: header-high for default headers, 52 taller (collapsing) for extended
|
|
87
|
+
*/
|
|
88
|
+
const DuoScrollGradient: React.FC<{
|
|
89
|
+
animatedValue?: AnimatedCompatValue;
|
|
90
|
+
gradientColor?: string;
|
|
91
|
+
headerBackground?: string;
|
|
92
|
+
heightHeader: number;
|
|
93
|
+
extended?: boolean;
|
|
94
|
+
// quick return header: the gradient leaves with the content instead of fading
|
|
95
|
+
followScroll?: boolean;
|
|
96
|
+
}> = ({
|
|
97
|
+
animatedValue,
|
|
98
|
+
gradientColor,
|
|
99
|
+
headerBackground,
|
|
100
|
+
heightHeader,
|
|
101
|
+
extended,
|
|
102
|
+
followScroll,
|
|
103
|
+
}) => {
|
|
104
|
+
const { theme } = useContext(ApplicationContext);
|
|
105
|
+
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
106
|
+
const color = gradientColor ?? theme.colors.gradient;
|
|
107
|
+
const image = headerBackground ?? theme.assets?.headerBackground;
|
|
108
|
+
|
|
109
|
+
const scrollStyle = useAnimatedStyle(() => {
|
|
110
|
+
if (followScroll) {
|
|
111
|
+
return {
|
|
112
|
+
height: heightHeader + (extended ? EXTENDED_GRADIENT_EXTRA : 0),
|
|
113
|
+
transform: [{ translateY: -Math.max(sharedValue.value, 0) }],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
height: extended
|
|
118
|
+
? interpolate(
|
|
119
|
+
sharedValue.value,
|
|
120
|
+
[0, EXTENDED_COLLAPSE_DISTANCE],
|
|
121
|
+
[heightHeader + EXTENDED_GRADIENT_EXTRA, heightHeader],
|
|
122
|
+
Extrapolation.CLAMP,
|
|
123
|
+
)
|
|
124
|
+
: heightHeader,
|
|
125
|
+
opacity: interpolate(
|
|
126
|
+
sharedValue.value,
|
|
127
|
+
[0, EXTENDED_GRADIENT_EXTRA],
|
|
128
|
+
[1, 0],
|
|
129
|
+
Extrapolation.CLAMP,
|
|
130
|
+
),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!color) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<Animated.View
|
|
140
|
+
pointerEvents={'none'}
|
|
141
|
+
style={[styles.gradient, scrollStyle]}
|
|
142
|
+
>
|
|
143
|
+
<LinearGradient
|
|
144
|
+
colors={[color, color + '00']}
|
|
145
|
+
style={StyleSheet.absoluteFill}
|
|
146
|
+
>
|
|
147
|
+
{!!image && (
|
|
148
|
+
<Image
|
|
149
|
+
style={styles.gradientFill}
|
|
150
|
+
source={{ uri: image }}
|
|
151
|
+
loading={false}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
</LinearGradient>
|
|
155
|
+
</Animated.View>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const styles = StyleSheet.create({
|
|
160
|
+
translucentBar: {
|
|
161
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
162
|
+
...Shadow.Light,
|
|
163
|
+
},
|
|
164
|
+
gradient: {
|
|
165
|
+
position: 'absolute',
|
|
166
|
+
top: 0,
|
|
167
|
+
left: 0,
|
|
168
|
+
right: 0,
|
|
169
|
+
overflow: 'hidden',
|
|
170
|
+
},
|
|
171
|
+
// banner image keeps its 375:154 shape, the gradient box clips it
|
|
172
|
+
gradientFill: {
|
|
173
|
+
width: '100%',
|
|
174
|
+
position: 'absolute',
|
|
175
|
+
aspectRatio: 375 / 154,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export { DuoGlassBackground, DuoScrollGradient };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
import { StackHeaderProps, Header } from '@react-navigation/stack';
|
|
3
|
+
import Animated, {
|
|
4
|
+
SharedValue,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
} from 'react-native-reanimated';
|
|
7
|
+
|
|
8
|
+
// how far the header is pushed above the top edge
|
|
9
|
+
const DuoHeaderOffsetContext = createContext<SharedValue<number> | undefined>(
|
|
10
|
+
undefined,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* iPhone Duo header that leaves with the content and comes back when scrolling back
|
|
15
|
+
*/
|
|
16
|
+
const DuoQuickReturnHeader: React.FC<
|
|
17
|
+
StackHeaderProps & { offset: SharedValue<number> }
|
|
18
|
+
> = ({ offset, ...props }) => {
|
|
19
|
+
const style = useAnimatedStyle(() => ({
|
|
20
|
+
transform: [{ translateY: -offset.value }],
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<DuoHeaderOffsetContext.Provider value={offset}>
|
|
25
|
+
<Animated.View pointerEvents={'box-none'} style={style}>
|
|
26
|
+
<Header {...props} />
|
|
27
|
+
</Animated.View>
|
|
28
|
+
</DuoHeaderOffsetContext.Provider>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* side bar controls render inside the header; this cancels its offset so they stay put
|
|
34
|
+
*/
|
|
35
|
+
const useSideBarCounterStyle = () => {
|
|
36
|
+
const offset = useContext(DuoHeaderOffsetContext);
|
|
37
|
+
return useAnimatedStyle(() => ({
|
|
38
|
+
transform: [{ translateY: offset ? offset.value : 0 }],
|
|
39
|
+
}));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export { DuoQuickReturnHeader, useSideBarCounterStyle };
|