@momo-kits/foundation 0.167.1-duo.13 → 0.167.1-duo.15
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.
|
@@ -34,8 +34,6 @@ const DEFAULT_TOOLBAR_HEIGHT =
|
|
|
34
34
|
DuoMetrics.controlSize + Spacing.M + 2 * DuoMetrics.itemPitch;
|
|
35
35
|
// Back, then the single button the toolbar folds into
|
|
36
36
|
const COMPRESSED_TOOLBAR_HEIGHT = 2 * DuoMetrics.controlSize + Spacing.M;
|
|
37
|
-
// Back alone, while a rail opened from the compressed state takes the toolbar slot
|
|
38
|
-
const BACK_ONLY_HEIGHT = DuoMetrics.controlSize;
|
|
39
37
|
|
|
40
38
|
const NO_COMPRESSION: DuoBarCompression = {
|
|
41
39
|
toolbarCompressed: false,
|
|
@@ -107,7 +105,9 @@ const DuoBarCompressionProvider: React.FC<{
|
|
|
107
105
|
expandTabBar: () => setOpened('tabBar'),
|
|
108
106
|
setToolbarHeight,
|
|
109
107
|
};
|
|
110
|
-
|
|
108
|
+
// an opened rail always shows, even when only the gap between the groups is short:
|
|
109
|
+
// it closes again once a tab is picked
|
|
110
|
+
if (opened === 'tabBar') {
|
|
111
111
|
return {
|
|
112
112
|
...open,
|
|
113
113
|
toolbarCompressed: true,
|
|
@@ -13,7 +13,7 @@ import Animated, {
|
|
|
13
13
|
withSpring,
|
|
14
14
|
} from 'react-native-reanimated';
|
|
15
15
|
import { Colors, Radius, Spacing } from '../../Consts';
|
|
16
|
-
import { ApplicationContext } from '../../Context';
|
|
16
|
+
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
17
17
|
import { Icon } from '../../Icon';
|
|
18
18
|
import { DuoMetrics, useScreenLayout } from '../../Layout/utils';
|
|
19
19
|
import { Text } from '../../Text';
|
|
@@ -33,40 +33,49 @@ const ICON_SIZE = 28;
|
|
|
33
33
|
const QR_ICON_SIZE = 24;
|
|
34
34
|
const RAIL_PADDING = 6;
|
|
35
35
|
const RAIL_GAP = Spacing.XXS;
|
|
36
|
+
const RAIL_PITCH = ITEM_SIZE + RAIL_GAP;
|
|
36
37
|
// selection bubble overflows the 48 rail cell by 5 above and below (Figma MT2)
|
|
37
38
|
const RAIL_BUBBLE_WIDTH = 44;
|
|
38
39
|
const RAIL_BUBBLE_HEIGHT = 58;
|
|
39
|
-
const FLOATING_HEIGHT =
|
|
40
|
+
const FLOATING_HEIGHT = DuoMetrics.floatingTabBarHeight;
|
|
40
41
|
const FLOATING_PADDING = Spacing.XS;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// the system bar sizes its pill to the tabs instead of stretching it across the display
|
|
43
|
+
const FLOATING_ITEM_WIDTH = 80;
|
|
44
|
+
// while pressed the selection bubble becomes the iOS 26 glass lens, growing past the
|
|
45
|
+
// pill's edges, wider and taller (80x54 to about 96x70)
|
|
46
|
+
const LENS_SCALE_X = 1.2;
|
|
47
|
+
const LENS_SCALE_Y = 1.3;
|
|
48
|
+
const FLOATING_BUBBLE_RADIUS = (FLOATING_HEIGHT - 2 * FLOATING_PADDING) / 2;
|
|
45
49
|
const SPRING = { damping: 18, stiffness: 220, mass: 0.8 };
|
|
46
50
|
|
|
51
|
+
/**
|
|
52
|
+
* distance from the screen bottom to the floating tab bar, overlapping the home
|
|
53
|
+
* indicator inset like the system tab bar
|
|
54
|
+
*/
|
|
55
|
+
const getFloatingTabBarBottom = (bottomInset: number) =>
|
|
56
|
+
Math.max(bottomInset - DuoMetrics.floatingTabBarInsetOverlap, Spacing.S);
|
|
57
|
+
|
|
47
58
|
/**
|
|
48
59
|
* space tab content leaves under the Duo floating tab bar, 0 elsewhere
|
|
49
60
|
*/
|
|
50
61
|
const useDuoTabBarInset = () => {
|
|
51
62
|
const { isIPhoneDuo, sideBar, insets } = useScreenLayout();
|
|
52
63
|
return isIPhoneDuo && !sideBar
|
|
53
|
-
? FLOATING_HEIGHT +
|
|
64
|
+
? FLOATING_HEIGHT + getFloatingTabBarBottom(insets.bottom)
|
|
54
65
|
: 0;
|
|
55
66
|
};
|
|
56
67
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const BarMaterial: React.FC<{ radius: number }> = ({ radius }) => {
|
|
68
|
+
const BarMaterial: React.FC<{ radius: number; interactive?: boolean }> = ({
|
|
69
|
+
radius,
|
|
70
|
+
interactive,
|
|
71
|
+
}) => {
|
|
64
72
|
const { theme } = useContext(ApplicationContext);
|
|
65
73
|
if (GlassView) {
|
|
66
74
|
return (
|
|
67
75
|
<GlassView
|
|
68
76
|
pointerEvents={'none'}
|
|
69
77
|
effect={'glass'}
|
|
78
|
+
interactive={interactive}
|
|
70
79
|
cornerRadius={radius}
|
|
71
80
|
useShadow
|
|
72
81
|
style={StyleSheet.absoluteFill}
|
|
@@ -89,6 +98,67 @@ const BarMaterial: React.FC<{ radius: number }> = ({ radius }) => {
|
|
|
89
98
|
);
|
|
90
99
|
};
|
|
91
100
|
|
|
101
|
+
// Figma Home-2026 tab bar "Selection": brand pink at 8% over glass
|
|
102
|
+
const SELECTION_TINT_ALPHA = '14';
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* translucent glass capsule under the selected floating tab with the Figma pink tint on
|
|
106
|
+
* top; the tint is a separate layer since a glass tint color renders opaque
|
|
107
|
+
*/
|
|
108
|
+
const SelectionMaterial: React.FC<{ radius: number }> = ({ radius }) => {
|
|
109
|
+
const { theme } = useContext(ApplicationContext);
|
|
110
|
+
return (
|
|
111
|
+
<>
|
|
112
|
+
{GlassView && (
|
|
113
|
+
<GlassView
|
|
114
|
+
pointerEvents={'none'}
|
|
115
|
+
effect={'glass'}
|
|
116
|
+
cornerRadius={radius}
|
|
117
|
+
style={StyleSheet.absoluteFill}
|
|
118
|
+
/>
|
|
119
|
+
)}
|
|
120
|
+
<View
|
|
121
|
+
pointerEvents={'none'}
|
|
122
|
+
style={[
|
|
123
|
+
StyleSheet.absoluteFill,
|
|
124
|
+
{
|
|
125
|
+
borderRadius: radius,
|
|
126
|
+
backgroundColor: theme.colors.primary + SELECTION_TINT_ALPHA,
|
|
127
|
+
},
|
|
128
|
+
]}
|
|
129
|
+
/>
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* tab label at its design size: like the system tab bar it doesn't grow with the wide
|
|
136
|
+
* Duo displays, which Text otherwise scales up to 5pt
|
|
137
|
+
*/
|
|
138
|
+
const TabLabel: React.FC<{
|
|
139
|
+
label: string;
|
|
140
|
+
color: string;
|
|
141
|
+
typography: 'label_xs_medium';
|
|
142
|
+
}> = ({ label, color, typography }) => {
|
|
143
|
+
const context = useContext<any>(MiniAppContext);
|
|
144
|
+
return (
|
|
145
|
+
<MiniAppContext.Provider
|
|
146
|
+
value={{
|
|
147
|
+
...context,
|
|
148
|
+
fontScale: { useOSScaleRate: false, userScaleRate: 1 },
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<Text
|
|
152
|
+
numberOfLines={1}
|
|
153
|
+
typography={typography}
|
|
154
|
+
color={color}
|
|
155
|
+
>
|
|
156
|
+
{label}
|
|
157
|
+
</Text>
|
|
158
|
+
</MiniAppContext.Provider>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
92
162
|
const getTabItems = ({
|
|
93
163
|
state,
|
|
94
164
|
descriptors,
|
|
@@ -150,41 +220,52 @@ const QrIcon: React.FC<{ icon: FloatingButtonProps['icon']; size: number }> = ({
|
|
|
150
220
|
type TabItem = ReturnType<typeof getTabItems>[number];
|
|
151
221
|
|
|
152
222
|
/**
|
|
153
|
-
* glass column of tab items with the sliding selection bubble
|
|
223
|
+
* glass column of icon-only tab items with the sliding selection bubble
|
|
154
224
|
*/
|
|
155
225
|
const Rail: React.FC<{
|
|
156
226
|
items: TabItem[];
|
|
157
227
|
index: number;
|
|
158
228
|
onItemPress?: () => void;
|
|
159
229
|
}> = ({ items, index, onItemPress }) => {
|
|
160
|
-
const
|
|
161
|
-
const
|
|
230
|
+
const bubbleOffset = useSharedValue(index * RAIL_PITCH);
|
|
231
|
+
const lens = useSharedValue(0);
|
|
162
232
|
|
|
163
233
|
useEffect(() => {
|
|
164
|
-
bubbleOffset.value = withSpring(index *
|
|
234
|
+
bubbleOffset.value = withSpring(index * RAIL_PITCH, SPRING);
|
|
165
235
|
}, [index, bubbleOffset]);
|
|
166
236
|
|
|
167
237
|
const bubbleStyle = useAnimatedStyle(() => ({
|
|
168
|
-
transform: [
|
|
238
|
+
transform: [
|
|
239
|
+
{ translateY: bubbleOffset.value },
|
|
240
|
+
{ scaleX: 1 + lens.value * (LENS_SCALE_X - 1) },
|
|
241
|
+
{ scaleY: 1 + lens.value * (LENS_SCALE_Y - 1) },
|
|
242
|
+
],
|
|
169
243
|
}));
|
|
170
244
|
|
|
171
245
|
return (
|
|
172
246
|
<View style={styles.rail} accessibilityLabel={'bottom_tab_bar'}>
|
|
173
|
-
<BarMaterial radius={
|
|
247
|
+
<BarMaterial radius={ITEM_SIZE / 2} interactive />
|
|
174
248
|
<Animated.View
|
|
175
249
|
pointerEvents={'none'}
|
|
176
|
-
style={[
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
/>
|
|
182
|
-
{items.map(item => (
|
|
250
|
+
style={[styles.railBubble, bubbleStyle]}
|
|
251
|
+
>
|
|
252
|
+
<SelectionMaterial radius={RAIL_BUBBLE_WIDTH / 2} />
|
|
253
|
+
</Animated.View>
|
|
254
|
+
{items.map((item, itemIndex) => (
|
|
183
255
|
<TouchableOpacity
|
|
184
256
|
key={item.route.key}
|
|
185
257
|
accessibilityRole={'tab'}
|
|
186
258
|
accessibilityLabel={item.label}
|
|
187
259
|
accessibilityState={{ selected: item.focused }}
|
|
260
|
+
activeOpacity={1}
|
|
261
|
+
onPressIn={() => {
|
|
262
|
+
bubbleOffset.value = withSpring(itemIndex * RAIL_PITCH, SPRING);
|
|
263
|
+
lens.value = withSpring(1, SPRING);
|
|
264
|
+
}}
|
|
265
|
+
onPressOut={() => {
|
|
266
|
+
bubbleOffset.value = withSpring(index * RAIL_PITCH, SPRING);
|
|
267
|
+
lens.value = withSpring(0, SPRING);
|
|
268
|
+
}}
|
|
188
269
|
onPress={() => {
|
|
189
270
|
item.onPress();
|
|
190
271
|
onItemPress?.();
|
|
@@ -267,65 +348,103 @@ const DuoRailTabBar: React.FC<Props> = props => {
|
|
|
267
348
|
};
|
|
268
349
|
|
|
269
350
|
/**
|
|
270
|
-
* floating labelled bar for the unfolded portrait display, QR in
|
|
351
|
+
* floating labelled glass bar for the unfolded portrait display, QR pinned in its own
|
|
352
|
+
* platter on the trailing side, like the host app's iOS 26 system tab bar
|
|
271
353
|
*/
|
|
272
354
|
const DuoFloatingTabBar: React.FC<Props> = props => {
|
|
273
355
|
const { theme } = useContext(ApplicationContext);
|
|
274
|
-
const {
|
|
356
|
+
const { insets } = useScreenLayout();
|
|
275
357
|
const { state, floatingButton, activeTintColor, inactiveTintColor } = props;
|
|
276
358
|
const items = getTabItems(props);
|
|
277
359
|
const [rowWidth, setRowWidth] = useState(0);
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
const useQr = !!floatingButton && state.routes.length % 2 === 0;
|
|
281
|
-
const middle = state.routes.length / 2;
|
|
282
|
-
const cellWidth = rowWidth / (state.routes.length + (useQr ? 1 : 0));
|
|
283
|
-
const visualIndex =
|
|
284
|
-
useQr && state.index >= middle ? state.index + 1 : state.index;
|
|
360
|
+
const cellWidth = rowWidth / state.routes.length;
|
|
285
361
|
|
|
286
362
|
const bubbleOffset = useSharedValue(0);
|
|
363
|
+
const lens = useSharedValue(0);
|
|
287
364
|
const measured = useRef(false);
|
|
288
365
|
|
|
289
366
|
useEffect(() => {
|
|
290
|
-
const target =
|
|
367
|
+
const target = state.index * cellWidth;
|
|
291
368
|
// snap on first layout so the bubble doesn't slide in from the left edge
|
|
292
369
|
bubbleOffset.value = measured.current ? withSpring(target, SPRING) : target;
|
|
293
370
|
measured.current = cellWidth > 0;
|
|
294
|
-
}, [
|
|
371
|
+
}, [state.index, cellWidth, bubbleOffset]);
|
|
295
372
|
|
|
296
373
|
const bubbleStyle = useAnimatedStyle(() => ({
|
|
297
|
-
transform: [
|
|
374
|
+
transform: [
|
|
375
|
+
{ translateX: bubbleOffset.value },
|
|
376
|
+
{ scaleX: 1 + lens.value * (LENS_SCALE_X - 1) },
|
|
377
|
+
{ scaleY: 1 + lens.value * (LENS_SCALE_Y - 1) },
|
|
378
|
+
],
|
|
298
379
|
}));
|
|
299
380
|
|
|
300
381
|
const onRowLayout = (e: LayoutChangeEvent) =>
|
|
301
382
|
setRowWidth(e.nativeEvent.layout.width);
|
|
302
383
|
|
|
303
|
-
|
|
304
|
-
<
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
384
|
+
return (
|
|
385
|
+
<View
|
|
386
|
+
pointerEvents={'box-none'}
|
|
387
|
+
style={[
|
|
388
|
+
styles.floating,
|
|
389
|
+
{
|
|
390
|
+
left: insets.left + DuoMetrics.floatingTabBarMargin,
|
|
391
|
+
right: insets.right + DuoMetrics.floatingTabBarMargin,
|
|
392
|
+
bottom: getFloatingTabBarBottom(insets.bottom),
|
|
393
|
+
},
|
|
394
|
+
]}
|
|
312
395
|
>
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
396
|
+
<View
|
|
397
|
+
style={[
|
|
398
|
+
styles.floatingBar,
|
|
399
|
+
{
|
|
400
|
+
width:
|
|
401
|
+
state.routes.length * FLOATING_ITEM_WIDTH + 2 * FLOATING_PADDING,
|
|
402
|
+
},
|
|
403
|
+
]}
|
|
404
|
+
accessibilityLabel={'bottom_tab_bar'}
|
|
318
405
|
>
|
|
319
|
-
{
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
406
|
+
<BarMaterial radius={FLOATING_HEIGHT / 2} interactive />
|
|
407
|
+
<View style={styles.floatingRow} onLayout={onRowLayout}>
|
|
408
|
+
{rowWidth > 0 && (
|
|
409
|
+
<Animated.View
|
|
410
|
+
pointerEvents={'none'}
|
|
411
|
+
style={[styles.floatingBubble, { width: cellWidth }, bubbleStyle]}
|
|
412
|
+
>
|
|
413
|
+
<SelectionMaterial radius={FLOATING_BUBBLE_RADIUS} />
|
|
414
|
+
</Animated.View>
|
|
415
|
+
)}
|
|
416
|
+
{items.map((item, index) => (
|
|
417
|
+
<TouchableOpacity
|
|
418
|
+
key={item.route.key}
|
|
419
|
+
accessibilityRole={'tab'}
|
|
420
|
+
accessibilityLabel={item.label}
|
|
421
|
+
accessibilityState={{ selected: item.focused }}
|
|
422
|
+
activeOpacity={1}
|
|
423
|
+
onPressIn={() => {
|
|
424
|
+
// the lens jumps to the pressed tab, not only the selected one
|
|
425
|
+
bubbleOffset.value = withSpring(index * cellWidth, SPRING);
|
|
426
|
+
lens.value = withSpring(1, SPRING);
|
|
427
|
+
}}
|
|
428
|
+
onPressOut={() => {
|
|
429
|
+
// onPress follows synchronously, a tab switch re-targets the bubble
|
|
430
|
+
bubbleOffset.value = withSpring(state.index * cellWidth, SPRING);
|
|
431
|
+
lens.value = withSpring(0, SPRING);
|
|
432
|
+
}}
|
|
433
|
+
onPress={item.onPress}
|
|
434
|
+
onLongPress={item.onLongPress}
|
|
435
|
+
style={styles.floatingItem}
|
|
436
|
+
>
|
|
437
|
+
{item.icon}
|
|
438
|
+
<TabLabel
|
|
439
|
+
label={item.label}
|
|
440
|
+
typography={'label_xs_medium'}
|
|
441
|
+
color={item.focused ? activeTintColor : inactiveTintColor}
|
|
442
|
+
/>
|
|
443
|
+
</TouchableOpacity>
|
|
444
|
+
))}
|
|
445
|
+
</View>
|
|
446
|
+
</View>
|
|
447
|
+
{floatingButton && (
|
|
329
448
|
<TouchableOpacity
|
|
330
449
|
accessibilityRole={'button'}
|
|
331
450
|
accessibilityLabel={floatingButton.label}
|
|
@@ -334,39 +453,7 @@ const DuoFloatingTabBar: React.FC<Props> = props => {
|
|
|
334
453
|
>
|
|
335
454
|
<QrIcon icon={floatingButton.icon} size={QR_ICON_SIZE} />
|
|
336
455
|
</TouchableOpacity>
|
|
337
|
-
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return (
|
|
342
|
-
<View
|
|
343
|
-
style={[
|
|
344
|
-
styles.floating,
|
|
345
|
-
{
|
|
346
|
-
width: barWidth,
|
|
347
|
-
left: (width - barWidth) / 2,
|
|
348
|
-
bottom: insets.bottom + Spacing.M,
|
|
349
|
-
},
|
|
350
|
-
]}
|
|
351
|
-
accessibilityLabel={'bottom_tab_bar'}
|
|
352
|
-
>
|
|
353
|
-
<BarMaterial radius={FLOATING_HEIGHT / 2} />
|
|
354
|
-
<View style={styles.floatingRow} onLayout={onRowLayout}>
|
|
355
|
-
{rowWidth > 0 && (
|
|
356
|
-
<Animated.View
|
|
357
|
-
pointerEvents={'none'}
|
|
358
|
-
style={[
|
|
359
|
-
styles.floatingBubble,
|
|
360
|
-
{
|
|
361
|
-
width: cellWidth,
|
|
362
|
-
backgroundColor: theme.colors.background.selected,
|
|
363
|
-
},
|
|
364
|
-
bubbleStyle,
|
|
365
|
-
]}
|
|
366
|
-
/>
|
|
367
|
-
)}
|
|
368
|
-
{cells}
|
|
369
|
-
</View>
|
|
456
|
+
)}
|
|
370
457
|
</View>
|
|
371
458
|
);
|
|
372
459
|
};
|
|
@@ -413,7 +500,6 @@ const styles = StyleSheet.create({
|
|
|
413
500
|
left: (ITEM_SIZE - RAIL_BUBBLE_WIDTH) / 2,
|
|
414
501
|
width: RAIL_BUBBLE_WIDTH,
|
|
415
502
|
height: RAIL_BUBBLE_HEIGHT,
|
|
416
|
-
borderRadius: RAIL_BUBBLE_WIDTH / 2,
|
|
417
503
|
},
|
|
418
504
|
railItem: {
|
|
419
505
|
width: ITEM_SIZE,
|
|
@@ -430,6 +516,12 @@ const styles = StyleSheet.create({
|
|
|
430
516
|
},
|
|
431
517
|
floating: {
|
|
432
518
|
position: 'absolute',
|
|
519
|
+
flexDirection: 'row',
|
|
520
|
+
justifyContent: 'center',
|
|
521
|
+
gap: Spacing.S,
|
|
522
|
+
},
|
|
523
|
+
floatingBar: {
|
|
524
|
+
flexShrink: 1,
|
|
433
525
|
height: FLOATING_HEIGHT,
|
|
434
526
|
padding: FLOATING_PADDING,
|
|
435
527
|
},
|
|
@@ -442,7 +534,6 @@ const styles = StyleSheet.create({
|
|
|
442
534
|
top: 0,
|
|
443
535
|
bottom: 0,
|
|
444
536
|
left: 0,
|
|
445
|
-
borderRadius: (FLOATING_HEIGHT - 2 * FLOATING_PADDING) / 2,
|
|
446
537
|
},
|
|
447
538
|
floatingItem: {
|
|
448
539
|
flex: 1,
|
|
@@ -450,12 +541,17 @@ const styles = StyleSheet.create({
|
|
|
450
541
|
justifyContent: 'center',
|
|
451
542
|
gap: Spacing.XXS,
|
|
452
543
|
},
|
|
544
|
+
// pink fills the whole pinned platter like the host app, shadow matches the glass bar
|
|
453
545
|
floatingQr: {
|
|
454
|
-
width:
|
|
455
|
-
height:
|
|
456
|
-
borderRadius:
|
|
546
|
+
width: FLOATING_HEIGHT,
|
|
547
|
+
height: FLOATING_HEIGHT,
|
|
548
|
+
borderRadius: FLOATING_HEIGHT / 2,
|
|
457
549
|
alignItems: 'center',
|
|
458
550
|
justifyContent: 'center',
|
|
551
|
+
shadowColor: Colors.black_20,
|
|
552
|
+
shadowOffset: { width: 0, height: 6 },
|
|
553
|
+
shadowOpacity: 0.16,
|
|
554
|
+
shadowRadius: 16,
|
|
459
555
|
},
|
|
460
556
|
});
|
|
461
557
|
|
|
@@ -316,7 +316,7 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
316
316
|
<DuoTabBar
|
|
317
317
|
{...props}
|
|
318
318
|
floatingButton={floatingButton}
|
|
319
|
-
inactiveTintColor={theme.colors.text.
|
|
319
|
+
inactiveTintColor={theme.colors.text.hint}
|
|
320
320
|
activeTintColor={theme.colors.primary}
|
|
321
321
|
/>
|
|
322
322
|
) : (
|
package/Layout/utils.ts
CHANGED
|
@@ -80,6 +80,11 @@ const HeaderMetrics = {
|
|
|
80
80
|
* - innerStatusBarCenterY / innerStatusBarWidth: on the inner display in portrait the
|
|
81
81
|
* clock and connectivity cluster sits at the top right corner (x 550–643 of 669, clock
|
|
82
82
|
* digits centered on y 47); its reserved region, when reported, is taller than the glyphs
|
|
83
|
+
* - floatingTabBarHeight / floatingTabBarMargin / floatingTabBarInsetOverlap: the host
|
|
84
|
+
* app's iOS 26 system tab bar on the inner display in portrait, a 62pt Liquid Glass pill
|
|
85
|
+
* sized to its tabs with the QR as a pinned 62pt platter beside it, centered and at
|
|
86
|
+
* least 20pt from the side edges; it overlaps the home indicator inset by 13pt (21pt
|
|
87
|
+
* above the screen bottom with a 34pt inset)
|
|
83
88
|
*/
|
|
84
89
|
const DuoMetrics = {
|
|
85
90
|
maxAspectRatio: 1.6,
|
|
@@ -93,6 +98,9 @@ const DuoMetrics = {
|
|
|
93
98
|
edgeInset: 24,
|
|
94
99
|
innerStatusBarCenterY: 47,
|
|
95
100
|
innerStatusBarWidth: 120,
|
|
101
|
+
floatingTabBarHeight: 62,
|
|
102
|
+
floatingTabBarMargin: 20,
|
|
103
|
+
floatingTabBarInsetOverlap: 13,
|
|
96
104
|
};
|
|
97
105
|
|
|
98
106
|
const NO_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };
|