@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.
Files changed (63) hide show
  1. package/Application/BottomSheet.hooks.ts +27 -17
  2. package/Application/BottomSheet.tsx +35 -27
  3. package/Application/BottomTab/ActiveIndicator.tsx +5 -1
  4. package/Application/BottomTab/BottomTabBar.styles.ts +18 -1
  5. package/Application/BottomTab/BottomTabBar.tsx +14 -16
  6. package/Application/BottomTab/CurvedContainer.tsx +8 -2
  7. package/Application/BottomTab/CustomBottomTabItem.tsx +2 -2
  8. package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
  9. package/Application/BottomTab/DuoTabBar.tsx +15 -3
  10. package/Application/BottomTab/TabScreen.tsx +11 -10
  11. package/Application/BottomTab/index.tsx +5 -5
  12. package/Application/BottomTab/metrics.ts +23 -3
  13. package/Application/BottomTab/usePressLift.ts +3 -3
  14. package/Application/Components/HeaderAnimated.tsx +13 -38
  15. package/Application/Components/HeaderBackground.tsx +3 -16
  16. package/Application/Components/HeaderExtendHeader.tsx +28 -41
  17. package/Application/Components/HeaderLeft.tsx +18 -4
  18. package/Application/Components/HeaderRight.styles.ts +13 -15
  19. package/Application/Components/HeaderTitle.shared.ts +11 -10
  20. package/Application/Components/HeaderToolkitAction.tsx +54 -21
  21. package/Application/Components/NavigationButton.tsx +6 -9
  22. package/Application/Components/SearchHeader.tsx +7 -20
  23. package/Application/Components/useToolkitActions.tsx +22 -21
  24. package/Application/ModalScreen.tsx +19 -15
  25. package/Application/StackScreen.tsx +13 -18
  26. package/Application/index.ts +1 -11
  27. package/Application/modalLayout.ts +1 -0
  28. package/Application/navigationOptions.tsx +15 -6
  29. package/Application/types.header.ts +2 -7
  30. package/Application/types.tabs.ts +0 -1
  31. package/Application/types.ts +0 -7
  32. package/Context/index.ts +3 -1
  33. package/Image/index.tsx +6 -6
  34. package/Input/ErrorView.tsx +59 -0
  35. package/Input/FloatingView.tsx +101 -0
  36. package/Input/Input.tsx +40 -37
  37. package/Input/InputDropDown.tsx +13 -10
  38. package/Input/InputMoney.tsx +17 -17
  39. package/Input/InputMoneyView.tsx +12 -12
  40. package/Input/InputOTP.tsx +46 -34
  41. package/Input/InputPhoneNumber.tsx +48 -43
  42. package/Input/InputSearch.tsx +26 -39
  43. package/Input/InputTextArea.tsx +29 -23
  44. package/Input/RenderTrailing.tsx +79 -0
  45. package/Input/constants.ts +8 -0
  46. package/Input/index.tsx +1 -7
  47. package/Input/utils.ts +126 -7
  48. package/Layout/FloatingButton.tsx +38 -24
  49. package/Layout/Screen.tsx +0 -3
  50. package/Layout/layoutMetrics.ts +43 -34
  51. package/Layout/screenHeader.tsx +52 -55
  52. package/Layout/screenTypes.ts +4 -13
  53. package/Pagination/PaginationScroll.tsx +5 -3
  54. package/Pagination/types.ts +3 -5
  55. package/Popup/PopupNotify.tsx +17 -14
  56. package/Popup/PopupPromotion.tsx +5 -4
  57. package/Popup/types.ts +2 -4
  58. package/Popup/utils.ts +7 -4
  59. package/Skeleton/index.tsx +15 -12
  60. package/package.json +1 -1
  61. package/Application/headerLayout.ts +0 -165
  62. package/Input/common.tsx +0 -257
  63. package/Input/moneyUtils.ts +0 -23
package/Input/utils.ts CHANGED
@@ -1,4 +1,110 @@
1
- import { Localize } from '../Application';
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 < 3) {
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 === 3 && i >= 1) {
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 isMissingLeadingZero = phone.length === 9 && /^[1-9]/.test(phone);
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 (phoneFormatted.length === 0) {
198
+ if (digitCount === 0) {
90
199
  error = localize.translate('enterPhoneNumber');
91
- } else if (phoneFormatted.length < 10 || phoneFormatted.length > 11) {
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 { formatMoneyToNumber, formatNumberToMoney, checkValidPhoneNumber };
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 = 12,
57
+ bottom = DEFAULT_BOTTOM,
48
58
  }: FloatingButtonProps) => {
49
59
  const { theme } = useContext(ApplicationContext);
50
- const enabled = animatedValue != null;
60
+ const hasScrollValue = animatedValue != null;
51
61
  const sharedValue = useNormalizedSharedValue(animatedValue);
52
- const scaledFontSize = useScaleSize(16);
53
- const scaledLineHeight = useScaleSize(22);
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' ? 36 : 48;
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 [showText, setShowText] = useState(true);
71
+ const [isLabelShown, setIsLabelShown] = useState(true);
62
72
 
63
73
  useEffect(() => {
64
74
  if (!label) return;
65
- opacityAnimated.value = withTiming(1, { duration: 100 });
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 || !enabled) return;
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, { duration: 100 });
89
+ opacityAnimated.value = withTiming(0, {
90
+ duration: ANIMATION_DURATION,
91
+ });
80
92
  widthAnimated.value = withTiming(
81
93
  minWidth,
82
- { duration: 100 },
94
+ { duration: ANIMATION_DURATION },
83
95
  finished => {
84
- if (finished) scheduleOnRN(setShowText, false);
96
+ if (finished) scheduleOnRN(setIsLabelShown, false);
85
97
  },
86
98
  );
87
99
  } else {
88
- opacityAnimated.value = withTiming(1, { duration: 100 });
100
+ opacityAnimated.value = withTiming(1, {
101
+ duration: ANIMATION_DURATION,
102
+ });
89
103
  widthAnimated.value = withTiming(
90
104
  maxWidth.value,
91
- { duration: 100 },
105
+ { duration: ANIMATION_DURATION },
92
106
  finished => {
93
- if (finished) scheduleOnRN(setShowText, true);
107
+ if (finished) scheduleOnRN(setIsLabelShown, true);
94
108
  },
95
109
  );
96
110
  }
97
111
  }
98
112
  }
99
113
  },
100
- [label, enabled, minWidth],
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' ? 12 : 24}
163
+ size={size === 'small' ? SMALL_ICON_SIZE : LARGE_ICON_SIZE}
150
164
  color={iconColor}
151
165
  />
152
- {showText && !!label && (
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: 100,
182
- paddingHorizontal: 12,
195
+ borderRadius: PILL_RADIUS,
196
+ paddingHorizontal: Spacing.M,
183
197
  flexWrap: 'wrap',
184
198
  alignSelf: 'flex-end',
185
199
  },
186
200
  label: {
187
- marginLeft: 8,
201
+ marginLeft: Spacing.S,
188
202
  },
189
- alignRight: { right: 12 },
203
+ alignRight: { right: Spacing.M },
190
204
  alignCenter: { alignSelf: 'center' },
191
205
  alignEnd: { alignSelf: 'flex-end' },
192
- small: { height: 36 },
193
- large: { height: 48 },
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
  };
@@ -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,
@@ -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 = getSearchTitleOptions(sharedValue);
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 = getSearchTitleOptions(sharedValue);
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({ ...frameOptions, ...options });
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 = getSearchTitleOptions(sharedValue);
238
- navigation?.instance?.setOptions({
239
- header: undefined,
240
- ...frameOptions,
241
- ...options,
242
- });
243
- }, [navigation?.instance, sharedValue, frameOptions]);
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 (or the stretch frame), clear of the side bar
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 = headerLayout.frameWidth;
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,
@@ -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). Set `false` for a footer that should span the full width —
64
- * e.g. one that already carries its own horizontal padding.
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 (84), or 0 when
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 | 84 | 124 | number;
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.