@momo-kits/foundation 0.167.1-duo.1 → 0.167.1-duo.2

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/Layout/utils.ts CHANGED
@@ -1,5 +1,10 @@
1
- import React, { Fragment, ReactElement } from 'react';
2
- import { useWindowDimensions } from 'react-native';
1
+ import React, { Fragment, ReactElement, useContext } from 'react';
2
+ import { Platform, useWindowDimensions, ViewStyle } from 'react-native';
3
+ import {
4
+ EdgeInsets,
5
+ SafeAreaInsetsContext,
6
+ } from 'react-native-safe-area-context';
7
+ import { Spacing } from '../Consts';
3
8
 
4
9
  /**
5
10
  * validate children type
@@ -38,10 +43,280 @@ const validateChildren = (
38
43
  }
39
44
  };
40
45
 
46
+ /**
47
+ * Header geometry
48
+ * - barHeight: navigation bar row below the status bar
49
+ * - iPhoneMinHeight: header never shorter than this on iPhone (landscape, iPhone Duo)
50
+ * - statusBarMinInset: a smaller top inset means the status bar is not on the top edge
51
+ * - largeWidth / largeMargin: @react-navigation/elements Header gives its content row
52
+ * 5pt side margins on iOS from 414pt wide
53
+ */
54
+ const HeaderMetrics = {
55
+ barHeight: 52,
56
+ iPhoneMinHeight: 72,
57
+ statusBarMinInset: 20,
58
+ largeWidth: 414,
59
+ largeMargin: 5,
60
+ };
61
+
62
+ /**
63
+ * iPhone Duo geometry, iOS 27.1 (Xcode 27.1 simulator)
64
+ * - maxAspectRatio: both displays are near 1.42 (inner) and 1.45 (outer), every slab
65
+ * iPhone is 1.77 or taller
66
+ * - innerMinHeight: window height from which the inner display is in use (outer display
67
+ * is 466pt tall in landscape, inner display 669pt)
68
+ * - controlSize / itemPitch: vertical bar of 48pt circular controls, item groups as 48pt
69
+ * wide capsules at a 50pt pitch, 12pt from the strip's inner edge and 12pt apart
70
+ * - firstItemTop: with the status bar in the strip, the first control sits under the
71
+ * camera, clock and connectivity cluster (cluster ends near y 151 on the outer display,
72
+ * y 98 on the inner display in landscape)
73
+ */
74
+ const DuoMetrics = {
75
+ maxAspectRatio: 1.6,
76
+ innerMinHeight: 600,
77
+ controlSize: 48,
78
+ itemPitch: 50,
79
+ firstItemTop: 170,
80
+ firstItemTopLandscape: 120,
81
+ };
82
+
83
+ const NO_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };
84
+
85
+ const isIPhone = () =>
86
+ Platform.OS === 'ios' && Platform.constants.interfaceIdiom === 'phone';
87
+
88
+ /**
89
+ * Header height and the room it keeps above the bar row for the status bar. On iPhone the
90
+ * header is at least iPhoneMinHeight, and with the status bar off the top edge the row
91
+ * spans the whole header, so the title stays vertically centered.
92
+ */
93
+ const getHeaderLayout = (topInset: number) => {
94
+ const height = HeaderMetrics.barHeight + topInset;
95
+ if (!isIPhone()) {
96
+ return { headerHeight: height, headerStatusBarHeight: topInset };
97
+ }
98
+
99
+ return {
100
+ headerHeight: Math.max(height, HeaderMetrics.iPhoneMinHeight),
101
+ headerStatusBarHeight:
102
+ topInset < HeaderMetrics.statusBarMinInset ? 0 : topInset,
103
+ };
104
+ };
105
+
106
+ export type SideBarEdge = 'left' | 'right';
107
+
108
+ export type SideBarLayout = {
109
+ /**
110
+ * Edge holding the side bar, the one with the larger safe-area inset.
111
+ */
112
+ edge: SideBarEdge;
113
+
114
+ /**
115
+ * Width of the side bar column, the safe-area inset on that edge.
116
+ */
117
+ width: number;
118
+
119
+ /**
120
+ * Window y of the first side bar item, below the navigation bar and the side status bar.
121
+ */
122
+ top: number;
123
+ };
124
+
125
+ export type ScreenLayout = {
126
+ /**
127
+ * Window width, from useWindowDimensions.
128
+ */
129
+ width: number;
130
+
131
+ /**
132
+ * Window height, from useWindowDimensions.
133
+ */
134
+ height: number;
135
+
136
+ /**
137
+ * `true` when running on iPhone Duo, outer or inner display.
138
+ */
139
+ isIPhoneDuo: boolean;
140
+
141
+ /**
142
+ * Raw safe-area insets.
143
+ */
144
+ insets: EdgeInsets;
145
+
146
+ /**
147
+ * Horizontal insets the screen content keeps clear of. 0 outside iPhone Duo.
148
+ */
149
+ insetLeft: number;
150
+ insetRight: number;
151
+
152
+ /**
153
+ * Width available to the screen content: window width minus the horizontal insets.
154
+ */
155
+ contentWidth: number;
156
+
157
+ /**
158
+ * Header height: the top safe-area inset plus the navigation bar row, at least 72pt on iPhone.
159
+ */
160
+ headerHeight: number;
161
+
162
+ /**
163
+ * Room above the navigation bar row for the status bar, the rest of headerHeight is
164
+ * the row, which centers the title vertically. 0 when the status bar is not on top.
165
+ */
166
+ headerStatusBarHeight: number;
167
+
168
+ /**
169
+ * Side column holding the back button and the toolkit, when iPhone Duo moves the
170
+ * bars to the side. `undefined` while the bars stay horizontal.
171
+ */
172
+ sideBar?: SideBarLayout;
173
+ };
174
+
175
+ /**
176
+ * Detect iPhone Duo from the window shape and the safe area: a phone idiom window
177
+ * near square, or a phone with asymmetric side insets (slab iPhones report equal
178
+ * left / right insets in landscape, iPhone Duo moves the status bar to one side).
179
+ */
180
+ const isIPhoneDuoLayout = (
181
+ width: number,
182
+ height: number,
183
+ insets: EdgeInsets,
184
+ ) => {
185
+ if (!isIPhone()) {
186
+ return false;
187
+ }
188
+
189
+ const shortSide = Math.min(width, height);
190
+ const aspectRatio = shortSide > 0 ? Math.max(width, height) / shortSide : 0;
191
+ const isSquare = aspectRatio > 0 && aspectRatio < DuoMetrics.maxAspectRatio;
192
+
193
+ return isSquare || insets.left !== insets.right;
194
+ };
195
+
196
+ /**
197
+ * Screen layout for the current window: iPhone Duo detection, horizontal content insets,
198
+ * content width and the side bar. Follows the window through fold, unfold, rotation and
199
+ * Split View since it reads useWindowDimensions and the safe-area context.
200
+ */
201
+ const useScreenLayout = (): ScreenLayout => {
202
+ const { width, height } = useWindowDimensions();
203
+ const insets = useContext(SafeAreaInsetsContext) ?? NO_INSETS;
204
+
205
+ const headerLayout = getHeaderLayout(insets.top);
206
+ const isIPhoneDuo = isIPhoneDuoLayout(width, height, insets);
207
+ if (!isIPhoneDuo) {
208
+ return {
209
+ width,
210
+ height,
211
+ isIPhoneDuo,
212
+ insets,
213
+ insetLeft: 0,
214
+ insetRight: 0,
215
+ contentWidth: width,
216
+ ...headerLayout,
217
+ };
218
+ }
219
+
220
+ const sideInset = Math.max(insets.left, insets.right);
221
+ let sideBar: SideBarLayout | undefined;
222
+ // narrower strips cannot host a control 12pt from the inner edge
223
+ if (sideInset >= DuoMetrics.controlSize + Spacing.M) {
224
+ let top = insets.top + HeaderMetrics.barHeight + Spacing.M;
225
+ if (insets.top < HeaderMetrics.statusBarMinInset) {
226
+ // status bar lives in the strip: start under its cluster, as the native bar does
227
+ const isInnerLandscape =
228
+ width > height && height >= DuoMetrics.innerMinHeight;
229
+ top = isInnerLandscape
230
+ ? DuoMetrics.firstItemTopLandscape
231
+ : DuoMetrics.firstItemTop;
232
+ }
233
+
234
+ sideBar = {
235
+ edge: insets.right >= insets.left ? 'right' : 'left',
236
+ width: sideInset,
237
+ top,
238
+ };
239
+ }
240
+
241
+ return {
242
+ width,
243
+ height,
244
+ isIPhoneDuo,
245
+ insets,
246
+ insetLeft: insets.left,
247
+ insetRight: insets.right,
248
+ contentWidth: width - insets.left - insets.right,
249
+ ...headerLayout,
250
+ sideBar,
251
+ };
252
+ };
253
+
254
+ /**
255
+ * Side margin @react-navigation/elements Header adds to its content row
256
+ */
257
+ const getHeaderMargin = (width: number) =>
258
+ Platform.OS === 'ios' && width >= HeaderMetrics.largeWidth
259
+ ? HeaderMetrics.largeMargin
260
+ : 0;
261
+
262
+ /**
263
+ * Absolute style that floats a header item into the side bar, relative to the header
264
+ * container (left or right) rendering it, 12pt from the strip's inner edge.
265
+ * `undefined` while there is no side bar.
266
+ * @param container header container rendering the item
267
+ * @param slot position in the side bar, 0 for back, 1 for the toolkit below it
268
+ */
269
+ const useSideBarItemStyle = (
270
+ container: 'left' | 'right',
271
+ slot: number,
272
+ ): ViewStyle | undefined => {
273
+ const { width, insets, headerStatusBarHeight, sideBar } = useScreenLayout();
274
+ if (!sideBar) {
275
+ return undefined;
276
+ }
277
+
278
+ const margin = getHeaderMargin(width);
279
+ const columnLeft = sideBar.edge === 'left' ? 0 : width - sideBar.width;
280
+ const top =
281
+ sideBar.top -
282
+ headerStatusBarHeight +
283
+ slot * (DuoMetrics.controlSize + Spacing.M);
284
+ const innerEdgeStyle: ViewStyle =
285
+ sideBar.edge === 'right'
286
+ ? { alignItems: 'flex-start', paddingLeft: Spacing.M }
287
+ : { alignItems: 'flex-end', paddingRight: Spacing.M };
288
+
289
+ if (container === 'left') {
290
+ return {
291
+ position: 'absolute',
292
+ top,
293
+ left: columnLeft - margin - insets.left,
294
+ width: sideBar.width,
295
+ ...innerEdgeStyle,
296
+ };
297
+ }
298
+
299
+ const containerRight = width - margin - insets.right;
300
+ return {
301
+ position: 'absolute',
302
+ top,
303
+ right: containerRight - (columnLeft + sideBar.width),
304
+ width: sideBar.width,
305
+ ...innerEdgeStyle,
306
+ };
307
+ };
308
+
41
309
  const useGridSystem = () => {
42
- const windowSize = useWindowDimensions();
43
- const numberOfColumns = windowSize.width > 600 ? 24 : 12;
310
+ const { contentWidth } = useScreenLayout();
311
+ const numberOfColumns = contentWidth > 600 ? 24 : 12;
44
312
  return { numberOfColumns };
45
313
  };
46
314
 
47
- export { useGridSystem, validateChildren };
315
+ export {
316
+ DuoMetrics,
317
+ getHeaderMargin,
318
+ useGridSystem,
319
+ useScreenLayout,
320
+ useSideBarItemStyle,
321
+ validateChildren,
322
+ };
@@ -13,7 +13,7 @@ import { Icon } from '../Icon';
13
13
  import { Image } from '../Image';
14
14
  import { Text, useScaleSize } from '../Text';
15
15
  import { PopupNotifyProps } from './types';
16
- import { runOnJS } from 'react-native-reanimated';
16
+ import { scheduleOnRN } from 'react-native-worklets';
17
17
 
18
18
  const PopupNotify: React.FC<PopupNotifyProps> = ({
19
19
  image,
@@ -50,7 +50,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
50
50
  onRequestClose?.(callback);
51
51
  } else {
52
52
  navigator?.pop();
53
- runOnJS(() => callback?.())();
53
+ scheduleOnRN(() => callback?.());
54
54
  }
55
55
  };
56
56
 
package/Text/index.tsx CHANGED
@@ -7,10 +7,6 @@ import {
7
7
  MiniAppContext,
8
8
  SkeletonContext,
9
9
  } from '../Context';
10
- import {
11
- NEW_DEFAULT_FONT_FAMILY,
12
- useResolvedFontFamily,
13
- } from '../Consts/designSystemFont';
14
10
  import { scaleSize, useScaleSize } from './utils';
15
11
  import { Skeleton } from '../Skeleton';
16
12
 
@@ -118,8 +114,7 @@ const TypoStyles = (typo: Typography, newFontFamily?: string) => {
118
114
  const typoStyle = styleSheet[typo] ?? styleSheet.paragraph_default;
119
115
  const { fontWeight, fontSize, fontStyle, lineHeight } = typoStyle;
120
116
  const style = FontStyle[fontStyle ?? 'normal'];
121
- const font = useResolvedFontFamily(newFontFamily ?? theme.font);
122
- const resolvedDefaultFamily = useResolvedFontFamily(NEW_DEFAULT_FONT_FAMILY);
117
+ const font = newFontFamily ?? theme.font;
123
118
  let fontFamily: string;
124
119
 
125
120
  switch (font) {
@@ -148,11 +143,7 @@ const TypoStyles = (typo: Typography, newFontFamily?: string) => {
148
143
  break;
149
144
  }
150
145
  default: {
151
- fontFamily = `${resolvedDefaultFamily}-${
152
- resolvedDefaultFamily === NEW_DEFAULT_FONT_FAMILY
153
- ? MomoTrustSans[fontWeight]
154
- : SFProText[fontWeight]
155
- }`;
146
+ fontFamily = `${font}-${SFProText[fontWeight]}${style}`;
156
147
  }
157
148
  }
158
149
 
package/Text/utils.ts CHANGED
@@ -2,7 +2,6 @@ import { Dimensions, PixelRatio, useWindowDimensions } from 'react-native';
2
2
  import { useContext } from 'react';
3
3
  import { MiniAppContext } from '../Context';
4
4
 
5
- const deviceWidth = Dimensions.get('window').width;
6
5
  const DEFAULT_SCREEN_SIZE = 375;
7
6
  const MAX_FONT_SCALE = 1.5;
8
7
  const MAX_DEVICE_SCALE = 5;
@@ -40,7 +39,7 @@ const useScaleSize = (size: number, scaleRate?: number) => {
40
39
 
41
40
  const scaleSize = (size: number) => {
42
41
  const fontScale = PixelRatio.getFontScale();
43
- const deviceScale = deviceWidth / DEFAULT_SCREEN_SIZE;
42
+ const deviceScale = Dimensions.get('window').width / DEFAULT_SCREEN_SIZE;
44
43
 
45
44
  let fontSizeDeviceScale = size;
46
45
  let fontSizeOSScale = size;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.167.1-duo.1",
3
+ "version": "0.167.1-duo.2",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},
@@ -23,7 +23,8 @@
23
23
  "lottie-react-native": "7.2.4"
24
24
  },
25
25
  "peerDependencies": {
26
- "react-native": "*"
26
+ "react-native": "*",
27
+ "react-native-worklets": ">=0.5.0"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@types/color": "3.0.6"
@@ -32,4 +33,4 @@
32
33
  "registry": "https://registry.npmjs.org/"
33
34
  },
34
35
  "license": "MoMo"
35
- }
36
+ }
@@ -1,38 +0,0 @@
1
- import { useContext } from 'react';
2
- import { MiniAppContext } from '../Context';
3
-
4
- export const NEW_DEFAULT_FONT_FAMILY = 'MomoTrustSans';
5
- export const LEGACY_DEFAULT_FONT_FAMILY = 'SFProText';
6
-
7
- /**
8
- * Font thuoc bo design system moi. Ngoai whitelist thi ha het ve font cu:
9
- * MoMoTrustDisplay chi duoc dung tu commit header title 5bfde6251 (DS-791),
10
- * truoc do header lay font theo theme.
11
- */
12
- const NEW_FONT_FAMILIES = [NEW_DEFAULT_FONT_FAMILY, 'MoMoTrustDisplay'];
13
-
14
- /**
15
- * Cờ whitelist design system, host đẩy xuống qua `decorateProps`
16
- * (`designSystemConfig.isWhiteListDesignSystem`). Ngoài whitelist thì kit giữ
17
- * nguyên font cũ.
18
- */
19
- export const useIsDesignSystemWhiteList = (): boolean => {
20
- const context = useContext<any>(MiniAppContext);
21
- return context?.designSystemConfig?.isWhiteListDesignSystem === true;
22
- };
23
-
24
- /**
25
- * Hạ font mặc định mới về font cũ khi user không nằm trong whitelist.
26
- * Font do caller chỉ định tường minh (MoMoSignature, AlegreyaSans...) giữ nguyên.
27
- */
28
- export const useResolvedFontFamily = (font: string): string => {
29
- const isWhiteList = useIsDesignSystemWhiteList();
30
- if (NEW_FONT_FAMILIES.includes(font) && !isWhiteList) {
31
- return LEGACY_DEFAULT_FONT_FAMILY;
32
- }
33
- return font;
34
- };
35
-
36
- /** Tên file font Regular tương ứng với nhánh whitelist đang áp dụng. */
37
- export const useDefaultRegularFontFamily = (): string =>
38
- `${useResolvedFontFamily(NEW_DEFAULT_FONT_FAMILY)}-Regular`;
@@ -1,137 +0,0 @@
1
- import React, { useContext } from 'react';
2
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
3
- import { Dimensions, StyleSheet, View } from 'react-native';
4
- import { ApplicationContext } from '../Context';
5
- import { Colors, Spacing } from '../Consts';
6
- import { GridContextProps } from './types';
7
- import { useGridSystem } from './utils';
8
-
9
- const GridSystem: React.FC<any> = ({
10
- width = Dimensions.get('window').width,
11
- useMargin = true,
12
- gutterSize = Spacing.M,
13
- screenGrid = true,
14
- gridColor = Colors.blue_07,
15
- }) => {
16
- const { theme } = useContext(ApplicationContext);
17
- const { numberOfColumns } = useGridSystem();
18
-
19
- const margin = useMargin ? Spacing.M : 0;
20
- const widthSection = width - margin * 2;
21
- const totalGutterSize = gutterSize * (numberOfColumns - 1);
22
- const sizePerSpan = (widthSection - totalGutterSize) / numberOfColumns;
23
-
24
- const grid: GridContextProps = {
25
- numberOfColumns,
26
- gutterSize,
27
- sizePerSpan,
28
- getSizeSpan: span => {
29
- return span * sizePerSpan + (span - 1) * gutterSize;
30
- },
31
- };
32
- const insets = useSafeAreaInsets();
33
-
34
- const list: number[] = [];
35
- for (let i = 1; i <= grid.numberOfColumns; i++) {
36
- list.push(i);
37
- }
38
-
39
- return (
40
- <>
41
- <View
42
- pointerEvents="none"
43
- style={[
44
- styles.gridContainer,
45
- screenGrid && insets,
46
- {
47
- borderColor: theme.colors.error.primary,
48
- marginHorizontal: margin,
49
- top: Spacing.M,
50
- bottom: Spacing.M,
51
- },
52
- ]}
53
- >
54
- {list.map((_, index) => {
55
- return (
56
- <View style={{ flexDirection: 'row' }} key={index.toString()}>
57
- {index !== 0 && <View style={{ width: grid.gutterSize }} />}
58
- <View
59
- key={`Grid${index.toString()}`}
60
- pointerEvents="none"
61
- style={[
62
- styles.gridColumn,
63
- {
64
- width: grid.sizePerSpan,
65
- backgroundColor: gridColor,
66
- },
67
- ]}
68
- />
69
- </View>
70
- );
71
- })}
72
- </View>
73
- {screenGrid && (
74
- <>
75
- <View pointerEvents="none" style={styles.dangerLeftContainer} />
76
- <View pointerEvents="none" style={styles.dangerRightContainer} />
77
- <View
78
- pointerEvents="none"
79
- style={[styles.dangerTopContainer, { height: Spacing.M }]}
80
- />
81
- <View
82
- pointerEvents="none"
83
- style={[styles.dangerBottomContainer, { height: insets.bottom }]}
84
- />
85
- </>
86
- )}
87
- </>
88
- );
89
- };
90
-
91
- const styles = StyleSheet.create({
92
- gridContainer: {
93
- position: 'absolute',
94
- top: 0,
95
- bottom: 0,
96
- left: 0,
97
- right: 0,
98
- flexDirection: 'row',
99
- borderWidth: 1,
100
- },
101
- gridColumn: {
102
- height: '100%',
103
- opacity: 0.2,
104
- },
105
- dangerTopContainer: {
106
- opacity: 0.2,
107
- backgroundColor: Colors.red_03,
108
- width: '100%',
109
- top: 0,
110
- position: 'absolute',
111
- },
112
- dangerBottomContainer: {
113
- opacity: 0.2,
114
- backgroundColor: Colors.red_03,
115
- width: '100%',
116
- bottom: 0,
117
- position: 'absolute',
118
- },
119
- dangerLeftContainer: {
120
- opacity: 0.2,
121
- backgroundColor: Colors.red_03,
122
- width: Spacing.M,
123
- height: '100%',
124
- left: 0,
125
- position: 'absolute',
126
- },
127
- dangerRightContainer: {
128
- opacity: 0.2,
129
- backgroundColor: Colors.red_03,
130
- width: Spacing.M,
131
- height: '100%',
132
- right: 0,
133
- position: 'absolute',
134
- },
135
- });
136
-
137
- export default GridSystem;