@momo-kits/foundation 0.167.1-beta.7 → 0.167.1-duo.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Application/BottomSheet.tsx +115 -14
- package/Application/BottomTab/BottomTabBar.tsx +6 -3
- package/Application/BottomTab/DuoBarCompression.tsx +135 -0
- package/Application/BottomTab/DuoTabBar.tsx +462 -0
- package/Application/BottomTab/index.tsx +155 -134
- package/Application/Components/DuoGlassBackground.tsx +179 -0
- package/Application/Components/DuoQuickReturnHeader.tsx +42 -0
- package/Application/Components/HeaderExtendHeader.tsx +65 -28
- package/Application/Components/HeaderLeft.tsx +23 -0
- package/Application/Components/HeaderRight.tsx +104 -9
- package/Application/Components/HeaderTitle.tsx +47 -21
- package/Application/Components/OptionalGlassView.ts +11 -0
- package/Application/Components/SearchHeader.tsx +15 -14
- package/Application/Components/SideBarButton.tsx +128 -0
- package/Application/ModalScreen.tsx +16 -2
- package/Application/NavigationContainer.tsx +36 -48
- package/Application/Navigator.ts +1 -3
- package/Application/StackScreen.tsx +2 -5
- package/Application/WidgetContainer.tsx +9 -15
- package/Application/device.ts +29 -0
- package/Application/types.ts +10 -1
- package/Application/utils.tsx +10 -3
- package/Consts/index.ts +0 -1
- package/Consts/theme.ts +3 -4
- package/Input/Input.tsx +3 -4
- package/Input/InputMoney.tsx +31 -51
- package/Input/InputPhoneNumber.tsx +3 -4
- package/Input/InputSearch.tsx +3 -4
- package/Input/InputTextArea.tsx +3 -4
- package/Layout/Card.tsx +8 -26
- package/Layout/FloatingButton.tsx +3 -3
- package/Layout/Item.tsx +1 -10
- package/Layout/ReservedRegions.tsx +47 -0
- package/Layout/Screen.tsx +240 -48
- package/Layout/Section.tsx +5 -18
- package/Layout/index.ts +4 -4
- package/Layout/styles.ts +5 -0
- package/Layout/utils.ts +375 -5
- package/Popup/PopupNotify.tsx +33 -22
- package/Popup/PopupPromotion.tsx +20 -8
- package/Popup/utils.ts +32 -0
- package/Text/index.tsx +5 -15
- package/Text/utils.ts +1 -2
- package/package.json +3 -2
- package/Consts/designSystemFont.ts +0 -25
- package/Layout/GridSystem.tsx +0 -137
package/Layout/utils.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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';
|
|
8
|
+
import { ReservedRegion, ReservedRegionsContext } from './ReservedRegions';
|
|
3
9
|
|
|
4
10
|
/**
|
|
5
11
|
* validate children type
|
|
@@ -38,10 +44,374 @@ const validateChildren = (
|
|
|
38
44
|
}
|
|
39
45
|
};
|
|
40
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Header geometry
|
|
49
|
+
* - barHeight: navigation bar row below the status bar
|
|
50
|
+
* - iPhoneMinHeight: header never shorter than this on iPhone (landscape, iPhone Duo)
|
|
51
|
+
* - statusBarMinInset: a smaller top inset means the status bar is not on the top edge
|
|
52
|
+
* - largeWidth / largeMargin: @react-navigation/elements Header gives its content row
|
|
53
|
+
* 5pt side margins on iOS from 414pt wide
|
|
54
|
+
*/
|
|
55
|
+
const HeaderMetrics = {
|
|
56
|
+
barHeight: 52,
|
|
57
|
+
iPhoneMinHeight: 72,
|
|
58
|
+
statusBarMinInset: 20,
|
|
59
|
+
largeWidth: 414,
|
|
60
|
+
largeMargin: 5,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* iPhone Duo geometry, iOS 27.1 (Xcode 27.1 simulator)
|
|
65
|
+
* - maxAspectRatio: both displays are near 1.42 (inner) and 1.45 (outer), every slab
|
|
66
|
+
* iPhone is 1.77 or taller
|
|
67
|
+
* - innerMinHeight: window height from which the inner display is in use (outer display
|
|
68
|
+
* is 466pt tall in landscape, inner display 669pt)
|
|
69
|
+
* - controlSize / itemPitch: vertical bar of 48pt circular controls, item groups as 48pt
|
|
70
|
+
* wide capsules at a 50pt pitch, 12pt from the strip's inner edge and 12pt apart
|
|
71
|
+
* - firstItemTop: with the status bar in the strip, the first control sits under the
|
|
72
|
+
* camera, clock and connectivity cluster (cluster ends near y 151 on the outer display,
|
|
73
|
+
* y 98 on the inner display in landscape)
|
|
74
|
+
* - outerHeaderHeight / outerCameraCenterY: on the outer display in portrait the header
|
|
75
|
+
* is 4pt taller than iPhoneMinHeight and its title lines up with the camera punchout
|
|
76
|
+
* (y 24–66 at the top of the strip)
|
|
77
|
+
* - edgeInset: on the outer display in landscape the strip holds no status bar, so its
|
|
78
|
+
* controls start this far from the top and stay this far above the bottom, unless a
|
|
79
|
+
* reserved region (the camera at either end) asks for more
|
|
80
|
+
*/
|
|
81
|
+
const DuoMetrics = {
|
|
82
|
+
maxAspectRatio: 1.6,
|
|
83
|
+
innerMinHeight: 600,
|
|
84
|
+
controlSize: 48,
|
|
85
|
+
itemPitch: 50,
|
|
86
|
+
firstItemTop: 170,
|
|
87
|
+
firstItemTopLandscape: 120,
|
|
88
|
+
outerHeaderHeight: 76,
|
|
89
|
+
outerCameraCenterY: 45,
|
|
90
|
+
edgeInset: 24,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const NO_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* how far system regions reach into a side strip from its top and from its bottom end
|
|
97
|
+
* (e.g. the camera, which sits at either end depending on the rotation), and the center
|
|
98
|
+
* y of the region reaching furthest from the top
|
|
99
|
+
*/
|
|
100
|
+
const getStripOcclusion = (
|
|
101
|
+
regions: ReservedRegion[],
|
|
102
|
+
stripLeft: number,
|
|
103
|
+
stripRight: number,
|
|
104
|
+
windowHeight: number,
|
|
105
|
+
) =>
|
|
106
|
+
regions.reduce(
|
|
107
|
+
(occlusion, region) => {
|
|
108
|
+
const overlapsStrip =
|
|
109
|
+
region.x < stripRight && region.x + region.width > stripLeft;
|
|
110
|
+
if (!overlapsStrip) {
|
|
111
|
+
return occlusion;
|
|
112
|
+
}
|
|
113
|
+
const centerY = region.y + region.height / 2;
|
|
114
|
+
if (centerY < windowHeight / 2) {
|
|
115
|
+
if (region.y + region.height > occlusion.top) {
|
|
116
|
+
occlusion.top = region.y + region.height;
|
|
117
|
+
occlusion.topCenter = centerY;
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
occlusion.bottom = Math.max(occlusion.bottom, windowHeight - region.y);
|
|
121
|
+
}
|
|
122
|
+
return occlusion;
|
|
123
|
+
},
|
|
124
|
+
{ top: 0, topCenter: 0, bottom: 0 },
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const isIPhone = () =>
|
|
128
|
+
Platform.OS === 'ios' && Platform.constants.interfaceIdiom === 'phone';
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Header height and the room it keeps above the bar row for the status bar. On iPhone the
|
|
132
|
+
* header is at least iPhoneMinHeight, and with the status bar off the top edge the row
|
|
133
|
+
* spans the whole header, so the title stays vertically centered.
|
|
134
|
+
*/
|
|
135
|
+
const getHeaderLayout = (topInset: number) => {
|
|
136
|
+
const height = HeaderMetrics.barHeight + topInset;
|
|
137
|
+
if (!isIPhone()) {
|
|
138
|
+
return { headerHeight: height, headerStatusBarHeight: topInset };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
headerHeight: Math.max(height, HeaderMetrics.iPhoneMinHeight),
|
|
143
|
+
headerStatusBarHeight:
|
|
144
|
+
topInset < HeaderMetrics.statusBarMinInset ? 0 : topInset,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Header whose bar row is centered on a window y, at least iPhoneMinHeight tall and
|
|
150
|
+
* reaching 8pt below a side bar control centered there
|
|
151
|
+
*/
|
|
152
|
+
const getCenteredHeaderLayout = (centerY: number) => {
|
|
153
|
+
const headerHeight = Math.max(
|
|
154
|
+
HeaderMetrics.iPhoneMinHeight,
|
|
155
|
+
centerY + DuoMetrics.controlSize / 2 + Spacing.S,
|
|
156
|
+
);
|
|
157
|
+
return {
|
|
158
|
+
headerHeight,
|
|
159
|
+
headerStatusBarHeight: Math.max(0, centerY * 2 - headerHeight),
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export type SideBarEdge = 'left' | 'right';
|
|
164
|
+
|
|
165
|
+
export type SideBarLayout = {
|
|
166
|
+
/**
|
|
167
|
+
* Edge holding the side bar, the one with the larger safe-area inset.
|
|
168
|
+
*/
|
|
169
|
+
edge: SideBarEdge;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Width of the side bar column, the safe-area inset on that edge.
|
|
173
|
+
*/
|
|
174
|
+
width: number;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Window y of the first side bar item, below the navigation bar and the side status bar.
|
|
178
|
+
*/
|
|
179
|
+
top: number;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Room to keep free above the window bottom, clear of a camera at the bottom end.
|
|
183
|
+
*/
|
|
184
|
+
bottom: number;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export type ScreenLayout = {
|
|
188
|
+
/**
|
|
189
|
+
* Window width, from useWindowDimensions.
|
|
190
|
+
*/
|
|
191
|
+
width: number;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Window height, from useWindowDimensions.
|
|
195
|
+
*/
|
|
196
|
+
height: number;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* `true` when running on iPhone Duo, outer or inner display.
|
|
200
|
+
*/
|
|
201
|
+
isIPhoneDuo: boolean;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Raw safe-area insets.
|
|
205
|
+
*/
|
|
206
|
+
insets: EdgeInsets;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Horizontal insets the screen content keeps clear of. 0 outside iPhone Duo.
|
|
210
|
+
*/
|
|
211
|
+
insetLeft: number;
|
|
212
|
+
insetRight: number;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Width available to the screen content: window width minus the horizontal insets.
|
|
216
|
+
*/
|
|
217
|
+
contentWidth: number;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Header height: the top safe-area inset plus the navigation bar row, at least 72pt on iPhone.
|
|
221
|
+
*/
|
|
222
|
+
headerHeight: number;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Room above the navigation bar row for the status bar, the rest of headerHeight is
|
|
226
|
+
* the row, which centers the title vertically. 0 when the status bar is not on top.
|
|
227
|
+
*/
|
|
228
|
+
headerStatusBarHeight: number;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Side column holding the back button and the toolkit, when iPhone Duo moves the
|
|
232
|
+
* bars to the side. `undefined` while the bars stay horizontal.
|
|
233
|
+
*/
|
|
234
|
+
sideBar?: SideBarLayout;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Detect iPhone Duo from the window shape and the safe area: a phone idiom window
|
|
239
|
+
* near square, or a phone with asymmetric side insets (slab iPhones report equal
|
|
240
|
+
* left / right insets in landscape, iPhone Duo moves the status bar to one side).
|
|
241
|
+
*/
|
|
242
|
+
const isIPhoneDuoLayout = (
|
|
243
|
+
width: number,
|
|
244
|
+
height: number,
|
|
245
|
+
insets: EdgeInsets,
|
|
246
|
+
) => {
|
|
247
|
+
if (!isIPhone()) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const shortSide = Math.min(width, height);
|
|
252
|
+
const aspectRatio = shortSide > 0 ? Math.max(width, height) / shortSide : 0;
|
|
253
|
+
const isSquare = aspectRatio > 0 && aspectRatio < DuoMetrics.maxAspectRatio;
|
|
254
|
+
|
|
255
|
+
return isSquare || insets.left !== insets.right;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Screen layout for the current window: iPhone Duo detection, horizontal content insets,
|
|
260
|
+
* content width and the side bar. Follows the window through fold, unfold, rotation and
|
|
261
|
+
* Split View since it reads useWindowDimensions and the safe-area context.
|
|
262
|
+
*/
|
|
263
|
+
const useScreenLayout = (): ScreenLayout => {
|
|
264
|
+
const { width, height } = useWindowDimensions();
|
|
265
|
+
const insets = useContext(SafeAreaInsetsContext) ?? NO_INSETS;
|
|
266
|
+
const reservedRegions = useContext(ReservedRegionsContext);
|
|
267
|
+
|
|
268
|
+
let headerLayout = getHeaderLayout(insets.top);
|
|
269
|
+
const isIPhoneDuo = isIPhoneDuoLayout(width, height, insets);
|
|
270
|
+
if (!isIPhoneDuo) {
|
|
271
|
+
return {
|
|
272
|
+
width,
|
|
273
|
+
height,
|
|
274
|
+
isIPhoneDuo,
|
|
275
|
+
insets,
|
|
276
|
+
insetLeft: 0,
|
|
277
|
+
insetRight: 0,
|
|
278
|
+
contentWidth: width,
|
|
279
|
+
...headerLayout,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const sideInset = Math.max(insets.left, insets.right);
|
|
284
|
+
let sideBar: SideBarLayout | undefined;
|
|
285
|
+
// narrower strips cannot host a control 12pt from the inner edge
|
|
286
|
+
if (sideInset >= DuoMetrics.controlSize + Spacing.M) {
|
|
287
|
+
const edge = insets.right >= insets.left ? 'right' : 'left';
|
|
288
|
+
const stripLeft = edge === 'right' ? width - sideInset : 0;
|
|
289
|
+
// keep controls clear of the camera and of the status bar cluster, which grows for
|
|
290
|
+
// calls and Live Activities. Region frames already include the system's margin
|
|
291
|
+
const occlusion = getStripOcclusion(
|
|
292
|
+
reservedRegions,
|
|
293
|
+
stripLeft,
|
|
294
|
+
stripLeft + sideInset,
|
|
295
|
+
height,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
let top = insets.top + HeaderMetrics.barHeight + Spacing.M;
|
|
299
|
+
if (insets.top < HeaderMetrics.statusBarMinInset) {
|
|
300
|
+
// status bar lives in the strip: start under its cluster, as the native bar does
|
|
301
|
+
const isLandscape = width > height;
|
|
302
|
+
if (!isLandscape) {
|
|
303
|
+
top = DuoMetrics.firstItemTop;
|
|
304
|
+
} else if (height >= DuoMetrics.innerMinHeight) {
|
|
305
|
+
top = DuoMetrics.firstItemTopLandscape;
|
|
306
|
+
} else {
|
|
307
|
+
// outer display in landscape: no status bar in the strip, the title row centers
|
|
308
|
+
// on the camera when it sits at the top end, otherwise on the back button
|
|
309
|
+
top = DuoMetrics.edgeInset;
|
|
310
|
+
const titleCenterY =
|
|
311
|
+
occlusion.top > top
|
|
312
|
+
? occlusion.topCenter
|
|
313
|
+
: top + DuoMetrics.controlSize / 2;
|
|
314
|
+
headerLayout = getCenteredHeaderLayout(titleCenterY);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (height > width && width < DuoMetrics.innerMinHeight) {
|
|
318
|
+
// outer display: the row starts below the top so the title centers on the camera
|
|
319
|
+
headerLayout = {
|
|
320
|
+
headerHeight: DuoMetrics.outerHeaderHeight,
|
|
321
|
+
headerStatusBarHeight:
|
|
322
|
+
DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
sideBar = {
|
|
328
|
+
edge,
|
|
329
|
+
width: sideInset,
|
|
330
|
+
top: Math.max(top, occlusion.top),
|
|
331
|
+
bottom: Math.max(DuoMetrics.edgeInset, occlusion.bottom),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
width,
|
|
337
|
+
height,
|
|
338
|
+
isIPhoneDuo,
|
|
339
|
+
insets,
|
|
340
|
+
insetLeft: insets.left,
|
|
341
|
+
insetRight: insets.right,
|
|
342
|
+
contentWidth: width - insets.left - insets.right,
|
|
343
|
+
...headerLayout,
|
|
344
|
+
sideBar,
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Side margin @react-navigation/elements Header adds to its content row
|
|
350
|
+
*/
|
|
351
|
+
const getHeaderMargin = (width: number) =>
|
|
352
|
+
Platform.OS === 'ios' && width >= HeaderMetrics.largeWidth
|
|
353
|
+
? HeaderMetrics.largeMargin
|
|
354
|
+
: 0;
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Absolute style that floats a header item into the side bar, relative to the header
|
|
358
|
+
* container (left or right) rendering it, 12pt from the strip's inner edge.
|
|
359
|
+
* `undefined` while there is no side bar.
|
|
360
|
+
* @param container header container rendering the item
|
|
361
|
+
* @param slot position in the side bar, 0 for back, 1 for the toolkit below it
|
|
362
|
+
*/
|
|
363
|
+
const useSideBarItemStyle = (
|
|
364
|
+
container: 'left' | 'right',
|
|
365
|
+
slot: number,
|
|
366
|
+
): ViewStyle | undefined => {
|
|
367
|
+
const { width, insets, headerStatusBarHeight, sideBar } = useScreenLayout();
|
|
368
|
+
if (!sideBar) {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const margin = getHeaderMargin(width);
|
|
373
|
+
const columnLeft = sideBar.edge === 'left' ? 0 : width - sideBar.width;
|
|
374
|
+
const top =
|
|
375
|
+
sideBar.top -
|
|
376
|
+
headerStatusBarHeight +
|
|
377
|
+
slot * (DuoMetrics.controlSize + Spacing.M);
|
|
378
|
+
const innerEdgeStyle: ViewStyle =
|
|
379
|
+
sideBar.edge === 'right'
|
|
380
|
+
? { alignItems: 'flex-start', paddingLeft: Spacing.M }
|
|
381
|
+
: { alignItems: 'flex-end', paddingRight: Spacing.M };
|
|
382
|
+
|
|
383
|
+
if (container === 'left') {
|
|
384
|
+
return {
|
|
385
|
+
position: 'absolute',
|
|
386
|
+
top,
|
|
387
|
+
left: columnLeft - margin - insets.left,
|
|
388
|
+
width: sideBar.width,
|
|
389
|
+
...innerEdgeStyle,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const containerRight = width - margin - insets.right;
|
|
394
|
+
return {
|
|
395
|
+
position: 'absolute',
|
|
396
|
+
top,
|
|
397
|
+
right: containerRight - (columnLeft + sideBar.width),
|
|
398
|
+
width: sideBar.width,
|
|
399
|
+
...innerEdgeStyle,
|
|
400
|
+
};
|
|
401
|
+
};
|
|
402
|
+
|
|
41
403
|
const useGridSystem = () => {
|
|
42
|
-
const
|
|
43
|
-
const numberOfColumns =
|
|
404
|
+
const { contentWidth } = useScreenLayout();
|
|
405
|
+
const numberOfColumns = contentWidth > 600 ? 24 : 12;
|
|
44
406
|
return { numberOfColumns };
|
|
45
407
|
};
|
|
46
408
|
|
|
47
|
-
export {
|
|
409
|
+
export {
|
|
410
|
+
DuoMetrics,
|
|
411
|
+
HeaderMetrics,
|
|
412
|
+
getHeaderMargin,
|
|
413
|
+
useGridSystem,
|
|
414
|
+
useScreenLayout,
|
|
415
|
+
useSideBarItemStyle,
|
|
416
|
+
validateChildren,
|
|
417
|
+
};
|
package/Popup/PopupNotify.tsx
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import React, { useContext, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Dimensions,
|
|
4
|
-
ScrollView,
|
|
5
|
-
StyleSheet,
|
|
6
|
-
TouchableOpacity,
|
|
7
|
-
View,
|
|
8
|
-
} from 'react-native';
|
|
2
|
+
import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
|
|
9
3
|
import { ApplicationContext, MiniAppContext } from '../Context';
|
|
10
4
|
import { Button } from '../Button';
|
|
11
5
|
import { Colors, Radius, Spacing, Styles } from '../Consts';
|
|
@@ -13,7 +7,8 @@ import { Icon } from '../Icon';
|
|
|
13
7
|
import { Image } from '../Image';
|
|
14
8
|
import { Text, useScaleSize } from '../Text';
|
|
15
9
|
import { PopupNotifyProps } from './types';
|
|
16
|
-
import {
|
|
10
|
+
import { scheduleOnRN } from 'react-native-worklets';
|
|
11
|
+
import { usePopupMaxHeight, usePopupWidth } from './utils';
|
|
17
12
|
|
|
18
13
|
const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
19
14
|
image,
|
|
@@ -29,7 +24,8 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
29
24
|
const context = useContext<any>(MiniAppContext);
|
|
30
25
|
const { theme, navigator, translate } = useContext(ApplicationContext);
|
|
31
26
|
const [scrollContent, setScrollContent] = useState(false);
|
|
32
|
-
const
|
|
27
|
+
const popupWidth = usePopupWidth();
|
|
28
|
+
const maxHeight = usePopupMaxHeight();
|
|
33
29
|
const scaledMaxHeight = useScaleSize(172);
|
|
34
30
|
|
|
35
31
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
@@ -50,7 +46,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
50
46
|
onRequestClose?.(callback);
|
|
51
47
|
} else {
|
|
52
48
|
navigator?.pop();
|
|
53
|
-
|
|
49
|
+
scheduleOnRN(() => callback?.());
|
|
54
50
|
}
|
|
55
51
|
};
|
|
56
52
|
|
|
@@ -172,18 +168,8 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
172
168
|
}
|
|
173
169
|
};
|
|
174
170
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
accessibilityLabel={'popup_notify'}
|
|
178
|
-
style={[
|
|
179
|
-
styles.container,
|
|
180
|
-
{
|
|
181
|
-
backgroundColor: theme.colors.background.surface,
|
|
182
|
-
width: widthDevice - Spacing.XL * 2,
|
|
183
|
-
},
|
|
184
|
-
showBaseLineDebug && styles.debugBaseLine,
|
|
185
|
-
]}
|
|
186
|
-
>
|
|
171
|
+
const body = (
|
|
172
|
+
<>
|
|
187
173
|
{!!image && <Image source={{ uri: image }} style={styles.image} />}
|
|
188
174
|
<View style={styles.content}>
|
|
189
175
|
<Text
|
|
@@ -225,6 +211,30 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
225
211
|
</View>
|
|
226
212
|
)}
|
|
227
213
|
</View>
|
|
214
|
+
</>
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<View
|
|
219
|
+
accessibilityLabel={'popup_notify'}
|
|
220
|
+
style={[
|
|
221
|
+
styles.container,
|
|
222
|
+
{
|
|
223
|
+
backgroundColor: theme.colors.background.surface,
|
|
224
|
+
width: popupWidth,
|
|
225
|
+
maxHeight,
|
|
226
|
+
},
|
|
227
|
+
showBaseLineDebug && styles.debugBaseLine,
|
|
228
|
+
]}
|
|
229
|
+
>
|
|
230
|
+
{maxHeight === undefined ? (
|
|
231
|
+
body
|
|
232
|
+
) : (
|
|
233
|
+
// scrolls when iPhone Duo landscape is too short; actions stay visible
|
|
234
|
+
<ScrollView style={styles.body} bounces={false}>
|
|
235
|
+
{body}
|
|
236
|
+
</ScrollView>
|
|
237
|
+
)}
|
|
228
238
|
<View style={styles.contentAction}>{buildAction()}</View>
|
|
229
239
|
{buildCloseIcon()}
|
|
230
240
|
</View>
|
|
@@ -242,6 +252,7 @@ const styles = StyleSheet.create({
|
|
|
242
252
|
aspectRatio: 1.7777,
|
|
243
253
|
overflow: 'hidden',
|
|
244
254
|
},
|
|
255
|
+
body: { flexShrink: 1 },
|
|
245
256
|
content: { padding: Spacing.XL },
|
|
246
257
|
description: {
|
|
247
258
|
marginTop: Spacing.S,
|
package/Popup/PopupPromotion.tsx
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import React, { useContext, useRef } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { StyleSheet, TouchableOpacity, View } from 'react-native';
|
|
3
3
|
import { PopupPromotionProps } from './types';
|
|
4
4
|
import { ApplicationContext, MiniAppContext } from '../Context';
|
|
5
5
|
import { Image } from '../Image';
|
|
6
6
|
import { Colors, Radius, Spacing } from '../Consts';
|
|
7
7
|
import { Icon } from '../Icon';
|
|
8
|
+
import { usePopupMaxHeight, usePopupWidth } from './utils';
|
|
9
|
+
|
|
10
|
+
const ASPECT_RATIO = 0.72;
|
|
11
|
+
const ICON_CLOSE_SIZE = 20;
|
|
12
|
+
// close button row under the image: marginTop + vertical padding + icon
|
|
13
|
+
const CLOSE_ROW_HEIGHT = Spacing.S * 3 + ICON_CLOSE_SIZE;
|
|
8
14
|
|
|
9
15
|
const PopupPromotion: React.FC<PopupPromotionProps> = ({
|
|
10
16
|
image,
|
|
@@ -14,7 +20,13 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
|
|
|
14
20
|
}) => {
|
|
15
21
|
const context = useContext<any>(MiniAppContext);
|
|
16
22
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
17
|
-
const
|
|
23
|
+
const popupWidth = usePopupWidth();
|
|
24
|
+
const maxHeight = usePopupMaxHeight();
|
|
25
|
+
// fit the portrait image by height when iPhone Duo leaves too little room
|
|
26
|
+
const imageWidth =
|
|
27
|
+
maxHeight === undefined
|
|
28
|
+
? popupWidth
|
|
29
|
+
: Math.min(popupWidth, (maxHeight - CLOSE_ROW_HEIGHT) * ASPECT_RATIO);
|
|
18
30
|
const closeAction = useRef('touch_outside');
|
|
19
31
|
|
|
20
32
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
@@ -69,7 +81,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
|
|
|
69
81
|
return (
|
|
70
82
|
<View
|
|
71
83
|
accessibilityLabel={'popup_promotion'}
|
|
72
|
-
style={showBaseLineDebug && styles.debugBaseLine}
|
|
84
|
+
style={[{ width: imageWidth }, showBaseLineDebug && styles.debugBaseLine]}
|
|
73
85
|
>
|
|
74
86
|
<TouchableOpacity onPress={() => onAction(onActionClick)}>
|
|
75
87
|
<Image
|
|
@@ -80,7 +92,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
|
|
|
80
92
|
style={[
|
|
81
93
|
styles.container,
|
|
82
94
|
{
|
|
83
|
-
width:
|
|
95
|
+
width: imageWidth,
|
|
84
96
|
},
|
|
85
97
|
]}
|
|
86
98
|
/>
|
|
@@ -92,7 +104,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
|
|
|
92
104
|
|
|
93
105
|
const styles = StyleSheet.create({
|
|
94
106
|
container: {
|
|
95
|
-
aspectRatio:
|
|
107
|
+
aspectRatio: ASPECT_RATIO,
|
|
96
108
|
width: '100%',
|
|
97
109
|
},
|
|
98
110
|
debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
|
|
@@ -103,11 +115,11 @@ const styles = StyleSheet.create({
|
|
|
103
115
|
marginTop: Spacing.S,
|
|
104
116
|
},
|
|
105
117
|
iconButton: {
|
|
106
|
-
padding:
|
|
118
|
+
padding: Spacing.S,
|
|
107
119
|
},
|
|
108
120
|
iconClose: {
|
|
109
|
-
width:
|
|
110
|
-
height:
|
|
121
|
+
width: ICON_CLOSE_SIZE,
|
|
122
|
+
height: ICON_CLOSE_SIZE,
|
|
111
123
|
alignItems: 'center',
|
|
112
124
|
justifyContent: 'center',
|
|
113
125
|
borderRadius: Radius.M,
|
package/Popup/utils.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Dimensions, useWindowDimensions } from 'react-native';
|
|
2
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
3
|
+
import { Spacing } from '../Consts';
|
|
4
|
+
import { getDuoTopInset, isIPhoneDuo } from '../Application/device';
|
|
5
|
+
|
|
6
|
+
// width of a system alert on the iPhone Duo inner display, in both orientations
|
|
7
|
+
const DUO_POPUP_MAX_WIDTH = 440;
|
|
8
|
+
|
|
9
|
+
const usePopupWidth = () => {
|
|
10
|
+
const windowSize = useWindowDimensions();
|
|
11
|
+
if (!isIPhoneDuo()) {
|
|
12
|
+
return Dimensions.get('window').width - Spacing.XL * 2;
|
|
13
|
+
}
|
|
14
|
+
return Math.min(windowSize.width - Spacing.XL * 2, DUO_POPUP_MAX_WIDTH);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// room left for a popup on iPhone Duo, where landscape is too short for it
|
|
18
|
+
const usePopupMaxHeight = () => {
|
|
19
|
+
const insets = useSafeAreaInsets();
|
|
20
|
+
const { width, height } = useWindowDimensions();
|
|
21
|
+
if (!isIPhoneDuo()) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return (
|
|
25
|
+
height -
|
|
26
|
+
getDuoTopInset(insets, width, height) -
|
|
27
|
+
insets.bottom -
|
|
28
|
+
Spacing.XL * 2
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export { usePopupWidth, usePopupMaxHeight };
|
package/Text/index.tsx
CHANGED
|
@@ -7,11 +7,6 @@ import {
|
|
|
7
7
|
MiniAppContext,
|
|
8
8
|
SkeletonContext,
|
|
9
9
|
} from '../Context';
|
|
10
|
-
import {
|
|
11
|
-
LEGACY_DEFAULT_FONT_FAMILY,
|
|
12
|
-
NEW_DEFAULT_FONT_FAMILY,
|
|
13
|
-
useResolvedFontFamily,
|
|
14
|
-
} from '../Consts/designSystemFont';
|
|
15
10
|
import { scaleSize, useScaleSize } from './utils';
|
|
16
11
|
import { Skeleton } from '../Skeleton';
|
|
17
12
|
|
|
@@ -119,11 +114,7 @@ const TypoStyles = (typo: Typography, newFontFamily?: string) => {
|
|
|
119
114
|
const typoStyle = styleSheet[typo] ?? styleSheet.paragraph_default;
|
|
120
115
|
const { fontWeight, fontSize, fontStyle, lineHeight } = typoStyle;
|
|
121
116
|
const style = FontStyle[fontStyle ?? 'normal'];
|
|
122
|
-
const
|
|
123
|
-
const font = newFontFamily ?? defaultFontFamily;
|
|
124
|
-
const resolvedDefaultFamily = useResolvedFontFamily(
|
|
125
|
-
LEGACY_DEFAULT_FONT_FAMILY,
|
|
126
|
-
);
|
|
117
|
+
const font = newFontFamily ?? theme.font;
|
|
127
118
|
let fontFamily: string;
|
|
128
119
|
|
|
129
120
|
switch (font) {
|
|
@@ -152,11 +143,7 @@ const TypoStyles = (typo: Typography, newFontFamily?: string) => {
|
|
|
152
143
|
break;
|
|
153
144
|
}
|
|
154
145
|
default: {
|
|
155
|
-
fontFamily = `${
|
|
156
|
-
resolvedDefaultFamily === NEW_DEFAULT_FONT_FAMILY
|
|
157
|
-
? MomoTrustSans[fontWeight]
|
|
158
|
-
: SFProText[fontWeight]
|
|
159
|
-
}`;
|
|
146
|
+
fontFamily = `${font}-${SFProText[fontWeight]}${style}`;
|
|
160
147
|
}
|
|
161
148
|
}
|
|
162
149
|
|
|
@@ -166,6 +153,9 @@ const TypoStyles = (typo: Typography, newFontFamily?: string) => {
|
|
|
166
153
|
fontSize: useScaleSize(fontSize),
|
|
167
154
|
lineHeight: useScaleSize(lineHeight),
|
|
168
155
|
fontStyle,
|
|
156
|
+
// fontFamily da mang san weight. Tren Android, ReactFontManager doi
|
|
157
|
+
// `fonts/<family>_bold.ttf` khi fontWeight >= 700; file do khong ton tai nen
|
|
158
|
+
// font se roi ve font he thong. Bo fontWeight de Android dung dung file.
|
|
169
159
|
fontWeight: undefined,
|
|
170
160
|
};
|
|
171
161
|
};
|
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 =
|
|
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-
|
|
3
|
+
"version": "0.167.1-duo.12",
|
|
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"
|