@momo-kits/foundation 0.167.1-duo.12 → 0.167.1-duo.13

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.
@@ -31,6 +31,7 @@ import { DuoTabBar } from './DuoTabBar';
31
31
  import { DuoBarCompressionProvider } from './DuoBarCompression';
32
32
  import { ApplicationContext, MiniAppContext } from '../../Context';
33
33
  import { useScreenLayout } from '../../Layout';
34
+ import { getHeaderRowOptions } from '../../Layout/utils';
34
35
 
35
36
  const Tab = createBottomTabNavigator();
36
37
  const Stack = createStackNavigator();
@@ -39,13 +40,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
39
40
  const { navigator } = useContext(ApplicationContext);
40
41
  const context = useContext<any>(MiniAppContext);
41
42
  const { nested, options, screen, initialParams } = route?.params;
42
- const { headerHeight, headerStatusBarHeight } = useScreenLayout();
43
- const headerStyleOptions = {
44
- headerStyle: {
45
- height: headerHeight,
46
- },
47
- headerStatusBarHeight,
48
- };
43
+ const headerStyleOptions = getHeaderRowOptions(useScreenLayout());
49
44
 
50
45
  const opacityValue = 0.3;
51
46
  const scaleValue = 0.97;
@@ -16,7 +16,7 @@ import { SearchHeaderProps } from '../types';
16
16
  import BackgroundImageView from './BackgroundImageView';
17
17
  import { AnimatedCompatValue, useNormalizedSharedValue } from '../utils';
18
18
  import { useScreenLayout } from '../../Layout';
19
- import { HeaderMetrics } from '../../Layout/utils';
19
+ import { DuoMetrics, HeaderMetrics } from '../../Layout/utils';
20
20
 
21
21
  const SCREEN_PADDING = 12;
22
22
  const BACK_WIDTH = 28;
@@ -51,17 +51,27 @@ const HeaderExtendHeader: React.FC<{
51
51
  const headerBackground = customBackground ?? theme.assets?.headerBackground;
52
52
  const layout = useScreenLayout();
53
53
 
54
+ // the iPhone Duo status bar row keeps side bar sized back and toolkit
55
+ const backWidth = layout.statusBarClearance
56
+ ? DuoMetrics.controlSize
57
+ : BACK_WIDTH;
58
+ const toolkitWidth = layout.statusBarClearance
59
+ ? DuoMetrics.itemPitch * 2 + SCREEN_PADDING
60
+ : TOOLKIT_WIDTH;
54
61
  const leftPosition =
55
62
  inputSearchProps?.leftPosition ||
56
- (layout.sideBar ? SCREEN_PADDING : BACK_WIDTH + 20);
63
+ (layout.sideBar ? SCREEN_PADDING : backWidth + 20);
57
64
  const headerRightWidth =
58
- customHeaderRightWidth ?? (layout.sideBar ? 0 : TOOLKIT_WIDTH);
65
+ customHeaderRightWidth ?? (layout.sideBar ? 0 : toolkitWidth);
59
66
  const contentLeft = layout.insetLeft + SCREEN_PADDING;
60
67
  const contentRight = layout.insetLeft + layout.contentWidth;
61
- const collapsedLeft = layout.insetLeft + leftPosition;
68
+ // collapsed on the status bar row, the search stays between its clusters
69
+ const collapsedLeft =
70
+ layout.insetLeft + leftPosition + (layout.statusBarClearance?.left ?? 0);
71
+ const collapsedRight = contentRight - (layout.statusBarClearance?.right ?? 0);
62
72
  const searchWidth = layout.contentWidth - SCREEN_PADDING * 2;
63
73
  const collapsedSearchWidth =
64
- contentRight - collapsedLeft - SCREEN_PADDING - headerRightWidth;
74
+ collapsedRight - collapsedLeft - SCREEN_PADDING - headerRightWidth;
65
75
  // a row taller than barHeight (iPhone Duo) centers the collapsed search on the title
66
76
  const collapsedRaise =
67
77
  (layout.headerHeight -
@@ -6,7 +6,7 @@ import { PopupNotify } from '../../Popup';
6
6
  import { NavigationButton } from './NavigationButton';
7
7
  import { SideBarButton } from './SideBarButton';
8
8
  import Animated from 'react-native-reanimated';
9
- import { useSideBarItemStyle } from '../../Layout/utils';
9
+ import { useScreenLayout, useSideBarItemStyle } from '../../Layout/utils';
10
10
  import { useSideBarCounterStyle } from './DuoQuickReturnHeader';
11
11
 
12
12
  /**
@@ -23,6 +23,7 @@ const HeaderLeft: React.FC<HeaderBackProps> = ({
23
23
  const { navigator } = useContext(ApplicationContext);
24
24
  const sideBarStyle = useSideBarItemStyle('left', 0);
25
25
  const counterStyle = useSideBarCounterStyle();
26
+ const { statusBarClearance } = useScreenLayout();
26
27
 
27
28
  const goBack = useCallback(() => {
28
29
  const canGoBack = navigator?.ref?.current?.canGoBack?.();
@@ -95,6 +96,20 @@ const HeaderLeft: React.FC<HeaderBackProps> = ({
95
96
  );
96
97
  }
97
98
 
99
+ // on the status bar row the controls keep the side bar size
100
+ if (statusBarClearance) {
101
+ return (
102
+ <View style={styles.headerLeft}>
103
+ <SideBarButton
104
+ accessibilityLabel="btn_navigation_back"
105
+ icon="arrow-back"
106
+ tintColor={tintColor}
107
+ onPress={goBackSafe}
108
+ />
109
+ </View>
110
+ );
111
+ }
112
+
98
113
  return (
99
114
  <View style={styles.headerLeft}>
100
115
  <NavigationButton
@@ -14,7 +14,7 @@ import { Tool, ToolGroup } from '../types';
14
14
  import { Icon } from '../../Icon';
15
15
  import { Text } from '../../Text';
16
16
  import Animated from 'react-native-reanimated';
17
- import { useSideBarItemStyle } from '../../Layout/utils';
17
+ import { useScreenLayout, useSideBarItemStyle } from '../../Layout/utils';
18
18
  import { useSideBarCounterStyle } from './DuoQuickReturnHeader';
19
19
  import { SideBarButton, SideBarGroup, SideBarGroupItem } from './SideBarButton';
20
20
  import { useDuoBarCompression } from '../BottomTab/DuoBarCompression';
@@ -85,6 +85,7 @@ const HeaderToolkitAction: React.FC<any> = ({
85
85
  const context = useContext<any>(MiniAppContext);
86
86
  const { toolbarCompressed, toolbarHidden, expandToolbar, setToolbarHeight } =
87
87
  useDuoBarCompression();
88
+ const { statusBarClearance } = useScreenLayout();
88
89
 
89
90
  const [isFavorite, setIsFavorite] = useState<boolean>(false);
90
91
  const [isLoading, setIsLoading] = useState(false);
@@ -358,6 +359,39 @@ const HeaderToolkitAction: React.FC<any> = ({
358
359
  );
359
360
  }
360
361
 
362
+ // on the status bar row the controls keep the side bar size
363
+ if (statusBarClearance) {
364
+ return (
365
+ <View style={styles.sideBarRow}>
366
+ {useShortcut && (
367
+ <SideBarButton
368
+ disabled={isLoading}
369
+ icon={iconShortcut}
370
+ onPress={shortcutOnPress}
371
+ tintColor={tintColor}
372
+ showBadge={showBadge}
373
+ />
374
+ )}
375
+ <SideBarGroup tintColor={tintColor} horizontal>
376
+ {useHelpCenter && (
377
+ <SideBarGroupItem
378
+ accessibilityLabel={'btn_navigation_help_center'}
379
+ icon={'help_center'}
380
+ tintColor={tintColor}
381
+ onPress={onPressHelpCenter}
382
+ />
383
+ )}
384
+ <SideBarGroupItem
385
+ accessibilityLabel={'btn_navigation_close'}
386
+ icon={closeIcon}
387
+ tintColor={tintColor}
388
+ onPress={onClose}
389
+ />
390
+ </SideBarGroup>
391
+ </View>
392
+ );
393
+ }
394
+
361
395
  return (
362
396
  <View style={Styles.row}>
363
397
  {useShortcut && (
@@ -471,6 +505,10 @@ const styles = StyleSheet.create({
471
505
  sideBarColumn: {
472
506
  gap: Spacing.M,
473
507
  },
508
+ sideBarRow: {
509
+ flexDirection: 'row',
510
+ gap: Spacing.M,
511
+ },
474
512
  headerLeft: {
475
513
  marginLeft: 12,
476
514
  },
@@ -33,15 +33,29 @@ const useHeaderFontFamily = (): string => {
33
33
  */
34
34
  const HEADER_ACTIONS_WIDTH = 172;
35
35
 
36
+ /**
37
+ * extra width the 48pt back and toolkit take on the iPhone Duo status bar row
38
+ */
39
+ const STATUS_BAR_ROW_ACTIONS_EXTRA = 55;
40
+
36
41
  /**
37
42
  * max title width, the iPhone Duo side bar takes back and toolkit out of the bar
38
43
  */
39
44
  const useTitleMaxWidth = () => {
40
- const { width, contentWidth, sideBar } = useScreenLayout();
45
+ const { width, contentWidth, sideBar, statusBarClearance } =
46
+ useScreenLayout();
47
+ const clearance = statusBarClearance
48
+ ? statusBarClearance.left + statusBarClearance.right
49
+ : 0;
41
50
  if (sideBar) {
42
- return contentWidth - Spacing.M * 2;
51
+ return contentWidth - Spacing.M * 2 - clearance;
43
52
  }
44
- return width - HEADER_ACTIONS_WIDTH;
53
+ return (
54
+ width -
55
+ HEADER_ACTIONS_WIDTH -
56
+ (statusBarClearance ? STATUS_BAR_ROW_ACTIONS_EXTRA : 0) -
57
+ clearance
58
+ );
45
59
  };
46
60
 
47
61
  type HeaderTitleInterpolate = {
@@ -8,6 +8,7 @@ import { ApplicationContext, MiniAppContext } from '../../Context';
8
8
  import { Text } from '../../Text';
9
9
  import { useNormalizedSharedValue } from '../utils';
10
10
  import { useScreenLayout } from '../../Layout';
11
+ import { DuoMetrics } from '../../Layout/utils';
11
12
 
12
13
  const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
13
14
  (
@@ -22,8 +23,9 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
22
23
  ) => {
23
24
  const { theme, navigator } = useContext(ApplicationContext);
24
25
  const context = useContext<any>(MiniAppContext);
25
- const BACK_WIDTH = 28;
26
26
  const layout = useScreenLayout();
27
+ // the iPhone Duo status bar row keeps a side bar sized back button
28
+ const BACK_WIDTH = layout.statusBarClearance ? DuoMetrics.controlSize : 28;
27
29
 
28
30
  const sharedValue = useNormalizedSharedValue(animatedValue);
29
31
  const leftPosition = props?.leftPosition ?? BACK_WIDTH + 20;
@@ -34,6 +36,10 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
34
36
  if (layout.sideBar) {
35
37
  searchMaxWidth = layout.contentWidth - Spacing.M * 2 - headerRightWidth;
36
38
  }
39
+ if (layout.statusBarClearance) {
40
+ searchMaxWidth -=
41
+ layout.statusBarClearance.left + layout.statusBarClearance.right;
42
+ }
37
43
 
38
44
  const backgroundStyle = useAnimatedStyle(() => {
39
45
  const t = Math.min(Math.max(sharedValue.value / 100, 0), 1);
@@ -54,19 +54,30 @@ const SideBarButton: React.FC<NavigationButtonProps> = ({
54
54
  };
55
55
 
56
56
  /**
57
- * 48pt wide glass capsule grouping related controls, one item per 50pt
57
+ * 48pt glass capsule grouping related controls, one item per 50pt, laid out vertically
58
+ * in the side bar or horizontally on a header row
58
59
  */
59
60
  const SideBarGroup: React.FC<{
60
61
  tintColor?: string;
62
+ horizontal?: boolean;
61
63
  style?: StyleProp<ViewStyle>;
62
64
  children: ReactNode;
63
- }> = ({ tintColor, style, children }) => {
65
+ }> = ({ tintColor, horizontal = false, style, children }) => {
64
66
  const count = React.Children.toArray(children).length;
65
- const height =
67
+ const length =
66
68
  count > 1 ? count * DuoMetrics.itemPitch : DuoMetrics.controlSize;
67
69
 
68
70
  return (
69
- <View style={[styles.control, getGlassStyle(tintColor), { height }, style]}>
71
+ <View
72
+ style={[
73
+ styles.control,
74
+ getGlassStyle(tintColor),
75
+ horizontal
76
+ ? { width: length, flexDirection: 'row' }
77
+ : { height: length },
78
+ style,
79
+ ]}
80
+ >
70
81
  {children}
71
82
  </View>
72
83
  );
@@ -31,6 +31,7 @@ import { HeaderTitle } from './Components/HeaderTitle';
31
31
  import { HeaderBackground } from './Components/HeaderBackground';
32
32
  import { type EdgeInsets } from 'react-native-safe-area-context';
33
33
  import { useScreenLayout } from '../Layout';
34
+ import { getHeaderRowOptions } from '../Layout/utils';
34
35
  import { ReservedRegionsProvider } from '../Layout/ReservedRegions';
35
36
 
36
37
  const Stack = createStackNavigator();
@@ -107,7 +108,7 @@ const Navigation: React.FC<any> = ({
107
108
  const navigationRef = useRef<any>(null);
108
109
  const isReady = useRef(false);
109
110
  const navigator = useRef(new Navigator(navigationRef, isReady));
110
- const { headerHeight, headerStatusBarHeight } = useScreenLayout();
111
+ const headerRowOptions = getHeaderRowOptions(useScreenLayout());
111
112
  const syncCanGoBack = useNativeCanGoBack();
112
113
  const isWhiteList = isWhiteListDesignSystem(context);
113
114
  const fontFamily = isWhiteList ? 'MomoTrustSans' : 'SFProText';
@@ -204,10 +205,7 @@ const Navigation: React.FC<any> = ({
204
205
  gestureEnabled: false,
205
206
  headerTitleAlign: 'center',
206
207
  headerTintColor: theme.colors.text.default,
207
- headerStyle: {
208
- height: headerHeight,
209
- },
210
- headerStatusBarHeight,
208
+ ...headerRowOptions,
211
209
  }}
212
210
  >
213
211
  <Stack.Screen
package/Layout/Screen.tsx CHANGED
@@ -525,6 +525,10 @@ const Screen = forwardRef(
525
525
  searchWidth = layout.contentWidth - Spacing.M * 2;
526
526
  searchLeft = Spacing.M - getHeaderMargin(layout.width);
527
527
  }
528
+ if (layout.statusBarClearance) {
529
+ searchWidth -=
530
+ layout.statusBarClearance.left + layout.statusBarClearance.right;
531
+ }
528
532
  const options: StackNavigationOptions = {
529
533
  headerRight: undefined,
530
534
  headerTitleContainerStyle: {
package/Layout/utils.ts CHANGED
@@ -77,6 +77,9 @@ const HeaderMetrics = {
77
77
  * - edgeInset: on the outer display in landscape the strip holds no status bar, so its
78
78
  * controls start this far from the top and stay this far above the bottom, unless a
79
79
  * reserved region (the camera at either end) asks for more
80
+ * - innerStatusBarCenterY / innerStatusBarWidth: on the inner display in portrait the
81
+ * clock and connectivity cluster sits at the top right corner (x 550–643 of 669, clock
82
+ * digits centered on y 47); its reserved region, when reported, is taller than the glyphs
80
83
  */
81
84
  const DuoMetrics = {
82
85
  maxAspectRatio: 1.6,
@@ -88,6 +91,8 @@ const DuoMetrics = {
88
91
  outerHeaderHeight: 76,
89
92
  outerCameraCenterY: 45,
90
93
  edgeInset: 24,
94
+ innerStatusBarCenterY: 47,
95
+ innerStatusBarWidth: 120,
91
96
  };
92
97
 
93
98
  const NO_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };
@@ -124,6 +129,39 @@ const getStripOcclusion = (
124
129
  { top: 0, topCenter: 0, bottom: 0 },
125
130
  );
126
131
 
132
+ /**
133
+ * how far the clock and battery clusters of a status bar on the top edge reach in from
134
+ * each side. `undefined` without reserved regions there
135
+ */
136
+ const getStatusBarClusters = (
137
+ regions: ReservedRegion[],
138
+ windowWidth: number,
139
+ topInset: number,
140
+ ) => {
141
+ const midX = windowWidth / 2;
142
+ const clusters = regions.filter(
143
+ region =>
144
+ region.y < topInset &&
145
+ region.y + region.height <= topInset &&
146
+ (region.x + region.width <= midX || region.x >= midX),
147
+ );
148
+ if (!clusters.length) {
149
+ return undefined;
150
+ }
151
+
152
+ return clusters.reduce(
153
+ (result, region) => {
154
+ if (region.x < midX) {
155
+ result.left = Math.max(result.left, region.x + region.width);
156
+ } else {
157
+ result.right = Math.max(result.right, windowWidth - region.x);
158
+ }
159
+ return result;
160
+ },
161
+ { left: 0, right: 0 },
162
+ );
163
+ };
164
+
127
165
  const isIPhone = () =>
128
166
  Platform.OS === 'ios' && Platform.constants.interfaceIdiom === 'phone';
129
167
 
@@ -232,6 +270,17 @@ export type ScreenLayout = {
232
270
  * bars to the side. `undefined` while the bars stay horizontal.
233
271
  */
234
272
  sideBar?: SideBarLayout;
273
+
274
+ /**
275
+ * Room the header row keeps from each side for the clock and battery clusters, when
276
+ * iPhone Duo puts the row on the status bar (inner display in portrait).
277
+ */
278
+ statusBarClearance?: StatusBarClearance;
279
+ };
280
+
281
+ export type StatusBarClearance = {
282
+ left: number;
283
+ right: number;
235
284
  };
236
285
 
237
286
  /**
@@ -280,6 +329,23 @@ const useScreenLayout = (): ScreenLayout => {
280
329
  };
281
330
  }
282
331
 
332
+ let statusBarClearance: StatusBarClearance | undefined;
333
+ const isInnerPortrait = height > width && width >= DuoMetrics.innerMinHeight;
334
+ if (isInnerPortrait && insets.top >= HeaderMetrics.statusBarMinInset) {
335
+ // the row shares the status bar band, between its clusters
336
+ const clusters = getStatusBarClusters(
337
+ reservedRegions,
338
+ width,
339
+ insets.top,
340
+ ) ?? { left: 0, right: DuoMetrics.innerStatusBarWidth + Spacing.S };
341
+ // center on the clock digits, the region center sits above them
342
+ headerLayout = {
343
+ headerHeight: DuoMetrics.innerStatusBarCenterY * 2,
344
+ headerStatusBarHeight: 0,
345
+ };
346
+ statusBarClearance = clusters;
347
+ }
348
+
283
349
  const sideInset = Math.max(insets.left, insets.right);
284
350
  let sideBar: SideBarLayout | undefined;
285
351
  // narrower strips cannot host a control 12pt from the inner edge
@@ -295,7 +361,9 @@ const useScreenLayout = (): ScreenLayout => {
295
361
  height,
296
362
  );
297
363
 
298
- let top = insets.top + HeaderMetrics.barHeight + Spacing.M;
364
+ let top = statusBarClearance
365
+ ? headerLayout.headerHeight + Spacing.M
366
+ : insets.top + HeaderMetrics.barHeight + Spacing.M;
299
367
  if (insets.top < HeaderMetrics.statusBarMinInset) {
300
368
  // status bar lives in the strip: start under its cluster, as the native bar does
301
369
  const isLandscape = width > height;
@@ -342,9 +410,30 @@ const useScreenLayout = (): ScreenLayout => {
342
410
  contentWidth: width - insets.left - insets.right,
343
411
  ...headerLayout,
344
412
  sideBar,
413
+ statusBarClearance,
345
414
  };
346
415
  };
347
416
 
417
+ /**
418
+ * Header row options for react-navigation: its height, the room for the status bar, and
419
+ * on iPhone Duo inner portrait the buttons kept clear of the clock and battery clusters
420
+ */
421
+ const getHeaderRowOptions = ({
422
+ headerHeight,
423
+ headerStatusBarHeight,
424
+ statusBarClearance,
425
+ sideBar,
426
+ }: ScreenLayout) => ({
427
+ headerStyle: { height: headerHeight },
428
+ headerStatusBarHeight,
429
+ // side bar controls are absolute within the containers, padding would not move them
430
+ ...(statusBarClearance &&
431
+ !sideBar && {
432
+ headerLeftContainerStyle: { paddingLeft: statusBarClearance.left },
433
+ headerRightContainerStyle: { paddingRight: statusBarClearance.right },
434
+ }),
435
+ });
436
+
348
437
  /**
349
438
  * Side margin @react-navigation/elements Header adds to its content row
350
439
  */
@@ -410,6 +499,7 @@ export {
410
499
  DuoMetrics,
411
500
  HeaderMetrics,
412
501
  getHeaderMargin,
502
+ getHeaderRowOptions,
413
503
  useGridSystem,
414
504
  useScreenLayout,
415
505
  useSideBarItemStyle,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.167.1-duo.12",
3
+ "version": "0.167.1-duo.13",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},