@momo-kits/foundation 0.169.9-beta.1 → 0.169.9
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 +27 -17
- package/Application/BottomSheet.tsx +35 -27
- package/Application/BottomTab/ActiveIndicator.tsx +5 -1
- package/Application/BottomTab/BottomTabBar.styles.ts +18 -1
- package/Application/BottomTab/BottomTabBar.tsx +14 -16
- package/Application/BottomTab/CurvedContainer.tsx +8 -2
- package/Application/BottomTab/CustomBottomTabItem.tsx +2 -2
- package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
- package/Application/BottomTab/DuoTabBar.tsx +15 -3
- package/Application/BottomTab/TabScreen.tsx +11 -10
- package/Application/BottomTab/index.tsx +5 -5
- package/Application/BottomTab/metrics.ts +23 -3
- package/Application/BottomTab/usePressLift.ts +3 -3
- package/Application/Components/HeaderAnimated.tsx +13 -38
- package/Application/Components/HeaderBackground.tsx +3 -16
- package/Application/Components/HeaderExtendHeader.tsx +28 -41
- package/Application/Components/HeaderLeft.tsx +18 -4
- package/Application/Components/HeaderRight.styles.ts +13 -15
- package/Application/Components/HeaderTitle.shared.ts +11 -10
- package/Application/Components/HeaderToolkitAction.tsx +54 -21
- package/Application/Components/NavigationButton.tsx +6 -9
- package/Application/Components/SearchHeader.tsx +7 -20
- package/Application/Components/useToolkitActions.tsx +22 -21
- package/Application/ModalScreen.tsx +19 -15
- package/Application/StackScreen.tsx +13 -18
- package/Application/index.ts +1 -11
- package/Application/modalLayout.ts +1 -0
- package/Application/navigationOptions.tsx +15 -6
- package/Application/types.header.ts +2 -7
- package/Application/types.tabs.ts +0 -1
- package/Application/types.ts +0 -7
- package/Context/index.ts +3 -1
- package/Image/index.tsx +6 -6
- package/Input/ErrorView.tsx +59 -0
- package/Input/FloatingView.tsx +101 -0
- package/Input/Input.tsx +40 -37
- package/Input/InputDropDown.tsx +13 -10
- package/Input/InputMoney.tsx +17 -17
- package/Input/InputMoneyView.tsx +12 -12
- package/Input/InputOTP.tsx +46 -34
- package/Input/InputPhoneNumber.tsx +48 -43
- package/Input/InputSearch.tsx +26 -39
- package/Input/InputTextArea.tsx +29 -23
- package/Input/RenderTrailing.tsx +79 -0
- package/Input/constants.ts +8 -0
- package/Input/index.tsx +1 -7
- package/Input/utils.ts +126 -7
- package/Layout/FloatingButton.tsx +38 -24
- package/Layout/Screen.tsx +0 -3
- package/Layout/layoutMetrics.ts +43 -34
- package/Layout/screenHeader.tsx +52 -55
- package/Layout/screenTypes.ts +4 -13
- package/Pagination/PaginationScroll.tsx +5 -3
- package/Pagination/types.ts +3 -5
- package/Popup/PopupNotify.tsx +17 -14
- package/Popup/PopupPromotion.tsx +5 -4
- package/Popup/types.ts +2 -4
- package/Popup/utils.ts +7 -4
- package/Skeleton/index.tsx +15 -12
- package/package.json +1 -1
- package/Application/headerLayout.ts +0 -165
- package/Input/common.tsx +0 -257
- package/Input/moneyUtils.ts +0 -23
package/Input/utils.ts
CHANGED
|
@@ -1,4 +1,110 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ViewStyle } from 'react-native';
|
|
2
|
+
import Localize from '../Application/Localize';
|
|
3
|
+
import { type Theme } from '../Application/types';
|
|
4
|
+
import styles from './styles';
|
|
5
|
+
|
|
6
|
+
const SMALL_HEIGHT = 48;
|
|
7
|
+
const LARGE_HEIGHT = 56;
|
|
8
|
+
const SMALL_LEADING_ICON_SIZE = 24;
|
|
9
|
+
const LARGE_LEADING_ICON_SIZE = 32;
|
|
10
|
+
const THOUSANDS_GROUP_SIZE = 3;
|
|
11
|
+
const PHONE_DIGITS_WITHOUT_ZERO = 9;
|
|
12
|
+
const MIN_PHONE_DIGITS = 10;
|
|
13
|
+
const MAX_PHONE_DIGITS = 11;
|
|
14
|
+
|
|
15
|
+
/** vi-VN number format (`.` thousands) for the InputMoney display value. */
|
|
16
|
+
const moneyFormatter = new Intl.NumberFormat('vi-VN');
|
|
17
|
+
|
|
18
|
+
/** Field height for `size` before font scaling; unset means large. */
|
|
19
|
+
const getInputHeight = (size?: 'small' | 'large') => {
|
|
20
|
+
if (size === 'small') {
|
|
21
|
+
return SMALL_HEIGHT;
|
|
22
|
+
}
|
|
23
|
+
return LARGE_HEIGHT;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Leading icon size for `size`; unset means large. */
|
|
27
|
+
const getLeadingIconSize = (size?: 'small' | 'large') => {
|
|
28
|
+
if (size === 'small') {
|
|
29
|
+
return SMALL_LEADING_ICON_SIZE;
|
|
30
|
+
}
|
|
31
|
+
return LARGE_LEADING_ICON_SIZE;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Border color by state; priority is disabled > error > focused > default. */
|
|
35
|
+
const getBorderColor = (
|
|
36
|
+
theme: Theme,
|
|
37
|
+
isFocused: boolean,
|
|
38
|
+
errorMessage?: string,
|
|
39
|
+
isDisabled?: boolean,
|
|
40
|
+
) => {
|
|
41
|
+
let borderColor = theme.colors.border.default;
|
|
42
|
+
|
|
43
|
+
if (isFocused) {
|
|
44
|
+
borderColor = theme.colors.primary;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (errorMessage) {
|
|
48
|
+
borderColor = theme.colors.error.primary;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isDisabled) {
|
|
52
|
+
borderColor = theme.colors.border.disable;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { borderColor };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Container style for `size`; multiline uses `height` as minHeight so the
|
|
60
|
+
* field can grow with its content.
|
|
61
|
+
*/
|
|
62
|
+
const getSizeStyle = (
|
|
63
|
+
size?: 'small' | 'large',
|
|
64
|
+
isMultiline: boolean = false,
|
|
65
|
+
height?: number,
|
|
66
|
+
) => {
|
|
67
|
+
let scaleStyle: ViewStyle = {};
|
|
68
|
+
if (height) {
|
|
69
|
+
scaleStyle = { height };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (isMultiline) {
|
|
73
|
+
return [
|
|
74
|
+
styles.multilineContainer,
|
|
75
|
+
{
|
|
76
|
+
minHeight: height,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (size === 'small') {
|
|
82
|
+
return [styles.smallContainer, scaleStyle];
|
|
83
|
+
}
|
|
84
|
+
return [styles.container, scaleStyle];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Locates the single edited span between `old` and `text`: common prefix
|
|
89
|
+
* length (`start`), common suffix length (`suffix`) and removed + inserted
|
|
90
|
+
* char count (`size`).
|
|
91
|
+
*/
|
|
92
|
+
const findEdit = (old: string, text: string) => {
|
|
93
|
+
let start = 0;
|
|
94
|
+
while (start < text.length && old[start] === text[start]) {
|
|
95
|
+
start += 1;
|
|
96
|
+
}
|
|
97
|
+
const maxSuffix = Math.min(text.length, old.length) - start;
|
|
98
|
+
let suffix = 0;
|
|
99
|
+
while (suffix < maxSuffix) {
|
|
100
|
+
if (old[old.length - 1 - suffix] !== text[text.length - 1 - suffix]) {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
suffix += 1;
|
|
104
|
+
}
|
|
105
|
+
const size = old.length + text.length - 2 * (start + suffix);
|
|
106
|
+
return { old, start, suffix, size };
|
|
107
|
+
};
|
|
2
108
|
|
|
3
109
|
/**
|
|
4
110
|
* Formats a number Vietnamese-style (`.` thousands, `,` decimals) with
|
|
@@ -18,7 +124,7 @@ const formatNumberToMoney = (number: number, currency = '') => {
|
|
|
18
124
|
}
|
|
19
125
|
|
|
20
126
|
const numberString = Math.abs(number).toString();
|
|
21
|
-
if (numberString.length <
|
|
127
|
+
if (numberString.length < THOUSANDS_GROUP_SIZE) {
|
|
22
128
|
const sign = isNegative ? '-' : '';
|
|
23
129
|
return `${sign}${numberString}${currency}`;
|
|
24
130
|
}
|
|
@@ -32,7 +138,7 @@ const formatNumberToMoney = (number: number, currency = '') => {
|
|
|
32
138
|
} else {
|
|
33
139
|
array.push(numberString[i] as string);
|
|
34
140
|
}
|
|
35
|
-
if (count ===
|
|
141
|
+
if (count === THOUSANDS_GROUP_SIZE && i >= 1) {
|
|
36
142
|
array.push('.');
|
|
37
143
|
count = 0;
|
|
38
144
|
}
|
|
@@ -67,6 +173,7 @@ const formatMoneyToNumber = (money: string, currencyUnit: string) => {
|
|
|
67
173
|
return Number(money);
|
|
68
174
|
};
|
|
69
175
|
|
|
176
|
+
/** Digits of `phone`, every other char stripped. */
|
|
70
177
|
function formatPhoneNumber(phone: string): string {
|
|
71
178
|
return phone.replace(/\D/g, '');
|
|
72
179
|
}
|
|
@@ -79,20 +186,32 @@ function checkValidPhoneNumber(phone: string): {
|
|
|
79
186
|
phoneFormatted: string;
|
|
80
187
|
error?: string;
|
|
81
188
|
} {
|
|
82
|
-
const
|
|
189
|
+
const hasLengthWithoutZero = phone.length === PHONE_DIGITS_WITHOUT_ZERO;
|
|
190
|
+
const isMissingLeadingZero = hasLengthWithoutZero && /^[1-9]/.test(phone);
|
|
83
191
|
const phoneNumber = isMissingLeadingZero ? `0${phone}` : phone;
|
|
84
192
|
|
|
85
193
|
const phoneFormatted = formatPhoneNumber(phoneNumber);
|
|
194
|
+
const digitCount = phoneFormatted.length;
|
|
86
195
|
const localize = new Localize({ vi: {}, en: {} });
|
|
87
196
|
let error: string | undefined;
|
|
88
197
|
|
|
89
|
-
if (
|
|
198
|
+
if (digitCount === 0) {
|
|
90
199
|
error = localize.translate('enterPhoneNumber');
|
|
91
|
-
} else if (
|
|
200
|
+
} else if (digitCount < MIN_PHONE_DIGITS || digitCount > MAX_PHONE_DIGITS) {
|
|
92
201
|
error = localize.translate('invalidPhoneNumber');
|
|
93
202
|
}
|
|
94
203
|
|
|
95
204
|
return { phoneFormatted: phoneFormatted, error };
|
|
96
205
|
}
|
|
97
206
|
|
|
98
|
-
export {
|
|
207
|
+
export {
|
|
208
|
+
moneyFormatter,
|
|
209
|
+
getInputHeight,
|
|
210
|
+
getLeadingIconSize,
|
|
211
|
+
getBorderColor,
|
|
212
|
+
getSizeStyle,
|
|
213
|
+
findEdit,
|
|
214
|
+
formatMoneyToNumber,
|
|
215
|
+
formatNumberToMoney,
|
|
216
|
+
checkValidPhoneNumber,
|
|
217
|
+
};
|
|
@@ -13,12 +13,22 @@ import Animated, {
|
|
|
13
13
|
} from 'react-native-reanimated';
|
|
14
14
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
15
15
|
import { ApplicationContext } from '../Context';
|
|
16
|
-
import { Colors } from '../Consts';
|
|
16
|
+
import { Colors, Spacing } from '../Consts';
|
|
17
17
|
import { Icon } from '../Icon';
|
|
18
18
|
import { useScaleSize, exportFontFamily } from '../Text';
|
|
19
19
|
import { type AnimatedCompatValue } from '../Application/utils';
|
|
20
20
|
import { useNormalizedSharedValue } from '../Application/hooks';
|
|
21
21
|
|
|
22
|
+
const DEFAULT_BOTTOM = Spacing.M;
|
|
23
|
+
const PILL_RADIUS = 100;
|
|
24
|
+
const LABEL_FONT_SIZE = 16;
|
|
25
|
+
const LABEL_LINE_HEIGHT = 22;
|
|
26
|
+
const SMALL_SIZE = 36;
|
|
27
|
+
const LARGE_SIZE = 48;
|
|
28
|
+
const SMALL_ICON_SIZE = 12;
|
|
29
|
+
const LARGE_ICON_SIZE = 24;
|
|
30
|
+
const ANIMATION_DURATION = 100;
|
|
31
|
+
|
|
22
32
|
export interface FloatingButtonProps {
|
|
23
33
|
label?: string;
|
|
24
34
|
onPress?: () => void;
|
|
@@ -44,60 +54,64 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
|
|
|
44
54
|
iconColor = '',
|
|
45
55
|
size = 'small',
|
|
46
56
|
animatedValue,
|
|
47
|
-
bottom =
|
|
57
|
+
bottom = DEFAULT_BOTTOM,
|
|
48
58
|
}: FloatingButtonProps) => {
|
|
49
59
|
const { theme } = useContext(ApplicationContext);
|
|
50
|
-
const
|
|
60
|
+
const hasScrollValue = animatedValue != null;
|
|
51
61
|
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
52
|
-
const scaledFontSize = useScaleSize(
|
|
53
|
-
const scaledLineHeight = useScaleSize(
|
|
62
|
+
const scaledFontSize = useScaleSize(LABEL_FONT_SIZE);
|
|
63
|
+
const scaledLineHeight = useScaleSize(LABEL_LINE_HEIGHT);
|
|
54
64
|
const labelFontFamily = exportFontFamily('bold');
|
|
55
65
|
const maxWidth = useSharedValue(0);
|
|
56
|
-
const minWidth = size === 'small' ?
|
|
66
|
+
const minWidth = size === 'small' ? SMALL_SIZE : LARGE_SIZE;
|
|
57
67
|
const opacityAnimated = useSharedValue(0);
|
|
58
68
|
const widthAnimated = useSharedValue<number | null>(null);
|
|
59
69
|
const lastOffset = useSharedValue(0);
|
|
60
70
|
const lastDirection = useSharedValue<string | null>(null);
|
|
61
|
-
const [
|
|
71
|
+
const [isLabelShown, setIsLabelShown] = useState(true);
|
|
62
72
|
|
|
63
73
|
useEffect(() => {
|
|
64
74
|
if (!label) return;
|
|
65
|
-
opacityAnimated.value = withTiming(1, { duration:
|
|
75
|
+
opacityAnimated.value = withTiming(1, { duration: ANIMATION_DURATION });
|
|
66
76
|
}, [label, opacityAnimated]);
|
|
67
77
|
|
|
68
78
|
useAnimatedReaction(
|
|
69
79
|
() => sharedValue.value,
|
|
70
80
|
value => {
|
|
71
81
|
'worklet';
|
|
72
|
-
if (!label || !
|
|
82
|
+
if (!label || !hasScrollValue) return;
|
|
73
83
|
if (value !== lastOffset.value && value > 0) {
|
|
74
84
|
const direction = value > lastOffset.value ? 'down' : 'up';
|
|
75
85
|
lastOffset.value = value;
|
|
76
86
|
if (lastDirection.value !== direction) {
|
|
77
87
|
lastDirection.value = direction;
|
|
78
88
|
if (direction === 'down') {
|
|
79
|
-
opacityAnimated.value = withTiming(0, {
|
|
89
|
+
opacityAnimated.value = withTiming(0, {
|
|
90
|
+
duration: ANIMATION_DURATION,
|
|
91
|
+
});
|
|
80
92
|
widthAnimated.value = withTiming(
|
|
81
93
|
minWidth,
|
|
82
|
-
{ duration:
|
|
94
|
+
{ duration: ANIMATION_DURATION },
|
|
83
95
|
finished => {
|
|
84
|
-
if (finished) scheduleOnRN(
|
|
96
|
+
if (finished) scheduleOnRN(setIsLabelShown, false);
|
|
85
97
|
},
|
|
86
98
|
);
|
|
87
99
|
} else {
|
|
88
|
-
opacityAnimated.value = withTiming(1, {
|
|
100
|
+
opacityAnimated.value = withTiming(1, {
|
|
101
|
+
duration: ANIMATION_DURATION,
|
|
102
|
+
});
|
|
89
103
|
widthAnimated.value = withTiming(
|
|
90
104
|
maxWidth.value,
|
|
91
|
-
{ duration:
|
|
105
|
+
{ duration: ANIMATION_DURATION },
|
|
92
106
|
finished => {
|
|
93
|
-
if (finished) scheduleOnRN(
|
|
107
|
+
if (finished) scheduleOnRN(setIsLabelShown, true);
|
|
94
108
|
},
|
|
95
109
|
);
|
|
96
110
|
}
|
|
97
111
|
}
|
|
98
112
|
}
|
|
99
113
|
},
|
|
100
|
-
[label,
|
|
114
|
+
[label, hasScrollValue, minWidth],
|
|
101
115
|
);
|
|
102
116
|
|
|
103
117
|
const containerStyle = useAnimatedStyle(() => ({
|
|
@@ -146,10 +160,10 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
|
|
|
146
160
|
<TouchableOpacity style={styles.content} onPress={onPress}>
|
|
147
161
|
<Icon
|
|
148
162
|
source={icon}
|
|
149
|
-
size={size === 'small' ?
|
|
163
|
+
size={size === 'small' ? SMALL_ICON_SIZE : LARGE_ICON_SIZE}
|
|
150
164
|
color={iconColor}
|
|
151
165
|
/>
|
|
152
|
-
{
|
|
166
|
+
{isLabelShown && !!label && (
|
|
153
167
|
<Animated.Text
|
|
154
168
|
allowFontScaling={false}
|
|
155
169
|
style={[
|
|
@@ -178,19 +192,19 @@ const styles = StyleSheet.create({
|
|
|
178
192
|
alignContent: 'center',
|
|
179
193
|
alignItems: 'center',
|
|
180
194
|
position: 'absolute',
|
|
181
|
-
borderRadius:
|
|
182
|
-
paddingHorizontal:
|
|
195
|
+
borderRadius: PILL_RADIUS,
|
|
196
|
+
paddingHorizontal: Spacing.M,
|
|
183
197
|
flexWrap: 'wrap',
|
|
184
198
|
alignSelf: 'flex-end',
|
|
185
199
|
},
|
|
186
200
|
label: {
|
|
187
|
-
marginLeft:
|
|
201
|
+
marginLeft: Spacing.S,
|
|
188
202
|
},
|
|
189
|
-
alignRight: { right:
|
|
203
|
+
alignRight: { right: Spacing.M },
|
|
190
204
|
alignCenter: { alignSelf: 'center' },
|
|
191
205
|
alignEnd: { alignSelf: 'flex-end' },
|
|
192
|
-
small: { height:
|
|
193
|
-
large: { height:
|
|
206
|
+
small: { height: SMALL_SIZE },
|
|
207
|
+
large: { height: LARGE_SIZE },
|
|
194
208
|
content: {
|
|
195
209
|
flexDirection: 'row',
|
|
196
210
|
alignItems: 'center',
|
package/Layout/Screen.tsx
CHANGED
|
@@ -67,7 +67,6 @@ const Screen = forwardRef(
|
|
|
67
67
|
headerBackground,
|
|
68
68
|
gradientColor,
|
|
69
69
|
trackingParams,
|
|
70
|
-
contentLayout,
|
|
71
70
|
}: ScreenProps,
|
|
72
71
|
ref: any,
|
|
73
72
|
) => {
|
|
@@ -145,7 +144,6 @@ const Screen = forwardRef(
|
|
|
145
144
|
gradientColor,
|
|
146
145
|
headerBackground,
|
|
147
146
|
layoutOffset,
|
|
148
|
-
contentLayout,
|
|
149
147
|
});
|
|
150
148
|
|
|
151
149
|
// mutates the content node so callers keep its methods plus the header setters
|
|
@@ -225,7 +223,6 @@ const Screen = forwardRef(
|
|
|
225
223
|
useShadowHeader={useShadowHeader}
|
|
226
224
|
gradientColor={gradientColor}
|
|
227
225
|
headerBackground={headerBackground}
|
|
228
|
-
contentLayout={contentLayout}
|
|
229
226
|
/>
|
|
230
227
|
);
|
|
231
228
|
};
|
package/Layout/layoutMetrics.ts
CHANGED
|
@@ -1,57 +1,66 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Header geometry
|
|
3
|
-
* - barHeight: navigation bar row below the status bar
|
|
4
|
-
* - iPhoneMinHeight: header never shorter than this on iPhone (landscape, iPhone Duo)
|
|
5
|
-
* - statusBarMinInset: a smaller top inset means the status bar is not on the top edge
|
|
6
|
-
* - largeWidth / largeMargin: @react-navigation/elements Header gives its content row
|
|
7
|
-
* 5pt side margins on iOS from 414pt wide
|
|
8
|
-
*/
|
|
1
|
+
/** Header geometry. */
|
|
9
2
|
export const HeaderMetrics = {
|
|
3
|
+
/** Navigation bar row below the status bar. */
|
|
10
4
|
barHeight: 52,
|
|
5
|
+
/** Header never shorter than this on iPhone (landscape, iPhone Duo). */
|
|
11
6
|
iPhoneMinHeight: 72,
|
|
7
|
+
/** A smaller top inset means the status bar is not on the top edge. */
|
|
12
8
|
statusBarMinInset: 20,
|
|
9
|
+
/**
|
|
10
|
+
* From this width @react-navigation/elements Header gives its content row
|
|
11
|
+
* `largeMargin` (5pt) side margins on iOS.
|
|
12
|
+
*/
|
|
13
13
|
largeWidth: 414,
|
|
14
14
|
largeMargin: 5,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* iPhone Duo geometry, iOS 27.1 (Xcode 27.1 simulator)
|
|
19
|
-
* - maxAspectRatio: both displays are near 1.42 (inner) and 1.45 (outer), every slab
|
|
20
|
-
* iPhone is 1.77 or taller
|
|
21
|
-
* - innerMinHeight: window height from which the inner display is in use (outer display
|
|
22
|
-
* is 466pt tall in landscape, inner display 669pt)
|
|
23
|
-
* - controlSize / itemPitch: vertical bar of 48pt circular controls, item groups as 48pt
|
|
24
|
-
* wide capsules at a 50pt pitch, 12pt from the strip's inner edge and 12pt apart
|
|
25
|
-
* - firstItemTop: with the status bar in the strip, the first control sits under the
|
|
26
|
-
* camera, clock and connectivity cluster (cluster ends near y 151 on the outer display,
|
|
27
|
-
* y 98 on the inner display in landscape)
|
|
28
|
-
* - outerHeaderHeight / outerCameraCenterY: on the outer display in portrait the header
|
|
29
|
-
* is 4pt taller than iPhoneMinHeight and its title lines up with the camera punchout
|
|
30
|
-
* (y 24–66 at the top of the strip)
|
|
31
|
-
* - edgeInset: on the outer display in landscape the strip holds no status bar, so its
|
|
32
|
-
* controls start this far from the top and stay this far above the bottom, unless a
|
|
33
|
-
* reserved region (the camera at either end) asks for more
|
|
34
|
-
* - innerStatusBarCenterY / innerStatusBarWidth: on the inner display in portrait the
|
|
35
|
-
* clock and connectivity cluster sits at the top right corner (x 550–643 of 669, clock
|
|
36
|
-
* digits centered on y 47); its reserved region, when reported, is taller than the glyphs
|
|
37
|
-
* - floatingTabBarHeight / floatingTabBarMargin / floatingTabBarInsetOverlap: the host
|
|
38
|
-
* app's iOS 26 system tab bar on the inner display in portrait, a 62pt Liquid Glass pill
|
|
39
|
-
* sized to its tabs with the QR as a pinned 62pt platter beside it, centered and at
|
|
40
|
-
* least 20pt from the side edges; it overlaps the home indicator inset by 13pt (21pt
|
|
41
|
-
* above the screen bottom with a 34pt inset)
|
|
42
|
-
*/
|
|
17
|
+
/** iPhone Duo geometry, iOS 27.1 (Xcode 27.1 simulator). */
|
|
43
18
|
export const DuoMetrics = {
|
|
19
|
+
/**
|
|
20
|
+
* Both displays are near 1.42 (inner) and 1.45 (outer); every slab iPhone is
|
|
21
|
+
* 1.77 or taller.
|
|
22
|
+
*/
|
|
44
23
|
maxAspectRatio: 1.6,
|
|
24
|
+
/**
|
|
25
|
+
* Window height from which the inner display is in use (outer display is
|
|
26
|
+
* 466pt tall in landscape, inner display 669pt).
|
|
27
|
+
*/
|
|
45
28
|
innerMinHeight: 600,
|
|
29
|
+
/**
|
|
30
|
+
* Vertical bar of 48pt circular controls; item groups are 48pt wide capsules
|
|
31
|
+
* at a 50pt pitch, 12pt from the strip's inner edge and 12pt apart.
|
|
32
|
+
*/
|
|
46
33
|
controlSize: 48,
|
|
47
34
|
itemPitch: 50,
|
|
35
|
+
/**
|
|
36
|
+
* With the status bar in the strip, the first control sits under the camera,
|
|
37
|
+
* clock and connectivity cluster (ends near y 151 outer, y 98 inner
|
|
38
|
+
* landscape).
|
|
39
|
+
*/
|
|
48
40
|
firstItemTop: 170,
|
|
49
41
|
firstItemTopLandscape: 120,
|
|
42
|
+
/**
|
|
43
|
+
* Outer display in portrait: the header is 4pt taller than iPhoneMinHeight
|
|
44
|
+
* and its title lines up with the camera punchout (y 24–66 at the strip's
|
|
45
|
+
* top).
|
|
46
|
+
*/
|
|
50
47
|
outerHeaderHeight: 76,
|
|
51
48
|
outerCameraCenterY: 45,
|
|
49
|
+
/**
|
|
50
|
+
* Outer display in landscape: no status bar in the strip, so controls keep
|
|
51
|
+
* this gap from both ends unless a reserved region (the camera) needs more.
|
|
52
|
+
*/
|
|
52
53
|
edgeInset: 24,
|
|
54
|
+
/**
|
|
55
|
+
* Inner display in portrait: clock and connectivity cluster at the top right
|
|
56
|
+
* (x 550–643 of 669, digits on y 47); a reported reserved region is taller.
|
|
57
|
+
*/
|
|
53
58
|
innerStatusBarCenterY: 47,
|
|
54
59
|
innerStatusBarWidth: 120,
|
|
60
|
+
/**
|
|
61
|
+
* Host app's iOS 26 tab bar, inner display in portrait: a 62pt Liquid Glass
|
|
62
|
+
* pill plus 62pt QR platter, ≥ 20pt from the sides, 13pt over the home inset.
|
|
63
|
+
*/
|
|
55
64
|
floatingTabBarHeight: 62,
|
|
56
65
|
floatingTabBarMargin: 20,
|
|
57
66
|
floatingTabBarInsetOverlap: 13,
|
package/Layout/screenHeader.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
import { StatusBar, StatusBarStyle } from 'react-native';
|
|
3
3
|
import { StackNavigationOptions } from '@react-navigation/stack';
|
|
4
|
-
import type { SharedValue } from 'react-native-reanimated';
|
|
5
4
|
import Navigation from '../Application/Navigation';
|
|
6
5
|
import { AnimatedHeader, SearchHeaderProps } from '../Application/types';
|
|
7
6
|
import { exportHeaderTitle } from '../Application/navigationOptions';
|
|
@@ -13,11 +12,6 @@ import {
|
|
|
13
12
|
SearchHeader,
|
|
14
13
|
} from '../Application';
|
|
15
14
|
import { DuoGlassBackground } from '../Application/Components/DuoGlassBackground';
|
|
16
|
-
import {
|
|
17
|
-
type HeaderContentLayout,
|
|
18
|
-
useHeaderFrameOptions,
|
|
19
|
-
useHeaderLayout,
|
|
20
|
-
} from '../Application/headerLayout';
|
|
21
15
|
import { getHeaderMargin } from './utils';
|
|
22
16
|
import { ANIMATED_BANNER_HEIGHT } from './screenTypes';
|
|
23
17
|
import { HeaderType } from './types';
|
|
@@ -35,31 +29,6 @@ type ScreenHeaderParams = {
|
|
|
35
29
|
gradientColor?: string;
|
|
36
30
|
headerBackground?: string;
|
|
37
31
|
layoutOffset: number;
|
|
38
|
-
contentLayout?: HeaderContentLayout;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Transparent header for an `inputSearchProps` Screen: its title fades out over the
|
|
43
|
-
* first 50pt of scroll while the search collapses into the bar.
|
|
44
|
-
*/
|
|
45
|
-
const getSearchTitleOptions = (
|
|
46
|
-
sharedValue: SharedValue<number>,
|
|
47
|
-
): StackNavigationOptions => {
|
|
48
|
-
return {
|
|
49
|
-
headerShown: true,
|
|
50
|
-
headerTransparent: true,
|
|
51
|
-
headerBackground: () => null,
|
|
52
|
-
headerTitle: (props: any) => (
|
|
53
|
-
<HeaderTitle
|
|
54
|
-
{...props}
|
|
55
|
-
interpolate={{
|
|
56
|
-
inputRange: [0, 50],
|
|
57
|
-
outputRange: [1, 0],
|
|
58
|
-
}}
|
|
59
|
-
animatedValue={sharedValue}
|
|
60
|
-
/>
|
|
61
|
-
),
|
|
62
|
-
};
|
|
63
32
|
};
|
|
64
33
|
|
|
65
34
|
/**
|
|
@@ -79,12 +48,9 @@ export const useScreenHeader = ({
|
|
|
79
48
|
gradientColor,
|
|
80
49
|
headerBackground,
|
|
81
50
|
layoutOffset,
|
|
82
|
-
contentLayout,
|
|
83
51
|
}: ScreenHeaderParams) => {
|
|
84
52
|
const currentTint = useRef<string | undefined>(undefined);
|
|
85
53
|
const appliedHeaderMode = useRef<string | undefined>(undefined);
|
|
86
|
-
const headerLayout = useHeaderLayout(contentLayout);
|
|
87
|
-
const frameOptions = useHeaderFrameOptions(contentLayout);
|
|
88
54
|
|
|
89
55
|
// keepTitle leaves a title the screen set itself when the same type is re-applied
|
|
90
56
|
const setHeaderType = useCallback(
|
|
@@ -113,7 +79,21 @@ export const useScreenHeader = ({
|
|
|
113
79
|
headerTitle: (props: any) => <HeaderTitle {...props} />,
|
|
114
80
|
};
|
|
115
81
|
if (inputSearchProps) {
|
|
116
|
-
options =
|
|
82
|
+
options = {
|
|
83
|
+
headerShown: true,
|
|
84
|
+
headerTransparent: true,
|
|
85
|
+
headerBackground: () => null,
|
|
86
|
+
headerTitle: (props: any) => (
|
|
87
|
+
<HeaderTitle
|
|
88
|
+
{...props}
|
|
89
|
+
interpolate={{
|
|
90
|
+
inputRange: [0, 50],
|
|
91
|
+
outputRange: [1, 0],
|
|
92
|
+
}}
|
|
93
|
+
animatedValue={sharedValue}
|
|
94
|
+
/>
|
|
95
|
+
),
|
|
96
|
+
};
|
|
117
97
|
}
|
|
118
98
|
break;
|
|
119
99
|
|
|
@@ -147,7 +127,21 @@ export const useScreenHeader = ({
|
|
|
147
127
|
headerTitle: (props: any) => <HeaderTitle {...props} />,
|
|
148
128
|
};
|
|
149
129
|
if (inputSearchProps) {
|
|
150
|
-
options =
|
|
130
|
+
options = {
|
|
131
|
+
headerShown: true,
|
|
132
|
+
headerTransparent: true,
|
|
133
|
+
headerBackground: () => null,
|
|
134
|
+
headerTitle: (props: any) => (
|
|
135
|
+
<HeaderTitle
|
|
136
|
+
{...props}
|
|
137
|
+
interpolate={{
|
|
138
|
+
inputRange: [0, 50],
|
|
139
|
+
outputRange: [1, 0],
|
|
140
|
+
}}
|
|
141
|
+
animatedValue={sharedValue}
|
|
142
|
+
/>
|
|
143
|
+
),
|
|
144
|
+
};
|
|
151
145
|
}
|
|
152
146
|
}
|
|
153
147
|
|
|
@@ -155,11 +149,7 @@ export const useScreenHeader = ({
|
|
|
155
149
|
delete options.headerTitle;
|
|
156
150
|
}
|
|
157
151
|
|
|
158
|
-
navigation?.instance?.setOptions({
|
|
159
|
-
header: undefined,
|
|
160
|
-
...frameOptions,
|
|
161
|
-
...options,
|
|
162
|
-
});
|
|
152
|
+
navigation?.instance?.setOptions({ header: undefined, ...options });
|
|
163
153
|
},
|
|
164
154
|
[
|
|
165
155
|
gradientColor,
|
|
@@ -170,7 +160,6 @@ export const useScreenHeader = ({
|
|
|
170
160
|
useShadowHeader,
|
|
171
161
|
hasSideBar,
|
|
172
162
|
quickReturnOptions,
|
|
173
|
-
frameOptions,
|
|
174
163
|
],
|
|
175
164
|
);
|
|
176
165
|
|
|
@@ -218,7 +207,7 @@ export const useScreenHeader = ({
|
|
|
218
207
|
};
|
|
219
208
|
}
|
|
220
209
|
|
|
221
|
-
navigation?.instance?.setOptions(
|
|
210
|
+
navigation?.instance?.setOptions(options);
|
|
222
211
|
},
|
|
223
212
|
[
|
|
224
213
|
gradientColor,
|
|
@@ -229,23 +218,32 @@ export const useScreenHeader = ({
|
|
|
229
218
|
hasSideBar,
|
|
230
219
|
layoutOffset,
|
|
231
220
|
quickReturnOptions,
|
|
232
|
-
frameOptions,
|
|
233
221
|
],
|
|
234
222
|
);
|
|
235
223
|
|
|
236
224
|
const setAnimatedSearch = useCallback(() => {
|
|
237
|
-
const options =
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
225
|
+
const options: StackNavigationOptions = {
|
|
226
|
+
headerShown: true,
|
|
227
|
+
headerTransparent: true,
|
|
228
|
+
headerBackground: () => null,
|
|
229
|
+
headerTitle: (props: any) => (
|
|
230
|
+
<HeaderTitle
|
|
231
|
+
{...props}
|
|
232
|
+
interpolate={{
|
|
233
|
+
inputRange: [0, 50],
|
|
234
|
+
outputRange: [1, 0],
|
|
235
|
+
}}
|
|
236
|
+
animatedValue={sharedValue}
|
|
237
|
+
/>
|
|
238
|
+
),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
navigation?.instance?.setOptions({ header: undefined, ...options });
|
|
242
|
+
}, [navigation?.instance, sharedValue]);
|
|
244
243
|
|
|
245
|
-
// the search field spans the content
|
|
246
|
-
// and status bar clusters
|
|
244
|
+
// the search field spans the content, clear of the side bar and status bar clusters
|
|
247
245
|
const setSearchHeader = (params: SearchHeaderProps) => {
|
|
248
|
-
let searchWidth =
|
|
246
|
+
let searchWidth = layout.width - 24;
|
|
249
247
|
let searchLeft = Spacing.M;
|
|
250
248
|
if (layout.sideBar) {
|
|
251
249
|
searchWidth = layout.contentWidth - Spacing.M * 2;
|
|
@@ -256,7 +254,6 @@ export const useScreenHeader = ({
|
|
|
256
254
|
searchWidth -= clearance.left + clearance.right;
|
|
257
255
|
}
|
|
258
256
|
const options: StackNavigationOptions = {
|
|
259
|
-
...frameOptions,
|
|
260
257
|
headerRight: undefined,
|
|
261
258
|
headerTitleContainerStyle: {
|
|
262
259
|
width: searchWidth,
|
package/Layout/screenTypes.ts
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
import { FloatingButtonProps } from './FloatingButton';
|
|
10
10
|
import { HeaderType } from './types';
|
|
11
11
|
import { InputRef } from '../Input';
|
|
12
|
-
import type { HeaderContentLayout } from '../Application/headerLayout';
|
|
13
12
|
|
|
14
13
|
export const ANIMATED_BANNER_HEIGHT = 210;
|
|
15
14
|
|
|
@@ -60,9 +59,8 @@ export interface ScreenProps extends ViewProps {
|
|
|
60
59
|
|
|
61
60
|
/**
|
|
62
61
|
* Optional. If `true`, the footer is inset by the standard horizontal margin
|
|
63
|
-
* (12pt each side)
|
|
64
|
-
*
|
|
65
|
-
* Defaults to `true`, on every platform.
|
|
62
|
+
* (12pt each side); `false` spans the full width, e.g. for a footer with its
|
|
63
|
+
* own horizontal padding. Defaults to `true`, on every platform.
|
|
66
64
|
*/
|
|
67
65
|
marginFooter?: boolean;
|
|
68
66
|
|
|
@@ -78,17 +76,10 @@ export interface ScreenProps extends ViewProps {
|
|
|
78
76
|
layoutOffset?: -8 | -24 | -56 | number;
|
|
79
77
|
|
|
80
78
|
/**
|
|
81
|
-
* Optional. Width of the right header. Defaults to the toolkit width (
|
|
79
|
+
* Optional. Width of the right header. Defaults to the toolkit width (73), or 0 when
|
|
82
80
|
* iPhone Duo floats the toolkit into its side bar.
|
|
83
81
|
*/
|
|
84
|
-
headerRightWidth?: 0 |
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Optional. `stretch` centers back, title or search and the right cluster of the
|
|
88
|
-
* header in a 600pt frame on wide windows (foldable inner display, tablet); the
|
|
89
|
-
* background and status bar stay full width. Defaults to `grid`.
|
|
90
|
-
*/
|
|
91
|
-
contentLayout?: HeaderContentLayout;
|
|
82
|
+
headerRightWidth?: 0 | 73 | 110 | number;
|
|
92
83
|
|
|
93
84
|
/**
|
|
94
85
|
* Optional. Props for the floating action button.
|