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..a009bb560c 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..01cac5b3f3 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..326d177281 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..9ad2d12f88 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 {
@@ -71,11 +70,11 @@ export const useReserveActionState = ({
eModeBorrowDisabled ||
maxAmountToBorrow === '0',
alerts: (
-
+
{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..1b43b7bdfc 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,24 @@ 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 { darkScheme } from 'src/utils/theme';
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 +52,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 +80,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 +93,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 +129,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 +160,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 +181,7 @@ export function AppHeader() {
FAQ.
-
+ Disable testnet
@@ -187,7 +195,7 @@ export function AppHeader() {
The app is running in fork mode.
-
+ Disable fork
@@ -205,190 +213,174 @@ 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',
+ ...darkScheme({ backgroundColor: figVars['bg-1'] }),
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..4b976b0a08 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 = () => {
+ ) : (
+ title
+ )
+ }
>
-
- {title}
-
-
-
- {tooltip}
-
+
+
);
};
diff --git a/src/modules/dashboard/lists/ListValueColumn.tsx b/src/modules/dashboard/lists/ListValueColumn.tsx
index beb7a10db2..1368704056 100644
--- a/src/modules/dashboard/lists/ListValueColumn.tsx
+++ b/src/modules/dashboard/lists/ListValueColumn.tsx
@@ -1,5 +1,6 @@
import { Box, Tooltip } from '@mui/material';
import { ReactNode } from 'react';
+import { onAccent } from 'src/utils/figmaColors';
import { ListColumn, ListColumnProps } from '../../../components/lists/ListColumn';
import { FormattedNumber } from '../../../components/primitives/FormattedNumber';
@@ -26,21 +27,16 @@ const Content = ({
{capsComponent}
{!withTooltip && !!subValue && !disabled && (
-
+
)}
>
);
@@ -71,16 +67,16 @@ export const ListValueColumn = ({
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..ae8b0d0a15 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',
}}
>
@@ -320,44 +319,40 @@ export const SupplyAssetsList = () => {
selectedCategories={selectedCategories}
onCategoriesChange={setSelectedCategories}
disabled={isLoading || !!error}
- sx={{
- buttonGroup: { width: '100%', maxWidth: '100%', height: '30px' },
- button: { fontSize: '0.7rem' },
- }}
/>
)}
-
+
{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 ? (
-
+
) : (
- ...
+
@@ -91,13 +91,13 @@ export const VotersListModal = ({
px: 4,
py: 2,
borderBottom: '1px solid',
- borderColor: 'divider',
+ borderColor: 'border-0',
}}
>
-
+ Addresses ({voters.nayVotes.length})
-
+ Votes
@@ -129,24 +129,24 @@ export const VotersListModal = ({
) : (
<>
- setVoteView(value)}
- sx={{ width: '100%', height: '44px', mt: 8, mb: 6 }}
+ sx={{ width: '100%', mt: 8, mb: 6 }}
>
-
+ Voted YAE
-
-
+
+ Voted NAY
-
-
+
+
{voteView === 'yaes' && yesVotesUI}
{voteView === 'nays' && noVotesUI}
>
diff --git a/src/modules/governance/proposal/VotingResults.tsx b/src/modules/governance/proposal/VotingResults.tsx
index 9e0627f76a..d33b29fb85 100644
--- a/src/modules/governance/proposal/VotingResults.tsx
+++ b/src/modules/governance/proposal/VotingResults.tsx
@@ -7,6 +7,7 @@ import { Link } from 'src/components/primitives/Link';
import { Row } from 'src/components/primitives/Row';
import { ProposalDetailDisplay, VotersSplitDisplay } from 'src/modules/governance/types';
import { useRootStore } from 'src/store/root';
+import { cardPaddingSx } from 'src/utils/cardStyles';
import { GENERAL } from 'src/utils/events';
import { StateBadge } from '../StateBadge';
@@ -24,7 +25,7 @@ export const VotingResults = ({ proposal, loading, voters, votesLoading }: Votin
const trackEvent = useRootStore((store) => store.trackEvent);
const discussionUrl = proposal?.discussions?.match(/https?:\/\/[^\s"]+/)?.[0];
return (
-
+ Voting results
@@ -76,7 +77,7 @@ export const VotingResults = ({ proposal, loading, voters, votesLoading }: Votin
caption={
<>
Current votes
-
+
Required
>
@@ -96,7 +97,7 @@ export const VotingResults = ({ proposal, loading, voters, votesLoading }: Votin
value={proposal.voteInfo.quorum}
visibleDecimals={2}
roundDown
- color="text.muted"
+ color="fg-3"
/>
@@ -123,7 +124,7 @@ export const VotingResults = ({ proposal, loading, voters, votesLoading }: Votin
caption={
<>
Current differential
-
+
Required
>
@@ -143,7 +144,7 @@ export const VotingResults = ({ proposal, loading, voters, votesLoading }: Votin
value={proposal.voteInfo.requiredDifferential}
visibleDecimals={2}
roundDown
- color="text.muted"
+ color="fg-3"
/>
@@ -159,7 +160,7 @@ export const VotingResults = ({ proposal, loading, voters, votesLoading }: Votin
})
}
href={discussionUrl}
- variant="outlined"
+ variant="tertiary"
fullWidth
endIcon={
diff --git a/src/modules/history/HistoryFilterMenu.tsx b/src/modules/history/HistoryFilterMenu.tsx
index f29114545c..a502cd0ccd 100644
--- a/src/modules/history/HistoryFilterMenu.tsx
+++ b/src/modules/history/HistoryFilterMenu.tsx
@@ -1,6 +1,7 @@
import { XCircleIcon } from '@heroicons/react/solid';
import { Trans } from '@lingui/macro';
-import { Check as CheckIcon, Sort as SortIcon } from '@mui/icons-material';
+import CheckIcon from '@mui/icons-material/Check';
+import SortIcon from '@mui/icons-material/Sort';
import {
Box,
Button,
@@ -16,6 +17,7 @@ import React, { useEffect, useState } from 'react';
import { DarkTooltip } from 'src/components/infoTooltips/DarkTooltip';
import { useRootStore } from 'src/store/root';
import { TRANSACTION_HISTORY } from 'src/utils/events';
+import { figVars } from 'src/utils/figmaColors';
import { FilterOptions } from './types';
@@ -117,7 +119,7 @@ export const HistoryFilterMenu: React.FC = ({
return (
-
+
TXs:
{displayedFilters}
@@ -144,7 +146,7 @@ export const HistoryFilterMenu: React.FC = ({
alignItems: 'center',
height: 36,
border: '1px solid',
- borderColor: 'divider',
+ borderColor: 'border-2',
borderRadius: '4px',
mr: downToMD ? 0 : 2,
ml: downToMD ? 4 : 0,
@@ -159,7 +161,7 @@ export const HistoryFilterMenu: React.FC = ({
= ({
{!allSelected && (
+ Reset
}
@@ -190,7 +192,9 @@ export const HistoryFilterMenu: React.FC = ({
}}
onClick={handleClearFilter}
>
-
+
+
+
)}
@@ -212,12 +216,12 @@ export const HistoryFilterMenu: React.FC = ({
@@ -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);
+ }}
+ >
+
+
+
-
-
- Underlying token
-
-
+
+ Underlying token
+ {
if (currentChainId !== connectedChainId) {
switchNetwork(currentChainId).then(() => {
@@ -166,21 +155,17 @@ export const AddTokenDropdown = ({
handleClose();
}}
>
-
-
- {poolReserve.underlyingToken.symbol}
-
{!hideAToken && (
-
-
-
- Aave aToken
-
-
+ <>
+
+
+ Aave aToken
+
-
-
- {poolReserve.aToken.symbol}
-
-
+ >
)}
{isSGHO && sGHOTokenAddress && (
-
-
-
- Savings GHO token
-
-
+ <>
+
+
+ Savings GHO token
+
-
-
- sGHO
-
+
-
+ >
)}
>
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..d5c68f31cc 100644
--- a/src/modules/reserve-overview/Gho/GhoReserveConfiguration.tsx
+++ b/src/modules/reserve-overview/Gho/GhoReserveConfiguration.tsx
@@ -1,7 +1,6 @@
-import { ExternalLinkIcon } from '@heroicons/react/solid';
import { Trans } from '@lingui/macro';
-import { Box, Button, Divider, SvgIcon, Typography } from '@mui/material';
-import { Link } from 'src/components/primitives/Link';
+import { Box, Divider, Typography } from '@mui/material';
+import { ExternalLinkButton } from 'src/components/ExternalLinkButton';
import { ReserveWithId } from 'src/hooks/app-data-provider/useAppDataProvider';
import { useAssetCapsSDK } from 'src/hooks/useAssetCapsSDK';
import { useRootStore } from 'src/store/root';
@@ -39,55 +38,16 @@ export const GhoReserveConfiguration: React.FC = (
accrued by minters of GHO would be directly transferred to the AaveDAO treasury.
-
-
-
- Techpaper
-
-
-
-
-
-
-
- Website
-
-
-
-
-
-
-
- FAQ
-
-
-
-
-
+
+
+ Techpaper
+
+
+ Website
+
+
+ FAQ
+
diff --git a/src/modules/reserve-overview/Gho/GhoReserveTopDetails.tsx b/src/modules/reserve-overview/Gho/GhoReserveTopDetails.tsx
index 1178591e83..f5d7790cd7 100644
--- a/src/modules/reserve-overview/Gho/GhoReserveTopDetails.tsx
+++ b/src/modules/reserve-overview/Gho/GhoReserveTopDetails.tsx
@@ -1,10 +1,10 @@
import { valueToBigNumber } from '@aave/math-utils';
import { Trans } from '@lingui/macro';
-import { Box, useMediaQuery, useTheme } from '@mui/material';
+import { useMediaQuery, useTheme } from '@mui/material';
import { BigNumber } from 'bignumber.js';
+import { PageHeaderStat } from 'src/components/PageHeader/PageHeaderStat';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
import { TextWithTooltip } from 'src/components/TextWithTooltip';
-import { TopInfoPanelItem } from 'src/components/TopInfoPanel/TopInfoPanelItem';
import { ReserveWithId, useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider';
export const GhoReserveTopDetails = ({ reserve }: { reserve: ReserveWithId }) => {
@@ -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..61a3e77095 100644
--- a/src/modules/reserve-overview/ReserveConfigurationWrapper.tsx
+++ b/src/modules/reserve-overview/ReserveConfigurationWrapper.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 dynamic from 'next/dynamic';
import { ReserveWithId } from 'src/hooks/app-data-provider/useAppDataProvider';
import { useRootStore } from 'src/store/root';
@@ -19,15 +19,13 @@ const ReserveConfiguration = dynamic(() =>
export const ReserveConfigurationWrapper: React.FC = ({ reserve }) => {
const currentMarket = useRootStore((state) => state.currentMarket);
- const { breakpoints } = useTheme();
- const downToXsm = useMediaQuery(breakpoints.down('xsm'));
const isGho = displayGhoForMintableMarket({
symbol: reserve.underlyingToken.symbol,
currentMarket,
});
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' });
}}
@@ -170,14 +170,14 @@ export const ConfigStatus = ({
) : enabled ? (
-
+
) : (
-
+
)}
{label && (
{label}
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 (
<>
-
-
-
-
-
-
-
+
+
+
-
-
- Underlying token
-
-
+
+ Underlying token
+ {
@@ -109,24 +93,19 @@ export const TokenLinkDropdown = ({
address: poolReserve?.underlyingToken.address.toLowerCase(),
})}
target="_blank"
- divider={showVariableDebtToken}
>
-
-
- {poolReserve.underlyingToken.symbol}
-
{!hideAToken && (
-
-
-
- Aave aToken
-
-
+ <>
+
+
+ Aave aToken
+
-
-
- {poolReserve.aToken.symbol}
-
-
+ >
)}
{showVariableDebtToken && (
-
-
+ <>
+
+ Aave debt token
-
-
- )}
- {showVariableDebtToken && (
- {
- trackEvent(RESERVE_DETAILS.RESERVE_TOKEN_ACTIONS, {
- type: 'Variable Debt',
- assetName: poolReserve.underlyingToken.name,
- asset: poolReserve.underlyingToken.address,
- aToken: poolReserve.aToken.address,
- market: currentMarket,
- variableDebtToken: poolReserve.vToken.address,
- });
- }}
- >
-
-
- {poolReserve.vToken.symbol}
-
-
+
+ {
+ trackEvent(RESERVE_DETAILS.RESERVE_TOKEN_ACTIONS, {
+ type: 'Variable Debt',
+ assetName: poolReserve.underlyingToken.name,
+ asset: poolReserve.underlyingToken.address,
+ aToken: poolReserve.aToken.address,
+ market: currentMarket,
+ variableDebtToken: poolReserve.vToken.address,
+ });
+ }}
+ >
+
+
+ >
)}
>
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-0']}`,
p: 4,
mb: 6,
- background: theme.palette.background.paper,
- })}
+ background: figVars['bg-2'],
+ }}
>
@@ -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..fa42f5f227 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-0']}`,
p: 4,
mb: 6,
- background: theme.palette.background.paper,
- })}
+ background: figVars['bg-2'],
+ }}
>
-
+ 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-0']}` },
p: { xs: 0, xsm: 4 },
background: {
xs: 'unset',
- xsm: theme.palette.background.paper,
+ xsm: figVars['bg-2'],
},
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..66f61544b4 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-0']}`,
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['bg-2'],
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..9e4fae8cdd 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-0']}` },
p: { xs: 0, xsm: 4 },
background: {
xs: 'unset',
- xsm: theme.palette.background.paper,
+ xsm: figVars['bg-2'],
},
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-0']}`,
p: 4,
- background: theme.palette.background.paper,
+ background: figVars['bg-2'],
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-0']}`,
p: 4,
mb: 6,
- background: theme.palette.background.paper,
- })}
+ background: figVars['bg-2'],
+ }}
>
@@ -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..4b51453daf 100644
--- a/src/modules/umbrella/StakeAssets/StakeAssetName.tsx
+++ b/src/modules/umbrella/StakeAssets/StakeAssetName.tsx
@@ -26,14 +26,14 @@ export const StakeAssetName = ({
-
+
Stake {symbol}
-
+
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/UmbrellaAssetsListItemLoader.tsx b/src/modules/umbrella/StakeAssets/UmbrellaAssetsListItemLoader.tsx
index 3b94dae30b..46c6f616b7 100644
--- a/src/modules/umbrella/StakeAssets/UmbrellaAssetsListItemLoader.tsx
+++ b/src/modules/umbrella/StakeAssets/UmbrellaAssetsListItemLoader.tsx
@@ -29,7 +29,7 @@ export const UmbrellaAssetsListItemLoader = () => {
-
+
diff --git a/src/modules/umbrella/StakeAssets/UmbrellaAssetsListMobileItem.tsx b/src/modules/umbrella/StakeAssets/UmbrellaAssetsListMobileItem.tsx
index f553675ce3..28aa54abe5 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,11 +77,11 @@ export const UmbrellaAssetsListMobileItem = ({ ...umbrellaStakeAsset }: MergedSt
textAlign: 'center',
}}
>
-
+
-
+
);
};
diff --git a/src/modules/umbrella/StakeAssets/UmbrellaStakeAssetsListItem.tsx b/src/modules/umbrella/StakeAssets/UmbrellaStakeAssetsListItem.tsx
index dac6868262..095c113e72 100644
--- a/src/modules/umbrella/StakeAssets/UmbrellaStakeAssetsListItem.tsx
+++ b/src/modules/umbrella/StakeAssets/UmbrellaStakeAssetsListItem.tsx
@@ -49,7 +49,7 @@ export const UmbrellaStakeAssetsListItem = ({ ...umbrellaStakeAsset }: MergedSta
-
+
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 (
-
-
-
- {marketNaming.name} {market.isFork ? 'Fork' : ''}
-
-
-
- {marketNaming.testChainName}
-
-
-
- );
- })}
-
- );
-};
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..380ed2eb25 100644
--- a/src/modules/umbrella/helpers/StakingDropdown.tsx
+++ b/src/modules/umbrella/helpers/StakingDropdown.tsx
@@ -3,7 +3,7 @@ import AccessTimeIcon from '@mui/icons-material/AccessTime';
import AddOutlinedIcon from '@mui/icons-material/AddOutlined';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import StartIcon from '@mui/icons-material/Start';
-import { Button, Stack, useMediaQuery, useTheme } from '@mui/material';
+import { Button, Stack, useTheme } from '@mui/material';
import IconButton from '@mui/material/IconButton';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
@@ -33,14 +33,17 @@ const StyledMenuItem = styled(MenuItem)({
},
});
-export const StakingDropdown = ({ stakeData }: { stakeData: MergedStakeData }) => {
+export const StakingDropdown = ({
+ stakeData,
+ fullWidth,
+}: {
+ stakeData: MergedStakeData;
+ fullWidth?: boolean;
+}) => {
const { openUmbrella, openUmbrellaStakeCooldown, openUmbrellaUnstake, openUmbrellaClaim } =
useModalContext();
const trackEvent = useRootStore((store) => store.trackEvent);
const now = useCurrentTimestamp(1);
- const { breakpoints } = useTheme();
-
- const isMobile = useMediaQuery(breakpoints.down('lg'));
const endOfCooldown = stakeData?.cooldownData.endOfCooldown || 0;
const unstakeWindow = stakeData?.cooldownData.withdrawalWindow || 0;
@@ -80,8 +83,9 @@ export const StakingDropdown = ({ stakeData }: { stakeData: MergedStakeData }) =
{!hasStakeTokenBalance && !hasUnclaimedRewards ? (
{
trackEvent(STAKE.STAKE_TOKEN, {
action: STAKE.OPEN_STAKE_MODAL,
@@ -104,7 +108,7 @@ export const StakingDropdown = ({ stakeData }: { stakeData: MergedStakeData }) =
<>
@@ -153,7 +157,7 @@ export const StakingDropdown = ({ stakeData }: { stakeData: MergedStakeData }) =
alignItems="center"
justifyContent="space-between"
>
-
+ Cooling down
@@ -191,7 +195,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/cardStyles.ts b/src/utils/cardStyles.ts
new file mode 100644
index 0000000000..f2fbae8a1e
--- /dev/null
+++ b/src/utils/cardStyles.ts
@@ -0,0 +1,14 @@
+import { SxProps, Theme } from '@mui/material';
+
+/**
+ * Standard padding for a `Paper variant="card"` panel: a tighter top than sides, and 16px sides on
+ * mobile stepping to 24px from `xsm`. Kept out of the `card` variant itself because several cards
+ * pad an inner Box instead and would double up.
+ *
+ *
+ */
+export const cardPaddingSx: SxProps = {
+ pt: 4,
+ pb: { xs: 4, xsm: 6 },
+ px: { xs: 4, xsm: 6 },
+};
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),
+ };
+};