diff --git a/.env.example b/.env.example
index d243f8d1e1..ca2813b50f 100644
--- a/.env.example
+++ b/.env.example
@@ -8,6 +8,8 @@ NEXT_PUBLIC_GOVERNANCE_CACHE_URL=https://governance-cache-api.aave.com/graphql
# Client on/off gate for gasless voting. The relay only works if VOTE_RELAY_URL and VOTE_RELAY_API_KEY are also set server-side.
NEXT_PUBLIC_ENABLE_GASLESS_VOTING=false
NEXT_PUBLIC_ENABLE_STAKING=true
+# Force-build the /dev/components showcase. Automatic on `next dev` and Vercel previews.
+NEXT_PUBLIC_ENABLE_DEV_PAGES=false
NEXT_PUBLIC_API_BASEURL=https://aave-api-v2.aave.com
NEXT_PUBLIC_TRANSAK_APP_URL=https://global.transak.com
NEXT_PUBLIC_TRANSAK_API_URL=https://api.transak.com
diff --git a/custom.d.ts b/custom.d.ts
index 923ce4a53f..0324420b5b 100644
--- a/custom.d.ts
+++ b/custom.d.ts
@@ -6,6 +6,7 @@ namespace NodeJS {
interface ProcessEnv {
NEXT_PUBLIC_ENABLE_GOVERNANCE: string;
NEXT_PUBLIC_ENABLE_STAKING: string;
+ NEXT_PUBLIC_ENABLE_DEV_PAGES?: string;
NEXT_PUBLIC_ENV: string;
NEXT_PUBLIC_API_BASEURL: string;
NEXT_PUBLIC_FORK_BASE_CHAIN_ID?: string;
diff --git a/next.config.js b/next.config.js
index 5ad0b01abc..5ed951774f 100644
--- a/next.config.js
+++ b/next.config.js
@@ -8,6 +8,17 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
const pageExtensions = ['page.tsx', 'ts'];
if (process.env.NEXT_PUBLIC_ENABLE_GOVERNANCE === 'true') pageExtensions.push('governance.tsx');
if (process.env.NEXT_PUBLIC_ENABLE_STAKING === 'true') pageExtensions.push('staking.tsx');
+// Component showcase at `/dev/components`. Its pages are named `*.dev.tsx`, so unless that
+// extension is registered here Next never sees them: no route, no bundle, a real 404 rather than a
+// blank page. On for `next dev` and Vercel preview builds; off for the production IPFS build, which
+// sets neither. A `VERCEL_ENV` of `production` vetoes it outright, so a dashboard variable left
+// scoped to every environment by mistake still can't leak the showcase into production.
+const enableDevPages =
+ process.env.VERCEL_ENV !== 'production' &&
+ (process.env.NEXT_PUBLIC_ENABLE_DEV_PAGES === 'true' ||
+ process.env.VERCEL_ENV === 'preview' ||
+ process.env.NODE_ENV === 'development');
+if (enableDevPages) pageExtensions.push('dev.tsx');
/** @type {import('next').NextConfig} */
module.exports = withSentryConfig(
diff --git a/pages/404.page.tsx b/pages/404.page.tsx
index 83a4a6dbe4..c3eb552506 100644
--- a/pages/404.page.tsx
+++ b/pages/404.page.tsx
@@ -44,7 +44,7 @@ export default function Aave404Page() {
We suggest you go back to the home page.
-
+
+
);
};
diff --git a/src/components/transactions/Swap/inputs/shared/NetworkSelector.tsx b/src/components/transactions/Swap/inputs/shared/NetworkSelector.tsx
index 5ce1bb177e..968318f191 100644
--- a/src/components/transactions/Swap/inputs/shared/NetworkSelector.tsx
+++ b/src/components/transactions/Swap/inputs/shared/NetworkSelector.tsx
@@ -1,13 +1,6 @@
-import { ChevronDownIcon } from '@heroicons/react/outline';
-import {
- Box,
- FormControl,
- MenuItem,
- Select,
- SelectChangeEvent,
- SvgIcon,
- Typography,
-} from '@mui/material';
+import { Box, Button, Menu, MenuItem, Typography } from '@mui/material';
+import { useState } from 'react';
+import { ChevronDownIcon } from 'src/components/icons/ChevronDownIcon';
import { MarketLogo } from 'src/components/MarketSwitcher';
import { SupportedNetworkWithChainId } from '../../helpers/shared/misc.helpers';
@@ -23,51 +16,57 @@ export const NetworkSelector = ({
selectedNetwork,
setSelectedNetwork,
}: NetworkSelectorProps) => {
- const handleChange = (event: SelectChangeEvent) => {
- setSelectedNetwork(Number(event.target.value));
- };
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = Boolean(anchorEl);
+ const selected = networks.find((network) => network.chainId === selectedNetwork);
+
return (
-
-
-
+
+ >
);
};
diff --git a/src/components/transactions/Swap/inputs/shared/PriceInput.tsx b/src/components/transactions/Swap/inputs/shared/PriceInput.tsx
index 2548279bcd..71c0eddfb5 100644
--- a/src/components/transactions/Swap/inputs/shared/PriceInput.tsx
+++ b/src/components/transactions/Swap/inputs/shared/PriceInput.tsx
@@ -5,6 +5,7 @@ import React, { useEffect, useRef, useState } from 'react';
import NumberFormat, { NumberFormatProps } from 'react-number-format';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
import { ExternalTokenIcon } from 'src/components/primitives/TokenIcon';
+import { figSurfaceShadow } from 'src/utils/figmaColors';
import { SwappableToken, TokenType } from '../../types';
@@ -258,20 +259,21 @@ export const PriceInput = ({
return (
({
- border: `1px solid ${theme.palette.divider}`,
- borderRadius: '6px',
+ sx={{
+ borderRadius: '0.75rem',
+ boxShadow: figSurfaceShadow('shadow-stroke-1'),
overflow: 'hidden',
+ backgroundColor: 'bg-2',
px: 3,
py: 2,
width: '100%',
transition: 'background-color 0.15s ease',
'&:hover': {
- backgroundColor: 'background.surface',
+ backgroundColor: 'bg-2',
},
- })}
+ }}
>
-
+
When 1 {fromAsset.symbol} is worth:
@@ -330,8 +332,8 @@ export const PriceInput = ({
/>
{toAsset.symbol}
@@ -350,11 +352,11 @@ export const PriceInput = ({
width: 22,
height: 22,
borderRadius: '50%',
- backgroundColor: 'background.paper',
+ backgroundColor: 'surface-elevated',
ml: 1,
transition: 'background-color 0.2s ease',
'&:hover': {
- backgroundColor: 'background.surface',
+ backgroundColor: 'bg-2',
},
'&:hover .refresh-spin': {
transform: 'rotate(360deg)',
@@ -382,20 +384,20 @@ export const PriceInput = ({
value={rate.usd ? rate.usd.toString() : 0}
compact
symbol="USD"
- variant="secondary12"
- color="text.muted"
- symbolsColor="text.muted"
+ variant="subheader2"
+ color="fg-3"
+ symbolsColor="fg-3"
flexGrow={1}
/>
)}
-
+
diff --git a/src/components/transactions/Swap/inputs/shared/QuoteProgressRing.tsx b/src/components/transactions/Swap/inputs/shared/QuoteProgressRing.tsx
index cda959fde6..e9975ac497 100644
--- a/src/components/transactions/Swap/inputs/shared/QuoteProgressRing.tsx
+++ b/src/components/transactions/Swap/inputs/shared/QuoteProgressRing.tsx
@@ -1,5 +1,4 @@
import { Box, CircularProgress, SxProps } from '@mui/material';
-import { alpha, useTheme } from '@mui/material/styles';
import { useEffect, useMemo, useState } from 'react';
type QuoteProgressRingProps = {
@@ -21,7 +20,6 @@ export const QuoteProgressRing = ({
paused = false,
sx,
}: QuoteProgressRingProps) => {
- const theme = useTheme();
const [now, setNow] = useState(Date.now());
useEffect(() => {
@@ -41,8 +39,8 @@ export const QuoteProgressRing = ({
// Opacity from 0.25 to 1.0 based on progress
const ratio = Math.max(0, Math.min(1, progress / 100));
const opacity = 0.25 + 0.75 * ratio;
- return alpha(theme.palette.primary.main, opacity);
- }, [progress, theme]);
+ return `rgba(var(--mui-palette-primary-mainChannel) / ${opacity})`;
+ }, [progress]);
if (!active || !lastUpdatedAt || intervalMs <= 0) return null;
diff --git a/src/components/transactions/Swap/inputs/shared/SwitchRates.tsx b/src/components/transactions/Swap/inputs/shared/SwitchRates.tsx
index 9de8c9fe20..bc6e0698a2 100644
--- a/src/components/transactions/Swap/inputs/shared/SwitchRates.tsx
+++ b/src/components/transactions/Swap/inputs/shared/SwitchRates.tsx
@@ -52,8 +52,8 @@ export const SwitchRates = ({
visibleDecimals={0}
variant="main12"
symbol={isSwitched ? destSymbol : srcSymbol}
- symbolsVariant="secondary12"
- symbolsColor="text.secondary"
+ symbolsVariant="subheader2"
+ symbolsColor="fg-2"
value={'1'}
/>
diff --git a/src/components/transactions/Swap/inputs/shared/SwitchSlippageSelector.tsx b/src/components/transactions/Swap/inputs/shared/SwitchSlippageSelector.tsx
index 30799d5610..2a6b472004 100644
--- a/src/components/transactions/Swap/inputs/shared/SwitchSlippageSelector.tsx
+++ b/src/components/transactions/Swap/inputs/shared/SwitchSlippageSelector.tsx
@@ -1,19 +1,19 @@
import { CogIcon } from '@heroicons/react/solid';
import { Trans } from '@lingui/macro';
import {
+ Alert,
Box,
Button,
InputAdornment,
InputBase,
Menu,
SvgIcon,
- ToggleButton,
- ToggleButtonGroup,
Typography,
} from '@mui/material';
import { MouseEvent, useEffect, useState } from 'react';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
-import { Warning } from 'src/components/primitives/Warning';
+import { StyledTxModalToggleButton } from 'src/components/StyledToggleButton';
+import { StyledTxModalToggleGroup } from 'src/components/StyledToggleButtonGroup';
import { ValidationData } from '../../helpers/shared/slippage.helpers';
@@ -123,7 +123,7 @@ export const SwitchSlippageSelector = ({
return (
-
+
{isCustomSlippage ? (
Custom slippage
) : provider === 'paraswap' ? (
@@ -157,32 +157,18 @@ export const SwitchSlippageSelector = ({
Max slippage
- handlePresetSlippageChange(value)}
+ onChange={(_, value) => value && handlePresetSlippageChange(value)}
+ // Compact menu footprint, sized to the custom-slippage input beside it; the
+ // shell/pill treatment comes from the shared control.
+ sx={{ width: 'auto', height: '28px' }}
>
{slippageOptions.map((option) => (
-
+
{isNaN(Number(option)) ? (
-
+
{provider === 'paraswap' ? Default : Auto}
) : (
@@ -191,13 +177,11 @@ export const SwitchSlippageSelector = ({
visibleDecimals={2}
symbol="%"
variant="subheader2"
- color="primary.main"
- symbolsColor="primary.main"
/>
)}
-
+
))}
-
+
-
+
%
@@ -216,18 +200,16 @@ export const SwitchSlippageSelector = ({
width: '120px',
border: 1,
borderWidth: '1px',
- backgroundColor: 'background.surface',
- borderColor: slippageValidation
- ? `${slippageValidation.severity}.main`
- : 'background.surface',
+ backgroundColor: 'bg-2',
+ borderColor: slippageValidation ? `${slippageValidation.severity}.main` : 'bg-2',
borderRadius: '4px',
}}
/>
{slippageValidation && (
-
+
{slippageValidation.message}
-
+
)}
@@ -252,7 +234,7 @@ export const SwitchSlippageSelector = ({
>
{
>
) : (
-
+ Please connect your wallet to swap collateral. close()} />
diff --git a/src/components/transactions/Swap/modals/DebtSwapModal.tsx b/src/components/transactions/Swap/modals/DebtSwapModal.tsx
index c684129b4d..d0ce49785c 100644
--- a/src/components/transactions/Swap/modals/DebtSwapModal.tsx
+++ b/src/components/transactions/Swap/modals/DebtSwapModal.tsx
@@ -25,7 +25,7 @@ export const DebtSwapModal = () => {
>
) : (
-
+ Please connect your wallet to swap debt. close()} />
diff --git a/src/components/transactions/Swap/modals/SwapModal.tsx b/src/components/transactions/Swap/modals/SwapModal.tsx
index b17fe507f2..e51c353a72 100644
--- a/src/components/transactions/Swap/modals/SwapModal.tsx
+++ b/src/components/transactions/Swap/modals/SwapModal.tsx
@@ -23,7 +23,7 @@ export const SwapModal = () => {
>
) : (
-
+ Please connect your wallet to swap tokens. close()} />
diff --git a/src/components/transactions/Swap/modals/request/NoEligibleAssetsToSwap.tsx b/src/components/transactions/Swap/modals/request/NoEligibleAssetsToSwap.tsx
index aac778659d..3584cb99d1 100644
--- a/src/components/transactions/Swap/modals/request/NoEligibleAssetsToSwap.tsx
+++ b/src/components/transactions/Swap/modals/request/NoEligibleAssetsToSwap.tsx
@@ -3,7 +3,7 @@ import { Typography } from '@mui/material';
export const NoEligibleAssetsToSwap = () => {
return (
-
+ No eligible assets to swap.
);
diff --git a/src/components/transactions/Swap/modals/result/CowOrderToast.tsx b/src/components/transactions/Swap/modals/result/CowOrderToast.tsx
index 930f25f093..a3360bc757 100644
--- a/src/components/transactions/Swap/modals/result/CowOrderToast.tsx
+++ b/src/components/transactions/Swap/modals/result/CowOrderToast.tsx
@@ -1,18 +1,16 @@
-import { useTheme } from '@mui/material';
import { Toaster } from 'sonner';
+import { figVars } from 'src/utils/figmaColors';
export const CowOrderToast = () => {
- const theme = useTheme();
-
return (
diff --git a/src/components/transactions/Swap/modals/result/SwapResultView.tsx b/src/components/transactions/Swap/modals/result/SwapResultView.tsx
index 83d7a734ad..f82b3665f3 100644
--- a/src/components/transactions/Swap/modals/result/SwapResultView.tsx
+++ b/src/components/transactions/Swap/modals/result/SwapResultView.tsx
@@ -59,17 +59,17 @@ export const SwapWithSurplusTooltip = ({
<>
- Base:
+ Base:
- Surplus: {' '}
- (
+ Surplus:{' '}
+ (
)
@@ -260,7 +260,7 @@ export const SwapTxSuccessView = ({
size={20}
sx={{
mr: 1,
- color: (theme) => theme.palette.grey[400],
+ color: (theme) => theme.vars.palette.grey[400],
}}
/>
Details will be available soon
@@ -276,7 +276,7 @@ export const SwapTxSuccessView = ({
customExplorerLinkText={customExplorerLinkText}
>
-
+
{provider === 'cowprotocol' ? (
<>
{orderStatus === 'open' ? (
@@ -301,17 +301,17 @@ export const SwapTxSuccessView = ({
-
+
{provider == 'cowprotocol' &&
((orderStatus == 'open' && !isNativeToken(symbol)) || orderStatus == 'failed')
? `${resultScreenTokensFromTitle ?? 'Send'}`
@@ -327,7 +327,7 @@ export const SwapTxSuccessView = ({
/>
+
{inAmount} {symbol}
}
@@ -345,14 +345,14 @@ export const SwapTxSuccessView = ({
-
+
{symbol}
-
+
{provider == 'cowprotocol' && (orderStatus == 'open' || orderStatus == 'failed')
? `${resultScreenTokensToTitle ?? 'Receive'}`
: `${resultScreenTokensToTitle ?? 'Received'}`}
@@ -367,7 +367,7 @@ export const SwapTxSuccessView = ({
/>
+
{outFinalAmount} {outSymbol}
}
@@ -385,7 +385,7 @@ export const SwapTxSuccessView = ({
-
+
{outSymbol}
@@ -394,7 +394,7 @@ export const SwapTxSuccessView = ({
{surplusDisplay}
@@ -403,15 +403,15 @@ export const SwapTxSuccessView = ({
-
+
Swap saved in your{' '}
-
+ Market
@@ -35,7 +35,7 @@ export function OrderTypeSelector({
value={OrderType.LIMIT}
disabled={switchType === OrderType.LIMIT || limitsOrderButtonBlocked}
>
-
+ Limit
diff --git a/src/components/transactions/Swap/warnings/postInputs/CowAdapterApprovalInfo.tsx b/src/components/transactions/Swap/warnings/postInputs/CowAdapterApprovalInfo.tsx
index c86701fc11..606fb950b1 100644
--- a/src/components/transactions/Swap/warnings/postInputs/CowAdapterApprovalInfo.tsx
+++ b/src/components/transactions/Swap/warnings/postInputs/CowAdapterApprovalInfo.tsx
@@ -1,6 +1,5 @@
import { Trans } from '@lingui/macro';
-import { Typography } from '@mui/material';
-import { Warning } from 'src/components/primitives/Warning';
+import { Alert } from '@mui/material';
import { useModalContext } from 'src/hooks/useModal';
import { SwapState } from '../../types';
@@ -19,13 +18,11 @@ export function CowAdapterApprovalInfo({ state }: { state: SwapState }) {
if (!isCow || !isAdapterFlow || approvalTxState?.success || !isFlashloan) return null;
return (
-
-
-
- A temporary contract will be used to execute the trade. Your wallet may show a warning for
- approving a new or empty address.
-
-
-
+
+
+ A temporary contract will be used to execute the trade. Your wallet may show a warning for
+ approving a new or empty address.
+
+
);
}
diff --git a/src/components/transactions/Swap/warnings/postInputs/CustomTokenWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/CustomTokenWarning.tsx
index 65e83056f4..cfee99c692 100644
--- a/src/components/transactions/Swap/warnings/postInputs/CustomTokenWarning.tsx
+++ b/src/components/transactions/Swap/warnings/postInputs/CustomTokenWarning.tsx
@@ -1,5 +1,4 @@
-import { Typography } from '@mui/material';
-import { Warning } from 'src/components/primitives/Warning';
+import { Alert } from '@mui/material';
import { SwapState, TokenType } from '../../types';
@@ -14,10 +13,8 @@ export function CustomTokenWarning({ state }: { state: SwapState }) {
}
return (
-
-
- You selected a custom imported token. Make sure it's the right token.
-
-
+
+ You selected a custom imported token. Make sure it's the right token.
+
);
}
diff --git a/src/components/transactions/Swap/warnings/postInputs/GasEstimationWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/GasEstimationWarning.tsx
index 285d993a39..efe2bfed86 100644
--- a/src/components/transactions/Swap/warnings/postInputs/GasEstimationWarning.tsx
+++ b/src/components/transactions/Swap/warnings/postInputs/GasEstimationWarning.tsx
@@ -1,6 +1,5 @@
import { Trans } from '@lingui/macro';
-import { Typography } from '@mui/material';
-import { Warning } from 'src/components/primitives/Warning';
+import { Alert } from '@mui/material';
import { SwapState } from '../../types';
@@ -14,12 +13,10 @@ export function GasEstimationWarning({ state }: { state: SwapState }) {
if (!hasGasEstimationWarning) return null;
return (
-
-
-
- The swap could not be completed. Try increasing slippage or changing the amount.
-
-
-
+
+
+ The swap could not be completed. Try increasing slippage or changing the amount.
+
+
);
}
diff --git a/src/components/transactions/Swap/warnings/postInputs/HighCostsLimitOrderWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/HighCostsLimitOrderWarning.tsx
index 91a86d91ae..b00083336e 100644
--- a/src/components/transactions/Swap/warnings/postInputs/HighCostsLimitOrderWarning.tsx
+++ b/src/components/transactions/Swap/warnings/postInputs/HighCostsLimitOrderWarning.tsx
@@ -1,8 +1,7 @@
import { valueToBigNumber } from '@aave/math-utils';
import { Trans } from '@lingui/macro';
-import { Typography } from '@mui/material';
+import { Alert } from '@mui/material';
import { useEffect, useMemo } from 'react';
-import { Warning } from 'src/components/primitives/Warning';
import { ActionsBlockedReason, OrderType, SwapState } from '../../types';
@@ -92,13 +91,11 @@ export function HighCostsLimitOrderWarning({
return null;
return (
-
-
-
- Estimated costs are {costsPercentOfSell.toFixed(2)}% of the sell amount. This order is
- unlikely to be filled.
-
-
-
+
+
+ Estimated costs are {costsPercentOfSell.toFixed(2)}% of the sell amount. This order is
+ unlikely to be filled.
+
+
);
}
diff --git a/src/components/transactions/Swap/warnings/postInputs/HighPriceImpactWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/HighPriceImpactWarning.tsx
index 29a282c8a6..471c1c3b3e 100644
--- a/src/components/transactions/Swap/warnings/postInputs/HighPriceImpactWarning.tsx
+++ b/src/components/transactions/Swap/warnings/postInputs/HighPriceImpactWarning.tsx
@@ -1,7 +1,6 @@
import { Trans } from '@lingui/macro';
-import { Box, Checkbox, Typography } from '@mui/material';
+import { Alert, Box, Checkbox } from '@mui/material';
import { Dispatch, useEffect, useMemo, useState } from 'react';
-import { Warning } from 'src/components/primitives/Warning';
import { SwapInputChanges } from '../../analytics/constants';
import { useHandleAnalytics } from '../../analytics/useTrackAnalytics';
@@ -54,43 +53,32 @@ export function HighPriceImpactWarning({
if (actionsBlockedReasonsAmount(state) > 1) return null;
return (
- 0.3 ? 'error' : 'warning'}
- icon={false}
+ data-size="small"
sx={{
+ width: '100%',
mt: 2,
mb: 2,
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
}}
>
-
-
- High price impact ({(lostValue * 100).toFixed(1)}%)! This route will
- return {state.isInvertedSwap ? 'more' : 'less'} due to low liquidity or small order size.
-
-
-
-
- Please review the swap values before confirming.
-
-
+
+ High price impact ({(lostValue * 100).toFixed(1)}%)! This route will return{' '}
+ {state.isInvertedSwap ? 'more' : 'less'} due to low liquidity or small order size.
+ {' '}
+ Please review the swap values before confirming.
{requireConfirmation && (
-
-
- I confirm the swap knowing that I could lose up to{' '}
- {(lostValue * 100).toFixed(0)}% on this swap.
-
-
+
+ I confirm the swap knowing that I could lose up to{' '}
+ {(lostValue * 100).toFixed(0)}% on this swap.
+ {
@@ -102,10 +90,11 @@ export function HighPriceImpactWarning({
);
}}
size="small"
+ sx={{ p: 0, ml: 2 }}
data-cy={'high-price-impact-checkbox'}
/>
)}
-
+
);
}
diff --git a/src/components/transactions/Swap/warnings/postInputs/LimitOrderAmountWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/LimitOrderAmountWarning.tsx
index ebd524c6f1..9878dedcda 100644
--- a/src/components/transactions/Swap/warnings/postInputs/LimitOrderAmountWarning.tsx
+++ b/src/components/transactions/Swap/warnings/postInputs/LimitOrderAmountWarning.tsx
@@ -1,8 +1,7 @@
import { valueToBigNumber } from '@aave/math-utils';
import { Trans } from '@lingui/macro';
-import { Typography } from '@mui/material';
+import { Alert } from '@mui/material';
import { useMemo } from 'react';
-import { Warning } from 'src/components/primitives/Warning';
import { SwapState } from '../../types';
import { OrderType } from '../../types/shared.types';
@@ -66,24 +65,20 @@ export function LimitOrderAmountWarning({ state }: { state: SwapState }) {
if (!shouldShowWarning) return null;
return (
-
-
-
- Your order amounts are {isHigherDifference ? 'significantly ' : ''} less favorable by{' '}
- {differencePercentage?.abs()?.toFixed(1) ?? '0'}% to the liquidity provider than
- recommended. This order may not be executed.
-
-
-
+
+ Your order amounts are {isHigherDifference ? 'significantly ' : ''} less favorable by{' '}
+ {differencePercentage?.abs()?.toFixed(1) ?? '0'}% to the liquidity provider than
+ recommended. This order may not be executed.
+
+
);
}
diff --git a/src/components/transactions/Swap/warnings/postInputs/LiquidationCriticalWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/LiquidationCriticalWarning.tsx
index b4ea0ff153..ad02155f4e 100644
--- a/src/components/transactions/Swap/warnings/postInputs/LiquidationCriticalWarning.tsx
+++ b/src/components/transactions/Swap/warnings/postInputs/LiquidationCriticalWarning.tsx
@@ -1,7 +1,6 @@
import { Trans } from '@lingui/macro';
-import { Typography } from '@mui/material';
+import { Alert } from '@mui/material';
import { Dispatch } from 'react';
-import { Warning } from 'src/components/primitives/Warning';
import { SwapParams, SwapState } from '../../types';
@@ -14,23 +13,20 @@ export function LiquidationCriticalWarning({
}) {
// TODO: move to be an error not a warning and remove isLiquidatable from state.
return (
-
-
-
- Your health factor after this swap will be critically low and may result in liquidation.
- Please choose a different asset or reduce the swap amount to stay safe.
-
-
-
+
+ Your health factor after this swap will be critically low and may result in liquidation.
+ Please choose a different asset or reduce the swap amount to stay safe.
+
+
);
}
diff --git a/src/components/transactions/Swap/warnings/postInputs/LowHealthFactorWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/LowHealthFactorWarning.tsx
index 9e3d3f0044..962ff81835 100644
--- a/src/components/transactions/Swap/warnings/postInputs/LowHealthFactorWarning.tsx
+++ b/src/components/transactions/Swap/warnings/postInputs/LowHealthFactorWarning.tsx
@@ -1,7 +1,6 @@
import { Trans } from '@lingui/macro';
-import { Box, Checkbox, Typography } from '@mui/material';
+import { Alert, Box, Checkbox } from '@mui/material';
import { Dispatch, useEffect, useState } from 'react';
-import { Warning } from 'src/components/primitives/Warning';
import { ActionsBlockedReason, SwapParams, SwapState } from '../../types';
import { shouldRequireConfirmationHFlow } from '../helpers';
@@ -40,44 +39,30 @@ export function LowHealthFactorWarning({
}
return (
-
-
-
- Low health factor after swap. Your position will carry a higher risk of liquidation.
-
-
+
+
+ Low health factor after swap. Your position will carry a higher risk of liquidation.
+
{!state.actionsBlocked[ActionsBlockedReason.IS_LIQUIDATABLE] && (
-
- I understand the liquidation risk and want to proceed
-
+ I understand the liquidation risk and want to proceed {
setLowHFConfirmed(!lowHFConfirmed);
}}
size="small"
+ sx={{ p: 0, ml: 2 }}
data-cy={'low-hf-checkbox'}
/>
)}
-
+
);
}
diff --git a/src/components/transactions/Swap/warnings/postInputs/SafetyModuleSwapWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/SafetyModuleSwapWarning.tsx
index c2f655ca4c..8e83d73dc7 100644
--- a/src/components/transactions/Swap/warnings/postInputs/SafetyModuleSwapWarning.tsx
+++ b/src/components/transactions/Swap/warnings/postInputs/SafetyModuleSwapWarning.tsx
@@ -1,7 +1,6 @@
import { Trans } from '@lingui/macro';
-import { Typography } from '@mui/material';
+import { Alert } from '@mui/material';
import { Link } from 'src/components/primitives/Link';
-import { Warning } from 'src/components/primitives/Warning';
import { SwapState } from '../../types';
import { SAFETY_MODULE_TOKENS } from '../constants';
@@ -13,16 +12,14 @@ export function SafetyModuleSwapWarning({ state }: { state: SwapState }) {
if (!isSwappingSafetyModuleToken) return null;
return (
-
-
-
- For swapping safety module assets please unstake your position{' '}
- close()}>
- here
-
- .
-
-
-
+
+
+ For swapping safety module assets please unstake your position{' '}
+ close()}>
+ here
+
+ .
+
+
);
}
diff --git a/src/components/transactions/Swap/warnings/postInputs/ShieldSwapWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/ShieldSwapWarning.tsx
index 92c6d6f409..ef875b1969 100644
--- a/src/components/transactions/Swap/warnings/postInputs/ShieldSwapWarning.tsx
+++ b/src/components/transactions/Swap/warnings/postInputs/ShieldSwapWarning.tsx
@@ -1,8 +1,6 @@
-import { ShieldExclamationIcon } from '@heroicons/react/outline';
import { Trans } from '@lingui/macro';
-import { Box, SvgIcon, Typography } from '@mui/material';
+import { Alert, AlertTitle } from '@mui/material';
import { Dispatch, useEffect, useMemo } from 'react';
-import { Warning } from 'src/components/primitives/Warning';
import { useRootStore } from 'src/store/root';
import { ActionsBlockedReason, SwapState } from '../../types';
@@ -42,31 +40,14 @@ export function ShieldSwapWarning({
if (!shouldBlock) return null;
return (
-
-
-
-
-
-
- Aave Shield: Transaction blocked
-
-
-
-
- This swap has a price impact of {(lostValue * 100).toFixed(1)}%, which exceeds the 25%
- safety threshold. To proceed, disable Aave Shield in the settings menu.
-
-
-
+
+
+ Aave Shield: Transaction blocked
+
+
+ This swap has a price impact of {(lostValue * 100).toFixed(1)}%, which exceeds the 25%
+ safety threshold. To proceed, disable Aave Shield in the settings menu.
+
+
);
}
diff --git a/src/components/transactions/Swap/warnings/postInputs/SlippageWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/SlippageWarning.tsx
index 7131b65fc3..1e4fd33a92 100644
--- a/src/components/transactions/Swap/warnings/postInputs/SlippageWarning.tsx
+++ b/src/components/transactions/Swap/warnings/postInputs/SlippageWarning.tsx
@@ -1,5 +1,4 @@
-import { Typography } from '@mui/material';
-import { Warning } from 'src/components/primitives/Warning';
+import { Alert } from '@mui/material';
import { OrderType, SwapState } from '../../types';
@@ -8,10 +7,8 @@ export function SlippageWarning({ state }: { state: SwapState }) {
if (state.orderType === OrderType.LIMIT) return null;
return (
-
-
- Slippage is lower than recommended. The swap may be delayed or fail.
-
-
+
+ Slippage is lower than recommended. The swap may be delayed or fail.
+
);
}
diff --git a/src/components/transactions/Swap/warnings/postInputs/USDTResetWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/USDTResetWarning.tsx
index 1c8f421169..82f7648fd2 100644
--- a/src/components/transactions/Swap/warnings/postInputs/USDTResetWarning.tsx
+++ b/src/components/transactions/Swap/warnings/postInputs/USDTResetWarning.tsx
@@ -1,6 +1,5 @@
import { Trans } from '@lingui/macro';
-import { Typography } from '@mui/material';
-import { Warning } from 'src/components/primitives/Warning';
+import { Alert } from '@mui/material';
import { SwapState } from '../../types';
@@ -8,13 +7,11 @@ export function USDTResetWarning({ state }: { state: SwapState }) {
if (!state.requiresApprovalReset) return null;
return (
-
-
-
- USDT on Ethereum requires approval reset before a new approval. This will require an
- additional transaction.
-
-
-
+
+
+ USDT on Ethereum requires approval reset before a new approval. This will require an
+ additional transaction.
+
+
);
}
diff --git a/src/components/transactions/Swap/warnings/postInputs/ZeroLTVDestinationWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/ZeroLTVDestinationWarning.tsx
index 18ed8bcadf..2de4487fcf 100644
--- a/src/components/transactions/Swap/warnings/postInputs/ZeroLTVDestinationWarning.tsx
+++ b/src/components/transactions/Swap/warnings/postInputs/ZeroLTVDestinationWarning.tsx
@@ -1,6 +1,5 @@
import { Trans } from '@lingui/macro';
-import { Typography } from '@mui/material';
-import { Warning } from 'src/components/primitives/Warning';
+import { Alert } from '@mui/material';
import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider';
import { hasNonZeroEffectiveLtv } from 'src/utils/hfUtils';
@@ -37,13 +36,11 @@ export function ZeroLTVDestinationWarning({ state }: { state: SwapState }) {
}
return (
-
-
-
- {destinationReserve.symbol} has a Loan-to-Value of 0, so it will not be enabled as
- collateral automatically after the swap.
-
-
-
+
+
+ {destinationReserve.symbol} has a Loan-to-Value of 0, so it will not be enabled as
+ collateral automatically after the swap.
+
+
);
}
diff --git a/src/components/transactions/Swap/warnings/preInputs/CowOpenOrdersWarning.tsx b/src/components/transactions/Swap/warnings/preInputs/CowOpenOrdersWarning.tsx
index 21ba7cf07c..d5d40c31bf 100644
--- a/src/components/transactions/Swap/warnings/preInputs/CowOpenOrdersWarning.tsx
+++ b/src/components/transactions/Swap/warnings/preInputs/CowOpenOrdersWarning.tsx
@@ -1,8 +1,7 @@
import { normalize } from '@aave/math-utils';
import { OrderStatus } from '@cowprotocol/cow-sdk';
-import { Link, Typography } from '@mui/material';
+import { Alert, Link } from '@mui/material';
import { useEffect, useState } from 'react';
-import { Warning } from 'src/components/primitives/Warning';
import { useSwapOrdersTracking } from 'src/hooks/useSwapOrdersTracking';
import { useRootStore } from 'src/store/root';
import { findByChainId } from 'src/ui-config/marketsConfig';
@@ -63,20 +62,18 @@ export function CowOpenOrdersWarning({ state }: { state: SwapState }) {
if (!cowOpenOrdersTotalAmountFormatted && !hasActiveForToken) return null;
return (
-
-
- {cowOpenOrdersTotalAmountFormatted ? (
- <>
- You have open orders for {cowOpenOrdersTotalAmountFormatted} {state.sourceToken.symbol}.{' '}
- >
- ) : (
- <>You have in-progress swaps for {state.sourceToken.symbol}. >
- )}
- Track them in your{' '}
-
- transaction history
-
-
-
+
+ {cowOpenOrdersTotalAmountFormatted ? (
+ <>
+ You have open orders for {cowOpenOrdersTotalAmountFormatted} {state.sourceToken.symbol}.{' '}
+ >
+ ) : (
+ <>You have in-progress swaps for {state.sourceToken.symbol}. >
+ )}
+ Track them in your{' '}
+
+ transaction history
+
+
);
}
diff --git a/src/components/transactions/Swap/warnings/preInputs/NativeLimitOrderInfo.tsx b/src/components/transactions/Swap/warnings/preInputs/NativeLimitOrderInfo.tsx
index 3cdd52ac6d..0ea8e5d745 100644
--- a/src/components/transactions/Swap/warnings/preInputs/NativeLimitOrderInfo.tsx
+++ b/src/components/transactions/Swap/warnings/preInputs/NativeLimitOrderInfo.tsx
@@ -1,6 +1,5 @@
import { Trans } from '@lingui/macro';
-import { Typography } from '@mui/material';
-import { Warning } from 'src/components/primitives/Warning';
+import { Alert } from '@mui/material';
import { SwapParams, SwapProvider, SwapState, SwapType, TokenType } from '../../types';
@@ -13,13 +12,11 @@ export function NativeLimitOrderInfo({ state, params }: { state: SwapState; para
if (!isClassicSwap || !isNativeInput || !isCoWProtocol) return null;
return (
-
-
-
- For security reasons, limit orders are not supported for Native tokens. To place a limit
- order, use the wrapped version.
-
-
-
+
+
+ For security reasons, limit orders are not supported for Native tokens. To place a limit
+ order, use the wrapped version.
+
+
);
}
diff --git a/src/components/transactions/TxActionsWrapper.tsx b/src/components/transactions/TxActionsWrapper.tsx
index bc8dc3d2ba..a9a383fddf 100644
--- a/src/components/transactions/TxActionsWrapper.tsx
+++ b/src/components/transactions/TxActionsWrapper.tsx
@@ -183,7 +183,7 @@ export const TxActionsWrapper = ({
{content}
{readOnlyModeAddress && (
-
+ Read-only mode. Connect to a wallet to perform transactions.
)}
diff --git a/src/components/transactions/Warnings/AAVEWarning.tsx b/src/components/transactions/Warnings/AAVEWarning.tsx
index 727d945e88..6c4f6cd91b 100644
--- a/src/components/transactions/Warnings/AAVEWarning.tsx
+++ b/src/components/transactions/Warnings/AAVEWarning.tsx
@@ -1,20 +1,17 @@
import { Trans } from '@lingui/macro';
-import { Link, Typography } from '@mui/material';
+import { Alert, Link } from '@mui/material';
import { ROUTES } from '../../primitives/Link';
-import { Warning } from '../../primitives/Warning';
export const AAVEWarning = () => {
return (
-
-
- Supplying your AAVE{' '}
- tokens is not the same as staking them. If you wish to stake your AAVE{' '}
- tokens, please go to the {' '}
-
- staking view
-
-
-
+
+ Supplying your AAVE{' '}
+ tokens is not the same as staking them. If you wish to stake your AAVE{' '}
+ tokens, please go to the {' '}
+
+ staking view
+
+
);
};
diff --git a/src/components/transactions/Warnings/BorrowCapWarning.tsx b/src/components/transactions/Warnings/BorrowCapWarning.tsx
index 204c06b4b4..9d74b2a70b 100644
--- a/src/components/transactions/Warnings/BorrowCapWarning.tsx
+++ b/src/components/transactions/Warnings/BorrowCapWarning.tsx
@@ -1,16 +1,16 @@
import { Trans } from '@lingui/macro';
-import { AlertProps } from '@mui/material';
+import { Alert, AlertProps } from '@mui/material';
import { AssetCapData } from 'src/hooks/useAssetCaps';
import { Link } from '../../primitives/Link';
-import { Warning } from '../../primitives/Warning';
type BorrowCapWarningProps = AlertProps & {
borrowCap: AssetCapData;
icon?: boolean;
};
-export const BorrowCapWarning = ({ borrowCap, icon = true, ...rest }: BorrowCapWarningProps) => {
+// `icon` is destructured only to keep it out of `...rest` (the alert always shows its severity icon).
+export const BorrowCapWarning = ({ borrowCap, icon, ...rest }: BorrowCapWarningProps) => {
// Don't show a warning when less than 98% utilized
if (!borrowCap.percentUsed || borrowCap.percentUsed < 98) return null;
@@ -27,11 +27,11 @@ export const BorrowCapWarning = ({ borrowCap, icon = true, ...rest }: BorrowCapW
};
return (
-
+
{renderText()}{' '}
Learn more
-
+
);
};
diff --git a/src/components/transactions/Warnings/ChangeNetworkWarning.tsx b/src/components/transactions/Warnings/ChangeNetworkWarning.tsx
index 7f71b1f56c..1ebd5adae7 100644
--- a/src/components/transactions/Warnings/ChangeNetworkWarning.tsx
+++ b/src/components/transactions/Warnings/ChangeNetworkWarning.tsx
@@ -1,14 +1,12 @@
import { ChainId } from '@aave/contract-helpers';
import { Trans } from '@lingui/macro';
-import { AlertProps, Button, CircularProgress, Typography } from '@mui/material';
+import { Alert, AlertProps, Button, CircularProgress } from '@mui/material';
import { useEffect, useState } from 'react';
import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
import { TrackEventProps } from 'src/store/analyticsSlice';
import { useRootStore } from 'src/store/root';
import { GENERAL } from 'src/utils/events';
-import { Warning } from '../../primitives/Warning';
-
export type ChangeNetworkWarningProps = AlertProps & {
funnel?: string;
networkName: string;
@@ -25,6 +23,7 @@ export const ChangeNetworkWarning = ({
funnel,
askManualSwitch = false,
autoSwitchOnMount = true,
+ sx,
...rest
}: ChangeNetworkWarningProps) => {
const { switchNetwork, switchNetworkError } = useWeb3Context();
@@ -70,46 +69,38 @@ export const ChangeNetworkWarning = ({
switchNetwork(chainId);
};
return (
-
{isAutoSwitching ? (
-
+ <>
Switching to {networkName}...
-
+ >
) : switchNetworkError ? (
-
-
- {hasAttemptedAutoSwitch
- ? "We couldn't switch the network automatically. Please check if you can change it from the wallet."
- : "Seems like we can't switch the network automatically. Please check if you can change it from the wallet."}
-
-
+
+ {hasAttemptedAutoSwitch
+ ? "We couldn't switch the network automatically. Please check if you can change it from the wallet."
+ : "Seems like we can't switch the network automatically. Please check if you can change it from the wallet."}
+
) : (
// Show manual switch option
-
+ <>
{hasAttemptedAutoSwitch
? `Auto-switch failed. Please manually switch to ${networkName}.`
: `Please switch to ${networkName}.`}
{' '}
{!askManualSwitch && (
-
-
- Switch Network
-
+
+ Switch Network
)}
-
+ >
)}
-
+
);
};
diff --git a/src/components/transactions/Warnings/CowLowerThanMarketWarning.tsx b/src/components/transactions/Warnings/CowLowerThanMarketWarning.tsx
index 504b906e23..685a9ae7a4 100644
--- a/src/components/transactions/Warnings/CowLowerThanMarketWarning.tsx
+++ b/src/components/transactions/Warnings/CowLowerThanMarketWarning.tsx
@@ -1,15 +1,12 @@
import { Trans } from '@lingui/macro';
-import { Typography } from '@mui/material';
-import { Warning } from 'src/components/primitives/Warning';
+import { Alert } from '@mui/material';
export const CowLowerThanMarketWarning = () => {
return (
-
-
-
- The selected rate is lower than the market price. You might incur a loss if you proceed.
-
-
-
+
+
+ The selected rate is lower than the market price. You might incur a loss if you proceed.
+
+
);
};
diff --git a/src/components/transactions/Warnings/DebtCeilingWarning.tsx b/src/components/transactions/Warnings/DebtCeilingWarning.tsx
index 84c7add649..37d5a49d10 100644
--- a/src/components/transactions/Warnings/DebtCeilingWarning.tsx
+++ b/src/components/transactions/Warnings/DebtCeilingWarning.tsx
@@ -1,20 +1,16 @@
import { Trans } from '@lingui/macro';
-import { AlertProps } from '@mui/material';
+import { Alert, AlertProps } from '@mui/material';
import { AssetCapData } from 'src/hooks/useAssetCaps';
import { Link } from '../../primitives/Link';
-import { Warning } from '../../primitives/Warning';
type DebtCeilingWarningProps = AlertProps & {
debtCeiling: AssetCapData;
icon?: boolean;
};
-export const DebtCeilingWarning = ({
- debtCeiling,
- icon = true,
- ...rest
-}: DebtCeilingWarningProps) => {
+// `icon` is destructured only to keep it out of `...rest` (the alert always shows its severity icon).
+export const DebtCeilingWarning = ({ debtCeiling, icon, ...rest }: DebtCeilingWarningProps) => {
// Don't show a warning when less than 98% utilized
if (!debtCeiling.percentUsed || debtCeiling.percentUsed < 98) return null;
@@ -35,7 +31,7 @@ export const DebtCeilingWarning = ({
};
return (
-
+
{renderText()}{' '}
Learn more
-
+
);
};
diff --git a/src/components/transactions/Warnings/IsolationModeWarning.tsx b/src/components/transactions/Warnings/IsolationModeWarning.tsx
index c7f486a855..7294c599dd 100644
--- a/src/components/transactions/Warnings/IsolationModeWarning.tsx
+++ b/src/components/transactions/Warnings/IsolationModeWarning.tsx
@@ -1,8 +1,7 @@
import { Trans } from '@lingui/macro';
-import { AlertColor, Typography } from '@mui/material';
+import { Alert, AlertColor, AlertTitle } from '@mui/material';
import { Link } from '../../primitives/Link';
-import { Warning } from '../../primitives/Warning';
interface IsolationModeWarningProps {
asset?: string;
@@ -11,18 +10,16 @@ interface IsolationModeWarningProps {
export const IsolationModeWarning = ({ asset, severity }: IsolationModeWarningProps) => {
return (
-
-
+
+ You are entering Isolation mode
-
-
-
- In Isolation mode, you cannot supply other assets as collateral. A global debt ceiling
- limits the borrowing power of the isolated asset. To exit isolation mode disable{' '}
- {asset ? asset : ''} as collateral before borrowing another asset. Read more in our{' '}
- FAQ
-
-
-
+
+
+ In Isolation mode, you cannot supply other assets as collateral. A global debt ceiling
+ limits the borrowing power of the isolated asset. To exit isolation mode disable{' '}
+ {asset ? asset : ''} as collateral before borrowing another asset. Read more in our{' '}
+ FAQ
+
+
);
};
diff --git a/src/components/transactions/Warnings/MarketWarning.tsx b/src/components/transactions/Warnings/MarketWarning.tsx
index b2e1a7b26a..ff403ab640 100644
--- a/src/components/transactions/Warnings/MarketWarning.tsx
+++ b/src/components/transactions/Warnings/MarketWarning.tsx
@@ -1,7 +1,5 @@
import { Trans } from '@lingui/macro';
-import { Link, Typography } from '@mui/material';
-
-import { Warning } from '../../primitives/Warning';
+import { Alert, Link } from '@mui/material';
const WarningMessage = ({ market }: { market: string }) => {
if (market) {
@@ -27,13 +25,11 @@ interface MarketWarningProps {
// NOTE: Deprecated for now as no frozen markets
export const MarketWarning = ({ marketName, forum }: MarketWarningProps) => {
return (
-
-
- {' '}
-
- {forum ? Join the community discussion : Learn more}
-
-
-
+
+ {' '}
+
+ {forum ? Join the community discussion : Learn more}
+
+
);
};
diff --git a/src/components/transactions/Warnings/ParaswapErrorDisplay.tsx b/src/components/transactions/Warnings/ParaswapErrorDisplay.tsx
index fa1df54442..b0ea78b9be 100644
--- a/src/components/transactions/Warnings/ParaswapErrorDisplay.tsx
+++ b/src/components/transactions/Warnings/ParaswapErrorDisplay.tsx
@@ -1,6 +1,5 @@
import { Trans } from '@lingui/macro';
-import { Box, Typography } from '@mui/material';
-import { Warning } from 'src/components/primitives/Warning';
+import { Alert, Box } from '@mui/material';
import { TxErrorType } from 'src/ui-config/errorMapping';
import { GasEstimationError } from '../FlowCommons/GasEstimationError';
@@ -18,12 +17,9 @@ export const ParaswapErrorDisplay: React.FC = ({ txError }) => {
{txError.rawError.message !== USER_DENIED_SIGNATURE &&
txError.rawError.message !== USER_DENIED_TRANSACTION && (
-
-
- {' '}
- Tip: Try increasing slippage or reduce input amount
-
-
+
+ Tip: Try increasing slippage or reduce input amount
+
)}
diff --git a/src/components/transactions/Warnings/SNXWarning.tsx b/src/components/transactions/Warnings/SNXWarning.tsx
index 1574614eb5..63981c7e21 100644
--- a/src/components/transactions/Warnings/SNXWarning.tsx
+++ b/src/components/transactions/Warnings/SNXWarning.tsx
@@ -1,19 +1,15 @@
import { Trans } from '@lingui/macro';
-import { Typography } from '@mui/material';
-
-import { Warning } from '../../primitives/Warning';
+import { Alert } from '@mui/material';
export const SNXWarning = () => {
return (
-
-
- Before supplying SNX{' '}
-
- {' '}
- please check that the amount you want to supply is not currently being used for staking.
- If it is being used for staking, your transaction might fail.
-
-
-
+
+ Before supplying SNX{' '}
+
+ {' '}
+ please check that the amount you want to supply is not currently being used for staking. If
+ it is being used for staking, your transaction might fail.
+
+
);
};
diff --git a/src/components/transactions/Warnings/SupplyCapWarning.tsx b/src/components/transactions/Warnings/SupplyCapWarning.tsx
index 34c4c9ad33..8d437f4a76 100644
--- a/src/components/transactions/Warnings/SupplyCapWarning.tsx
+++ b/src/components/transactions/Warnings/SupplyCapWarning.tsx
@@ -1,16 +1,16 @@
import { Trans } from '@lingui/macro';
-import { AlertProps } from '@mui/material';
+import { Alert, AlertProps } from '@mui/material';
import { AssetCapData } from 'src/hooks/useAssetCaps';
import { Link } from '../../primitives/Link';
-import { Warning } from '../../primitives/Warning';
type SupplyCapWarningProps = AlertProps & {
supplyCap: AssetCapData;
icon?: boolean;
};
-export const SupplyCapWarning = ({ supplyCap, icon = true, ...rest }: SupplyCapWarningProps) => {
+// `icon` is destructured only to keep it out of `...rest` (the alert always shows its severity icon).
+export const SupplyCapWarning = ({ supplyCap, icon, ...rest }: SupplyCapWarningProps) => {
// Don't show a warning when less than 98% utilized
if (!supplyCap.percentUsed || supplyCap.percentUsed < 98) return null;
@@ -28,11 +28,11 @@ export const SupplyCapWarning = ({ supplyCap, icon = true, ...rest }: SupplyCapW
};
return (
-
+
{renderText()}{' '}
Learn more
-
+
);
};
diff --git a/src/components/transactions/Warnings/USDTResetWarning.tsx b/src/components/transactions/Warnings/USDTResetWarning.tsx
index dfd320b172..35585116de 100644
--- a/src/components/transactions/Warnings/USDTResetWarning.tsx
+++ b/src/components/transactions/Warnings/USDTResetWarning.tsx
@@ -1,16 +1,13 @@
import { Trans } from '@lingui/macro';
-import { Typography } from '@mui/material';
-import { Warning } from 'src/components/primitives/Warning';
+import { Alert } from '@mui/material';
export const USDTResetWarning = () => {
return (
-
-
-
- USDT on Ethereum requires approval reset before a new approval. This will require an
- additional transaction.
-
-
-
+
+
+ USDT on Ethereum requires approval reset before a new approval. This will require an
+ additional transaction.
+
+
);
};
diff --git a/src/components/transactions/Withdraw/WithdrawModalContent.tsx b/src/components/transactions/Withdraw/WithdrawModalContent.tsx
index bc427401f9..65ec8542bb 100644
--- a/src/components/transactions/Withdraw/WithdrawModalContent.tsx
+++ b/src/components/transactions/Withdraw/WithdrawModalContent.tsx
@@ -1,9 +1,8 @@
import { API_ETH_MOCK_ADDRESS } from '@aave/contract-helpers';
import { valueToBigNumber } from '@aave/math-utils';
import { Trans } from '@lingui/macro';
-import { Box, Checkbox, Typography } from '@mui/material';
+import { Alert, Box, Checkbox, Typography } from '@mui/material';
import { useRef, useState } from 'react';
-import { Warning } from 'src/components/primitives/Warning';
import { ExtendedFormattedUser } from 'src/hooks/app-data-provider/useAppDataProvider';
import { useModalContext } from 'src/hooks/useModal';
import { useZeroLTVBlockingWithdraw } from 'src/hooks/useZeroLTVBlockingWithdraw';
@@ -188,12 +187,12 @@ export const WithdrawModalContent = ({
{displayRiskCheckbox && (
<>
-
+
Withdrawing this amount will reduce your health factor and increase risk of
liquidation.
-
+
-
+ Withdraw
@@ -49,7 +49,7 @@ export function WithdrawTypeSelector({
trackEvent(WITHDRAW_MODAL.SWITCH_WITHDRAW_TYPE, { withdrawType: 'Withdraw and Swap' })
}
>
-
+ Withdraw & Swap
diff --git a/src/hooks/useConnectGate.ts b/src/hooks/useConnectGate.ts
new file mode 100644
index 0000000000..d766560f87
--- /dev/null
+++ b/src/hooks/useConnectGate.ts
@@ -0,0 +1,21 @@
+import { useModal } from 'connectkit';
+import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
+
+/**
+ * Returns a wrapper that runs `action` when a wallet is connected, or opens the ConnectKit
+ * (Family) wallet-connect modal when it isn't. Used by entry points like the header's Swap /
+ * Bridge buttons so unauthenticated users go straight to connect instead of a modal's own
+ * connect step.
+ */
+export const useConnectGate = () => {
+ const { currentAccount } = useWeb3Context();
+ const { setOpen } = useModal();
+
+ return (action: () => void) => {
+ if (!currentAccount) {
+ setOpen(true);
+ return;
+ }
+ action();
+ };
+};
diff --git a/src/hooks/usePinnedMarket.ts b/src/hooks/usePinnedMarket.ts
new file mode 100644
index 0000000000..7d259fdf85
--- /dev/null
+++ b/src/hooks/usePinnedMarket.ts
@@ -0,0 +1,20 @@
+import { useEffect } from 'react';
+import { useRootStore } from 'src/store/root';
+import { CustomMarket } from 'src/ui-config/marketsConfig';
+
+/**
+ * Pins the app's selected market to `market` for the lifetime of the calling page, restoring the
+ * user's prior market on unmount — so a page that must run on a single instance (e.g. staking /
+ * safety module on Core) can force it without a lasting global change. The header, lists, and tx
+ * modals all read the market from the store, so pinning here covers the whole page. No-op when
+ * already on `market`.
+ */
+export const usePinnedMarket = (market: CustomMarket) => {
+ useEffect(() => {
+ const { currentMarket: prevMarket, setCurrentMarket } = useRootStore.getState();
+ if (prevMarket !== market) {
+ setCurrentMarket(market, true); // true = don't touch the URL query param
+ return () => setCurrentMarket(prevMarket, true);
+ }
+ }, [market]);
+};
diff --git a/src/hooks/useReserveActionState.tsx b/src/hooks/useReserveActionState.tsx
index 6a9db8200e..1d50158b17 100644
--- a/src/hooks/useReserveActionState.tsx
+++ b/src/hooks/useReserveActionState.tsx
@@ -1,8 +1,7 @@
import { ExternalLinkIcon } from '@heroicons/react/solid';
import { Trans } from '@lingui/macro';
-import { Button, Stack, SvgIcon, Typography } from '@mui/material';
+import { Alert, Button, Stack, SvgIcon, Typography } from '@mui/material';
import { Link, ROUTES } from 'src/components/primitives/Link';
-import { Warning } from 'src/components/primitives/Warning';
import { getEmodeMessage } from 'src/components/transactions/Emode/EmodeNaming';
import { isFunSupplyAsset } from 'src/components/transactions/FunCheckout/funSupplyAssets';
import {
@@ -75,7 +74,7 @@ export const useReserveActionState = ({
{balance === '0' && !isGho && (
<>
{currentNetworkConfig.isTestnet ? (
-
+
Your {networkName} wallet is empty. Get free test {reserve.name} at
{' '}
@@ -108,13 +107,12 @@ export const useReserveActionState = ({
)}
-
+
) : (
)}
@@ -122,29 +120,29 @@ export const useReserveActionState = ({
)}
{(balance !== '0' || isGho) && user?.totalCollateralMarketReferenceCurrency === '0' && (
-
+ To borrow you need to supply any asset to be used as collateral.
-
+
)}
{isolationModeBorrowDisabled && (
-
+ Collateral usage is limited because of Isolation mode.
-
+
)}
{eModeBorrowDisabled && isolationModeBorrowDisabled && (
-
+
Borrowing is unavailable because you’ve enabled Efficiency Mode (E-Mode) and Isolation
mode. To manage E-Mode and Isolation mode visit your{' '}
Dashboard.
-
+
)}
{eModeBorrowDisabled && !isolationModeBorrowDisabled && (
-
+
Borrowing is unavailable because you’ve enabled Efficiency Mode (E-Mode) for{' '}
{replaceUnderscoresWithSpaces(
@@ -153,16 +151,16 @@ export const useReserveActionState = ({
category. To manage E-Mode categories visit your{' '}
Dashboard.
-
+
)}
{!eModeBorrowDisabled && isolationModeBorrowDisabled && (
-
+
Borrowing is unavailable because you’re using Isolation mode. To manage Isolation mode
visit your Dashboard.
-
+
)}
{maxAmountToSupply === '0' &&
diff --git a/src/layouts/AppFooter.tsx b/src/layouts/AppFooter.tsx
index 604995cdb9..83a4f32440 100644
--- a/src/layouts/AppFooter.tsx
+++ b/src/layouts/AppFooter.tsx
@@ -1,9 +1,13 @@
import { Trans } from '@lingui/macro';
-import { GitHub, Instagram, LinkedIn, X } from '@mui/icons-material';
-import { Box, styled, SvgIcon, Typography } from '@mui/material';
+import GitHub from '@mui/icons-material/GitHub';
+import Instagram from '@mui/icons-material/Instagram';
+import LinkedIn from '@mui/icons-material/LinkedIn';
+import X from '@mui/icons-material/X';
+import { Box, Container, styled, SvgIcon, Typography } from '@mui/material';
import { DuneIcon, TikTok } from 'public/icons/footer/icons';
import { Link } from 'src/components/primitives/Link';
import { useRootStore } from 'src/store/root';
+import { figVars } from 'src/utils/figmaColors';
import { useShallow } from 'zustand/shallow';
import DiscordIcon from '/public/icons/discord.svg';
@@ -13,14 +17,14 @@ interface StyledLinkProps {
onClick?: React.MouseEventHandler;
}
-const StyledLink = styled(Link)(({ theme }) => ({
- color: theme.palette.text.muted,
+const StyledLink = styled(Link)({
+ color: figVars['fg-3'],
'&:hover': {
- color: theme.palette.text.primary,
+ color: figVars['fg-1'],
},
display: 'flex',
alignItems: 'center',
-}));
+});
const FOOTER_ICONS = [
{
@@ -114,39 +118,46 @@ export function AppFooter() {
return (
({
- display: 'flex',
- padding: ['22px 0px 40px 0px', '0 22px 0 40px', '20px 22px'],
width: '100%',
- justifyContent: 'space-between',
- alignItems: 'center',
- gap: '22px',
- flexDirection: ['column', 'column', 'row'],
boxShadow:
theme.palette.mode === 'light'
? 'inset 0px 1px 0px rgba(0, 0, 0, 0.04)'
: 'inset 0px 1px 0px rgba(255, 255, 255, 0.12)',
})}
>
-
- {FOOTER_LINKS.map((link) => (
-
- {link.label}
-
- ))}
-
-
- {FOOTER_ICONS.map((icon) => (
-
-
- {icon.icon}
-
-
- ))}
-
+ {/* Horizontal padding + maxWidth come from the themed MuiContainer breakpoint ladder, same as
+ AppHeader, so the footer's content edges line up with the header's at every viewport width. */}
+
+
+ {FOOTER_LINKS.map((link) => (
+
+ {link.label}
+
+ ))}
+
+
+ {FOOTER_ICONS.map((icon) => (
+
+
+ {icon.icon}
+
+
+ ))}
+
+
);
}
diff --git a/src/layouts/AppGlobalStyles.tsx b/src/layouts/AppGlobalStyles.tsx
index ca31f629ea..e6bbca809e 100644
--- a/src/layouts/AppGlobalStyles.tsx
+++ b/src/layouts/AppGlobalStyles.tsx
@@ -1,61 +1,43 @@
-import { useMediaQuery } from '@mui/material';
import CssBaseline from '@mui/material/CssBaseline';
-import { createTheme, ThemeProvider } from '@mui/material/styles';
-import { deepmerge } from '@mui/utils';
-import React, { ReactNode, useEffect, useMemo, useState } from 'react';
+import GlobalStyles from '@mui/material/GlobalStyles';
+import { Experimental_CssVarsProvider as CssVarsProvider } from '@mui/material/styles';
+import { ReactNode, useMemo } from 'react';
-import { getDesignTokens, getThemedComponents } from '../utils/theme';
-
-export const ColorModeContext = React.createContext({
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- toggleColorMode: () => {},
-});
-
-type Mode = 'light' | 'dark';
+import { buildP3Overrides, createAppTheme } from '../utils/theme';
/**
- * Main Layout component which wrapps around the whole app
- * @param param0
- * @returns
+ * Main layout wrapper around the whole app. Provides the MUI theme via the CSS-variables
+ * engine: both color schemes are baked into CSS custom properties once, and light/dark is
+ * switched by toggling the `data-mui-color-scheme` attribute on (persisted by MUI,
+ * seeded from the OS preference). Components read/set the scheme via `useColorScheme()`.
*/
export function AppGlobalStyles({ children }: { children: ReactNode }) {
- const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
- const [mode, setMode] = useState(prefersDarkMode ? 'dark' : 'light');
- const colorMode = useMemo(
- () => ({
- toggleColorMode: () => {
- setMode((prevMode) => {
- const newMode = prevMode === 'light' ? 'dark' : 'light';
- localStorage.setItem('colorMode', newMode);
- return newMode;
- });
+ const theme = useMemo(() => createAppTheme(), []);
+
+ // Display-P3 layer: on wide-gamut displays that support the syntax, override the sRGB
+ // `--mui-palette-*` vars with their P3 equivalents. Everything else keeps the sRGB base.
+ const p3Styles = useMemo(() => {
+ const { light, dark } = buildP3Overrides(theme);
+ return {
+ '@supports (color: color(display-p3 1 1 1))': {
+ '@media (color-gamut: p3)': {
+ // Doubled selectors (specificity 0,2,0) beat MUI's own var sheets (0,1,0), so the
+ // P3 layer wins regardless of stylesheet source order — and still match both
+ // and the showcase's local `data-mui-color-scheme` wrapper.
+ ':root:root, [data-mui-color-scheme="light"][data-mui-color-scheme="light"]': light,
+ '[data-mui-color-scheme="dark"][data-mui-color-scheme="dark"]': dark,
+ },
},
- }),
- []
- );
-
- useEffect(() => {
- const initialMode = localStorage?.getItem('colorMode') as Mode;
- if (initialMode) {
- setMode(initialMode);
- } else if (prefersDarkMode) {
- setMode('dark');
- }
- }, []);
-
- const theme = useMemo(() => {
- const themeCreate = createTheme(getDesignTokens(mode));
- return deepmerge(themeCreate, getThemedComponents(themeCreate));
- }, [mode]);
+ };
+ }, [theme]);
return (
-
-
- {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
-
+
+ {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
+
+
- {children}
-
-
+ {children}
+
);
}
diff --git a/src/layouts/AppHeader.tsx b/src/layouts/AppHeader.tsx
index c798446032..33159ddd65 100644
--- a/src/layouts/AppHeader.tsx
+++ b/src/layouts/AppHeader.tsx
@@ -1,13 +1,10 @@
-import {
- InformationCircleIcon,
- SparklesIcon,
- SwitchHorizontalIcon,
-} from '@heroicons/react/outline';
+import { InformationCircleIcon } from '@heroicons/react/outline';
import { Trans } from '@lingui/macro';
import {
Badge,
Button,
CircularProgress,
+ Container,
NoSsr,
Slide,
styled,
@@ -22,18 +19,23 @@ import * as React from 'react';
import { useEffect, useState } from 'react';
import { AvatarSize } from 'src/components/Avatar';
import { ContentWithTooltip } from 'src/components/ContentWithTooltip';
+import { AaveLogo } from 'src/components/icons/AaveLogo';
+import { BridgeIcon } from 'src/components/icons/BridgeIcon';
+import { SwapIcon } from 'src/components/icons/SwapIcon';
import { AAVE_PRO_URL } from 'src/components/MarketSwitcher';
import { UserDisplay } from 'src/components/UserDisplay';
import { ConnectWalletButton } from 'src/components/WalletConnection/ConnectWalletButton';
+import { useConnectGate } from 'src/hooks/useConnectGate';
import { useModalContext } from 'src/hooks/useModal';
import { useSwapOrdersTracking } from 'src/hooks/useSwapOrdersTracking';
import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
import { useRootStore } from 'src/store/root';
+import { iconButtonSx } from 'src/utils/buttonStyles';
+import { figVars } from 'src/utils/figmaColors';
import { ENABLE_TESTNET, FORK_ENABLED, isFeatureEnabled } from 'src/utils/marketsAndNetworksConfig';
import { useShallow } from 'zustand/shallow';
import { Link } from '../components/primitives/Link';
-import { uiConfig } from '../uiConfig';
import { NavItems } from './components/NavItems';
import { MobileMenu } from './MobileMenu';
import { SettingsMenu } from './SettingsMenu';
@@ -49,8 +51,8 @@ const StyledBadge = styled(Badge)(({ theme }) => ({
borderRadius: '20px',
width: '10px',
height: '10px',
- backgroundColor: `${theme.palette.secondary.main}`,
- color: `${theme.palette.secondary.main}`,
+ backgroundColor: `${theme.vars.palette.secondary.main}`,
+ color: `${theme.vars.palette.secondary.main}`,
'&::after': {
position: 'absolute',
top: 0,
@@ -77,11 +79,12 @@ const StyledBadge = styled(Badge)(({ theme }) => ({
function HideOnScroll({ children }: Props) {
const { breakpoints } = useTheme();
- const md = useMediaQuery(breakpoints.down('md'));
- const trigger = useScrollTrigger({ threshold: md ? 160 : 80 });
+ const mdlg = useMediaQuery(breakpoints.down('mdlg'));
+ const trigger = useScrollTrigger({ threshold: 80 });
+ // Mobile keeps the header pinned (never hides on scroll); desktop still hides past the threshold.
return (
-
+
{children}
);
@@ -89,11 +92,24 @@ function HideOnScroll({ children }: Props) {
const SWITCH_VISITED_KEY = 'switchVisited';
+// Dev-only environment badges (testnet / fork) — intentionally off-brand magenta to stand out.
+const envBadgeSx = {
+ backgroundColor: '#B6509E',
+ boxShadow: 'none',
+ '&:hover, &.Mui-focusVisible': { backgroundColor: 'rgba(182, 80, 158, 0.7)', boxShadow: 'none' },
+ // The pill variant tints on hover via a ::before overlay; the badge steps its own fill instead.
+ '&:hover::before, &.Mui-focusVisible::before': { backgroundColor: 'transparent' },
+};
+
export function AppHeader() {
const { breakpoints } = useTheme();
- const md = useMediaQuery(breakpoints.down('md'));
+ const mdlg = useMediaQuery(breakpoints.down('mdlg'));
const sm = useMediaQuery(breakpoints.down('sm'));
- const smd = useMediaQuery('(max-width:1120px)');
+ const lg = useMediaQuery(breakpoints.down('lg'));
+ // Shared by the Swap + Bridge triggers: icon-only square when collapsed (below lg), text otherwise.
+ const collapsingTriggerSx = lg
+ ? [iconButtonSx, { alignItems: 'center', '& .MuiButton-startIcon': { mx: 0 } }]
+ : { p: '0 0.88rem', minWidth: 'unset', alignItems: 'center' };
const [, setVisitedSwitch] = useState(() => {
if (typeof window === 'undefined') return true;
@@ -112,26 +128,17 @@ export function AppHeader() {
const { openSwitch, openBridge, openReadMode } = useModalContext();
const { readOnlyMode } = useWeb3Context();
- const [walletWidgetOpen, setWalletWidgetOpen] = useState(false);
- const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+ const openOrConnect = useConnectGate();
const { hasActiveOrders } = useSwapOrdersTracking();
useEffect(() => {
- if (mobileDrawerOpen && !md) {
+ if (!mdlg) {
setMobileDrawerOpen(false);
}
- if (walletWidgetOpen) {
- setWalletWidgetOpen(false);
- }
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [md]);
+ }, [mdlg]);
- const headerHeight = 48;
-
- const toggleMobileMenu = (state: boolean) => {
- if (md) setMobileDrawerOpen(state);
- setMobileMenuOpen(state);
- };
+ const headerHeight = 72;
const disableTestnet = () => {
localStorage.setItem('testnetsEnabled', 'false');
@@ -152,11 +159,11 @@ export function AppHeader() {
const handleSwitchClick = () => {
localStorage.setItem(SWITCH_VISITED_KEY, 'true');
setVisitedSwitch(true);
- openSwitch();
+ openOrConnect(openSwitch);
};
const handleBridgeClick = () => {
- openBridge();
+ openOrConnect(openBridge);
};
const testnetTooltip = (
@@ -173,7 +180,7 @@ export function AppHeader() {
FAQ.
-
+ Disable testnet
@@ -187,7 +194,7 @@ export function AppHeader() {
The app is running in fork mode.
-
+ Disable fork
@@ -205,190 +212,173 @@ export function AppHeader() {
top: 0,
transition: theme.transitions.create('top'),
zIndex: theme.zIndex.appBar,
- bgcolor: theme.palette.background.header,
- padding: {
- xs: mobileMenuOpen || walletWidgetOpen ? '8px 20px' : '8px 8px 8px 20px',
- xsm: '8px 20px',
- },
+ bgcolor: 'bg-3',
display: 'flex',
- alignItems: 'center',
- flexDirection: 'space-between',
- boxShadow: 'inset 0px -1px 0px rgba(242, 243, 247, 0.16)',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ boxShadow: `inset 0px -1px 0px ${figVars['border-0']}`,
})}
>
- setMobileMenuOpen(false)}
>
-
-
-
- {ENABLE_TESTNET && (
-
-
- TESTNET
-
-
-
-
-
- )}
-
-
- {FORK_ENABLED && currentMarketData?.isFork && (
-
-
- FORK
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
- setMobileDrawerOpen(false)}
>
-
- {smd ? 'V4' : 'Aave V4'}
-
-
-
+
+
+
+ {ENABLE_TESTNET && (
+
+
+ TESTNET
+
+
+
+
+
+ )}
+
+
+ {FORK_ENABLED && currentMarketData?.isFork && (
+
+
+ FORK
+
+
+
+
+
+ )}
+
-
-
+
+
+
+
+
+
+
- {!smd && (
-
- Bridge GHO
-
- )}
-
-
-
+
+ {lg ? 'V4' : 'Aave V4'}
+
-
-
+
-
-
-
+
- {!smd && (
-
- Swap
-
- )}
-
- {hasActiveOrders ? (
- theme.palette.grey[200],
- }}
- />
- ) : (
-
-
-
+
+ {hasActiveOrders ? (
+ theme.vars.palette.grey[200],
+ }}
+ />
+ ) : (
+
+ )}
+
+ }
+ sx={collapsingTriggerSx}
+ aria-label="Switch tool"
+ disabled={!showSwitchButton}
+ >
+ {!lg && (
+
+ Swap
+
)}
-
-
-
-
+
+
+
- {readOnlyMode ? (
- {
- openReadMode();
- }}
- >
-
-
- ) : (
-
- )}
+
+
+ }
+ sx={collapsingTriggerSx}
+ >
+ {!lg && (
+
+ Bridge GHO
+
+ )}
+
+
+
+
+ {readOnlyMode ? (
+ {
+ openReadMode();
+ }}
+ >
+
+
+ ) : (
+
+ )}
-
-
-
+ {!mdlg && }
- {!walletWidgetOpen && (
-
+
- )}
+
);
diff --git a/src/layouts/MobileMenu.tsx b/src/layouts/MobileMenu.tsx
index 711b0f0984..219e4d2e11 100644
--- a/src/layouts/MobileMenu.tsx
+++ b/src/layouts/MobileMenu.tsx
@@ -1,27 +1,17 @@
-import { MenuIcon } from '@heroicons/react/outline';
import { Trans } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import {
- Box,
- Button,
- Divider,
- List,
- ListItem,
- ListItemIcon,
- ListItemText,
- SvgIcon,
- Typography,
-} from '@mui/material';
-import React, { ReactNode, useEffect, useState } from 'react';
+import { Box, Button, Divider, List, ListItem, ListItemText } from '@mui/material';
+import { useEffect, useState } from 'react';
+import { BridgeIcon } from 'src/components/icons/BridgeIcon';
+import { SwapIcon } from 'src/components/icons/SwapIcon';
+import { useConnectGate } from 'src/hooks/useConnectGate';
import { useModalContext } from 'src/hooks/useModal';
-import { PROD_ENV } from 'src/utils/marketsAndNetworksConfig';
+import { useRootStore } from 'src/store/root';
+import { figVars } from 'src/utils/figmaColors';
+import { isFeatureEnabled, PROD_ENV } from 'src/utils/marketsAndNetworksConfig';
-import { Link } from '../components/primitives/Link';
-import { moreNavigation } from '../ui-config/menu-items';
import { DarkModeSwitcher } from './components/DarkModeSwitcher';
import { DrawerWrapper } from './components/DrawerWrapper';
import { LanguageListItem, LanguagesList } from './components/LanguageSwitcher';
-import { MobileCloseButton } from './components/MobileCloseButton';
import { NavItems } from './components/NavItems';
import { ShieldSwitcher } from './components/ShieldSwitcher';
import { TestNetModeSwitcher } from './components/TestNetModeSwitcher';
@@ -32,97 +22,184 @@ interface MobileMenuProps {
headerHeight: number;
}
-const MenuItemsWrapper = ({ children, title }: { children: ReactNode; title: ReactNode }) => (
-
-
-
- {title}
-
+// The options scroll area: full-width so its scrollbar sits on the right edge, with 0.75rem inner
+// padding for the content.
+const scrollAreaSx = {
+ flex: 1,
+ minHeight: 0,
+ overflowY: 'auto',
+ px: '0.75rem',
+ pb: '3rem',
+} as const;
- {children}
-
+// Rows inside the drawer lists: 3rem tall, H3 label text, gutters zeroed so they align with the
+// scroll area's 0.75rem inset. Applied via sx so the shared row components (SettingSwitchRow,
+// LanguagesList) don't need to know about it.
+const menuListSx = {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '0.5rem',
+ '& .MuiListItem-root': {
+ minHeight: '3rem',
+ borderRadius: '0.5rem',
+ px: 0,
+ cursor: 'pointer',
+ },
+ '& .MuiListItemText-primary': { fontSize: '1.125rem', fontWeight: 500, lineHeight: '120%' },
+};
+
+// The hamburger (three rounded lines, per the design SVG) that morphs into an X. Rendered inside
+// one fixed-size button (below), so toggling never resizes the button and shifts the header.
+// One bar of the hamburger; the three uses below add position + the open-state transform.
+const toggleBar = {
+ position: 'absolute' as const,
+ left: '4px',
+ width: '16px',
+ height: '2px',
+ borderRadius: '1px',
+ backgroundColor: 'currentColor',
+ transition: 'transform 0.2s ease, opacity 0.2s ease',
+};
-
+const MenuToggleIcon = ({ open }: { open: boolean }) => (
+
+
+
+
);
export const MobileMenu = ({ open, setOpen, headerHeight }: MobileMenuProps) => {
- const { i18n } = useLingui();
const [isLanguagesListOpen, setIsLanguagesListOpen] = useState(false);
- const { openReadMode } = useModalContext();
+ // Drives the top scrim: it only shows once the options actually scroll, so it never dims the
+ // first row at rest.
+ const [scrolled, setScrolled] = useState(false);
+ const { openReadMode, openSwitch, openBridge } = useModalContext();
+ const openOrConnect = useConnectGate();
+ const currentMarketData = useRootStore((store) => store.currentMarketData);
+ const showSwitchButton = isFeatureEnabled.switch(currentMarketData);
useEffect(() => setIsLanguagesListOpen(false), [open]);
+ // A fresh scroll area always starts at the top, so reset on open / view switch.
+ useEffect(() => setScrolled(false), [open, isLanguagesListOpen]);
const handleOpenReadMode = () => {
setOpen(false);
openReadMode();
};
+ const handleSwap = () => {
+ setOpen(false);
+ openOrConnect(openSwitch);
+ };
+
+ const handleBridge = () => {
+ setOpen(false);
+ openOrConnect(openBridge);
+ };
+
return (
<>
- {open ? (
-
- ) : (
- setOpen(true)}
- >
-
-
-
-
- )}
+ setOpen(!open)}
+ >
+
+
+ {/* Fade scrim over the top of the scroll area (mirrors the bottom scrim). Only shown once
+ scrolled, so it never dims the first row at rest. Inset from the top by the drawer's
+ padding (clean band under the header) and from the right so it never touches the scrollbar. */}
+
{!isLanguagesListOpen ? (
<>
- Menu}>
+ {/* Only the options scroll — the action buttons below stay pinned. */}
+ setScrolled(e.currentTarget.scrollTop > 0)}>
-
- Global settings}>
-
+
+ {/* Watch Wallet sits above the global-settings rows, no divider between them. */}
+
+
+
+ Watch Wallet
+
+
{PROD_ENV && }
setIsLanguagesListOpen(true)} />
-
- Links}>
-
-
-
- Watch wallet
-
-
+
- setOpen(false)}
+
+ {/* Fade scrim over the bottom of the scroll area, in place of a divider. */}
+
+
+ }
+ onClick={handleSwap}
+ disabled={!showSwitchButton}
>
-
- Migrate to Aave V3
-
-
- {moreNavigation.map((item, index) => (
-
-
- {item.icon}
-
-
- {i18n._(item.title)}
-
- ))}
-
-
+ Swap
+
+ }
+ onClick={handleBridge}
+ >
+ Bridge GHO
+
+
+
>
) : (
-
- setIsLanguagesListOpen(false)} />
-
+ setScrolled(e.currentTarget.scrollTop > 0)}>
+
+ setIsLanguagesListOpen(false)} />
+
+
)}
>
diff --git a/src/layouts/SettingsMenu.tsx b/src/layouts/SettingsMenu.tsx
index cd5a6a98f0..773eea82d3 100644
--- a/src/layouts/SettingsMenu.tsx
+++ b/src/layouts/SettingsMenu.tsx
@@ -1,7 +1,7 @@
-import { CogIcon } from '@heroicons/react/solid';
import { Trans } from '@lingui/macro';
-import { Button, ListItemText, Menu, MenuItem, SvgIcon, Typography } from '@mui/material';
+import { Button, Divider, ListItemText, Menu, MenuItem } from '@mui/material';
import React, { useState } from 'react';
+import { SettingsIcon } from 'src/components/icons/SettingsIcon';
import { useModalContext } from 'src/hooks/useModal';
import { DEFAULT_LOCALE } from 'src/libs/LanguageProvider';
import { useRootStore } from 'src/store/root';
@@ -64,18 +64,16 @@ export function SettingsMenu() {
return (
<>
-
-
-
+
diff --git a/src/layouts/SupportModal.tsx b/src/layouts/SupportModal.tsx
index 21a8dfbdcf..92ac117735 100644
--- a/src/layouts/SupportModal.tsx
+++ b/src/layouts/SupportModal.tsx
@@ -203,20 +203,11 @@ export const SupportModal = () => {
) : (
-
+ Support
-
+
Let us know how we can help you. You may also consider joining our community
@@ -224,7 +215,7 @@ export const SupportModal = () => {
diff --git a/src/modules/dashboard/lists/ListValueRow.tsx b/src/modules/dashboard/lists/ListValueRow.tsx
index 943493d4db..a859f4c785 100644
--- a/src/modules/dashboard/lists/ListValueRow.tsx
+++ b/src/modules/dashboard/lists/ListValueRow.tsx
@@ -23,19 +23,15 @@ export const ListValueRow = ({
-
+
{capsComponent}
{!disabled && (
diff --git a/src/modules/dashboard/lists/SlippageList.tsx b/src/modules/dashboard/lists/SlippageList.tsx
index 320bca36b4..64866f9acc 100644
--- a/src/modules/dashboard/lists/SlippageList.tsx
+++ b/src/modules/dashboard/lists/SlippageList.tsx
@@ -54,10 +54,10 @@ export const ListSlippageButton = ({
text={
-
+
Slippage tolerance{' '}
-
+
{selectedSlippage}%{' '}
@@ -66,7 +66,7 @@ export const ListSlippageButton = ({
}
- variant="secondary14"
+ variant="h5"
/>
}
disabled={false}
@@ -84,7 +84,7 @@ export const ListSlippageButton = ({
data-cy={`slippageMenu_${selectedSlippage}`}
>
-
+ Select slippage tolerance
@@ -116,8 +116,8 @@ export const ListSlippageButton = ({
Powered by
@@ -133,7 +133,7 @@ export const ListSlippageButton = ({
-
+
Velora
diff --git a/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListItem.tsx b/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListItem.tsx
index bcb6968fb5..e147574a29 100644
--- a/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListItem.tsx
+++ b/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListItem.tsx
@@ -111,6 +111,7 @@ export const SuppliedPositionsListItem = ({
{showSwitchButton ? (
{
@@ -130,6 +131,7 @@ export const SuppliedPositionsListItem = ({
) : (
openSupply(underlyingAsset, currentMarket, reserve.name, 'dashboard')}
@@ -138,8 +140,9 @@ export const SuppliedPositionsListItem = ({
)}
{
openWithdraw(underlyingAsset, currentMarket, reserve.name, 'dashboard');
}}
diff --git a/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListMobileItem.tsx b/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListMobileItem.tsx
index ccf8856a20..c05c0a8291 100644
--- a/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListMobileItem.tsx
+++ b/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListMobileItem.tsx
@@ -95,7 +95,7 @@ export const SuppliedPositionsListMobileItem = ({
incentives={aIncentivesData}
address={aTokenAddress}
symbol={symbol}
- variant="secondary14"
+ variant="h5"
market={currentMarket}
protocolAction={ProtocolAction.supply}
/>
@@ -146,7 +146,7 @@ export const SuppliedPositionsListMobileItem = ({
)}
openWithdraw(underlyingAsset, currentMarket, reserve.name, 'dashboard')}
sx={{ ml: 1.5 }}
fullWidth
diff --git a/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsList.tsx b/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsList.tsx
index 12864d5841..21864a9e09 100644
--- a/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsList.tsx
+++ b/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsList.tsx
@@ -1,14 +1,13 @@
import { API_ETH_MOCK_ADDRESS } from '@aave/contract-helpers';
import { USD_DECIMALS, valueToBigNumber } from '@aave/math-utils';
import { Trans } from '@lingui/macro';
-import { Box, Typography, useMediaQuery, useTheme } from '@mui/material';
+import { Alert, Box, Typography, useMediaQuery, useTheme } from '@mui/material';
import { BigNumber } from 'bignumber.js';
import { Fragment, useState } from 'react';
import { AssetCategoryMultiSelect } from 'src/components/AssetCategoryMultiselect';
import { ListColumn } from 'src/components/lists/ListColumn';
import { ListHeaderTitle } from 'src/components/lists/ListHeaderTitle';
import { ListHeaderWrapper } from 'src/components/lists/ListHeaderWrapper';
-import { Warning } from 'src/components/primitives/Warning';
import { isFunSupplyAsset } from 'src/components/transactions/FunCheckout/funSupplyAssets';
import { AssetCapsProvider } from 'src/hooks/useAssetCaps';
import { useCoingeckoCategories } from 'src/hooks/useCoinGeckoCategories';
@@ -292,7 +291,7 @@ export const SupplyAssetsList = () => {
width: '100%',
alignItems: 'center',
justifyContent: 'space-between',
- mr: 2,
+ mr: '0.62rem',
}}
>
@@ -329,35 +328,35 @@ export const SupplyAssetsList = () => {
)}
{user?.isInIsolationMode ? (
-
+
Collateral usage is limited because of isolation mode.{' '}
Learn More
-
+
) : (
filteredSupplyReserves.length === 0 &&
!supplyDisabled &&
(isTestnet ? (
-
+ Your {networkName} wallet is empty. Get free test assets at {' '}
-
+
{networkName} Faucet
-
+
) : (
))
)}
{supplyDisabled && (
-
+
We couldn't find any assets related to your search. Try again with a
different category.
-
+
)}
diff --git a/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsListItem.tsx b/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsListItem.tsx
index 4f7023053a..9ea4564ce3 100644
--- a/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsListItem.tsx
+++ b/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsListItem.tsx
@@ -1,6 +1,5 @@
import { ProtocolAction } from '@aave/contract-helpers';
-import { SwitchHorizontalIcon } from '@heroicons/react/outline';
-import { EyeIcon } from '@heroicons/react/solid';
+import { InformationCircleIcon } from '@heroicons/react/outline';
import { Trans } from '@lingui/macro';
import {
Box,
@@ -15,6 +14,8 @@ import {
} from '@mui/material';
import { useState } from 'react';
import { ContentWithTooltip } from 'src/components/ContentWithTooltip';
+import { DotsHorizontalIcon } from 'src/components/icons/DotsHorizontalIcon';
+import { SwapIcon } from 'src/components/icons/SwapIcon';
import { IncentivesCard } from 'src/components/incentives/IncentivesCard';
import { WrappedTokenTooltipContent } from 'src/components/infoTooltips/WrappedTokenToolTipContent';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
@@ -28,8 +29,10 @@ import { useAssetCaps } from 'src/hooks/useAssetCaps';
import { useModalContext } from 'src/hooks/useModal';
import { useWrappedTokens } from 'src/hooks/useWrappedTokens';
import { useRootStore } from 'src/store/root';
+import { iconButtonSx } from 'src/utils/buttonStyles';
import { DashboardReserve } from 'src/utils/dashboardSortUtils';
import { DASHBOARD } from 'src/utils/events';
+import { onAccent } from 'src/utils/figmaColors';
import { isFeatureEnabled } from 'src/utils/marketsAndNetworksConfig';
import { showExternalIncentivesTooltip } from 'src/utils/utils';
@@ -188,12 +191,7 @@ export const SupplyAssetsListItemDesktop = ({
justifyContent: 'center',
}}
>
-
+
@@ -237,7 +235,7 @@ export const SupplyAssetsListItemDesktop = ({
{debtCeiling.isMaxed ? (
-
+
) : (
- ...
+
@@ -432,7 +421,7 @@ export const SupplyAssetsListItemMobile = ({
incentives={aIncentivesData}
address={aTokenAddress}
symbol={symbol}
- variant="secondary14"
+ variant="h5"
market={currentMarket}
protocolAction={ProtocolAction.supply}
/>
@@ -463,7 +452,7 @@ export const SupplyAssetsListItemMobile = ({
fullWidth
/>
@@ -137,7 +137,7 @@ export const SupplyAssetsListMobileItem = ({
Supply & {
chainId: number;
- icon?: boolean;
sx?: SxProps;
};
-export function WalletEmptyInfo({ bridge, name, chainId, icon, sx }: WalletEmptyInfoProps) {
+export function WalletEmptyInfo({ bridge, name, chainId, sx }: WalletEmptyInfoProps) {
const network = [ChainId.avalanche].includes(chainId) ? 'Ethereum & Bitcoin' : 'Ethereum';
const trackEvent = useRootStore((store) => store.trackEvent);
return (
-
+
{bridge ? (
Your {name} wallet is empty. Purchase or transfer assets or use{' '}
@@ -40,6 +38,6 @@ export function WalletEmptyInfo({ bridge, name, chainId, icon, sx }: WalletEmpty
) : (
Your {name} wallet is empty. Purchase or transfer assets.
)}
-
+
);
}
diff --git a/src/modules/dev/ComponentShowcase/components/BannersSection/index.tsx b/src/modules/dev/ComponentShowcase/components/BannersSection/index.tsx
new file mode 100644
index 0000000000..b12df191af
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/BannersSection/index.tsx
@@ -0,0 +1,59 @@
+import { Box } from '@mui/material';
+import AnalyticsBanner from 'src/components/Analytics/AnalyticsConsent';
+import TopBarNotify from 'src/layouts/TopBarNotify';
+import { SavingsGhoBanner } from 'src/modules/markets/Gho/GhoBanner';
+
+import { Section } from '../Section';
+import { Specimen } from '../Specimen';
+
+// Mainnet-keyed mock campaign so the (preview) top-bar banner always has content to render,
+// independent of the store's current chain — TopBarNotify's preview mode also falls back to the
+// first campaign here if the active chain doesn't match.
+const PREVIEW_CAMPAIGNS = {
+ 1: {
+ notifyText: 'Aave V4 is now live on Ethereum mainnet.',
+ buttonText: 'Try it out',
+ buttonAction: {
+ type: 'url' as const,
+ value: 'https://pro.aave.com',
+ target: '_blank' as const,
+ },
+ bannerVersion: 'showcase',
+ },
+};
+
+/**
+ * Live gallery of the app's page-level banners. The two normally state-gated banners
+ * (`TopBarNotify`, `AnalyticsBanner`) are rendered in `preview` mode so they always show and don't
+ * read/mutate real dismissal or consent state; `SavingsGhoBanner` renders live off app context.
+ *
+ * The Specimen stage is a flex row (children size to content), so each banner is wrapped in a
+ * full-width box to make it span the stage the way it does its real page.
+ */
+export const BannersSection = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/ButtonsSection/index.tsx b/src/modules/dev/ComponentShowcase/components/ButtonsSection/index.tsx
new file mode 100644
index 0000000000..5808ca1ddb
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/ButtonsSection/index.tsx
@@ -0,0 +1,61 @@
+import { ChevronRightIcon, PlusIcon } from '@heroicons/react/outline';
+import { Button, SvgIcon } from '@mui/material';
+
+import { Section } from '../Section';
+import { Specimen } from '../Specimen';
+
+const VARIANTS = ['contained', 'tertiary', 'outlined', 'text'] as const;
+const SIZES = ['small', 'medium', 'large'] as const;
+const COLORS = ['primary', 'success', 'warning', 'error', 'secondary', 'info', 'inherit'] as const;
+
+export const ButtonsSection = () => (
+
+ {VARIANTS.map((variant) => (
+
+ {SIZES.map((size) => (
+
+ {size}
+
+ ))}
+
+ disabled
+
+
+
+
+ }
+ >
+ start icon
+
+
+
+
+ }
+ >
+ end icon
+
+
+ ))}
+
+ {(['contained', 'outlined'] as const).map((colorVariant) => (
+
+ {COLORS.map((color) => (
+
+ {color}
+
+ ))}
+
+ ))}
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/ColorSpecimen/index.tsx b/src/modules/dev/ComponentShowcase/components/ColorSpecimen/index.tsx
new file mode 100644
index 0000000000..1aa22785e4
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/ColorSpecimen/index.tsx
@@ -0,0 +1,95 @@
+import { Box, Typography } from '@mui/material';
+import { FigmaColorName, figVars } from 'src/utils/figmaColors';
+
+import { ColorRole } from '../../utils/catalog';
+import { Swatch } from '../Swatch';
+import { TokenHexLabel } from '../TokenHexLabel';
+
+const SPECIMEN_WIDTH = 220;
+
+/**
+ * Renders a color token the way it's meant to be used: `text` colors as text, `bg` as a surface,
+ * `border` as a divider inside a card, `shadow` as a drop shadow. Anything without a single obvious
+ * use (`swatch`) falls back to the plain color chip.
+ */
+export const ColorSpecimen = ({ role, name }: { role: ColorRole; name: FigmaColorName }) => {
+ const value = figVars[name];
+
+ if (role === 'text') {
+ // Framed on a surface so the sample sits with its label instead of floating on the page.
+ return (
+
+
+
+ The quick brown fox
+
+
+
+
+ );
+ }
+
+ if (role === 'bg') {
+ return (
+
+
+
+
+ );
+ }
+
+ if (role === 'border') {
+ // A two-row card split by the token, so it reads as a divider/separator.
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
+ if (role === 'shadow') {
+ return (
+
+
+
+
+ );
+ }
+
+ return ;
+};
diff --git a/src/modules/dev/ComponentShowcase/components/ColorsSection/index.tsx b/src/modules/dev/ComponentShowcase/components/ColorsSection/index.tsx
new file mode 100644
index 0000000000..a9163147a6
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/ColorsSection/index.tsx
@@ -0,0 +1,27 @@
+import { Box, Typography } from '@mui/material';
+
+import { COLOR_GROUPS } from '../../utils/catalog';
+import { ColorSpecimen } from '../ColorSpecimen';
+import { Section } from '../Section';
+
+export const ColorsSection = () => {
+ return (
+
+ {COLOR_GROUPS.map((group) => (
+
+
+ {group.title}
+
+
+ {group.names.map((name) => (
+
+ ))}
+
+
+ ))}
+
+ );
+};
diff --git a/src/modules/dev/ComponentShowcase/components/DataPrimitivesSection/index.tsx b/src/modules/dev/ComponentShowcase/components/DataPrimitivesSection/index.tsx
new file mode 100644
index 0000000000..5ccfb3cb63
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/DataPrimitivesSection/index.tsx
@@ -0,0 +1,87 @@
+import { Box } from '@mui/material';
+import { useState } from 'react';
+import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
+import { Row } from 'src/components/primitives/Row';
+import { TokenIcon } from 'src/components/primitives/TokenIcon';
+import {
+ DetailsCollateralLine,
+ DetailsCooldownLine,
+ DetailsHFLine,
+ DetailsNumberLine,
+ DetailsNumberLineWithSub,
+ DetailsTextLine,
+ DetailsUnwrapSwitch,
+ TxModalDetails,
+} from 'src/components/transactions/FlowCommons/TxModalDetails';
+import { CollateralType } from 'src/helpers/types';
+
+import { Section } from '../Section';
+import { Specimen } from '../Specimen';
+
+const TOKENS = ['AAVE', 'ETH', 'USDC', 'DAI', 'GHO', 'WBTC'];
+
+const UnwrapDemo = () => {
+ const [unwrapped, setUnwrapped] = useState(false);
+ return (
+
+ );
+};
+
+export const DataPrimitivesSection = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {TOKENS.map((symbol) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/EmptyStatesSection/index.tsx b/src/modules/dev/ComponentShowcase/components/EmptyStatesSection/index.tsx
new file mode 100644
index 0000000000..2648edb9a5
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/EmptyStatesSection/index.tsx
@@ -0,0 +1,69 @@
+import { Box, Typography } from '@mui/material';
+import { ReactNode } from 'react';
+import { ConnectWalletPaper } from 'src/components/ConnectWalletPaper';
+import { EmptyStatePaper } from 'src/components/EmptyStatePaper';
+import { SGhoLoggedOutPreview } from 'src/modules/sGho/SGhoLoggedOutPreview';
+import { YourInfoSidebar } from 'src/modules/sGho/YourInfoSidebar';
+
+import { Section } from '../Section';
+import { Specimen } from '../Specimen';
+
+// A titled sub-group within the section — a full-width band with a subheader and a wrapping
+// row of specimens (mirrors the grouping used in ColorsSection).
+const Group = ({ title, children }: { title: string; children: ReactNode }) => (
+
+
+ {title}
+
+
+ {children}
+
+
+);
+
+/**
+ * Live gallery of the app's empty-state UI (connect-wallet prompts and the no-positions
+ * placeholder). The showcase page never connects a wallet, so these render in their real
+ * disconnected/empty state off ambient app context — only literal/mock props are supplied.
+ *
+ * The Specimen stage is a flex row (children size to content), so wide/card-like empty states are
+ * wrapped in a full-width box to make them span the stage the way they do on their real page.
+ */
+export const EmptyStatesSection = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/FeedbackSection/index.tsx b/src/modules/dev/ComponentShowcase/components/FeedbackSection/index.tsx
new file mode 100644
index 0000000000..8974e625db
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/FeedbackSection/index.tsx
@@ -0,0 +1,58 @@
+import { Alert, Box, LinearProgress, Skeleton } from '@mui/material';
+import { CheckBadge } from 'src/components/primitives/CheckBadge';
+import { NoData } from 'src/components/primitives/NoData';
+
+import { Section } from '../Section';
+import { Specimen } from '../Specimen';
+
+const SEVERITIES = ['error', 'warning', 'info', 'success'] as const;
+
+export const FeedbackSection = () => (
+
+
+
+ {SEVERITIES.map((severity) => (
+
+ This is a {severity} alert. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
+ do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+ quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo.
+
+ ))}
+
+
+
+
+
+ {SEVERITIES.map((severity) => (
+
+ This is a {severity} alert. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
+ do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+ quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo.
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/FormControlsSection/index.tsx b/src/modules/dev/ComponentShowcase/components/FormControlsSection/index.tsx
new file mode 100644
index 0000000000..3c8d941494
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/FormControlsSection/index.tsx
@@ -0,0 +1,158 @@
+import {
+ Box,
+ Checkbox,
+ FormControlLabel,
+ InputAdornment,
+ MenuItem,
+ Radio,
+ RadioGroup,
+ Select,
+ SelectChangeEvent,
+ Switch,
+ TextField,
+} from '@mui/material';
+import { useState } from 'react';
+import { SearchInput } from 'src/components/SearchInput';
+
+import { Section } from '../Section';
+import { Specimen } from '../Specimen';
+
+// Showcase-only: render Select menus inline (disablePortal) so they inherit the showcase's LOCAL
+// `data-mui-color-scheme` box instead of escaping to and following the app's global scheme.
+const SELECT_MENU_PROPS = { disablePortal: true } as const;
+
+// Shared option set for the Select demos below.
+const NETWORKS = [
+ { value: 'ethereum', label: 'Ethereum' },
+ { value: 'base', label: 'Base' },
+ { value: 'arbitrum', label: 'Arbitrum' },
+];
+
+const networkMenuItems = NETWORKS.map((n) => (
+
+));
+
+const SelectDemo = () => {
+ const [value, setValue] = useState('ethereum');
+ return (
+
+ );
+};
+
+const MultiSelectDemo = () => {
+ const [value, setValue] = useState([]);
+ return (
+
+ );
+};
+
+export const FormControlsSection = () => (
+
+
+
+
+
+
+
+ USDC }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } label="With label" />
+
+
+
+
+
+
+
+
+
+
+
+ } label="Market" />
+ } label="Limit" />
+ } label="Disabled" disabled />
+
+
+
+
+
+
+
+
+ } label="With label" />
+
+
+
+ undefined}
+ wrapperSx={{ width: { xs: '100%', sm: 320 } }}
+ />
+
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/IconsSection/index.tsx b/src/modules/dev/ComponentShowcase/components/IconsSection/index.tsx
new file mode 100644
index 0000000000..d3bb16e0ed
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/IconsSection/index.tsx
@@ -0,0 +1,1957 @@
+import {
+ ArrowCircleRightIcon as OlArrowCircleRightIcon,
+ ArrowDownIcon as OlArrowDownIcon,
+ ArrowNarrowRightIcon as OlArrowNarrowRightIcon,
+ BookOpenIcon as OlBookOpenIcon,
+ CalendarIcon as OlCalendarIcon,
+ CheckCircleIcon as OlCheckCircleIcon,
+ CheckIcon as OlCheckIcon,
+ ChevronDownIcon as OlChevronDownIcon,
+ ChevronRightIcon as OlChevronRightIcon,
+ ChevronUpIcon as OlChevronUpIcon,
+ ClockIcon as OlClockIcon,
+ CreditCardIcon as OlCreditCardIcon,
+ DocumentDownloadIcon as OlDocumentDownloadIcon,
+ DuplicateIcon as OlDuplicateIcon,
+ ExclamationCircleIcon as OlExclamationCircleIcon,
+ ExclamationIcon as OlExclamationIcon,
+ ExternalLinkIcon as OlExternalLinkIcon,
+ InformationCircleIcon as OlInformationCircleIcon,
+ LogoutIcon as OlLogoutIcon,
+ MenuIcon as OlMenuIcon,
+ PlusIcon as OlPlusIcon,
+ QuestionMarkCircleIcon as OlQuestionMarkCircleIcon,
+ RefreshIcon as OlRefreshIcon,
+ SearchIcon as OlSearchIcon,
+ ShieldExclamationIcon as OlShieldExclamationIcon,
+ SwitchHorizontalIcon as OlSwitchHorizontalIcon,
+ SwitchVerticalIcon as OlSwitchVerticalIcon,
+ XIcon as OlXIcon,
+} from '@heroicons/react/outline';
+import {
+ ArrowLeftIcon as SoArrowLeftIcon,
+ ArrowNarrowRightIcon as SoArrowNarrowRightIcon,
+ CheckCircleIcon as SoCheckCircleIcon,
+ CheckIcon as SoCheckIcon,
+ ChevronRightIcon as SoChevronRightIcon,
+ CogIcon as SoCogIcon,
+ DotsHorizontalIcon as SoDotsHorizontalIcon,
+ DownloadIcon as SoDownloadIcon,
+ ExclamationIcon as SoExclamationIcon,
+ ExternalLinkIcon as SoExternalLinkIcon,
+ EyeIcon as SoEyeIcon,
+ LightningBoltIcon as SoLightningBoltIcon,
+ MinusSmIcon as SoMinusSmIcon,
+ QuestionMarkCircleIcon as SoQuestionMarkCircleIcon,
+ SearchIcon as SoSearchIcon,
+ XCircleIcon as SoXCircleIcon,
+} from '@heroicons/react/solid';
+import MuiAccessTime from '@mui/icons-material/AccessTime';
+import MuiAddOutlined from '@mui/icons-material/AddOutlined';
+import MuiArrowBackOutlined from '@mui/icons-material/ArrowBackOutlined';
+import MuiArrowDownward from '@mui/icons-material/ArrowDownward';
+import MuiArrowOutward from '@mui/icons-material/ArrowOutward';
+import MuiCheck from '@mui/icons-material/Check';
+import MuiCheckRounded from '@mui/icons-material/CheckRounded';
+import MuiClose from '@mui/icons-material/Close';
+import MuiContentCopy from '@mui/icons-material/ContentCopy';
+import MuiExpandMore from '@mui/icons-material/ExpandMore';
+import MuiGitHub from '@mui/icons-material/GitHub';
+import MuiInstagram from '@mui/icons-material/Instagram';
+import MuiKeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown';
+import MuiKeyboardArrowUp from '@mui/icons-material/KeyboardArrowUp';
+import MuiLaunch from '@mui/icons-material/Launch';
+import MuiLinkedIn from '@mui/icons-material/LinkedIn';
+import MuiLocalGasStation from '@mui/icons-material/LocalGasStation';
+import MuiMoreHoriz from '@mui/icons-material/MoreHoriz';
+import MuiSort from '@mui/icons-material/Sort';
+import MuiStart from '@mui/icons-material/Start';
+import MuiTwitter from '@mui/icons-material/Twitter';
+import MuiWarningAmber from '@mui/icons-material/WarningAmber';
+import MuiX from '@mui/icons-material/X';
+import { Box, Typography } from '@mui/material';
+import { DuneIcon, TikTok } from 'public/icons/footer/icons';
+import { ReactNode } from 'react';
+import { AaveLogo } from 'src/components/icons/AaveLogo';
+import { ArrowUpRightIcon } from 'src/components/icons/ArrowUpRightIcon';
+import { BridgeIcon } from 'src/components/icons/BridgeIcon';
+import { ChevronDownIcon } from 'src/components/icons/ChevronDownIcon';
+import { ChevronRightIcon } from 'src/components/icons/ChevronRightIcon';
+import { ChevronUpDownIcon } from 'src/components/icons/ChevronUpDownIcon';
+import { CloseIcon } from 'src/components/icons/CloseIcon';
+import { DotsHorizontalIcon } from 'src/components/icons/DotsHorizontalIcon';
+import { HeyIcon } from 'src/components/icons/HeyIcon';
+import { LensIcon } from 'src/components/icons/LensIcon';
+import { MinusIcon } from 'src/components/icons/MinusIcon';
+import { SearchIcon } from 'src/components/icons/SearchIcon';
+import { SettingsIcon } from 'src/components/icons/SettingsIcon';
+import { StarIcon } from 'src/components/icons/StarIcon';
+import { SwapIcon } from 'src/components/icons/SwapIcon';
+import { WalletIcon } from 'src/components/icons/WalletIcon';
+import { WalletOutlineIcon } from 'src/components/icons/WalletOutlineIcon';
+import { IncentivesIcon } from 'src/components/incentives/IncentivesButton';
+import { figVars } from 'src/utils/figmaColors';
+
+import { Section } from '../Section';
+
+// Auto-inventoried from the codebase: EVERY icon in the project. Icon components come first
+// (project glyphs, heroicons outline/solid, @mui/icons-material, and bespoke ones), followed by
+// every .svg asset under public/icons (token/network/wallet/flag logos, etc.) rendered as .
+// Third-party component names are aliased (Ol*/So*/Mui*) to avoid clashing with project icons.
+const SIZE = 28;
+const heroStyle = { width: SIZE, height: SIZE } as const;
+const assetImgStyle = { height: SIZE, width: 'auto', maxWidth: 56 } as const;
+
+type Entry = { name: string; node: ReactNode };
+type Group = { title: string; icons: Entry[] };
+
+const GROUPS: Group[] = [
+ {
+ title: 'Project — src/components/icons',
+ icons: [
+ { name: 'AaveLogo', node: },
+ { name: 'ArrowUpRightIcon', node: },
+ { name: 'BridgeIcon', node: },
+ { name: 'ChevronDownIcon', node: },
+ { name: 'ChevronRightIcon', node: },
+ { name: 'ChevronUpDownIcon', node: },
+ { name: 'CloseIcon', node: },
+ { name: 'DotsHorizontalIcon', node: },
+ { name: 'HeyIcon', node: },
+ { name: 'LensIcon', node: },
+ { name: 'MinusIcon', node: },
+ { name: 'SearchIcon', node: },
+ { name: 'SettingsIcon', node: },
+ { name: 'StarIcon', node: },
+ { name: 'SwapIcon', node: },
+ { name: 'WalletIcon', node: },
+ { name: 'WalletOutlineIcon', node: },
+ ],
+ },
+ {
+ title: 'Heroicons — outline',
+ icons: [
+ { name: 'ArrowCircleRightIcon', node: },
+ { name: 'ArrowDownIcon', node: },
+ { name: 'ArrowNarrowRightIcon', node: },
+ { name: 'BookOpenIcon', node: },
+ { name: 'CalendarIcon', node: },
+ { name: 'CheckCircleIcon', node: },
+ { name: 'CheckIcon', node: },
+ { name: 'ChevronDownIcon', node: },
+ { name: 'ChevronRightIcon', node: },
+ { name: 'ChevronUpIcon', node: },
+ { name: 'ClockIcon', node: },
+ { name: 'CreditCardIcon', node: },
+ { name: 'DocumentDownloadIcon', node: },
+ { name: 'DuplicateIcon', node: },
+ { name: 'ExclamationCircleIcon', node: },
+ { name: 'ExclamationIcon', node: },
+ { name: 'ExternalLinkIcon', node: },
+ { name: 'InformationCircleIcon', node: },
+ { name: 'LogoutIcon', node: },
+ { name: 'MenuIcon', node: },
+ { name: 'PlusIcon', node: },
+ { name: 'QuestionMarkCircleIcon', node: },
+ { name: 'RefreshIcon', node: },
+ { name: 'SearchIcon', node: },
+ { name: 'ShieldExclamationIcon', node: },
+ { name: 'SwitchHorizontalIcon', node: },
+ { name: 'SwitchVerticalIcon', node: },
+ { name: 'XIcon', node: },
+ ],
+ },
+ {
+ title: 'Heroicons — solid',
+ icons: [
+ { name: 'ArrowLeftIcon', node: },
+ { name: 'ArrowNarrowRightIcon', node: },
+ { name: 'CheckCircleIcon', node: },
+ { name: 'CheckIcon', node: },
+ { name: 'ChevronRightIcon', node: },
+ { name: 'CogIcon', node: },
+ { name: 'DotsHorizontalIcon', node: },
+ { name: 'DownloadIcon', node: },
+ { name: 'ExclamationIcon', node: },
+ { name: 'ExternalLinkIcon', node: },
+ { name: 'EyeIcon', node: },
+ { name: 'LightningBoltIcon', node: },
+ { name: 'MinusSmIcon', node: },
+ { name: 'QuestionMarkCircleIcon', node: },
+ { name: 'SearchIcon', node: },
+ { name: 'XCircleIcon', node: },
+ ],
+ },
+ {
+ title: 'MUI — @mui/icons-material',
+ icons: [
+ { name: 'AccessTime', node: },
+ { name: 'AddOutlined', node: },
+ { name: 'ArrowBackOutlined', node: },
+ { name: 'ArrowDownward', node: },
+ { name: 'ArrowOutward', node: },
+ { name: 'Check', node: },
+ { name: 'CheckRounded', node: },
+ { name: 'Close', node: },
+ { name: 'ContentCopy', node: },
+ { name: 'ExpandMore', node: },
+ { name: 'GitHub', node: },
+ { name: 'Instagram', node: },
+ { name: 'KeyboardArrowDown', node: },
+ { name: 'KeyboardArrowUp', node: },
+ { name: 'Launch', node: },
+ { name: 'LinkedIn', node: },
+ { name: 'LocalGasStation', node: },
+ { name: 'MoreHoriz', node: },
+ { name: 'Sort', node: },
+ { name: 'Start', node: },
+ { name: 'Twitter', node: },
+ { name: 'WarningAmber', node: },
+ { name: 'X', node: },
+ ],
+ },
+ {
+ title: 'Bespoke / brand components',
+ icons: [
+ { name: 'IncentivesIcon', node: },
+ { name: 'TikTok', node: },
+ { name: 'DuneIcon', node: },
+ ],
+ },
+ {
+ title: 'Assets — Icons (public/icons root)',
+ icons: [
+ {
+ name: 'discord',
+ node: ,
+ },
+ {
+ name: 'github',
+ node: ,
+ },
+ {
+ name: 'lens-logo',
+ node: (
+
+ ),
+ },
+ {
+ name: 'lenster',
+ node: ,
+ },
+ ],
+ },
+ {
+ title: 'Assets — Public root',
+ icons: [
+ {
+ name: 'aave-com-logo-header',
+ node: (
+
+ ),
+ },
+ {
+ name: 'aave-logo-purple',
+ node: (
+
+ ),
+ },
+ {
+ name: 'aave',
+ node: ,
+ },
+ {
+ name: 'aaveLogo',
+ node: ,
+ },
+ {
+ name: 'aave_santa',
+ node: ,
+ },
+ {
+ name: 'gho-group',
+ node: ,
+ },
+ {
+ name: 'illustration-green',
+ node: (
+
+ ),
+ },
+ {
+ name: 'lightningBoltGradient',
+ node: (
+
+ ),
+ },
+ {
+ name: 'resting-gho-hat-purple',
+ node: (
+
+ ),
+ },
+ {
+ name: 'sgho-banner',
+ node: (
+
+ ),
+ },
+ ],
+ },
+ {
+ title: 'Assets — Bridge',
+ icons: [
+ {
+ name: 'arbitrum',
+ node: (
+
+ ),
+ },
+ {
+ name: 'avalanche',
+ node: (
+
+ ),
+ },
+ {
+ name: 'optimism',
+ node: (
+
+ ),
+ },
+ {
+ name: 'polygon',
+ node: (
+
+ ),
+ },
+ ],
+ },
+ {
+ title: 'Assets — Flags',
+ icons: [
+ {
+ name: 'cn',
+ node: ,
+ },
+ {
+ name: 'el',
+ node: ,
+ },
+ {
+ name: 'en',
+ node: ,
+ },
+ {
+ name: 'es',
+ node: ,
+ },
+ {
+ name: 'fr',
+ node: ,
+ },
+ {
+ name: 'it',
+ node: ,
+ },
+ {
+ name: 'jp',
+ node: ,
+ },
+ {
+ name: 'kr',
+ node: ,
+ },
+ {
+ name: 'pr',
+ node: ,
+ },
+ {
+ name: 'tr',
+ node: ,
+ },
+ {
+ name: 'vt',
+ node: ,
+ },
+ ],
+ },
+ {
+ title: 'Assets — Health factor',
+ icons: [
+ {
+ name: 'HAL',
+ node: (
+
+ ),
+ },
+ {
+ name: 'HALHover',
+ node: (
+
+ ),
+ },
+ ],
+ },
+ {
+ title: 'Assets — Markets',
+ icons: [
+ {
+ name: 'aptos',
+ node: (
+
+ ),
+ },
+ {
+ name: 'etherfi',
+ node: (
+
+ ),
+ },
+ {
+ name: 'horizon',
+ node: (
+
+ ),
+ },
+ {
+ name: 'lido',
+ node: (
+
+ ),
+ },
+ {
+ name: 'linea',
+ node: (
+
+ ),
+ },
+ ],
+ },
+ {
+ title: 'Assets — Network logos',
+ icons: [
+ {
+ name: 'arbitrum',
+ node: (
+
+ ),
+ },
+ {
+ name: 'avalanche',
+ node: (
+
+ ),
+ },
+ {
+ name: 'base',
+ node: (
+
+ ),
+ },
+ {
+ name: 'binance',
+ node: (
+
+ ),
+ },
+ {
+ name: 'celo',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ethereum',
+ node: (
+
+ ),
+ },
+ {
+ name: 'gnosis',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ink',
+ node: (
+
+ ),
+ },
+ {
+ name: 'linea',
+ node: (
+
+ ),
+ },
+ {
+ name: 'mantle',
+ node: (
+
+ ),
+ },
+ {
+ name: 'megaeth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'metis',
+ node: (
+
+ ),
+ },
+ {
+ name: 'monad',
+ node: (
+
+ ),
+ },
+ {
+ name: 'optimism',
+ node: (
+
+ ),
+ },
+ {
+ name: 'plasma',
+ node: (
+
+ ),
+ },
+ {
+ name: 'polygon',
+ node: (
+
+ ),
+ },
+ {
+ name: 'scroll',
+ node: (
+
+ ),
+ },
+ {
+ name: 'soneium',
+ node: (
+
+ ),
+ },
+ {
+ name: 'sonic',
+ node: (
+
+ ),
+ },
+ {
+ name: 'xlayer',
+ node: (
+
+ ),
+ },
+ {
+ name: 'zksync',
+ node: (
+
+ ),
+ },
+ ],
+ },
+ {
+ title: 'Assets — On-ramp services',
+ icons: [
+ {
+ name: 'transak',
+ node: (
+
+ ),
+ },
+ ],
+ },
+ {
+ title: 'Assets — Other',
+ icons: [
+ {
+ name: 'aci-black',
+ node: (
+
+ ),
+ },
+ {
+ name: 'aci-white',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ethena',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ether.fi',
+ node: (
+
+ ),
+ },
+ {
+ name: 'kernel',
+ node: (
+
+ ),
+ },
+ {
+ name: 'merkl-black',
+ node: (
+
+ ),
+ },
+ {
+ name: 'merkl-white',
+ node: (
+
+ ),
+ },
+ {
+ name: 'spark',
+ node: (
+
+ ),
+ },
+ {
+ name: 'superfest',
+ node: (
+
+ ),
+ },
+ {
+ name: 'velora',
+ node: (
+
+ ),
+ },
+ {
+ name: 'zksync-ignite',
+ node: (
+
+ ),
+ },
+ ],
+ },
+ {
+ title: 'Assets — Staking',
+ icons: [
+ {
+ name: 'emission-staking-icon',
+ node: (
+
+ ),
+ },
+ {
+ name: 'trust-staking-icon',
+ node: (
+
+ ),
+ },
+ ],
+ },
+ {
+ title: 'Assets — Token logos',
+ icons: [
+ {
+ name: '1inch',
+ node: (
+
+ ),
+ },
+ {
+ name: 'aave-token-round',
+ node: (
+
+ ),
+ },
+ {
+ name: 'aave',
+ node: (
+
+ ),
+ },
+ {
+ name: 'acred',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ampl',
+ node: (
+
+ ),
+ },
+ {
+ name: 'arb',
+ node: ,
+ },
+ {
+ name: 'ausd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'avax',
+ node: (
+
+ ),
+ },
+ {
+ name: 'bal',
+ node: ,
+ },
+ {
+ name: 'bat',
+ node: ,
+ },
+ {
+ name: 'bnb',
+ node: ,
+ },
+ {
+ name: 'bpt',
+ node: ,
+ },
+ {
+ name: 'btc',
+ node: ,
+ },
+ {
+ name: 'buidl',
+ node: (
+
+ ),
+ },
+ {
+ name: 'busd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'cake',
+ node: (
+
+ ),
+ },
+ {
+ name: 'cbbtc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'cbeth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'celo',
+ node: (
+
+ ),
+ },
+ {
+ name: 'crv',
+ node: ,
+ },
+ {
+ name: 'crvusd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'cvx',
+ node: ,
+ },
+ {
+ name: 'dai',
+ node: ,
+ },
+ {
+ name: 'default',
+ node: (
+
+ ),
+ },
+ {
+ name: 'dpi',
+ node: ,
+ },
+ {
+ name: 'ebtc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'enj',
+ node: ,
+ },
+ {
+ name: 'ens',
+ node: ,
+ },
+ {
+ name: 'eth-round',
+ node: (
+
+ ),
+ },
+ {
+ name: 'eth',
+ node: ,
+ },
+ {
+ name: 'ethfi',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ethx',
+ node: (
+
+ ),
+ },
+ {
+ name: 'eura',
+ node: (
+
+ ),
+ },
+ {
+ name: 'eurc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'eure',
+ node: (
+
+ ),
+ },
+ {
+ name: 'eurm',
+ node: (
+
+ ),
+ },
+ {
+ name: 'eurs',
+ node: (
+
+ ),
+ },
+ {
+ name: 'eusde',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ezeth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'fbtc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'fdusd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'fei',
+ node: ,
+ },
+ {
+ name: 'frax',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ftm',
+ node: ,
+ },
+ {
+ name: 'fxs',
+ node: ,
+ },
+ {
+ name: 'gho',
+ node: ,
+ },
+ {
+ name: 'ghst',
+ node: (
+
+ ),
+ },
+ {
+ name: 'gno',
+ node: ,
+ },
+ {
+ name: 'gnosissdai',
+ node: (
+
+ ),
+ },
+ {
+ name: 'gusd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'jaaa',
+ node: (
+
+ ),
+ },
+ {
+ name: 'jeur',
+ node: (
+
+ ),
+ },
+ {
+ name: 'jtrsy',
+ node: (
+
+ ),
+ },
+ {
+ name: 'kbtc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'knc',
+ node: ,
+ },
+ {
+ name: 'kncl',
+ node: (
+
+ ),
+ },
+ {
+ name: 'lbtc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ldo',
+ node: ,
+ },
+ {
+ name: 'lend',
+ node: (
+
+ ),
+ },
+ {
+ name: 'link',
+ node: (
+
+ ),
+ },
+ {
+ name: 'lusd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'mai',
+ node: ,
+ },
+ {
+ name: 'mana',
+ node: (
+
+ ),
+ },
+ {
+ name: 'maticx',
+ node: (
+
+ ),
+ },
+ {
+ name: 'mega',
+ node: (
+
+ ),
+ },
+ {
+ name: 'megausd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'metis',
+ node: (
+
+ ),
+ },
+ {
+ name: 'mglobal',
+ node: (
+
+ ),
+ },
+ {
+ name: 'mkr',
+ node: ,
+ },
+ {
+ name: 'mnt',
+ node: ,
+ },
+ {
+ name: 'mon',
+ node: ,
+ },
+ {
+ name: 'musd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'okb',
+ node: ,
+ },
+ {
+ name: 'one',
+ node: ,
+ },
+ {
+ name: 'op',
+ node: ,
+ },
+ {
+ name: 'oseth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'pax',
+ node: ,
+ },
+ {
+ name: 'pol',
+ node: ,
+ },
+ {
+ name: 'bpt',
+ node: (
+
+ ),
+ },
+ {
+ name: 'guni',
+ node: (
+
+ ),
+ },
+ {
+ name: 'uni',
+ node: (
+
+ ),
+ },
+ {
+ name: 'pteusde',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ptsrusde',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ptsusde',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ptusde',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ptusdg',
+ node: (
+
+ ),
+ },
+ {
+ name: 'pyusd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'rai',
+ node: ,
+ },
+ {
+ name: 'ren',
+ node: ,
+ },
+ {
+ name: 'renfil',
+ node: (
+
+ ),
+ },
+ {
+ name: 'rep',
+ node: ,
+ },
+ {
+ name: 'reth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'rez',
+ node: ,
+ },
+ {
+ name: 'rlusd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'rpl',
+ node: ,
+ },
+ {
+ name: 'rseth',
+ node: (
+
+ ),
+ },
+ {
+ name: 's',
+ node: ,
+ },
+ {
+ name: 'savax',
+ node: (
+
+ ),
+ },
+ {
+ name: 'scr',
+ node: ,
+ },
+ {
+ name: 'sd',
+ node: ,
+ },
+ {
+ name: 'sdai',
+ node: (
+
+ ),
+ },
+ {
+ name: 'seth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'sgho',
+ node: (
+
+ ),
+ },
+ {
+ name: 'snx',
+ node: ,
+ },
+ {
+ name: 'solvbtc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'srusde',
+ node: (
+
+ ),
+ },
+ {
+ name: 'stcusd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'steth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'stg',
+ node: ,
+ },
+ {
+ name: 'stkaave',
+ node: (
+
+ ),
+ },
+ {
+ name: 'stkbpt',
+ node: (
+
+ ),
+ },
+ {
+ name: 'stkbptv2',
+ node: (
+
+ ),
+ },
+ {
+ name: 'stkgho',
+ node: (
+
+ ),
+ },
+ {
+ name: 'stmatic',
+ node: (
+
+ ),
+ },
+ {
+ name: 'sts',
+ node: ,
+ },
+ {
+ name: 'susd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'susde',
+ node: (
+
+ ),
+ },
+ {
+ name: 'sushi',
+ node: (
+
+ ),
+ },
+ {
+ name: 'syrupusd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'syrupusdc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'syrupusdt',
+ node: (
+
+ ),
+ },
+ {
+ name: 'tbtc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'teth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'tribe',
+ node: (
+
+ ),
+ },
+ {
+ name: 'tusd',
+ node: (
+
+ ),
+ },
+ {
+ name: 'tydroinkpoints',
+ node: (
+
+ ),
+ },
+ {
+ name: 'uni',
+ node: ,
+ },
+ {
+ name: 'uscc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'usdbc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'usdc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'usde',
+ node: (
+
+ ),
+ },
+ {
+ name: 'usdg',
+ node: (
+
+ ),
+ },
+ {
+ name: 'usdm',
+ node: (
+
+ ),
+ },
+ {
+ name: 'usdp',
+ node: (
+
+ ),
+ },
+ {
+ name: 'usds',
+ node: (
+
+ ),
+ },
+ {
+ name: 'usdt',
+ node: (
+
+ ),
+ },
+ {
+ name: 'usdt0',
+ node: (
+
+ ),
+ },
+ {
+ name: 'usdtb',
+ node: (
+
+ ),
+ },
+ {
+ name: 'usd₮0',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ust',
+ node: ,
+ },
+ {
+ name: 'ustb',
+ node: (
+
+ ),
+ },
+ {
+ name: 'usyc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'vbill',
+ node: (
+
+ ),
+ },
+ {
+ name: 'wavax',
+ node: (
+
+ ),
+ },
+ {
+ name: 'wbnb',
+ node: (
+
+ ),
+ },
+ {
+ name: 'wbtc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'weeth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'weth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'wftm',
+ node: (
+
+ ),
+ },
+ {
+ name: 'wmnt',
+ node: (
+
+ ),
+ },
+ {
+ name: 'wokb',
+ node: (
+
+ ),
+ },
+ {
+ name: 'wone',
+ node: (
+
+ ),
+ },
+ {
+ name: 'wpol',
+ node: (
+
+ ),
+ },
+ {
+ name: 'wrseth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'ws',
+ node: ,
+ },
+ {
+ name: 'wsteth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'wxdai',
+ node: (
+
+ ),
+ },
+ {
+ name: 'wxpl',
+ node: (
+
+ ),
+ },
+ {
+ name: 'xaut',
+ node: (
+
+ ),
+ },
+ {
+ name: 'xaut0',
+ node: (
+
+ ),
+ },
+ {
+ name: 'xbeth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'xbtc',
+ node: (
+
+ ),
+ },
+ {
+ name: 'xdai',
+ node: (
+
+ ),
+ },
+ {
+ name: 'xeth',
+ node: (
+
+ ),
+ },
+ {
+ name: 'xoksol',
+ node: (
+
+ ),
+ },
+ {
+ name: 'xpl',
+ node: ,
+ },
+ {
+ name: 'xsol',
+ node: (
+
+ ),
+ },
+ {
+ name: 'xsushi',
+ node: (
+
+ ),
+ },
+ {
+ name: 'yfi',
+ node: ,
+ },
+ {
+ name: 'zk',
+ node: ,
+ },
+ {
+ name: 'zrx',
+ node: ,
+ },
+ ],
+ },
+ {
+ title: 'Assets — Wallet icons',
+ icons: [
+ {
+ name: 'browserWallet',
+ node: (
+
+ ),
+ },
+ {
+ name: 'coinbase',
+ node: (
+
+ ),
+ },
+ {
+ name: 'frame',
+ node: (
+
+ ),
+ },
+ {
+ name: 'torus',
+ node: (
+
+ ),
+ },
+ {
+ name: 'walletConnect',
+ node: (
+
+ ),
+ },
+ ],
+ },
+];
+
+const IconTile = ({ name, node }: Entry) => (
+
+
+ {node}
+
+
+ {name}
+
+
+);
+
+export const IconsSection = () => (
+
+ {GROUPS.map((group) => (
+
+
+ {group.title} ({group.icons.length})
+
+
+ {group.icons.map((icon) => (
+
+ ))}
+
+
+ ))}
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/OverlaysSection/index.tsx b/src/modules/dev/ComponentShowcase/components/OverlaysSection/index.tsx
new file mode 100644
index 0000000000..91d9acbf1a
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/OverlaysSection/index.tsx
@@ -0,0 +1,82 @@
+import { Button, Menu, MenuItem, Typography } from '@mui/material';
+import { useState } from 'react';
+import { ContentWithTooltip } from 'src/components/ContentWithTooltip';
+import { BasicModal } from 'src/components/primitives/BasicModal';
+import { TextWithTooltip } from 'src/components/TextWithTooltip';
+
+import { Section } from '../Section';
+import { Specimen } from '../Specimen';
+
+const ModalDemo = () => {
+ const [open, setOpen] = useState(false);
+ return (
+ <>
+ setOpen(true)}>
+ Open modal
+
+
+
+ Modal title
+
+
+ BasicModal renders the Paper "modal" variant plus the themed backdrop and close
+ icon.
+
+
+ >
+ );
+};
+
+const MenuDemo = () => {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = Boolean(anchorEl);
+ const close = () => setAnchorEl(null);
+ return (
+ <>
+ {/* aria-expanded lets the pill trigger keep the open/active fill (pillStyle keys off it)
+ — MUI does not set it automatically for Menu triggers. */}
+ setAnchorEl(e.currentTarget)}
+ >
+ Open menu
+
+
+ >
+ );
+};
+
+export const OverlaysSection = () => (
+
+
+
+
+
+
+
+
+
+
+ Tooltip body content.}
+ >
+
+ Click me
+
+
+
+
+
+
+ Explanation of the metric goes here.
+
+
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/Section/index.tsx b/src/modules/dev/ComponentShowcase/components/Section/index.tsx
new file mode 100644
index 0000000000..b5f7333f72
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/Section/index.tsx
@@ -0,0 +1,37 @@
+import { Box, Typography } from '@mui/material';
+import { ReactNode } from 'react';
+import { figVars } from 'src/utils/figmaColors';
+
+interface SectionProps {
+ title: string;
+ description?: string;
+ children: ReactNode;
+}
+
+export const Section = ({ title, description, children }: SectionProps) => (
+
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ {children}
+
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/ShowcaseLayout/index.tsx b/src/modules/dev/ComponentShowcase/components/ShowcaseLayout/index.tsx
new file mode 100644
index 0000000000..49fc92ffed
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/ShowcaseLayout/index.tsx
@@ -0,0 +1,186 @@
+import { MenuIcon } from '@heroicons/react/outline';
+import {
+ Box,
+ Container,
+ Drawer,
+ IconButton,
+ PaletteMode,
+ SvgIcon,
+ Typography,
+} from '@mui/material';
+import { useColorScheme } from '@mui/material/styles';
+import { ReactNode, useState } from 'react';
+import { Link } from 'src/components/primitives/Link';
+
+import { SHOWCASE_GROUPS, SHOWCASE_SECTIONS } from '../../utils/registry';
+import { ThemeControl } from '../ThemeControl';
+
+interface ShowcaseLayoutProps {
+ activeSlug: string;
+ children: ReactNode;
+}
+
+const SIDEBAR_WIDTH = 248;
+
+export const ShowcaseLayout = ({ activeSlug, children }: ShowcaseLayoutProps) => {
+ const { mode: appMode, systemMode } = useColorScheme();
+
+ // The showcase runs on its OWN color scheme (seeded once from the app's) so switching it
+ // here re-declares the CSS variables for this subtree only — via the `data-mui-color-scheme`
+ // attribute — without flipping the whole app. Colors below use `sx` palette shortcuts,
+ // which resolve to CSS-var refs and therefore follow that attribute.
+ const [scheme, setScheme] = useState(
+ () => (appMode === 'system' ? systemMode : appMode) ?? 'light'
+ );
+ const [mobileNavOpen, setMobileNavOpen] = useState(false);
+
+ // Some sections (page-wide banners) opt out of the max-width content container.
+ const fullBleed = SHOWCASE_SECTIONS.find((s) => s.slug === activeSlug)?.fullBleed ?? false;
+
+ // One nav block, reused by the desktop sidebar and the mobile drawer.
+ const nav = (
+ <>
+
+ Components
+
+
+ {SHOWCASE_GROUPS.map((group, index) => (
+
+
+ {group.label}
+
+
+ {group.sections.map((section) => {
+ const active = section.slug === activeSlug;
+ return (
+ setMobileNavOpen(false)}
+ sx={{
+ display: 'block',
+ py: 1,
+ px: 2,
+ borderRadius: '8px',
+ color: active ? 'fg-1' : 'fg-2',
+ backgroundColor: active ? 'selected' : 'transparent',
+ '&:hover': {
+ color: 'fg-1',
+ backgroundColor: active ? 'selected' : 'button-hover',
+ },
+ }}
+ >
+ {section.label}
+
+ );
+ })}
+
+ ))}
+ >
+ );
+
+ return (
+
+ {/* Persistent sidebar (md and up) */}
+
+ {nav}
+
+
+ {/* Mobile drawer (below md). It portals to , outside the local-scheme wrapper above,
+ so the inner Box re-declares `data-mui-color-scheme` to keep it on the showcase theme. */}
+ setMobileNavOpen(false)}
+ sx={{ display: { xs: 'block', md: 'none' } }}
+ PaperProps={{ sx: { width: SIDEBAR_WIDTH, border: 'none' } }}
+ >
+
+ {nav}
+
+
+
+ {/* Content */}
+
+
+
+ setMobileNavOpen(true)}
+ sx={{ display: { xs: 'inline-flex', md: 'none' }, ml: -1 }}
+ >
+
+
+
+
+
+ Component showcase
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+};
diff --git a/src/modules/dev/ComponentShowcase/components/Specimen/index.tsx b/src/modules/dev/ComponentShowcase/components/Specimen/index.tsx
new file mode 100644
index 0000000000..164adce784
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/Specimen/index.tsx
@@ -0,0 +1,55 @@
+import { Box, Typography } from '@mui/material';
+import { ReactNode } from 'react';
+import { figVars } from 'src/utils/figmaColors';
+
+interface SpecimenProps {
+ label?: string;
+ fullWidth?: boolean;
+ // Cross-axis alignment of the controls on the stage row. Defaults to 'center' (best for
+ // toggles); pass 'flex-start' when items differ in height (e.g. a field with error text) so
+ // their top edges line up.
+ align?: 'center' | 'flex-start';
+ children: ReactNode;
+}
+
+// A single example: a small uppercase caption above the component, which sits on a
+// plain bordered "stage" (no fill) — matching the reference showcase.
+export const Specimen = ({ label, fullWidth, align = 'center', children }: SpecimenProps) => (
+
+ {label && (
+
+ {label}
+
+ )}
+
+ {children}
+
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/SurfacesSection/index.tsx b/src/modules/dev/ComponentShowcase/components/SurfacesSection/index.tsx
new file mode 100644
index 0000000000..413d9db8bd
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/SurfacesSection/index.tsx
@@ -0,0 +1,102 @@
+import { Box, Button, Paper, Typography } from '@mui/material';
+import { ListWrapper } from 'src/components/lists/ListWrapper';
+import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
+import { Row } from 'src/components/primitives/Row';
+import { ReserveOverviewBox } from 'src/components/ReserveOverviewBox';
+import { TopInfoPanel } from 'src/components/TopInfoPanel/TopInfoPanel';
+import { TopInfoPanelItem } from 'src/components/TopInfoPanel/TopInfoPanelItem';
+import { StakeActionBox } from 'src/modules/staking/StakeActionBox';
+
+import { Section } from '../Section';
+import { Specimen } from '../Specimen';
+
+const PAPER_VARIANTS = ['elevation', 'outlined', 'modal', 'card'] as const;
+
+export const SurfacesSection = () => (
+
+ {PAPER_VARIANTS.map((variant) => (
+
+
+ Paper {variant}
+
+
+ ))}
+
+
+
+ Card title}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Showcase panel
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ —}
+ dataCy="showcaseStake"
+ >
+
+ Stake
+
+
+
+
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/Swatch/index.tsx b/src/modules/dev/ComponentShowcase/components/Swatch/index.tsx
new file mode 100644
index 0000000000..e89bbcb2a8
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/Swatch/index.tsx
@@ -0,0 +1,35 @@
+import { Box } from '@mui/material';
+import { FigmaColorName, figVars } from 'src/utils/figmaColors';
+
+import { TokenHexLabel } from '../TokenHexLabel';
+
+interface SwatchProps {
+ name: FigmaColorName;
+ value: string;
+}
+
+// A checkerboard backing so alpha tokens (borders/shadows/scrim) stay visible.
+const CHECKERBOARD = {
+ backgroundImage:
+ 'linear-gradient(45deg, #c4c4c4 25%, transparent 25%), linear-gradient(-45deg, #c4c4c4 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #c4c4c4 75%), linear-gradient(-45deg, transparent 75%, #c4c4c4 75%)',
+ backgroundSize: '12px 12px',
+ backgroundPosition: '0 0, 0 6px, 6px -6px, -6px 0px',
+};
+
+export const Swatch = ({ name, value }: SwatchProps) => (
+
+
+
+
+
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/ThemeControl/index.tsx b/src/modules/dev/ComponentShowcase/components/ThemeControl/index.tsx
new file mode 100644
index 0000000000..b127d68324
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/ThemeControl/index.tsx
@@ -0,0 +1,30 @@
+import { Box, Button, PaletteMode, Typography } from '@mui/material';
+
+interface ThemeControlProps {
+ mode: PaletteMode;
+ onChange: (mode: PaletteMode) => void;
+}
+
+// Segmented Light/Dark control for the showcase's local theme. Uses the themed
+// Button variants so it reads natively in whichever mode is active.
+export const ThemeControl = ({ mode, onChange }: ThemeControlProps) => (
+
+
+ Theme
+
+
+ {(['light', 'dark'] as const).map((value) => (
+ onChange(value)}
+ sx={{ textTransform: 'capitalize' }}
+ >
+ {value}
+
+ ))}
+
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/TogglesBadgesSection/index.tsx b/src/modules/dev/ComponentShowcase/components/TogglesBadgesSection/index.tsx
new file mode 100644
index 0000000000..ad56a5de20
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/TogglesBadgesSection/index.tsx
@@ -0,0 +1,73 @@
+import { Box, Typography } from '@mui/material';
+import { useState } from 'react';
+import { BadgeSize, ExclamationBadge } from 'src/components/badges/ExclamationBadge';
+import { StyledTxModalToggleButton } from 'src/components/StyledToggleButton';
+import { StyledTxModalToggleGroup } from 'src/components/StyledToggleButtonGroup';
+
+import { Section } from '../Section';
+import { Specimen } from '../Specimen';
+
+interface ToggleOption {
+ value: string;
+ label: string;
+ disabled?: boolean;
+}
+
+const ToggleDemo = ({ options, initial }: { options: ToggleOption[]; initial: string }) => {
+ const [value, setValue] = useState(initial);
+ return (
+ v && setValue(v)}>
+ {options.map((o) => (
+
+ {o.label}
+
+ ))}
+
+ );
+};
+
+export const TogglesBadgesSection = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/src/modules/dev/ComponentShowcase/components/TokenHexLabel/index.tsx b/src/modules/dev/ComponentShowcase/components/TokenHexLabel/index.tsx
new file mode 100644
index 0000000000..8a42e3f38e
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/TokenHexLabel/index.tsx
@@ -0,0 +1,33 @@
+import { Typography } from '@mui/material';
+import { FigmaColorName } from 'src/utils/figmaColors';
+import { darkScheme } from 'src/utils/theme';
+
+import { HEX_TEXT, tokenHex } from '../../utils/tokenHex';
+
+// The scheme being rendered gets fg-2, the other fg-4 — so it's unambiguous which value the swatch
+// above is actually painting, while both stay readable for checking against Figma without toggling.
+// Hoisted: neither depends on props, and every swatch on the colors page renders two of them.
+const ACTIVE = { ...HEX_TEXT, display: 'block', color: 'fg-2', ...darkScheme({ color: 'fg-4' }) };
+const INACTIVE = { ...HEX_TEXT, display: 'block', color: 'fg-4', ...darkScheme({ color: 'fg-2' }) };
+
+/**
+ * Token name plus BOTH modes' source values. Shared by every specimen that labels a color token, so
+ * the showcase reports hexes one way instead of one way per component.
+ */
+export const TokenHexLabel = ({ name }: { name: FigmaColorName }) => {
+ const { light, dark } = tokenHex(name);
+
+ return (
+ <>
+
+ {name}
+
+
+ {light}
+
+
+ {dark}
+
+ >
+ );
+};
diff --git a/src/modules/dev/ComponentShowcase/components/TypographySection/index.tsx b/src/modules/dev/ComponentShowcase/components/TypographySection/index.tsx
new file mode 100644
index 0000000000..cdba8388b2
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/components/TypographySection/index.tsx
@@ -0,0 +1,17 @@
+import { Typography } from '@mui/material';
+
+import { TYPOGRAPHY_VARIANTS } from '../../utils/catalog';
+import { Section } from '../Section';
+import { Specimen } from '../Specimen';
+
+export const TypographySection = () => (
+
+ {TYPOGRAPHY_VARIANTS.map((variant) => (
+
+
+ The quick brown fox jumps over the lazy dog — 1234567890
+
+
+ ))}
+
+);
diff --git a/src/modules/dev/ComponentShowcase/utils/catalog.ts b/src/modules/dev/ComponentShowcase/utils/catalog.ts
new file mode 100644
index 0000000000..605694b6d0
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/utils/catalog.ts
@@ -0,0 +1,58 @@
+import { TypographyProps } from '@mui/material';
+import { FigmaColorName } from 'src/utils/figmaColors';
+
+type TypographyVariant = TypographyProps['variant'];
+
+// The typography variants enabled in the theme. The default MUI variants
+// (body1/body2/button/subtitle*/h6/overline) are disabled in theme.tsx, so
+// they are intentionally omitted here.
+export const TYPOGRAPHY_VARIANTS: TypographyVariant[] = [
+ 'display1',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'subheader1',
+ 'subheader2',
+ 'description',
+ 'caption',
+ 'secondary21',
+ 'secondary16',
+ 'main12',
+ 'buttonL',
+ 'buttonM',
+ 'buttonS',
+ 'helperText',
+];
+
+export type ColorRole = 'bg' | 'text' | 'border' | 'shadow' | 'swatch';
+
+// Figma color tokens grouped for the showcase. `role` decides how each group is presented — text
+// colors as text, backgrounds as surfaces, borders as dividers, shadows as shadows. Names are keys
+// of `figmaLight`, so each resolves in both light and dark via the flattened palette tokens.
+export const COLOR_GROUPS: { title: string; role: ColorRole; names: FigmaColorName[] }[] = [
+ {
+ title: 'Backgrounds',
+ role: 'bg',
+ names: ['bg-max', 'bg-1', 'bg-2', 'bg-3', 'bg-4', 'bg-5', 'bg-6'],
+ },
+ {
+ title: 'Foreground / Text',
+ role: 'text',
+ names: ['fg-max', 'fg-1', 'fg-2', 'fg-3', 'fg-4', 'fg-5'],
+ },
+ { title: 'Borders / dividers', role: 'border', names: ['border-0', 'border-1', 'border-2'] },
+ {
+ title: 'Shadows',
+ role: 'shadow',
+ names: [
+ 'shadow-low',
+ 'shadow-medium',
+ 'shadow-high',
+ 'shadow-strong',
+ 'shadow-stroke-1',
+ 'shadow-stroke-2',
+ ],
+ },
+];
diff --git a/src/modules/dev/ComponentShowcase/utils/registry.tsx b/src/modules/dev/ComponentShowcase/utils/registry.tsx
new file mode 100644
index 0000000000..5f40ebde88
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/utils/registry.tsx
@@ -0,0 +1,118 @@
+import dynamic from 'next/dynamic';
+import { ComponentType } from 'react';
+
+export interface ShowcaseSection {
+ slug: string;
+ label: string;
+ group: string;
+ Component: ComponentType;
+ /** Opt out of the layout's max-width content container (e.g. full-width page banners). */
+ fullBleed?: boolean;
+}
+
+// One entry per route (`/dev/components/`). Each section is lazily loaded so a
+// given page only ships its own section's code — keeping every page light.
+export const SHOWCASE_SECTIONS: ShowcaseSection[] = [
+ {
+ slug: 'colors',
+ label: 'Colors',
+ group: 'Foundations',
+ Component: dynamic(() => import('../components/ColorsSection').then((m) => m.ColorsSection)),
+ },
+ {
+ slug: 'typography',
+ label: 'Typography',
+ group: 'Foundations',
+ Component: dynamic(() =>
+ import('../components/TypographySection').then((m) => m.TypographySection)
+ ),
+ },
+ {
+ slug: 'icons',
+ label: 'Icons',
+ group: 'Foundations',
+ Component: dynamic(() => import('../components/IconsSection').then((m) => m.IconsSection)),
+ },
+ {
+ slug: 'buttons',
+ label: 'Buttons',
+ group: 'Inputs & actions',
+ Component: dynamic(() => import('../components/ButtonsSection').then((m) => m.ButtonsSection)),
+ },
+ {
+ slug: 'form-controls',
+ label: 'Form controls',
+ group: 'Inputs & actions',
+ Component: dynamic(() =>
+ import('../components/FormControlsSection').then((m) => m.FormControlsSection)
+ ),
+ },
+ {
+ slug: 'toggles-badges',
+ label: 'Toggles & badges',
+ group: 'Inputs & actions',
+ Component: dynamic(() =>
+ import('../components/TogglesBadgesSection').then((m) => m.TogglesBadgesSection)
+ ),
+ },
+ {
+ slug: 'feedback',
+ label: 'Feedback',
+ group: 'Feedback & overlays',
+ Component: dynamic(() =>
+ import('../components/FeedbackSection').then((m) => m.FeedbackSection)
+ ),
+ },
+ {
+ slug: 'overlays',
+ label: 'Overlays & modal',
+ group: 'Feedback & overlays',
+ Component: dynamic(() =>
+ import('../components/OverlaysSection').then((m) => m.OverlaysSection)
+ ),
+ },
+ {
+ slug: 'empty-states',
+ label: 'Empty states',
+ group: 'Feedback & overlays',
+ Component: dynamic(() =>
+ import('../components/EmptyStatesSection').then((m) => m.EmptyStatesSection)
+ ),
+ },
+ {
+ slug: 'surfaces',
+ label: 'Surfaces & cards',
+ group: 'Data & surfaces',
+ Component: dynamic(() =>
+ import('../components/SurfacesSection').then((m) => m.SurfacesSection)
+ ),
+ },
+ {
+ slug: 'data-primitives',
+ label: 'Data primitives',
+ group: 'Data & surfaces',
+ Component: dynamic(() =>
+ import('../components/DataPrimitivesSection').then((m) => m.DataPrimitivesSection)
+ ),
+ },
+ {
+ slug: 'banners',
+ label: 'Banners',
+ group: 'Data & surfaces',
+ fullBleed: true,
+ Component: dynamic(() => import('../components/BannersSection').then((m) => m.BannersSection)),
+ },
+];
+
+// Sections grouped for the sidebar, preserving declaration order.
+export const SHOWCASE_GROUPS = SHOWCASE_SECTIONS.reduce<
+ { label: string; sections: ShowcaseSection[] }[]
+>((groups, section) => {
+ const group = groups.find((g) => g.label === section.group);
+ if (group) {
+ group.sections.push(section);
+ } else {
+ groups.push({ label: section.group, sections: [section] });
+ }
+ return groups;
+}, []);
diff --git a/src/modules/dev/ComponentShowcase/utils/tokenHex.ts b/src/modules/dev/ComponentShowcase/utils/tokenHex.ts
new file mode 100644
index 0000000000..4199bdf355
--- /dev/null
+++ b/src/modules/dev/ComponentShowcase/utils/tokenHex.ts
@@ -0,0 +1,23 @@
+import { FigmaColorName, pickFigma } from 'src/utils/figmaColors';
+
+const LIGHT = pickFigma('light');
+const DARK = pickFigma('dark');
+
+/**
+ * Normalise a token's source value for display: hex uppercased to match how Figma shows it,
+ * `rgba()`/`hsl()` left exactly as authored. `figmaColors.ts` has mixed casing (`#18181B` next to
+ * `#0a0a0a`), so without this the same token can read differently on two showcase pages.
+ */
+const formatColor = (value: string) => (value.startsWith('#') ? value.toUpperCase() : value);
+
+/** Monospace so hex digits line up when scanning a column of tokens. */
+export const HEX_TEXT = {
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
+ letterSpacing: 0,
+} as const;
+
+/** Both modes' source values for a token, display-normalised. */
+export const tokenHex = (name: FigmaColorName) => ({
+ light: formatColor(LIGHT[name]),
+ dark: formatColor(DARK[name]),
+});
diff --git a/src/modules/faucet/FaucetAssetsList.tsx b/src/modules/faucet/FaucetAssetsList.tsx
index f952364f34..86ecce6224 100644
--- a/src/modules/faucet/FaucetAssetsList.tsx
+++ b/src/modules/faucet/FaucetAssetsList.tsx
@@ -114,7 +114,7 @@ export default function FaucetAssetsList() {
{reserve.name}
-
+
{reserve.symbol}
@@ -123,15 +123,11 @@ export default function FaucetAssetsList() {
{!downToXSM && (
-
+
)}
-
+
{!currentMarketData.addresses.FAUCET ? (
{
-
+ Faucet
diff --git a/src/modules/faucet/FaucetMobileItemLoader.tsx b/src/modules/faucet/FaucetMobileItemLoader.tsx
index b5dd5efefd..68e9c1fdc5 100644
--- a/src/modules/faucet/FaucetMobileItemLoader.tsx
+++ b/src/modules/faucet/FaucetMobileItemLoader.tsx
@@ -13,7 +13,7 @@ export const FaucetMobileItemLoader = () => {
-
+ Faucet
diff --git a/src/modules/governance/DelegatedInfoPanel.tsx b/src/modules/governance/DelegatedInfoPanel.tsx
index 85065eff8a..c64e048526 100644
--- a/src/modules/governance/DelegatedInfoPanel.tsx
+++ b/src/modules/governance/DelegatedInfoPanel.tsx
@@ -42,7 +42,7 @@ const DelegatedPower: React.FC = ({
return (
-
+ {title}
@@ -64,7 +64,7 @@ const DelegatedPower: React.FC = ({
value={Number(aavePower) + Number(stkAavePower) + Number(aAavePower)}
variant="subheader1"
/>
-
+
AAVE + stkAAVE + aAAVE
@@ -161,12 +161,12 @@ export const DelegatedInfoPanel = () => {
powers.aAavePropositionDelegatee !== constants.AddressZero;
return (
-
+ Delegated power
-
+
Use your AAVE, stkAAVE, or aAave balance to delegate your voting and proposition powers.
You will not be sending any tokens, only the rights to vote and propose changes to the
@@ -176,7 +176,7 @@ export const DelegatedInfoPanel = () => {
href="https://docs.aave.com/developers/v/2.0/protocol-governance/governance"
target="_blank"
variant="description"
- color="text.secondary"
+ color="fg-2"
sx={{ textDecoration: 'underline', ml: 1 }}
onClick={() => trackEvent(GENERAL.EXTERNAL_LINK, { link: 'Learn More Delegation' })}
>
@@ -184,7 +184,7 @@ export const DelegatedInfoPanel = () => {
{disableButton ? (
-
+ You have no AAVE/stkAAVE/aAave balance to delegate.
) : (
@@ -212,20 +212,13 @@ export const DelegatedInfoPanel = () => {
- openGovDelegation()}
- >
+ openGovDelegation()}>
Set up delegation
{showRevokeButton && (
openRevokeGovDelegation()}
>
diff --git a/src/modules/governance/FormattedProposalTime.tsx b/src/modules/governance/FormattedProposalTime.tsx
index 0a14b77256..905f6d7a2d 100644
--- a/src/modules/governance/FormattedProposalTime.tsx
+++ b/src/modules/governance/FormattedProposalTime.tsx
@@ -27,11 +27,7 @@ export function FormattedProposalTime({
if ([ProposalState.Pending].includes(state)) {
return (
-
+
{state}
starts
@@ -43,11 +39,7 @@ export function FormattedProposalTime({
if ([ProposalState.Active].includes(state)) {
return (
-
+
{state}
ends
@@ -67,11 +59,7 @@ export function FormattedProposalTime({
) {
return (
-
+
{state}
on
@@ -85,11 +73,7 @@ export function FormattedProposalTime({
const canBeExecuted = timestamp > executionTime;
return (
-
+
{canBeExecuted ? Expires : Can be executed}
diff --git a/src/modules/governance/GovernanceTopPanel.tsx b/src/modules/governance/GovernanceTopPanel.tsx
index 4444a46959..8adbeb9c2f 100644
--- a/src/modules/governance/GovernanceTopPanel.tsx
+++ b/src/modules/governance/GovernanceTopPanel.tsx
@@ -1,9 +1,9 @@
import { ChainId } from '@aave/contract-helpers';
-import { ExternalLinkIcon } from '@heroicons/react/outline';
import { Trans } from '@lingui/macro';
-import { Box, Button, SvgIcon, Typography, useMediaQuery, useTheme } from '@mui/material';
+import { Box, Button, Typography, useMediaQuery, useTheme } from '@mui/material';
import * as React from 'react';
import { ChainAvailabilityText } from 'src/components/ChainAvailabilityText';
+import { ArrowUpRightIcon } from 'src/components/icons/ArrowUpRightIcon';
import { Link } from 'src/components/primitives/Link';
import { useRootStore } from 'src/store/root';
import { GENERAL } from 'src/utils/events';
@@ -20,21 +20,17 @@ function ExternalLink({ text, href }: ExternalLinkProps) {
return (
trackEvent(GENERAL.EXTERNAL_LINK, { Link: text })}
+ endIcon={}
+ sx={{ minWidth: 'unset' }}
>
-
- {text}
-
-
-
-
+ {text}
);
}
@@ -51,16 +47,12 @@ export const GovernanceTopPanel = () => {
- {/* */}
-
+ Aave Governance
-
+
Aave is a fully decentralized, community governed protocol by the AAVE token-holders.
AAVE token-holders collectively discuss, propose, and vote on upgrades to the
@@ -70,7 +62,7 @@ export const GovernanceTopPanel = () => {
trackEvent(GENERAL.EXTERNAL_LINK, { Link: 'FAQ Docs Governance' })}
href="https://aave.com/docs/ecosystem/governance"
- sx={{ textDecoration: 'underline', color: '#8E92A3' }}
+ sx={{ textDecoration: 'underline', color: 'fg-3' }}
>
documentation
@@ -83,15 +75,15 @@ export const GovernanceTopPanel = () => {
sx={{
display: 'flex',
alignItems: 'center',
- gap: '16px',
+ gap: '0.5rem',
flexWrap: 'wrap',
maxWidth: 'sm',
}}
>
-
-
+
+
-
+
);
diff --git a/src/modules/governance/ProposalListHeader.tsx b/src/modules/governance/ProposalListHeader.tsx
index d6ef45e8bf..94b3d6ec8d 100644
--- a/src/modules/governance/ProposalListHeader.tsx
+++ b/src/modules/governance/ProposalListHeader.tsx
@@ -34,13 +34,16 @@ export const ProposalListHeaderDesktop: React.FC
}) => {
return (
<>
-
+ Proposals
-
- Filter
-
-
-
+
@@ -22,7 +22,7 @@ const HistoryRowItem = () => {
-
+
@@ -36,7 +36,7 @@ export const HistoryItemLoader = () => {
return (
<>
-
+
diff --git a/src/modules/history/HistoryMobileItemLoader.tsx b/src/modules/history/HistoryMobileItemLoader.tsx
index 75c92b7251..9d53f555a7 100644
--- a/src/modules/history/HistoryMobileItemLoader.tsx
+++ b/src/modules/history/HistoryMobileItemLoader.tsx
@@ -13,7 +13,7 @@ const HistoryMobileRowItem = () => {
-
+
@@ -27,7 +27,7 @@ export const HistoryMobileItemLoader = () => {
return (
<>
-
+
diff --git a/src/modules/history/HistoryWrapper.tsx b/src/modules/history/HistoryWrapper.tsx
index e12aa5521b..2ce0a92544 100644
--- a/src/modules/history/HistoryWrapper.tsx
+++ b/src/modules/history/HistoryWrapper.tsx
@@ -137,7 +137,7 @@ export const HistoryWrapper = () => {
Transactions
-
+ This list may not include all your swaps.
@@ -171,7 +171,7 @@ export const HistoryWrapper = () => {
-
+ .CSV
@@ -189,7 +189,7 @@ export const HistoryWrapper = () => {
-
+ .JSON
@@ -206,7 +206,7 @@ export const HistoryWrapper = () => {
.sort((a, b) => new Date(b[0]).getTime() - new Date(a[0]).getTime())
.map(([date, txns], groupIndex) => (
-
+
{date}
{txns.map((transaction: TransactionHistoryItemUnion, index: number) => {
@@ -234,17 +234,17 @@ export const HistoryWrapper = () => {
my: 24,
}}
>
-
+ Nothing found
-
+
We couldn't find any transactions related to your search. Try again with a
different asset name, or reset filters.
{
setSearchQuery('');
setFilterQuery([]);
@@ -266,7 +266,7 @@ export const HistoryWrapper = () => {
flex: 1,
}}
>
-
+
{currentMarket === 'proto_plasma_v3' ? (
Transaction history for Plasma not supported yet, coming soon.
) : (
diff --git a/src/modules/history/HistoryWrapperMobile.tsx b/src/modules/history/HistoryWrapperMobile.tsx
index e5ffc71a28..d1d6a8efdd 100644
--- a/src/modules/history/HistoryWrapperMobile.tsx
+++ b/src/modules/history/HistoryWrapperMobile.tsx
@@ -172,7 +172,7 @@ export const HistoryWrapperMobile = () => {
open={Boolean(menuAnchorEl)}
onClose={handleDownloadMenuClose}
>
-
+ Export data to
@@ -152,7 +153,7 @@ export const MigrationMarketCard: FC = ({
) : (
)}
-
+
{!loading && userSummaryAfterMigration ? (
diff --git a/src/modules/migration/MigrationMobileList.tsx b/src/modules/migration/MigrationMobileList.tsx
index 9b5aedeb50..74714b144d 100644
--- a/src/modules/migration/MigrationMobileList.tsx
+++ b/src/modules/migration/MigrationMobileList.tsx
@@ -34,6 +34,10 @@ export const MigrationMobileList = ({
return (
{titleComponent}
@@ -42,7 +46,7 @@ export const MigrationMobileList = ({
>
{(isAvailable || loading) && (
-
+
-
+
{numSelected}/{numAvailable} assets selected
diff --git a/src/modules/migration/MigrationSelectionBox.tsx b/src/modules/migration/MigrationSelectionBox.tsx
index c5508a75e0..8196cdb8f1 100644
--- a/src/modules/migration/MigrationSelectionBox.tsx
+++ b/src/modules/migration/MigrationSelectionBox.tsx
@@ -1,6 +1,7 @@
import { CheckIcon, MinusSmIcon } from '@heroicons/react/solid';
-import { Box, SvgIcon, useTheme } from '@mui/material';
+import { Box, SvgIcon } from '@mui/material';
import { ListHeaderTitle } from 'src/components/lists/ListHeaderTitle';
+import { figVars } from 'src/utils/figmaColors';
interface MigrationSelectionBoxProps {
allSelected: boolean;
@@ -15,10 +16,9 @@ export const MigrationSelectionBox = ({
onSelectAllClick,
disabled,
}: MigrationSelectionBoxProps) => {
- const theme = useTheme();
const selectionBoxStyle = {
- border: `2px solid ${theme.palette.text.secondary}`,
- background: theme.palette.text.secondary,
+ border: `2px solid ${figVars['fg-2']}`,
+ background: figVars['fg-2'],
width: 16,
height: 16,
borderRadius: '2px',
@@ -34,8 +34,8 @@ export const MigrationSelectionBox = ({
{allSelected ? (
-
+
) : numSelected !== 0 ? (
-
+
diff --git a/src/modules/migration/MigrationTopPanel.tsx b/src/modules/migration/MigrationTopPanel.tsx
index cc216f3117..d297fc8477 100644
--- a/src/modules/migration/MigrationTopPanel.tsx
+++ b/src/modules/migration/MigrationTopPanel.tsx
@@ -25,7 +25,7 @@ export const MigrationTopPanel = () => {
}}
>
{
if (!v3Price) return { v3Amount: undefined, v3TotalPrice: undefined };
@@ -33,34 +32,33 @@ export const StETHMigrationWarning: React.FC = ({
);
return (
-
-
-
- stETH tokens will be migrated to Wrapped stETH using Lido Protocol wrapper which leads to
- supply balance change after migration:{' '}
- {v3Amount ? (
- <>
-
- {' ('}
-
- {').'}
- >
- ) : (
-
- )}
- {' '}
-
-
+
+ stETH tokens will be migrated to Wrapped stETH using Lido Protocol wrapper which leads to
+ supply balance change after migration:{' '}
+ {v3Amount ? (
+ <>
+
+ {' ('}
+
+ {').'}
+ >
+ ) : (
+
+ )}
+ {' '}
+
);
};
diff --git a/src/modules/reserve-overview/AddTokenDropdown.tsx b/src/modules/reserve-overview/AddTokenDropdown.tsx
index 6bef75cc01..900566d453 100644
--- a/src/modules/reserve-overview/AddTokenDropdown.tsx
+++ b/src/modules/reserve-overview/AddTokenDropdown.tsx
@@ -1,19 +1,20 @@
import { Trans } from '@lingui/macro';
-import { Box, Menu, MenuItem, Typography } from '@mui/material';
+import { Box, Divider, Menu, MenuItem } from '@mui/material';
import * as React from 'react';
import { useEffect, useState } from 'react';
-import { CircleIcon } from 'src/components/CircleIcon';
-import { WalletIcon } from 'src/components/icons/WalletIcon';
-import { Base64Token, TokenIcon } from 'src/components/primitives/TokenIcon';
+import { WalletOutlineIcon } from 'src/components/icons/WalletOutlineIcon';
+import { Base64Token } from 'src/components/primitives/TokenIcon';
import { ReserveWithId } from 'src/hooks/app-data-provider/useAppDataProvider';
import { ERC20TokenType } from 'src/libs/web3-data-provider/Web3Provider';
import { useRootStore } from 'src/store/root';
import { RESERVE_DETAILS } from 'src/utils/events';
+import { ReserveHeaderIconButton } from './ReserveHeaderIconButton';
+import { MenuSectionLabel, TokenMenuItemContent } from './TokenMenuItems';
+
interface AddTokenDropdownProps {
poolReserve: ReserveWithId;
iconSymbol?: string;
- downToSM: boolean;
switchNetwork: (chainId: number) => Promise;
addERC20Token: (args: ERC20TokenType) => Promise;
currentChainId: number;
@@ -26,7 +27,6 @@ interface AddTokenDropdownProps {
export const AddTokenDropdown = ({
poolReserve,
iconSymbol,
- downToSM,
switchNetwork,
addERC20Token,
currentChainId,
@@ -81,9 +81,11 @@ export const AddTokenDropdown = ({
return (
<>
- {/* Load base64 token symbol for adding underlying and aTokens to wallet */}
+ {/* Hidden base64 image-generators for the add-to-wallet menu (they serialize the token SVG
+ for MetaMask). Absolutely positioned so these 0×0 nodes don't sit in the flex row as
+ gap-consuming siblings between the two header icon buttons. */}
{poolReserve?.underlyingToken.symbol && !/_/.test(poolReserve.underlyingToken.symbol) && (
- <>
+
)}
{isSGHO && }
- >
+
)}
-
-
- {
- trackEvent(RESERVE_DETAILS.ADD_TOKEN_TO_WALLET_DROPDOWN, {
- asset: poolReserve.underlyingToken.address,
- assetName: poolReserve.underlyingToken.name,
- });
- }}
- sx={{
- display: 'inline-flex',
- alignItems: 'center',
- '&:hover': {
- '.Wallet__icon': { opacity: '0 !important' },
- '.Wallet__iconHover': { opacity: '1 !important' },
- },
- cursor: 'pointer',
- }}
- >
-
-
-
+ ) => {
+ trackEvent(RESERVE_DETAILS.ADD_TOKEN_TO_WALLET_DROPDOWN, {
+ asset: poolReserve.underlyingToken.address,
+ assetName: poolReserve.underlyingToken.name,
+ });
+ handleClick(event);
+ }}
+ >
+
+
+
>
diff --git a/src/modules/reserve-overview/BorrowInfo.tsx b/src/modules/reserve-overview/BorrowInfo.tsx
index 6855b3d65e..d85c30dff7 100644
--- a/src/modules/reserve-overview/BorrowInfo.tsx
+++ b/src/modules/reserve-overview/BorrowInfo.tsx
@@ -14,6 +14,7 @@ import { TextWithTooltip } from 'src/components/TextWithTooltip';
import { ReserveWithId } from 'src/hooks/app-data-provider/useAppDataProvider';
import { AssetCapHookData } from 'src/hooks/useAssetCapsSDK';
import { GENERAL } from 'src/utils/events';
+import { figVars } from 'src/utils/figmaColors';
import { displayGhoForMintableMarket } from 'src/utils/ghoUtilities';
import { MarketDataType, NetworkConfig } from 'src/utils/marketsAndNetworksConfig';
@@ -75,15 +76,16 @@ export const BorrowInfo = ({
<>
Maximum amount available to borrow is{' '}
- {' '}
+ {' '}
{reserve.underlyingToken.symbol} (
).
@@ -122,25 +124,22 @@ export const BorrowInfo = ({
}
>
-
+ of
-
+
@@ -159,7 +158,7 @@ export const BorrowInfo = ({
}
>
-
+
)}
@@ -189,7 +188,7 @@ export const BorrowInfo = ({
incentives={borrowProtocolIncentives}
address={reserve.vToken.address}
symbol={reserve.underlyingToken.symbol}
- variant="main16"
+ variant="h4"
market={currentMarketData.market}
protocolAction={ProtocolAction.borrow}
inlineIncentives={true}
@@ -198,7 +197,7 @@ export const BorrowInfo = ({
{reserve.borrowInfo?.borrowCap.usd && reserve.borrowInfo?.borrowCap.usd !== '0' && (
Borrow cap}>
-
+
)}
diff --git a/src/modules/reserve-overview/Gho/GhoReserveConfiguration.tsx b/src/modules/reserve-overview/Gho/GhoReserveConfiguration.tsx
index 82f496e676..808bbd39c2 100644
--- a/src/modules/reserve-overview/Gho/GhoReserveConfiguration.tsx
+++ b/src/modules/reserve-overview/Gho/GhoReserveConfiguration.tsx
@@ -48,7 +48,7 @@ export const GhoReserveConfiguration: React.FC = (
>
= (
= (
{
@@ -12,8 +12,7 @@ export const GhoReserveTopDetails = ({ reserve }: { reserve: ReserveWithId }) =>
const theme = useTheme();
const downToSM = useMediaQuery(theme.breakpoints.down('sm'));
- const valueTypographyVariant = downToSM ? 'main16' : 'main21';
- const symbolsTypographyVariant = downToSM ? 'secondary16' : 'secondary21';
+ const valueTypographyVariant = downToSM ? 'h4' : 'h2';
const totalBorrowed = BigNumber.min(
valueToBigNumber(reserve.borrowInfo?.total.amount.value ?? '0'),
@@ -22,33 +21,24 @@ export const GhoReserveTopDetails = ({ reserve }: { reserve: ReserveWithId }) =>
return (
<>
- Total borrowed} loading={loading} hideIcon>
-
-
+ Total borrowed} loading={loading}>
+
+
- Maximum available to borrow}
- loading={loading}
- hideIcon
- >
+ Maximum available to borrow} loading={loading}>
-
+
- Price}>
+ Price}
+ sx={{ lineHeight: '0.875rem', letterSpacing: 0 }}
+ >
The Aave Protocol is programmed to always use the price of 1 GHO = $1. This is
different from using market pricing via oracles for other crypto assets. This creates
@@ -57,18 +47,9 @@ export const GhoReserveTopDetails = ({ reserve }: { reserve: ReserveWithId }) =>
}
loading={loading}
- hideIcon
>
-
-
-
-
+
+
>
);
};
diff --git a/src/modules/reserve-overview/Gho/SavingsGho.tsx b/src/modules/reserve-overview/Gho/SavingsGho.tsx
index 22adb4ba23..062fe92eee 100644
--- a/src/modules/reserve-overview/Gho/SavingsGho.tsx
+++ b/src/modules/reserve-overview/Gho/SavingsGho.tsx
@@ -86,11 +86,7 @@ export const SavingsGho = () => {
{stakeDataLoading && }
{!stakeDataLoading && stakeData && (
-
+
{' ('}
{
}
bottomLineComponent={
-
+ Instant
}
@@ -150,15 +146,15 @@ export const SavingsGho = () => {
pt: 2,
}}
>
-
+ Amount in cooldown
@@ -178,7 +174,7 @@ export const SavingsGho = () => {
Deposit
{stakeUserData.stakeTokenUserBalance !== '0' && (
- openSavingsGhoWithdraw()}>
+ openSavingsGhoWithdraw()}>
Withdraw
)}
diff --git a/src/modules/reserve-overview/ReserveActions.tsx b/src/modules/reserve-overview/ReserveActions.tsx
index d6ee4d9caa..66349f7042 100644
--- a/src/modules/reserve-overview/ReserveActions.tsx
+++ b/src/modules/reserve-overview/ReserveActions.tsx
@@ -1,12 +1,11 @@
import { API_ETH_MOCK_ADDRESS } from '@aave/contract-helpers';
import { BigNumberValue, USD_DECIMALS, valueToBigNumber } from '@aave/math-utils';
import { Trans } from '@lingui/macro';
-import { Box, Button, Divider, Paper, Skeleton, Stack, Typography, useTheme } from '@mui/material';
+import { Alert, Box, Button, Divider, Paper, Skeleton, Stack, Typography } from '@mui/material';
import React, { ReactNode, useState } from 'react';
import { WalletIcon } from 'src/components/icons/WalletIcon';
import { getMarketInfoById } from 'src/components/MarketSwitcher';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
-import { Warning } from 'src/components/primitives/Warning';
import { StyledTxModalToggleButton } from 'src/components/StyledToggleButton';
import { StyledTxModalToggleGroup } from 'src/components/StyledToggleButtonGroup';
import { FunSupplyButton } from 'src/components/transactions/FunCheckout/FunSupplyButton';
@@ -21,6 +20,7 @@ import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
import { BuyWithFiat } from 'src/modules/staking/BuyWithFiat';
import { useRootStore } from 'src/store/root';
import { GENERAL } from 'src/utils/events';
+import { figVars } from 'src/utils/figmaColors';
import {
assetCanBeBorrowedByUser,
getMaxAmountAvailableToBorrow,
@@ -185,20 +185,20 @@ export const ReserveActions = ({ reserve }: ReserveActionsProps) => {
const PauseWarning = () => {
return (
-
+ Because this asset is paused, no actions can be taken until further notice
-
+
);
};
const FrozenWarning = () => {
return (
-
+
Since this asset is frozen, the only available actions are withdraw and repay which can be
accessed from the Dashboard
-
+
);
};
@@ -243,7 +243,7 @@ const ActionsSkeleton = () => {
const PaperWrapper = ({ children }: { children: ReactNode }) => {
return (
-
+ Your info
@@ -255,12 +255,12 @@ const PaperWrapper = ({ children }: { children: ReactNode }) => {
const ConnectWallet = () => {
return (
-
+
<>
Your info
-
+ Please connect a wallet to view your personal information here.
@@ -316,8 +316,8 @@ const SupplyAction = ({
@@ -375,8 +375,8 @@ const BorrowAction = ({
@@ -415,11 +415,11 @@ const WrappedBaseAssetSelector = ({
sx={{ mb: 4 }}
>
- {assetSymbol}
+ {assetSymbol}
- {baseAssetSymbol}
+ {baseAssetSymbol}
);
@@ -434,8 +434,8 @@ interface ValueWithSymbolProps {
const ValueWithSymbol = ({ value, symbol, children }: ValueWithSymbolProps) => {
return (
-
-
+
+
{symbol}
{children}
@@ -449,26 +449,24 @@ interface WalletBalanceProps {
marketTitle: string;
}
export const WalletBalance = ({ balance, symbol, marketTitle }: WalletBalanceProps) => {
- const theme = useTheme();
-
return (
({
+ sx={{
width: '42px',
height: '42px',
- background: theme.palette.background.surface,
- border: `0.5px solid ${theme.palette.background.disabled}`,
+ background: figVars['bg-2'],
+ border: `0.5px solid ${figVars['bg-6']}`,
borderRadius: '12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
- })}
+ }}
>
-
+
-
+
Wallet balance
diff --git a/src/modules/reserve-overview/ReserveConfiguration.tsx b/src/modules/reserve-overview/ReserveConfiguration.tsx
index b56dbd095e..76b18d4fb8 100644
--- a/src/modules/reserve-overview/ReserveConfiguration.tsx
+++ b/src/modules/reserve-overview/ReserveConfiguration.tsx
@@ -1,12 +1,11 @@
import { AaveV2Ethereum } from '@aave-dao/aave-address-book';
import { ExternalLinkIcon } from '@heroicons/react/solid';
import { Trans } from '@lingui/macro';
-import { Box, Button, Divider, SvgIcon } from '@mui/material';
+import { Alert, Box, Button, Divider, SvgIcon } from '@mui/material';
import { getFrozenProposalLink } from 'src/components/infoTooltips/FrozenTooltip';
import { PausedTooltipText } from 'src/components/infoTooltips/PausedTooltip';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
import { Link } from 'src/components/primitives/Link';
-import { Warning } from 'src/components/primitives/Warning';
import { AMPLWarning } from 'src/components/Warnings/AMPLWarning';
import { BorrowDisabledWarning } from 'src/components/Warnings/BorrowDisabledWarning';
import {
@@ -62,7 +61,7 @@ export const ReserveConfiguration: React.FC = ({ rese
<>
{reserve.isFrozen && !offboardingDiscussion ? (
-
+
This asset is frozen due to an Aave community decision.{' '}
= ({ rese
More details
-
+
) : offboardingDiscussion ? (
-
+
-
+
) : (
reserve.underlyingToken.symbol == 'AMPL' && (
-
+
-
+
)
)}
{reserve.isPaused ? (
reserve.underlyingToken.symbol === 'MAI' ? (
-
+
MAI has been paused due to a community decision. Supply, borrows and repays are
impacted.{' '}
@@ -103,11 +102,11 @@ export const ReserveConfiguration: React.FC = ({ rese
More details
-
+
) : (
-
+
-
+
)
) : null}
@@ -134,12 +133,12 @@ export const ReserveConfiguration: React.FC = ({ rese
{reserve.borrowInfo?.borrowingState !== 'ENABLED' &&
!reserve.eModeInfo?.some((eMode) => eMode.canBeBorrowed) && (
-
+
-
+
)}
= ({ rese
@@ -204,7 +203,7 @@ export const ReserveConfiguration: React.FC = ({ rese
}
component={Link}
size="small"
- variant="outlined"
+ variant="tertiary"
sx={{ verticalAlign: 'top' }}
>
Interest rate strategy
diff --git a/src/modules/reserve-overview/ReserveConfigurationWrapper.tsx b/src/modules/reserve-overview/ReserveConfigurationWrapper.tsx
index 0a0a009e19..37bcc75029 100644
--- a/src/modules/reserve-overview/ReserveConfigurationWrapper.tsx
+++ b/src/modules/reserve-overview/ReserveConfigurationWrapper.tsx
@@ -27,7 +27,7 @@ export const ReserveConfigurationWrapper: React.FC =
});
return (
-
+ = ({ reserve }
@@ -73,7 +73,7 @@ export const ReserveEModePanel: React.FC = ({ reserve }
@@ -88,7 +88,7 @@ export const ReserveEModePanel: React.FC = ({ reserve }
@@ -96,7 +96,7 @@ export const ReserveEModePanel: React.FC = ({ reserve }
))}
-
+
E-Mode increases your LTV for a selected category of assets, meaning that when E-mode is
enabled, you will have higher borrowing power over assets of the same E-mode category
@@ -105,7 +105,7 @@ export const ReserveEModePanel: React.FC = ({ reserve }
href={ROUTES.dashboard}
sx={{ textDecoration: 'underline' }}
variant="caption"
- color="text.secondary"
+ color="fg-2"
onClick={() => {
trackEvent(RESERVE_DETAILS.GO_DASHBOARD_EMODE);
}}
@@ -117,7 +117,7 @@ export const ReserveEModePanel: React.FC = ({ reserve }
href="https://aave.com/help/borrowing/e-mode"
sx={{ textDecoration: 'underline' }}
variant="caption"
- color="text.secondary"
+ color="fg-2"
onClick={() => {
trackEvent(GENERAL.EXTERNAL_LINK, { Link: 'E-mode FAQ' });
}}
@@ -129,7 +129,7 @@ export const ReserveEModePanel: React.FC = ({ reserve }
href="https://github.com/aave/aave-v3-core/blob/master/techpaper/Aave_V3_Technical_Paper.pdf"
sx={{ textDecoration: 'underline' }}
variant="caption"
- color="text.secondary"
+ color="fg-2"
onClick={() => {
trackEvent(GENERAL.EXTERNAL_LINK, { Link: 'V3 Tech Paper' });
}}
diff --git a/src/modules/reserve-overview/ReserveFactorOverview.tsx b/src/modules/reserve-overview/ReserveFactorOverview.tsx
index 9937620893..cedfa8559a 100644
--- a/src/modules/reserve-overview/ReserveFactorOverview.tsx
+++ b/src/modules/reserve-overview/ReserveFactorOverview.tsx
@@ -58,7 +58,7 @@ export const ReserveFactorOverview = ({
/>
}
>
-
+
-
+ View contract
diff --git a/src/modules/reserve-overview/ReserveHeaderIconButton.tsx b/src/modules/reserve-overview/ReserveHeaderIconButton.tsx
new file mode 100644
index 0000000000..5e32c654f4
--- /dev/null
+++ b/src/modules/reserve-overview/ReserveHeaderIconButton.tsx
@@ -0,0 +1,53 @@
+import { Trans } from '@lingui/macro';
+import { Box, Typography } from '@mui/material';
+import { ReactNode } from 'react';
+import { DarkTooltip } from 'src/components/infoTooltips/DarkTooltip';
+import { figSurfaceShadow } from 'src/utils/figmaColors';
+
+interface ReserveHeaderIconButtonProps {
+ tooltipText: string;
+ /** Button diameter — 1.75rem next to the token name, 1.25rem beside the oracle price. */
+ size?: string;
+ children: ReactNode;
+}
+
+// Surface icon button for the reserve header affordances (token contracts / add-to-wallet /
+// oracle link): a bg-3 circle with the shared shadow-low-border-2 ring. The icon color is a
+// constant `fg-2` via `currentColor` (icon children only need `stroke="currentColor"`); hover
+// tints the circle background instead — one step down the ramp to bg-5.
+export const ReserveHeaderIconButton = ({
+ tooltipText,
+ size = '1.75rem',
+ children,
+}: ReserveHeaderIconButtonProps) => {
+ return (
+
+ {tooltipText}
+
+ }
+ >
+
+ {children}
+
+
+ );
+};
diff --git a/src/modules/reserve-overview/ReservePanels.tsx b/src/modules/reserve-overview/ReservePanels.tsx
index 70f359669e..57590e363d 100644
--- a/src/modules/reserve-overview/ReservePanels.tsx
+++ b/src/modules/reserve-overview/ReservePanels.tsx
@@ -1,5 +1,6 @@
import { Box, BoxProps, Typography, TypographyProps, useMediaQuery, useTheme } from '@mui/material';
import type { ReactNode } from 'react';
+import { figVars } from 'src/utils/figmaColors';
export const PanelRow: React.FC = (props) => (
= ({ title, children, className
position: 'absolute',
right: 4,
top: 'calc(50% - 17px)',
- borderRight: (theme) => `1px solid ${theme.palette.divider}`,
+ borderRight: `1px solid ${figVars['border-2']}`,
},
}
: {}),
}}
className={className}
>
-
+
{title}
reserve.underlyingAsset === underlyingAsset
) as ComputedReserveData;
- const valueTypographyVariant = downToSM ? 'main16' : 'main21';
- const symbolsTypographyVariant = downToSM ? 'secondary16' : 'secondary21';
-
- const iconStyling = {
- display: 'inline-flex',
- alignItems: 'center',
- color: '#A5A8B6',
- '&:hover': { color: '#F1F1F3' },
- cursor: 'pointer',
- };
+ const valueTypographyVariant = downToSM ? 'h4' : 'h2';
return (
<>
- Reserve Size} loading={loading} hideIcon>
+ Reserve size} loading={loading}>
-
+
- Available liquidity} loading={loading} hideIcon>
+ Available liquidity} loading={loading}>
-
+
- Utilization Rate} loading={loading} hideIcon>
+ Utilization rate} loading={loading}>
-
+
- Oracle price} loading={loading} hideIcon>
-
+ Oracle price} loading={loading}>
+
- {loading ? (
-
- ) : (
-
-
- trackEvent(GENERAL.EXTERNAL_LINK, {
- Link: 'Oracle Price',
- oracle: poolReserve?.priceOracle,
- assetName: poolReserve.name,
- asset: poolReserve.underlyingAsset,
- })
- }
- href={currentNetworkConfig.explorerLinkBuilder({
- address: poolReserve?.priceOracle,
- })}
- sx={iconStyling}
- >
-
-
-
-
-
- )}
+
+
+ trackEvent(GENERAL.EXTERNAL_LINK, {
+ Link: 'Oracle Price',
+ oracle: poolReserve?.priceOracle,
+ assetName: poolReserve.name,
+ asset: poolReserve.underlyingAsset,
+ })
+ }
+ href={currentNetworkConfig.explorerLinkBuilder({
+ address: poolReserve?.priceOracle,
+ })}
+ sx={{
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '100%',
+ height: '100%',
+ color: 'inherit',
+ }}
+ >
+
+
+
-
+
>
);
};
diff --git a/src/modules/reserve-overview/ReserveTopDetailsWrapper.tsx b/src/modules/reserve-overview/ReserveTopDetailsWrapper.tsx
index d627000594..5f63f58595 100644
--- a/src/modules/reserve-overview/ReserveTopDetailsWrapper.tsx
+++ b/src/modules/reserve-overview/ReserveTopDetailsWrapper.tsx
@@ -1,15 +1,5 @@
import { Trans } from '@lingui/macro';
-import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackOutlined';
-import {
- Box,
- Button,
- Divider,
- Skeleton,
- SvgIcon,
- Typography,
- useMediaQuery,
- useTheme,
-} from '@mui/material';
+import { Box, Skeleton, SvgIcon, Typography } from '@mui/material';
import { useRouter } from 'next/router';
import { getMarketInfoById, MarketLogo } from 'src/components/MarketSwitcher';
import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
@@ -19,7 +9,6 @@ import { displayGhoForMintableMarket } from 'src/utils/ghoUtilities';
import { useShallow } from 'zustand/shallow';
import { TopInfoPanel } from '../../components/TopInfoPanel/TopInfoPanel';
-import { TopInfoPanelItem } from '../../components/TopInfoPanel/TopInfoPanelItem';
import { useAppDataContext } from '../../hooks/app-data-provider/useAppDataProvider';
import { AddTokenDropdown } from './AddTokenDropdown';
import { GhoReserveTopDetails } from './Gho/GhoReserveTopDetails';
@@ -36,8 +25,6 @@ export const ReserveTopDetailsWrapper = ({ underlyingAsset }: ReserveTopDetailsP
const [currentMarket, currentChainId] = useRootStore(
useShallow((state) => [state.currentMarket, state.currentChainId])
);
-
- const { market, logo } = getMarketInfoById(currentMarket);
const {
addERC20Token,
switchNetwork,
@@ -45,8 +32,7 @@ export const ReserveTopDetailsWrapper = ({ underlyingAsset }: ReserveTopDetailsP
currentAccount,
} = useWeb3Context();
- const theme = useTheme();
- const downToSM = useMediaQuery(theme.breakpoints.down('sm'));
+ const { market, logo } = getMarketInfoById(currentMarket);
const poolReserve = supplyReserves.find(
(reserve) => reserve.underlyingToken.address.toLowerCase() === underlyingAsset?.toLowerCase()
@@ -65,18 +51,15 @@ export const ReserveTopDetailsWrapper = ({ underlyingAsset }: ReserveTopDetailsP
? iconSymbol
: poolReserve!.underlyingToken.symbol;
- const valueTypographyVariant = downToSM ? 'main16' : 'main21';
-
const ReserveIcon = () => {
return (
-
+
{loading ? (
-
+
) : (
)}
@@ -86,9 +69,11 @@ export const ReserveTopDetailsWrapper = ({ underlyingAsset }: ReserveTopDetailsP
const ReserveName = () => {
return loading ? (
-
+
) : (
- {poolReserve.underlyingToken.name}
+
+ {poolReserve.underlyingToken.name}
+
);
};
@@ -100,144 +85,118 @@ export const ReserveTopDetailsWrapper = ({ underlyingAsset }: ReserveTopDetailsP
return (
- {
+ // https://github.com/vercel/next.js/discussions/34980
+ if (!!history.state.idx) router.back();
+ else router.push('/markets');
+ }}
+ sx={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: '0.25rem',
+ width: 'fit-content',
+ mb: '1rem',
+ cursor: 'pointer',
+ color: 'fg-3',
+ '&:hover': { color: 'fg-1' },
+ }}
+ >
+
+
+
+
-
-
-
- }
- onClick={() => {
- // https://github.com/vercel/next.js/discussions/34980
- if (!!history.state.idx) router.back();
- else router.push('/markets');
- }}
- sx={{ mr: 3, mb: downToSM ? '24px' : '0' }}
- >
- Go Back
-
-
-
-
-
- {market.marketTitle} Market
-
- {market.v3 && (
- theme.palette.gradients.aaveGradient,
- }}
- >
- Version 3
-
- )}
-
-
-
- {downToSM && (
-
-
-
+ Back
+
+
+ }
+ >
+
+
+
+
+
+
+
{!loading && (
-
+
{poolReserve.underlyingToken.symbol}
)}
-
-
- {loading ? (
-
- ) : (
-
-
- {currentAccount && (
-
- )}
-
- )}
-
-
- )}
-
- }
- >
- {!downToSM && (
- <>
- {poolReserve.underlyingToken.symbol}}
- withoutIconWrapper
- icon={}
- loading={loading}
- >
-
-
-
-
-
- {currentAccount && (
-
+
- )}
-
+ {currentAccount && (
+
+ )}
+
+ )}
-
-
- >
- )}
- {isGho ? (
-
- ) : (
-
- )}
+
+
+ on
+
+
+
+ {market.marketTitle}
+
+
+
+
+
+
+ {isGho ? (
+
+ ) : (
+
+ )}
+
+
);
};
diff --git a/src/modules/reserve-overview/SupplyInfo.tsx b/src/modules/reserve-overview/SupplyInfo.tsx
index e111353d83..74903bb16d 100644
--- a/src/modules/reserve-overview/SupplyInfo.tsx
+++ b/src/modules/reserve-overview/SupplyInfo.tsx
@@ -1,7 +1,7 @@
import { ProtocolAction } from '@aave/contract-helpers';
import { valueToBigNumber } from '@aave/math-utils';
import { Trans } from '@lingui/macro';
-import { AlertTitle, Box, Typography } from '@mui/material';
+import { Alert, AlertTitle, Box, Typography } from '@mui/material';
import { CapsCircularStatus } from 'src/components/caps/CapsCircularStatus';
import { DebtCeilingStatus } from 'src/components/caps/DebtCeilingStatus';
import { mapAaveProtocolIncentives } from 'src/components/incentives/incentives.helper';
@@ -11,7 +11,6 @@ import { LiquidationThresholdTooltip } from 'src/components/infoTooltips/Liquida
import { MaxLTVTooltip } from 'src/components/infoTooltips/MaxLTVTooltip';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
import { Link } from 'src/components/primitives/Link';
-import { Warning } from 'src/components/primitives/Warning';
import { ReserveOverviewBox } from 'src/components/ReserveOverviewBox';
import { ReserveSubheader } from 'src/components/ReserveSubheader';
import { TextWithTooltip } from 'src/components/TextWithTooltip';
@@ -67,7 +66,7 @@ export const SupplyInfo = ({
valueToBigNumber(reserve.supplyInfo.supplyCap.amount.value).toNumber() -
valueToBigNumber(reserve.supplyInfo.total.value).toNumber()
}
- variant="secondary12"
+ variant="subheader2"
/>{' '}
{reserve.underlyingToken.symbol} (
).
@@ -114,26 +113,23 @@ export const SupplyInfo = ({
}
>
-
+ of
-
+ of
@@ -151,7 +147,7 @@ export const SupplyInfo = ({
}
>
-
+
)}
@@ -161,7 +157,7 @@ export const SupplyInfo = ({
incentives={supplyProtocolIncentives}
address={reserve.aToken.address}
symbol={reserve.underlyingToken.symbol}
- variant="main16"
+ variant="h4"
market={currentMarketData.market}
protocolAction={ProtocolAction.supply}
inlineIncentives={true}
@@ -184,19 +180,16 @@ export const SupplyInfo = ({
Collateral usage
-
-
+
+ Asset can only be used as collateral in isolation mode only.
-
-
- In Isolation mode you cannot supply other assets as collateral for borrowing. Assets
- used as collateral in Isolation mode can only be borrowed to a specific debt
- ceiling.{' '}
-
- Learn more
-
-
-
+
+ In Isolation mode you cannot supply other assets as collateral for borrowing. Assets
+ used as collateral in Isolation mode can only be borrowed to a specific debt ceiling.{' '}
+
+ Learn more
+
+
) : reserve.supplyInfo.liquidationThreshold.value !== '0' ? (
Collateral usage
-
+
This asset can only be used as collateral in E-Mode:{' '}
{reserve.eModeInfo
@@ -225,16 +218,16 @@ export const SupplyInfo = ({
.map((eMode) => replaceUnderscoresWithSpaces(eMode.label))
.join(', ')}
-
+
) : (
Collateral usage
-
+ Asset cannot be used as collateral.
-
+
)}
@@ -265,7 +258,7 @@ export const SupplyInfo = ({
@@ -289,7 +282,7 @@ export const SupplyInfo = ({
@@ -313,7 +306,7 @@ export const SupplyInfo = ({
@@ -331,7 +324,7 @@ export const SupplyInfo = ({
)}
{reserve.underlyingToken.symbol == 'stETH' && (
-
+ Staking Rewards
@@ -345,7 +338,7 @@ export const SupplyInfo = ({
>
Learn more
-
+
)}
diff --git a/src/modules/reserve-overview/TimeRangeSelector.tsx b/src/modules/reserve-overview/TimeRangeSelector.tsx
index 395443be05..9bfd2f2f4b 100644
--- a/src/modules/reserve-overview/TimeRangeSelector.tsx
+++ b/src/modules/reserve-overview/TimeRangeSelector.tsx
@@ -1,5 +1,7 @@
import { TimeWindow } from '@aave/react';
-import { SxProps, Theme, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
+import { SxProps, Theme, Typography } from '@mui/material';
+import { StyledTxModalToggleButton } from 'src/components/StyledToggleButton';
+import { StyledTxModalToggleGroup } from 'src/components/StyledToggleButtonGroup';
export const supportedTimeRangeOptions = ['1m', '3m', '6m', '1y'] as const;
@@ -52,47 +54,20 @@ export const TimeRangeSelector = ({
};
return (
-
- {timeRanges.map((interval) => {
- return (
- | undefined => ({
- '&.MuiToggleButtonGroup-grouped:not(.Mui-selected), &.MuiToggleButtonGroup-grouped&.Mui-disabled':
- {
- border: '0.5px solid transparent',
- backgroundColor: 'background.surface',
- color: 'action.disabled',
- },
- '&.MuiToggleButtonGroup-grouped&.Mui-selected': {
- borderRadius: '4px',
- border: `0.5px solid ${theme.palette.divider}`,
- boxShadow: '0px 2px 1px rgba(0, 0, 0, 0.05), 0px 0px 1px rgba(0, 0, 0, 0.25)',
- backgroundColor: 'background.paper',
- },
- ...props.sx?.button,
- })}
- >
- {formattedInterval(interval)}
-
- );
- })}
-
+ {timeRanges.map((interval) => (
+
+ {formattedInterval(interval)}
+
+ ))}
+
);
};
diff --git a/src/modules/reserve-overview/TokenLinkDropdown.tsx b/src/modules/reserve-overview/TokenLinkDropdown.tsx
index 15d71038ff..f083ce7fc1 100644
--- a/src/modules/reserve-overview/TokenLinkDropdown.tsx
+++ b/src/modules/reserve-overview/TokenLinkDropdown.tsx
@@ -1,20 +1,19 @@
-import { ExternalLinkIcon } from '@heroicons/react/outline';
import { Trans } from '@lingui/macro';
-import { Box, Menu, MenuItem, SvgIcon, Typography } from '@mui/material';
+import { Box, Divider, Menu, MenuItem } from '@mui/material';
import * as React from 'react';
import { useState } from 'react';
-import { CircleIcon } from 'src/components/CircleIcon';
-import { TokenIcon } from 'src/components/primitives/TokenIcon';
+import { ArrowUpRightIcon } from 'src/components/icons/ArrowUpRightIcon';
import { ReserveWithId } from 'src/hooks/app-data-provider/useAppDataProvider';
import { useRootStore } from 'src/store/root';
import { useShallow } from 'zustand/shallow';
import { RESERVE_DETAILS } from '../../utils/events';
+import { ReserveHeaderIconButton } from './ReserveHeaderIconButton';
+import { MenuSectionLabel, TokenMenuItemContent } from './TokenMenuItems';
interface TokenLinkDropdownProps {
poolReserve: ReserveWithId;
iconSymbol?: string;
- downToSM: boolean;
hideAToken?: boolean;
hideVariableDebtToken?: boolean;
}
@@ -22,7 +21,6 @@ interface TokenLinkDropdownProps {
export const TokenLinkDropdown = ({
poolReserve,
iconSymbol,
- downToSM,
hideAToken,
hideVariableDebtToken,
}: TokenLinkDropdownProps) => {
@@ -61,21 +59,9 @@ export const TokenLinkDropdown = ({
return (
<>
-
-
-
-
-
-
-
+
+
+
>
diff --git a/src/modules/reserve-overview/TokenMenuItems.tsx b/src/modules/reserve-overview/TokenMenuItems.tsx
new file mode 100644
index 0000000000..6637af89bb
--- /dev/null
+++ b/src/modules/reserve-overview/TokenMenuItems.tsx
@@ -0,0 +1,36 @@
+import { Box, ListItemIcon, Typography } from '@mui/material';
+import { ReactNode } from 'react';
+import { TokenIcon } from 'src/components/primitives/TokenIcon';
+
+/** Group heading inside the reserve token dropdowns; the 0.38rem inset aligns it with the rows. */
+export const MenuSectionLabel = ({ children }: { children: ReactNode }) => (
+
+
+ {children}
+
+
+);
+
+interface TokenMenuItemContentProps {
+ symbol: string;
+ label: ReactNode;
+ aToken?: boolean;
+ waToken?: boolean;
+}
+
+/** Icon + symbol row shared by the "view contracts" and "add to wallet" token dropdowns. */
+export const TokenMenuItemContent = ({
+ symbol,
+ label,
+ aToken,
+ waToken,
+}: TokenMenuItemContentProps) => (
+ <>
+
+
+
+
+ {label}
+
+ >
+);
diff --git a/src/modules/reserve-overview/graphs/ApyGraph.tsx b/src/modules/reserve-overview/graphs/ApyGraph.tsx
index 35a14591ac..86869d65a2 100644
--- a/src/modules/reserve-overview/graphs/ApyGraph.tsx
+++ b/src/modules/reserve-overview/graphs/ApyGraph.tsx
@@ -180,7 +180,7 @@ export const ApyGraph = withTooltip(
borderRadius: '99px',
}}
>
-
+
Avg {avgFormatted}%
@@ -303,11 +303,7 @@ export const ApyGraph = withTooltip(
left={tooltipLeft + 40}
style={theme.palette.mode === 'light' ? tooltipStyles : tooltipStylesDark}
>
-
+
{formatDate(getDate(tooltipData), selectedTimeRange)}
(
justifyContent="space-between"
alignItems="center"
>
-
+
{field.text}
-
+
{getData(tooltipData, field.name).toFixed(2)}%
@@ -382,7 +378,7 @@ export const PlaceholderChart = ({
-
+
No data available
diff --git a/src/modules/reserve-overview/graphs/ApyGraphContainer.tsx b/src/modules/reserve-overview/graphs/ApyGraphContainer.tsx
index 644f548bf5..19888fef1c 100644
--- a/src/modules/reserve-overview/graphs/ApyGraphContainer.tsx
+++ b/src/modules/reserve-overview/graphs/ApyGraphContainer.tsx
@@ -7,9 +7,10 @@ import {
useSupplyAPYHistory,
} from '@aave/react';
import { Trans } from '@lingui/macro';
-import { Box, CircularProgress, Typography } from '@mui/material';
+import { Box, CircularProgress, Typography, useTheme } from '@mui/material';
import { ParentSize } from '@visx/responsive';
import { useState } from 'react';
+import { pickFigma } from 'src/utils/figmaColors';
import { ApyGraph, FormattedReserveHistoryItem, PlaceholderChart } from './ApyGraph';
import { GraphLegend } from './GraphLegend';
@@ -42,6 +43,7 @@ type ApyGraphProps = {
export const SupplyApyGraph = ({ chain, underlyingToken, market }: ApyGraphProps) => {
const [selectedTimeRange, setSelectedTimeRange] = useState(TimeWindow.LastWeek);
+ const { palette } = useTheme();
const { data, loading, error } = useSupplyAPYHistory({
chainId: chainId(chain),
@@ -53,7 +55,7 @@ export const SupplyApyGraph = ({ chain, underlyingToken, market }: ApyGraphProps
return (
{
const [selectedTimeRange, setSelectedTimeRange] = useState(TimeWindow.LastWeek);
+ const { palette } = useTheme();
const { data, loading, error } = useBorrowAPYHistory({
chainId: chainId(chain),
@@ -76,7 +79,7 @@ export const BorrowApyGraph = ({ chain, underlyingToken, market }: ApyGraphProps
return (
-
+ Loading data...
diff --git a/src/modules/reserve-overview/graphs/GraphLegend.tsx b/src/modules/reserve-overview/graphs/GraphLegend.tsx
index 5a4679bd0e..7060b8765c 100644
--- a/src/modules/reserve-overview/graphs/GraphLegend.tsx
+++ b/src/modules/reserve-overview/graphs/GraphLegend.tsx
@@ -23,7 +23,7 @@ export function GraphLegend({
borderRadius: '50%',
}}
/>
-
+
{label.text}
diff --git a/src/modules/reserve-overview/graphs/InterestRateModelGraph.tsx b/src/modules/reserve-overview/graphs/InterestRateModelGraph.tsx
index e5224dccdf..8e229fafcc 100644
--- a/src/modules/reserve-overview/graphs/InterestRateModelGraph.tsx
+++ b/src/modules/reserve-overview/graphs/InterestRateModelGraph.tsx
@@ -378,7 +378,7 @@ export const InterestRateModelGraph = withTooltip(
parseFloat(reserve.totalDebtUSD) >
0 ? (
<>
-
+ Borrow amount to reach {tooltipData.utilization}% utilization
@@ -394,7 +394,7 @@ export const InterestRateModelGraph = withTooltip(
>
) : (
<>
-
+
Repayment amount to reach {tooltipData.utilization}% utilization
@@ -417,10 +417,10 @@ export const InterestRateModelGraph = withTooltip(
{fields.map((field) => (
-
+
{field.text}
-
+
{tooltipValueAccessors[field.name](tooltipData).toFixed(2)}%
diff --git a/src/modules/reserve-overview/graphs/MeritApyGraph.tsx b/src/modules/reserve-overview/graphs/MeritApyGraph.tsx
index 40d848a32c..d2ab2b03f6 100644
--- a/src/modules/reserve-overview/graphs/MeritApyGraph.tsx
+++ b/src/modules/reserve-overview/graphs/MeritApyGraph.tsx
@@ -196,7 +196,7 @@ export const MeritApyGraph = withTooltip(
borderRadius: '99px',
}}
>
-
+
Avg {averageLine.avgFormatted}%
@@ -287,18 +287,14 @@ export const MeritApyGraph = withTooltip(
left={tooltipLeft + 40}
style={theme.palette.mode === 'light' ? tooltipStyles : tooltipStylesDark}
>
-
+
{formatDate(getDate(tooltipData))}
-
+
Merit APY
-
+
{getMeritApy(tooltipData).toFixed(2)}%
diff --git a/src/modules/reserve-overview/graphs/MeritApyGraphContainer.tsx b/src/modules/reserve-overview/graphs/MeritApyGraphContainer.tsx
index 5de95b4252..2c16bf4f1a 100644
--- a/src/modules/reserve-overview/graphs/MeritApyGraphContainer.tsx
+++ b/src/modules/reserve-overview/graphs/MeritApyGraphContainer.tsx
@@ -98,7 +98,7 @@ export const MeritApyGraphContainer = ({
}}
>
-
+ Loading data...
@@ -122,7 +122,7 @@ export const MeritApyGraphContainer = ({
Data couldn't be fetched, please reload graph.
{onRetry && (
-
+ Reload
)}
diff --git a/src/modules/sGho/SGhoCard.tsx b/src/modules/sGho/SGhoCard.tsx
index 12fb435a21..25df16cbd3 100644
--- a/src/modules/sGho/SGhoCard.tsx
+++ b/src/modules/sGho/SGhoCard.tsx
@@ -1,5 +1,5 @@
import { Trans } from '@lingui/macro';
-import { Box, Paper, Typography, useMediaQuery, useTheme } from '@mui/material';
+import { Box, Paper, Typography } from '@mui/material';
import { useWalletBalances } from 'src/hooks/app-data-provider/useWalletBalances';
import { useModalContext } from 'src/hooks/useModal';
import { useSavingsMarketData } from 'src/hooks/useSavingsMarketData';
@@ -10,8 +10,6 @@ import { SGhoDepositPanel } from './SGhoDepositPanel';
export const SGhoCard = () => {
const { chainId, marketKey } = useSavingsMarketData();
- const { breakpoints } = useTheme();
- const downToXsm = useMediaQuery(breakpoints.down('xsm'));
const { openSwitch, openSGhoVaultDeposit, openSGhoVaultWithdraw } = useModalContext();
const { vault, loading: vaultLoading } = useSGhoVaultContext();
@@ -42,10 +40,9 @@ export const SGhoCard = () => {
return (
({
+ sx={{
display: 'flex',
alignItems: { xs: 'stretch', xsm: 'center' },
justifyContent: 'space-between',
flexDirection: { xs: 'column', xsm: 'row' },
gap: 4,
borderRadius: { xs: '8px', xsm: '6px' },
- border: `1px solid ${theme.palette.divider}`,
+ border: `1px solid ${figVars['border-2']}`,
p: 4,
mb: 6,
- background: theme.palette.background.paper,
- })}
+ background: figVars['surface-elevated'],
+ }}
>
@@ -43,13 +44,13 @@ export const SGhoDepositRow = ({
sGHO
-
+ Available to deposit:
@@ -66,10 +67,10 @@ export const SGhoDepositRow = ({
}}
>
-
+ Staking APR
-
+
{hasGho ? (
diff --git a/src/modules/sGho/SGhoHeader.tsx b/src/modules/sGho/SGhoHeader.tsx
index ef6d450ab9..c2af1d9551 100644
--- a/src/modules/sGho/SGhoHeader.tsx
+++ b/src/modules/sGho/SGhoHeader.tsx
@@ -1,16 +1,16 @@
import { Trans } from '@lingui/macro';
-import { Box, Stack, Typography, useMediaQuery, useTheme } from '@mui/material';
+import { Typography, useMediaQuery, useTheme } from '@mui/material';
import NumberFlow from '@number-flow/react';
import { BigNumber } from 'bignumber.js';
import { useEffect, useState } from 'react';
+import { PageHeader } from 'src/components/PageHeader/PageHeader';
+import { PageHeaderStat } from 'src/components/PageHeader/PageHeaderStat';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
import { TokenIcon } from 'src/components/primitives/TokenIcon';
import { TextWithTooltip } from 'src/components/TextWithTooltip';
-import { TopInfoPanel } from 'src/components/TopInfoPanel/TopInfoPanel';
import { useSGhoVaultContext } from 'src/modules/sGho/SGhoVaultContext';
import { useRootStore } from 'src/store/root';
-
-import { TopInfoPanelItem } from '../../components/TopInfoPanel/TopInfoPanelItem';
+import { convertAprToApy } from 'src/utils/utils';
export const SGHOHeader: React.FC = () => {
const theme = useTheme();
@@ -23,16 +23,13 @@ export const SGHOHeader: React.FC = () => {
});
}, [trackEvent]);
- const upToLG = useMediaQuery(theme.breakpoints.up('lg'));
const downToSM = useMediaQuery(theme.breakpoints.down('sm'));
- const downToXSM = useMediaQuery(theme.breakpoints.down('xsm'));
- const valueTypographyVariant = downToSM ? 'main16' : 'main21';
- const symbolsTypographyVariant = downToSM ? 'secondary16' : 'secondary21';
- const symbolsColor = theme.palette.text.muted;
- const iconSize = valueTypographyVariant === 'main21' ? 20 : 16;
+ const valueTypographyVariant = downToSM ? 'h4' : 'h2';
+ const iconSize = valueTypographyVariant === 'h2' ? 20 : 16;
const apr = vault?.targetRate ? +vault.targetRate.value : 0;
+ const apyPercent = (convertAprToApy(apr) * 100).toFixed(2);
const totalDepositedUSD = vault?.totalAssets?.usd ?? '0';
const totalAssetsValue = vault?.totalAssets ? +vault.totalAssets.amount.value : 0;
@@ -54,77 +51,45 @@ export const SGHOHeader: React.FC = () => {
}, [weeklyRewardsEstimate]);
return (
-
-
-
-
- Savings GHO
-
-
-
-
-
- Deposit GHO into Savings GHO (sGHO) and earn{' '}
-
- {(apr * 100).toFixed(2)}%
- {' '}
- APR on your GHO holdings. There are no lockups, no rehypothecation, and you can
- withdraw anytime. Simply deposit GHO, receive sGHO tokens representing your balance,
- and watch your savings grow.
-
-
-
+ Savings GHO}
+ titleIcon={}
+ description={
+
+ Deposit GHO into Savings GHO (sGHO) and earn {apyPercent}% APY on your GHO holdings.
+
}
>
- Current APR} loading={loading}>
-
-
+ Current APR} loading={loading}>
+
+
- Total Deposited} loading={loading}>
+ Total Deposited} loading={loading}>
-
+
- Price} loading={loading}>
+ Price} loading={loading}>
-
+
-
- Weekly Rewards} variant="inherit">
-
- Estimated weekly rewards based on your current sGHO balance and APR. Actual rewards
- may vary depending on market conditions.
-
-
-
+ Weekly Rewards} variant="inherit">
+
+ Estimated weekly rewards based on your current sGHO balance and APR. Actual rewards
+ may vary depending on market conditions.
+
+
}
loading={loading}
>
@@ -167,11 +132,11 @@ export const SGHOHeader: React.FC = () => {
) : (
-
+
—
)}
-
-
+
+
);
};
diff --git a/src/modules/sGho/SGhoLoggedOutPreview.tsx b/src/modules/sGho/SGhoLoggedOutPreview.tsx
index 8347fe2d72..bf6a7c70f1 100644
--- a/src/modules/sGho/SGhoLoggedOutPreview.tsx
+++ b/src/modules/sGho/SGhoLoggedOutPreview.tsx
@@ -1,6 +1,7 @@
import { Trans } from '@lingui/macro';
import { Box, Button, Typography, useMediaQuery, useTheme } from '@mui/material';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
+import { figVars } from 'src/utils/figmaColors';
import { StakeActionBox } from '../staking/StakeActionBox';
@@ -24,28 +25,28 @@ export const SGhoLoggedOutPreview = ({ rate }: SGhoLoggedOutPreviewProps) => {
Deposit GHO
-
+ Deposit GHO and earn up to {(rate * 100).toFixed(2)}% APR ({
+ sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderRadius: { xs: '8px', xsm: '6px' },
- border: `1px solid ${theme.palette.divider}`,
+ border: `1px solid ${figVars['border-2']}`,
p: 4,
mb: 6,
- background: theme.palette.background.paper,
- })}
+ background: figVars['surface-elevated'],
+ }}
>
-
+ Staking APR
-
+ {
valueUSD="0"
dataCy="sghoBalanceBox_loggedOut"
bottomLineTitle={
-
+ Cooldown period
}
bottomLineComponent={
-
+ Instant
}
>
-
+ Withdraw
diff --git a/src/modules/sGho/SGhoSavingsRate.tsx b/src/modules/sGho/SGhoSavingsRate.tsx
index 8b34d53daf..bdefe0d4ea 100644
--- a/src/modules/sGho/SGhoSavingsRate.tsx
+++ b/src/modules/sGho/SGhoSavingsRate.tsx
@@ -27,29 +27,29 @@ export const SGhoSavingsRate = ({ totalDepositedUSD, rate }: SGhoSavingsRateProp
sx={{ mb: 4 }}
>
-
+ Total Deposited
-
+ APR
-
+
-
+ APY, fixed rate
-
+
diff --git a/src/modules/sGho/SGhoWithdrawRow.tsx b/src/modules/sGho/SGhoWithdrawRow.tsx
index 25d520ef47..54608e0d3d 100644
--- a/src/modules/sGho/SGhoWithdrawRow.tsx
+++ b/src/modules/sGho/SGhoWithdrawRow.tsx
@@ -20,18 +20,18 @@ export const SGhoWithdrawRow = ({ balance, balanceUSD, onWithdraw }: SGhoWithdra
valueUSD={balanceUSD}
dataCy="sghoBalanceBox"
bottomLineTitle={
-
+ Cooldown period
}
bottomLineComponent={
-
+ Instant
}
>
{
return (
{
Your info
-
+ Please connect a wallet to view your personal information here.
diff --git a/src/modules/staking/BuyWithFiat.tsx b/src/modules/staking/BuyWithFiat.tsx
index 51a2de843e..cb3d680080 100644
--- a/src/modules/staking/BuyWithFiat.tsx
+++ b/src/modules/staking/BuyWithFiat.tsx
@@ -34,7 +34,7 @@ export const BuyWithFiat = ({ cryptoSymbol, networkMarketName, funnel }: BuyWith
return isAvailable ? (
<>
(
diff --git a/src/modules/staking/GetABPToken.tsx b/src/modules/staking/GetABPToken.tsx
index 43ee4ae77c..a0ba9828df 100644
--- a/src/modules/staking/GetABPToken.tsx
+++ b/src/modules/staking/GetABPToken.tsx
@@ -26,7 +26,7 @@ export const GetABPToken = () => {
<>
{
diff --git a/src/modules/staking/GetGhoToken.tsx b/src/modules/staking/GetGhoToken.tsx
index 65df1456ab..a0d17fe09f 100644
--- a/src/modules/staking/GetGhoToken.tsx
+++ b/src/modules/staking/GetGhoToken.tsx
@@ -16,7 +16,7 @@ export const GetGhoToken = () => {
<>
= ({
// const distributionEnded = Date.now() / 1000 > Number(stakeData.distributionEnd);
return (
-
+ = ({
/>
-
+
Total deposited:{' '}
= ({
({
+ sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: { xs: 'flex-start', xsm: 'center' },
flexDirection: { xs: 'column', xsm: 'row' },
gap: { xs: 0, xsm: 2 },
borderRadius: { xs: 0, xsm: '6px' },
- border: { xs: 'unset', xsm: `1px solid ${theme.palette.divider}` },
+ border: { xs: 'unset', xsm: `1px solid ${figVars['border-2']}` },
p: { xs: 0, xsm: 4 },
background: {
xs: 'unset',
- xsm: theme.palette.background.paper,
+ xsm: figVars['surface-elevated'],
},
position: 'relative',
'&:after': {
@@ -185,9 +186,9 @@ export const GhoStakingPanel: React.FC = ({
left: '-16px',
width: 'calc(100% + 32px)',
height: '1px',
- bgcolor: { xs: 'divider', xsm: 'transparent' },
+ bgcolor: { xs: 'border-2', xsm: 'transparent' },
},
- })}
+ }}
>
= ({
/>
-
+
Total deposited{' '}
= ({
}}
>
-
+ Deposit APR
@@ -270,13 +264,10 @@ export const GhoStakingPanel: React.FC = ({
mb: { xs: 3, xsm: 0 },
}}
>
-
+ Max slashing
-
+ = ({
mb: { xs: 3, xsm: 0 },
}}
>
-
+ Wallet Balance
@@ -371,17 +359,17 @@ export const GhoStakingPanel: React.FC = ({
bottomLineComponent={
<>
{isCooldownActive && !isUnstakeWindowActive ? (
-
+
) : isUnstakeWindowActive ? (
-
+
) : (
-
+ Instant
)}
@@ -398,15 +386,15 @@ export const GhoStakingPanel: React.FC = ({
pt: 2,
}}
>
-
+ Amount in cooldown
@@ -419,7 +407,7 @@ export const GhoStakingPanel: React.FC = ({
{isUnstakeWindowActive && (
= ({
{availableToReactivateCooldown && (
+
Reactivate cooldown period to unstake{' '}
{Number(
@@ -445,7 +429,7 @@ export const GhoStakingPanel: React.FC = ({
}
>
= ({
{isCooldownActive && !isUnstakeWindowActive && (
= ({
{availableToReactivateCooldown && (
+
Reactivate cooldown period to unstake{' '}
{Number(
@@ -489,7 +469,7 @@ export const GhoStakingPanel: React.FC = ({
}
>
= ({
{!isCooldownActive && (
{
diff --git a/src/modules/staking/StakeActionBox.tsx b/src/modules/staking/StakeActionBox.tsx
index 9bac225c4b..7d97a7e8bb 100644
--- a/src/modules/staking/StakeActionBox.tsx
+++ b/src/modules/staking/StakeActionBox.tsx
@@ -1,5 +1,6 @@
import { Box, Typography } from '@mui/material';
import React, { ReactNode } from 'react';
+import { figVars } from 'src/utils/figmaColors';
import { FormattedNumber } from '../../components/primitives/FormattedNumber';
import { Row } from '../../components/primitives/Row';
@@ -29,11 +30,11 @@ export const StakeActionBox = ({
}: StakeActionBoxProps) => {
return (
({
+ sx={{
flex: 1,
display: 'flex',
borderRadius: '6px',
- border: `1px solid ${theme.palette.divider}`,
+ border: `1px solid ${figVars['border-2']}`,
position: 'relative',
'&:after': {
content: "''",
@@ -43,26 +44,26 @@ export const StakeActionBox = ({
bottom: -1,
left: -1,
right: -1,
- background: gradientBorder ? theme.palette.gradients.aaveGradient : 'transparent',
+ background: gradientBorder ? figVars['purple-1'] : 'transparent',
},
- })}
+ }}
>
({
+ sx={{
flex: 1,
p: 4,
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
borderRadius: '6px',
- background: theme.palette.background.paper,
+ background: figVars['surface-elevated'],
position: 'relative',
zIndex: 2,
- })}
+ }}
data-cy={dataCy}
>
-
+
{title}
@@ -70,28 +71,23 @@ export const StakeActionBox = ({
value={value}
visibleDecimals={2}
variant="secondary21"
- color={+value === 0 ? 'text.muted' : 'text.primary'}
+ color={+value === 0 ? 'fg-3' : 'fg-1'}
data-cy={`amountNative`}
/>
{children}
-
+
{bottomLineComponent}
{cooldownAmount}
diff --git a/src/modules/staking/StakingHeader.tsx b/src/modules/staking/StakingHeader.tsx
index 2d37e8649d..d403acb1fb 100644
--- a/src/modules/staking/StakingHeader.tsx
+++ b/src/modules/staking/StakingHeader.tsx
@@ -1,16 +1,8 @@
-import { ChainId } from '@aave/contract-helpers';
import { Trans } from '@lingui/macro';
-import { Box, Stack, Typography, useMediaQuery, useTheme } from '@mui/material';
-import { ChainAvailabilityText } from 'src/components/ChainAvailabilityText';
+import { useMediaQuery, useTheme } from '@mui/material';
+import { PageHeader } from 'src/components/PageHeader/PageHeader';
+import { PageHeaderStat } from 'src/components/PageHeader/PageHeaderStat';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
-import { Row } from 'src/components/primitives/Row';
-import { TextWithTooltip } from 'src/components/TextWithTooltip';
-import { TopInfoPanel } from 'src/components/TopInfoPanel/TopInfoPanel';
-import { useRootStore } from 'src/store/root';
-import { GENERAL } from 'src/utils/events';
-
-import { Link } from '../../components/primitives/Link';
-import { TopInfoPanelItem } from '../../components/TopInfoPanel/TopInfoPanelItem';
interface StakingHeaderProps {
tvl: {
@@ -22,109 +14,39 @@ interface StakingHeaderProps {
export const StakingHeader: React.FC = ({ tvl, stkEmission, loading }) => {
const theme = useTheme();
- const upToLG = useMediaQuery(theme.breakpoints.up('lg'));
const downToSM = useMediaQuery(theme.breakpoints.down('sm'));
- const downToXSM = useMediaQuery(theme.breakpoints.down('xsm'));
-
- const valueTypographyVariant = downToSM ? 'main16' : 'main21';
- const symbolsTypographyVariant = downToSM ? 'secondary16' : 'secondary21';
- const trackEvent = useRootStore((store) => store.trackEvent);
+ const valueVariant = downToSM ? 'h4' : 'h2';
const total = Object.values(tvl || {}).reduce((acc, item) => acc + item, 0);
- const TotalFundsTooltip = () => {
- return (
-
-
- {Object.entries(tvl)
- .sort((a, b) => b[1] - a[1])
- .map(([key, value]) => (
-
-
-
- ))}
-
-
- );
- };
-
return (
-
-
-
- {/* */}
-
- Safety Module
-
-
-
-
-
- The Safety Module has been upgraded to{' '}
-
- Umbrella
-
- , a new system that introduces automated slashing, aToken staking, and improved
- incentives design.
-
-
-
-
- AAVE and ABPT holders (Ethereum network only) can stake their assets in the Safety
- Module to add more security to the protocol and earn Safety Incentives. In the case of
- a shortfall event, your stake can be slashed to cover the deficit, providing an
- additional layer of protection for the protocol.
- {' '}
-
- trackEvent(GENERAL.EXTERNAL_LINK, {
- Link: 'Staking Risks',
- })
- }
- >
- Learn more about risks involved
-
-
-
+
+ The Safety Module has been upgraded to Umbrella, a new system that introduces automated
+ slashing, aToken staking, and improved incentives design.
+
}
>
-
- Funds in the Safety Module
-
-
- }
- loading={loading}
- >
+ Funds in the Safety Module} loading={loading}>
-
+
- Total emission per day} loading={loading}>
+ Total emission per day} loading={loading}>
-
-
+
+
);
};
diff --git a/src/modules/staking/StakingPanel.tsx b/src/modules/staking/StakingPanel.tsx
index c9657b9f10..ce4277e614 100644
--- a/src/modules/staking/StakingPanel.tsx
+++ b/src/modules/staking/StakingPanel.tsx
@@ -25,6 +25,7 @@ import { TextWithTooltip } from 'src/components/TextWithTooltip';
import { StakeTokenFormatted } from 'src/hooks/stake/useGeneralStakeUiData';
import { useCurrentTimestamp } from 'src/hooks/useCurrentTimestamp';
import { GENERAL } from 'src/utils/events';
+import { figVars } from 'src/utils/figmaColors';
import { StakeActionBox } from './StakeActionBox';
import { StakingPanelSkeleton } from './StakingPanelSkeleton';
@@ -122,7 +123,7 @@ export const StakingPanel: React.FC = ({
const distributionEnded = Date.now() / 1000 > Number(stakeData.distributionEnd);
return (
-
+ = ({
/>
-
+
Total staked:{' '}
= ({
({
+ sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: { xs: 'flex-start', xsm: 'center' },
flexDirection: { xs: 'column', xsm: 'row' },
gap: { xs: 0, xsm: 2 },
borderRadius: { xs: 0, xsm: '6px' },
- border: { xs: 'unset', xsm: `1px solid ${theme.palette.divider}` },
+ border: { xs: 'unset', xsm: `1px solid ${figVars['border-2']}` },
p: { xs: 0, xsm: 4 },
background: {
xs: 'unset',
- xsm: theme.palette.background.paper,
+ xsm: figVars['surface-elevated'],
},
position: 'relative',
'&:after': {
@@ -182,9 +183,9 @@ export const StakingPanel: React.FC = ({
left: '-16px',
width: 'calc(100% + 32px)',
height: '1px',
- bgcolor: { xs: 'divider', xsm: 'transparent' },
+ bgcolor: { xs: 'border-2', xsm: 'transparent' },
},
- })}
+ }}
>
= ({
/>
-
+
Total staked{' '}
= ({
}}
>
-
+ Staking APR
{distributionEnded && (
@@ -262,7 +256,7 @@ export const StakingPanel: React.FC = ({
href="https://governance.aave.com"
sx={{ textDecoration: 'underline' }}
variant="caption"
- color="text.secondary"
+ color="fg-2"
>
Learn more
@@ -276,7 +270,7 @@ export const StakingPanel: React.FC = ({
sx={{ mr: 2 }}
value={stakeData.stakeApyFormatted}
percent
- variant="secondary14"
+ variant="h5"
/>
@@ -289,13 +283,10 @@ export const StakingPanel: React.FC = ({
mb: { xs: 3, xsm: 0 },
}}
>
-
+ Max slashing
-
+ = ({
mb: { xs: 3, xsm: 0 },
}}
>
-
+ Wallet Balance
@@ -349,7 +337,7 @@ export const StakingPanel: React.FC = ({
>
= ({
bottomLineComponent={
<>
{isCooldownActive && !isUnstakeWindowActive ? (
-
+
) : isUnstakeWindowActive ? (
-
+
) : (
-
+
)}
@@ -443,15 +431,15 @@ export const StakingPanel: React.FC = ({
pt: 2,
}}
>
-
+ Amount in cooldown
@@ -464,7 +452,7 @@ export const StakingPanel: React.FC = ({
{isUnstakeWindowActive && (
= ({
{availableToReactivateCooldown && (
+
Reactivate cooldown period to unstake{' '}
{Number(
@@ -490,7 +474,7 @@ export const StakingPanel: React.FC = ({
}
>
= ({
{isCooldownActive && !isUnstakeWindowActive && (
= ({
{availableToReactivateCooldown && (
+
Reactivate cooldown period to unstake{' '}
{Number(
@@ -534,7 +514,7 @@ export const StakingPanel: React.FC = ({
}
>
= ({
{!isCooldownActive && (
= ({
}
>
@@ -582,6 +562,7 @@ export const StakingPanel: React.FC = ({
display: 'flex',
flexDirection: { sm: 'row', xs: 'column' },
justifyContent: 'space-between',
+ gap: '0.75rem',
}}
>
= ({
return (
({
+ sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: 'row',
borderRadius: '6px',
- border: `1px solid ${theme.palette.divider}`,
+ border: `1px solid ${figVars['border-2']}`,
p: 4,
- background: theme.palette.background.paper,
+ background: figVars['surface-elevated'],
width: '250px',
height: '68px',
margin: '0 auto',
@@ -61,7 +62,7 @@ export const StakingPanelNoWallet: React.FC = ({
height: '1px',
bgcolor: 'transparent',
},
- })}
+ }}
>
= ({
>
-
+
{stakedToken}
@@ -87,7 +88,7 @@ export const StakingPanelNoWallet: React.FC = ({
>
{stakedToken !== 'GHO' && (
-
+ Staking APR
@@ -96,14 +97,14 @@ export const StakingPanelNoWallet: React.FC = ({
)}
{stakedToken === 'GHO' && (
-
+ Incentives APR
diff --git a/src/modules/staking/StakingPanelSkeleton.tsx b/src/modules/staking/StakingPanelSkeleton.tsx
index b7f5156af7..86abc18efd 100644
--- a/src/modules/staking/StakingPanelSkeleton.tsx
+++ b/src/modules/staking/StakingPanelSkeleton.tsx
@@ -2,7 +2,7 @@ import { Paper, Skeleton, Stack } from '@mui/material';
export const StakingPanelSkeleton = () => {
return (
-
+
diff --git a/src/modules/stkGho/StkGhoCard.tsx b/src/modules/stkGho/StkGhoCard.tsx
index 829fa963e1..a7c1619236 100644
--- a/src/modules/stkGho/StkGhoCard.tsx
+++ b/src/modules/stkGho/StkGhoCard.tsx
@@ -1,7 +1,6 @@
import { StakeUIUserData } from '@aave/contract-helpers/dist/esm/V3-uiStakeDataProvider-contract/types';
import { Trans } from '@lingui/macro';
-import { Box, Paper, Typography, useMediaQuery, useTheme } from '@mui/material';
-import { Warning } from 'src/components/primitives/Warning';
+import { Alert, Box, Paper, Typography } from '@mui/material';
import { StakeTokenFormatted, useGeneralStakeUiData } from 'src/hooks/stake/useGeneralStakeUiData';
import { useUserStakeUiData } from 'src/hooks/stake/useUserStakeUiData';
import { useModalContext } from 'src/hooks/useModal';
@@ -16,8 +15,6 @@ export const StkGhoCard = () => {
const [trackEvent, currentMarketData] = useRootStore(
useShallow((store) => [store.trackEvent, store.currentMarketData])
);
- const { breakpoints } = useTheme();
- const downToXsm = useMediaQuery(breakpoints.down('xsm'));
const { data: stakeGeneralResult } = useGeneralStakeUiData(currentMarketData);
const { data: stakeUserResult } = useUserStakeUiData(currentMarketData);
@@ -34,10 +31,9 @@ export const StkGhoCard = () => {
return (
{
-
+ Rewards for legacy Savings GHO have ended. Migrate to continue earning.
-
+
{
openSwitch('', targetChainId);
@@ -56,11 +57,11 @@ export const StkGhoDepositRow = ({
cursor: meritIncentives ? 'pointer' : 'default',
}}
>
-
+ APR
-
+
{meritIncentives && }
@@ -68,18 +69,18 @@ export const StkGhoDepositRow = ({
return (
({
+ sx={{
display: 'flex',
alignItems: { xs: 'stretch', xsm: 'center' },
justifyContent: 'space-between',
flexDirection: { xs: 'column', xsm: 'row' },
gap: { xs: 4, xsm: 4 },
borderRadius: { xs: '8px', xsm: '6px' },
- border: `1px solid ${theme.palette.divider}`,
+ border: `1px solid ${figVars['border-2']}`,
p: 4,
mb: 6,
- background: theme.palette.background.paper,
- })}
+ background: figVars['surface-elevated'],
+ }}
>
@@ -88,13 +89,13 @@ export const StkGhoDepositRow = ({
stkGHO
-
+ Available to deposit:
diff --git a/src/modules/stkGho/StkGhoSavingsRate.tsx b/src/modules/stkGho/StkGhoSavingsRate.tsx
index 29b33163b2..eabc9f3501 100644
--- a/src/modules/stkGho/StkGhoSavingsRate.tsx
+++ b/src/modules/stkGho/StkGhoSavingsRate.tsx
@@ -42,22 +42,22 @@ export const StkGhoSavingsRate = ({ totalDepositedUSD }: StkGhoSavingsRateProps)
sx={{ mb: 4 }}
>
-
+ Total Deposited
-
+ APY
-
+
diff --git a/src/modules/stkGho/StkGhoWithdrawRow.tsx b/src/modules/stkGho/StkGhoWithdrawRow.tsx
index eae5dfefab..b803b97c5a 100644
--- a/src/modules/stkGho/StkGhoWithdrawRow.tsx
+++ b/src/modules/stkGho/StkGhoWithdrawRow.tsx
@@ -50,18 +50,18 @@ export const StkGhoWithdrawRow = ({
valueUSD={stakedUSD}
dataCy={`stakedBox_${stakedToken}`}
bottomLineTitle={
-
+ Cooldown period
}
bottomLineComponent={
-
+ Instant
}
>
{!isCooldownActive && !isUnstakeWindowActive ? (
<>
-
+
>
) : (
diff --git a/src/modules/umbrella/AmountStakedUnderlyingItem.tsx b/src/modules/umbrella/AmountStakedUnderlyingItem.tsx
index 3dbde29b5f..227fbffb27 100644
--- a/src/modules/umbrella/AmountStakedUnderlyingItem.tsx
+++ b/src/modules/umbrella/AmountStakedUnderlyingItem.tsx
@@ -6,13 +6,7 @@ import { useRootStore } from 'src/store/root';
import { usePreviewRedeem } from './hooks/usePreviewRedeem';
-export const AmountStakedUnderlyingItem = ({
- stakeData,
- isMobile,
-}: {
- stakeData: MergedStakeData;
- isMobile?: boolean;
-}) => {
+export const AmountStakedUnderlyingItem = ({ stakeData }: { stakeData: MergedStakeData }) => {
const currentMarketData = useRootStore((s) => s.currentMarketData);
const chainId = currentMarketData?.chainId;
@@ -33,13 +27,8 @@ export const AmountStakedUnderlyingItem = ({
const assetUnderlyingAmount = isGhoToken ? formattedGhoAmount : sharesEquivalentAssets;
return (
-
-
+
+
);
};
diff --git a/src/modules/umbrella/AvailableToClaimItem.tsx b/src/modules/umbrella/AvailableToClaimItem.tsx
index 86d384ba36..7132927020 100644
--- a/src/modules/umbrella/AvailableToClaimItem.tsx
+++ b/src/modules/umbrella/AvailableToClaimItem.tsx
@@ -7,13 +7,7 @@ import { ListValueColumn } from '../dashboard/lists/ListValueColumn';
import { AmountAvailableItem } from './helpers/AmountAvailableItem';
import { MultiIconWithTooltip } from './helpers/MultiIcon';
-export const AvailableToClaimItem = ({
- stakeData,
- isMobile,
-}: {
- stakeData: MergedStakeData;
- isMobile?: boolean;
-}) => {
+export const AvailableToClaimItem = ({ stakeData }: { stakeData: MergedStakeData }) => {
const icons = stakeData.formattedRewards.map((reward) => ({
src: reward.rewardTokenSymbol,
aToken: reward.aToken,
@@ -30,13 +24,7 @@ export const AvailableToClaimItem = ({
);
return (
-
+ {
return (
-
+ Rewards available to claim
diff --git a/src/modules/umbrella/AvailableToStakeItem.tsx b/src/modules/umbrella/AvailableToStakeItem.tsx
index d8be5de1f0..9353ed3517 100644
--- a/src/modules/umbrella/AvailableToStakeItem.tsx
+++ b/src/modules/umbrella/AvailableToStakeItem.tsx
@@ -7,13 +7,7 @@ import { MergedStakeData } from 'src/hooks/stake/useUmbrellaSummary';
import { AmountAvailableItem } from './helpers/AmountAvailableItem';
import { MultiIconWithTooltip } from './helpers/MultiIcon';
-export const AvailableToStakeItem = ({
- stakeData,
- isMobile,
-}: {
- stakeData: MergedStakeData;
- isMobile?: boolean;
-}) => {
+export const AvailableToStakeItem = ({ stakeData }: { stakeData: MergedStakeData }) => {
const {
stataTokenAssetBalance: underlyingWaTokenBalance,
aTokenBalanceAvailableToStake,
@@ -47,17 +41,12 @@ export const AvailableToStakeItem = ({
Number(aTokenBalanceAvailableToStake);
return (
-
+
{stakeData.underlyingIsStataToken ? (
-
+ Your balance of assets that are available to stake
diff --git a/src/modules/umbrella/StakeAssets/StakeAssetName.tsx b/src/modules/umbrella/StakeAssets/StakeAssetName.tsx
index 0f5c6f2113..6932377a3d 100644
--- a/src/modules/umbrella/StakeAssets/StakeAssetName.tsx
+++ b/src/modules/umbrella/StakeAssets/StakeAssetName.tsx
@@ -33,7 +33,7 @@ export const StakeAssetName = ({
-
+
Total staked:{' '}
+ Target liquidity
}
@@ -61,7 +61,7 @@ export const StakeAssetName = ({
-
+ Reward APY at target liquidity
diff --git a/src/modules/umbrella/StakeAssets/UmbrellaAssetsList.tsx b/src/modules/umbrella/StakeAssets/UmbrellaAssetsList.tsx
index 9387033cc8..82d448c4f2 100644
--- a/src/modules/umbrella/StakeAssets/UmbrellaAssetsList.tsx
+++ b/src/modules/umbrella/StakeAssets/UmbrellaAssetsList.tsx
@@ -1,5 +1,5 @@
import { Trans } from '@lingui/macro';
-import { Box, useMediaQuery } from '@mui/material';
+import { Box, useMediaQuery, useTheme } from '@mui/material';
import { useMemo, useState } from 'react';
import { ListColumn } from 'src/components/lists/ListColumn';
import { ListHeaderTitle } from 'src/components/lists/ListHeaderTitle';
@@ -20,23 +20,23 @@ const listHeaders = [
sortKey: 'symbol',
},
{
- title: ,
+ title: ,
sortKey: 'totalAPY',
},
{
- title: ,
+ title: ,
sortKey: 'stakeTokenUnderlyingBalance',
},
{
- title: ,
+ title: ,
sortKey: 'stakeSharesTokens',
},
{
- title: Available to Stake,
+ title: Av. to Stake,
sortKey: 'totalAvailableToStake',
},
{
- title: Available to Claim,
+ title: Av. to Claim,
sortKey: 'totalAvailableToClaim',
},
{
@@ -55,7 +55,8 @@ export default function UmbrellaAssetsList({
stakedDataWithTokenBalances,
isLoadingStakedDataWithTokenBalances,
}: UmbrelaAssetsListProps) {
- const isTableChangedToCards = useMediaQuery('(max-width:1125px)');
+ const theme = useTheme();
+ const isTableChangedToCards = useMediaQuery(theme.breakpoints.down('mdlg'));
const [sortName, setSortName] = useState('');
const [sortDesc, setSortDesc] = useState(false);
diff --git a/src/modules/umbrella/StakeAssets/UmbrellaAssetsListContainer.tsx b/src/modules/umbrella/StakeAssets/UmbrellaAssetsListContainer.tsx
index 22ab96706f..d29726367d 100644
--- a/src/modules/umbrella/StakeAssets/UmbrellaAssetsListContainer.tsx
+++ b/src/modules/umbrella/StakeAssets/UmbrellaAssetsListContainer.tsx
@@ -1,11 +1,15 @@
import { Trans } from '@lingui/macro';
-import { useMediaQuery, useTheme } from '@mui/material';
+import { Box, Paper, useMediaQuery, useTheme } from '@mui/material';
import { useState } from 'react';
-import { ListWrapper } from 'src/components/lists/ListWrapper';
+import { AssetsFilterBar } from 'src/components/AssetsFilterBar';
import { NoSearchResults } from 'src/components/NoSearchResults';
-import { TitleWithSearchBar } from 'src/components/TitleWithSearchBar';
import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider';
import { useUmbrellaSummary } from 'src/hooks/stake/useUmbrellaSummary';
+import { useCoingeckoCategories } from 'src/hooks/useCoinGeckoCategories';
+import {
+ AssetCategory,
+ matchesSelectedCategories,
+} from 'src/modules/markets/utils/assetCategories';
import { useRootStore } from 'src/store/root';
import { useShallow } from 'zustand/shallow';
@@ -19,54 +23,71 @@ export const UmbrellaAssetsListContainer = () => {
const { data: stakedDataWithTokenBalances, loading: isLoadingStakedDataWithTokenBalances } =
useUmbrellaSummary(currentMarketData);
+ const {
+ data: categoryData,
+ isLoading: isLoadingCategories,
+ error: categoriesError,
+ } = useCoingeckoCategories();
const [searchTerm, setSearchTerm] = useState('');
+ const [inWalletOnly, setInWalletOnly] = useState(false);
+ const [selectedCategories, setSelectedCategories] = useState([]);
const { breakpoints } = useTheme();
const sm = useMediaQuery(breakpoints.down('sm'));
- const filteredData = stakedDataWithTokenBalances?.stakeData.filter((res) => {
- if (!searchTerm) return true;
- const term = searchTerm.toLowerCase().trim();
-
- return res.name.toLowerCase().includes(term) || res.iconSymbol.toLowerCase().includes(term);
- });
+ const filteredData = stakedDataWithTokenBalances?.stakeData
+ // Search by asset name or symbol
+ .filter((res) => {
+ if (!searchTerm) return true;
+ const term = searchTerm.toLowerCase().trim();
+ return res.name.toLowerCase().includes(term) || res.iconSymbol.toLowerCase().includes(term);
+ })
+ // "In Wallet": only assets the user holds in their wallet (raw underlying token balance)
+ .filter((res) => !inWalletOnly || Number(res.formattedBalances.underlyingTokenBalance) > 0)
+ // Category filter (shares the markets page's dynamic CoinGecko categorization)
+ .filter((res) =>
+ matchesSelectedCategories(
+ res.symbol,
+ selectedCategories,
+ categoryData?.stablecoinSymbols,
+ categoryData?.ethCorrelatedSymbols
+ )
+ );
const noStakeAssetsConfigured =
!isLoadingStakedDataWithTokenBalances && !stakedDataWithTokenBalances;
return (
- Assets to stake}
- searchPlaceholder={sm ? 'Search asset' : 'Search asset name or symbol'}
- />
- }
- >
-
+
- {noStakeAssetsConfigured ? (
-
- ) : (
- !loading &&
- !isLoadingStakedDataWithTokenBalances &&
- filteredData?.length === 0 && (
-
- We couldn't find any assets related to your search. Try again with a different
- asset name, symbol, or address.
-
- }
- />
- )
- )}
-
+ div:first-of-type > hr': { display: 'none' } }}>
+
+
+ {noStakeAssetsConfigured ? (
+
+ ) : (
+ !loading &&
+ !isLoadingStakedDataWithTokenBalances &&
+ filteredData?.length === 0 && (
+ We couldn't find any assets related to your search.}
+ />
+ )
+ )}
+
+
);
};
diff --git a/src/modules/umbrella/StakeAssets/UmbrellaAssetsListMobileItem.tsx b/src/modules/umbrella/StakeAssets/UmbrellaAssetsListMobileItem.tsx
index f553675ce3..8b8b81c04a 100644
--- a/src/modules/umbrella/StakeAssets/UmbrellaAssetsListMobileItem.tsx
+++ b/src/modules/umbrella/StakeAssets/UmbrellaAssetsListMobileItem.tsx
@@ -43,7 +43,7 @@ export const UmbrellaAssetsListMobileItem = ({ ...umbrellaStakeAsset }: MergedSt
textAlign: 'center',
}}
>
-
+
-
+ } captionVariant="description" mb={3} align="flex-start">
@@ -64,7 +64,7 @@ export const UmbrellaAssetsListMobileItem = ({ ...umbrellaStakeAsset }: MergedSt
mb={3}
align="flex-start"
>
-
+ Available to claim} captionVariant="description" mb={3}>
@@ -77,7 +77,7 @@ export const UmbrellaAssetsListMobileItem = ({ ...umbrellaStakeAsset }: MergedSt
textAlign: 'center',
}}
>
-
+
diff --git a/src/modules/umbrella/StakeCooldownModalContent.tsx b/src/modules/umbrella/StakeCooldownModalContent.tsx
index 9a8e6a3496..41f7928fdd 100644
--- a/src/modules/umbrella/StakeCooldownModalContent.tsx
+++ b/src/modules/umbrella/StakeCooldownModalContent.tsx
@@ -2,7 +2,7 @@ import { valueToBigNumber } from '@aave/math-utils';
import { ArrowDownIcon, CalendarIcon } from '@heroicons/react/outline';
import { ArrowNarrowRightIcon } from '@heroicons/react/solid';
import { Trans } from '@lingui/macro';
-import { Box, Checkbox, FormControlLabel, Stack, SvgIcon, Typography } from '@mui/material';
+import { Alert, Box, Checkbox, FormControlLabel, Stack, SvgIcon, Typography } from '@mui/material';
import { BigNumber } from 'bignumber.js';
import dayjs from 'dayjs';
import { parseUnits } from 'ethers/lib/utils';
@@ -10,7 +10,6 @@ import React, { useState } from 'react';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
import { Link } from 'src/components/primitives/Link';
import { TokenIcon } from 'src/components/primitives/TokenIcon';
-import { Warning } from 'src/components/primitives/Warning';
import { TxErrorView } from 'src/components/transactions/FlowCommons/Error';
import { GasEstimationError } from 'src/components/transactions/FlowCommons/GasEstimationError';
import { TxSuccessView } from 'src/components/transactions/FlowCommons/Success';
@@ -173,26 +172,22 @@ export const StakeCooldownModalContent = ({ stakeData }: { stakeData: MergedStak
pb: '30px',
}}
>
-
+ Amount available to unstake
-
+
@@ -208,18 +203,18 @@ export const StakeCooldownModalContent = ({ stakeData }: { stakeData: MergedStak
pb: '30px',
}}
>
-
+ Unstake window
-
+
{dateMessage(stakeCooldownSeconds)}
-
+
{dateMessage(stakeCooldownSeconds + stakeUnstakeWindow)}
@@ -328,14 +323,12 @@ export const StakeCooldownModalContent = ({ stakeData }: { stakeData: MergedStak
)}
-
-
-
- If you DO NOT unstake within {timeMessage(stakeUnstakeWindow)} of unstake window, you
- will need to activate cooldown process again.
-
-
-
+
+
+ If you DO NOT unstake within {timeMessage(stakeUnstakeWindow)} of unstake window, you will
+ need to activate cooldown process again.
+
+
diff --git a/src/modules/umbrella/StakingApyItem.tsx b/src/modules/umbrella/StakingApyItem.tsx
index 769c0a11ef..b64886ea24 100644
--- a/src/modules/umbrella/StakingApyItem.tsx
+++ b/src/modules/umbrella/StakingApyItem.tsx
@@ -10,13 +10,7 @@ import invariant from 'tiny-invariant';
import { IconData, MultiIconWithTooltip } from './helpers/MultiIcon';
-export const StakingApyItem = ({
- stakeData,
- isMobile,
-}: {
- stakeData: MergedStakeData;
- isMobile?: boolean;
-}) => {
+export const StakingApyItem = ({ stakeData }: { stakeData: MergedStakeData }) => {
const { reserves } = useAppDataContext();
const icons: IconData[] = [];
@@ -68,24 +62,14 @@ export const StakingApyItem = ({
}
return (
-
-
+
+
+
{stakeData.underlyingIsStataToken ? (
Staking this asset will earn the underlying asset supply yield in addition to
@@ -145,9 +129,9 @@ export const StakingApyTooltipcontent = ({
symbol={reward.symbol}
sx={{ fontSize: '20px', mr: 1 }}
/>
- {reward.name}
+ {reward.name}
{reward.fromSupply && (
-
+
(supply)
)}
@@ -157,8 +141,8 @@ export const StakingApyTooltipcontent = ({
width="100%"
>
-
-
+
+ APY
@@ -172,18 +156,18 @@ export const StakingApyTooltipcontent = ({
mt: 1,
pt: 2,
borderTop: '1px solid',
- borderColor: 'divider',
+ borderColor: 'border-2',
}}
caption={
-
+ Total
}
width="100%"
>
-
-
+
+ APY
diff --git a/src/modules/umbrella/UmbrellaAssetsDefault.tsx b/src/modules/umbrella/UmbrellaAssetsDefault.tsx
index ac2fbe44ac..47d4e7b969 100644
--- a/src/modules/umbrella/UmbrellaAssetsDefault.tsx
+++ b/src/modules/umbrella/UmbrellaAssetsDefault.tsx
@@ -1,13 +1,20 @@
import { Trans } from '@lingui/macro';
-import { Box, Skeleton, Stack, Typography, useMediaQuery } from '@mui/material';
+import { Box, Paper, Skeleton, Stack, useMediaQuery, useTheme } from '@mui/material';
+import { useState } from 'react';
+import { AssetsFilterBar } from 'src/components/AssetsFilterBar';
import { ListColumn } from 'src/components/lists/ListColumn';
import { ListHeaderTitle } from 'src/components/lists/ListHeaderTitle';
import { ListHeaderWrapper } from 'src/components/lists/ListHeaderWrapper';
import { ListItem } from 'src/components/lists/ListItem';
-import { ListWrapper } from 'src/components/lists/ListWrapper';
+import { NoSearchResults } from 'src/components/NoSearchResults';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
import { Row } from 'src/components/primitives/Row';
import { FormattedStakeData, useStakeDataSummary } from 'src/hooks/stake/useUmbrellaSummary';
+import { useCoingeckoCategories } from 'src/hooks/useCoinGeckoCategories';
+import {
+ AssetCategory,
+ matchesSelectedCategories,
+} from 'src/modules/markets/utils/assetCategories';
import { useRootStore } from 'src/store/root';
import { useShallow } from 'zustand/shallow';
@@ -16,23 +23,76 @@ import { NoStakeAssets } from './NoStakeAssets';
import { StakeAssetName } from './StakeAssets/StakeAssetName';
export const UmrellaAssetsDefaultListContainer = () => {
+ const [currentMarketData] = useRootStore(useShallow((store) => [store.currentMarketData]));
+ const { data: stakeData, loading } = useStakeDataSummary(currentMarketData);
+ const {
+ data: categoryData,
+ isLoading: isLoadingCategories,
+ error: categoriesError,
+ } = useCoingeckoCategories();
+
+ const [searchTerm, setSearchTerm] = useState('');
+ const [selectedCategories, setSelectedCategories] = useState([]);
+ const { breakpoints } = useTheme();
+ const sm = useMediaQuery(breakpoints.down('sm'));
+
+ const filteredAssets = stakeData?.stakeAssets
+ // Search by asset symbol
+ .filter((res) => {
+ if (!searchTerm) return true;
+ const term = searchTerm.toLowerCase().trim();
+ return res.symbol.toLowerCase().includes(term);
+ })
+ // Category filter (shares the markets page's dynamic CoinGecko categorization)
+ .filter((res) =>
+ matchesSelectedCategories(
+ res.symbol,
+ selectedCategories,
+ categoryData?.stablecoinSymbols,
+ categoryData?.ethCorrelatedSymbols
+ )
+ );
+
+ const noStakeAssetsConfigured = !loading && (!stakeData || stakeData.stakeAssets.length === 0);
+
return (
-
- Assets to stake
-
- }
- >
-
-
+
+
+
+ div:first-of-type > hr': { display: 'none' } }}>
+
+
+ {noStakeAssetsConfigured ? (
+
+ ) : (
+ !loading &&
+ filteredAssets?.length === 0 && (
+ We couldn't find any assets related to your search.}
+ />
+ )
+ )}
+
+
);
};
-export const UmbrellaAssetsDefault = () => {
- const [currentMarketData] = useRootStore(useShallow((store) => [store.currentMarketData]));
- const { data: stakeData, loading } = useStakeDataSummary(currentMarketData);
- const isTableChangedToCards = useMediaQuery('(max-width:1125px)');
+export const UmbrellaAssetsDefault = ({
+ stakeAssets,
+ loading,
+}: {
+ stakeAssets: FormattedStakeData[];
+ loading: boolean;
+}) => {
+ const theme = useTheme();
+ const isTableChangedToCards = useMediaQuery(theme.breakpoints.down('mdlg'));
if (loading) {
return isTableChangedToCards ? (
@@ -52,8 +112,9 @@ export const UmbrellaAssetsDefault = () => {
);
}
- if (!loading && (!stakeData || stakeData.stakeAssets.length === 0)) {
- return ;
+ // Empty states (no assets configured / no search results) are handled by the container.
+ if (stakeAssets.length === 0) {
+ return null;
}
return (
@@ -72,14 +133,13 @@ export const UmbrellaAssetsDefault = () => {
)}
- {stakeData &&
- stakeData.stakeAssets.map((data, index) =>
- !isTableChangedToCards ? (
-
- ) : (
-
- )
- )}
+ {stakeAssets.map((data, index) =>
+ !isTableChangedToCards ? (
+
+ ) : (
+
+ )
+ )}
>
);
};
@@ -102,7 +162,7 @@ const AssetListItem = ({ stakeData }: { stakeData: FormattedStakeData }) => {
@@ -137,7 +197,7 @@ const AssetListItemMobile = ({ stakeData }: { stakeData: FormattedStakeData }) =
diff --git a/src/modules/umbrella/UmbrellaClaimModalContent.tsx b/src/modules/umbrella/UmbrellaClaimModalContent.tsx
index 80eda03a5f..507dd8207e 100644
--- a/src/modules/umbrella/UmbrellaClaimModalContent.tsx
+++ b/src/modules/umbrella/UmbrellaClaimModalContent.tsx
@@ -130,8 +130,8 @@ export const UmbrellaClaimAllModalContent = ({ stakeData }: UmbrellaClaimAllModa
>
-
-
+
+
{reward.symbol}
@@ -140,7 +140,7 @@ export const UmbrellaClaimAllModalContent = ({ stakeData }: UmbrellaClaimAllModa
variant="helperText"
compact
symbol="USD"
- color="text.secondary"
+ color="fg-2"
/>
))}
@@ -231,8 +231,8 @@ export const UmbrellaClaimModalContent = ({ stakeData }: UmbrellaClaimModalConte
>
-
-
+
+
{reward.symbol}
@@ -241,7 +241,7 @@ export const UmbrellaClaimModalContent = ({ stakeData }: UmbrellaClaimModalConte
variant="helperText"
compact
symbol="USD"
- color="text.secondary"
+ color="fg-2"
/>
))}
diff --git a/src/modules/umbrella/UmbrellaHeader.tsx b/src/modules/umbrella/UmbrellaHeader.tsx
index 19b885315a..27bdbd1ff4 100644
--- a/src/modules/umbrella/UmbrellaHeader.tsx
+++ b/src/modules/umbrella/UmbrellaHeader.tsx
@@ -1,254 +1,88 @@
import { Trans } from '@lingui/macro';
-import { Box, Button, Stack, Typography, useMediaQuery, useTheme } from '@mui/material';
+import { useMediaQuery, useTheme } from '@mui/material';
+import { PageHeader } from 'src/components/PageHeader/PageHeader';
+import { PageHeaderStat } from 'src/components/PageHeader/PageHeaderStat';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
-import { TopInfoPanel } from 'src/components/TopInfoPanel/TopInfoPanel';
import { useStakeDataSummary, useUmbrellaSummary } from 'src/hooks/stake/useUmbrellaSummary';
-import { useModalContext } from 'src/hooks/useModal';
import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
import { useRootStore } from 'src/store/root';
import { MarketDataType } from 'src/ui-config/marketsConfig';
-import { GENERAL } from 'src/utils/events';
-import { useShallow } from 'zustand/shallow';
-import { Link } from '../../components/primitives/Link';
-import { TopInfoPanelItem } from '../../components/TopInfoPanel/TopInfoPanelItem';
-import { MarketSwitcher } from './UmbrellaMarketSwitcher';
+type StatProps = {
+ currentMarketData: MarketDataType;
+ valueVariant: 'h4' | 'h2';
+};
export const UmbrellaHeader: React.FC = () => {
const theme = useTheme();
const { currentAccount } = useWeb3Context();
- const [currentMarketData, trackEvent] = useRootStore(
- useShallow((store) => [store.currentMarketData, store.trackEvent])
- );
- // const [trackEvent, currentMarket, setCurrentMarket] = useRootStore(
- // useShallow((store) => [store.trackEvent, store.currentMarket, store.setCurrentMarket])
- // );
+ // The market is pinned to Core on the staking page (see pages/staking.page.tsx), so this reads Core.
+ const currentMarketData = useRootStore((store) => store.currentMarketData);
- const upToLG = useMediaQuery(theme.breakpoints.up('lg'));
const downToSM = useMediaQuery(theme.breakpoints.down('sm'));
- const downToXSM = useMediaQuery(theme.breakpoints.down('xsm'));
-
- const valueTypographyVariant = downToSM ? 'main16' : 'main21';
- const symbolsTypographyVariant = downToSM ? 'secondary16' : 'secondary21';
+ const valueVariant = downToSM ? 'h4' : 'h2';
return (
-
- {/* */}
-
- {/* */}
-
- Staking
-
-
-
-
-
-
- Umbrella is the upgraded version of the Safety Module. Manage your previously staked
- assets
- {' '}
-
- here.
-
-
-
-
- Stake your Aave aTokens or underlying assets to earn rewards. In case of a shortfall
- event, your stake may be slashed to cover the deficit.
- {' '}
-
- trackEvent(GENERAL.EXTERNAL_LINK, {
- Link: 'Staking Risks',
- })
- }
- >
- Learn more about the risks.
-
-
-
- }
+ Stake your Aave aTokens or underlying assets to earn rewards.}
>
+
{currentAccount ? (
-
- ) : (
-
- )}
-
+
+ ) : null}
+
);
};
-const UmbrellaHeaderUserDetails = ({
- currentMarketData,
- valueTypographyVariant,
- symbolsTypographyVariant,
-}: {
- currentMarketData: MarketDataType;
- valueTypographyVariant: 'main16' | 'main21';
- symbolsTypographyVariant: 'secondary16' | 'secondary21';
-}) => {
- const theme = useTheme();
+// Total staked across the instance — shown whether or not a wallet is connected.
+const TotalStakedStat = ({ currentMarketData, valueVariant }: StatProps) => {
+ const { data: stakeData, loading } = useStakeDataSummary(currentMarketData);
+
+ return (
+ Total Staked} loading={loading}>
+
+
+ );
+};
+
+// Connected-only stats. Kept separate so `useUmbrellaSummary` (user-specific) is gated to the
+// connected branch rather than run for logged-out visitors.
+const UmbrellaUserStats = ({ currentMarketData, valueVariant }: StatProps) => {
const { data: stakedDataWithTokenBalances, loading: isLoadingStakedDataWithTokenBalances } =
useUmbrellaSummary(currentMarketData);
- const { data: stakeData, loading } = useStakeDataSummary(currentMarketData);
- const { openUmbrellaClaimAll } = useModalContext();
const totalUSDAggregateStaked = stakedDataWithTokenBalances?.aggregatedTotalStakedUSD;
const weightedAverageApy = stakedDataWithTokenBalances?.weightedAverageApy;
- const userRewardsUsd = stakedDataWithTokenBalances?.stakeData.reduce((acc, stake) => {
- const totalAvailableToClaim = stake.formattedRewards.reduce(
- (sum, reward) => sum + Number(reward.accruedUsd || '0'),
- 0
- );
- return acc + totalAvailableToClaim;
- }, 0);
-
- const userHasRewards =
- userRewardsUsd !== undefined && userRewardsUsd > 0 && !isLoadingStakedDataWithTokenBalances;
-
return (
<>
-
- Total amount staked
-
- }
- loading={loading}
- >
-
-
-
- Staked Balance
-
- }
+ Staked Balance}
loading={isLoadingStakedDataWithTokenBalances}
>
-
+
- Net APY}
- loading={isLoadingStakedDataWithTokenBalances}
- >
+ Net APY} loading={isLoadingStakedDataWithTokenBalances}>
-
- {userHasRewards && (
- Available rewards}
- loading={isLoadingStakedDataWithTokenBalances}
- hideIcon
- >
-
-
-
-
-
- openUmbrellaClaimAll()}
- sx={{ minWidth: 'unset', ml: { xs: 0, xsm: 2 } }}
- >
- Claim
-
-
-
- )}
- >
- );
-};
-
-const UmbrellaHeaderDefault = ({
- currentMarketData,
- valueTypographyVariant,
- symbolsTypographyVariant,
-}: {
- currentMarketData: MarketDataType;
- valueTypographyVariant: 'main16' | 'main21';
- symbolsTypographyVariant: 'secondary16' | 'secondary21';
-}) => {
- const theme = useTheme();
- const { data: stakeData, loading } = useStakeDataSummary(currentMarketData);
-
- return (
- <>
-
- Total amount staked
-
- }
- loading={loading}
- >
-
-
+
>
);
};
diff --git a/src/modules/umbrella/UmbrellaMarketSwitcher.tsx b/src/modules/umbrella/UmbrellaMarketSwitcher.tsx
deleted file mode 100644
index 78d352f5aa..0000000000
--- a/src/modules/umbrella/UmbrellaMarketSwitcher.tsx
+++ /dev/null
@@ -1,385 +0,0 @@
-import { ChevronDownIcon } from '@heroicons/react/outline';
-import { Trans } from '@lingui/macro';
-import {
- Box,
- BoxProps,
- ListItemText,
- MenuItem,
- SvgIcon,
- TextField,
- Tooltip,
- Typography,
- useMediaQuery,
- useTheme,
-} from '@mui/material';
-import React, { useState } from 'react';
-import { useRootStore } from 'src/store/root';
-import { BaseNetworkConfig } from 'src/ui-config/networksConfig';
-import { DASHBOARD } from 'src/utils/events';
-import {
- availableMarkets,
- CustomMarket,
- ENABLE_TESTNET,
- MarketDataType,
- marketsData,
- networkConfigs,
- STAGING_ENV,
-} from 'src/utils/marketsAndNetworksConfig';
-import { useShallow } from 'zustand/shallow';
-
-export const getMarketInfoById = (marketId: CustomMarket) => {
- const market: MarketDataType = marketsData[marketId as CustomMarket];
- const network: BaseNetworkConfig = networkConfigs[market.chainId];
- const logo = market.logo || network.networkLogoPath;
-
- return { market, logo };
-};
-
-export const getMarketHelpData = (marketName: string) => {
- const testChains = [
- 'Görli',
- 'Ropsten',
- 'Mumbai',
- 'Sepolia',
- 'Fuji',
- 'Testnet',
- 'Kovan',
- 'Rinkeby',
- ];
- const arrayName = marketName.split(' ');
- const testChainName = arrayName.filter((el) => testChains.indexOf(el) > -1);
- const marketTitle = arrayName.filter((el) => !testChainName.includes(el)).join(' ');
-
- return {
- name: marketTitle,
- testChainName: testChainName[0],
- };
-};
-
-export type Market = {
- marketTitle: string;
- networkName: string;
- networkLogo: string;
- selected?: boolean;
-};
-
-type MarketLogoProps = {
- size: number;
- logo: string;
- testChainName?: string;
- sx?: BoxProps;
-};
-
-export const MarketLogo = ({ size, logo, testChainName, sx }: MarketLogoProps) => {
- return (
-
-
-
- {testChainName && (
-
-
- {testChainName.split('')[0]}
-
-
- )}
-
- );
-};
-
-enum SelectedMarketVersion {
- V2,
- V3,
-}
-
-// TODO
-// Fetch markets that are active for umbrella.
-// Strip out any code not used for v2
-// Style to design specifications
-
-export const MarketSwitcher = () => {
- const [selectedMarketVersion] = useState(SelectedMarketVersion.V3);
- const theme = useTheme();
- const upToLG = useMediaQuery(theme.breakpoints.up('lg'));
- const downToXSM = useMediaQuery(theme.breakpoints.down('xsm'));
- const [trackEvent, currentMarket, setCurrentMarket] = useRootStore(
- useShallow((store) => [store.trackEvent, store.currentMarket, store.setCurrentMarket])
- );
-
- const isV3MarketsAvailable = availableMarkets
- .map((marketId: CustomMarket) => {
- const { market } = getMarketInfoById(marketId);
-
- return market.v3;
- })
- .some((item) => !!item);
-
- const handleMarketSelect = (e: React.ChangeEvent) => {
- trackEvent(DASHBOARD.CHANGE_MARKET, { market: e.target.value });
- setCurrentMarket(e.target.value as unknown as CustomMarket);
- };
-
- // const marketBlurbs: { [key: string]: JSX.Element } = {
- // proto_mainnet_v3: (
- // Main Ethereum market with the largest selection of assets and yield options
- // ),
- // proto_lido_v3: (
- // Optimized for efficiency and risk by supporting blue-chip collateral assets
- // ),
- // };
-
- return (
- null,
- renderValue: (marketId) => {
- const { market, logo } = getMarketInfoById(marketId as CustomMarket);
-
- return (
-
- {/* Main Row with Market Name */}
-
-
-
-
- {getMarketHelpData(market.marketTitle).name} {market.isFork ? 'Fork' : ''}
- {/* {upToLG &&
- (currentMarket === 'proto_mainnet_v3' || currentMarket === 'proto_lido_v3')
- ? 'Instance'
- : ' Market'} */}
-
-
-
- {/*
- V2
- */}
-
-
-
-
-
-
-
- {/* {marketBlurbs[currentMarket] && (
-
- {marketBlurbs[currentMarket]}
-
- )} */}
-
- );
- },
-
- sx: {
- '&.MarketSwitcher__select .MuiSelect-outlined': {
- pl: 0,
- py: 0,
- backgroundColor: 'transparent !important',
- },
- '.MuiSelect-icon': { color: '#F1F1F3' },
- },
- MenuProps: {
- anchorOrigin: {
- vertical: 'bottom',
- horizontal: 'right',
- },
- transformOrigin: {
- vertical: 'top',
- horizontal: 'right',
- },
- PaperProps: {
- style: {
- minWidth: 240,
- },
- variant: 'outlined',
- elevation: 0,
- },
- },
- }}
- >
-
-
-
- {ENABLE_TESTNET || STAGING_ENV ? 'Select Aave Testnet Market' : 'Select Aave Market'}
-
-
-
- {isV3MarketsAvailable && (
-
- {/* {
- if (value !== null) {
- setSelectedMarketVersion(value);
- }
- }}
- sx={{
- width: '100%',
- height: '36px',
- background: theme.palette.primary.main,
- border: `1px solid ${
- theme.palette.mode === 'dark' ? 'rgba(235, 235, 237, 0.12)' : '#1B2030'
- }`,
- borderRadius: '6px',
- marginTop: '16px',
- marginBottom: '12px',
- padding: '2px',
- }}
- >
-
- theme.palette.gradients.aaveGradient,
- backgroundClip: 'text',
- color: 'transparent',
- }
- : {
- color: theme.palette.mode === 'dark' ? '#0F121D' : '#FFFFFF',
- }
- }
- >
- Version 3
-
-
-
- theme.palette.gradients.aaveGradient,
- backgroundClip: 'text',
- color: 'transparent',
- }
- : {
- color: theme.palette.mode === 'dark' ? '#0F121D' : '#FFFFFF',
- }
- }
- >
- Version 2
-
-
- */}
-
- )}
- {availableMarkets.map((marketId: CustomMarket) => {
- const { market, logo } = getMarketInfoById(marketId);
- const marketNaming = getMarketHelpData(market.marketTitle);
- return (
-
- );
- })}
-
- );
-};
diff --git a/src/modules/umbrella/UmbrellaModalContent.tsx b/src/modules/umbrella/UmbrellaModalContent.tsx
index 7ff2fd4bc3..22876f56ea 100644
--- a/src/modules/umbrella/UmbrellaModalContent.tsx
+++ b/src/modules/umbrella/UmbrellaModalContent.tsx
@@ -1,11 +1,10 @@
import { USD_DECIMALS, valueToBigNumber } from '@aave/math-utils';
import { Trans } from '@lingui/macro';
-import { Box, Checkbox, Skeleton, Stack, Typography } from '@mui/material';
+import { Alert, Box, Checkbox, Skeleton, Stack, Typography } from '@mui/material';
import { parseUnits } from 'ethers/lib/utils';
import React, { useState } from 'react';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
import { Row } from 'src/components/primitives/Row';
-import { Warning } from 'src/components/primitives/Warning';
import { TextWithTooltip } from 'src/components/TextWithTooltip';
import { AssetInput } from 'src/components/transactions/AssetInput';
import { TxErrorView } from 'src/components/transactions/FlowCommons/Error';
@@ -224,10 +223,10 @@ export const UmbrellaModalContent = ({ stakeData, user, userReserve, poolReserve
/>
) : (
<>
-
+
-
+
Staking this amount will reduce your health factor and increase risk of liquidation.
-
+
-
+
>
)}
diff --git a/src/modules/umbrella/helpers/AmountAvailableItem.tsx b/src/modules/umbrella/helpers/AmountAvailableItem.tsx
index ed5a818530..99aba67b69 100644
--- a/src/modules/umbrella/helpers/AmountAvailableItem.tsx
+++ b/src/modules/umbrella/helpers/AmountAvailableItem.tsx
@@ -27,12 +27,12 @@ export const AmountAvailableItem = ({
aToken={aToken}
waToken={waToken}
/>
- {name}
+ {name}
}
width="100%"
>
-
+
);
};
diff --git a/src/modules/umbrella/helpers/ApyTooltip.tsx b/src/modules/umbrella/helpers/ApyTooltip.tsx
index 7ffc3c9a72..8b79c5205a 100644
--- a/src/modules/umbrella/helpers/ApyTooltip.tsx
+++ b/src/modules/umbrella/helpers/ApyTooltip.tsx
@@ -1,10 +1,11 @@
import { Trans } from '@lingui/macro';
+import { TypographyProps } from '@mui/material';
import { Link } from 'src/components/primitives/Link';
import { TextWithTooltip } from 'src/components/TextWithTooltip';
-export const ApyTooltip = () => {
+export const ApyTooltip = ({ variant }: { variant?: TypographyProps['variant'] }) => {
return (
- APY}>
+ APY} variant={variant}>
<>
Reward APY adjusts with total staked amount, following a curve that targets optimal
diff --git a/src/modules/umbrella/helpers/Helpers.tsx b/src/modules/umbrella/helpers/Helpers.tsx
index 831ffa4015..6e4a2c28ce 100644
--- a/src/modules/umbrella/helpers/Helpers.tsx
+++ b/src/modules/umbrella/helpers/Helpers.tsx
@@ -44,7 +44,7 @@ export const UmbrellaAssetBreakdown = ({
flexDirection: 'column',
}}
>
-
+
Participating in staking {symbol} gives annualized rewards. Your wallet balance is the
sum of your aTokens and underlying assets. The breakdown to stake is below
@@ -70,7 +70,7 @@ export const UmbrellaAssetBreakdown = ({
@@ -92,7 +92,7 @@ export const UmbrellaAssetBreakdown = ({
@@ -115,12 +115,12 @@ export const UmbrellaAssetBreakdown = ({
- ({ pt: 1, mt: 1 })}>
+ Total} height={32}>
diff --git a/src/modules/umbrella/helpers/SharesTooltip.tsx b/src/modules/umbrella/helpers/SharesTooltip.tsx
index 1ee186dd4a..2e5849b8fc 100644
--- a/src/modules/umbrella/helpers/SharesTooltip.tsx
+++ b/src/modules/umbrella/helpers/SharesTooltip.tsx
@@ -1,9 +1,10 @@
import { Trans } from '@lingui/macro';
+import { TypographyProps } from '@mui/material';
import { TextWithTooltip } from 'src/components/TextWithTooltip';
-export const SharesTooltip = () => {
+export const SharesTooltip = ({ variant }: { variant?: TypographyProps['variant'] }) => {
return (
- Shares}>
+ Shares} variant={variant}>
<>
Shares are Umbrella Stake Tokens you receive when staking. They represent your ownership
diff --git a/src/modules/umbrella/helpers/StakedUnderlyingTooltip.tsx b/src/modules/umbrella/helpers/StakedUnderlyingTooltip.tsx
index 3449c3c03e..284e245c07 100644
--- a/src/modules/umbrella/helpers/StakedUnderlyingTooltip.tsx
+++ b/src/modules/umbrella/helpers/StakedUnderlyingTooltip.tsx
@@ -1,9 +1,10 @@
import { Trans } from '@lingui/macro';
+import { TypographyProps } from '@mui/material';
import { TextWithTooltip } from 'src/components/TextWithTooltip';
-export const StakedUnderlyingTooltip = () => {
+export const StakedUnderlyingTooltip = ({ variant }: { variant?: TypographyProps['variant'] }) => {
return (
- Staked Underlying}>
+ Staked Underlying} variant={variant}>
<>
Total amount of underlying assets staked. This number represents the combined sum of your
diff --git a/src/modules/umbrella/helpers/StakingDropdown.tsx b/src/modules/umbrella/helpers/StakingDropdown.tsx
index 803f60992f..d5daccef0d 100644
--- a/src/modules/umbrella/helpers/StakingDropdown.tsx
+++ b/src/modules/umbrella/helpers/StakingDropdown.tsx
@@ -81,7 +81,8 @@ export const StakingDropdown = ({ stakeData }: { stakeData: MergedStakeData }) =
{
trackEvent(STAKE.STAKE_TOKEN, {
action: STAKE.OPEN_STAKE_MODAL,
@@ -113,7 +114,7 @@ export const StakingDropdown = ({ stakeData }: { stakeData: MergedStakeData }) =
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
- size="medium"
+ size="small"
>
@@ -153,7 +154,7 @@ export const StakingDropdown = ({ stakeData }: { stakeData: MergedStakeData }) =
alignItems="center"
justifyContent="space-between"
>
-
+ Cooling down
@@ -191,7 +192,7 @@ export const StakingDropdown = ({ stakeData }: { stakeData: MergedStakeData }) =
alignItems="center"
justifyContent="space-between"
>
-
+ Withdraw
diff --git a/src/utils/buttonStyles.ts b/src/utils/buttonStyles.ts
new file mode 100644
index 0000000000..663698abbf
--- /dev/null
+++ b/src/utils/buttonStyles.ts
@@ -0,0 +1,24 @@
+import { SxProps, Theme } from '@mui/material';
+
+/**
+ * Icon-only button styling: a square button — no min-width, equal 0.25rem padding on all
+ * sides, and a fixed 0.5rem radius regardless of button size. Compose it in `sx` on top of
+ * any Button variant/size (it only adjusts sizing):
+ *
+ *
+ *
+ *
+ */
+export const iconButtonSx = {
+ minWidth: 0,
+ p: '0.25rem',
+ // Square: match the width to the button's own height (set by its size slot) so it's a square
+ // whatever the icon's width — otherwise a medium button (36px tall) with an 18px icon renders
+ // as a tall rectangle.
+ aspectRatio: '1',
+ // Fixed radius even at size="small" (whose slot would otherwise apply 0.375rem); sx wins
+ // over the theme's per-size styleOverride.
+ borderRadius: '0.5rem',
+ // `satisfies` (not a `SxProps` annotation) keeps the narrow literal type so this can also be
+ // composed inside an `sx` array — e.g. `sx={[iconButtonSx, { ... }]}`.
+} satisfies SxProps;
diff --git a/src/utils/colorToP3.ts b/src/utils/colorToP3.ts
new file mode 100644
index 0000000000..f53dbe8aef
--- /dev/null
+++ b/src/utils/colorToP3.ts
@@ -0,0 +1,18 @@
+import { decomposeColor } from '@mui/material/styles';
+
+/**
+ * Convert an sRGB color string (hex, `rgb()`, or `rgba()`) to its Display-P3 equivalent
+ * using the same naive channel mapping the Figma export uses (channels / 255, relabeled as
+ * `color(display-p3 …)`). This matches the design source's P3 values and, on wide-gamut
+ * displays, renders saturated colors richer while leaving near-grays visually unchanged.
+ *
+ * Used to generate the `@supports (color-gamut: p3)` override layer for the theme's CSS
+ * variables. Non-color / already-`color()` inputs are returned unchanged.
+ */
+export const colorToP3 = (color: string): string => {
+ if (!color.startsWith('#') && !color.startsWith('rgb')) return color;
+ // decomposeColor parses #nnn / #nnnnnn / rgb() / rgba() → { values: [r, g, b, a?] } (r,g,b 0-255).
+ const [r, g, b, a] = decomposeColor(color).values;
+ const channels = `${r / 255} ${g / 255} ${b / 255}`;
+ return a === undefined ? `color(display-p3 ${channels})` : `color(display-p3 ${channels} / ${a})`;
+};
diff --git a/src/utils/figmaColors.ts b/src/utils/figmaColors.ts
new file mode 100644
index 0000000000..d42e4b3903
--- /dev/null
+++ b/src/utils/figmaColors.ts
@@ -0,0 +1,306 @@
+/**
+ * Figma color tokens — the SINGLE SOURCE OF TRUTH for every color value in the app (light +
+ * dark). The theme flattens these onto the MUI palette root, so each becomes a `--mui-palette-*`
+ * CSS var (Display-P3 + sRGB fallback). Consume them as bare token strings in `sx`
+ * (`sx={{ bgcolor: 'bg-1' }}`) or via `figVars` outside `sx` — never hand-write hex in components.
+ */
+export const figmaLight = {
+ 'bg-max': '#f0f0f0',
+ 'bg-1': '#fafafa',
+ 'bg-2': '#fcfcfc',
+ 'bg-3': '#ffffff',
+ 'bg-4': '#f2f2f2',
+ 'bg-5': '#f1f1f1',
+ 'bg-6': '#ebebeb',
+ 'border-0': 'rgba(0, 0, 0, 0.06)',
+ 'border-1': 'rgba(0, 0, 0, 0.08)',
+ 'border-2': 'rgba(0, 0, 0, 0.1)',
+ 'fg-max': '#000000',
+ 'fg-1': '#000000',
+ 'fg-2': '#666666',
+ 'fg-3': '#7d7d7d',
+ 'fg-4': '#a8a8a8',
+ 'fg-5': '#b3b3b3',
+ // Muted icon grey (search, sortable-column chevrons, …). Deliberately mode-agnostic — the same
+ // value in both maps — unlike the fg-* ramp steps.
+ 'fg-icon': '#A8A8A8',
+ selected: 'rgba(46, 15, 15, 0.04)',
+ 'blue-1': '#1a88f8',
+ 'blue-2': '#48abff',
+ 'blue-3': '#a9e7ff',
+ 'yellow-1': '#ffb200',
+ 'yellow-2': '#ffcc00',
+ 'yellow-3': '#f6d551',
+ 'red-1': '#f24900',
+ 'red-2': '#ff8947',
+ 'red-3': '#ffc693',
+ 'purple-1': '#9391f7',
+ 'purple-2': '#bcbbff',
+ 'purple-3': '#e2e0ff',
+ 'green-1': '#1f807b',
+ 'green-2': '#63bbb6',
+ 'green-3': '#9debe7',
+ 'cyan-1': '#6bcef5',
+ 'cyan-2': '#b5e7fa',
+ 'cyan-3': '#dff6ff',
+ 'navy-1': '#1c4886',
+ 'navy-2': '#6188c0',
+ 'navy-3': '#b0d3ff',
+ 'shadow-low': 'rgba(0, 0, 0, 0.03)',
+ 'shadow-medium': 'rgba(0, 0, 0, 0.05)',
+ 'shadow-high': 'rgba(0, 0, 0, 0.07)',
+ 'shadow-strong': 'rgba(0, 0, 0, 0.11)',
+ 'shadow-stroke-1': 'rgba(0, 0, 0, 0.06)',
+ 'shadow-stroke-2': 'rgba(0, 0, 0, 0.08)',
+ ethereum: '#25292e',
+ focus: 'rgba(26, 136, 248, 0.2)',
+ scrim: 'rgba(247, 246, 246, 0.8)',
+ // Data-viz categorical palette (17 hues, red → pink).
+ 'data-red': '#FF4760',
+ 'data-coral': '#FF513D',
+ 'data-orange': '#FF7029',
+ 'data-honey': '#FF8C00',
+ 'data-yellow': '#DBA400',
+ 'data-pear': '#CCAB00',
+ 'data-light-green': '#22CE80',
+ 'data-green': '#00BD68',
+ 'data-matcha': '#89BE2D',
+ 'data-seafoam': '#00B89F',
+ 'data-teal': '#05B4C7',
+ 'data-lagoon': '#12B4D9',
+ 'data-blue': '#38B0F5',
+ 'data-azure': '#4797FF',
+ 'data-purple': '#837AFF',
+ 'data-lavender': '#C061FF',
+ 'data-pink': '#EB47CF',
+ 'button-hover': 'rgba(0, 0, 0, 0.025)',
+ 'data-green-gho': '#5dff93',
+ // Gold for the favourited market star (mode-agnostic; Figma color(display-p3 1 0.7 0)).
+ 'favourite-star': '#FFB300',
+ // Alert "danger" severity red (icon + gradient); distinct from the muted error-* palette.
+ danger: '#DC2626',
+ // sGHO markets-banner gradient: a data-green wash at 6% fading to the banner's own surface.
+ 'sgho-banner-green': 'rgba(50, 201, 88, 0.06)',
+ 'chain-testnet': '#8594ab',
+ 'chain-ethereum': '#25292e',
+ 'chain-polygon': '#8347e5',
+ 'chain-base': '#0052ff',
+ 'chain-optimism': '#e84142',
+ 'chain-lens': '#36a136',
+ 'chain-arbitrum': '#28a0f0',
+ 'chain-blast': '#ffc700',
+ 'chain-scroll': '#f8cf6e',
+ 'chain-worldchain': '#ff9d00',
+ 'chain-zksync': '#8c8dfe',
+ bone: '#f6f7f4',
+ // Opaque hover fill for the Select trigger. A SINGLE per-mode token rather than a base +
+ // `darkScheme()` override, so it resolves to the NEAREST color scheme — the dev showcase's local
+ // toggle works even when the app's global scheme differs (the dark selector matches any ancestor,
+ // including , so a two-token swap leaks across a nested scheme boundary).
+ 'bg-4-hover': '#f6f7f4',
+ // --- semantic tokens promoted from theme-file literals (SoT) ---
+ 'secondary-main': '#FF607B',
+ 'secondary-light': '#FF607B',
+ 'secondary-dark': '#B34356',
+ 'error-light': '#D26666',
+ 'error-dark': '#BC0000',
+ 'error-text': '#4F1919',
+ 'error-bg': '#F9EBEB',
+ 'warning-light': '#FFCE00',
+ 'warning-dark': '#C67F15',
+ 'warning-text': '#63400A',
+ 'warning-bg': '#FEF5E8',
+ 'info-light': '#0062D2',
+ 'info-dark': '#002754',
+ 'info-text': '#002754',
+ 'info-bg': '#E5EFFB',
+ 'success-light': '#90FF95',
+ 'success-dark': '#318435',
+ 'success-text': '#1C4B1E',
+ 'success-bg': '#ECF8ED',
+ 'disabled-fg': '#BBBECA',
+ 'disabled-bg': '#EAEBEF',
+ 'input-line': '#383D511F',
+ 'input-border-hover': '#CBCDD8',
+ 'surface-elevated': '#ffffff',
+ 'table-bg': '#ffffff',
+ // --- semantic / button (Figma collection) ---
+ 'button-hover-primary': 'rgba(255, 255, 255, 0.16)',
+ 'button-hover-secondary': 'rgba(0, 0, 0, 0.03)',
+ 'button-hover-tertiary': 'rgba(0, 0, 0, 0.04)',
+} as const;
+
+export const figmaDark = {
+ 'bg-max': '#0a0a0b',
+ 'bg-1': '#100f0f',
+ 'bg-2': '#1a1919',
+ 'bg-3': '#1f1e1e',
+ 'bg-4': '#2a2828',
+ 'bg-5': '#393737',
+ 'bg-6': '#494646',
+ 'border-0': 'rgba(255, 255, 255, 0.06)',
+ 'border-1': 'rgba(255, 255, 255, 0.08)',
+ 'border-2': 'rgba(255, 255, 255, 0.12)',
+ 'fg-max': '#ffffff',
+ 'fg-1': '#ffffff',
+ 'fg-2': '#bcbbbb',
+ 'fg-3': '#8f8e8e',
+ 'fg-4': '#636161',
+ 'fg-5': '#ffffff',
+ // Muted icon grey (search, sortable-column chevrons, …). Deliberately mode-agnostic — the same
+ // value in both maps — unlike the fg-* ramp steps.
+ 'fg-icon': '#A8A8A8',
+ selected: 'rgba(255, 255, 255, 0.06)',
+ 'blue-1': '#1a88f8',
+ 'blue-2': '#48abff',
+ 'blue-3': '#a9e7ff',
+ 'yellow-1': '#ffc42c',
+ 'yellow-2': '#ffd631',
+ 'yellow-3': '#fff7ae',
+ 'red-1': '#f24900',
+ 'red-2': '#ff8947',
+ 'red-3': '#ffc693',
+ 'purple-1': '#9391f7',
+ 'purple-2': '#bcbbff',
+ 'purple-3': '#e2e0ff',
+ 'green-1': '#1f807b',
+ 'green-2': '#63bbb6',
+ 'green-3': '#9debe7',
+ 'cyan-1': '#6bcef5',
+ 'cyan-2': '#b5e7fa',
+ 'cyan-3': '#dff6ff',
+ 'navy-1': '#1c4886',
+ 'navy-2': '#6188c0',
+ 'navy-3': '#b0d3ff',
+ 'data-red': '#E05269',
+ 'data-coral': '#FF7045',
+ 'data-orange': '#E68662',
+ 'data-honey': '#F59942',
+ 'data-yellow': '#FDC75A',
+ 'data-pear': '#FFE042',
+ 'data-light-green': '#C1E38D',
+ 'data-green': '#66C399',
+ 'data-matcha': '#92D492',
+ 'data-seafoam': '#78D3B3',
+ 'data-teal': '#8DE3CC',
+ 'data-lagoon': '#83DDDF',
+ 'data-blue': '#88D5ED',
+ 'data-azure': '#88C0FE',
+ 'data-purple': '#A5A3FF',
+ 'data-lavender': '#C9A1EF',
+ 'data-pink': '#E1A4D9',
+ 'shadow-low': 'rgba(0, 0, 0, 0.15)',
+ 'shadow-medium': 'rgba(0, 0, 0, 0.3)',
+ 'shadow-high': 'rgba(0, 0, 0, 0.35)',
+ 'shadow-strong': 'rgba(0, 0, 0, 0.5)',
+ 'shadow-stroke-1': 'rgba(255, 255, 255, 0.08)',
+ 'shadow-stroke-2': 'rgba(255, 255, 255, 0.1)',
+ ethereum: '#434b55',
+ focus: 'rgba(85, 167, 251, 0.3)',
+ scrim: 'rgba(71, 67, 67, 0.8)',
+ 'button-hover': 'rgba(255, 255, 255, 0.025)',
+ 'table-item-hover-1': '#1e1d1d',
+ 'table-item-hover-2': '#282727',
+ 'data-green-gho': '#5dff93',
+ // Gold for the favourited market star (mode-agnostic; Figma color(display-p3 1 0.7 0)).
+ 'favourite-star': '#FFB300',
+ // Alert "danger" severity red (icon + gradient); distinct from the muted error-* palette.
+ danger: '#DC2626',
+ // sGHO markets-banner gradient: a data-green wash at 6% fading to the banner's own surface.
+ 'sgho-banner-green': 'rgba(102, 195, 153, 0.06)',
+ 'wallet-modal-more-networks-label': 'rgba(255, 255, 255, 0.4)',
+ 'chain-testnet': '#bfc6d1',
+ 'chain-ethereum': '#7e8287',
+ 'chain-polygon': '#8347e5',
+ 'chain-base': '#0052ff',
+ 'chain-optimism': '#e84142',
+ 'chain-lens': '#36a136',
+ 'chain-arbitrum': '#28a0f0',
+ 'chain-blast': '#ffc700',
+ 'chain-scroll': '#f8cf6e',
+ 'chain-worldchain': '#ff9d00',
+ 'chain-zksync': '#8c8dfe',
+ bone: '#f6f7f4',
+ 'bg-4-hover': '#28282a',
+ // --- semantic tokens promoted from theme-file literals (SoT) ---
+ 'secondary-main': '#F48FB1',
+ 'secondary-light': '#F6A5C0',
+ 'secondary-dark': '#AA647B',
+ 'error-light': '#E57373',
+ 'error-dark': '#D32F2F',
+ 'error-text': '#FBB4AF',
+ 'error-bg': '#2E0C0A',
+ 'warning-light': '#FFB74D',
+ 'warning-dark': '#F57C00',
+ 'warning-text': '#FFDCA8',
+ 'warning-bg': '#301E04',
+ 'info-light': '#4FC3F7',
+ 'info-dark': '#0288D1',
+ 'info-text': '#A9E2FB',
+ 'info-bg': '#071F2E',
+ 'success-light': '#90FF95',
+ 'success-dark': '#388E3C',
+ 'success-text': '#C2E4C3',
+ 'success-bg': '#0A130B',
+ 'disabled-fg': '#EBEBEF4D',
+ 'disabled-bg': '#EBEBEF1F',
+ 'input-line': '#EBEBEF6B',
+ 'input-border-hover': '#CBCDD8',
+ 'surface-elevated': '#1E1E20',
+ 'table-bg': '#1A1919',
+ // --- semantic / button (Figma collection) ---
+ 'button-hover-primary': 'rgba(0, 0, 0, 0.16)',
+ 'button-hover-secondary': 'rgba(255, 255, 255, 0.04)',
+ 'button-hover-tertiary': 'rgba(255, 255, 255, 0.06)',
+} as const;
+
+// Token names shared by both modes (light is the common subset; dark adds a few extras).
+export type FigmaColorName = keyof typeof figmaLight;
+
+/** Resolve a single Figma color token for the active mode. */
+export const figmaColor = (mode: 'light' | 'dark', name: FigmaColorName) =>
+ mode === 'dark' ? figmaDark[name] : figmaLight[name];
+
+/**
+ * Pick the whole token map for a mode — the terse way to build the palette:
+ * const t = pickFigma(mode);
+ * text: { primary: t['fg-1'], secondary: t['fg-2'] }
+ */
+export const pickFigma = (mode: 'light' | 'dark'): Record =>
+ mode === 'dark' ? figmaDark : figmaLight;
+
+/**
+ * Terse, P3-safe accessor for the design tokens as CSS variables.
+ *
+ * The tokens are flattened onto the MUI palette root (see `theme.tsx`), so MUI generates a
+ * `--mui-palette-` custom property per token and the Display-P3 layer overrides those on
+ * wide-gamut displays. `figVars['bg-1']` therefore emits `var(--mui-palette-bg-1)`, which gets
+ * P3 + the structural sRGB fallback — unlike a raw `theme.palette['bg-1']` hex read, which does
+ * not. Use it in `styled()`, plain JS, and interpolated strings; inside `sx` the bare string
+ * form (`sx={{ bgcolor: 'bg-1' }}`) already resolves to the same var with no import.
+ *
+ * Gotcha: never pass a var-based color (this, a bare `sx` token, or `theme.vars.palette.*`) to a
+ * raw SVG/icon presentation attribute (``) — `var()` doesn't
+ * resolve there. Use a concrete hex, or apply the color via `sx`/`style` (CSS) instead.
+ *
+ * The `--mui-palette-` naming is coupled to MUI's var generation and to the tokens living
+ * at the palette root — the same coupling `collectP3Vars` (theme.tsx) relies on.
+ */
+export const figVars = Object.fromEntries(
+ Object.keys(figmaLight).map((name) => [name, `var(--mui-palette-${name})`])
+) as Record;
+
+/**
+ * Always-white, mode-independent. For text/icons that sit on a fixed colored surface (brand
+ * gradients, always-dark chips). A concrete hex — NOT a CSS var — so it also resolves in raw
+ * SVG/icon presentation attributes (`color=`/`fill=`), where `var()` does not.
+ */
+export const onAccent = '#ffffff';
+
+/**
+ * The shared "surface" box-shadow: a soft drop shadow plus a 1px ring that stands in
+ * for a border. Used by the secondary buttons, menus/paper, and the dashboard cards.
+ * `stroke` selects the ring token (cards use `shadow-stroke-1` for a slightly stronger hairline).
+ */
+export const figSurfaceShadow = (stroke: FigmaColorName = 'shadow-stroke-2'): string =>
+ `0px 2px 4px 0px ${figVars['shadow-low']}, 0px 0px 0px 1px ${figVars[stroke]}`;
diff --git a/src/utils/insetHighlight.ts b/src/utils/insetHighlight.ts
new file mode 100644
index 0000000000..e99eebc062
--- /dev/null
+++ b/src/utils/insetHighlight.ts
@@ -0,0 +1,77 @@
+import { CSSObject, Theme } from '@mui/material/styles';
+
+import { motion } from './motion';
+
+interface InsetHighlightOpts {
+ /** Only `transitions` is read — accepts the app theme or a plain MUI `Theme`. */
+ theme: Pick;
+ /** Corner radius of the highlight pseudo-element. */
+ radius: string | number;
+ /** Even inset applied to every side; overridden per-side by the props below. */
+ inset?: string | number;
+ top?: string | number;
+ right?: string | number;
+ bottom?: string | number;
+ left?: string | number;
+ /** Resting scale the highlight grows in from on activation (default 0.96). */
+ restScale?: number;
+ /**
+ * When set, the highlight is "on" at rest — a persistent selected state: full scale and this
+ * fill, rather than transparent-until-hover. Leave undefined for hover-only rows so no
+ * `background-color` is emitted at rest.
+ */
+ restFill?: string;
+}
+
+/**
+ * The inset-pseudo highlight recipe shared by the dropdown menu items (`MuiMenuItem` in
+ * `theme.tsx`) and the market-switcher option rows (`MarketSwitcher.tsx`). Draws the
+ * hover/selected fill on a `::before` inset from the row's edges — so adjacent highlights keep a
+ * visual gap while the physical row is unchanged — sitting behind the row's content
+ * (`zIndex: -1` under `isolation: isolate`) and growing in from `restScale` → 1.
+ *
+ * Pair with {@link insetHighlightActive} under the consumer's own hover/focus/selected selectors
+ * to set the fill and final scale (the trigger selectors differ per consumer: MUI classes for
+ * MenuItem, `:hover` + a JS boolean for the switcher).
+ */
+export const insetHighlightBase = ({
+ theme,
+ radius,
+ inset,
+ top,
+ right,
+ bottom,
+ left,
+ restScale = 0.96,
+ restFill,
+}: InsetHighlightOpts): CSSObject => ({
+ position: 'relative',
+ isolation: 'isolate',
+ '&::before': {
+ content: '""',
+ position: 'absolute',
+ top: top ?? inset ?? 0,
+ right: right ?? inset ?? 0,
+ bottom: bottom ?? inset ?? 0,
+ left: left ?? inset ?? 0,
+ zIndex: -1,
+ borderRadius: radius,
+ transform: restFill ? 'scale(1)' : `scale(${restScale})`,
+ transition: theme.transitions.create(['transform', 'background-color'], {
+ duration: motion.duration.hover,
+ }),
+ // Only emit a resting fill when persistently "on" — hover-only consumers (and the MenuItem
+ // refactor) stay identical, with no `background-color` until their own trigger fires.
+ ...(restFill ? { backgroundColor: restFill } : {}),
+ },
+});
+
+/**
+ * The "on" state for an {@link insetHighlightBase} highlight: the fill plus the grown-in scale.
+ * Apply under the consumer's hover / keyboard-focus / selected selectors, e.g.
+ * `'&:hover::before': insetHighlightActive(figVars['button-hover'])`.
+ */
+export const insetHighlightActive = (fill: string): CSSObject => ({
+ backgroundColor: fill,
+ transform: 'scale(1)',
+});
diff --git a/src/utils/motion.ts b/src/utils/motion.ts
new file mode 100644
index 0000000000..e23d0dea0e
--- /dev/null
+++ b/src/utils/motion.ts
@@ -0,0 +1,26 @@
+/**
+ * Central motion tokens — the single source of truth for overlay/dialog animation
+ * timing across the app. Consumed by the theme's transition defaults and by the
+ * shared transition components (e.g. `ScaleFade`). Values mirror the reference
+ * project's overlay "feel": a fast, subtle pop.
+ *
+ * Kept in its own module (rather than in `theme.tsx`) so shared transitions can read
+ * these tokens without importing `theme.tsx`, which would create an import cycle
+ * (`theme` → `ScaleFade` → `theme`).
+ */
+export const motion = {
+ duration: {
+ /** dropdowns, menus, selects, popovers */
+ overlay: 100,
+ /** interactive control feedback — button hover/focus state transitions */
+ hover: 100,
+ /** modal enter/exit — reserved for Phase 2 (modals are not animated yet) */
+ modal: 200,
+ /** mobile modal slide-up — reserved for Phase 2/3 */
+ modalMobile: 300,
+ },
+ easing: {
+ standard: 'ease',
+ smooth: 'cubic-bezier(0.19, 1, 0.22, 1)',
+ },
+} as const;
diff --git a/src/utils/theme.tsx b/src/utils/theme.tsx
index adee35697d..049b0e19d2 100644
--- a/src/utils/theme.tsx
+++ b/src/utils/theme.tsx
@@ -1,23 +1,190 @@
-import {
- CheckCircleIcon,
- ChevronDownIcon,
- ExclamationCircleIcon,
- ExclamationIcon,
- InformationCircleIcon,
-} from '@heroicons/react/outline';
-import { SvgIcon, Theme, ThemeOptions } from '@mui/material';
-import { createTheme } from '@mui/material/styles';
+import { Box, SvgIcon, ThemeOptions } from '@mui/material';
+import { type CSSObject, createTheme, experimental_extendTheme } from '@mui/material/styles';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { ColorPartial } from '@mui/material/styles/createPalette';
+// Augments MUI's base `Theme` (the one component `sx`/`styled` callbacks receive) with `.vars`,
+// so `theme.vars.palette.*` typechecks app-wide, not only against this file's `AppTheme` param.
+import type {} from '@mui/material/themeCssVarsAugmentation';
import React from 'react';
+import {
+ AlertErrorIcon,
+ AlertInfoIcon,
+ AlertSuccessIcon,
+ AlertWarningIcon,
+} from 'src/components/icons/AlertIcons';
+import { ChevronUpDownIcon } from 'src/components/icons/ChevronUpDownIcon';
+import { ScaleFade } from 'src/components/primitives/transitions/ScaleFade';
+
+import { colorToP3 } from './colorToP3';
+import { type FigmaColorName, figSurfaceShadow, figVars, onAccent, pickFigma } from './figmaColors';
+import { insetHighlightActive, insetHighlightBase } from './insetHighlight';
+import { motion } from './motion';
+
+// The app theme is built with MUI's CSS-variables engine (`experimental_extendTheme`), so it
+// carries `.vars` (CSS custom-property refs like `figVars['bg-1']`) and
+// `.applyStyles(scheme, …)` for per-color-scheme overrides.
+type AppTheme = ReturnType;
+
+// MUI's `theme.applyStyles('dark', …)` needs the provider theme's `getColorSchemeSelector`,
+// which the raw `extendTheme` result (used to build the component overrides statically)
+// doesn't carry — so calling it there hits the classic `palette.mode` branch and throws (the
+// raw theme has no top-level `palette`). This helper inlines the exact CSS-vars selector
+// `applyStyles` emits, matching any ancestor with `data-mui-color-scheme="dark"` — the
+// element (app-wide) or a local wrapper (the dev showcase) — so both switch correctly.
+export const darkScheme = (styles: CSSObject): CSSObject => ({
+ '*:where([data-mui-color-scheme="dark"]) &': styles,
+});
+
+// Dropdown geometry: the menu paper's corner radius and the list's inset. The option-row
+// highlight radius is derived from these (paper radius − inset) to stay concentric, so keep
+// them together here — otherwise that relationship silently drifts.
+const MENU_PAPER_RADIUS = '0.75rem';
+const MENU_LIST_INSET = '0.38rem';
+
+/**
+ * The `::before` box the hover and disabled overlays both paint on: inset to the element's edges,
+ * behind its content but above its own background (`zIndex: -1` under `isolation: isolate`).
+ */
+const insetLayer: CSSObject = {
+ content: "''",
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ borderRadius: 'inherit',
+ zIndex: -1,
+};
+
+/**
+ * Composites a translucent `semantic/button` hover token over the button's own fill. Assigning one
+ * to `backgroundColor` would replace the base fill rather than tint it.
+ */
+const hoverOverlay = (fill: string): CSSObject => ({
+ position: 'relative',
+ isolation: 'isolate',
+ '&::before': {
+ ...insetLayer,
+ transition: `background-color ${motion.duration.hover}ms ${motion.easing.standard}`,
+ },
+ '&:hover::before, &.Mui-focusVisible::before, &[aria-expanded="true"]::before': {
+ backgroundColor: fill,
+ },
+});
+
+/**
+ * The shared resting fill for the opaque "white pill" surfaces — the pill button variants and the
+ * Select trigger — so the tokens live here once instead of being restated ~470 lines apart. bg-3 in
+ * both modes, so it needs no `darkScheme` override.
+ */
+const surfaceFill = {
+ backgroundColor: figVars['bg-3'],
+ boxShadow: figSurfaceShadow(),
+};
+/** Opaque hover step for the Select trigger, which tints by fill rather than by overlay. */
+const surfaceFillHover = { backgroundColor: figVars['bg-4-hover'], boxShadow: figSurfaceShadow() };
+
+/**
+ * The "white pill" buttons, per the Figma `semantic/button` scale. Both sit on `surfaceFill` with a
+ * hairline ring instead of a border; they differ only in dark-mode fill and hover strength, so one
+ * factory keeps them from drifting. On hover the ring is re-asserted — the global `disableElevation`
+ * default otherwise strips it — and `border` is forced to none to suppress MUI's default outlined
+ * hover border.
+ */
+const pillStyle = (hoverToken: FigmaColorName, darkFill?: FigmaColorName) => ({
+ ...surfaceFill,
+ ...(darkFill ? darkScheme({ backgroundColor: figVars[darkFill] }) : {}),
+ ...hoverOverlay(figVars[hoverToken]),
+ color: figVars['fg-1'],
+ border: 'none',
+ '& .MuiButton-startIcon': {
+ color: figVars['fg-3'],
+ },
+ '&:hover, &.Mui-focusVisible, &[aria-expanded="true"]': {
+ boxShadow: figSurfaceShadow(),
+ border: 'none',
+ },
+});
+
+/** Secondary: bg-3 in both modes. */
+const secondaryPillStyle = pillStyle('button-hover-secondary');
+/** Tertiary: one step up the dark ramp, with a stronger hover tint. */
+const tertiaryPillStyle = pillStyle('button-hover-tertiary', 'bg-4');
+
+/** Shared disabled state for both pill variants. */
+const pillDisabled = {
+ color: figVars['fg-3'],
+ border: 'none',
+ boxShadow: figSurfaceShadow(),
+};
+
+// Alert severity surface: a gradient from the severity colour (left) fading to bg-2 (right), plus
+// the full colour + a 20% tint behind/inside the icon box. The two modes differ only in `tint` —
+// dark lifts it so the wash stays visible against the darker canvas — so the gradient itself is
+// written once here rather than duplicated into the dark override.
+const severityGradient = (color: string, tint: string) =>
+ `linear-gradient(90deg, color-mix(in srgb, ${color} ${tint}, transparent) 0%, ${figVars['bg-2']} 100%), ${figVars['bg-2']}`;
+
+const alertSeverityStyle = (color: string): CSSObject => ({
+ background: severityGradient(color, '3%'),
+ '.MuiAlert-icon': {
+ color,
+ backgroundColor: `color-mix(in srgb, ${color} 20%, transparent)`,
+ },
+ ...darkScheme({ background: severityGradient(color, '5%') }),
+});
+
+// Shared box geometry for the custom selection-control icons (checkbox + radio).
+const checkboxIconBox = { width: 18, height: 18, borderRadius: '0.375rem' };
+
+// Keyboard-focus ring shared by the buttons and the selection controls / switch: a 2px ring in the
+// element's own colour, offset 3px out.
+const focusRing = { outline: '2px solid currentColor', outlineOffset: '3px' } as const;
+
+// Selection-control (checkbox + radio) icon recipes — shared so the two never drift. The unchecked
+// box is transparent (it picks up whatever surface it sits on) with an inset border-0 hairline that
+// darkens to fg-4 on hover (keyed to the shared .MuiButtonBase-root both controls carry, so one
+// selector covers both); the checked box is a purple-1 fill centered on its glyph. Radio spreads
+// these and overrides borderRadius to a circle.
+const selectionControlResting = {
+ ...checkboxIconBox,
+ backgroundColor: 'transparent',
+ boxShadow: `inset 0 0 0 1px ${figVars['border-0']}`,
+ boxSizing: 'border-box' as const,
+ '.MuiButtonBase-root:hover &': {
+ boxShadow: `inset 0 0 0 1px ${figVars['fg-4']}`,
+ },
+ // Keyboard-focus ring (see `focusRing`), hugging the icon box. The focus class lands on the
+ // shared ButtonBase root, so key it off that.
+ '.MuiButtonBase-root.Mui-focusVisible &': focusRing,
+};
+const selectionControlChecked = {
+ ...checkboxIconBox,
+ backgroundColor: figVars['purple-1'],
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ // Keyboard-focus ring (see `focusRing`).
+ '.MuiButtonBase-root.Mui-focusVisible &': focusRing,
+};
+const selectionControlRootReset = {
+ root: {
+ '&:hover, &.Mui-focusVisible': {
+ backgroundColor: 'transparent',
+ },
+ },
+};
+
+// Soft shadow under the Switch's thumb.
+const controlThumbShadow = '0px 1px 1px rgba(0, 0, 0, 0.12)';
const theme = createTheme();
const {
typography: { pxToRem },
} = theme;
-const FONT = 'Inter, Arial';
+const FONT = "'Inter Variable', Inter, Arial";
declare module '@mui/material/styles/createPalette' {
interface PaletteColor extends ColorPartial {}
@@ -30,30 +197,18 @@ declare module '@mui/material/styles/createPalette' {
default: string;
paper: string;
surface: string;
- surface2: string;
- header: string;
- disabled: string;
}
- interface Palette {
- gradients: {
- aaveGradient: string;
- newGradient: string;
- };
- other: {
- standardInputLine: string;
- };
- }
+ // Design tokens are flattened onto the palette root (see `getDesignTokens`), so each token is
+ // a first-class palette member. This also turns a token name that collides with a built-in
+ // palette key (e.g. `error`, `background`) into a compile error rather than a silent overwrite.
+ interface Palette extends Record {}
- interface PaletteOptions {
- gradients: {
- aaveGradient: string;
- newGradient: string;
- };
- }
+ interface PaletteOptions extends Partial> {}
}
interface TypographyCustomVariants {
+ base: React.CSSProperties;
display1: React.CSSProperties;
subheader1: React.CSSProperties;
subheader2: React.CSSProperties;
@@ -62,15 +217,9 @@ interface TypographyCustomVariants {
buttonM: React.CSSProperties;
buttonS: React.CSSProperties;
helperText: React.CSSProperties;
- tooltip: React.CSSProperties;
- main21: React.CSSProperties;
secondary21: React.CSSProperties;
- main16: React.CSSProperties;
secondary16: React.CSSProperties;
- main14: React.CSSProperties;
- secondary14: React.CSSProperties;
main12: React.CSSProperties;
- secondary12: React.CSSProperties;
}
declare module '@mui/material/styles' {
@@ -89,6 +238,7 @@ declare module '@mui/material/styles' {
// Update the Typography's variant prop options
declare module '@mui/material/Typography' {
interface TypographyPropsVariantOverrides {
+ base: true;
display1: true;
subheader1: true;
subheader2: true;
@@ -97,16 +247,10 @@ declare module '@mui/material/Typography' {
buttonM: true;
buttonS: true;
helperText: true;
- tooltip: true;
- main21: true;
secondary21: true;
- main16: true;
secondary16: true;
- main14: true;
- secondary14: true;
main12: true;
- secondary12: true;
- h5: false;
+ h5: true;
h6: false;
subtitle1: false;
subtitle2: false;
@@ -117,99 +261,97 @@ declare module '@mui/material/Typography' {
}
}
+// Add a `tertiary` button variant (the secondary pill minus its ring/shadow).
declare module '@mui/material/Button' {
interface ButtonPropsVariantOverrides {
- surface: true;
- gradient: true;
+ tertiary: true;
+ }
+}
+
+declare module '@mui/material/Paper' {
+ interface PaperPropsVariantOverrides {
+ modal: true;
+ card: true;
+ table: true;
}
}
export const getDesignTokens = (mode: 'light' | 'dark') => {
- const getColor = (lightColor: string, darkColor: string) =>
- mode === 'dark' ? darkColor : lightColor;
+ const t = pickFigma(mode); // ← the one line of setup
return {
breakpoints: {
- keys: ['xs', 'xsm', 'sm', 'md', 'lg', 'xl', 'xxl'],
+ keys: ['xs', 'xsm', 'sm', 'md', 'mdlg', 'lg', 'xl', 'xxl'],
values: { xs: 0, xsm: 640, sm: 760, md: 960, mdlg: 1125, lg: 1280, xl: 1575, xxl: 1800 },
},
palette: {
mode,
+ // Design tokens flattened onto the palette root → MUI generates a `--mui-palette-`
+ // var per token, so `sx={{ bgcolor: 'bg-1' }}` and `figVars['bg-1']` both resolve to it.
+ ...t,
primary: {
- main: getColor('#383D51', '#EAEBEF'),
- light: getColor('#62677B', '#F1F1F3'),
- dark: getColor('#292E41', '#D2D4DC'),
- contrast: getColor('#FFFFFF', '#0F121D'),
+ main: t['fg-1'],
+ light: t['fg-2'],
+ dark: t['fg-max'],
+ contrastText: t['bg-1'],
},
secondary: {
- main: getColor('#FF607B', '#F48FB1'),
- light: getColor('#FF607B', '#F6A5C0'),
- dark: getColor('#B34356', '#AA647B'),
+ main: t['secondary-main'],
+ light: t['secondary-light'],
+ dark: t['secondary-dark'],
},
error: {
- main: getColor('#BC0000B8', '#F44336'),
- light: getColor('#D26666', '#E57373'),
- dark: getColor('#BC0000', '#D32F2F'),
- '100': getColor('#4F1919', '#FBB4AF'), // for alert text
- '200': getColor('#F9EBEB', '#2E0C0A'), // for alert background
+ main: t['red-1'],
+ light: t['error-light'],
+ dark: t['error-dark'],
+ '100': t['error-text'], // alert text
+ '200': t['error-bg'], // alert background
},
warning: {
- main: getColor('#F89F1A', '#FFA726'),
- light: getColor('#FFCE00', '#FFB74D'),
- dark: getColor('#C67F15', '#F57C00'),
- '100': getColor('#63400A', '#FFDCA8'), // for alert text
- '200': getColor('#FEF5E8', '#301E04'), // for alert background
+ main: t['yellow-1'],
+ light: t['warning-light'],
+ dark: t['warning-dark'],
+ '100': t['warning-text'],
+ '200': t['warning-bg'],
},
info: {
- main: getColor('#0062D2', '#29B6F6'),
- light: getColor('#0062D2', '#4FC3F7'),
- dark: getColor('#002754', '#0288D1'),
- '100': getColor('#002754', '#A9E2FB'), // for alert text
- '200': getColor('#E5EFFB', '#071F2E'), // for alert background
+ main: t['blue-1'],
+ light: t['info-light'],
+ dark: t['info-dark'],
+ '100': t['info-text'],
+ '200': t['info-bg'],
},
success: {
- main: getColor('#4CAF50', '#66BB6A'),
- light: getColor('#90FF95', '#90FF95'),
- dark: getColor('#318435', '#388E3C'),
- '100': getColor('#1C4B1E', '#C2E4C3'), // for alert text
- '200': getColor('#ECF8ED', '#0A130B'), // for alert background
+ main: t['data-green'],
+ light: t['success-light'],
+ dark: t['success-dark'],
+ '100': t['success-text'],
+ '200': t['success-bg'],
},
text: {
- primary: getColor('#303549', '#F1F1F3'),
- secondary: getColor('#62677B', '#A5A8B6'),
- disabled: getColor('#D2D4DC', '#62677B'),
- muted: getColor('#A5A8B6', '#8E92A3'),
- highlight: getColor('#383D51', '#C9B3F9'),
+ primary: t['fg-1'],
+ secondary: t['fg-2'],
+ disabled: t['fg-4'],
+ muted: t['fg-3'],
},
background: {
- default: getColor('#F1F1F3', '#1B2030'),
- paper: getColor('#FFFFFF', '#292E41'),
- surface: getColor('#F7F7F9', '#383D51'),
- surface2: getColor('#F9F9FB', '#383D51'),
- header: getColor('#2B2D3C', '#1B2030'),
- disabled: getColor('#EAEBEF', '#EBEBEF14'),
- },
- divider: getColor('#EAEBEF', '#EBEBEF14'),
- action: {
- active: getColor('#8E92A3', '#EBEBEF8F'),
- hover: getColor('#F1F1F3', '#EBEBEF14'),
- selected: getColor('#EAEBEF', '#EBEBEF29'),
- disabled: getColor('#BBBECA', '#EBEBEF4D'),
- disabledBackground: getColor('#EAEBEF', '#EBEBEF1F'),
- focus: getColor('#F1F1F3', '#EBEBEF1F'),
- },
- other: {
- standardInputLine: getColor('#383D511F', '#EBEBEF6B'),
+ default: t['bg-5'],
+ paper: t['surface-elevated'],
+ surface: t['bg-2'],
},
- gradients: {
- aaveGradient: 'linear-gradient(248.86deg, #B6509E 10.51%, #2EBAC6 93.41%)',
- newGradient: 'linear-gradient(79.67deg, #8C3EBC 0%, #007782 95.82%)',
+ divider: t['border-0'],
+ action: {
+ active: t['fg-3'],
+ hover: t['button-hover'],
+ selected: t['selected'],
+ disabled: t['disabled-fg'],
+ disabledBackground: t['disabled-bg'],
+ focus: t['focus'],
},
},
spacing: 4,
typography: {
fontFamily: FONT,
- h5: undefined,
h6: undefined,
subtitle1: undefined,
subtitle2: undefined,
@@ -233,16 +375,14 @@ export const getDesignTokens = (mode: 'light' | 'dark') => {
},
h2: {
fontFamily: FONT,
- fontWeight: 600,
- letterSpacing: 'unset',
- lineHeight: '133.4%',
- fontSize: pxToRem(21),
+ fontWeight: 500,
+ lineHeight: '120%',
+ fontSize: pxToRem(24),
},
h3: {
fontFamily: FONT,
- fontWeight: 600,
- letterSpacing: pxToRem(0.15),
- lineHeight: '160%',
+ fontWeight: 500,
+ lineHeight: '120%',
fontSize: pxToRem(18),
},
h4: {
@@ -252,6 +392,12 @@ export const getDesignTokens = (mode: 'light' | 'dark') => {
lineHeight: pxToRem(24),
fontSize: pxToRem(16),
},
+ h5: {
+ fontFamily: FONT,
+ fontWeight: 500,
+ lineHeight: pxToRem(18),
+ fontSize: pxToRem(14),
+ },
subheader1: {
fontFamily: FONT,
fontWeight: 600,
@@ -266,6 +412,12 @@ export const getDesignTokens = (mode: 'light' | 'dark') => {
lineHeight: pxToRem(16),
fontSize: pxToRem(12),
},
+ base: {
+ fontFamily: FONT,
+ fontWeight: 400,
+ lineHeight: '100%',
+ fontSize: pxToRem(14),
+ },
description: {
fontFamily: FONT,
fontWeight: 400,
@@ -290,7 +442,8 @@ export const getDesignTokens = (mode: 'light' | 'dark') => {
buttonM: {
fontFamily: FONT,
fontWeight: 500,
- lineHeight: pxToRem(24),
+ letterSpacing: '-0.00563rem',
+ lineHeight: '1.25rem',
fontSize: pxToRem(14),
},
buttonS: {
@@ -308,32 +461,12 @@ export const getDesignTokens = (mode: 'light' | 'dark') => {
lineHeight: pxToRem(12),
fontSize: pxToRem(10),
},
- tooltip: {
- fontFamily: FONT,
- fontWeight: 400,
- letterSpacing: pxToRem(0.15),
- lineHeight: pxToRem(16),
- fontSize: pxToRem(12),
- },
- main21: {
- fontFamily: FONT,
- fontWeight: 800,
- lineHeight: '133.4%',
- fontSize: pxToRem(21),
- },
secondary21: {
fontFamily: FONT,
fontWeight: 500,
lineHeight: '133.4%',
fontSize: pxToRem(21),
},
- main16: {
- fontFamily: FONT,
- fontWeight: 600,
- letterSpacing: pxToRem(0.15),
- lineHeight: pxToRem(24),
- fontSize: pxToRem(16),
- },
secondary16: {
fontFamily: FONT,
fontWeight: 500,
@@ -341,20 +474,6 @@ export const getDesignTokens = (mode: 'light' | 'dark') => {
lineHeight: pxToRem(24),
fontSize: pxToRem(16),
},
- main14: {
- fontFamily: FONT,
- fontWeight: 600,
- letterSpacing: pxToRem(0.15),
- lineHeight: pxToRem(20),
- fontSize: pxToRem(14),
- },
- secondary14: {
- fontFamily: FONT,
- fontWeight: 500,
- letterSpacing: pxToRem(0.15),
- lineHeight: pxToRem(20),
- fontSize: pxToRem(14),
- },
main12: {
fontFamily: FONT,
fontWeight: 600,
@@ -362,18 +481,43 @@ export const getDesignTokens = (mode: 'light' | 'dark') => {
lineHeight: pxToRem(16),
fontSize: pxToRem(12),
},
- secondary12: {
- fontFamily: FONT,
- fontWeight: 500,
- letterSpacing: pxToRem(0.1),
- lineHeight: pxToRem(16),
- fontSize: pxToRem(12),
- },
},
} as ThemeOptions;
};
-export function getThemedComponents(theme: Theme) {
+/**
+ * Subtle press feedback shared by buttons and dropdown triggers: the control scales down
+ * slightly while active (pointer/touch down), and never when disabled. Pair with a `transform`
+ * transition (at `motion.duration.hover`) so the release animates back. Reduced-motion users
+ * get the scale instantly via the global `prefers-reduced-motion` rule in MuiCssBaseline.
+ */
+const pressScaleActive = {
+ '&:active:not(.Mui-disabled)': {
+ transform: 'scale(0.99)',
+ },
+};
+
+/**
+ * Disabled button treatment: the label/icon stay crisp while the button's own background (+ box
+ * shadow) render at 50% on an `opacity: 0.5` `::before` layer. Opacity is used (not color-mix /
+ * channel alpha) so the faded fill keeps its Display-P3 color; a box-shadow also has no opacity of
+ * its own, so fading a layer is the only clean way to halve it. `isolation: isolate` makes the root
+ * a stacking context so the `z-index: -1` layer sits behind the label, not behind the parent bg.
+ */
+const disabledFade = (opts: { color: string; before: CSSObject }): CSSObject => ({
+ color: opts.color,
+ backgroundColor: 'transparent',
+ border: 'none',
+ boxShadow: 'none',
+ isolation: 'isolate',
+ '&::before': {
+ ...insetLayer,
+ opacity: 0.5,
+ ...opts.before,
+ },
+});
+
+export function getThemedComponents(theme: AppTheme) {
return {
components: {
MuiSkeleton: {
@@ -386,27 +530,58 @@ export function getThemedComponents(theme: Theme) {
MuiOutlinedInput: {
styleOverrides: {
root: {
- borderRadius: '6px',
- borderColor: theme.palette.divider,
- '&:hover .MuiOutlinedInput-notchedOutline': {
- borderColor: '#CBCDD8',
+ borderRadius: '0.5rem',
+ // Text inputs (everything that isn't a Select): a bg-3 surface with the shared
+ // surface shadow (shadow-low drop + shadow-stroke-2 1px ring) instead of a border.
+ // Selects keep their own fill via the `:has(.MuiSelect-select)` block below.
+ '&:not(:has(.MuiSelect-select))': {
+ backgroundColor: figVars['bg-3'],
+ boxShadow: figSurfaceShadow(),
+ '& .MuiOutlinedInput-notchedOutline': { border: 'none' },
},
- '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
- borderColor: '#CBCDD8',
+ // Select trigger = the outlined-button surface: the same `surfaceFill` recipe the
+ // pill uses, same 0.5rem radius (from `root`). The tokens are shared rather than
+ // restated so the two can't drift. The notched border is dropped — the ring IS the
+ // outline — so there's no blueish or animated border; hover & open step the fill while
+ // the ring stays put. `pillStyle` itself isn't spread here: its fg-1 color,
+ // start-icon selector and `[aria-expanded]` selector are all wrong for an input (the
+ // attribute lands on the inner `.MuiSelect-select`, hence the `:has()` below).
+ '&:has(.MuiSelect-select)': {
+ ...surfaceFill,
+ // Animate the hover/open fill+ring step (was instant — the root had no transition).
+ transition: theme.transitions.create(['background-color', 'box-shadow'], {
+ duration: motion.duration.hover,
+ }),
+ '& .MuiOutlinedInput-notchedOutline': { border: 'none' },
+ // Open fill is keyed to the Select's actual open state (`aria-expanded` on the
+ // select), NOT `.Mui-focused`: a Select keeps focus after its menu closes, so a
+ // focus-based fill would linger after closing and while other fields are focused.
+ '&:hover, &:has(.MuiSelect-select[aria-expanded="true"])': {
+ ...surfaceFillHover,
+ '& .MuiOutlinedInput-notchedOutline': { border: 'none' },
+ },
+ // Keyboard-focus ring only (browser deems focus visible → keyboard nav, not the
+ // focus MUI restores to the trigger on close). Matches the outlined-button ring.
+ '&:has(.MuiSelect-select:focus-visible)': {
+ outline: `2px solid ${figVars['fg-1']}`,
+ outlineOffset: '3px',
+ },
+ // Disabled dropdown: inert to hover / pointer / touch (no hover fill step, no
+ // pointer cursor). The faded look still comes from MUI's `.Mui-disabled` text.
+ '&.Mui-disabled': {
+ pointerEvents: 'none',
+ },
},
},
},
},
- MuiSlider: {
- styleOverrides: {
- root: {
- '& .MuiSlider-thumb': {
- color: theme.palette.mode === 'light' ? '#62677B' : '#C9B3F9',
- },
- '& .MuiSlider-track': {
- color: theme.palette.mode === 'light' ? '#383D51' : '#9C93B3',
- },
- },
+ MuiButtonBase: {
+ defaultProps: {
+ // No ripple / pressed "splash" on any control (menu items, buttons, icon
+ // buttons, toggles, checkboxes, tabs, …). Interaction is conveyed by hover,
+ // keyboard focus, and the press-scale — not MUI's ripple. Set on ButtonBase so
+ // it covers every ButtonBase-derived component in one place.
+ disableRipple: true,
},
},
MuiButton: {
@@ -415,55 +590,155 @@ export function getThemedComponents(theme: Theme) {
},
styleOverrides: {
root: {
- borderRadius: '4px',
+ // Size to content + padding, not MUI's default 64px floor (which let buttons in tight
+ // flex rows squish below their content). Row action buttons re-add an even floor
+ // locally (ListButtonsColumn); deliberate collapses keep their own minWidth: 0.
+ minWidth: 'unset',
+ // Never wrap the label to a second line — buttons size to their text and stay one line
+ // even in tight flex rows (e.g. the sGHO markets banner's action row).
+ whiteSpace: 'nowrap',
+ // Hover/focus state transition at 100ms (overrides MUI's 250ms default).
+ // `transform` is included so the active-press scale animates in and out.
+ transition: theme.transitions.create(
+ ['background-color', 'box-shadow', 'border-color', 'color', 'transform'],
+ { duration: motion.duration.hover }
+ ),
+ // Subtle press feedback — scale down while active (not when disabled).
+ ...pressScaleActive,
+ // Keyboard-focus ring in the variant's own text color; ButtonBase zeroes the
+ // native outline, so we set our own (2px, offset 3px out).
+ '&.Mui-focusVisible': {
+ outline: '2px solid currentColor',
+ outlineOffset: '3px',
+ },
},
sizeLarge: {
...theme.typography.buttonL,
- padding: '10px 24px',
+ height: '48px',
+ padding: '0 24px',
+ borderRadius: '0.625rem',
},
sizeMedium: {
...theme.typography.buttonM,
- padding: '6px 12px',
+ height: '36px',
+ // Text-side padding; a start/end icon's -4px slot margin (MUI default) tightens
+ // the icon side to ~10px automatically.
+ padding: '0 0.88rem',
+ borderRadius: '0.5rem',
},
sizeSmall: {
- ...theme.typography.buttonS,
- padding: '0 6px',
+ // v3: small buttons use buttonM (14px / 500 / no uppercase) + 0.62rem side padding —
+ // the same label style as sizeMedium, at a compact height. (Was the legacy buttonS:
+ // uppercase 10px / 6px padding, which the button rework never migrated.)
+ ...theme.typography.buttonM,
+ height: '28px',
+ padding: '0 0.62rem',
+ borderRadius: '0.375rem',
},
},
variants: [
+ // Secondary pill (`variant="outlined"`): bg-3 in both modes.
{
- props: { variant: 'surface' },
+ props: { color: 'primary', variant: 'outlined' },
style: {
- color: theme.palette.common.white,
- border: '1px solid',
- borderColor: '#EBEBED1F',
- backgroundColor: '#383D51',
- '&:hover, &.Mui-focusVisible': {
- backgroundColor: theme.palette.background.header,
- },
+ ...secondaryPillStyle,
+ '&.Mui-disabled': pillDisabled,
},
},
{
- props: { variant: 'gradient' },
+ props: { variant: 'contained', color: 'primary' },
style: {
- color: theme.palette.common.white,
- background: theme.palette.gradients.aaveGradient,
- transition: 'all 0.2s ease',
- '&:hover, &.Mui-focusVisible': {
- background: theme.palette.gradients.aaveGradient,
- opacity: '0.9',
+ backgroundColor: figVars['fg-1'],
+ // Same lift as the outlined pill, but ringed in the button's own fill (not
+ // shadow-stroke-2) — the opaque bg already reads as a boundary, so the ring just
+ // needs to disappear into it while the drop-shadow layer still adds the lift.
+ boxShadow: figSurfaceShadow('fg-1'),
+ ...hoverOverlay(figVars['button-hover-primary']),
+ // The root focus ring uses `currentColor`, which here is contrastText (bg-1) —
+ // nearly the same shade as the page background, so it's invisible. Re-point it at
+ // fg-1 (same ink the outlined variant's ring uses) so it reads against the page.
+ '&.Mui-focusVisible': {
+ outlineColor: figVars['fg-1'],
},
+ // Disabled: crisp label, fg-1 fill at 50% (no box-shadow on contained).
+ '&.Mui-disabled': disabledFade({
+ color: figVars['bg-1'],
+ before: { backgroundColor: figVars['fg-1'] },
+ }),
},
},
+ // Tertiary pill: the app's default button.
{
- props: { color: 'primary', variant: 'outlined' },
+ props: { variant: 'tertiary', color: 'primary' },
style: {
- background: theme.palette.background.surface,
- borderColor: theme.palette.divider,
+ ...tertiaryPillStyle,
+ '&.Mui-disabled': pillDisabled,
},
},
],
},
+ MuiIconButton: {
+ styleOverrides: {
+ root: {
+ transition: theme.transitions.create(['background-color', 'color', 'transform'], {
+ duration: motion.duration.hover,
+ }),
+ // Subtle press feedback — scale down while active (not when disabled).
+ ...pressScaleActive,
+ // Keep the hover fill while the menu this button opens is expanded (open === hover).
+ // MUI's IconButton hover is `action.hover` (= button-hover), so match it.
+ '&[aria-expanded="true"]': {
+ backgroundColor: figVars['button-hover'],
+ },
+ },
+ },
+ },
+ MuiToggleButton: {
+ styleOverrides: {
+ root: {
+ transition: theme.transitions.create(
+ ['background-color', 'color', 'transform', 'opacity'],
+ {
+ duration: motion.duration.hover,
+ }
+ ),
+ // Subtle press feedback — scale down while active (not when disabled).
+ ...pressScaleActive,
+ },
+ },
+ },
+ MuiCheckbox: {
+ defaultProps: {
+ icon: ,
+ checkedIcon: (
+
+
+
+
+
+ ),
+ },
+ styleOverrides: selectionControlRootReset,
+ },
+ MuiRadio: {
+ defaultProps: {
+ // Circular twin of the custom checkbox — shares its recipe, overriding the shape.
+ icon: ,
+ checkedIcon: (
+
+
+
+ ),
+ },
+ styleOverrides: selectionControlRootReset,
+ },
MuiTypography: {
defaultProps: {
variant: 'description',
@@ -473,23 +748,19 @@ export function getThemedComponents(theme: Theme) {
h2: 'h2',
h3: 'h3',
h4: 'h4',
+ h5: 'p',
subheader1: 'p',
subheader2: 'p',
caption: 'p',
+ base: 'p',
description: 'p',
buttonL: 'p',
buttonM: 'p',
buttonS: 'p',
main12: 'p',
- main14: 'p',
- main16: 'p',
- main21: 'p',
- secondary12: 'p',
- secondary14: 'p',
secondary16: 'p',
secondary21: 'p',
helperText: 'span',
- tooltip: 'span',
},
},
},
@@ -500,21 +771,56 @@ export function getThemedComponents(theme: Theme) {
},
MuiMenu: {
defaultProps: {
+ // Menu hard-defaults transitionDuration='auto' and forwards it explicitly,
+ // shadowing the MuiPopover default below — so menus/selects need the duration
+ // set here too. TransitionComponent is set explicitly as well (rather than
+ // relying on the inner Popover's own default) to keep the theme authoritative.
+ TransitionComponent: ScaleFade,
+ transitionDuration: motion.duration.overlay,
PaperProps: {
- elevation: 0,
variant: 'outlined',
style: {
minWidth: 240,
- marginTop: '4px',
},
},
},
+ styleOverrides: {
+ // Own the dropdown paper's look HERE (not only via PaperProps) so it survives
+ // components that inject their own paper slotProps and drop the theme's PaperProps —
+ // most notably Select, whose menu would otherwise lose the 8px offset + outlined
+ // surface and look nothing like our other dropdowns. `&&` outweighs the MuiPaper
+ // variant styles. With the 0.38rem list inset + 2rem rows (MuiMenuItem), every
+ // dropdown (Selects included) matches the settings menu.
+ paper: {
+ '&&': {
+ marginTop: '8px',
+ borderRadius: MENU_PAPER_RADIUS,
+ border: 'none',
+ boxShadow: figSurfaceShadow(),
+ backgroundColor: figVars['surface-elevated'],
+ },
+ // Dark surface at the SAME doubled specificity as the light fill above, so it wins
+ // in dark mode. (The darkScheme helper's single `&` lost to `&&`, which left the
+ // light paper — and light-looking options — showing in dark mode.)
+ '*:where([data-mui-color-scheme="dark"]) &&': {
+ backgroundColor: figVars['bg-2'],
+ },
+ '.MuiList-root': { padding: MENU_LIST_INSET },
+ },
+ },
+ },
+ MuiPopover: {
+ // Covers raw Popover usages (MarketSwitcher desktop, multiselects, swap inputs).
+ defaultProps: {
+ TransitionComponent: ScaleFade,
+ transitionDuration: motion.duration.overlay,
+ },
},
MuiList: {
styleOverrides: {
root: {
- '.MuiMenuItem-root+.MuiDivider-root, .MuiDivider-root': {
- marginTop: '4px',
+ '.MuiDivider-root': {
+ marginTop: '8px',
marginBottom: '4px',
},
},
@@ -527,21 +833,59 @@ export function getThemedComponents(theme: Theme) {
MuiMenuItem: {
styleOverrides: {
root: {
- padding: '12px 16px',
+ minHeight: '2rem',
+ // MUI relaxes MenuItem min-height to `auto` at ≥sm; re-assert 2rem there so
+ // every option row is a firm 2rem tall on desktop too.
+ [theme.breakpoints.up('sm')]: { minHeight: '2rem' },
+ padding: '0.31rem 0.38rem',
+ // The hover/selected highlight is a pseudo-element inset 1px top & bottom, so
+ // adjacent highlights keep a small gap while the row itself stays full-height — the
+ // hover target is continuous, so moving between rows never interrupts the highlight.
+ // Shared recipe (geometry + motion) lives in insetHighlight.ts; the radius is kept
+ // concentric with the menu paper (paper radius − list inset).
+ ...insetHighlightBase({
+ theme,
+ radius: `calc(${MENU_PAPER_RADIUS} - ${MENU_LIST_INSET})`,
+ top: '1px',
+ bottom: '1px',
+ }),
+ // Hover, keyboard focus (arrow-key nav sets .Mui-focusVisible), and the selected row
+ // all share one subtle highlight — the button-hover fill, never MUI's primary tint.
+ '&:hover::before, &.Mui-focusVisible::before, &.Mui-selected::before':
+ insetHighlightActive(figVars['button-hover']),
+ // Highlight lives on the pseudo above — keep the row's own background clear.
+ // The compound selected states are listed explicitly: MUI's base MenuItem paints
+ // `&.Mui-selected:hover` / `&.Mui-selected.Mui-focusVisible` with a primary tint at
+ // higher specificity than a lone `&.Mui-selected`, so without these the selected row
+ // would show a stronger fill than other rows on hover/keyboard-focus.
+ '&:hover, &.Mui-focusVisible, &.Mui-selected, &.Mui-selected:hover, &.Mui-selected.Mui-focusVisible':
+ {
+ backgroundColor: 'transparent',
+ },
+ // A row's leading icon sits one step back from its label, exactly like a button's
+ // start-icon (fg-3 icon against fg-1 text — see `pillStyle`). Scoped to a
+ // DIRECT SvgIcon child so it only catches currentColor UI icons; brand artwork
+ // (TokenIcon, MarketLogo) is ``-based and unaffected.
+ '& > .MuiSvgIcon-root': {
+ color: figVars['fg-3'],
+ },
},
},
},
MuiListItemText: {
styleOverrides: {
root: {
- ...theme.typography.subheader1,
+ ...theme.typography.subheader2,
+ fontSize: pxToRem(14),
+ fontWeight: 400,
+ lineHeight: pxToRem(14),
},
},
},
MuiListItemIcon: {
styleOverrides: {
root: {
- color: theme.palette.primary.light,
+ color: theme.vars.palette.primary.light,
minWidth: 'unset !important',
marginRight: '12px',
},
@@ -558,26 +902,58 @@ export function getThemedComponents(theme: Theme) {
MuiPaper: {
styleOverrides: {
root: {
- borderRadius: '4px',
+ borderRadius: '8px',
},
},
variants: [
{
props: { variant: 'outlined' },
style: {
- border: `1px solid ${theme.palette.divider}`,
- boxShadow: '0px 0px 2px rgba(0, 0, 0, 0.2), 0px 2px 10px rgba(0, 0, 0, 0.1)',
- background:
- theme.palette.mode === 'light'
- ? theme.palette.background.paper
- : theme.palette.background.surface,
+ border: 'none',
+ boxShadow: figSurfaceShadow(),
+ background: figVars['surface-elevated'],
+ ...darkScheme({
+ background: figVars['bg-2'],
+ }),
},
},
{
props: { variant: 'elevation' },
style: {
boxShadow: '0px 2px 1px rgba(0, 0, 0, 0.05), 0px 0px 1px rgba(0, 0, 0, 0.25)',
- ...(theme.palette.mode === 'dark' ? { backgroundImage: 'none' } : {}),
+ ...darkScheme({ backgroundImage: 'none' }),
+ },
+ },
+ {
+ props: { variant: 'modal' },
+ style: {
+ borderRadius: '0.75rem',
+ backgroundColor: figVars['bg-1'],
+ ...darkScheme({ backgroundColor: figVars['bg-2'] }),
+ boxShadow: `0 0 0 1px ${figVars['border-1']}, 0 4px 16px 0 ${figVars['shadow-medium']}`,
+ },
+ },
+ {
+ // Canonical content card surface — the module cards (reserve-overview, staking, sGho,
+ // …). surface-elevated in light / bg-2 in dark, 10px radius, the shared surface ring
+ // (shadow-stroke-1 hairline + soft drop). Asset tables use the `table` variant below.
+ props: { variant: 'card' },
+ style: {
+ backgroundColor: figVars['surface-elevated'],
+ ...darkScheme({ backgroundColor: figVars['bg-2'] }),
+ borderRadius: '10px',
+ boxShadow: figSurfaceShadow('shadow-stroke-1'),
+ },
+ },
+ {
+ // The `card` surface on the table fill — the single source of truth for ListWrapper and
+ // the standalone asset tables. Only differs from `card` in dark mode, so a table left on
+ // `card` by mistake is invisible in light and wrong in dark.
+ props: { variant: 'table' },
+ style: {
+ backgroundColor: figVars['table-bg'],
+ borderRadius: '10px',
+ boxShadow: figSurfaceShadow('shadow-stroke-1'),
},
},
],
@@ -589,10 +965,8 @@ export function getThemedComponents(theme: Theme) {
flexDirection: 'column',
flex: 1,
paddingBottom: '39px',
- [theme.breakpoints.up('xs')]: {
- paddingLeft: '8px',
- paddingRight: '8px',
- },
+ paddingLeft: '8px',
+ paddingRight: '8px',
[theme.breakpoints.up('xsm')]: {
paddingLeft: '20px',
paddingRight: '20px',
@@ -601,18 +975,29 @@ export function getThemedComponents(theme: Theme) {
paddingLeft: '48px',
paddingRight: '48px',
},
+ // 20px, not the 96px this used to carry. The box is still uncapped here, so padding IS
+ // the gutter: 96px made the content *narrower* at 960 (863px → 768px) than it was at
+ // 959, and left it 152px behind the page content all the way to 1279 — the header and
+ // footer visibly disagreed with the page they framed. This ladder must stay identical
+ // to whatever a page's own Container resolves to, or the two drift apart again.
[theme.breakpoints.up('md')]: {
- paddingLeft: '96px',
- paddingRight: '96px',
+ paddingLeft: '20px',
+ paddingRight: '20px',
},
[theme.breakpoints.up('lg')]: {
paddingLeft: '20px',
paddingRight: '20px',
+ maxWidth: '1280px',
},
+ // The `xl` gutter is only safe because `maxWidth` rises with it: 96px of padding inside
+ // a box capped at 1632px still yields 1440px of content (1632 − 2×96), so content grows
+ // 1383px → 1440px across 1575–1632 and then holds, meeting `xxl` exactly. Raising this
+ // padding *without* lifting the cap is the old bug — it takes width from the content
+ // instead of adding outer gutter. Never pad a capped box without widening the cap.
[theme.breakpoints.up('xl')]: {
- maxWidth: 'unset',
paddingLeft: '96px',
paddingRight: '96px',
+ maxWidth: '1632px',
},
[theme.breakpoints.up('xxl')]: {
paddingLeft: 0,
@@ -625,34 +1010,55 @@ export function getThemedComponents(theme: Theme) {
MuiSwitch: {
styleOverrides: {
root: {
- height: 20 + 6 * 2,
- width: 34 + 6 * 2,
- padding: 6,
+ width: '1.75rem',
+ height: '1.125rem',
+ padding: 0,
+ flexShrink: 0,
+ borderRadius: '9px',
+ // Keyboard-focus ring (see `focusRing`). The focus class lands on the inner
+ // switchBase, so key the root's ring off it.
+ '&:has(.Mui-focusVisible)': focusRing,
},
switchBase: {
- padding: 8,
+ padding: 0,
+ margin: '2px',
'&.Mui-checked': {
- transform: 'translateX(14px)',
+ transform: 'translateX(10px)',
'& + .MuiSwitch-track': {
- backgroundColor: theme.palette.success.main,
+ backgroundColor: figVars['purple-1'],
opacity: 1,
},
},
'&.Mui-disabled': {
- opacity: theme.palette.mode === 'dark' ? 0.3 : 0.7,
+ opacity: 0.7,
+ ...darkScheme({ opacity: 0.3 }),
},
},
thumb: {
- color: theme.palette.common.white,
- borderRadius: '6px',
- width: '16px',
- height: '16px',
- boxShadow: '0px 1px 1px rgba(0, 0, 0, 0.12)',
+ color: onAccent,
+ borderRadius: '50%',
+ width: '14px',
+ height: '14px',
+ boxShadow: controlThumbShadow,
},
track: {
opacity: 1,
- backgroundColor: theme.palette.action.active,
- borderRadius: '8px',
+ backgroundColor: figVars['bg-6'],
+ borderRadius: '9px',
+ },
+ },
+ },
+ MuiFormControlLabel: {
+ styleOverrides: {
+ root: {
+ // A Switch has no internal padding, so MUI's default -11px label offset (meant for
+ // padded checkboxes/radios) crams the switch against whatever precedes it in a row.
+ // Zero it for switch-labeled controls, and give the switch↔label text a 0.5rem gap.
+ // Checkbox/radio labels keep MUI's defaults.
+ '&:has(.MuiSwitch-root)': {
+ marginLeft: 0,
+ gap: '0.5rem',
+ },
},
},
},
@@ -669,129 +1075,133 @@ export function getThemedComponents(theme: Theme) {
MuiTableCell: {
styleOverrides: {
root: {
- borderColor: theme.palette.divider,
+ borderColor: figVars['border-2'],
+ },
+ // Column labels are fg-3 app-wide. MUI defaults the `head` variant to text.primary
+ // (fg-1), which reads as body ink — this pins every cell to the muted
+ // header token, matching the `ListHeaderTitle` primitive the list-based tables use.
+ head: {
+ color: figVars['fg-3'],
},
},
},
MuiAlert: {
styleOverrides: {
root: {
- boxShadow: 'none',
- borderRadius: '4px',
- padding: '8px 12px',
- ...theme.typography.caption,
+ display: 'flex',
alignItems: 'flex-start',
- '.MuiAlert-message': {
- padding: 0,
- paddingTop: '2px',
- paddingBottom: '2px',
- },
+ gap: '0.88rem',
+ padding: '1rem 1.25rem',
+ borderRadius: '0.375rem',
+ boxShadow: figSurfaceShadow(),
+ // Icon box: a 2.5rem rounded square with a border-0 hairline. Its per-severity tint
+ // fill + icon color are set in the severity variants below.
'.MuiAlert-icon': {
- padding: 0,
+ margin: 0,
+ padding: '0.625rem',
+ width: '2.5rem',
+ height: '2.5rem',
+ flexShrink: 0,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: '0.375rem',
+ boxShadow: `inset 0 0 0 1px ${figVars['border-0']}`,
opacity: 1,
'.MuiSvgIcon-root': {
- fontSize: pxToRem(20),
+ fontSize: '1rem',
+ flexShrink: 0,
},
},
- a: {
- ...theme.typography.caption,
+ // Message: Paragraph text in fg-max, centered against the icon box on a single line;
+ // multi-line grows and top-aligns via the container's flex-start.
+ '.MuiAlert-message': {
+ padding: 0,
+ alignSelf: 'center',
+ color: figVars['fg-max'],
+ fontFamily: FONT,
+ fontWeight: 400,
+ fontSize: pxToRem(14),
+ lineHeight: pxToRem(19),
+ },
+ // Title (AlertTitle): identical to the message text, one weight step up (500). No
+ // bespoke per-alert heading styling — overrides MUI's larger/heavier default + margins.
+ '.MuiAlertTitle-root': {
+ margin: 0,
+ marginBottom: '0.13rem',
+ color: figVars['fg-max'],
+ fontFamily: FONT,
fontWeight: 500,
+ fontSize: pxToRem(14),
+ lineHeight: pxToRem(19),
+ },
+ a: {
+ color: 'inherit',
+ fontWeight: 'inherit',
textDecoration: 'underline',
'&:hover': {
textDecoration: 'none',
},
},
'.MuiButton-text': {
- ...theme.typography.caption,
- fontWeight: 500,
+ // Inline buttons (copy / switch-network / …) fully match the alert text — same font
+ // size/family/line-height, no uppercase — plus an underline. Otherwise they keep MUI's
+ // button typography and render a size off (most visibly in the small variant).
+ color: 'inherit',
+ // `font` shorthand inherits family/size/weight/line-height in one go; letter-spacing
+ // isn't part of it, so inherit that separately.
+ font: 'inherit',
+ letterSpacing: 'inherit',
+ textTransform: 'none',
textDecoration: 'underline',
padding: 0,
margin: 0,
minWidth: 'unset',
+ height: 'auto',
+ verticalAlign: 'baseline',
'&:hover': {
textDecoration: 'none',
background: 'transparent',
},
},
- },
- },
- defaultProps: {
- iconMapping: {
- error: (
-
-
-
- ),
- info: (
-
-
-
- ),
- success: (
-
-
-
- ),
- warning: (
-
-
-
- ),
- },
- },
- variants: [
- {
- props: { severity: 'error' },
- style: {
- color: theme.palette.error['100'],
- background: theme.palette.error['200'],
- a: {
- color: theme.palette.error['100'],
- },
- '.MuiButton-text': {
- color: theme.palette.error['100'],
- },
- },
- },
- {
- props: { severity: 'info' },
- style: {
- color: theme.palette.info['100'],
- background: theme.palette.info['200'],
- a: {
- color: theme.palette.info['100'],
- },
- '.MuiButton-text': {
- color: theme.palette.info['100'],
+ // Compact sizing: tighter padding + a 2rem icon box (the glyph inside keeps the default
+ // 1rem size). Shared by both `small` and `small-icon`. `small` additionally shrinks the
+ // text to 0.75rem; `small-icon` keeps the default-size text (for dense inline chips,
+ // e.g. history status badges).
+ '&[data-size="small"], &[data-size="small-icon"]': {
+ padding: '0.75rem',
+ gap: '0.75rem',
+ '.MuiAlert-icon': {
+ width: '2rem',
+ height: '2rem',
+ padding: '0.53125rem 0.5rem 0.46875rem 0.5rem',
},
},
- },
- {
- props: { severity: 'success' },
- style: {
- color: theme.palette.success['100'],
- background: theme.palette.success['200'],
- a: {
- color: theme.palette.success['100'],
+ '&[data-size="small"]': {
+ '.MuiAlert-message': {
+ fontSize: '0.75rem',
+ lineHeight: '1.0125rem',
},
- '.MuiButton-text': {
- color: theme.palette.success['100'],
+ '.MuiAlertTitle-root': {
+ fontSize: '0.75rem',
+ lineHeight: '1.0125rem',
},
},
},
- {
- props: { severity: 'warning' },
- style: {
- color: theme.palette.warning['100'],
- background: theme.palette.warning['200'],
- a: {
- color: theme.palette.warning['100'],
- },
- '.MuiButton-text': {
- color: theme.palette.warning['100'],
- },
- },
+ },
+ defaultProps: {
+ iconMapping: {
+ error: ,
+ info: ,
+ success: ,
+ warning: ,
},
+ },
+ variants: [
+ { props: { severity: 'error' }, style: alertSeverityStyle(figVars['danger']) },
+ { props: { severity: 'info' }, style: alertSeverityStyle(figVars['purple-1']) },
+ { props: { severity: 'success' }, style: alertSeverityStyle(figVars['data-green']) },
+ { props: { severity: 'warning' }, style: alertSeverityStyle(figVars['favourite-star']) },
],
},
MuiCssBaseline: {
@@ -801,48 +1211,124 @@ export function getThemedComponents(theme: Theme) {
fontWeight: 400,
fontSize: pxToRem(14),
minWidth: '375px',
+ backgroundColor: figVars['bg-1'],
'> div:first-of-type': {
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
},
},
+ // Respect the OS "reduce motion" preference app-wide (incl. the dev showcase,
+ // since CssBaseline is injected once at the app root).
+ '@media (prefers-reduced-motion: reduce)': {
+ '*, *::before, *::after': {
+ animationDuration: '0.01ms !important',
+ animationIterationCount: '1 !important',
+ transitionDuration: '0.01ms !important',
+ scrollBehavior: 'auto !important',
+ },
+ },
},
},
MuiSvgIcon: {
styleOverrides: {
colorPrimary: {
- color: theme.palette.primary.light,
+ color: theme.vars.palette.primary.light,
},
},
},
MuiSelect: {
defaultProps: {
IconComponent: (props) => (
-
-
-
+
),
},
styleOverrides: {
outlined: {
- backgroundColor: theme.palette.background.surface,
+ // The trigger's fill + ring live on the OutlinedInput root (see MuiOutlinedInput)
+ // so they're rounded and wrapped like the outlined button; here just the text.
...theme.typography.buttonM,
- padding: '6px 12px',
- color: theme.palette.primary.light,
+ color: figVars['fg-1'],
},
},
},
MuiLinearProgress: {
styleOverrides: {
bar1Indeterminate: {
- background: theme.palette.gradients.aaveGradient,
+ background: figVars['purple-1'],
},
bar2Indeterminate: {
- background: theme.palette.gradients.aaveGradient,
+ background: figVars['purple-1'],
},
},
},
},
} as ThemeOptions;
}
+
+/**
+ * Assemble the full app MUI theme (CSS-variables mode): both color schemes' design tokens
+ * plus the component overrides. Single source of truth shared by the app root
+ * (`AppGlobalStyles`) and the dev component showcase, so they can't drift apart. Color
+ * scheme is switched via the `data-mui-color-scheme` attribute, not by rebuilding the theme.
+ */
+export const createAppTheme = () => {
+ const light = getDesignTokens('light');
+ const dark = getDesignTokens('dark');
+ const shared = {
+ breakpoints: light.breakpoints,
+ spacing: light.spacing,
+ typography: light.typography,
+ colorSchemes: {
+ light: { palette: light.palette },
+ dark: { palette: dark.palette },
+ },
+ };
+ // Build a base theme first so `getThemedComponents` can read its `.vars` (CSS-var refs),
+ // then rebuild with those overrides attached. (A build-once `theme.components = …` mutation
+ // trips MUI's `Components` typing, so the two-pass is the type-clean form.)
+ const base = experimental_extendTheme(shared);
+ return experimental_extendTheme({
+ ...shared,
+ components: getThemedComponents(base).components,
+ });
+};
+
+// --- Display-P3 override layer -------------------------------------------------------------
+
+const isColorValue = (v: string) => v.startsWith('#') || v.startsWith('rgb');
+
+// Walk a color scheme's palette and, for every solid color leaf, emit a P3 override keyed to
+// the CSS variable MUI generates for it (`--mui-palette-`). Non-color
+// leaves (numbers, `mode`, channel strings like "32 29 29", gradients) are skipped.
+const collectP3Vars = (
+ node: Record,
+ path: string[],
+ out: Record
+) => {
+ Object.entries(node).forEach(([key, value]) => {
+ if (typeof value === 'string' && isColorValue(value)) {
+ out[`--mui-palette-${[...path, key].join('-')}`] = colorToP3(value);
+ } else if (value && typeof value === 'object') {
+ collectP3Vars(value as Record, [...path, key], out);
+ }
+ });
+};
+
+/**
+ * Build Display-P3 overrides for the generated `--mui-palette-*` CSS variables — one entry
+ * per solid color token, per color scheme. Injected under `@supports (color-gamut: p3)` so
+ * wide-gamut displays get the richer color while everything else keeps the sRGB base var.
+ * (Alpha-composited tints via MUI's `rgba( / a)` stay sRGB — see migration notes.)
+ */
+export const buildP3Overrides = (theme: AppTheme) => {
+ const forScheme = (scheme?: { palette?: unknown }) => {
+ const out: Record = {};
+ collectP3Vars((scheme?.palette ?? {}) as Record, [], out);
+ return out;
+ };
+ return {
+ light: forScheme(theme.colorSchemes.light),
+ dark: forScheme(theme.colorSchemes.dark),
+ };
+};