@momo-kits/foundation 0.167.1-duo.4 → 0.167.1-duo.5
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/BottomTab/DuoTabBar.tsx +55 -107
- package/Application/BottomTab/index.tsx +132 -143
- package/Application/Components/DuoGlassBackground.tsx +26 -28
- package/Application/Components/HeaderLeft.tsx +2 -8
- package/Application/Components/HeaderRight.tsx +2 -47
- package/Application/NavigationContainer.tsx +11 -14
- package/Application/types.ts +0 -5
- package/Layout/Screen.tsx +3 -79
- package/Layout/utils.ts +8 -76
- package/package.json +1 -1
- package/Application/BottomTab/DuoBarCompression.tsx +0 -135
- package/Application/Components/DuoQuickReturnHeader.tsx +0 -42
- package/Layout/ReservedRegions.tsx +0 -47
|
@@ -18,7 +18,6 @@ import { Icon } from '../../Icon';
|
|
|
18
18
|
import { DuoMetrics, useScreenLayout } from '../../Layout/utils';
|
|
19
19
|
import { Text } from '../../Text';
|
|
20
20
|
import { GlassView } from '../Components/OptionalGlassView';
|
|
21
|
-
import { useDuoBarCompression } from './DuoBarCompression';
|
|
22
21
|
import { FloatingButtonProps } from '../types';
|
|
23
22
|
import { TabBarIcon } from './TabBarIcon';
|
|
24
23
|
|
|
@@ -147,121 +146,76 @@ const QrIcon: React.FC<{ icon: FloatingButtonProps['icon']; size: number }> = ({
|
|
|
147
146
|
return <>{icon}</>;
|
|
148
147
|
};
|
|
149
148
|
|
|
150
|
-
type TabItem = ReturnType<typeof getTabItems>[number];
|
|
151
|
-
|
|
152
149
|
/**
|
|
153
|
-
*
|
|
154
|
-
*/
|
|
155
|
-
const Rail: React.FC<{
|
|
156
|
-
items: TabItem[];
|
|
157
|
-
index: number;
|
|
158
|
-
onItemPress?: () => void;
|
|
159
|
-
}> = ({ items, index, onItemPress }) => {
|
|
160
|
-
const { theme } = useContext(ApplicationContext);
|
|
161
|
-
const bubbleOffset = useSharedValue(index * (ITEM_SIZE + RAIL_GAP));
|
|
162
|
-
|
|
163
|
-
useEffect(() => {
|
|
164
|
-
bubbleOffset.value = withSpring(index * (ITEM_SIZE + RAIL_GAP), SPRING);
|
|
165
|
-
}, [index, bubbleOffset]);
|
|
166
|
-
|
|
167
|
-
const bubbleStyle = useAnimatedStyle(() => ({
|
|
168
|
-
transform: [{ translateY: bubbleOffset.value }],
|
|
169
|
-
}));
|
|
170
|
-
|
|
171
|
-
return (
|
|
172
|
-
<View style={styles.rail} accessibilityLabel={'bottom_tab_bar'}>
|
|
173
|
-
<BarMaterial radius={Radius.XL} />
|
|
174
|
-
<Animated.View
|
|
175
|
-
pointerEvents={'none'}
|
|
176
|
-
style={[
|
|
177
|
-
styles.railBubble,
|
|
178
|
-
{ backgroundColor: theme.colors.background.selected },
|
|
179
|
-
bubbleStyle,
|
|
180
|
-
]}
|
|
181
|
-
/>
|
|
182
|
-
{items.map(item => (
|
|
183
|
-
<TouchableOpacity
|
|
184
|
-
key={item.route.key}
|
|
185
|
-
accessibilityRole={'tab'}
|
|
186
|
-
accessibilityLabel={item.label}
|
|
187
|
-
accessibilityState={{ selected: item.focused }}
|
|
188
|
-
onPress={() => {
|
|
189
|
-
item.onPress();
|
|
190
|
-
onItemPress?.();
|
|
191
|
-
}}
|
|
192
|
-
onLongPress={item.onLongPress}
|
|
193
|
-
style={styles.railItem}
|
|
194
|
-
>
|
|
195
|
-
{item.icon}
|
|
196
|
-
</TouchableOpacity>
|
|
197
|
-
))}
|
|
198
|
-
</View>
|
|
199
|
-
);
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* vertical bar anchored to the bottom of the Duo side bar, QR button below it (Figma MT1-C/E).
|
|
204
|
-
* When the side bar is short, it can shrink to one button showing the current tab
|
|
205
|
-
* (HIG "Tab bar compressed"); tapping it expands the rail and compresses the toolbar
|
|
150
|
+
* vertical bar anchored to the bottom of the Duo side bar, QR button below it (Figma MT1-C/E)
|
|
206
151
|
*/
|
|
207
152
|
const DuoRailTabBar: React.FC<Props> = props => {
|
|
208
153
|
const { theme } = useContext(ApplicationContext);
|
|
209
154
|
const { sideBar } = useScreenLayout();
|
|
210
|
-
const { tabBarCompressed, expandTabBar, onTabSelected } =
|
|
211
|
-
useDuoBarCompression();
|
|
212
155
|
const { state, floatingButton } = props;
|
|
213
156
|
const items = getTabItems(props);
|
|
214
|
-
const
|
|
157
|
+
const bubbleOffset = useSharedValue(state.index * (ITEM_SIZE + RAIL_GAP));
|
|
215
158
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
},
|
|
223
|
-
sideBar.edge === 'right'
|
|
224
|
-
? { alignItems: 'flex-start' as const, paddingLeft: Spacing.M }
|
|
225
|
-
: { alignItems: 'flex-end' as const, paddingRight: Spacing.M },
|
|
226
|
-
];
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
bubbleOffset.value = withSpring(
|
|
161
|
+
state.index * (ITEM_SIZE + RAIL_GAP),
|
|
162
|
+
SPRING,
|
|
163
|
+
);
|
|
164
|
+
}, [state.index, bubbleOffset]);
|
|
227
165
|
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
accessibilityLabel={floatingButton.label}
|
|
232
|
-
onPress={floatingButton.onPress}
|
|
233
|
-
style={[styles.railQr, { backgroundColor: theme.colors.primary }]}
|
|
234
|
-
>
|
|
235
|
-
<QrIcon icon={floatingButton.icon} size={QR_ICON_SIZE} />
|
|
236
|
-
</TouchableOpacity>
|
|
237
|
-
);
|
|
166
|
+
const bubbleStyle = useAnimatedStyle(() => ({
|
|
167
|
+
transform: [{ translateY: bubbleOffset.value }],
|
|
168
|
+
}));
|
|
238
169
|
|
|
239
170
|
if (!sideBar) {
|
|
240
171
|
return null;
|
|
241
172
|
}
|
|
242
173
|
|
|
243
|
-
if (!tabBarCompressed) {
|
|
244
|
-
return (
|
|
245
|
-
<View pointerEvents={'box-none'} style={columnStyle}>
|
|
246
|
-
<Rail items={items} index={state.index} onItemPress={onTabSelected} />
|
|
247
|
-
{qr}
|
|
248
|
-
</View>
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
174
|
return (
|
|
253
|
-
<View
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
175
|
+
<View
|
|
176
|
+
pointerEvents={'box-none'}
|
|
177
|
+
style={[
|
|
178
|
+
styles.railColumn,
|
|
179
|
+
{ [sideBar.edge]: 0, width: sideBar.width },
|
|
180
|
+
sideBar.edge === 'right'
|
|
181
|
+
? { alignItems: 'flex-start', paddingLeft: Spacing.M }
|
|
182
|
+
: { alignItems: 'flex-end', paddingRight: Spacing.M },
|
|
183
|
+
]}
|
|
184
|
+
>
|
|
185
|
+
<View style={styles.rail} accessibilityLabel={'bottom_tab_bar'}>
|
|
261
186
|
<BarMaterial radius={Radius.XL} />
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
187
|
+
<Animated.View
|
|
188
|
+
pointerEvents={'none'}
|
|
189
|
+
style={[
|
|
190
|
+
styles.railBubble,
|
|
191
|
+
{ backgroundColor: theme.colors.background.selected },
|
|
192
|
+
bubbleStyle,
|
|
193
|
+
]}
|
|
194
|
+
/>
|
|
195
|
+
{items.map(item => (
|
|
196
|
+
<TouchableOpacity
|
|
197
|
+
key={item.route.key}
|
|
198
|
+
accessibilityRole={'tab'}
|
|
199
|
+
accessibilityLabel={item.label}
|
|
200
|
+
accessibilityState={{ selected: item.focused }}
|
|
201
|
+
onPress={item.onPress}
|
|
202
|
+
onLongPress={item.onLongPress}
|
|
203
|
+
style={styles.railItem}
|
|
204
|
+
>
|
|
205
|
+
{item.icon}
|
|
206
|
+
</TouchableOpacity>
|
|
207
|
+
))}
|
|
208
|
+
</View>
|
|
209
|
+
{floatingButton && (
|
|
210
|
+
<TouchableOpacity
|
|
211
|
+
accessibilityRole={'button'}
|
|
212
|
+
accessibilityLabel={floatingButton.label}
|
|
213
|
+
onPress={floatingButton.onPress}
|
|
214
|
+
style={[styles.railQr, { backgroundColor: theme.colors.primary }]}
|
|
215
|
+
>
|
|
216
|
+
<QrIcon icon={floatingButton.icon} size={QR_ICON_SIZE} />
|
|
217
|
+
</TouchableOpacity>
|
|
218
|
+
)}
|
|
265
219
|
</View>
|
|
266
220
|
);
|
|
267
221
|
};
|
|
@@ -393,15 +347,9 @@ const styles = StyleSheet.create({
|
|
|
393
347
|
},
|
|
394
348
|
railColumn: {
|
|
395
349
|
position: 'absolute',
|
|
350
|
+
bottom: Spacing.XL,
|
|
396
351
|
gap: Spacing.M,
|
|
397
352
|
},
|
|
398
|
-
railCompact: {
|
|
399
|
-
width: ITEM_SIZE,
|
|
400
|
-
height: ITEM_SIZE,
|
|
401
|
-
borderRadius: ITEM_SIZE / 2,
|
|
402
|
-
alignItems: 'center',
|
|
403
|
-
justifyContent: 'center',
|
|
404
|
-
},
|
|
405
353
|
rail: {
|
|
406
354
|
width: ITEM_SIZE,
|
|
407
355
|
paddingVertical: RAIL_PADDING,
|
|
@@ -28,7 +28,6 @@ import { BottomTabProps } from '../types';
|
|
|
28
28
|
import { getOptions, getStackOptions, useAppState } from '../utils';
|
|
29
29
|
import BottomTabBar from './BottomTabBar';
|
|
30
30
|
import { DuoTabBar } from './DuoTabBar';
|
|
31
|
-
import { DuoBarCompressionProvider } from './DuoBarCompression';
|
|
32
31
|
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
33
32
|
import { useScreenLayout } from '../../Layout';
|
|
34
33
|
|
|
@@ -206,7 +205,6 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
206
205
|
navigation,
|
|
207
206
|
initialRouteName,
|
|
208
207
|
floatingButton,
|
|
209
|
-
compressionBehavior,
|
|
210
208
|
}) => {
|
|
211
209
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
212
210
|
const { isIPhoneDuo } = useScreenLayout();
|
|
@@ -283,156 +281,147 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
283
281
|
};
|
|
284
282
|
|
|
285
283
|
return (
|
|
286
|
-
<
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
284
|
+
<Tab.Navigator
|
|
285
|
+
screenOptions={{
|
|
286
|
+
headerShown: false,
|
|
287
|
+
headerTransparent: true,
|
|
288
|
+
headerBackground: undefined,
|
|
289
|
+
tabBarLabelPosition: 'below-icon',
|
|
290
|
+
tabBarBackground: () => (
|
|
291
|
+
<View
|
|
292
|
+
style={[
|
|
293
|
+
styles.container,
|
|
294
|
+
{
|
|
295
|
+
backgroundColor: theme.colors.background.surface,
|
|
296
|
+
},
|
|
297
|
+
]}
|
|
298
|
+
>
|
|
299
|
+
<Animated.View style={[styles.indicatorContainer, indicatorStyle]}>
|
|
300
|
+
<View
|
|
301
|
+
style={[
|
|
302
|
+
styles.indicator,
|
|
303
|
+
{ backgroundColor: theme.colors.primary },
|
|
304
|
+
]}
|
|
305
|
+
/>
|
|
306
|
+
</Animated.View>
|
|
307
|
+
</View>
|
|
308
|
+
),
|
|
309
|
+
}}
|
|
310
|
+
tabBar={props =>
|
|
311
|
+
isIPhoneDuo ? (
|
|
312
|
+
<DuoTabBar
|
|
313
|
+
{...props}
|
|
314
|
+
floatingButton={floatingButton}
|
|
315
|
+
inactiveTintColor={theme.colors.text.default}
|
|
316
|
+
activeTintColor={theme.colors.primary}
|
|
317
|
+
/>
|
|
318
|
+
) : (
|
|
319
|
+
<BottomTabBar
|
|
320
|
+
{...props}
|
|
321
|
+
floatingButton={floatingButton}
|
|
322
|
+
inactiveTintColor={theme.colors.text.hint}
|
|
323
|
+
activeTintColor={theme.colors.primary}
|
|
324
|
+
style={[
|
|
325
|
+
{
|
|
326
|
+
height:
|
|
327
|
+
64 +
|
|
328
|
+
(Platform.OS === 'ios'
|
|
329
|
+
? Math.min(insets.bottom, 21)
|
|
330
|
+
: insets.bottom),
|
|
331
|
+
},
|
|
332
|
+
]}
|
|
333
|
+
/>
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
initialRouteName={initialRouteName}
|
|
290
337
|
>
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
style={[
|
|
300
|
-
styles.container,
|
|
301
|
-
{
|
|
302
|
-
backgroundColor: theme.colors.background.surface,
|
|
303
|
-
},
|
|
304
|
-
]}
|
|
305
|
-
>
|
|
306
|
-
<Animated.View
|
|
307
|
-
style={[styles.indicatorContainer, indicatorStyle]}
|
|
308
|
-
>
|
|
309
|
-
<View
|
|
310
|
-
style={[
|
|
311
|
-
styles.indicator,
|
|
312
|
-
{ backgroundColor: theme.colors.primary },
|
|
313
|
-
]}
|
|
314
|
-
/>
|
|
315
|
-
</Animated.View>
|
|
316
|
-
</View>
|
|
317
|
-
),
|
|
318
|
-
}}
|
|
319
|
-
tabBar={props =>
|
|
320
|
-
isIPhoneDuo ? (
|
|
321
|
-
<DuoTabBar
|
|
322
|
-
{...props}
|
|
323
|
-
floatingButton={floatingButton}
|
|
324
|
-
inactiveTintColor={theme.colors.text.default}
|
|
325
|
-
activeTintColor={theme.colors.primary}
|
|
326
|
-
/>
|
|
327
|
-
) : (
|
|
328
|
-
<BottomTabBar
|
|
329
|
-
{...props}
|
|
330
|
-
floatingButton={floatingButton}
|
|
331
|
-
inactiveTintColor={theme.colors.text.hint}
|
|
332
|
-
activeTintColor={theme.colors.primary}
|
|
333
|
-
style={[
|
|
334
|
-
{
|
|
335
|
-
height:
|
|
336
|
-
64 +
|
|
337
|
-
(Platform.OS === 'ios'
|
|
338
|
-
? Math.min(insets.bottom, 21)
|
|
339
|
-
: insets.bottom),
|
|
340
|
-
},
|
|
341
|
-
]}
|
|
342
|
-
/>
|
|
343
|
-
)
|
|
338
|
+
{tabs.map((item, index) => {
|
|
339
|
+
const isNum = !isNaN(parseInt(`${item?.badgeLabel}`, 10));
|
|
340
|
+
const isDot = typeof item?.badgeLabel === 'string' && !item?.badgeLabel;
|
|
341
|
+
const isNumZero = isNum && Number(item.badgeLabel) === 0;
|
|
342
|
+
|
|
343
|
+
let badgeLabel = item?.badgeLabel;
|
|
344
|
+
if (isNumZero) {
|
|
345
|
+
badgeLabel = undefined;
|
|
344
346
|
}
|
|
345
|
-
initialRouteName={initialRouteName}
|
|
346
|
-
>
|
|
347
|
-
{tabs.map((item, index) => {
|
|
348
|
-
const isNum = !isNaN(parseInt(`${item?.badgeLabel}`, 10));
|
|
349
|
-
const isDot =
|
|
350
|
-
typeof item?.badgeLabel === 'string' && !item?.badgeLabel;
|
|
351
|
-
const isNumZero = isNum && Number(item.badgeLabel) === 0;
|
|
352
347
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
348
|
+
let badgeStyle: {
|
|
349
|
+
marginRight: number;
|
|
350
|
+
top: number;
|
|
351
|
+
backgroundColor: string;
|
|
352
|
+
width?: number;
|
|
353
|
+
} = {
|
|
354
|
+
marginRight: -8,
|
|
355
|
+
top: -4,
|
|
356
|
+
backgroundColor: Colors.orange_03,
|
|
357
|
+
};
|
|
357
358
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
backgroundColor: string;
|
|
362
|
-
width?: number;
|
|
363
|
-
} = {
|
|
364
|
-
marginRight: -8,
|
|
359
|
+
if (isNum) {
|
|
360
|
+
badgeStyle = {
|
|
361
|
+
marginRight: -4,
|
|
365
362
|
top: -4,
|
|
363
|
+
backgroundColor: Colors.red_03,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (isDot) {
|
|
367
|
+
badgeStyle = {
|
|
368
|
+
marginRight: -2,
|
|
369
|
+
top: -2,
|
|
366
370
|
backgroundColor: Colors.orange_03,
|
|
371
|
+
width: 12,
|
|
367
372
|
};
|
|
373
|
+
}
|
|
368
374
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return (
|
|
395
|
-
<Text
|
|
396
|
-
numberOfLines={1}
|
|
397
|
-
typography={
|
|
398
|
-
focused ? 'label_xs_medium' : 'description_xs_regular'
|
|
399
|
-
}
|
|
400
|
-
color={color}
|
|
401
|
-
>
|
|
402
|
-
{item.label}
|
|
403
|
-
</Text>
|
|
404
|
-
);
|
|
375
|
+
return (
|
|
376
|
+
<Tab.Screen
|
|
377
|
+
key={`BottomTab-${item.name}`}
|
|
378
|
+
name={item.name}
|
|
379
|
+
component={TabScreen}
|
|
380
|
+
initialParams={{ ...item, nested, index }}
|
|
381
|
+
listeners={handler}
|
|
382
|
+
options={{
|
|
383
|
+
tabBarLabel: ({ focused, color }) => {
|
|
384
|
+
return (
|
|
385
|
+
<Text
|
|
386
|
+
numberOfLines={1}
|
|
387
|
+
typography={
|
|
388
|
+
focused ? 'label_xs_medium' : 'description_xs_regular'
|
|
389
|
+
}
|
|
390
|
+
color={color}
|
|
391
|
+
>
|
|
392
|
+
{item.label}
|
|
393
|
+
</Text>
|
|
394
|
+
);
|
|
395
|
+
},
|
|
396
|
+
title: item.label,
|
|
397
|
+
tabBarStyle: [
|
|
398
|
+
{
|
|
399
|
+
height: 64,
|
|
405
400
|
},
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}}
|
|
431
|
-
/>
|
|
432
|
-
);
|
|
433
|
-
})}
|
|
434
|
-
</Tab.Navigator>
|
|
435
|
-
</DuoBarCompressionProvider>
|
|
401
|
+
styles.tabStyle,
|
|
402
|
+
],
|
|
403
|
+
tabBarBadge: badgeLabel,
|
|
404
|
+
tabBarBadgeStyle: [
|
|
405
|
+
{
|
|
406
|
+
borderColor: 'white',
|
|
407
|
+
borderWidth: 2,
|
|
408
|
+
fontSize: 10,
|
|
409
|
+
lineHeight: 14,
|
|
410
|
+
fontWeight: 'bold',
|
|
411
|
+
alignSelf: 'center',
|
|
412
|
+
fontFamily: exportFontFamily('bold'),
|
|
413
|
+
},
|
|
414
|
+
badgeStyle,
|
|
415
|
+
],
|
|
416
|
+
tabBarIcon: ({ color }) => (
|
|
417
|
+
<Icon source={item.icon} color={color} size={28} />
|
|
418
|
+
),
|
|
419
|
+
...item,
|
|
420
|
+
}}
|
|
421
|
+
/>
|
|
422
|
+
);
|
|
423
|
+
})}
|
|
424
|
+
</Tab.Navigator>
|
|
436
425
|
);
|
|
437
426
|
};
|
|
438
427
|
|
|
@@ -82,8 +82,8 @@ const DuoGlassBackground: React.FC<{
|
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
* pink header gradient drawn under the content
|
|
86
|
-
*
|
|
85
|
+
* pink header gradient drawn under the content: header-high for default headers
|
|
86
|
+
* (scrolls away), 52 taller for extended ones (collapses and fades, as off Duo)
|
|
87
87
|
*/
|
|
88
88
|
const DuoScrollGradient: React.FC<{
|
|
89
89
|
animatedValue?: AnimatedCompatValue;
|
|
@@ -91,15 +91,12 @@ const DuoScrollGradient: React.FC<{
|
|
|
91
91
|
headerBackground?: string;
|
|
92
92
|
heightHeader: number;
|
|
93
93
|
extended?: boolean;
|
|
94
|
-
// quick return header: the gradient leaves with the content instead of fading
|
|
95
|
-
followScroll?: boolean;
|
|
96
94
|
}> = ({
|
|
97
95
|
animatedValue,
|
|
98
96
|
gradientColor,
|
|
99
97
|
headerBackground,
|
|
100
98
|
heightHeader,
|
|
101
99
|
extended,
|
|
102
|
-
followScroll,
|
|
103
100
|
}) => {
|
|
104
101
|
const { theme } = useContext(ApplicationContext);
|
|
105
102
|
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
@@ -107,21 +104,19 @@ const DuoScrollGradient: React.FC<{
|
|
|
107
104
|
const image = headerBackground ?? theme.assets?.headerBackground;
|
|
108
105
|
|
|
109
106
|
const scrollStyle = useAnimatedStyle(() => {
|
|
110
|
-
if (
|
|
107
|
+
if (!extended) {
|
|
111
108
|
return {
|
|
112
|
-
height: heightHeader
|
|
109
|
+
height: heightHeader,
|
|
113
110
|
transform: [{ translateY: -Math.max(sharedValue.value, 0) }],
|
|
114
111
|
};
|
|
115
112
|
}
|
|
116
113
|
return {
|
|
117
|
-
height:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
)
|
|
124
|
-
: heightHeader,
|
|
114
|
+
height: interpolate(
|
|
115
|
+
sharedValue.value,
|
|
116
|
+
[0, EXTENDED_COLLAPSE_DISTANCE],
|
|
117
|
+
[heightHeader + EXTENDED_GRADIENT_EXTRA, heightHeader],
|
|
118
|
+
Extrapolation.CLAMP,
|
|
119
|
+
),
|
|
125
120
|
opacity: interpolate(
|
|
126
121
|
sharedValue.value,
|
|
127
122
|
[0, EXTENDED_GRADIENT_EXTRA],
|
|
@@ -140,18 +135,21 @@ const DuoScrollGradient: React.FC<{
|
|
|
140
135
|
pointerEvents={'none'}
|
|
141
136
|
style={[styles.gradient, scrollStyle]}
|
|
142
137
|
>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
138
|
+
{/* extended squeezes the whole fade into its box like main; default clips it */}
|
|
139
|
+
<View style={extended ? StyleSheet.absoluteFill : styles.gradientFill}>
|
|
140
|
+
<LinearGradient
|
|
141
|
+
colors={[color, color + '00']}
|
|
142
|
+
style={StyleSheet.absoluteFill}
|
|
143
|
+
>
|
|
144
|
+
{!!image && (
|
|
145
|
+
<Image
|
|
146
|
+
style={styles.gradientFill}
|
|
147
|
+
source={{ uri: image }}
|
|
148
|
+
loading={false}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
</LinearGradient>
|
|
152
|
+
</View>
|
|
155
153
|
</Animated.View>
|
|
156
154
|
);
|
|
157
155
|
};
|
|
@@ -168,7 +166,7 @@ const styles = StyleSheet.create({
|
|
|
168
166
|
right: 0,
|
|
169
167
|
overflow: 'hidden',
|
|
170
168
|
},
|
|
171
|
-
//
|
|
169
|
+
// default header gradient keeps the off-Duo 375:154 shape, the header clips it
|
|
172
170
|
gradientFill: {
|
|
173
171
|
width: '100%',
|
|
174
172
|
position: 'absolute',
|
|
@@ -5,9 +5,7 @@ import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
|
5
5
|
import { PopupNotify } from '../../Popup';
|
|
6
6
|
import { NavigationButton } from './NavigationButton';
|
|
7
7
|
import { SideBarButton } from './SideBarButton';
|
|
8
|
-
import Animated from 'react-native-reanimated';
|
|
9
8
|
import { useSideBarItemStyle } from '../../Layout/utils';
|
|
10
|
-
import { useSideBarCounterStyle } from './DuoQuickReturnHeader';
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
11
|
* default header left used for nav
|
|
@@ -22,7 +20,6 @@ const HeaderLeft: React.FC<HeaderBackProps> = ({
|
|
|
22
20
|
const context = useContext<any>(MiniAppContext);
|
|
23
21
|
const { navigator } = useContext(ApplicationContext);
|
|
24
22
|
const sideBarStyle = useSideBarItemStyle('left', 0);
|
|
25
|
-
const counterStyle = useSideBarCounterStyle();
|
|
26
23
|
|
|
27
24
|
const goBack = useCallback(() => {
|
|
28
25
|
const canGoBack = navigator?.ref?.current?.canGoBack?.();
|
|
@@ -81,17 +78,14 @@ const HeaderLeft: React.FC<HeaderBackProps> = ({
|
|
|
81
78
|
|
|
82
79
|
if (floating && sideBarStyle) {
|
|
83
80
|
return (
|
|
84
|
-
<
|
|
85
|
-
style={[sideBarStyle, counterStyle]}
|
|
86
|
-
pointerEvents={'box-none'}
|
|
87
|
-
>
|
|
81
|
+
<View style={sideBarStyle} pointerEvents={'box-none'}>
|
|
88
82
|
<SideBarButton
|
|
89
83
|
accessibilityLabel="btn_navigation_back"
|
|
90
84
|
icon="arrow-back"
|
|
91
85
|
tintColor={tintColor}
|
|
92
86
|
onPress={goBackSafe}
|
|
93
87
|
/>
|
|
94
|
-
</
|
|
88
|
+
</View>
|
|
95
89
|
);
|
|
96
90
|
}
|
|
97
91
|
|
|
@@ -13,12 +13,8 @@ import { PopupNotify } from '../../Popup';
|
|
|
13
13
|
import { Tool, ToolGroup } from '../types';
|
|
14
14
|
import { Icon } from '../../Icon';
|
|
15
15
|
import { Text } from '../../Text';
|
|
16
|
-
import Animated from 'react-native-reanimated';
|
|
17
16
|
import { useSideBarItemStyle } from '../../Layout/utils';
|
|
18
|
-
import { useSideBarCounterStyle } from './DuoQuickReturnHeader';
|
|
19
17
|
import { SideBarButton, SideBarGroup, SideBarGroupItem } from './SideBarButton';
|
|
20
|
-
import { useDuoBarCompression } from '../BottomTab/DuoBarCompression';
|
|
21
|
-
import { DuoMetrics } from '../../Layout/utils';
|
|
22
18
|
|
|
23
19
|
const DID_SYNC_NEW_HOME = 'did_sync_new_home';
|
|
24
20
|
|
|
@@ -30,7 +26,6 @@ const HeaderRight: React.FC<any> = props => {
|
|
|
30
26
|
const { headerRight = {}, floating = false } = props;
|
|
31
27
|
const { useOnBoarding = false, buttonOnBoarding, onPress } = headerRight;
|
|
32
28
|
const sideBarStyle = useSideBarItemStyle('right', 1);
|
|
33
|
-
const counterStyle = useSideBarCounterStyle();
|
|
34
29
|
|
|
35
30
|
if (typeof props.headerRight === 'function') {
|
|
36
31
|
return props.headerRight(props);
|
|
@@ -48,16 +43,13 @@ const HeaderRight: React.FC<any> = props => {
|
|
|
48
43
|
|
|
49
44
|
if (floating && sideBarStyle) {
|
|
50
45
|
return (
|
|
51
|
-
<
|
|
52
|
-
style={[sideBarStyle, counterStyle]}
|
|
53
|
-
pointerEvents={'box-none'}
|
|
54
|
-
>
|
|
46
|
+
<View style={sideBarStyle} pointerEvents={'box-none'}>
|
|
55
47
|
<HeaderToolkitAction
|
|
56
48
|
{...props.headerRight}
|
|
57
49
|
tintColor={props.tintColor}
|
|
58
50
|
vertical
|
|
59
51
|
/>
|
|
60
|
-
</
|
|
52
|
+
</View>
|
|
61
53
|
);
|
|
62
54
|
}
|
|
63
55
|
|
|
@@ -83,8 +75,6 @@ const HeaderToolkitAction: React.FC<any> = ({
|
|
|
83
75
|
}) => {
|
|
84
76
|
const { navigator } = useContext(ApplicationContext);
|
|
85
77
|
const context = useContext<any>(MiniAppContext);
|
|
86
|
-
const { toolbarCompressed, toolbarHidden, expandToolbar, setToolbarHeight } =
|
|
87
|
-
useDuoBarCompression();
|
|
88
78
|
|
|
89
79
|
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
|
90
80
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -291,41 +281,6 @@ const HeaderToolkitAction: React.FC<any> = ({
|
|
|
291
281
|
? '16_navigation_close_circle'
|
|
292
282
|
: '16_basic_home';
|
|
293
283
|
|
|
294
|
-
const groupCount = useHelpCenter ? 2 : 1;
|
|
295
|
-
// Back slot, the optional shortcut, then the help/home capsule
|
|
296
|
-
const fullToolbarHeight =
|
|
297
|
-
DuoMetrics.controlSize +
|
|
298
|
-
Spacing.M +
|
|
299
|
-
(useShortcut ? DuoMetrics.controlSize + Spacing.M : 0) +
|
|
300
|
-
(groupCount > 1
|
|
301
|
-
? groupCount * DuoMetrics.itemPitch
|
|
302
|
-
: DuoMetrics.controlSize);
|
|
303
|
-
|
|
304
|
-
useEffect(() => {
|
|
305
|
-
if (vertical) {
|
|
306
|
-
setToolbarHeight?.(fullToolbarHeight);
|
|
307
|
-
}
|
|
308
|
-
}, [vertical, fullToolbarHeight, setToolbarHeight]);
|
|
309
|
-
|
|
310
|
-
// an open tab rail takes this slot for a moment
|
|
311
|
-
if (vertical && toolbarHidden) {
|
|
312
|
-
return null;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// HIG "Toolbar compressed": tools fold into one button; tapping it expands the
|
|
316
|
-
// toolbar and compresses the tab bar instead. Not the useMore icon, to tell them apart
|
|
317
|
-
if (vertical && toolbarCompressed) {
|
|
318
|
-
return (
|
|
319
|
-
<SideBarButton
|
|
320
|
-
accessibilityLabel={'btn_navigation_toolbar'}
|
|
321
|
-
icon={'navigation_menu'}
|
|
322
|
-
tintColor={tintColor}
|
|
323
|
-
showBadge={showBadge}
|
|
324
|
-
onPress={expandToolbar}
|
|
325
|
-
/>
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
284
|
if (vertical) {
|
|
330
285
|
return (
|
|
331
286
|
<View style={styles.sideBarColumn}>
|
|
@@ -31,7 +31,6 @@ import { HeaderTitle } from './Components/HeaderTitle';
|
|
|
31
31
|
import { HeaderBackground } from './Components/HeaderBackground';
|
|
32
32
|
import { type EdgeInsets } from 'react-native-safe-area-context';
|
|
33
33
|
import { useScreenLayout } from '../Layout';
|
|
34
|
-
import { ReservedRegionsProvider } from '../Layout/ReservedRegions';
|
|
35
34
|
|
|
36
35
|
const Stack = createStackNavigator();
|
|
37
36
|
|
|
@@ -77,19 +76,17 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
|
|
|
77
76
|
frame: { x: 0, y: 0, width: 0, height: 0 },
|
|
78
77
|
}}
|
|
79
78
|
>
|
|
80
|
-
<
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
</MiniAppContext.Provider>
|
|
92
|
-
</ReservedRegionsProvider>
|
|
79
|
+
<MiniAppContext.Provider value={mergedContext}>
|
|
80
|
+
<Navigation
|
|
81
|
+
screen={screen}
|
|
82
|
+
theme={theme}
|
|
83
|
+
options={options}
|
|
84
|
+
maxApi={maxApi}
|
|
85
|
+
setCurrentContext={setCurrentContext}
|
|
86
|
+
initialParams={initialParams}
|
|
87
|
+
localize={localize}
|
|
88
|
+
/>
|
|
89
|
+
</MiniAppContext.Provider>
|
|
93
90
|
</SafeAreaProvider>
|
|
94
91
|
);
|
|
95
92
|
};
|
package/Application/types.ts
CHANGED
|
@@ -333,11 +333,6 @@ export type BottomTabProps = {
|
|
|
333
333
|
blur?: (e: EventArg<'blur', false, unknown>) => void;
|
|
334
334
|
};
|
|
335
335
|
floatingButton?: FloatingButtonProps;
|
|
336
|
-
/**
|
|
337
|
-
* iPhone Duo: when the side bar can't hold both bars, which one starts compressed;
|
|
338
|
-
* tapping the compressed one swaps them. Defaults to 'toolbar'
|
|
339
|
-
*/
|
|
340
|
-
compressionBehavior?: 'toolbar' | 'tabBar';
|
|
341
336
|
};
|
|
342
337
|
|
|
343
338
|
export type FloatingButtonProps = {
|
package/Layout/Screen.tsx
CHANGED
|
@@ -9,7 +9,6 @@ import React, {
|
|
|
9
9
|
useContext,
|
|
10
10
|
useEffect,
|
|
11
11
|
useImperativeHandle,
|
|
12
|
-
useMemo,
|
|
13
12
|
useRef,
|
|
14
13
|
} from 'react';
|
|
15
14
|
import {
|
|
@@ -25,11 +24,7 @@ import {
|
|
|
25
24
|
View,
|
|
26
25
|
ViewProps,
|
|
27
26
|
} from 'react-native';
|
|
28
|
-
import {
|
|
29
|
-
useAnimatedReaction,
|
|
30
|
-
useSharedValue,
|
|
31
|
-
withTiming,
|
|
32
|
-
} from 'react-native-reanimated';
|
|
27
|
+
import { useSharedValue, withTiming } from 'react-native-reanimated';
|
|
33
28
|
import {
|
|
34
29
|
useSafeAreaFrame,
|
|
35
30
|
useSafeAreaInsets,
|
|
@@ -61,7 +56,6 @@ import {
|
|
|
61
56
|
DuoGlassBackground,
|
|
62
57
|
DuoScrollGradient,
|
|
63
58
|
} from '../Application/Components/DuoGlassBackground';
|
|
64
|
-
import { DuoQuickReturnHeader } from '../Application/Components/DuoQuickReturnHeader';
|
|
65
59
|
import { useDuoTabBarInset } from '../Application/BottomTab/DuoTabBar';
|
|
66
60
|
|
|
67
61
|
export interface ScreenProps extends ViewProps {
|
|
@@ -274,41 +268,6 @@ const Screen = forwardRef(
|
|
|
274
268
|
!animatedHeader &&
|
|
275
269
|
!inputSearchProps;
|
|
276
270
|
|
|
277
|
-
// iPhone Duo: the header leaves with the content and returns on a reverse scroll
|
|
278
|
-
const useQuickReturn =
|
|
279
|
-
hasSideBar && scrollable && (useDuoGlass || !!animatedHeader);
|
|
280
|
-
const headerHide = useSharedValue(0);
|
|
281
|
-
const lastScrollY = useSharedValue(0);
|
|
282
|
-
|
|
283
|
-
useAnimatedReaction(
|
|
284
|
-
() => sharedValue.value,
|
|
285
|
-
y => {
|
|
286
|
-
const delta = y - lastScrollY.value;
|
|
287
|
-
lastScrollY.value = y;
|
|
288
|
-
if (!useQuickReturn || y <= 0) {
|
|
289
|
-
headerHide.value = 0;
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
headerHide.value = Math.min(
|
|
293
|
-
Math.max(headerHide.value + delta, 0),
|
|
294
|
-
heightHeader,
|
|
295
|
-
);
|
|
296
|
-
},
|
|
297
|
-
[useQuickReturn, heightHeader],
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
const quickReturnOptions: StackNavigationOptions = useMemo(
|
|
301
|
-
() =>
|
|
302
|
-
useQuickReturn
|
|
303
|
-
? {
|
|
304
|
-
header: (props: any) => (
|
|
305
|
-
<DuoQuickReturnHeader {...props} offset={headerHide} />
|
|
306
|
-
),
|
|
307
|
-
}
|
|
308
|
-
: { header: undefined },
|
|
309
|
-
[useQuickReturn, headerHide],
|
|
310
|
-
);
|
|
311
|
-
|
|
312
271
|
let keyboardOffset = heightHeader - bottomInset;
|
|
313
272
|
if (
|
|
314
273
|
headerType === 'extended' ||
|
|
@@ -342,7 +301,6 @@ const Screen = forwardRef(
|
|
|
342
301
|
|
|
343
302
|
case 'extended':
|
|
344
303
|
options = {
|
|
345
|
-
...quickReturnOptions,
|
|
346
304
|
headerShown: true,
|
|
347
305
|
headerTransparent: true,
|
|
348
306
|
headerBackground: () =>
|
|
@@ -373,7 +331,6 @@ const Screen = forwardRef(
|
|
|
373
331
|
default:
|
|
374
332
|
if (hasSideBar && !inputSearchProps) {
|
|
375
333
|
options = {
|
|
376
|
-
...quickReturnOptions,
|
|
377
334
|
headerTransparent: true,
|
|
378
335
|
headerTintColor: Colors.black_17,
|
|
379
336
|
headerShown: true,
|
|
@@ -418,7 +375,7 @@ const Screen = forwardRef(
|
|
|
418
375
|
}
|
|
419
376
|
}
|
|
420
377
|
|
|
421
|
-
navigation?.instance?.setOptions(
|
|
378
|
+
navigation?.instance?.setOptions(options);
|
|
422
379
|
},
|
|
423
380
|
[
|
|
424
381
|
gradientColor,
|
|
@@ -428,7 +385,6 @@ const Screen = forwardRef(
|
|
|
428
385
|
sharedValue,
|
|
429
386
|
useShadowHeader,
|
|
430
387
|
hasSideBar,
|
|
431
|
-
quickReturnOptions,
|
|
432
388
|
],
|
|
433
389
|
);
|
|
434
390
|
|
|
@@ -444,7 +400,6 @@ const Screen = forwardRef(
|
|
|
444
400
|
}
|
|
445
401
|
|
|
446
402
|
options = {
|
|
447
|
-
...quickReturnOptions,
|
|
448
403
|
headerTintColor: currentTint.current ?? Colors.black_17,
|
|
449
404
|
headerTransparent: true,
|
|
450
405
|
headerBackground: (props: any) =>
|
|
@@ -487,7 +442,6 @@ const Screen = forwardRef(
|
|
|
487
442
|
hasSideBar,
|
|
488
443
|
layoutOffset,
|
|
489
444
|
heightHeader,
|
|
490
|
-
quickReturnOptions,
|
|
491
445
|
],
|
|
492
446
|
);
|
|
493
447
|
|
|
@@ -511,7 +465,7 @@ const Screen = forwardRef(
|
|
|
511
465
|
),
|
|
512
466
|
};
|
|
513
467
|
|
|
514
|
-
navigation?.instance?.setOptions(
|
|
468
|
+
navigation?.instance?.setOptions(options);
|
|
515
469
|
}, [navigation?.instance, sharedValue]);
|
|
516
470
|
|
|
517
471
|
/**
|
|
@@ -639,36 +593,8 @@ const Screen = forwardRef(
|
|
|
639
593
|
* handle scroll end
|
|
640
594
|
* @param e
|
|
641
595
|
*/
|
|
642
|
-
/**
|
|
643
|
-
* a half-shown quick return header settles fully in or out
|
|
644
|
-
*/
|
|
645
|
-
const snapQuickReturnHeader = (offsetY: number) => {
|
|
646
|
-
const hide = headerHide.value;
|
|
647
|
-
// above heightHeader the header still sits on the content
|
|
648
|
-
if (
|
|
649
|
-
!useQuickReturn ||
|
|
650
|
-
offsetY < heightHeader ||
|
|
651
|
-
hide <= 0 ||
|
|
652
|
-
hide >= heightHeader
|
|
653
|
-
) {
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
headerHide.value = withTiming(
|
|
657
|
-
hide > heightHeader / 2 ? heightHeader : 0,
|
|
658
|
-
{ duration: 200 },
|
|
659
|
-
);
|
|
660
|
-
};
|
|
661
|
-
|
|
662
|
-
const handleMomentumScrollEnd = (
|
|
663
|
-
e: NativeSyntheticEvent<NativeScrollEvent>,
|
|
664
|
-
) => {
|
|
665
|
-
snapQuickReturnHeader(e.nativeEvent.contentOffset.y);
|
|
666
|
-
scrollViewProps?.onMomentumScrollEnd?.(e);
|
|
667
|
-
};
|
|
668
|
-
|
|
669
596
|
const handleScrollEnd = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
670
597
|
const offsetY = e.nativeEvent.contentOffset.y;
|
|
671
|
-
snapQuickReturnHeader(offsetY);
|
|
672
598
|
if (inputSearchProps && offsetY < 100 && offsetY > 0) {
|
|
673
599
|
Animated.timing(animatedValue.current, {
|
|
674
600
|
toValue: 0,
|
|
@@ -796,7 +722,6 @@ const Screen = forwardRef(
|
|
|
796
722
|
headerBackground={headerBackground}
|
|
797
723
|
heightHeader={heightHeader}
|
|
798
724
|
extended={headerType === 'extended'}
|
|
799
|
-
followScroll={useQuickReturn}
|
|
800
725
|
/>
|
|
801
726
|
) : (
|
|
802
727
|
<HeaderExtendHeader
|
|
@@ -834,7 +759,6 @@ const Screen = forwardRef(
|
|
|
834
759
|
showsVerticalScrollIndicator={false}
|
|
835
760
|
onScroll={handleScroll}
|
|
836
761
|
onScrollEndDrag={handleScrollEnd}
|
|
837
|
-
onMomentumScrollEnd={handleMomentumScrollEnd}
|
|
838
762
|
scrollEventThrottle={16}
|
|
839
763
|
style={[
|
|
840
764
|
Styles.flex,
|
package/Layout/utils.ts
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
SafeAreaInsetsContext,
|
|
6
6
|
} from 'react-native-safe-area-context';
|
|
7
7
|
import { Spacing } from '../Consts';
|
|
8
|
-
import { ReservedRegion, ReservedRegionsContext } from './ReservedRegions';
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* validate children type
|
|
@@ -71,12 +70,6 @@ const HeaderMetrics = {
|
|
|
71
70
|
* - firstItemTop: with the status bar in the strip, the first control sits under the
|
|
72
71
|
* camera, clock and connectivity cluster (cluster ends near y 151 on the outer display,
|
|
73
72
|
* 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
73
|
*/
|
|
81
74
|
const DuoMetrics = {
|
|
82
75
|
maxAspectRatio: 1.6,
|
|
@@ -85,40 +78,10 @@ const DuoMetrics = {
|
|
|
85
78
|
itemPitch: 50,
|
|
86
79
|
firstItemTop: 170,
|
|
87
80
|
firstItemTopLandscape: 120,
|
|
88
|
-
outerHeaderHeight: 76,
|
|
89
|
-
outerCameraCenterY: 45,
|
|
90
|
-
edgeInset: 24,
|
|
91
81
|
};
|
|
92
82
|
|
|
93
83
|
const NO_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
94
84
|
|
|
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)
|
|
98
|
-
*/
|
|
99
|
-
const getStripOcclusion = (
|
|
100
|
-
regions: ReservedRegion[],
|
|
101
|
-
stripLeft: number,
|
|
102
|
-
stripRight: number,
|
|
103
|
-
windowHeight: number,
|
|
104
|
-
) =>
|
|
105
|
-
regions.reduce(
|
|
106
|
-
(occlusion, region) => {
|
|
107
|
-
const overlapsStrip =
|
|
108
|
-
region.x < stripRight && region.x + region.width > stripLeft;
|
|
109
|
-
if (!overlapsStrip) {
|
|
110
|
-
return occlusion;
|
|
111
|
-
}
|
|
112
|
-
if (region.y + region.height / 2 < windowHeight / 2) {
|
|
113
|
-
occlusion.top = Math.max(occlusion.top, region.y + region.height);
|
|
114
|
-
} else {
|
|
115
|
-
occlusion.bottom = Math.max(occlusion.bottom, windowHeight - region.y);
|
|
116
|
-
}
|
|
117
|
-
return occlusion;
|
|
118
|
-
},
|
|
119
|
-
{ top: 0, bottom: 0 },
|
|
120
|
-
);
|
|
121
|
-
|
|
122
85
|
const isIPhone = () =>
|
|
123
86
|
Platform.OS === 'ios' && Platform.constants.interfaceIdiom === 'phone';
|
|
124
87
|
|
|
@@ -157,11 +120,6 @@ export type SideBarLayout = {
|
|
|
157
120
|
* Window y of the first side bar item, below the navigation bar and the side status bar.
|
|
158
121
|
*/
|
|
159
122
|
top: number;
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Room to keep free above the window bottom, clear of a camera at the bottom end.
|
|
163
|
-
*/
|
|
164
|
-
bottom: number;
|
|
165
123
|
};
|
|
166
124
|
|
|
167
125
|
export type ScreenLayout = {
|
|
@@ -243,9 +201,8 @@ const isIPhoneDuoLayout = (
|
|
|
243
201
|
const useScreenLayout = (): ScreenLayout => {
|
|
244
202
|
const { width, height } = useWindowDimensions();
|
|
245
203
|
const insets = useContext(SafeAreaInsetsContext) ?? NO_INSETS;
|
|
246
|
-
const reservedRegions = useContext(ReservedRegionsContext);
|
|
247
204
|
|
|
248
|
-
|
|
205
|
+
const headerLayout = getHeaderLayout(insets.top);
|
|
249
206
|
const isIPhoneDuo = isIPhoneDuoLayout(width, height, insets);
|
|
250
207
|
if (!isIPhoneDuo) {
|
|
251
208
|
return {
|
|
@@ -267,42 +224,17 @@ const useScreenLayout = (): ScreenLayout => {
|
|
|
267
224
|
let top = insets.top + HeaderMetrics.barHeight + Spacing.M;
|
|
268
225
|
if (insets.top < HeaderMetrics.statusBarMinInset) {
|
|
269
226
|
// status bar lives in the strip: start under its cluster, as the native bar does
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
} else {
|
|
276
|
-
// outer display in landscape: no status bar in the strip
|
|
277
|
-
top = DuoMetrics.edgeInset;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (height > width && width < DuoMetrics.innerMinHeight) {
|
|
281
|
-
// outer display: the row starts below the top so the title centers on the camera
|
|
282
|
-
headerLayout = {
|
|
283
|
-
headerHeight: DuoMetrics.outerHeaderHeight,
|
|
284
|
-
headerStatusBarHeight:
|
|
285
|
-
DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
227
|
+
const isInnerLandscape =
|
|
228
|
+
width > height && height >= DuoMetrics.innerMinHeight;
|
|
229
|
+
top = isInnerLandscape
|
|
230
|
+
? DuoMetrics.firstItemTopLandscape
|
|
231
|
+
: DuoMetrics.firstItemTop;
|
|
288
232
|
}
|
|
289
233
|
|
|
290
|
-
const edge = insets.right >= insets.left ? 'right' : 'left';
|
|
291
|
-
const stripLeft = edge === 'right' ? width - sideInset : 0;
|
|
292
|
-
// keep controls clear of the camera and of the status bar cluster, which grows for
|
|
293
|
-
// calls and Live Activities. Region frames already include the system's margin
|
|
294
|
-
const occlusion = getStripOcclusion(
|
|
295
|
-
reservedRegions,
|
|
296
|
-
stripLeft,
|
|
297
|
-
stripLeft + sideInset,
|
|
298
|
-
height,
|
|
299
|
-
);
|
|
300
|
-
|
|
301
234
|
sideBar = {
|
|
302
|
-
edge,
|
|
235
|
+
edge: insets.right >= insets.left ? 'right' : 'left',
|
|
303
236
|
width: sideInset,
|
|
304
|
-
top
|
|
305
|
-
bottom: Math.max(DuoMetrics.edgeInset, occlusion.bottom),
|
|
237
|
+
top,
|
|
306
238
|
};
|
|
307
239
|
}
|
|
308
240
|
|
package/package.json
CHANGED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
createContext,
|
|
3
|
-
ReactNode,
|
|
4
|
-
useContext,
|
|
5
|
-
useMemo,
|
|
6
|
-
useState,
|
|
7
|
-
} from 'react';
|
|
8
|
-
import { Spacing } from '../../Consts';
|
|
9
|
-
import { useScreenLayout } from '../../Layout';
|
|
10
|
-
import { DuoMetrics } from '../../Layout/utils';
|
|
11
|
-
|
|
12
|
-
export type DuoBarCompressionBehavior = 'toolbar' | 'tabBar';
|
|
13
|
-
|
|
14
|
-
type DuoBarCompression = {
|
|
15
|
-
toolbarCompressed: boolean;
|
|
16
|
-
tabBarCompressed: boolean;
|
|
17
|
-
// the open rail needs the toolbar button slot too: only Back stays
|
|
18
|
-
toolbarHidden?: boolean;
|
|
19
|
-
// tapping the compressed bar expands it and compresses the other one
|
|
20
|
-
expandToolbar?: () => void;
|
|
21
|
-
expandTabBar?: () => void;
|
|
22
|
-
// a rail opened from the compressed state closes once a tab is picked
|
|
23
|
-
onTabSelected?: () => void;
|
|
24
|
-
// header buttons report their full height so the tab bar knows what is left
|
|
25
|
-
setToolbarHeight?: (height: number) => void;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const RAIL_PADDING = 6;
|
|
29
|
-
const RAIL_GAP = Spacing.XXS;
|
|
30
|
-
// space kept between the top and the bottom groups (HIG Vertical controls)
|
|
31
|
-
const GROUP_GAP = Spacing.M;
|
|
32
|
-
// Back, then the right toolkit: help + home capsule
|
|
33
|
-
const DEFAULT_TOOLBAR_HEIGHT =
|
|
34
|
-
DuoMetrics.controlSize + Spacing.M + 2 * DuoMetrics.itemPitch;
|
|
35
|
-
// Back, then the single button the toolbar folds into
|
|
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
|
-
|
|
40
|
-
const NO_COMPRESSION: DuoBarCompression = {
|
|
41
|
-
toolbarCompressed: false,
|
|
42
|
-
tabBarCompressed: false,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const DuoBarCompressionContext =
|
|
46
|
-
createContext<DuoBarCompression>(NO_COMPRESSION);
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* height of the vertical tab rail, plus the QR button under it
|
|
50
|
-
*/
|
|
51
|
-
const getRailHeight = (tabCount: number, hasQr: boolean) =>
|
|
52
|
-
2 * RAIL_PADDING +
|
|
53
|
-
tabCount * DuoMetrics.controlSize +
|
|
54
|
-
Math.max(tabCount - 1, 0) * RAIL_GAP +
|
|
55
|
-
(hasQr ? Spacing.M + DuoMetrics.controlSize : 0);
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* keeps the side bar's toolbar and tab bar clear of each other (HIG, Designing for
|
|
59
|
-
* iPhone Duo: Vertical controls):
|
|
60
|
-
* - both fit: both stay whole
|
|
61
|
-
* - one fits whole beside the other compressed: one is compressed, tapping it swaps them
|
|
62
|
-
* - otherwise both rest compressed; tapping one opens it for a moment (the rail closes
|
|
63
|
-
* again once a tab is picked)
|
|
64
|
-
*/
|
|
65
|
-
const DuoBarCompressionProvider: React.FC<{
|
|
66
|
-
behavior?: DuoBarCompressionBehavior;
|
|
67
|
-
tabCount: number;
|
|
68
|
-
hasQr: boolean;
|
|
69
|
-
children: ReactNode;
|
|
70
|
-
}> = ({ behavior = 'toolbar', tabCount, hasQr, children }) => {
|
|
71
|
-
const { height, sideBar } = useScreenLayout();
|
|
72
|
-
const [toolbarHeight, setToolbarHeight] = useState(DEFAULT_TOOLBAR_HEIGHT);
|
|
73
|
-
// 'toolbar' compression keeps the tab bar whole until the toolbar is tapped
|
|
74
|
-
const [expanded, setExpanded] = useState<'toolbar' | 'tabBar'>(
|
|
75
|
-
behavior === 'toolbar' ? 'tabBar' : 'toolbar',
|
|
76
|
-
);
|
|
77
|
-
// bar opened for a moment while both rest compressed
|
|
78
|
-
const [opened, setOpened] = useState<'toolbar' | 'tabBar'>();
|
|
79
|
-
|
|
80
|
-
const value = useMemo(() => {
|
|
81
|
-
if (!sideBar) {
|
|
82
|
-
return NO_COMPRESSION;
|
|
83
|
-
}
|
|
84
|
-
const room = height - sideBar.bottom - sideBar.top - GROUP_GAP;
|
|
85
|
-
const tabBar = getRailHeight(tabCount, hasQr);
|
|
86
|
-
const compressedTabBar = getRailHeight(1, hasQr) - 2 * RAIL_PADDING;
|
|
87
|
-
const fits = (toolbar: number, tabs: number) => toolbar + tabs <= room;
|
|
88
|
-
|
|
89
|
-
if (fits(toolbarHeight, tabBar)) {
|
|
90
|
-
return { ...NO_COMPRESSION, setToolbarHeight };
|
|
91
|
-
}
|
|
92
|
-
const canSwap =
|
|
93
|
-
fits(COMPRESSED_TOOLBAR_HEIGHT, tabBar) &&
|
|
94
|
-
fits(toolbarHeight, compressedTabBar);
|
|
95
|
-
if (canSwap) {
|
|
96
|
-
return {
|
|
97
|
-
toolbarCompressed: expanded === 'tabBar',
|
|
98
|
-
tabBarCompressed: expanded === 'toolbar',
|
|
99
|
-
expandToolbar: () => setExpanded('toolbar'),
|
|
100
|
-
expandTabBar: () => setExpanded('tabBar'),
|
|
101
|
-
setToolbarHeight,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const open = {
|
|
106
|
-
expandToolbar: () => setOpened('toolbar'),
|
|
107
|
-
expandTabBar: () => setOpened('tabBar'),
|
|
108
|
-
setToolbarHeight,
|
|
109
|
-
};
|
|
110
|
-
if (opened === 'tabBar' && fits(BACK_ONLY_HEIGHT, tabBar)) {
|
|
111
|
-
return {
|
|
112
|
-
...open,
|
|
113
|
-
toolbarCompressed: true,
|
|
114
|
-
toolbarHidden: true,
|
|
115
|
-
tabBarCompressed: false,
|
|
116
|
-
onTabSelected: () => setOpened(undefined),
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
return {
|
|
120
|
-
...open,
|
|
121
|
-
toolbarCompressed: opened !== 'toolbar',
|
|
122
|
-
tabBarCompressed: true,
|
|
123
|
-
};
|
|
124
|
-
}, [expanded, opened, hasQr, height, sideBar, tabCount, toolbarHeight]);
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<DuoBarCompressionContext.Provider value={value}>
|
|
128
|
-
{children}
|
|
129
|
-
</DuoBarCompressionContext.Provider>
|
|
130
|
-
);
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const useDuoBarCompression = () => useContext(DuoBarCompressionContext);
|
|
134
|
-
|
|
135
|
-
export { DuoBarCompressionProvider, useDuoBarCompression };
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext } from 'react';
|
|
2
|
-
import { StackHeaderProps, Header } from '@react-navigation/stack';
|
|
3
|
-
import Animated, {
|
|
4
|
-
SharedValue,
|
|
5
|
-
useAnimatedStyle,
|
|
6
|
-
} from 'react-native-reanimated';
|
|
7
|
-
|
|
8
|
-
// how far the header is pushed above the top edge
|
|
9
|
-
const DuoHeaderOffsetContext = createContext<SharedValue<number> | undefined>(
|
|
10
|
-
undefined,
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* iPhone Duo header that leaves with the content and comes back when scrolling back
|
|
15
|
-
*/
|
|
16
|
-
const DuoQuickReturnHeader: React.FC<
|
|
17
|
-
StackHeaderProps & { offset: SharedValue<number> }
|
|
18
|
-
> = ({ offset, ...props }) => {
|
|
19
|
-
const style = useAnimatedStyle(() => ({
|
|
20
|
-
transform: [{ translateY: -offset.value }],
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<DuoHeaderOffsetContext.Provider value={offset}>
|
|
25
|
-
<Animated.View pointerEvents={'box-none'} style={style}>
|
|
26
|
-
<Header {...props} />
|
|
27
|
-
</Animated.View>
|
|
28
|
-
</DuoHeaderOffsetContext.Provider>
|
|
29
|
-
);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* side bar controls render inside the header; this cancels its offset so they stay put
|
|
34
|
-
*/
|
|
35
|
-
const useSideBarCounterStyle = () => {
|
|
36
|
-
const offset = useContext(DuoHeaderOffsetContext);
|
|
37
|
-
return useAnimatedStyle(() => ({
|
|
38
|
-
transform: [{ translateY: offset ? offset.value : 0 }],
|
|
39
|
-
}));
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export { DuoQuickReturnHeader, useSideBarCounterStyle };
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import React, { createContext, ReactNode, useState } from 'react';
|
|
2
|
-
import { Platform, StyleSheet } from 'react-native';
|
|
3
|
-
|
|
4
|
-
export type ReservedRegion = {
|
|
5
|
-
x: number;
|
|
6
|
-
y: number;
|
|
7
|
-
width: number;
|
|
8
|
-
height: number;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
// native probe is optional so apps without @momo-kits/reserved-regions still bundle
|
|
12
|
-
let ReservedRegionsView: React.ComponentType<any> | undefined;
|
|
13
|
-
try {
|
|
14
|
-
ReservedRegionsView =
|
|
15
|
-
require('@momo-kits/reserved-regions').ReservedRegionsView;
|
|
16
|
-
} catch {
|
|
17
|
-
ReservedRegionsView = undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* screen regions the system currently occludes, in window coordinates
|
|
22
|
-
*/
|
|
23
|
-
const ReservedRegionsContext = createContext<ReservedRegion[]>([]);
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* tracks the system occlusion regions (iOS 27.1+), e.g. the iPhone Duo status bar
|
|
27
|
-
* cluster growing for a call or a Live Activity
|
|
28
|
-
*/
|
|
29
|
-
const ReservedRegionsProvider: React.FC<{ children: ReactNode }> = ({
|
|
30
|
-
children,
|
|
31
|
-
}) => {
|
|
32
|
-
const [regions, setRegions] = useState<ReservedRegion[]>([]);
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<ReservedRegionsContext.Provider value={regions}>
|
|
36
|
-
{children}
|
|
37
|
-
{Platform.OS === 'ios' && !!ReservedRegionsView && (
|
|
38
|
-
<ReservedRegionsView
|
|
39
|
-
style={StyleSheet.absoluteFill}
|
|
40
|
-
onRegionsChange={setRegions}
|
|
41
|
-
/>
|
|
42
|
-
)}
|
|
43
|
-
</ReservedRegionsContext.Provider>
|
|
44
|
-
);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export { ReservedRegionsContext, ReservedRegionsProvider };
|