Skip to content

Commit ff88a69

Browse files
committed
feat: scaffold hub page discovery tabs A/B test (coreMCU589)
- Add HomepageDiscoveryTabs component with Portfolio, Perpetuals, and Predictions tabs - Wire A/B test gate in Wallet — treatment renders discovery tabs, control renders existing homepage - Lazy-mount Perps and Predictions inline as tab content with hideHeader prop to suppress duplicate headers - Extend TabsList/TabsBar/Tab to support per-tab icons using local icon system - Register portfolio and predict custom SVG icons - Add per-tab gradient overlay that bleeds into wallet header with color per tab - Fix Perps safe area top inset when rendered as embedded tab
1 parent 469a6ad commit ff88a69

16 files changed

Lines changed: 546 additions & 141 deletions

File tree

app/component-library/components-temp/Tabs/Tab/Tab.tsx

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@ import {
88
Text,
99
TextVariant,
1010
FontWeight,
11+
Box,
12+
BoxFlexDirection,
13+
BoxAlignItems,
14+
BoxJustifyContent,
1115
} from '@metamask/design-system-react-native';
16+
import Icon, { IconSize, IconColor } from '../../../components/Icons/Icon';
1217

1318
// Internal dependencies.
1419
import { TabProps } from './Tab.types';
1520

1621
const Tab: React.FC<TabProps> = ({
1722
label,
23+
iconName,
1824
isActive,
1925
isDisabled = false,
2026
onPress,
@@ -42,42 +48,83 @@ const Tab: React.FC<TabProps> = ({
4248
>
4349
<Pressable
4450
style={tw.style(
45-
'px-0 py-1 flex-row items-center justify-center relative',
51+
'px-0 py-1 flex-col items-center justify-center relative',
4652
isDisabled && 'opacity-50',
4753
)}
4854
onPress={isDisabled ? undefined : onPress}
4955
disabled={isDisabled}
5056
testID={testID}
5157
{...pressableProps}
5258
>
53-
{/* Hidden bold text that determines layout size */}
54-
<Text
55-
variant={TextVariant.BodyMd}
56-
fontWeight={FontWeight.Bold}
57-
numberOfLines={1}
58-
style={tw.style('opacity-0')}
59-
>
60-
{label}
61-
</Text>
62-
63-
{/* Visible text positioned absolutely over the hidden text */}
64-
<Text
65-
variant={TextVariant.BodyMd}
66-
fontWeight={
67-
isActive && !isDisabled ? FontWeight.Bold : FontWeight.Regular
68-
}
69-
twClassName={
70-
isDisabled
71-
? 'text-muted'
72-
: isActive
73-
? 'text-default'
74-
: 'text-alternative'
75-
}
76-
numberOfLines={1}
77-
style={tw.style('absolute inset-0 flex items-center justify-center')}
78-
>
79-
{label}
80-
</Text>
59+
{iconName ? (
60+
// Icon mode: simple column stack, no layout-shift trick needed
61+
<Box
62+
twClassName="mb-1"
63+
flexDirection={BoxFlexDirection.Column}
64+
alignItems={BoxAlignItems.Center}
65+
justifyContent={BoxJustifyContent.Center}
66+
>
67+
<Icon
68+
name={iconName}
69+
size={IconSize.Lg}
70+
color={
71+
isDisabled
72+
? IconColor.Muted
73+
: isActive
74+
? IconColor.Default
75+
: IconColor.Alternative
76+
}
77+
style={tw.style('mb-1')}
78+
/>
79+
<Text
80+
variant={TextVariant.BodySm}
81+
fontWeight={
82+
isActive && !isDisabled ? FontWeight.Bold : FontWeight.Regular
83+
}
84+
twClassName={
85+
isDisabled
86+
? 'text-muted'
87+
: isActive
88+
? 'text-default'
89+
: 'text-alternative'
90+
}
91+
numberOfLines={1}
92+
>
93+
{label}
94+
</Text>
95+
</Box>
96+
) : (
97+
// No icon: use hidden/visible text trick to prevent layout shift on bold toggle
98+
<>
99+
<Text
100+
variant={TextVariant.BodyMd}
101+
fontWeight={FontWeight.Bold}
102+
numberOfLines={1}
103+
style={tw.style('opacity-0')}
104+
>
105+
{label}
106+
</Text>
107+
<Text
108+
variant={TextVariant.BodyMd}
109+
fontWeight={
110+
isActive && !isDisabled ? FontWeight.Bold : FontWeight.Regular
111+
}
112+
twClassName={
113+
isDisabled
114+
? 'text-muted'
115+
: isActive
116+
? 'text-default'
117+
: 'text-alternative'
118+
}
119+
numberOfLines={1}
120+
style={tw.style(
121+
'absolute inset-0 flex items-center justify-center',
122+
)}
123+
>
124+
{label}
125+
</Text>
126+
</>
127+
)}
81128
</Pressable>
82129
</View>
83130
);

app/component-library/components-temp/Tabs/Tab/Tab.types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Third party dependencies.
22
import { PressableProps, LayoutChangeEvent } from 'react-native';
33

4+
// Internal dependencies.
5+
import { IconName } from 'app/component-library/components/Icons/Icon/Icon.types';
6+
47
/**
58
* Tab component props
69
*/
@@ -9,6 +12,10 @@ export interface TabProps extends PressableProps {
912
* The label text for the tab
1013
*/
1114
label: string;
15+
/**
16+
* Optional icon rendered above the label.
17+
*/
18+
iconName?: IconName;
1219
/**
1320
* Whether the tab is currently active
1421
*/

app/component-library/components-temp/Tabs/TabsBar/TabsBar.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ const TabsBar: React.FC<TabsBarProps> = ({
281281

282282
return (
283283
<Box
284-
twClassName={`relative overflow-hidden px-4 ${twClassName || ''}`}
284+
twClassName={`relative overflow-hidden px-4 border-b border-border-default ${twClassName || ''}`}
285285
testID={testID}
286286
onLayout={handleContainerLayout as (layoutEvent: unknown) => void}
287287
{...boxProps}
@@ -304,21 +304,26 @@ const TabsBar: React.FC<TabsBarProps> = ({
304304
<Tab
305305
key={tab.key}
306306
label={tab.label}
307+
iconName={tab.iconName}
307308
isActive={index === activeIndex}
308309
isDisabled={tab.isDisabled}
309310
onPress={() => handleTabPress(index)}
310311
onLayout={(layoutEvent) => handleTabLayout(index, layoutEvent)}
311312
testID={tab.testID ?? `${testID}-tab-${index}`}
313+
style={tw.style('py-2')}
312314
/>
313315
))}
314316

315317
{/* Animated underline for scrollable tabs */}
316318
{activeIndex >= 0 && isInitialized && (
317319
<Animated.View
318-
style={tw.style('absolute bottom-0 h-0.5 bg-icon-default', {
319-
width: underlineWidthAnimated,
320-
transform: [{ translateX: underlineAnimated }],
321-
})}
320+
style={tw.style(
321+
'absolute -bottom-px h-0.5 bg-icon-default z-1',
322+
{
323+
width: underlineWidthAnimated,
324+
transform: [{ translateX: underlineAnimated }],
325+
},
326+
)}
322327
/>
323328
)}
324329
</Box>
@@ -333,6 +338,7 @@ const TabsBar: React.FC<TabsBarProps> = ({
333338
<Tab
334339
key={tab.key}
335340
label={tab.label}
341+
iconName={tab.iconName}
336342
isActive={index === activeIndex}
337343
isDisabled={tab.isDisabled}
338344
onPress={() => handleTabPress(index)}
@@ -344,7 +350,7 @@ const TabsBar: React.FC<TabsBarProps> = ({
344350
{/* Animated underline for non-scrollable tabs */}
345351
{activeIndex >= 0 && isInitialized && (
346352
<Animated.View
347-
style={tw.style('absolute bottom-0 h-0.5 bg-icon-default', {
353+
style={tw.style('absolute -bottom-px h-0.5 bg-icon-default', {
348354
width: underlineWidthAnimated,
349355
transform: [{ translateX: underlineAnimated }],
350356
})}

app/component-library/components-temp/Tabs/TabsBar/TabsBar.types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import React from 'react';
44
// External dependencies.
55
import { BoxProps } from '@metamask/design-system-react-native';
66

7+
// Internal dependencies.
8+
import { IconName } from 'app/component-library/components/Icons/Icon/Icon.types';
9+
710
/**
811
* Individual tab item data interface
912
*/
@@ -13,6 +16,10 @@ export interface TabItem {
1316
content: React.ReactNode;
1417
isDisabled?: boolean;
1518
testID?: string;
19+
/**
20+
* Optional icon rendered above the tab label.
21+
*/
22+
iconName?: IconName;
1623
}
1724

1825
/**

app/component-library/components-temp/Tabs/TabsList/TabsList.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { runOnJS } from 'react-native-reanimated';
1414
import { InteractionManager } from 'react-native';
1515

1616
import TabsBar from '../TabsBar';
17+
import type { IconName } from '../../../components/Icons/Icon/Icon.types';
1718
import { TabsListProps, TabsListRef, TabItem } from './TabsList.types';
1819

1920
const TAB_LOAD_FALLBACK_TIMEOUT_MS = 250;
@@ -38,6 +39,7 @@ const TabsList = forwardRef<TabsListRef, TabsListProps>(
3839
.map((child, index) => {
3940
const props = (child as React.ReactElement).props as {
4041
tabLabel?: string;
42+
tabIcon?: IconName;
4143
isDisabled?: boolean;
4244
testID?: string;
4345
};
@@ -47,6 +49,7 @@ const TabsList = forwardRef<TabsListRef, TabsListProps>(
4749
key:
4850
(child as React.ReactElement).key?.toString() || `tab-${index}`,
4951
label: tabLabel,
52+
iconName: props.tabIcon,
5053
content: child,
5154
isDisabled,
5255
isLoaded: false,

app/component-library/components-temp/Tabs/TabsList/TabsList.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { BoxProps } from '@metamask/design-system-react-native';
66

77
// Internal dependencies.
88
import { TabsBarProps } from '../TabsBar/TabsBar.types';
9+
import { IconName } from '../../../components/Icons/Icon/Icon.types';
910

1011
/**
1112
* Individual tab item data interface
@@ -16,6 +17,7 @@ export interface TabItem {
1617
content: React.ReactNode;
1718
isDisabled?: boolean;
1819
testID?: string;
20+
iconName?: IconName;
1921
}
2022

2123
/**

app/component-library/components/Icons/Icon/Icon.assets.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ import peopleSVG from './assets/people.svg';
177177
import personcancelSVG from './assets/person-cancel.svg';
178178
import pinSVG from './assets/pin.svg';
179179
import plantSVG from './assets/plant.svg';
180+
import portfolioSVG from './assets/portfolio.svg';
181+
import predictSVG from './assets/predict.svg';
180182
import plugSVG from './assets/plug.svg';
181183
import plusandminusSVG from './assets/plus-and-minus.svg';
182184
import policyalertSVG from './assets/policy-alert.svg';
@@ -461,6 +463,8 @@ export const assetByIconName: AssetByIconName = {
461463
[IconName.PersonCancel]: personcancelSVG,
462464
[IconName.Pin]: pinSVG,
463465
[IconName.Plant]: plantSVG,
466+
[IconName.Portfolio]: portfolioSVG,
467+
[IconName.Predict]: predictSVG,
464468
[IconName.Plug]: plugSVG,
465469
[IconName.PlusAndMinus]: plusandminusSVG,
466470
[IconName.PolicyAlert]: policyalertSVG,

app/component-library/components/Icons/Icon/Icon.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ export enum IconName {
247247
PersonCancel = 'PersonCancel',
248248
Pin = 'Pin',
249249
Plant = 'Plant',
250+
Portfolio = 'Portfolio',
251+
Predict = 'Predict',
250252
Plug = 'Plug',
251253
PlusAndMinus = 'PlusAndMinus',
252254
PolicyAlert = 'PolicyAlert',
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)