@momo-kits/foundation 0.169.7-beta.4 → 0.169.7-beta.6

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 (146) hide show
  1. package/Application/BottomSheet.hooks.ts +8 -8
  2. package/Application/BottomSheet.parts.tsx +44 -34
  3. package/Application/BottomSheet.tsx +40 -37
  4. package/Application/BottomTab/ActiveIndicator.tsx +15 -14
  5. package/Application/BottomTab/BottomTabBar.tsx +49 -62
  6. package/Application/BottomTab/BottomTabInset.tsx +8 -4
  7. package/Application/BottomTab/CurvedContainer.tsx +14 -12
  8. package/Application/BottomTab/CustomBottomTabItem.tsx +13 -13
  9. package/Application/BottomTab/DuoBarCompression.tsx +13 -19
  10. package/Application/BottomTab/DuoRailTabBar.tsx +20 -24
  11. package/Application/BottomTab/DuoTabBar.tsx +8 -10
  12. package/Application/BottomTab/TabBarIcon.tsx +15 -29
  13. package/Application/BottomTab/TabScreen.tsx +1 -5
  14. package/Application/BottomTab/index.tsx +33 -30
  15. package/Application/BottomTab/metrics.ts +23 -23
  16. package/Application/BottomTab/tabItems.ts +1 -1
  17. package/Application/BottomTab/usePressLift.ts +2 -2
  18. package/Application/Components/BackgroundImageView.tsx +4 -0
  19. package/Application/Components/DuoGlassBackground.tsx +5 -4
  20. package/Application/Components/HeaderAnimated.tsx +40 -12
  21. package/Application/Components/HeaderBackground.tsx +19 -2
  22. package/Application/Components/HeaderExtendHeader.tsx +56 -34
  23. package/Application/Components/HeaderLeft.tsx +4 -2
  24. package/Application/Components/HeaderRight.styles.ts +15 -9
  25. package/Application/Components/HeaderRight.tsx +2 -1
  26. package/Application/Components/HeaderTitle.shared.ts +19 -12
  27. package/Application/Components/HeaderTitle.tsx +4 -3
  28. package/Application/Components/HeaderToolkitAction.tsx +28 -22
  29. package/Application/Components/NavigationButton.tsx +10 -6
  30. package/Application/Components/SearchHeader.tsx +41 -15
  31. package/Application/Components/TitleJourney.tsx +2 -2
  32. package/Application/Components/TitleLocation.tsx +2 -2
  33. package/Application/Components/TitleUser.tsx +2 -2
  34. package/Application/Components/useToolkitActions.tsx +17 -32
  35. package/Application/Localize.ts +7 -0
  36. package/Application/ModalScreen.tsx +10 -10
  37. package/Application/Navigation.ts +14 -16
  38. package/Application/NavigationContainer.tsx +19 -18
  39. package/Application/Navigator.ts +20 -48
  40. package/Application/ScaleSizeProvider.tsx +13 -6
  41. package/Application/StackScreen.tracking.ts +11 -33
  42. package/Application/StackScreen.tsx +19 -54
  43. package/Application/TooltipPortal.tsx +12 -5
  44. package/Application/WidgetContainer.tsx +10 -4
  45. package/Application/headerLayout.ts +158 -0
  46. package/Application/index.ts +11 -1
  47. package/Application/modalLayout.ts +13 -14
  48. package/Application/navigationOptions.tsx +27 -32
  49. package/Application/types.header.ts +7 -2
  50. package/Application/types.ts +7 -0
  51. package/Badge/Badge.tsx +5 -0
  52. package/Badge/BadgeDot.tsx +1 -0
  53. package/Badge/BadgeDotAnimation.tsx +15 -7
  54. package/Badge/BadgeRibbon.tsx +46 -38
  55. package/Badge/Shape.tsx +2 -0
  56. package/Badge/types.ts +2 -2
  57. package/Button/index.tsx +5 -16
  58. package/Button/utils.ts +9 -15
  59. package/CheckBox/index.tsx +4 -0
  60. package/Consts/theme.ts +10 -10
  61. package/Context/index.ts +5 -1
  62. package/Divider/DashDivider.tsx +1 -0
  63. package/Divider/index.tsx +4 -0
  64. package/FoundationList/index.tsx +4 -0
  65. package/Icon/index.tsx +6 -6
  66. package/Icon/types.ts +1 -1
  67. package/IconButton/index.tsx +4 -12
  68. package/Image/index.tsx +9 -7
  69. package/Input/Input.tsx +8 -9
  70. package/Input/InputDropDown.tsx +10 -12
  71. package/Input/InputMoney.tsx +12 -8
  72. package/Input/InputMoneyView.tsx +14 -7
  73. package/Input/InputOTP.tsx +35 -33
  74. package/Input/InputPhoneNumber.tsx +8 -9
  75. package/Input/InputSearch.tsx +22 -8
  76. package/Input/InputTextArea.tsx +4 -0
  77. package/Input/SearchTextTyping.tsx +4 -0
  78. package/Input/SystemTextInput.tsx +7 -2
  79. package/Input/TextTyping.tsx +4 -0
  80. package/Input/common.tsx +26 -12
  81. package/Input/index.tsx +24 -21
  82. package/Input/moneyUtils.ts +10 -4
  83. package/Input/styles.ts +0 -5
  84. package/Input/utils.ts +14 -12
  85. package/Layout/Card.tsx +7 -4
  86. package/Layout/FloatingButton.tsx +4 -0
  87. package/Layout/Item.tsx +6 -2
  88. package/Layout/ItemList.tsx +9 -7
  89. package/Layout/ItemSectionList.tsx +2 -1
  90. package/Layout/ReservedRegions.tsx +25 -25
  91. package/Layout/Screen.tsx +59 -58
  92. package/Layout/Section.tsx +8 -5
  93. package/Layout/TrackingScope.tsx +1 -0
  94. package/Layout/screenContent.tsx +4 -2
  95. package/Layout/screenFooter.tsx +28 -18
  96. package/Layout/screenHeader.tsx +88 -82
  97. package/Layout/screenQuickReturn.tsx +17 -20
  98. package/Layout/screenScroll.ts +4 -7
  99. package/Layout/screenTypes.ts +13 -4
  100. package/Layout/types.ts +2 -4
  101. package/Layout/utils.ts +31 -37
  102. package/Loader/DotLoader.tsx +4 -0
  103. package/Loader/Loader.tsx +4 -0
  104. package/Loader/ProgressBar.tsx +1 -0
  105. package/Loader/Spinner.tsx +4 -0
  106. package/Loader/utils.ts +11 -7
  107. package/Pagination/Dot.tsx +4 -0
  108. package/Pagination/PaginationDot.tsx +1 -0
  109. package/Pagination/PaginationNumber.tsx +5 -3
  110. package/Pagination/PaginationScroll.tsx +5 -0
  111. package/Pagination/PaginationWhiteDot.tsx +1 -0
  112. package/Pagination/index.tsx +4 -0
  113. package/Pagination/types.ts +2 -2
  114. package/Popup/PopupNotify.tsx +44 -48
  115. package/Popup/PopupPromotion.tsx +4 -7
  116. package/Popup/types.ts +4 -4
  117. package/Popup/utils.ts +4 -0
  118. package/Radio/index.tsx +4 -0
  119. package/Skeleton/index.tsx +8 -0
  120. package/Switch/index.tsx +1 -0
  121. package/Tag/index.tsx +5 -1
  122. package/Tag/types.ts +3 -3
  123. package/Text/index.tsx +14 -3
  124. package/Text/styles.ts +1 -1
  125. package/Text/types.ts +1 -6
  126. package/Text/utils.ts +20 -4
  127. package/Title/index.tsx +41 -32
  128. package/ViewBoundary/index.tsx +4 -2
  129. package/index.ts +0 -6
  130. package/package.json +1 -1
  131. package/Application/KitAdoption/AdoptionBadge.tsx +0 -119
  132. package/Application/KitAdoption/AdoptionProbe.tsx +0 -166
  133. package/Application/KitAdoption/AdoptionStore.ts +0 -80
  134. package/Application/KitAdoption/Authorship/utils.ts +0 -137
  135. package/Application/KitAdoption/Slots/utils.ts +0 -68
  136. package/Application/KitAdoption/constants.ts +0 -120
  137. package/Application/KitAdoption/index.ts +0 -12
  138. package/Application/KitAdoption/styles.ts +0 -55
  139. package/Application/KitAdoption/types.ts +0 -61
  140. package/Application/KitAdoption/utils.ts +0 -376
  141. package/Application/__tests__/KitAdoption.integration.test.tsx +0 -253
  142. package/Application/__tests__/KitAdoption.test.ts +0 -283
  143. package/Application/__tests__/KitAdoptionAuthorship.test.ts +0 -132
  144. package/Application/__tests__/KitAdoptionStore.test.ts +0 -110
  145. package/Application/__tests__/KitAdoptionStructural.test.ts +0 -193
  146. package/Application/__tests__/modalLayout.test.ts +0 -128
@@ -1,26 +1,24 @@
1
1
  import { Alert } from 'react-native';
2
2
  import { version } from '../package.json';
3
- import { getAdoptionTrackingParams } from './KitAdoption';
4
- import type { AdoptionMeasureRef } from './KitAdoption/types';
5
3
 
6
4
  type ScreenTrackingDeps = {
7
5
  tracking: { current: any };
8
6
  context: any;
9
7
  navigator: any;
10
8
  screenName: string;
11
- adoptionMeasure?: AdoptionMeasureRef;
12
9
  };
13
10
 
11
+ /**
12
+ * Auto-tracking reporters of a stack screen. Each event fires at most once per
13
+ * screen, guarded by the `release*` flags on `tracking.current`.
14
+ */
14
15
  export const createScreenTracking = ({
15
16
  tracking,
16
17
  context,
17
18
  navigator,
18
19
  screenName,
19
- adoptionMeasure,
20
20
  }: ScreenTrackingDeps) => {
21
- /**
22
- * tracking for screen load
23
- */
21
+ /** Reports the screen load time. */
24
22
  const onScreenLoad = () => {
25
23
  if (!tracking.current?.releaseLoad) {
26
24
  let timeLoad = tracking.current.timeLoad;
@@ -28,8 +26,6 @@ export const createScreenTracking = ({
28
26
  timeLoad = tracking.current.endTime - tracking.current.startTime;
29
27
  }
30
28
 
31
- const adoption = getAdoptionTrackingParams(adoptionMeasure?.current?.());
32
-
33
29
  context?.autoTracking?.({
34
30
  ...context,
35
31
  screenName,
@@ -40,7 +36,6 @@ export const createScreenTracking = ({
40
36
  params: tracking.current.params,
41
37
  version: version,
42
38
  lastElement: tracking.current.lastElement,
43
- adoption,
44
39
  });
45
40
  navigator?.maxApi?.stopTrace?.(
46
41
  tracking.current.traceIdLoad,
@@ -49,29 +44,22 @@ export const createScreenTracking = ({
49
44
  );
50
45
  tracking.current.releaseLoad = true;
51
46
 
52
- /**
53
- * debug
54
- */
55
47
  navigator?.maxApi?.showToastDebug?.({
56
48
  appId: context.appId,
57
49
  message: `${screenName} screen_load_time ${timeLoad}`,
58
50
  type: 'ERROR',
59
51
  });
60
- if (
61
- __DEV__ &&
62
- tracking.current.lastElement?.children?.current?.length > 0
63
- ) {
52
+ const lastChildren = tracking.current.lastElement?.children?.current;
53
+ if (__DEV__ && lastChildren?.length > 0) {
64
54
  Alert.alert(
65
55
  `${screenName}- load ${timeLoad}ms`,
66
- JSON.stringify(tracking.current.lastElement?.children?.current),
56
+ JSON.stringify(lastChildren),
67
57
  );
68
58
  }
69
59
  }
70
60
  };
71
61
 
72
- /**
73
- * tracking for screen load
74
- */
62
+ /** Reports the time until the screen became interactive. */
75
63
  const onScreenInteraction = () => {
76
64
  if (!tracking.current?.releaseInteraction) {
77
65
  let timeLoad = tracking.current.timeLoad;
@@ -99,9 +87,6 @@ export const createScreenTracking = ({
99
87
  );
100
88
  tracking.current.releaseInteraction = true;
101
89
 
102
- /**
103
- * debug toast
104
- */
105
90
  navigator?.maxApi?.showToastDebug?.({
106
91
  appId: context.appId,
107
92
  message: `${screenName} screen_interaction_time ${tracking.current.timeInteraction}`,
@@ -110,9 +95,7 @@ export const createScreenTracking = ({
110
95
  }
111
96
  };
112
97
 
113
- /**
114
- * tracking for first interaction by user
115
- */
98
+ /** Reports the user's first interaction with the screen. */
116
99
  const onFirstInteraction = (action: string) => {
117
100
  if (!tracking.current?.releaseUserInteraction) {
118
101
  const timeLoad = tracking.current.endTime - tracking.current.startTime;
@@ -126,9 +109,6 @@ export const createScreenTracking = ({
126
109
  });
127
110
  tracking.current.releaseUserInteraction = true;
128
111
 
129
- /**
130
- * debug
131
- */
132
112
  navigator?.maxApi?.showToastDebug?.({
133
113
  appId: context.appId,
134
114
  message: `${screenName} user_interaction ${timeLoad}`,
@@ -137,9 +117,7 @@ export const createScreenTracking = ({
137
117
  }
138
118
  };
139
119
 
140
- /**
141
- * tracking for loading screen
142
- */
120
+ /** Reports how long the skeleton loading state lasted. */
143
121
  const onScreenLoading = () => {
144
122
  const start = tracking.current.timeStartLoading;
145
123
  const end = tracking.current.timeEndLoading;
@@ -8,15 +8,13 @@ import { SwipeBackHaptic } from './Components/SwipeBackHaptic';
8
8
  import { useAppState } from './hooks';
9
9
  import { createScreenTracking } from './StackScreen.tracking';
10
10
  import { TooltipPortalHost, TooltipPortalProvider } from './TooltipPortal';
11
- import { AdoptionProbe } from './KitAdoption';
12
- import type { WalkResult } from './KitAdoption/types';
11
+ import { useHeaderFrameOptions } from './headerLayout';
13
12
 
14
13
  const runAfterInteractions = InteractionManager.runAfterInteractions;
15
14
 
16
15
  /**
17
- * container for stack screen
18
- * @param props
19
- * @constructor
16
+ * Hosts a feature screen in the stack: applies its header options, provides
17
+ * `ScreenContext` and reports navigation, load and interaction tracking.
20
18
  */
21
19
  const StackScreen: React.FC<any> = props => {
22
20
  const { navigator } = useContext(ApplicationContext);
@@ -43,9 +41,6 @@ const StackScreen: React.FC<any> = props => {
43
41
  lastElement: undefined,
44
42
  });
45
43
  const widgets = useRef<any>([]);
46
- const adoptionMeasure = useRef<(() => WalkResult | undefined) | undefined>(
47
- undefined,
48
- );
49
44
  const context = useContext<any>(MiniAppContext);
50
45
 
51
46
  const {
@@ -57,6 +52,7 @@ const StackScreen: React.FC<any> = props => {
57
52
  const navigation = useRef(new Navigation(props.navigation, context)).current;
58
53
  const heightHeader = useHeaderHeight();
59
54
  const { isBackgroundToForeground } = useAppState();
55
+ const headerFrameOptions = useHeaderFrameOptions(options?.contentLayout);
60
56
 
61
57
  const data = {
62
58
  ...initialParams,
@@ -69,18 +65,19 @@ const StackScreen: React.FC<any> = props => {
69
65
 
70
66
  const screenName = Component?.name || Component?.type?.name || 'Invalid';
71
67
 
72
- /**
73
- * set options for screen
74
- */
75
68
  useLayoutEffect(() => {
76
69
  if (options) {
77
70
  navigation.setOptions(options);
78
71
  }
79
72
  }, [navigation, options]);
80
73
 
81
- /**
82
- * tracking for screen
83
- */
74
+ // re-applied on fold, unfold and rotation, apart from the other options
75
+ useLayoutEffect(() => {
76
+ if (headerFrameOptions) {
77
+ navigation.instance.setOptions(headerFrameOptions);
78
+ }
79
+ }, [navigation, headerFrameOptions]);
80
+
84
81
  useEffect(() => {
85
82
  let focusScreen: any;
86
83
  let onFocusApp: any;
@@ -154,6 +151,7 @@ const StackScreen: React.FC<any> = props => {
154
151
  // eslint-disable-next-line react-hooks/exhaustive-deps
155
152
  }, []);
156
153
 
154
+ /** Reports a navigation to this screen: `back` if it was shown before. */
157
155
  const onScreenNavigated = (pre: string, current: string) => {
158
156
  if (!isBackgroundToForeground) {
159
157
  const item: any = {
@@ -171,9 +169,6 @@ const StackScreen: React.FC<any> = props => {
171
169
 
172
170
  tracking.current.mounted = true;
173
171
 
174
- /**
175
- * debug toast
176
- */
177
172
  navigator?.maxApi?.showToastDebug?.({
178
173
  appId: context.appId,
179
174
  message: `${screenName} screen_navigated`,
@@ -187,13 +182,7 @@ const StackScreen: React.FC<any> = props => {
187
182
  onScreenInteraction,
188
183
  onFirstInteraction,
189
184
  onScreenLoading,
190
- } = createScreenTracking({
191
- tracking,
192
- context,
193
- navigator,
194
- screenName,
195
- adoptionMeasure,
196
- });
185
+ } = createScreenTracking({ tracking, context, navigator, screenName });
197
186
 
198
187
  return (
199
188
  <ScreenContext.Provider
@@ -201,9 +190,7 @@ const StackScreen: React.FC<any> = props => {
201
190
  screenName,
202
191
  bottomTab: !!bottomTab,
203
192
  onElementLoad: (item: any) => {
204
- /**
205
- * widget handle
206
- */
193
+ // Widgets record their own load span and don't end the screen load.
207
194
  if (item?.componentName === 'Widget') {
208
195
  const index = widgets.current.findIndex(
209
196
  (element: any) => element.componentId === item.componentId,
@@ -227,35 +214,25 @@ const StackScreen: React.FC<any> = props => {
227
214
  return;
228
215
  }
229
216
 
230
- /**
231
- * tracking for element screen load
232
- */
233
217
  clearTimeout(tracking.current.timeoutLoad);
234
218
  tracking.current.endTime = Date.now();
235
219
  tracking.current.timeoutInteraction?.cancel?.();
236
220
  tracking.current.timeoutInteraction = runAfterInteractions(() => {
237
- tracking.current.timeInteraction =
238
- Date.now() - tracking.current.startTime;
221
+ const elapsed = Date.now() - tracking.current.startTime;
222
+ tracking.current.timeInteraction = elapsed;
239
223
  });
240
224
 
241
- /**
242
- * support for debug last element
243
- */
244
225
  if (item?.componentName) {
245
226
  tracking.current.lastElement = item;
246
227
  }
247
228
 
248
- /**
249
- * for stop tracking when user interaction
250
- */
229
+ // A user interaction settles load and interaction tracking at once.
251
230
  if (item?.interaction) {
252
231
  onScreenLoad();
253
232
  onScreenInteraction();
254
233
  onFirstInteraction(item?.action);
255
234
  }
256
- /**
257
- * timeout for handle tracking screen
258
- */
235
+ // Reports once no element has loaded for 2s.
259
236
  tracking.current.timeoutLoad = setTimeout(() => {
260
237
  const time = tracking.current.endTime - tracking.current.startTime;
261
238
  tracking.current.widgets = widgets.current;
@@ -273,16 +250,10 @@ const StackScreen: React.FC<any> = props => {
273
250
  tracking.current.timeStartLoading = Date.now();
274
251
  }
275
252
 
276
- /**
277
- * tracking for loading screen
278
- */
279
253
  if (!loading) {
280
254
  tracking.current.timeEndLoading = Date.now();
281
255
  }
282
256
 
283
- /**
284
- * timeout for handle tracking screen
285
- */
286
257
  clearTimeout(tracking.current.timeoutLoading);
287
258
  tracking.current.timeoutLoading = setTimeout(() => {
288
259
  onScreenLoading();
@@ -294,13 +265,7 @@ const StackScreen: React.FC<any> = props => {
294
265
  }}
295
266
  >
296
267
  <TooltipPortalProvider>
297
- <AdoptionProbe
298
- screenName={screenName}
299
- navigation={props.navigation}
300
- measureRef={adoptionMeasure}
301
- >
302
- <Component heightHeader={heightHeader} {...data} />
303
- </AdoptionProbe>
268
+ <Component heightHeader={heightHeader} {...data} />
304
269
  <TooltipPortalHost />
305
270
  <SwipeBackHaptic />
306
271
  </TooltipPortalProvider>
@@ -21,10 +21,14 @@ const TooltipPortalContext = createContext<TooltipPortalContextValue | null>(
21
21
  null,
22
22
  );
23
23
 
24
+ /**
25
+ * Registry of tooltip contents that `TooltipPortalHost` renders in one layer
26
+ * above the screen, so tooltips aren't clipped by their anchor's parents.
27
+ */
24
28
  export const TooltipPortalProvider: React.FC<{ children: ReactNode }> = ({
25
29
  children,
26
30
  }) => {
27
- // Use ref for synchronous updates + reducer for manual re-renders
31
+ // The ref map updates synchronously; the reducer only triggers re-renders.
28
32
  const portalsRef = useRef<Map<string, ReactNode>>(new Map());
29
33
  const hostRef = useRef<View>(null);
30
34
  const [updateCounter, forceUpdate] = useReducer((x: number) => x + 1, 0);
@@ -32,16 +36,15 @@ export const TooltipPortalProvider: React.FC<{ children: ReactNode }> = ({
32
36
 
33
37
  const register = useCallback((id: string, content: ReactNode) => {
34
38
  portalsRef.current.set(id, content);
35
- forceUpdate(); // Trigger re-render to show new content
39
+ forceUpdate();
36
40
  }, []);
37
41
 
38
42
  const unregister = useCallback((id: string, immediate = false) => {
39
43
  const hasItem = portalsRef.current.has(id);
40
- if (!hasItem) return; // Already removed
44
+ if (!hasItem) return;
41
45
 
42
46
  portalsRef.current.delete(id);
43
47
 
44
- // Clear any pending updates
45
48
  if (pendingUpdateRef.current) {
46
49
  clearTimeout(pendingUpdateRef.current);
47
50
  pendingUpdateRef.current = null;
@@ -72,6 +75,7 @@ export const TooltipPortalProvider: React.FC<{ children: ReactNode }> = ({
72
75
  );
73
76
  };
74
77
 
78
+ /** Full-screen, touch-through layer that renders every registered tooltip. */
75
79
  export const TooltipPortalHost: React.FC = () => {
76
80
  const context = useContext(TooltipPortalContext);
77
81
 
@@ -95,6 +99,10 @@ export const TooltipPortalHost: React.FC = () => {
95
99
  );
96
100
  };
97
101
 
102
+ /**
103
+ * Tooltip portal API. Outside a provider it returns no-ops (and warns in dev)
104
+ * so a tooltip degrades instead of crashing.
105
+ */
98
106
  export const useTooltipPortal = () => {
99
107
  const context = useContext(TooltipPortalContext);
100
108
 
@@ -104,7 +112,6 @@ export const useTooltipPortal = () => {
104
112
  'useTooltipPortal must be used within TooltipPortalProvider. Tooltips may not appear correctly.',
105
113
  );
106
114
  }
107
- // Return no-op functions if context is missing
108
115
  return {
109
116
  register: () => { },
110
117
  unregister: () => { },
@@ -18,6 +18,7 @@ import { StyleSheet, View } from 'react-native';
18
18
 
19
19
  const Stack = createStackNavigator();
20
20
 
21
+ /** Widget screen that reports its laid-out height to size the container. */
21
22
  const WidgetContent: React.FC = ({ route }: any) => {
22
23
  const Component = route?.params?.screen;
23
24
  return (
@@ -31,6 +32,10 @@ const WidgetContent: React.FC = ({ route }: any) => {
31
32
  );
32
33
  };
33
34
 
35
+ /**
36
+ * Root of an embedded widget: its own navigator with the widget screen and the
37
+ * `Modal` route, sized to the widget's content height.
38
+ */
34
39
  const WidgetContainer: React.FC<WidgetContainerProps> = ({
35
40
  widget,
36
41
  theme = defaultTheme,
@@ -61,11 +66,9 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
61
66
  headerGradient = context?.designConfig?.headerGradient;
62
67
  }
63
68
 
64
- /**
65
- * inject data for navigator
66
- */
67
69
  navigator.current.maxApi = maxApi;
68
70
 
71
+ /** Text for a localization key, or the current-language side of `{ vi, en }`. */
69
72
  const translate = (data?: string | { vi: string; en: string }) => {
70
73
  if (data && typeof data !== 'string') {
71
74
  return localize?.translateData(data);
@@ -81,7 +84,10 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
81
84
  };
82
85
  const isWhiteList = isWhiteListDesignSystem(mergedContext);
83
86
  const fontFamily = isWhiteList ? 'MomoTrustSans' : 'SFProText';
84
- const navigationFontFamily = isWhiteList ? 'MomoTrustSans-Regular' : 'SFProText';
87
+ let navigationFontFamily = 'SFProText';
88
+ if (isWhiteList) {
89
+ navigationFontFamily = 'MomoTrustSans-Regular';
90
+ }
85
91
 
86
92
  return (
87
93
  <View style={[styles.container, { height: height ?? '100%' }]}>
@@ -0,0 +1,158 @@
1
+ import { useMemo } from 'react';
2
+ import { useWindowDimensions, type ViewStyle } from 'react-native';
3
+ import type { StackNavigationOptions } from '@react-navigation/stack';
4
+ import { Spacing } from '../Consts';
5
+ import { getHeaderMargin } from '../Layout/utils';
6
+ import { FOLDABLE_MIN_WIDTH } from './modalLayout';
7
+ import { isIPhoneDuo } from './utils';
8
+
9
+ // Top navigation spec, shared with the Compose / SwiftUI kits
10
+ const HEADER_FRAME_MAX_WIDTH = 600;
11
+
12
+ /**
13
+ * Top navigation geometry on every device
14
+ * - buttonSize / iconSize: round back and shortcut buttons, icons inside them and the pill
15
+ * - pillWidth / pillHeight: help center | home capsule
16
+ * - clusterWidth: shortcut, gap and pill on the right of the bar
17
+ * - searchHeight: search field inside the header
18
+ * - titleGap: space between back and the title, and the title and the right cluster
19
+ * - sidePadding: bar edge to back and to the right cluster
20
+ * - gradientHeight: header gradient, kept at the phone height on wide windows
21
+ * - bannerWidth: animated banner image, kept at the phone width on wide windows
22
+ */
23
+ const HeaderBarMetrics = {
24
+ buttonSize: 32,
25
+ iconSize: 20,
26
+ pillWidth: 72,
27
+ pillHeight: 32,
28
+ clusterWidth: 112,
29
+ searchHeight: 32,
30
+ titleGap: Spacing.S,
31
+ sidePadding: Spacing.M,
32
+ gradientHeight: 154,
33
+ bannerWidth: 375,
34
+ };
35
+
36
+ /**
37
+ * `grid` keeps the header row at the bar edges, `stretch` centers it in a 600pt
38
+ * frame on wide windows. Background and status bar always span the window.
39
+ */
40
+ type HeaderContentLayout = 'grid' | 'stretch';
41
+
42
+ type HeaderLayout = typeof HeaderBarMetrics & {
43
+ /**
44
+ * Window whose short side is at least 600 (Galaxy Z Fold / Pixel Fold inner
45
+ * display, tablets), iPhone Duo excluded. Phones, landscape included, are not.
46
+ */
47
+ isWide: boolean;
48
+ /**
49
+ * Window edge to back and to the right cluster: `sidePadding`, or centering the
50
+ * row in the 600pt frame with `stretch` on a wide window.
51
+ */
52
+ frameInset: number;
53
+ /**
54
+ * Header row width between the two `frameInset`s.
55
+ */
56
+ frameWidth: number;
57
+ };
58
+
59
+ type HeaderLayoutInput = {
60
+ width: number;
61
+ height: number;
62
+ isDuo: boolean;
63
+ contentLayout?: HeaderContentLayout;
64
+ };
65
+
66
+ /**
67
+ * Header geometry for a window, pure so it can be tested without a device.
68
+ */
69
+ const getHeaderLayout = ({
70
+ width,
71
+ height,
72
+ isDuo,
73
+ contentLayout = 'grid',
74
+ }: HeaderLayoutInput): HeaderLayout => {
75
+ const { sidePadding } = HeaderBarMetrics;
76
+ const isWide = !isDuo && Math.min(width, height) >= FOLDABLE_MIN_WIDTH;
77
+ let frameInset = sidePadding;
78
+ if (isWide && contentLayout === 'stretch') {
79
+ const centered = (width - HEADER_FRAME_MAX_WIDTH) / 2;
80
+ frameInset = Math.max(sidePadding, centered);
81
+ }
82
+ return {
83
+ ...HeaderBarMetrics,
84
+ isWide,
85
+ frameInset,
86
+ frameWidth: width - frameInset * 2,
87
+ };
88
+ };
89
+
90
+ /**
91
+ * Widest a title can be between back and the right cluster of the header row.
92
+ */
93
+ const getTitleBudget = (layout: HeaderLayout) => {
94
+ const { buttonSize, titleGap, clusterWidth } = layout;
95
+ return layout.frameWidth - buttonSize - titleGap * 2 - clusterWidth;
96
+ };
97
+
98
+ /**
99
+ * Header geometry for the current window; follows fold, unfold and rotation.
100
+ */
101
+ const useHeaderLayout = (contentLayout?: HeaderContentLayout): HeaderLayout => {
102
+ const { width, height } = useWindowDimensions();
103
+ const isDuo = isIPhoneDuo();
104
+ return useMemo(() => {
105
+ return getHeaderLayout({ width, height, isDuo, contentLayout });
106
+ }, [width, height, isDuo, contentLayout]);
107
+ };
108
+
109
+ const TITLE_CONTAINER_RESET: ViewStyle = {
110
+ marginLeft: 0,
111
+ marginEnd: 0,
112
+ marginStart: 0,
113
+ marginHorizontal: 0,
114
+ marginVertical: 0,
115
+ margin: 0,
116
+ };
117
+
118
+ /**
119
+ * React Navigation container styles that keep back, title and the right cluster
120
+ * in the `stretch` frame. `undefined` for `grid`, and on iPhone Duo, which keeps
121
+ * its own header row.
122
+ */
123
+ const useHeaderFrameOptions = (
124
+ contentLayout?: HeaderContentLayout,
125
+ ): StackNavigationOptions | undefined => {
126
+ const layout = useHeaderLayout(contentLayout);
127
+ const { width } = useWindowDimensions();
128
+ const isDuo = isIPhoneDuo();
129
+ return useMemo(() => {
130
+ if (contentLayout !== 'stretch' || isDuo) {
131
+ return undefined;
132
+ }
133
+ // the header content row already sits this far in on large iOS windows
134
+ const shift = layout.frameInset - layout.sidePadding;
135
+ const containerShift = Math.max(0, shift - getHeaderMargin(width));
136
+ return {
137
+ headerLeftContainerStyle: { paddingLeft: containerShift },
138
+ headerRightContainerStyle: { paddingRight: containerShift },
139
+ headerTitleContainerStyle: {
140
+ ...TITLE_CONTAINER_RESET,
141
+ left: layout.titleGap,
142
+ maxWidth: getTitleBudget(layout),
143
+ },
144
+ };
145
+ }, [contentLayout, isDuo, layout, width]);
146
+ };
147
+
148
+ export type { HeaderContentLayout, HeaderLayout };
149
+
150
+ export {
151
+ HeaderBarMetrics,
152
+ HEADER_FRAME_MAX_WIDTH,
153
+ TITLE_CONTAINER_RESET,
154
+ getHeaderLayout,
155
+ getTitleBudget,
156
+ useHeaderLayout,
157
+ useHeaderFrameOptions,
158
+ };
@@ -26,6 +26,13 @@ import {
26
26
  type ModalLayout,
27
27
  type ModalRect,
28
28
  } from './modalLayout';
29
+ import {
30
+ HeaderBarMetrics,
31
+ getHeaderLayout,
32
+ useHeaderLayout,
33
+ type HeaderContentLayout,
34
+ type HeaderLayout,
35
+ } from './headerLayout';
29
36
 
30
37
  export {
31
38
  NavigationContainer,
@@ -41,6 +48,9 @@ export {
41
48
  useTooltipPortal,
42
49
  getModalLayout,
43
50
  useModalLayout,
51
+ HeaderBarMetrics,
52
+ getHeaderLayout,
53
+ useHeaderLayout,
44
54
  };
45
55
 
46
- export type { ModalLayout, ModalRect };
56
+ export type { ModalLayout, ModalRect, HeaderContentLayout, HeaderLayout };
@@ -73,11 +73,12 @@ type ModalLayoutInput = {
73
73
  hinge?: ReservedRegion;
74
74
  };
75
75
 
76
- const clamp = (value: number, min: number, max: number) =>
77
- Math.min(Math.max(value, min), max);
76
+ const clamp = (value: number, min: number, max: number) => {
77
+ return Math.min(Math.max(value, min), max);
78
+ };
78
79
 
79
80
  /**
80
- * Modal geometry for a window, pure so it can be tested without a device
81
+ * Modal geometry for a window, pure so it can be tested without a device.
81
82
  */
82
83
  const getModalLayout = ({
83
84
  width,
@@ -91,8 +92,9 @@ const getModalLayout = ({
91
92
  const isFoldable = isDuo || shortSide >= FOLDABLE_MIN_WIDTH;
92
93
  const isDuoOuter = isDuo && shortSide < FOLDABLE_MIN_WIDTH;
93
94
  const hingeRight = hinge ? hinge.x + hinge.width : 0;
94
- const isHalfFolded =
95
- isDuo && !!hinge && hinge.x > 0 && hingeRight < width - insets.right;
95
+ const hasHinge = !!hinge && hinge.x > 0;
96
+ const isHingeInWindow = hingeRight < width - insets.right;
97
+ const isHalfFolded = isDuo && hasHinge && isHingeInWindow;
96
98
 
97
99
  const safeLeft = isHalfFolded ? hingeRight : insets.left;
98
100
  const placement = {
@@ -112,9 +114,9 @@ const getModalLayout = ({
112
114
  minWidth: popupWidth,
113
115
  maxWidth: popupWidth,
114
116
  margin: Spacing.XL,
115
- radius: Radius.L,
117
+ radius: Radius.XL,
116
118
  },
117
- sheet: { margin: 0, radius: Radius.M },
119
+ sheet: { margin: 0, radius: Radius.XL },
118
120
  scrimOpacity: SCRIM_OPACITY,
119
121
  };
120
122
  }
@@ -125,6 +127,7 @@ const getModalLayout = ({
125
127
 
126
128
  const sheetMargin = Spacing.M;
127
129
  const sheetSpace = placement.width - sheetMargin * 2;
130
+ const sheetMinWidth = Math.min(SHEET_MIN_WIDTH, sheetSpace);
128
131
  const sheetMaxHeight = height - insets.top - Spacing.M;
129
132
 
130
133
  return {
@@ -139,11 +142,7 @@ const getModalLayout = ({
139
142
  radius: Radius.XL,
140
143
  },
141
144
  sheet: {
142
- width: clamp(
143
- sheetSpace,
144
- Math.min(SHEET_MIN_WIDTH, sheetSpace),
145
- SHEET_MAX_WIDTH,
146
- ),
145
+ width: clamp(sheetSpace, sheetMinWidth, SHEET_MAX_WIDTH),
147
146
  maxHeight: sheetMaxHeight,
148
147
  minHeight: Math.min(SHEET_MIN_HEIGHT, sheetMaxHeight),
149
148
  margin: sheetMargin,
@@ -154,8 +153,8 @@ const getModalLayout = ({
154
153
  };
155
154
 
156
155
  /**
157
- * Modal geometry for the current window, follows fold, unfold, rotation and the
158
- * iPhone Duo hinge
156
+ * Modal geometry for the current window; follows fold, unfold, rotation and
157
+ * the iPhone Duo hinge.
159
158
  */
160
159
  const useModalLayout = (): ModalLayout => {
161
160
  const { width, height } = useWindowDimensions();