From a6909791dd0f03e91782c86e2e3a526ae6d46933 Mon Sep 17 00:00:00 2001
From: fairlighteth <31534717+fairlighteth@users.noreply.github.com>
Date: Thu, 13 Nov 2025 18:09:45 +0000
Subject: [PATCH] feat(tokenselector): enhance VirtualList component
---
.../src/common/pure/VirtualList/index.tsx | 122 +++++++++---
apps/cowswap-frontend/src/locales/en-US.po | 27 +--
apps/cowswap-frontend/src/theme/consts.tsx | 1 +
libs/tokens/src/pure/TokenLogo/index.tsx | 174 +++++++++++-------
libs/ui/src/enum.ts | 35 ++++
libs/ui/src/pure/Input/index.tsx | 13 +-
libs/ui/src/pure/Popover/index.tsx | 166 ++++++++++++-----
libs/ui/src/theme/ThemeColorVars.tsx | 149 +++++++++++++++
8 files changed, 537 insertions(+), 150 deletions(-)
diff --git a/apps/cowswap-frontend/src/common/pure/VirtualList/index.tsx b/apps/cowswap-frontend/src/common/pure/VirtualList/index.tsx
index a5f10673af..2134dc4129 100644
--- a/apps/cowswap-frontend/src/common/pure/VirtualList/index.tsx
+++ b/apps/cowswap-frontend/src/common/pure/VirtualList/index.tsx
@@ -1,4 +1,4 @@
-import { ReactNode, useCallback, useRef } from 'react'
+import { ReactNode, useCallback, useLayoutEffect, useRef } from 'react'
import { useVirtualizer, VirtualItem } from '@tanstack/react-virtual'
import ms from 'ms.macro'
@@ -7,15 +7,72 @@ import { ListInner, ListScroller, ListWrapper, LoadingRows } from './styled'
const scrollDelay = ms`400ms`
-// TODO: Add proper return type annotation
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-const threeDivs = () => (
- <>
-
-
-
- >
-)
+const LoadingPlaceholder: () => ReactNode = () => {
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+interface VirtualListRowProps {
+ item: VirtualItem
+ loading?: boolean
+ items: T[]
+ getItemView(items: T[], item: VirtualItem): ReactNode
+ measureElement(element: Element | null): void
+}
+
+function VirtualListRow({ item, loading, items, getItemView, measureElement }: VirtualListRowProps): ReactNode {
+ if (loading) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+ {getItemView(items, item)}
+
+ )
+}
+
+interface VirtualListRowsProps {
+ virtualItems: VirtualItem[]
+ loading?: boolean
+ items: T[]
+ getItemView(items: T[], item: VirtualItem): ReactNode
+ measureElement(element: Element | null): void
+}
+
+function renderVirtualListRows({
+ virtualItems,
+ loading,
+ items,
+ getItemView,
+ measureElement,
+}: VirtualListRowsProps): ReactNode[] {
+ const elements: ReactNode[] = []
+
+ for (const item of virtualItems) {
+ elements.push(
+ ,
+ )
+ }
+
+ return elements
+}
interface VirtualListProps {
id?: string
@@ -26,10 +83,9 @@ interface VirtualListProps {
loading?: boolean
estimateSize?: () => number
children?: ReactNode
+ scrollResetKey?: string | number | boolean
}
-// TODO: Add proper return type annotation
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function VirtualList({
id,
items,
@@ -37,7 +93,8 @@ export function VirtualList({
getItemView,
children,
estimateSize = () => 56,
-}: VirtualListProps) {
+ scrollResetKey,
+}: VirtualListProps): ReactNode {
const parentRef = useRef(null)
const wrapperRef = useRef(null)
const scrollTimeoutRef = useRef(undefined)
@@ -53,6 +110,7 @@ export function VirtualList({
}, scrollDelay)
}, [])
+ // eslint-disable-next-line react-hooks/incompatible-library
const virtualizer = useVirtualizer({
getScrollElement: () => parentRef.current,
count: items.length,
@@ -60,24 +118,40 @@ export function VirtualList({
overscan: 5,
})
+ useLayoutEffect(() => {
+ if (scrollResetKey === undefined) {
+ return
+ }
+
+ const scrollContainer = parentRef.current
+
+ if (scrollContainer) {
+ scrollContainer.scrollTop = 0
+ scrollContainer.scrollLeft = 0
+
+ if (typeof scrollContainer.scrollTo === 'function') {
+ scrollContainer.scrollTo({ top: 0, left: 0, behavior: 'auto' })
+ }
+ }
+
+ virtualizer.scrollToOffset(0, { align: 'start' })
+ }, [scrollResetKey, virtualizer])
+
const virtualItems = virtualizer.getVirtualItems()
+ const virtualRows = renderVirtualListRows({
+ virtualItems,
+ loading,
+ items,
+ getItemView,
+ measureElement: virtualizer.measureElement,
+ })
return (
{children}
- {virtualItems.map((item) => {
- if (loading) {
- return {threeDivs()}
- }
-
- return (
-
- {getItemView(items, item)}
-
- )
- })}
+ {virtualRows}
diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po
index 85dd692ccd..ea987f8f0b 100644
--- a/apps/cowswap-frontend/src/locales/en-US.po
+++ b/apps/cowswap-frontend/src/locales/en-US.po
@@ -455,7 +455,6 @@ msgid "View details"
msgstr "View details"
#: apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx
-#: apps/cowswap-frontend/src/modules/tokensList/pure/ChainsSelector/index.tsx
msgid "More"
msgstr "More"
@@ -827,8 +826,8 @@ msgid "Copied"
msgstr "Copied"
#: apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx
-msgid "Can't find your token on the list?"
-msgstr "Can't find your token on the list?"
+#~ msgid "Can't find your token on the list?"
+#~ msgstr "Can't find your token on the list?"
#: apps/cowswap-frontend/src/modules/trade/pure/ReceiveAmountTitle/index.tsx
msgid "icon"
@@ -847,8 +846,8 @@ msgid "Please connect your wallet to one of our supported networks."
msgstr "Please connect your wallet to one of our supported networks."
#: apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx
-msgid "<0>Read our guide0> on how to add custom tokens."
-msgstr "<0>Read our guide0> on how to add custom tokens."
+#~ msgid "<0>Read our guide0> on how to add custom tokens."
+#~ msgstr "<0>Read our guide0> on how to add custom tokens."
#: apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx
msgid "Retry"
@@ -1220,8 +1219,8 @@ msgid "Partner fee can not be more than {PARTNER_FEE_MAX_BPS} BPS!"
msgstr "Partner fee can not be more than {PARTNER_FEE_MAX_BPS} BPS!"
#: apps/cowswap-frontend/src/modules/tokensList/pure/TokensContent/index.tsx
-msgid "Manage Token Lists"
-msgstr "Manage Token Lists"
+#~ msgid "Manage Token Lists"
+#~ msgstr "Manage Token Lists"
#: apps/cowswap-frontend/src/legacy/components/Tokens/TokensTable.tsx
msgid "No results found"
@@ -3156,7 +3155,6 @@ msgstr "CoW Swap's robust solver competition protects your slippage from being e
msgid "Aave Debt Swap Flashloan"
msgstr "Aave Debt Swap Flashloan"
-#: apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx
#: apps/cowswap-frontend/src/modules/yield/pure/TargetPoolPreviewInfo.tsx
msgid "Details"
msgstr "Details"
@@ -3812,6 +3810,10 @@ msgstr "User rejected approval transaction"
msgid "Swap on"
msgstr "Swap on"
+#: apps/cowswap-frontend/src/modules/tokensList/pure/TokenSearchContent/index.tsx
+msgid "Can't find your token on the list? <0>Read our guide0> on how to add custom tokens."
+msgstr "Can't find your token on the list? <0>Read our guide0> on how to add custom tokens."
+
#: apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx
#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/TransactionSubmittedContent/index.tsx
msgid "Transaction"
@@ -4322,7 +4324,6 @@ msgstr "Decrease Value"
#: apps/cowswap-frontend/src/legacy/components/Tokens/TokensTable.tsx
#: apps/cowswap-frontend/src/modules/ethFlow/pure/WrappingPreview/WrapCard.tsx
#: apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx
-#: apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx
msgid "Balance"
msgstr "Balance"
@@ -4385,8 +4386,8 @@ msgid "funds"
msgstr "funds"
#: apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx
-msgid "Pool details"
-msgstr "Pool details"
+#~ msgid "Pool details"
+#~ msgstr "Pool details"
#: apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx
msgid "Slippage adjusted to {slippageBpsPercentage}% to ensure quick execution"
@@ -5894,8 +5895,8 @@ msgid "You sold <0/>"
msgstr "You sold <0/>"
#: apps/cowswap-frontend/src/modules/tokensList/pure/ChainsSelector/index.tsx
-msgid "Less"
-msgstr "Less"
+#~ msgid "Less"
+#~ msgstr "Less"
#: libs/hook-dapp-lib/src/hookDappsRegistry.ts
msgid "Claim your LlamaPay vesting contract funds"
diff --git a/apps/cowswap-frontend/src/theme/consts.tsx b/apps/cowswap-frontend/src/theme/consts.tsx
index 5230490c10..8a008740d7 100644
--- a/apps/cowswap-frontend/src/theme/consts.tsx
+++ b/apps/cowswap-frontend/src/theme/consts.tsx
@@ -25,6 +25,7 @@ export const WIDGET_MAX_WIDTH = {
limit: '1350px',
content: '680px',
tokenSelect: '590px',
+ tokenSelectSidebar: '700px',
}
export const TextWrapper = styled(Text)<{ color: keyof Colors; override?: boolean }>`
diff --git a/libs/tokens/src/pure/TokenLogo/index.tsx b/libs/tokens/src/pure/TokenLogo/index.tsx
index 7f49b5e345..6d2c67f048 100644
--- a/libs/tokens/src/pure/TokenLogo/index.tsx
+++ b/libs/tokens/src/pure/TokenLogo/index.tsx
@@ -1,5 +1,5 @@
import { atom, useAtom } from 'jotai'
-import { useCallback, useMemo } from 'react'
+import { ReactNode, useCallback, useMemo } from 'react'
import {
BaseChainInfo,
@@ -41,11 +41,19 @@ export interface TokenLogoProps {
hideNetworkBadge?: boolean
}
-// TODO: Break down this large function into smaller functions
-// TODO: Add proper return type annotation
-// TODO: Reduce function complexity by extracting logic
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, complexity, max-lines-per-function
-export function TokenLogo({
+export function TokenLogo(props: TokenLogoProps): ReactNode {
+ const { token } = props
+
+ if (token instanceof LpToken) {
+ return
+ }
+
+ return
+}
+
+type StandardTokenLogoProps = TokenLogoProps & { token?: TokenWithLogo | Currency | null }
+
+function StandardTokenLogo({
logoURI,
token,
className,
@@ -53,31 +61,10 @@ export function TokenLogo({
sizeMobile,
noWrap,
hideNetworkBadge,
-}: TokenLogoProps) {
- const tokensByAddress = useTokensByAddressMap()
-
+}: StandardTokenLogoProps): ReactNode {
const [invalidUrls, setInvalidUrls] = useAtom(invalidUrlsAtom)
- const isLpToken = token instanceof LpToken
-
- const urls = useMemo(() => {
- if (token instanceof LpToken) return
-
- // TODO: get rid of Currency usage and remove type casting
- if (token) {
- if (token instanceof NativeCurrency) {
- return [cowprotocolTokenLogoUrl(NATIVE_CURRENCY_ADDRESS.toLowerCase(), token.chainId as SupportedChainId)]
- }
-
- return getTokenLogoUrls(token as TokenWithLogo)
- }
-
- return logoURI ? uriToHttp(logoURI) : []
- }, [logoURI, token])
-
- const validUrls = useMemo(() => urls && urls.filter((url) => !invalidUrls[url]), [urls, invalidUrls])
-
- const currentUrl = validUrls?.[0]
+ const { currentUrl, initial } = useTokenLogoUrl({ token, logoURI, invalidUrls })
const logoUrl = useNetworkLogo(token?.chainId)
const showNetworkBadge = logoUrl && !hideNetworkBadge
@@ -88,40 +75,7 @@ export function TokenLogo({
setInvalidUrls((state) => ({ ...state, [currentUrl]: true }))
}, [currentUrl, setInvalidUrls])
- const initial = token?.symbol?.[0] || token?.name?.[0]
-
- if (isLpToken) {
- return (
-
-
-
-
-
-
-
-
-
-
- )
- }
-
- const actualTokenContent = currentUrl ? (
-
-
-
- ) : initial ? (
-
-
-
- ) : (
-
-
-
- )
+ const actualTokenContent = renderTokenLogoContent({ currentUrl, onError, token, initial })
if (noWrap) {
return actualTokenContent
@@ -137,7 +91,12 @@ export function TokenLogo({
const cutThicknessForCalc = getBorderWidth(chainLogoSizeForCalc)
return (
-
+
<>
{showNetworkBadge ? (
)
}
+
+type LpTokenLogoProps = Omit & { token: LpToken }
+
+function LpTokenLogo({ token, className, size = 36, sizeMobile }: LpTokenLogoProps): ReactNode {
+ const tokensByAddress = useTokensByAddressMap()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+interface TokenLogoUrlOptions {
+ token?: TokenWithLogo | Currency | null
+ logoURI?: string
+ invalidUrls: Record
+}
+
+function useTokenLogoUrl({ token, logoURI, invalidUrls }: TokenLogoUrlOptions): {
+ currentUrl?: string
+ initial?: string
+} {
+ const urls = useMemo(() => {
+ if (token instanceof LpToken) {
+ return []
+ }
+
+ if (token instanceof NativeCurrency) {
+ return [cowprotocolTokenLogoUrl(NATIVE_CURRENCY_ADDRESS.toLowerCase(), token.chainId as SupportedChainId)]
+ }
+
+ if (token) {
+ return getTokenLogoUrls(token as TokenWithLogo)
+ }
+
+ return logoURI ? uriToHttp(logoURI) : []
+ }, [logoURI, token])
+
+ const validUrls = useMemo(() => urls && urls.filter((url) => !invalidUrls[url]), [urls, invalidUrls])
+ const currentUrl = validUrls?.[0]
+ const initial = token?.symbol?.[0] || token?.name?.[0]
+
+ return { currentUrl, initial }
+}
+
+interface TokenLogoContentOptions {
+ currentUrl?: string
+ onError: () => void
+ token?: TokenWithLogo | Currency | null
+ initial?: string
+}
+
+function renderTokenLogoContent({ currentUrl, onError, token, initial }: TokenLogoContentOptions): ReactNode {
+ if (currentUrl) {
+ return (
+
+
+
+ )
+ }
+
+ if (initial) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/libs/ui/src/enum.ts b/libs/ui/src/enum.ts
index 7a13500436..6e7c2039ce 100644
--- a/libs/ui/src/enum.ts
+++ b/libs/ui/src/enum.ts
@@ -101,6 +101,41 @@ export enum UI {
COLOR_GREEN = '--cow-color-green',
COLOR_RED = '--cow-color-red',
+ // Chain-specific accent colors
+ COLOR_CHAIN_ETHEREUM_BG = '--cow-color-chain-ethereum-bg',
+ COLOR_CHAIN_ETHEREUM_BORDER = '--cow-color-chain-ethereum-border',
+ COLOR_CHAIN_ETHEREUM_ACCENT = '--cow-color-chain-ethereum-accent',
+ COLOR_CHAIN_BNB_BG = '--cow-color-chain-bnb-bg',
+ COLOR_CHAIN_BNB_BORDER = '--cow-color-chain-bnb-border',
+ COLOR_CHAIN_BNB_ACCENT = '--cow-color-chain-bnb-accent',
+ COLOR_CHAIN_BASE_BG = '--cow-color-chain-base-bg',
+ COLOR_CHAIN_BASE_BORDER = '--cow-color-chain-base-border',
+ COLOR_CHAIN_BASE_ACCENT = '--cow-color-chain-base-accent',
+ COLOR_CHAIN_ARBITRUM_BG = '--cow-color-chain-arbitrum-bg',
+ COLOR_CHAIN_ARBITRUM_BORDER = '--cow-color-chain-arbitrum-border',
+ COLOR_CHAIN_ARBITRUM_ACCENT = '--cow-color-chain-arbitrum-accent',
+ COLOR_CHAIN_POLYGON_BG = '--cow-color-chain-polygon-bg',
+ COLOR_CHAIN_POLYGON_BORDER = '--cow-color-chain-polygon-border',
+ COLOR_CHAIN_POLYGON_ACCENT = '--cow-color-chain-polygon-accent',
+ COLOR_CHAIN_AVALANCHE_BG = '--cow-color-chain-avalanche-bg',
+ COLOR_CHAIN_AVALANCHE_BORDER = '--cow-color-chain-avalanche-border',
+ COLOR_CHAIN_AVALANCHE_ACCENT = '--cow-color-chain-avalanche-accent',
+ COLOR_CHAIN_GNOSIS_BG = '--cow-color-chain-gnosis-bg',
+ COLOR_CHAIN_GNOSIS_BORDER = '--cow-color-chain-gnosis-border',
+ COLOR_CHAIN_GNOSIS_ACCENT = '--cow-color-chain-gnosis-accent',
+ COLOR_CHAIN_LENS_BG = '--cow-color-chain-lens-bg',
+ COLOR_CHAIN_LENS_BORDER = '--cow-color-chain-lens-border',
+ COLOR_CHAIN_LENS_ACCENT = '--cow-color-chain-lens-accent',
+ COLOR_CHAIN_SEPOLIA_BG = '--cow-color-chain-sepolia-bg',
+ COLOR_CHAIN_SEPOLIA_BORDER = '--cow-color-chain-sepolia-border',
+ COLOR_CHAIN_SEPOLIA_ACCENT = '--cow-color-chain-sepolia-accent',
+ COLOR_CHAIN_LINEA_BG = '--cow-color-chain-linea-bg',
+ COLOR_CHAIN_LINEA_BORDER = '--cow-color-chain-linea-border',
+ COLOR_CHAIN_LINEA_ACCENT = '--cow-color-chain-linea-accent',
+ COLOR_CHAIN_PLASMA_BG = '--cow-color-chain-plasma-bg',
+ COLOR_CHAIN_PLASMA_BORDER = '--cow-color-chain-plasma-border',
+ COLOR_CHAIN_PLASMA_ACCENT = '--cow-color-chain-plasma-accent',
+
// Neutral colors - Base grayscale palette from black (0) to white (100)
COLOR_WHITE = '--cow-color-neutral-100',
COLOR_NEUTRAL_100 = '--cow-color-neutral-100',
diff --git a/libs/ui/src/pure/Input/index.tsx b/libs/ui/src/pure/Input/index.tsx
index 4503352d71..c1a5dfb8b3 100644
--- a/libs/ui/src/pure/Input/index.tsx
+++ b/libs/ui/src/pure/Input/index.tsx
@@ -1,4 +1,4 @@
-import { InputHTMLAttributes } from 'react'
+import { InputHTMLAttributes, ReactNode } from 'react'
import { Search } from 'react-feather'
import styled from 'styled-components/macro'
@@ -31,15 +31,18 @@ const SearchInputEl = styled.input`
border-radius: 12px;
border: none;
- ::placeholder {
+ &::placeholder {
color: inherit;
opacity: 0.7;
+ transition: color 0.1s ease-in-out;
+ }
+
+ &:focus::placeholder {
+ color: transparent;
}
`
-// TODO: Add proper return type annotation
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export function SearchInput(props: InputHTMLAttributes) {
+export function SearchInput(props: InputHTMLAttributes): ReactNode {
return (
diff --git a/libs/ui/src/pure/Popover/index.tsx b/libs/ui/src/pure/Popover/index.tsx
index ca0078e5e1..9e45b4c42b 100644
--- a/libs/ui/src/pure/Popover/index.tsx
+++ b/libs/ui/src/pure/Popover/index.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useMemo, useState } from 'react'
+import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useMediaQuery, useInterval, useElementViewportTracking } from '@cowprotocol/common-hooks'
@@ -24,7 +24,9 @@ const MOBILE_FULL_WIDTH_STYLES = {
boxSizing: 'border-box' as const,
}
-function createMobileModifiers(arrowElement: HTMLDivElement | null): Array>>> {
+function createMobileModifiers(
+ arrowElement: HTMLDivElement | null,
+): Array>>> {
return [
{
name: 'offset',
@@ -44,7 +46,9 @@ function createMobileModifiers(arrowElement: HTMLDivElement | null): Array>>> {
+function createDesktopModifiers(
+ arrowElement: HTMLDivElement | null,
+): Array>>> {
return [
{ name: 'offset', options: { offset: [8, 8] } },
{ name: 'arrow', options: { element: arrowElement } },
@@ -52,7 +56,6 @@ function createDesktopModifiers(arrowElement: HTMLDivElement | null): Array(() => show || forceMount)
+
+ useEffect(() => {
+ if ((show || forceMount) && !hasMountedPortal) {
+ setHasMountedPortal(true)
+ }
+ }, [show, forceMount, hasMountedPortal])
+
+ return forceMount || show || hasMountedPortal
}
export default function Popover(props: PopoverProps): React.JSX.Element {
@@ -84,75 +104,133 @@ export default function Popover(props: PopoverProps): React.JSX.Element {
showMobileBackdrop = false,
mobileBorderRadius,
zIndex = 999999,
+ forceMount = false,
} = props
const [referenceElement, setReferenceElement] = useState(null)
const [popperElement, setPopperElement] = useState(null)
const [arrowElement, setArrowElement] = useState(null)
-
const isMobile = useMediaQuery(Media.upToSmall(false))
const shouldUseFullWidth = isMobile && mobileMode === PopoverMobileMode.FullWidth
-
- // Use hook for viewport tracking and utility for backdrop height calculation
const { rect } = useElementViewportTracking(referenceElement, shouldUseFullWidth && showMobileBackdrop)
-
const backdropHeight = useMemo(() => {
if (!shouldUseFullWidth || !showMobileBackdrop) return '100vh'
return calculateAvailableSpaceAbove(rect, 8)
}, [rect, shouldUseFullWidth, showMobileBackdrop])
-
const options = useMemo(
(): Options => ({
placement: shouldUseFullWidth ? 'top' : placement,
strategy: 'fixed',
- modifiers: shouldUseFullWidth
- ? createMobileModifiers(arrowElement)
- : createDesktopModifiers(arrowElement),
+ modifiers: shouldUseFullWidth ? createMobileModifiers(arrowElement) : createDesktopModifiers(arrowElement),
}),
[arrowElement, placement, shouldUseFullWidth],
)
-
const { styles, update, attributes } = usePopper(referenceElement, popperElement, options)
-
const updateCallback = useCallback(() => {
update?.()
}, [update])
const intervalDelay = useMemo(() => (show ? 100 : null), [show])
useInterval(updateCallback, intervalDelay)
-
+ const shouldRenderPortal = useLazyPortalMount(show, forceMount)
+ const popperStyle = {
+ ...styles.popper,
+ zIndex,
+ ...(shouldUseFullWidth && MOBILE_FULL_WIDTH_STYLES),
+ ...(shouldUseFullWidth && mobileBorderRadius && { borderRadius: mobileBorderRadius }),
+ }
+ const arrowPlacement = (attributes.popper?.['data-popper-placement'] as string | undefined)?.split('-')[0] ?? ''
return (
<>
{children}
-
- {isMobile && showMobileBackdrop && }
-
+ >
+ )
+}
+
+interface PopoverPortalProps {
+ shouldRender: boolean
+ show: boolean
+ isMobile: boolean
+ showMobileBackdrop: boolean
+ backdropHeight: string
+ className?: string
+ setPopperElement(value: HTMLDivElement | null): void
+ popperStyle: React.CSSProperties
+ popperAttributes: ReturnType['attributes']['popper']
+ bgColor?: string
+ color?: string
+ borderColor?: string
+ content: React.ReactNode
+ setArrowElement(value: HTMLDivElement | null): void
+ arrowStyle: React.CSSProperties
+ arrowAttributes: ReturnType['attributes']['arrow']
+ arrowPlacement: string
+}
+
+function PopoverPortal({
+ shouldRender,
+ show,
+ isMobile,
+ showMobileBackdrop,
+ backdropHeight,
+ className,
+ setPopperElement,
+ popperStyle,
+ popperAttributes,
+ bgColor,
+ color,
+ borderColor,
+ content,
+ setArrowElement,
+ arrowStyle,
+ arrowAttributes,
+ arrowPlacement,
+}: PopoverPortalProps): React.ReactNode {
+ if (!shouldRender) {
+ return null
+ }
+
+ return (
+
+ {isMobile && showMobileBackdrop && }
+
+ {content}
+
- {content}
-
-
-
- >
+ {...arrowAttributes}
+ />
+
+
)
}
diff --git a/libs/ui/src/theme/ThemeColorVars.tsx b/libs/ui/src/theme/ThemeColorVars.tsx
index b5b4872b06..f39c641a93 100644
--- a/libs/ui/src/theme/ThemeColorVars.tsx
+++ b/libs/ui/src/theme/ThemeColorVars.tsx
@@ -5,6 +5,153 @@ import { css } from 'styled-components/macro'
import { UI } from '../enum'
+interface ChainAccentConfig {
+ bgVar: UI
+ borderVar: UI
+ accentVar?: UI
+ lightBg: string
+ darkBg: string
+ lightBorder: string
+ darkBorder: string
+ lightColor: string
+ darkColor: string
+}
+
+interface ChainAccentInput {
+ bgVar: UI
+ borderVar: UI
+ accentVar?: UI
+ color: string
+ lightColor?: string
+ darkColor?: string
+ lightBgAlpha?: number
+ darkBgAlpha?: number
+ lightBorderAlpha?: number
+ darkBorderAlpha?: number
+}
+
+const CHAIN_LIGHT_BG_ALPHA = 0.22
+const CHAIN_DARK_BG_ALPHA = 0.32
+const CHAIN_LIGHT_BORDER_ALPHA = 0.45
+const CHAIN_DARK_BORDER_ALPHA = 0.65
+
+const chainAlpha = (color: string, alpha: number): string => transparentize(color, 1 - alpha)
+
+function createChainAccent({
+ bgVar,
+ borderVar,
+ accentVar,
+ color,
+ lightColor = color,
+ darkColor = color,
+ lightBgAlpha = CHAIN_LIGHT_BG_ALPHA,
+ darkBgAlpha = CHAIN_DARK_BG_ALPHA,
+ lightBorderAlpha = CHAIN_LIGHT_BORDER_ALPHA,
+ darkBorderAlpha = CHAIN_DARK_BORDER_ALPHA,
+}: ChainAccentInput): ChainAccentConfig {
+ return {
+ bgVar,
+ borderVar,
+ accentVar,
+ lightBg: chainAlpha(lightColor, lightBgAlpha),
+ darkBg: chainAlpha(darkColor, darkBgAlpha),
+ lightBorder: chainAlpha(lightColor, lightBorderAlpha),
+ darkBorder: chainAlpha(darkColor, darkBorderAlpha),
+ lightColor,
+ darkColor,
+ }
+}
+
+const CHAIN_ACCENT_CONFIG: ChainAccentConfig[] = [
+ createChainAccent({
+ bgVar: UI.COLOR_CHAIN_ETHEREUM_BG,
+ borderVar: UI.COLOR_CHAIN_ETHEREUM_BORDER,
+ accentVar: UI.COLOR_CHAIN_ETHEREUM_ACCENT,
+ color: '#627EEA',
+ }),
+ createChainAccent({
+ bgVar: UI.COLOR_CHAIN_BNB_BG,
+ borderVar: UI.COLOR_CHAIN_BNB_BORDER,
+ accentVar: UI.COLOR_CHAIN_BNB_ACCENT,
+ color: '#F0B90B',
+ }),
+ createChainAccent({
+ bgVar: UI.COLOR_CHAIN_BASE_BG,
+ borderVar: UI.COLOR_CHAIN_BASE_BORDER,
+ accentVar: UI.COLOR_CHAIN_BASE_ACCENT,
+ color: '#0052FF',
+ }),
+ createChainAccent({
+ bgVar: UI.COLOR_CHAIN_ARBITRUM_BG,
+ borderVar: UI.COLOR_CHAIN_ARBITRUM_BORDER,
+ accentVar: UI.COLOR_CHAIN_ARBITRUM_ACCENT,
+ color: '#1B4ADD',
+ }),
+ createChainAccent({
+ bgVar: UI.COLOR_CHAIN_POLYGON_BG,
+ borderVar: UI.COLOR_CHAIN_POLYGON_BORDER,
+ accentVar: UI.COLOR_CHAIN_POLYGON_ACCENT,
+ color: '#8247E5',
+ }),
+ createChainAccent({
+ bgVar: UI.COLOR_CHAIN_AVALANCHE_BG,
+ borderVar: UI.COLOR_CHAIN_AVALANCHE_BORDER,
+ accentVar: UI.COLOR_CHAIN_AVALANCHE_ACCENT,
+ color: '#FF3944',
+ }),
+ createChainAccent({
+ bgVar: UI.COLOR_CHAIN_GNOSIS_BG,
+ borderVar: UI.COLOR_CHAIN_GNOSIS_BORDER,
+ accentVar: UI.COLOR_CHAIN_GNOSIS_ACCENT,
+ color: '#07795B',
+ }),
+ createChainAccent({
+ bgVar: UI.COLOR_CHAIN_LENS_BG,
+ borderVar: UI.COLOR_CHAIN_LENS_BORDER,
+ accentVar: UI.COLOR_CHAIN_LENS_ACCENT,
+ color: '#5A5A5A',
+ darkColor: '#D7D7D7',
+ }),
+ createChainAccent({
+ bgVar: UI.COLOR_CHAIN_SEPOLIA_BG,
+ borderVar: UI.COLOR_CHAIN_SEPOLIA_BORDER,
+ accentVar: UI.COLOR_CHAIN_SEPOLIA_ACCENT,
+ color: '#C12FF2',
+ }),
+ createChainAccent({
+ bgVar: UI.COLOR_CHAIN_LINEA_BG,
+ borderVar: UI.COLOR_CHAIN_LINEA_BORDER,
+ accentVar: UI.COLOR_CHAIN_LINEA_ACCENT,
+ color: '#61DFFF',
+ }),
+ createChainAccent({
+ bgVar: UI.COLOR_CHAIN_PLASMA_BG,
+ borderVar: UI.COLOR_CHAIN_PLASMA_BORDER,
+ accentVar: UI.COLOR_CHAIN_PLASMA_ACCENT,
+ color: '#569F8C',
+ }),
+]
+
+const CHAIN_ACCENT_VAR_DECLARATIONS = CHAIN_ACCENT_CONFIG.map(({
+ bgVar,
+ borderVar,
+ accentVar,
+ lightBg,
+ darkBg,
+ lightBorder,
+ darkBorder,
+ lightColor,
+ darkColor,
+}) => css`
+ ${bgVar}: ${({ theme }) => (theme.darkMode ? darkBg : lightBg)};
+ ${borderVar}: ${({ theme }) => (theme.darkMode ? darkBorder : lightBorder)};
+ ${accentVar
+ ? css`
+ ${accentVar}: ${({ theme }) => (theme.darkMode ? darkColor : lightColor)};
+ `
+ : ''}
+`)
+
export const ThemeColorVars = css`
:root {
// V3
@@ -83,6 +230,8 @@ export const ThemeColorVars = css`
${UI.COLOR_ALERT_TEXT_DARKER}: ${({ theme }) =>
getContrastText(theme.alert, theme.darkMode ? darken(theme.alert, 0.55) : darken(theme.alert, 0.35))};
+ ${CHAIN_ACCENT_VAR_DECLARATIONS}
+
${UI.COLOR_WARNING}: ${({ theme }) => theme.warning};
${UI.COLOR_WARNING_BG}: ${({ theme }) => transparentize(theme.warning, 0.85)};
${UI.COLOR_WARNING_TEXT}: ${({ theme }) =>