@momo-kits/foundation 0.168.1-beta.35 → 0.168.1-beta.37
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 +61 -252
- package/Application/BottomTab/BottomTabBar.styles.ts +65 -0
- package/Application/BottomTab/BottomTabBar.tsx +4 -72
- package/Application/BottomTab/DuoRailTabBar.tsx +227 -0
- package/Application/BottomTab/DuoTabBar.styles.ts +81 -0
- package/Application/BottomTab/DuoTabBar.tsx +12 -305
- package/Application/BottomTab/TabScreen.tsx +164 -0
- package/Application/BottomTab/index.tsx +8 -165
- package/Application/ModalScreen.tsx +1 -1
- package/Application/index.ts +6 -2
- package/Application/modalLayout.ts +2 -5
- package/Application/types.tabs.ts +5 -0
- package/Application/types.ts +12 -247
- package/Application/utils.ts +0 -36
- package/Layout/Screen.tsx +63 -648
- package/Layout/index.ts +8 -3
- 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/package.json +1 -1
- package/Application/device.ts +0 -21
|
@@ -16,43 +16,24 @@ import {
|
|
|
16
16
|
} from 'react-native';
|
|
17
17
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
18
18
|
import { useHeaderHeight } from '@react-navigation/elements';
|
|
19
|
-
import { Spacing } from '../Consts';
|
|
20
|
-
import { DuoMetrics, useScreenLayout } from '../Layout/utils';
|
|
21
19
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
22
20
|
import { BottomSheetParams, NavigatorProps } from './types';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
21
|
+
import { isIPhoneDuo } from './utils';
|
|
22
|
+
import { useModalLayout } from './modalLayout';
|
|
25
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Sheet geometry for the current window: foldable placement and size from
|
|
26
|
+
* `useModalLayout`, plus the insets and offsets the keyboard handling needs.
|
|
27
|
+
*/
|
|
26
28
|
export const useBottomSheetLayout = () => {
|
|
27
29
|
const windowSize = useWindowDimensions();
|
|
28
30
|
const isDuo = isIPhoneDuo();
|
|
29
|
-
const
|
|
30
|
-
isDuo &&
|
|
31
|
-
Math.min(windowSize.width, windowSize.height) >= DUO_REGULAR_MIN_WIDTH;
|
|
32
|
-
const isDuoFloating = isDuo && !isDuoCentered;
|
|
33
|
-
const { sideBar } = useScreenLayout();
|
|
34
|
-
const sideColumnWidth = (sideBar?.width ?? 0) - Spacing.M;
|
|
35
|
-
const sideColumn =
|
|
36
|
-
isDuoFloating &&
|
|
37
|
-
sideColumnWidth >= DuoMetrics.controlSize + Spacing.M &&
|
|
38
|
-
sideBar
|
|
39
|
-
? sideBar
|
|
40
|
-
: undefined;
|
|
41
|
-
const hasSideColumn = !!sideColumn;
|
|
31
|
+
const { isFoldable, placement, sheet, scrimOpacity } = useModalLayout();
|
|
42
32
|
// 'screen' is the outer display on iPhone Duo, even when unfolded
|
|
43
33
|
const screenHeight = Dimensions.get('screen').height;
|
|
44
34
|
const heightDevice = isDuo ? windowSize.height : screenHeight;
|
|
45
35
|
const insets = useSafeAreaInsets();
|
|
46
36
|
const heightHeader = useHeaderHeight();
|
|
47
|
-
const topGap = Math.max(
|
|
48
|
-
sideColumn
|
|
49
|
-
? insets.top
|
|
50
|
-
: getDuoTopInset(insets, windowSize.width, windowSize.height),
|
|
51
|
-
Spacing.XL,
|
|
52
|
-
);
|
|
53
|
-
const sideInsetLeft = isDuoFloating ? getSideInset(insets.left) : 0;
|
|
54
|
-
const sideInsetRight = isDuoFloating ? getSideInset(insets.right) : 0;
|
|
55
|
-
const indicatorShift = (sideInsetRight - sideInsetLeft) / 2;
|
|
56
37
|
const isIOS = Platform.OS === 'ios';
|
|
57
38
|
const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
|
|
58
39
|
const keyboardOffset = heightHeader - bottomInset;
|
|
@@ -60,17 +41,11 @@ export const useBottomSheetLayout = () => {
|
|
|
60
41
|
return {
|
|
61
42
|
windowSize,
|
|
62
43
|
isDuo,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
hasSideColumn,
|
|
44
|
+
isFoldable,
|
|
45
|
+
placement,
|
|
46
|
+
sheet,
|
|
47
|
+
scrimOpacity,
|
|
68
48
|
heightDevice,
|
|
69
|
-
insets,
|
|
70
|
-
topGap,
|
|
71
|
-
sideInsetLeft,
|
|
72
|
-
sideInsetRight,
|
|
73
|
-
indicatorShift,
|
|
74
49
|
bottomInset,
|
|
75
50
|
keyboardOffset,
|
|
76
51
|
};
|
|
@@ -124,9 +99,14 @@ export const useDuoKeyboardLift = (
|
|
|
124
99
|
return keyboardLift;
|
|
125
100
|
};
|
|
126
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Slide of the sheet and fade of the scrim behind it, up to the
|
|
104
|
+
* `scrimOpacity` of the modal spec (lighter on foldables).
|
|
105
|
+
*/
|
|
127
106
|
export const useSheetAnimatedStyles = (
|
|
128
107
|
translateY: SharedValue<number>,
|
|
129
108
|
heightDevice: number,
|
|
109
|
+
scrimOpacity: number,
|
|
130
110
|
) => {
|
|
131
111
|
const animatedStyle = useAnimatedStyle(() => {
|
|
132
112
|
return {
|
|
@@ -139,7 +119,7 @@ export const useSheetAnimatedStyles = (
|
|
|
139
119
|
opacity: interpolate(
|
|
140
120
|
translateY.value,
|
|
141
121
|
[0, heightDevice],
|
|
142
|
-
[
|
|
122
|
+
[scrimOpacity, 0],
|
|
143
123
|
Extrapolation.CLAMP,
|
|
144
124
|
),
|
|
145
125
|
};
|
|
@@ -1,169 +1,90 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
ScaledSize,
|
|
5
|
-
TouchableOpacity,
|
|
6
|
-
View,
|
|
7
|
-
} from 'react-native';
|
|
8
|
-
import type { EdgeInsets } from 'react-native-safe-area-context';
|
|
9
|
-
import { Radius, Spacing, Styles } from '../Consts';
|
|
2
|
+
import { PanResponderInstance, View, ViewStyle } from 'react-native';
|
|
3
|
+
import { Spacing, Styles } from '../Consts';
|
|
10
4
|
import layoutStyles from '../Layout/styles';
|
|
11
5
|
import { Text } from '../Text';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
INDICATOR_HEIGHT,
|
|
17
|
-
SIDE_COLUMN_OFFSET,
|
|
18
|
-
styles,
|
|
19
|
-
} from './BottomSheet.styles';
|
|
6
|
+
import { INDICATOR_HEIGHT, styles } from './BottomSheet.styles';
|
|
7
|
+
import { ModalIconButton } from './ModalIconButton';
|
|
8
|
+
import { type ModalLayout } from './modalLayout';
|
|
20
9
|
import { BottomSheetParams, Theme } from './types';
|
|
21
10
|
|
|
22
|
-
type
|
|
11
|
+
type SheetHeaderParams = {
|
|
23
12
|
panHandlers: PanResponderInstance['panHandlers'];
|
|
24
|
-
indicatorShift: number;
|
|
25
13
|
useDivider: boolean;
|
|
26
14
|
theme: Theme;
|
|
27
|
-
hasSideColumn: boolean;
|
|
28
15
|
showBack: boolean;
|
|
29
16
|
onPressBack: () => void;
|
|
30
17
|
onPressClose: () => void;
|
|
31
18
|
iconBack: string;
|
|
32
19
|
options: BottomSheetParams['options'];
|
|
33
20
|
leftOptions: BottomSheetParams['leftOptions'];
|
|
34
|
-
sideColumn?: SideBarLayout;
|
|
35
|
-
windowHeight: number;
|
|
36
|
-
sheetTop: number;
|
|
37
|
-
sideColumnHeight: number;
|
|
38
|
-
sideColumnWidth: number;
|
|
39
21
|
};
|
|
40
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Drag handle and header row: back, title (or a custom header) and close, with
|
|
25
|
+
* the 32 modal icon buttons on every device.
|
|
26
|
+
*/
|
|
41
27
|
export const renderSheetHeader = ({
|
|
42
28
|
panHandlers,
|
|
43
|
-
indicatorShift,
|
|
44
29
|
useDivider,
|
|
45
30
|
theme,
|
|
46
|
-
hasSideColumn,
|
|
47
31
|
showBack,
|
|
48
32
|
onPressBack,
|
|
49
33
|
onPressClose,
|
|
50
34
|
iconBack,
|
|
51
35
|
options,
|
|
52
36
|
leftOptions,
|
|
53
|
-
}:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<View style={{ height: INDICATOR_HEIGHT }}>
|
|
58
|
-
<View
|
|
59
|
-
style={[
|
|
60
|
-
styles.indicator,
|
|
61
|
-
indicatorShift !== 0 && {
|
|
62
|
-
transform: [{ translateX: indicatorShift }],
|
|
63
|
-
},
|
|
64
|
-
]}
|
|
65
|
-
/>
|
|
66
|
-
</View>
|
|
67
|
-
<View
|
|
68
|
-
style={[
|
|
69
|
-
styles.header,
|
|
70
|
-
useDivider && styles.headerDivider,
|
|
71
|
-
useDivider && {
|
|
72
|
-
borderBottomColor: theme.colors.border.default,
|
|
73
|
-
},
|
|
74
|
-
]}
|
|
75
|
-
>
|
|
76
|
-
{showButtons && showBack ? (
|
|
77
|
-
<TouchableOpacity style={styles.iconButton} onPress={onPressBack}>
|
|
78
|
-
<Icon source={iconBack} />
|
|
79
|
-
</TouchableOpacity>
|
|
80
|
-
) : null}
|
|
81
|
-
|
|
82
|
-
{options.header ? (
|
|
83
|
-
<View style={Styles.flex}>{options.header}</View>
|
|
84
|
-
) : (
|
|
85
|
-
<>
|
|
86
|
-
{showButtons && !leftOptions ? (
|
|
87
|
-
<View style={styles.iconButton} />
|
|
88
|
-
) : null}
|
|
89
|
-
<Text
|
|
90
|
-
style={[
|
|
91
|
-
Styles.flex,
|
|
92
|
-
styles.title,
|
|
93
|
-
!showButtons && styles.titleInset,
|
|
94
|
-
]}
|
|
95
|
-
numberOfLines={1}
|
|
96
|
-
typography="header_default_bold"
|
|
97
|
-
>
|
|
98
|
-
{options.title ?? ''}
|
|
99
|
-
</Text>
|
|
100
|
-
</>
|
|
101
|
-
)}
|
|
102
|
-
{showButtons ? (
|
|
103
|
-
<TouchableOpacity style={styles.iconButton} onPress={onPressClose}>
|
|
104
|
-
<Icon source="navigation_close" />
|
|
105
|
-
</TouchableOpacity>
|
|
106
|
-
) : null}
|
|
107
|
-
</View>
|
|
37
|
+
}: SheetHeaderParams) => (
|
|
38
|
+
<View {...panHandlers}>
|
|
39
|
+
<View style={{ height: INDICATOR_HEIGHT }}>
|
|
40
|
+
<View style={styles.indicator} />
|
|
108
41
|
</View>
|
|
109
|
-
);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* iPhone Duo folded: back and close move to the side bar column, drawn on
|
|
114
|
-
* the part of the sheet under the side safe area. They line up with the
|
|
115
|
-
* header row, stay below the status bar cluster on a tall sheet and above a
|
|
116
|
-
* camera at the bottom end of the strip.
|
|
117
|
-
*/
|
|
118
|
-
export const renderSheetSideColumn = ({
|
|
119
|
-
sideColumn,
|
|
120
|
-
windowHeight,
|
|
121
|
-
sheetTop,
|
|
122
|
-
sideColumnHeight,
|
|
123
|
-
sideColumnWidth,
|
|
124
|
-
showBack,
|
|
125
|
-
iconBack,
|
|
126
|
-
onPressBack,
|
|
127
|
-
onPressClose,
|
|
128
|
-
}: SheetPartsParams) => {
|
|
129
|
-
if (!sideColumn) {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
const lowestTop =
|
|
133
|
-
windowHeight - sideColumn.bottom - sideColumnHeight - sheetTop;
|
|
134
|
-
const top = Math.min(
|
|
135
|
-
Math.max(sideColumn.top - sheetTop, SIDE_COLUMN_OFFSET),
|
|
136
|
-
lowestTop,
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
return (
|
|
140
42
|
<View
|
|
141
|
-
pointerEvents={'box-none'}
|
|
142
43
|
style={[
|
|
143
|
-
styles.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
44
|
+
styles.header,
|
|
45
|
+
useDivider && styles.headerDivider,
|
|
46
|
+
useDivider && {
|
|
47
|
+
borderBottomColor: theme.colors.border.default,
|
|
48
|
+
},
|
|
147
49
|
]}
|
|
148
50
|
>
|
|
149
51
|
{showBack ? (
|
|
150
|
-
<
|
|
52
|
+
<ModalIconButton icon={iconBack} onPress={onPressBack} />
|
|
151
53
|
) : null}
|
|
152
|
-
|
|
54
|
+
|
|
55
|
+
{options.header ? (
|
|
56
|
+
<View style={Styles.flex}>{options.header}</View>
|
|
57
|
+
) : (
|
|
58
|
+
<>
|
|
59
|
+
{!leftOptions ? <View style={styles.iconButtonSpace} /> : null}
|
|
60
|
+
<Text
|
|
61
|
+
style={[Styles.flex, styles.title]}
|
|
62
|
+
numberOfLines={1}
|
|
63
|
+
typography="header_default_bold"
|
|
64
|
+
>
|
|
65
|
+
{options.title ?? ''}
|
|
66
|
+
</Text>
|
|
67
|
+
</>
|
|
68
|
+
)}
|
|
69
|
+
<ModalIconButton icon="navigation_close" onPress={onPressClose} />
|
|
153
70
|
</View>
|
|
154
|
-
|
|
155
|
-
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
156
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Footer pinned under the sheet content, `null` when the sheet has none.
|
|
76
|
+
*/
|
|
157
77
|
export const renderSheetFooter = ({
|
|
158
78
|
footerComponent,
|
|
159
79
|
theme,
|
|
160
|
-
sideInsetStyle,
|
|
161
80
|
}: {
|
|
162
81
|
footerComponent: BottomSheetParams['footerComponent'];
|
|
163
82
|
theme: Theme;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
83
|
+
}) => {
|
|
84
|
+
if (!footerComponent) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return (
|
|
167
88
|
<View
|
|
168
89
|
style={[
|
|
169
90
|
layoutStyles.shadow,
|
|
@@ -171,61 +92,56 @@ export const renderSheetFooter = ({
|
|
|
171
92
|
paddingBottom: Spacing.S,
|
|
172
93
|
backgroundColor: theme.colors.background.surface,
|
|
173
94
|
},
|
|
174
|
-
sideInsetStyle,
|
|
175
95
|
]}
|
|
176
96
|
>
|
|
177
97
|
{footerComponent}
|
|
178
98
|
</View>
|
|
179
99
|
);
|
|
100
|
+
};
|
|
180
101
|
|
|
181
102
|
type SheetStyleParams = {
|
|
182
103
|
backgroundColor: string;
|
|
183
104
|
isDuo: boolean;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
topGap: number;
|
|
105
|
+
isFoldable: boolean;
|
|
106
|
+
placement: ModalLayout['placement'];
|
|
107
|
+
sheet: ModalLayout['sheet'];
|
|
188
108
|
keyboardLift: number;
|
|
189
|
-
sideColumn?: SideBarLayout;
|
|
190
|
-
sideColumnHeight: number;
|
|
191
|
-
insets: EdgeInsets;
|
|
192
109
|
showBaseLineDebug: boolean;
|
|
193
110
|
};
|
|
194
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Sheet container style. On foldables the sheet is centered in the placement
|
|
114
|
+
* area, clear of the iPhone Duo control column and on the right half when half
|
|
115
|
+
* folded; the keyboard lift comes off its max height.
|
|
116
|
+
*/
|
|
195
117
|
export const getSheetStyle = ({
|
|
196
118
|
backgroundColor,
|
|
197
119
|
isDuo,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
topGap,
|
|
120
|
+
isFoldable,
|
|
121
|
+
placement,
|
|
122
|
+
sheet,
|
|
202
123
|
keyboardLift,
|
|
203
|
-
sideColumn,
|
|
204
|
-
sideColumnHeight,
|
|
205
|
-
insets,
|
|
206
124
|
showBaseLineDebug,
|
|
207
|
-
}: SheetStyleParams) =>
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
showBaseLineDebug && styles.debugBaseLine,
|
|
231
|
-
];
|
|
125
|
+
}: SheetStyleParams) => {
|
|
126
|
+
const { width, maxHeight } = sheet;
|
|
127
|
+
let foldableStyle: ViewStyle | undefined;
|
|
128
|
+
if (isFoldable && width !== undefined && maxHeight !== undefined) {
|
|
129
|
+
foldableStyle = {
|
|
130
|
+
width,
|
|
131
|
+
marginLeft: placement.x + (placement.width - width) / 2,
|
|
132
|
+
maxHeight: maxHeight - keyboardLift,
|
|
133
|
+
minHeight: sheet.minHeight,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return [
|
|
137
|
+
styles.sheetContainer,
|
|
138
|
+
{
|
|
139
|
+
backgroundColor,
|
|
140
|
+
borderTopLeftRadius: sheet.radius,
|
|
141
|
+
borderTopRightRadius: sheet.radius,
|
|
142
|
+
},
|
|
143
|
+
isDuo && { marginBottom: keyboardLift },
|
|
144
|
+
foldableStyle,
|
|
145
|
+
showBaseLineDebug && styles.debugBaseLine,
|
|
146
|
+
];
|
|
147
|
+
};
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import { StyleSheet } from 'react-native';
|
|
2
2
|
import { Colors, Radius, Spacing } from '../Consts';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
// iPhone Duo inner display: center the sheet instead of spanning the width
|
|
6
|
-
export const DUO_REGULAR_MIN_WIDTH = 600;
|
|
3
|
+
import { MODAL_ICON_BUTTON_SIZE } from './ModalIconButton';
|
|
7
4
|
|
|
8
5
|
export const INDICATOR_HEIGHT = 16;
|
|
9
6
|
export const HEADER_HEIGHT = 56;
|
|
10
7
|
export const DUO_ORIENTATIONS = ['portrait', 'landscape'];
|
|
11
|
-
export const SIDE_COLUMN_OFFSET =
|
|
12
|
-
INDICATOR_HEIGHT + (HEADER_HEIGHT - DuoMetrics.controlSize) / 2;
|
|
13
|
-
|
|
14
|
-
export const getSideInset = (inset: number) => Math.max(inset - Spacing.M, 0);
|
|
15
8
|
|
|
16
9
|
export const styles = StyleSheet.create({
|
|
17
10
|
container: {
|
|
@@ -20,7 +13,7 @@ export const styles = StyleSheet.create({
|
|
|
20
13
|
},
|
|
21
14
|
overlay: {
|
|
22
15
|
flex: 1,
|
|
23
|
-
backgroundColor:
|
|
16
|
+
backgroundColor: Colors.black_20,
|
|
24
17
|
},
|
|
25
18
|
indicator: {
|
|
26
19
|
width: 40,
|
|
@@ -35,25 +28,16 @@ export const styles = StyleSheet.create({
|
|
|
35
28
|
height: HEADER_HEIGHT,
|
|
36
29
|
flexDirection: 'row',
|
|
37
30
|
alignItems: 'center',
|
|
31
|
+
paddingHorizontal: Spacing.M,
|
|
32
|
+
gap: Spacing.S,
|
|
38
33
|
},
|
|
39
34
|
title: { textAlign: 'center' },
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
height: 24,
|
|
44
|
-
alignItems: 'center',
|
|
45
|
-
justifyContent: 'center',
|
|
46
|
-
marginHorizontal: Spacing.M,
|
|
35
|
+
iconButtonSpace: {
|
|
36
|
+
width: MODAL_ICON_BUTTON_SIZE,
|
|
37
|
+
height: MODAL_ICON_BUTTON_SIZE,
|
|
47
38
|
},
|
|
48
39
|
shrink: { flexShrink: 1 },
|
|
49
|
-
sideColumn: {
|
|
50
|
-
position: 'absolute',
|
|
51
|
-
gap: Spacing.M,
|
|
52
|
-
},
|
|
53
|
-
alignRight: { right: 0 },
|
|
54
|
-
alignLeft: { left: 0 },
|
|
55
40
|
debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
|
|
56
41
|
headerDivider: { borderBottomWidth: 1 },
|
|
57
42
|
sheetContainer: { overflow: 'hidden' },
|
|
58
|
-
centered: { alignSelf: 'center' },
|
|
59
43
|
});
|