diff --git a/apps/main/src/dao/components/ComboBoxSelectGauge/index.tsx b/apps/main/src/dao/components/ComboBoxSelectGauge/index.tsx index 51cfe1ccbe..7dcac23a26 100644 --- a/apps/main/src/dao/components/ComboBoxSelectGauge/index.tsx +++ b/apps/main/src/dao/components/ComboBoxSelectGauge/index.tsx @@ -9,8 +9,8 @@ import { useUserGaugeWeightVotesQuery } from '@/dao/entities/user-gauge-weight-v import useStore from '@/dao/store/useStore' import { GaugeFormattedData } from '@/dao/types/dao.types' import { delayAction } from '@/dao/utils' -import useMediaQuery from '@mui/material/useMediaQuery' import ModalDialog from '@ui/Dialog' +import { useIsMobile } from '@ui-kit/hooks/useBreakpoints' import { t } from '@ui-kit/lib/i18n' import { Chain } from '@ui-kit/utils/network' @@ -35,7 +35,7 @@ const ComboBoxGauges = ({ const setSelectedGauge = useStore((state) => state.gauges.setSelectedGauge) const setStateByKey = useStore((state) => state.gauges.setStateByKey) const gaugeMapper = useStore((state) => state.gauges.gaugeMapper) - const isMobile = useMediaQuery((t) => t.breakpoints.down('tablet')) + const isMobile = useIsMobile() const { data: userGaugeWeightVotes } = useUserGaugeWeightVotesQuery({ chainId: Chain.Ethereum, // DAO is only used on mainnet diff --git a/apps/main/src/dex/components/PageCreatePool/ConfirmModal/ModalDialog.tsx b/apps/main/src/dex/components/PageCreatePool/ConfirmModal/ModalDialog.tsx index 6f7c349eea..9e165a43d8 100644 --- a/apps/main/src/dex/components/PageCreatePool/ConfirmModal/ModalDialog.tsx +++ b/apps/main/src/dex/components/PageCreatePool/ConfirmModal/ModalDialog.tsx @@ -4,12 +4,12 @@ import type { AriaOverlayProps } from 'react-aria' import type { OverlayTriggerState } from 'react-stately' import styled from 'styled-components' import useStore from '@/dex/store/useStore' -import useMediaQuery from '@mui/material/useMediaQuery' import type { AriaDialogProps } from '@react-types/dialog' import Box from '@ui/Box' import Icon from '@ui/Icon' import IconButton from '@ui/IconButton' import { breakpoints } from '@ui/utils/responsive' +import { useIsMobile } from '@ui-kit/hooks/useBreakpoints' interface Props extends AriaOverlayProps, AriaDialogProps { footerContent?: ReactNode @@ -38,7 +38,7 @@ const ModalDialog = ({ const { titleProps } = useDialog(props, modalRef) usePreventScroll({ isDisabled: false }) // prevent scrolling while modal is open - const isMobile = useMediaQuery((t) => t.breakpoints.down('tablet')) + const isMobile = useIsMobile() const isSmUp = useStore((state) => state.isSmUp) const { buttonProps: closeButtonProps } = useButton( diff --git a/apps/main/src/dex/components/PageCreatePool/TokensInPool/SelectTokenButton.tsx b/apps/main/src/dex/components/PageCreatePool/TokensInPool/SelectTokenButton.tsx index 01e39d0624..dc7e8660d9 100644 --- a/apps/main/src/dex/components/PageCreatePool/TokensInPool/SelectTokenButton.tsx +++ b/apps/main/src/dex/components/PageCreatePool/TokensInPool/SelectTokenButton.tsx @@ -5,7 +5,6 @@ import { CreateToken } from '@/dex/components/PageCreatePool/types' import useStore from '@/dex/store/useStore' import { ChainId, CurveApi } from '@/dex/types/main.types' import { delayAction } from '@/dex/utils' -import useMediaQuery from '@mui/material/useMediaQuery' import { useButton } from '@react-aria/button' import { useFilter } from '@react-aria/i18n' import { useOverlayTriggerState } from '@react-stately/overlays' @@ -15,6 +14,7 @@ import Checkbox from '@ui/Checkbox' import Spinner, { SpinnerWrapper } from '@ui/Spinner' import { Chip } from '@ui/Typography' import { TokenSelectorModal } from '@ui-kit/features/select-token/ui/modal/TokenSelectorModal' +import { useIsMobile } from '@ui-kit/hooks/useBreakpoints' import { t } from '@ui-kit/lib/i18n' import { TokenIcon } from '@ui-kit/shared/ui/TokenIcon' import { type Address, filterTokens, shortenAddress } from '@ui-kit/utils' @@ -46,7 +46,7 @@ const SelectTokenButton = ({ const { buttonProps: openButtonProps } = useButton({ onPress: () => overlayTriggerState.open() }, openButtonRef) const { endsWith } = useFilter({ sensitivity: 'base' }) - const isMobile = useMediaQuery((t) => t.breakpoints.down('tablet')) + const isMobile = useIsMobile() const nativeToken = useStore((state) => state.networks.nativeToken[chainId]) const userAddedTokens = useStore((state) => state.createPool.userAddedTokens) diff --git a/apps/main/src/dex/components/PoolLabel.tsx b/apps/main/src/dex/components/PoolLabel.tsx index d711d708a6..36dcff991b 100644 --- a/apps/main/src/dex/components/PoolLabel.tsx +++ b/apps/main/src/dex/components/PoolLabel.tsx @@ -7,11 +7,11 @@ import usePoolAlert from '@/dex/hooks/usePoolAlert' import useTokenAlert from '@/dex/hooks/useTokenAlert' import useStore from '@/dex/store/useStore' import { PoolData, PoolDataCache } from '@/dex/types/main.types' -import useMediaQuery from '@mui/material/useMediaQuery' import AlertBox from '@ui/AlertBox' import Box from '@ui/Box' import { TooltipAlert as AlertTooltipIcon } from '@ui/Tooltip' import { Chip } from '@ui/Typography' +import { useIsMobile } from '@ui-kit/hooks/useBreakpoints' import { TokenIcons } from '@ui-kit/shared/ui/TokenIcons' type PoolListProps = { @@ -43,7 +43,7 @@ const PoolLabel = ({ className = '', blockchainId, isVisible = true, poolData, p const poolAlert = usePoolAlert(poolData?.pool.address, poolData?.hasVyperVulnerability) const tokenAlert = useTokenAlert(poolData?.tokenAddressesAll ?? []) - const isMobile = useMediaQuery((t) => t.breakpoints.down('tablet')) + const isMobile = useIsMobile() const searchedTerms = useStore((state) => state.poolList.searchedTerms) const { quickViewValue, onClick } = poolListProps ?? {} diff --git a/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsTable.tsx b/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsTable.tsx index d46fc8a097..0846e0fc84 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsTable.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsTable.tsx @@ -10,7 +10,6 @@ import { LendingMarketsFilters } from '@/loan/components/PageLlamaMarkets/Lendin import { LlamaMarketExpandedPanel } from '@/loan/components/PageLlamaMarkets/LlamaMarketExpandedPanel' import { MarketsFilterChips } from '@/loan/components/PageLlamaMarkets/MarketsFilterChips' import { type LlamaMarketsResult } from '@/loan/entities/llama-markets' -import { useMediaQuery } from '@mui/material' import Stack from '@mui/material/Stack' import { ExpandedState, @@ -18,8 +17,10 @@ import { getExpandedRowModel, getFilteredRowModel, getSortedRowModel, + SortingState, useReactTable, } from '@tanstack/react-table' +import { useIsMobile, useIsTablet } from '@ui-kit/hooks/useBreakpoints' import { useSortFromQueryString } from '@ui-kit/hooks/useSortFromQueryString' import { t } from '@ui-kit/lib/i18n' import { DataTable } from '@ui-kit/shared/ui/DataTable' @@ -28,19 +29,19 @@ import { TableFilters, useColumnFilters } from '@ui-kit/shared/ui/DataTable/Tabl import { useVisibilitySettings } from '@ui-kit/shared/ui/DataTable/TableVisibilitySettingsPopover' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' -const { Spacing, MaxWidth, Sizing } = SizesAndSpaces +const { Spacing, MaxWidth } = SizesAndSpaces /** * Hook to manage the visibility of columns in the Llama Markets table. * The visibility on mobile is based on the sort field. * On larger devices, it uses the visibility settings that may be customized by the user. */ -const useVisibility = (sortField: LlamaMarketColumnId, hasPositions: boolean | undefined) => { - const isMobile = useMediaQuery((t) => t.breakpoints.down('tablet')) +const useVisibility = (sorting: SortingState, hasPositions: boolean | undefined) => { + const sortField = (sorting.length ? sorting : DEFAULT_SORT)[0].id as LlamaMarketColumnId const groups = useMemo(() => createLlamaMarketsColumnOptions(hasPositions), [hasPositions]) - const visibilitySettings = useVisibilitySettings(groups) + const visibilitySettings = useVisibilitySettings(groups, LLAMA_MARKET_COLUMNS) const columnVisibility = useMemo(() => createLlamaMarketsMobileColumns(sortField), [sortField]) - return { ...visibilitySettings, ...(isMobile && { columnVisibility }) } + return { sortField, ...visibilitySettings, ...(useIsMobile() && { columnVisibility }) } } // todo: rename to LlamaMarketsTable @@ -62,8 +63,7 @@ export const LendingMarketsTable = ({ { id: LlamaMarketColumnId.LiquidityUsd, value: [minLiquidity, undefined] }, ]) const [sorting, onSortingChange] = useSortFromQueryString(DEFAULT_SORT) - const sortField = (sorting.length ? sorting : DEFAULT_SORT)[0].id as LlamaMarketColumnId - const { columnSettings, columnVisibility, toggleVisibility } = useVisibility(sortField, result?.hasPositions) + const { columnSettings, columnVisibility, toggleVisibility, sortField } = useVisibility(sorting, result?.hasPositions) const [expanded, setExpanded] = useState({}) const table = useReactTable({ columns: LLAMA_MARKET_COLUMNS, @@ -89,9 +89,9 @@ export const LendingMarketsTable = ({ title={t`Llamalend Markets`} diff --git a/apps/main/src/loan/components/PageLlamaMarkets/LlamaMarketExpandedPanel.tsx b/apps/main/src/loan/components/PageLlamaMarkets/LlamaMarketExpandedPanel.tsx index 0ee0af476d..b95b56094a 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/LlamaMarketExpandedPanel.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/LlamaMarketExpandedPanel.tsx @@ -1,67 +1,113 @@ import Link from 'next/link' +import { useMemo } from 'react' import { LineGraphCell } from '@/loan/components/PageLlamaMarkets/cells' import { LlamaMarketColumnId } from '@/loan/components/PageLlamaMarkets/columns.enum' import { FavoriteMarketButton } from '@/loan/components/PageLlamaMarkets/FavoriteMarketButton' import { useUserMarketStats } from '@/loan/entities/llama-market-stats' +import useStore from '@/loan/store/useStore' +import { ArrowRight } from '@carbon/icons-react' import Button from '@mui/material/Button' +import CardHeader from '@mui/material/CardHeader' +import Grid from '@mui/material/Grid2' import Stack from '@mui/material/Stack' +import Typography from '@mui/material/Typography' +import { useIsTiny } from '@ui-kit/hooks/useBreakpoints' import { t } from '@ui-kit/lib/i18n' import { CopyIconButton } from '@ui-kit/shared/ui/CopyIconButton' -import { ExpansionPanelSection } from '@ui-kit/shared/ui/DataTable/ExpansionPanelSection' import { type ExpandedPanel } from '@ui-kit/shared/ui/DataTable/ExpansionRow' import { Metric } from '@ui-kit/shared/ui/Metric' +import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' import type { LlamaMarket } from '../../entities/llama-markets' +const { Spacing } = SizesAndSpaces + +function useMobileGraphSize() { + const pageWidth = useStore((state) => state.layout.windowWidth) + const isTiny = useIsTiny() + return useMemo(() => ({ width: pageWidth ? pageWidth - (isTiny ? 20 : 40) : 300, height: 48 }), [pageWidth, isTiny]) +} + export const LlamaMarketExpandedPanel: ExpandedPanel = ({ row: { original: market } }) => { const { data: earnings, error: earningsError } = useUserMarketStats(market, LlamaMarketColumnId.UserEarnings) const { data: deposited, error: depositedError } = useUserMarketStats(market, LlamaMarketColumnId.UserDeposited) + const { leverage, utilizationPercent, liquidityUsd, userHasPosition, url, address, rates } = market + const graphSize = useMobileGraphSize() return ( <> - - {t`Market Details`} - - - - } - > - - - {market.rates.lend && ( - <> - - - + + + + {t`Market Details`} + + + + + + } + sx={{ paddingInline: 0 }} + > + + + + + {leverage > 0 && ( + + + )} - - - - {market.userHasPosition && ( - - {earnings?.earnings != null && } + + + + + + + + + + {t`7D Rate Chart`} + + + + + + + {userHasPosition && ( + + + + + {earnings?.earnings != null && ( + + + + )} {deposited?.deposited != null && ( - + + + )} - + )} diff --git a/apps/main/src/loan/components/PageLlamaMarkets/MarketsFilterChips.tsx b/apps/main/src/loan/components/PageLlamaMarkets/MarketsFilterChips.tsx index e7353ddac9..5694937e7a 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/MarketsFilterChips.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/MarketsFilterChips.tsx @@ -8,7 +8,7 @@ import PersonIcon from '@mui/icons-material/Person' import Grid from '@mui/material/Grid2' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' -import useMediaQuery from '@mui/material/useMediaQuery' +import { useIsMobile } from '@ui-kit/hooks/useBreakpoints' import { t } from '@ui-kit/lib/i18n' import { HeartIcon } from '@ui-kit/shared/icons/HeartIcon' import { PointsIcon } from '@ui-kit/shared/icons/PointsIcon' @@ -83,8 +83,6 @@ export const MarketsFilterChips = ({ const [rewards, toggleRewards] = useToggleFilter(LlamaMarketColumnId.Rewards, props) const [marketTypes, toggleMarkets] = useMarketTypeFilter(props) const { address } = useAccount() - const isMobile = useMediaQuery((t) => t.breakpoints.down('tablet')) - return ( - + {t`Hidden Markets`} {hiddenMarketCount} diff --git a/apps/main/src/loan/components/PageLlamaMarkets/Page.tsx b/apps/main/src/loan/components/PageLlamaMarkets/Page.tsx index 5b26ae94a9..7595c913c8 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/Page.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/Page.tsx @@ -18,6 +18,7 @@ import Box from '@mui/material/Box' import Skeleton from '@mui/material/Skeleton' import { useUserProfileStore } from '@ui-kit/features/user-profile' import { SMALL_POOL_TVL } from '@ui-kit/features/user-profile/store' +import { useIsTiny } from '@ui-kit/hooks/useBreakpoints' import { logSuccess } from '@ui-kit/lib' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' import { Address } from '@ui-kit/utils' @@ -72,7 +73,7 @@ export const LlamaMarketsPage = (props: CrvUsdServerData) => { const headerHeight = useHeaderHeight(bannerHeight) const showSkeleton = !data && (!isError || isLoading) // on initial render isLoading is still false return ( - + {showSkeleton ? ( ) : ( diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/LineGraphCell.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/LineGraphCell.tsx index 1ff7fcd23b..6360d52ffe 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/LineGraphCell.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/LineGraphCell.tsx @@ -10,7 +10,7 @@ import { t } from '@ui-kit/lib/i18n' import { DesignSystem } from '@ui-kit/themes/design' import { RateType, useSnapshots } from '../hooks/useSnapshots' -const graphSize = { width: 172, height: 48 } +const defaultGraphSize = { width: 100, height: 48 } /** * Get the color for the line graph. Will be green if the last value is higher than the first, red if lower, and blue if equal. @@ -33,12 +33,13 @@ const calculateDomain = type RateCellProps = { market: LlamaMarket type: RateType + graphSize?: typeof defaultGraphSize } /** * Line graph cell that displays the average historical APY for a vault and a given type (borrow or lend). */ -export const LineGraphCell = ({ market, type }: RateCellProps) => { +export const LineGraphCell = ({ market, type, graphSize = defaultGraphSize }: RateCellProps) => { const ref = useRef(null) const entry = useIntersectionObserver(ref, { freezeOnceVisible: true }) const { snapshots, snapshotKey, isLoading, rate, error } = useSnapshots(market, type, entry?.isIntersecting) diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketBadges.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketBadges.tsx index 741e6f75ed..fa6690e24b 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketBadges.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketBadges.tsx @@ -1,18 +1,19 @@ import { getRewardsDescription } from '@/loan/components/PageLlamaMarkets/cells/MarketTitleCell/cell.utils' import { FavoriteMarketButton } from '@/loan/components/PageLlamaMarkets/FavoriteMarketButton' -import { useFavoriteMarket } from '@/loan/entities/favorite-markets' +import { useUserMarketStats } from '@/loan/entities/llama-market-stats' import { LlamaMarket, LlamaMarketType } from '@/loan/entities/llama-markets' import Box from '@mui/material/Box' import Chip from '@mui/material/Chip' import Stack from '@mui/material/Stack' -import { useTheme } from '@mui/material/styles' +import Typography from '@mui/material/Typography' import useMediaQuery from '@mui/material/useMediaQuery' import { t } from '@ui-kit/lib/i18n' +import { ExclamationTriangleIcon } from '@ui-kit/shared/icons/ExclamationTriangleIcon' import { RewardIcons } from '@ui-kit/shared/ui/RewardIcon' import { Tooltip } from '@ui-kit/shared/ui/Tooltip' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' -const { Spacing } = SizesAndSpaces +const { Spacing, Sizing } = SizesAndSpaces const poolTypeNames: Record string> = { [LlamaMarketType.Lend]: () => t`Lend`, @@ -25,15 +26,15 @@ const poolTypeTooltips: Record string> = { } /** Displays badges for a pool, such as the chain icon and the pool type. */ -export const MarketBadges = ({ market: { address, rewards, type, leverage } }: { market: LlamaMarket }) => { - const [isFavorite, toggleFavorite] = useFavoriteMarket(address) - const isMobile = useMediaQuery((t) => t.breakpoints.down('tablet')) - const iconColor = useTheme().design.Text.TextColors.Highlight +export const MarketBadges = ({ market, isMobile }: { market: LlamaMarket; isMobile: boolean }) => { + const { address, rewards, type, leverage, deprecatedMessage } = market + const isSmall = useMediaQuery('(max-width:1250px)') + const { isCollateralEroded } = useUserMarketStats(market)?.data ?? {} return ( - + 0 && ( - + {isMobile ? ( + 🔥 + ) : ( + + )} )} @@ -61,6 +66,21 @@ export const MarketBadges = ({ market: { address, rewards, type, leverage } }: { )} + {deprecatedMessage && ( + + + {!isSmall && t`Deprecated`} + + + + )} + + {isCollateralEroded && ( + + + + )} + ) diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketTitleCell.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketTitleCell.tsx index cdf232c663..7776a55ce9 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketTitleCell.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketTitleCell.tsx @@ -1,13 +1,12 @@ import RouterLink from 'next/link' import { MouseEvent } from 'react' import { MarketBadges } from '@/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketBadges' -import { MarketWarnings } from '@/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketWarnings' import { LlamaMarket } from '@/loan/entities/llama-markets' import MuiLink from '@mui/material/Link' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' -import useMediaQuery from '@mui/material/useMediaQuery' import { CellContext } from '@tanstack/react-table' +import { useIsMobile } from '@ui-kit/hooks/useBreakpoints' import { t } from '@ui-kit/lib/i18n' import { CopyIconButton } from '@ui-kit/shared/ui/CopyIconButton' import { ClickableInRowClass, DesktopOnlyHoverClass } from '@ui-kit/shared/ui/DataTable' @@ -16,36 +15,46 @@ import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' const { Spacing } = SizesAndSpaces -export const MarketTitleCell = ({ row: { original: market } }: CellContext) => ( - - - - - - t.breakpoints.down('tablet')) && { - // cancel click on mobile so the panel can open, there is a separate button for it - onClick: (e: MouseEvent) => e.preventDefault(), - })} - > - {market.assets.collateral.symbol} - {market.assets.borrowed.symbol} - - - - +export const MarketTitleCell = ({ row: { original: market } }: CellContext) => { + const isMobile = useIsMobile() + return ( + + + + + ) => e.preventDefault(), + })} + sx={{ + // for very small screens, truncate the text and limit to a maximum width + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + maxWidth: '40vw', // make sure the other column will fit in small screens + paddingBlock: { mobile: '5px', tablet: 0 }, + }} + > + {market.assets.collateral.symbol} - {market.assets.borrowed.symbol} + + + + + - -) + ) +} diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketWarnings.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketWarnings.tsx deleted file mode 100644 index 830807fe9e..0000000000 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketWarnings.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useUserMarketStats } from '@/loan/entities/llama-market-stats' -import { LlamaMarket } from '@/loan/entities/llama-markets' -import Chip from '@mui/material/Chip' -import Stack from '@mui/material/Stack' -import Typography from '@mui/material/Typography' -import { t } from '@ui-kit/lib/i18n' -import { ExclamationTriangleIcon } from '@ui-kit/shared/icons/ExclamationTriangleIcon' -import { Tooltip } from '@ui-kit/shared/ui/Tooltip' -import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' - -const { Spacing } = SizesAndSpaces - -/** - * Displays warnings for a pool, such as deprecated pools or pools with collateral corrosion. - */ -export const MarketWarnings = ({ market }: { market: LlamaMarket }) => { - const { isCollateralEroded } = useUserMarketStats(market)?.data ?? {} - return ( - - {market.deprecatedMessage && ( - - - {t`Deprecated`} - - - - )} - {isCollateralEroded && ( - - - - )} - - ) -} diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/RateCell.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/RateCell.tsx index efc219208b..795112e693 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/RateCell.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/RateCell.tsx @@ -3,13 +3,14 @@ import { LlamaMarket } from '@/loan/entities/llama-markets' import Chip from '@mui/material/Chip' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' +import { useIsMobile } from '@ui-kit/hooks/useBreakpoints' import useIntersectionObserver from '@ui-kit/hooks/useIntersectionObserver' import { t } from '@ui-kit/lib/i18n' import { RewardIcons } from '@ui-kit/shared/ui/RewardIcon' import { Tooltip } from '@ui-kit/shared/ui/Tooltip' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' import { RateType, useSnapshots } from '../hooks/useSnapshots' -import { formatPercent, getRewardsAction } from './cell.format' +import { formatPercent, formatPercentFixed, getRewardsAction } from './cell.format' import { RateTooltipContent } from './RateCellTooltip' const { Spacing } = SizesAndSpaces @@ -21,6 +22,7 @@ export const RateCell = ({ market, type }: { market: LlamaMarket; type: RateType const { rewards, type: marketType } = market const rewardsAction = getRewardsAction(marketType, type) const poolRewards = rewards.filter(({ action }) => action == rewardsAction) + const isMobile = useIsMobile() return ( - {averageRate != null && formatPercent(averageRate)} + {isMobile ? rate != null && formatPercentFixed(rate) : averageRate != null && formatPercentFixed(averageRate)} - {rate != null && ( + {!isMobile && rate != null && ( {formatPercent(rate)} diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/cell.format.ts b/apps/main/src/loan/components/PageLlamaMarkets/cells/cell.format.ts index ae0ab98bf3..1ff8a875b2 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/cell.format.ts +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/cell.format.ts @@ -3,6 +3,7 @@ import { LlamaMarketType } from '@/loan/entities/llama-markets' import type { RewardsAction } from '@ui/CampaignRewards/types' export const formatPercent = (rate: number) => `${rate.toPrecision(4)}%` +export const formatPercentFixed = (rate: number) => `${rate.toFixed(2)}%` export const getRewardsAction = (marketType: LlamaMarketType, type: RateType): RewardsAction => marketType === LlamaMarketType.Mint ? 'loan' : type == 'borrow' ? 'borrow' : 'supply' diff --git a/apps/main/src/loan/components/PageLlamaMarkets/columns.tsx b/apps/main/src/loan/components/PageLlamaMarkets/columns.tsx index 684cdc9ea1..839bde07e0 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/columns.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/columns.tsx @@ -35,7 +35,6 @@ export const LLAMA_MARKET_COLUMNS = [ header: t`Health`, cell: PercentageCell, meta: { type: 'numeric', hideZero: true }, - size: ColumnWidth.sm, sortUndefined: 'last', }), columnHelper.display({ @@ -43,7 +42,6 @@ export const LLAMA_MARKET_COLUMNS = [ header: t`Borrow Amount`, cell: PriceCell, meta: { type: 'numeric', borderRight: true }, - size: ColumnWidth.sm, sortUndefined: 'last', }), columnHelper.display({ @@ -51,7 +49,6 @@ export const LLAMA_MARKET_COLUMNS = [ header: t`My Earnings`, cell: PriceCell, meta: { type: 'numeric' }, - size: ColumnWidth.sm, sortUndefined: 'last', }), columnHelper.display({ @@ -59,7 +56,6 @@ export const LLAMA_MARKET_COLUMNS = [ header: t`Supplied Amount`, cell: PriceCell, meta: { type: 'numeric', borderRight: true }, - size: ColumnWidth.sm, filterFn: boolFilterFn, sortUndefined: 'last', }), @@ -68,41 +64,29 @@ export const LLAMA_MARKET_COLUMNS = [ header: t`7D Avg Borrow Rate`, cell: (c) => , meta: { type: 'numeric' }, - size: ColumnWidth.sm, sortUndefined: 'last', }), - columnHelper.accessor('rates.borrow', { - id: LlamaMarketColumnId.BorrowChart, - header: t`7D Borrow Rate Chart`, - cell: (c) => , - size: ColumnWidth.md, - }), columnHelper.accessor('rates.lend', { id: LlamaMarketColumnId.LendRate, header: t`7D Avg Supply Yield`, cell: (c) => , meta: { type: 'numeric' }, - size: ColumnWidth.sm, sortUndefined: 'last', }), - columnHelper.accessor('rates.lend', { - id: LlamaMarketColumnId.LendChart, - header: t`7D Supply Yield Chart`, - cell: (c) => , - size: ColumnWidth.md, - sortUndefined: 'last', + columnHelper.accessor('rates.borrow', { + id: LlamaMarketColumnId.BorrowChart, + header: t`7D Rate Chart`, + cell: (c) => , }), columnHelper.accessor(LlamaMarketColumnId.UtilizationPercent, { header: t`Utilization`, cell: PercentageCell, meta: { type: 'numeric' }, - size: ColumnWidth.sm, }), columnHelper.accessor(LlamaMarketColumnId.LiquidityUsd, { header: t`Available Liquidity`, cell: CompactUsdCell, meta: { type: 'numeric' }, - size: ColumnWidth.sm, }), // Following columns are used in tanstack filter, but they are displayed together in MarketTitleCell hidden(LlamaMarketColumnId.Chain, LlamaMarketColumnId.Chain, multiFilterFn), diff --git a/apps/main/src/loan/components/PageLlamaMarkets/hooks/useLlamaMarketsColumnVisibility.tsx b/apps/main/src/loan/components/PageLlamaMarkets/hooks/useLlamaMarketsColumnVisibility.tsx index 187391b058..999dca8a37 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/hooks/useLlamaMarketsColumnVisibility.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/hooks/useLlamaMarketsColumnVisibility.tsx @@ -34,13 +34,18 @@ export const createLlamaMarketsColumnOptions = (hasPositions: boolean | undefine active: true, enabled: true, }, + { + label: t`Chart`, + columns: [LlamaMarketColumnId.BorrowChart], + active: true, + enabled: true, + }, ], }, { label: t`Borrow`, options: [ { columns: [LlamaMarketColumnId.BorrowRate], active: true, enabled: true }, - { label: t`Chart`, columns: [LlamaMarketColumnId.BorrowChart], active: true, enabled: true }, { label: t`Borrow Details`, columns: [LlamaMarketColumnId.UserHealth, LlamaMarketColumnId.UserBorrowed], @@ -53,7 +58,6 @@ export const createLlamaMarketsColumnOptions = (hasPositions: boolean | undefine label: t`Lend`, options: [ { columns: [LlamaMarketColumnId.LendRate], active: true, enabled: true }, - { label: t`Chart`, columns: [LlamaMarketColumnId.LendChart], active: false, enabled: true }, { label: t`Lend Details`, columns: [LlamaMarketColumnId.UserEarnings, LlamaMarketColumnId.UserDeposited], diff --git a/apps/main/src/loan/entities/crvusd-snapshots.ts b/apps/main/src/loan/entities/crvusd-snapshots.ts index c0e9308a41..df5df399c1 100644 --- a/apps/main/src/loan/entities/crvusd-snapshots.ts +++ b/apps/main/src/loan/entities/crvusd-snapshots.ts @@ -9,7 +9,7 @@ export type CrvUsdSnapshot = Snapshot export const { useQuery: useCrvUsdSnapshots } = queryFactory({ queryKey: (params: ContractParams) => [...rootKeys.contract(params), 'crvUsd', 'snapshots'] as const, queryFn: ({ blockchainId, contractAddress }: ContractQuery): Promise => - getSnapshots(blockchainId as Chain, contractAddress, { agg: 'none' }), + getSnapshots(blockchainId as Chain, contractAddress, { agg: 'none', fetch_on_chain: false }), staleTime: '10m', validationSuite: contractValidationSuite, }) diff --git a/apps/main/src/loan/entities/lending-snapshots.ts b/apps/main/src/loan/entities/lending-snapshots.ts index 8aa5dd6e27..29c2994727 100644 --- a/apps/main/src/loan/entities/lending-snapshots.ts +++ b/apps/main/src/loan/entities/lending-snapshots.ts @@ -15,7 +15,7 @@ export const { useQuery: useLendingSnapshots } = queryFactory({ if (!chains.includes(chain)) return [] // backend gives 404 for optimism // todo: pass {sort_by: 'DATE_ASC, start: now-week} and remove reverse (backend is timing out) - const response = await getSnapshots(chain, contractAddress, { agg: 'none' }) + const response = await getSnapshots(chain, contractAddress, { agg: 'none', fetch_on_chain: false }) return response.reverse() }, staleTime: '1h', diff --git a/apps/main/src/loan/entities/lending-vaults.ts b/apps/main/src/loan/entities/lending-vaults.ts index 47fe8617e6..ea937a6bd4 100644 --- a/apps/main/src/loan/entities/lending-vaults.ts +++ b/apps/main/src/loan/entities/lending-vaults.ts @@ -31,7 +31,9 @@ export const { queryFn: async (): Promise => { const chains = await fetchSupportedLendingChains({}) const markets = await Promise.all( - chains.map(async (chain) => (await getMarkets(chain)).map((market) => ({ ...market, chain }))), + chains.map(async (chain) => + (await getMarkets(chain, { fetch_on_chain: false })).map((market) => ({ ...market, chain })), + ), ) return markets.flat() }, diff --git a/apps/main/src/loan/entities/mint-markets.ts b/apps/main/src/loan/entities/mint-markets.ts index 4e92e90aab..e3274af156 100644 --- a/apps/main/src/loan/entities/mint-markets.ts +++ b/apps/main/src/loan/entities/mint-markets.ts @@ -43,7 +43,7 @@ export const { // todo: create separate query for the loop, so it can be cached separately chains.map(async (blockchainId) => { const chain = blockchainId as Chain - const data = await getMarkets(chain) + const data = await getMarkets(chain, { fetch_on_chain: false }) return await addStableCoinPrices({ chain, data }) }), ) diff --git a/apps/main/src/loan/store/createLayoutSlice.ts b/apps/main/src/loan/store/createLayoutSlice.ts index 1c060f9821..8a8be265e0 100644 --- a/apps/main/src/loan/store/createLayoutSlice.ts +++ b/apps/main/src/loan/store/createLayoutSlice.ts @@ -17,6 +17,7 @@ type SliceState = { isXSmDown: boolean isXXSm: boolean pageWidth: PageWidthClassName | null + windowWidth: number | null scrollY: number } @@ -53,6 +54,7 @@ const DEFAULT_STATE: SliceState = { isXSmDown: false, isXXSm: false, pageWidth: null, + windowWidth: null, scrollY: 0, } @@ -70,6 +72,7 @@ const createLayoutSlice = (set: SetState, get: GetState) => ({ set( produce((state: State) => { + state.layout.windowWidth = window.innerWidth state.layout.pageWidth = pageWidthClassName state.layout.isXSmDown = isXSmDown state.layout.isSmUp = isSmUp || isMd || isLgUp diff --git a/packages/curve-ui-kit/src/hooks/useBreakpoints.ts b/packages/curve-ui-kit/src/hooks/useBreakpoints.ts new file mode 100644 index 0000000000..55bf486fee --- /dev/null +++ b/packages/curve-ui-kit/src/hooks/useBreakpoints.ts @@ -0,0 +1,10 @@ +import type { Theme } from '@mui/material' +import useMediaQuery from '@mui/material/useMediaQuery' + +const isDesktopUp = (theme: Theme) => theme.breakpoints.up('desktop') +const isTabletDown = (theme: Theme) => theme.breakpoints.down('tablet') + +export const useIsTiny = () => useMediaQuery('(max-width:400px)') +export const useIsMobile = () => useMediaQuery(isTabletDown) +export const useIsDesktop = () => useMediaQuery(isDesktopUp) +export const useIsTablet = () => ![useIsDesktop(), useIsMobile()].includes(true) diff --git a/packages/curve-ui-kit/src/shared/ui/DataTable/DataCell.tsx b/packages/curve-ui-kit/src/shared/ui/DataTable/DataCell.tsx index 65edb9335e..5a10c9537d 100644 --- a/packages/curve-ui-kit/src/shared/ui/DataTable/DataCell.tsx +++ b/packages/curve-ui-kit/src/shared/ui/DataTable/DataCell.tsx @@ -1,3 +1,4 @@ +import { mapValues } from 'lodash' import { Stack } from '@mui/material' import Box from '@mui/material/Box' import Typography from '@mui/material/Typography' @@ -15,36 +16,47 @@ const { Spacing } = SizesAndSpaces export const DataCell = ({ cell, isMobile, - isLast, + isSticky, }: { cell: Cell isMobile: boolean - isLast: boolean // todo: get rid of column hidden meta, use column.getIsLastColumn() + isSticky: boolean }) => { const { column, row } = cell const { variant, borderRight } = column.columnDef.meta ?? {} const children = flexRender(column.columnDef.cell, cell.getContext()) - const sx = { + + // with the collapse icon there is an extra wrapper, so keep the sx separate + const wrapperSx = { textAlign: getAlignment(column), paddingInline: Spacing.sm, - paddingBlock: Spacing.xs, // `md` removed, content should be vertically centered + // 1px less for the border bottom + paddingBlock: mapValues({ ...Spacing.xs, mobile: Spacing.md.mobile }, (value) => `${value} calc(${value} - 1px)`), } - const showCollapseIcon = isMobile && isLast + + const showCollapseIcon = isMobile && column.getIsLastColumn() return ( `1px solid ${t.design.Layer[1].Outline}` }), + ...((borderRight || isSticky) && { borderRight: (t) => `1px solid ${t.design.Layer[1].Outline}` }), + ...(isSticky && { + position: 'sticky', + left: 0, + zIndex: (t) => t.zIndex.tableStickyColumn, + backgroundColor: (t) => t.design.Table.Row.Default, + }), + borderBlockEnd: (t) => `1px solid ${t.design.Layer[1].Outline}`, }} data-testid={`data-table-cell-${column.id}`} > {showCollapseIcon ? ( - + {children} void) => { // ignore clicks on elements that should be clickable inside the row if (hasParentWithClass(target, ClickableInRowClass, { untilTag: 'TR' })) { @@ -23,24 +25,28 @@ const onCellClick = (target: EventTarget, url: string, routerNavigate: (href: st } } +export type DataRowProps = { + row: Row + isLast: boolean + expandedPanel: ExpandedPanel + shouldStickFirstColumn: boolean +} + export const DataRow = ({ + isLast, row, - sx, expandedPanel, -}: { - row: Row - sx?: SxProps - expandedPanel: ExpandedPanel -}) => { + shouldStickFirstColumn, +}: DataRowProps) => { + const isMobile = useIsMobile() const [element, setElement] = useState(null) // note: useRef doesn't get updated in cypress const { push } = useRouter() const url = row.original.url - const isMobile = useMediaQuery((t) => t.breakpoints.down('tablet')) const onClickDesktop = useCallback( (e: MouseEvent) => onCellClick(e.target, url, push), [url, push], ) - const visibleCells = row.getVisibleCells().filter((cell) => !cell.column.columnDef.meta?.hidden) + const visibleCells = row.getVisibleCells() return ( <> @@ -48,23 +54,33 @@ export const DataRow = ({ `1px solid ${t.design.Layer[1].Outline}`, cursor: 'pointer', transition: `border-bottom ${TransitionFunction}`, [`& .${DesktopOnlyHoverClass}`]: { opacity: { mobile: 1, desktop: 0 }, transition: `opacity ${TransitionFunction}`, }, - '&:hover': { [`& .${DesktopOnlyHoverClass}`]: { opacity: { desktop: 1 } } }, + '&:hover': { + [`& .${DesktopOnlyHoverClass}`]: { opacity: { desktop: 1 } }, + '& td, & th': { + backgroundColor: (t) => t.design.Table.Row.Hover, + }, + }, [`&.${CypressHoverClass}`]: { [`& .${DesktopOnlyHoverClass}`]: { opacity: { desktop: 1 } } }, - ...sx, + ...(isLast && { + // to avoid the sticky header showing without any rows, show the last row on top of it + position: 'sticky', + zIndex: (t) => t.zIndex.tableStickyLastRow, + top: 0, + backgroundColor: (t) => t.design.Table.Row.Default, + }), }} ref={setElement} data-testid={element && `data-table-row-${row.id}`} onClick={isMobile ? () => row.toggleExpanded() : onClickDesktop} > {visibleCells.map((cell, index) => ( - + ))} diff --git a/packages/curve-ui-kit/src/shared/ui/DataTable/DataTable.tsx b/packages/curve-ui-kit/src/shared/ui/DataTable/DataTable.tsx index fc8ea3d001..34322bf82e 100644 --- a/packages/curve-ui-kit/src/shared/ui/DataTable/DataTable.tsx +++ b/packages/curve-ui-kit/src/shared/ui/DataTable/DataTable.tsx @@ -4,16 +4,14 @@ import Table from '@mui/material/Table' import TableBody from '@mui/material/TableBody' import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' -import type { ExpandedPanel } from '@ui-kit/shared/ui/DataTable/ExpansionRow' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' -import type { SxProps } from '@ui-kit/utils' import { type TableItem, type TanstackTable } from './data-table.utils' -import { DataRow } from './DataRow' +import { DataRow, type DataRowProps } from './DataRow' import { EmptyStateRow } from './EmptyStateRow' import { FilterRow } from './FilterRow' import { HeaderCell } from './HeaderCell' -const { Sizing, MinWidth } = SizesAndSpaces +const { Sizing } = SizesAndSpaces /** * DataTable component to render the table with headers and rows. @@ -23,49 +21,50 @@ export const DataTable = ({ headerHeight, emptyText, children, - rowSx, - expandedPanel, + ...rowProps }: { table: TanstackTable headerHeight: string emptyText: string children?: ReactNode // passed to - rowSx?: SxProps minRowHeight?: number - expandedPanel: ExpandedPanel -}) => ( - t.design.Layer[1].Fill, - borderCollapse: 'separate' /* Don't collapse to avoid funky stuff with the sticky header */, - }} - data-testid="data-table" - > - ({ - zIndex: t.zIndex.appBar - 1, - position: 'sticky', - top: headerHeight, - backgroundColor: t.design.Table.Header.Fill, - })} - data-testid="data-table-head" +} & Omit, 'row' | 'isLast'>) => { + const { rows } = table.getRowModel() + const { shouldStickFirstColumn } = rowProps + return ( +
t.design.Layer[1].Fill, + borderCollapse: 'separate' /* Don't collapse to avoid funky stuff with the sticky header */, + }} + data-testid="data-table" > - {children && {children}} + ({ + zIndex: t.zIndex.tableHeader, + position: 'sticky', + top: headerHeight, + backgroundColor: t.design.Table.Header.Fill, + marginBlock: Sizing['sm'], + })} + data-testid="data-table-head" + > + {children && {children}} - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.length === 0 && {emptyText}} - {table.getRowModel().rows.map((row) => ( - key={row.id} row={row} sx={rowSx} expandedPanel={expandedPanel} /> - ))} - -
-) + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header, index) => ( + + ))} + + ))} + + + {rows.length === 0 && {emptyText}} + {rows.map((row, index) => ( + key={row.id} row={row} isLast={index === rows.length - 1} {...rowProps} /> + ))} + + + ) +} diff --git a/packages/curve-ui-kit/src/shared/ui/DataTable/ExpansionPanelSection.tsx b/packages/curve-ui-kit/src/shared/ui/DataTable/ExpansionPanelSection.tsx deleted file mode 100644 index ba8d84369f..0000000000 --- a/packages/curve-ui-kit/src/shared/ui/DataTable/ExpansionPanelSection.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ReactNode } from 'react' -import CardHeader from '@mui/material/CardHeader' -import Grid from '@mui/material/Grid2' -import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' - -const { Spacing } = SizesAndSpaces - -/** - * Expansion panel section used to group children in a grid layout, inside an `ExpansionRow`. - */ -export const ExpansionPanelSection = ({ children, title }: { children: ReactNode[]; title: ReactNode }) => ( - - - - - {children.filter(Boolean).map((child, index) => ( - - {child} - - ))} - -) diff --git a/packages/curve-ui-kit/src/shared/ui/DataTable/ExpansionRow.tsx b/packages/curve-ui-kit/src/shared/ui/DataTable/ExpansionRow.tsx index af394db2dd..527becabc4 100644 --- a/packages/curve-ui-kit/src/shared/ui/DataTable/ExpansionRow.tsx +++ b/packages/curve-ui-kit/src/shared/ui/DataTable/ExpansionRow.tsx @@ -6,7 +6,7 @@ import TableCell from '@mui/material/TableCell' import TableRow from '@mui/material/TableRow' import { type Row } from '@tanstack/react-table' import type { TableItem } from '@ui-kit/shared/ui/DataTable/data-table.utils' -import { getInsetShadow } from '@ui-kit/themes/basic-theme/shadows' +import { getInsetShadow, getShadow } from '@ui-kit/themes/basic-theme/shadows' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' const { Spacing } = SizesAndSpaces @@ -26,12 +26,15 @@ export function ExpansionRow({ expandedPanel: ExpandedPanel colSpan: number }) { - const boxShadow = useInsetShadow() const { render, onExited, expanded } = useRowExpansion(row) + const { design } = useTheme() + const boxShadow = useMemo(() => getShadow(design, 3), [design]) + const insetShadow = useMemo(() => getInsetShadow(design, 3), [design]) return ( render && ( - - + // add a scale(1) so the box-shadow is applied correctly on top of the next table row + + (row: Row) { const onExited = useCallback(() => setRender(false), []) return { render, onExited, expanded } } - -/** - * Hook to get the inset shadow for the expansion row. - * - * The shadow is defined in the design, but it doesn't work between HTML rows. - */ -function useInsetShadow() { - const { design } = useTheme() - return useMemo(() => getInsetShadow(design, 3), [design]) -} diff --git a/packages/curve-ui-kit/src/shared/ui/DataTable/FilterRow.tsx b/packages/curve-ui-kit/src/shared/ui/DataTable/FilterRow.tsx index cb8a7ee7ac..1f4e0d52a4 100644 --- a/packages/curve-ui-kit/src/shared/ui/DataTable/FilterRow.tsx +++ b/packages/curve-ui-kit/src/shared/ui/DataTable/FilterRow.tsx @@ -13,7 +13,7 @@ export const FilterRow = ({ children: ReactNode table: TanstackTable }) => ( - + count + headers.length, 0)} sx={(t) => ({ backgroundColor: t.design.Layer[1].Fill, padding: 0, borderBottomWidth: 0 })} diff --git a/packages/curve-ui-kit/src/shared/ui/DataTable/HeaderCell.tsx b/packages/curve-ui-kit/src/shared/ui/DataTable/HeaderCell.tsx index a5b3a4baeb..e6d6d6879c 100644 --- a/packages/curve-ui-kit/src/shared/ui/DataTable/HeaderCell.tsx +++ b/packages/curve-ui-kit/src/shared/ui/DataTable/HeaderCell.tsx @@ -8,46 +8,55 @@ import { getAlignment, getExtraColumnPadding, getFlexAlignment, type TableItem } const { Spacing } = SizesAndSpaces -export const HeaderCell = ({ header }: { header: Header }) => { +export const HeaderCell = ({ + header, + isSticky, +}: { + header: Header + isSticky: boolean +}) => { const { column } = header const isSorted = column.getIsSorted() const canSort = column.getCanSort() - const { hidden, borderRight } = column.columnDef.meta ?? {} + const { borderRight } = column.columnDef.meta ?? {} return ( - !hidden && ( - `1px solid ${t.design.Layer[1].Outline}` }), - }} - colSpan={header.colSpan} - width={header.getSize()} - onClick={column.getToggleSortingHandler()} - data-testid={`data-table-header-${column.id}`} - variant="tableHeaderS" - > - - {flexRender(column.columnDef.header, header.getContext())} - - - - ) + `1px solid ${t.design.Layer[1].Outline}` }), + ...(isSticky && { + position: 'sticky', + left: 0, + zIndex: (t) => t.zIndex.tableHeaderStickyColumn, + backgroundColor: (t) => t.design.Table.Header.Fill, + }), + }} + colSpan={header.colSpan} + onClick={column.getToggleSortingHandler()} + data-testid={`data-table-header-${column.id}`} + variant="tableHeaderS" + > + + {flexRender(column.columnDef.header, header.getContext())} + + + ) } diff --git a/packages/curve-ui-kit/src/shared/ui/DataTable/TableFilters.tsx b/packages/curve-ui-kit/src/shared/ui/DataTable/TableFilters.tsx index 38da715808..17dd38bbf0 100644 --- a/packages/curve-ui-kit/src/shared/ui/DataTable/TableFilters.tsx +++ b/packages/curve-ui-kit/src/shared/ui/DataTable/TableFilters.tsx @@ -1,5 +1,4 @@ import { forwardRef, ReactNode, useCallback, useMemo, useRef, useState } from 'react' -import { useMediaQuery } from '@mui/material' import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Collapse from '@mui/material/Collapse' @@ -10,6 +9,7 @@ import Stack from '@mui/material/Stack' import SvgIcon from '@mui/material/SvgIcon' import Typography from '@mui/material/Typography' import { ColumnFiltersState } from '@tanstack/react-table' +import { useIsMobile, useIsTiny } from '@ui-kit/hooks/useBreakpoints' import { useFilterExpanded } from '@ui-kit/hooks/useLocalStorage' import { useSwitch } from '@ui-kit/hooks/useSwitch' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' @@ -79,9 +79,10 @@ export const TableFilters = ({ const [filterExpanded, setFilterExpanded] = useFilterExpanded(title) const [visibilitySettingsOpen, openVisibilitySettings, closeVisibilitySettings] = useSwitch() const settingsRef = useRef(null) - const isMobile = useMediaQuery((t) => t.breakpoints.down('tablet')) + const isMobile = useIsMobile() + const maxWidth = `calc(100vw${useIsTiny() ? '' : ' - 20px'})` // in tiny screens we remove the table margins completely return ( - + {title} diff --git a/packages/curve-ui-kit/src/shared/ui/DataTable/TableVisibilitySettingsPopover.tsx b/packages/curve-ui-kit/src/shared/ui/DataTable/TableVisibilitySettingsPopover.tsx index 7340c9711e..a4388a588d 100644 --- a/packages/curve-ui-kit/src/shared/ui/DataTable/TableVisibilitySettingsPopover.tsx +++ b/packages/curve-ui-kit/src/shared/ui/DataTable/TableVisibilitySettingsPopover.tsx @@ -4,6 +4,7 @@ import Popover from '@mui/material/Popover' import Stack from '@mui/material/Stack' import Switch from '@mui/material/Switch' import Typography from '@mui/material/Typography' +import { ColumnDef } from '@tanstack/react-table' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' export type VisibilityOption = { @@ -46,35 +47,37 @@ export const TableVisibilitySettingsPopover = ({ }} > - {visibilityGroups.map(({ options, label }) => ( - - `1px solid ${t.design.Layer[1].Outline}` }} - > - {label} - - {options - .filter((option) => option.label) - .map( - ({ columns, active, label, enabled }) => - enabled && ( - toggleVisibility(columns)} - size="small" - /> - } - label={label} - /> - ), - )} - - ))} + {visibilityGroups + .filter(({ options }) => options.some((o) => o.label)) + .map(({ options, label }) => ( + + `1px solid ${t.design.Layer[1].Outline}` }} + > + {label} + + {options + .filter((option) => option.label) + .map( + ({ columns, active, label, enabled }) => + enabled && ( + toggleVisibility(columns)} + size="small" + /> + } + label={label} + /> + ), + )} + + ))} ) @@ -100,7 +103,10 @@ const flatten = (visibilitySettings: VisibilityGroup(groups: VisibilityGroup[]) => { +export const useVisibilitySettings = ( + groups: VisibilityGroup[], + columns: ColumnDef[], +) => { /** current visibility settings in grouped format */ const [visibilitySettings, setVisibilitySettings] = useState(groups) @@ -124,7 +130,14 @@ export const useVisibilitySettings = (groups: Visibili ) /** current column visibility state as used internally by tanstack */ - const columnVisibility = useMemo(() => flatten(visibilitySettings), [visibilitySettings]) + const columnVisibility = useMemo( + () => + ({ + ...flatten(visibilitySettings), + ...Object.fromEntries(columns.filter((c) => c.meta?.hidden).map((c) => [c.id, false])), + }) as Record, + [columns, visibilitySettings], + ) return { columnSettings: visibilitySettings, columnVisibility, toggleVisibility } } diff --git a/packages/curve-ui-kit/src/shared/ui/DataTable/data-table.utils.ts b/packages/curve-ui-kit/src/shared/ui/DataTable/data-table.utils.ts index fb7c7b4448..6f727e7023 100644 --- a/packages/curve-ui-kit/src/shared/ui/DataTable/data-table.utils.ts +++ b/packages/curve-ui-kit/src/shared/ui/DataTable/data-table.utils.ts @@ -26,7 +26,7 @@ const { Spacing } = SizesAndSpaces * In the figma design, the first and last columns seem to be aligned to the table title. * However, the normal padding causes them to be misaligned. */ -export const getExtraColumnPadding = (column: Column) => ({ +export const getExtraColumnPadding = (column: Column) => ({ ...(column.getIsFirstColumn() && { paddingInlineStart: Spacing.md }), ...(column.getIsLastColumn() && { paddingInlineEnd: Spacing.md }), }) diff --git a/packages/curve-ui-kit/src/shared/ui/InvertOnHover.tsx b/packages/curve-ui-kit/src/shared/ui/InvertOnHover.tsx index 02db86ae45..739224845e 100644 --- a/packages/curve-ui-kit/src/shared/ui/InvertOnHover.tsx +++ b/packages/curve-ui-kit/src/shared/ui/InvertOnHover.tsx @@ -1,11 +1,6 @@ -import { cloneElement, type ReactElement } from 'react' +import { type ReactElement } from 'react' import type { Theme } from '@mui/material' -import { useClassObserver } from '@ui-kit/hooks/useClassObserver' -import { useSwitch } from '@ui-kit/hooks/useSwitch' -import { InvertTheme } from '@ui-kit/shared/ui/ThemeProvider' -import { TransitionFunction } from '@ui-kit/themes/design/0_primitives' import type { SxProps } from '@ui-kit/utils' -import { classNames, CypressHoverClass, useNativeEventInCypress } from '@ui-kit/utils/dom' /** * A component that inverts the theme when hovered. @@ -37,36 +32,8 @@ type InvertOnHoverProps = { disabled?: boolean } -const defaultHoverColor = (t: Theme) => t.design.Layer.TypeAction.Hover - -export const InvertOnHover = ({ - children: child, - hoverEl, - hoverColor = defaultHoverColor, - disabled, -}: InvertOnHoverProps) => { - const [isHover, onMouseEnter, onMouseLeave] = useSwitch(false) - const inverted = useClassObserver(hoverEl, 'Mui-focusVisible') || isHover - const childSx = (child.props.sx as Record) ?? {} - - useNativeEventInCypress(hoverEl, 'mouseenter', onMouseEnter) - if (disabled) return child - - return ( - - {cloneElement(child, { - ...child.props, - className: classNames(inverted && CypressHoverClass, child.props?.className), - onMouseEnter, - onMouseLeave, - sx: { - ...childSx, - color: (theme) => theme.palette.text.secondary, // by default components have color: 'inherit' which breaks the inverted theme - transition: [`background-color ${TransitionFunction}`, childSx['transition']].filter(Boolean).join(', '), - '&:hover': { ...childSx['&:hover'], backgroundColor: hoverColor }, - ...(inverted && { backgroundColor: hoverColor }), - }, - })} - - ) -} +/** + * A component that inverts the theme when hovered. Currently, disabled due to performance issues. + * In the future we want to use css variables to achieve the same effect without fully changing the theme. + */ +export const InvertOnHover = ({ children }: InvertOnHoverProps) => children diff --git a/packages/curve-ui-kit/src/shared/ui/Metric.tsx b/packages/curve-ui-kit/src/shared/ui/Metric.tsx index e5222ecfd1..6ca93d87d6 100644 --- a/packages/curve-ui-kit/src/shared/ui/Metric.tsx +++ b/packages/curve-ui-kit/src/shared/ui/Metric.tsx @@ -54,14 +54,21 @@ const percentage: UnitOptions = { abbreviate: false, } -export const UNITS = ['dollar', 'percentage'] as const -type Unit = (typeof UNITS)[number] | UnitOptions +const multiplier: UnitOptions = { + symbol: 'x', + position: 'suffix', + abbreviate: true, +} -const UNIT_MAP: Record<(typeof UNITS)[number], UnitOptions> = { +const UNIT_MAP = { dollar, percentage, + multiplier, } as const +type Unit = keyof typeof UNIT_MAP | UnitOptions +export const UNITS = Object.keys(UNIT_MAP) as unknown as keyof typeof UNIT_MAP + // Default value formatter. const formatValue = (value: number, decimals?: number): string => value.toLocaleString(undefined, { diff --git a/packages/curve-ui-kit/src/themes/basic-theme/basic-theme.d.ts b/packages/curve-ui-kit/src/themes/basic-theme/basic-theme.d.ts index 81cfbfa26b..75096a28a7 100644 --- a/packages/curve-ui-kit/src/themes/basic-theme/basic-theme.d.ts +++ b/packages/curve-ui-kit/src/themes/basic-theme/basic-theme.d.ts @@ -27,4 +27,12 @@ declare module '@mui/material/styles' { tertiary: string highlight: string } + + interface ZIndex { + tableStickyColumn: number + tableHeader: number + tableFilters: number + tableHeaderStickyColumn: number + tableStickyLastRow: number + } } diff --git a/packages/curve-ui-kit/src/themes/basic-theme/basic-theme.ts b/packages/curve-ui-kit/src/themes/basic-theme/basic-theme.ts index d1a39e49b8..90e7e5d46f 100644 --- a/packages/curve-ui-kit/src/themes/basic-theme/basic-theme.ts +++ b/packages/curve-ui-kit/src/themes/basic-theme/basic-theme.ts @@ -18,8 +18,17 @@ export const basicMuiTheme = createMuiTheme({ }, spacing: Object.values(Spacing), direction: 'ltr', + zIndex: { + tableStickyColumn: 100, // the sticky column in the table + tableFilters: 110, // the filters in the table header + tableHeader: 120, // the whole table header including filters + tableHeaderStickyColumn: 130, // the sticky column in the table header + tableStickyLastRow: 140, // the last row in the table is sticky so we don't show the header without any data + }, }) +export type Responsive = Record + /** * Create a responsive object based on the breakpoints defined in the basicMuiTheme. * @@ -50,4 +59,10 @@ export const handleBreakpoints = (values: Record +export const mapBreakpoints = ( + values: Responsive, + callback: (value: string, breakpoint: Breakpoint) => CSSObject, +): CSSObject => + Object.fromEntries( + Object.entries(values).map(([breakpoint, value]) => [breakpoint, callback(value, breakpoint as Breakpoint)]), + ) diff --git a/packages/curve-ui-kit/src/themes/basic-theme/shadows.ts b/packages/curve-ui-kit/src/themes/basic-theme/shadows.ts index 92819addd8..ec8fd59224 100644 --- a/packages/curve-ui-kit/src/themes/basic-theme/shadows.ts +++ b/packages/curve-ui-kit/src/themes/basic-theme/shadows.ts @@ -1,6 +1,6 @@ import type { DesignSystem } from '@ui-kit/themes/design' -type ShadowElevation = -2 | -1 | 1 | 2 | 3 +export type ShadowElevation = -2 | -1 | 1 | 2 | 3 export const getShadow = (design: DesignSystem, elevation: ShadowElevation) => ({ diff --git a/packages/curve-ui-kit/src/themes/components/chip/mui-chip.ts b/packages/curve-ui-kit/src/themes/components/chip/mui-chip.ts index 107f9e2a82..63540ed488 100644 --- a/packages/curve-ui-kit/src/themes/components/chip/mui-chip.ts +++ b/packages/curve-ui-kit/src/themes/components/chip/mui-chip.ts @@ -16,9 +16,6 @@ const createColor = (color: keyof DesignSystem['Badges']['Fill'], Badges: Design props: { color: color.toLowerCase() as ChipProps['color'] }, }) -// note: the design system is using inverted themes for this color, there is no semantic colors for the clickable chips. -const invertPrimary = (color: DesignSystem['Color']) => color.Neutral[50] - const { Sizing, Spacing, IconSize } = SizesAndSpaces type ChipSizeDefinition = { @@ -30,7 +27,7 @@ type ChipSizeDefinition = { type ChipSizes = NonNullable const chipSizes: Record = { - extraSmall: { font: 'bodyXsBold', height: IconSize.md, iconSize: IconSize.sm }, + extraSmall: { font: 'bodyXsBold', height: IconSize.sm, iconSize: IconSize.sm }, small: { font: 'buttonXs', height: IconSize.md, iconSize: IconSize.sm }, medium: { font: 'buttonXs', height: Sizing.md, iconSize: IconSize.md }, large: { font: 'buttonM', height: Sizing.md, iconSize: IconSize.lg }, @@ -54,7 +51,7 @@ const chipSizeClickable: Record & { delet * - We do not use the "variant" prop (at the time of writing). */ export const defineMuiChip = ( - { Chips, Color, Text: { TextColors }, Layer, Badges }: DesignSystem, + { Chips, Text: { TextColors }, Badges }: DesignSystem, typography: TypographyOptions, ): Components['MuiChip'] => ({ styleOverrides: { diff --git a/packages/curve-ui-kit/src/themes/design/1_sizes_spaces.ts b/packages/curve-ui-kit/src/themes/design/1_sizes_spaces.ts index 3c96c84dbc..5f20ded91c 100644 --- a/packages/curve-ui-kit/src/themes/design/1_sizes_spaces.ts +++ b/packages/curve-ui-kit/src/themes/design/1_sizes_spaces.ts @@ -267,7 +267,6 @@ export const SizesAndSpaces = { column: MappedColumnWidth, }, MinWidth: { - table: { desktop: '60rem' }, // 960px tableHeader: '50rem', // 800px select: '5rem', // 80px actionCard: '20rem', // 320px diff --git a/packages/curve-ui-kit/src/widgets/Footer/Footer.tsx b/packages/curve-ui-kit/src/widgets/Footer/Footer.tsx index bdddfc2c88..45dfae14b0 100644 --- a/packages/curve-ui-kit/src/widgets/Footer/Footer.tsx +++ b/packages/curve-ui-kit/src/widgets/Footer/Footer.tsx @@ -2,6 +2,7 @@ import Box from '@mui/material/Box' import Grid from '@mui/material/Grid2' import { styled } from '@mui/material/styles' import { LlamaImg } from '@ui/images' +import { useIsTiny } from '@ui-kit/hooks/useBreakpoints' import { useBetaFlag } from '@ui-kit/hooks/useLocalStorage' import { useSwitch } from '@ui-kit/hooks/useSwitch' import { AppName } from '@ui-kit/shared/routes' @@ -28,6 +29,7 @@ export const Footer = ({ appName, networkId, headerHeight }: FooterProps) => { const [isBetaModalOpen, openBetaModal, closeBetaModal] = useSwitch() const [isBetaSnackbarVisible, openBetaSnackbar, closeBetaSnackbar] = useSwitch() const [isBeta, setIsBeta] = useBetaFlag() + const isTiny = useIsTiny() return ( { desktop: 3, }} > -
+
))} diff --git a/packages/curve-ui-kit/src/widgets/Footer/Section.tsx b/packages/curve-ui-kit/src/widgets/Footer/Section.tsx index cade19f642..50bb745e1a 100644 --- a/packages/curve-ui-kit/src/widgets/Footer/Section.tsx +++ b/packages/curve-ui-kit/src/widgets/Footer/Section.tsx @@ -9,9 +9,10 @@ export type SectionProps = { links: Omit[] networkId: string appName: AppName + isTiny: boolean } -export const Section = ({ title, links, networkId, appName }: SectionProps) => ( +export const Section = ({ title, links, networkId, appName, isTiny }: SectionProps) => ( ( {links.map((link) => ( - + ))} diff --git a/packages/curve-ui-kit/src/widgets/Header/Header.tsx b/packages/curve-ui-kit/src/widgets/Header/Header.tsx index eb4b5a1b85..6a92491ff8 100644 --- a/packages/curve-ui-kit/src/widgets/Header/Header.tsx +++ b/packages/curve-ui-kit/src/widgets/Header/Header.tsx @@ -1,20 +1,17 @@ import { usePathname } from 'next/navigation' import { useMemo } from 'react' -import type { Theme } from '@mui/material' import { useTheme } from '@mui/material/styles' -import useMediaQuery from '@mui/material/useMediaQuery' import { WalletToast } from '@ui-kit/features/connect-wallet' import { WagmiConnectModal } from '@ui-kit/features/connect-wallet/ui/WagmiConnectModal' +import { useIsDesktop } from '@ui-kit/hooks/useBreakpoints' import { useBetaFlag } from '@ui-kit/hooks/useLocalStorage' import { routeToPage } from '@ui-kit/shared/routes' import { DESKTOP_HEADER_HEIGHT, DesktopHeader } from './DesktopHeader' import { calcMobileHeaderHeight, MobileHeader } from './MobileHeader' import { HeaderProps } from './types' -const isDesktopQuery = (theme: Theme) => theme.breakpoints.up('desktop') - export const Header = ({ routes, ...props }: HeaderProps) => { - const isDesktop = useMediaQuery(isDesktopQuery, { noSsr: true }) + const isDesktop = useIsDesktop() const [isBeta] = useBetaFlag() const pathname = usePathname() const { networkId, height } = props @@ -38,7 +35,7 @@ export const Header = ({ routes, ...props }: HeaderProp * Helper function to calculate the header height based on the banner height and the current screen size */ export const useHeaderHeight = (bannerHeight: number | undefined) => { - const isDesktop = useMediaQuery(isDesktopQuery, { noSsr: true }) + const isDesktop = useIsDesktop() const theme = useTheme() const headerHeight = isDesktop ? DESKTOP_HEADER_HEIGHT : calcMobileHeaderHeight(theme) return `calc(${headerHeight} + ${bannerHeight ?? 0}px)` diff --git a/packages/ui/src/Dialog/OpenDialogButton.tsx b/packages/ui/src/Dialog/OpenDialogButton.tsx index 6c632c2351..90f40e6781 100644 --- a/packages/ui/src/Dialog/OpenDialogButton.tsx +++ b/packages/ui/src/Dialog/OpenDialogButton.tsx @@ -1,3 +1,4 @@ +import { useIsMobile } from 'curve-ui-kit/src/hooks/useBreakpoints' import { ReactNode, useRef } from 'react' import { useButton } from 'react-aria' import styled from 'styled-components' @@ -5,7 +6,6 @@ import Button from 'ui/src/Button' import type { ButtonProps } from 'ui/src/Button/types' import Icon from 'ui/src/Icon/Icon' import { delayAction } from 'ui/src/utils/helpers' -import useMediaQuery from '@mui/material/useMediaQuery' import type { OverlayTriggerState } from '@react-stately/overlays' interface OpenDialogButtonProps extends ButtonProps { @@ -23,7 +23,7 @@ const OpenDialogButton = ({ ...props }: OpenDialogButtonProps) => { const openButtonRef = useRef(null) - const isMobile = useMediaQuery((t) => t.breakpoints.down('tablet')) + const isMobile = useIsMobile() const { buttonProps } = useButton( { onPress: () => (isMobile ? delayAction(overlayTriggerState.open) : overlayTriggerState.open()) }, openButtonRef, diff --git a/packages/ui/src/TableButtonFiltersMobile/TableButtonFiltersMobile.tsx b/packages/ui/src/TableButtonFiltersMobile/TableButtonFiltersMobile.tsx index e1207938dd..89ae5a3d2d 100644 --- a/packages/ui/src/TableButtonFiltersMobile/TableButtonFiltersMobile.tsx +++ b/packages/ui/src/TableButtonFiltersMobile/TableButtonFiltersMobile.tsx @@ -1,8 +1,8 @@ +import { useIsMobile } from 'curve-ui-kit/src/hooks/useBreakpoints' import { useMemo } from 'react' import ModalDialog, { OpenDialogButton } from 'ui/src/Dialog' import { RadioGroup } from 'ui/src/Radio' import { delayAction } from 'ui/src/utils/helpers' -import useMediaQuery from '@mui/material/useMediaQuery' import { useOverlayTriggerState } from '@react-stately/overlays' import TableButtonFiltersMobileItem from './components/TableButtonFiltersMobileItem' import TableButtonFiltersMobileItemIcon from './components/TableButtonFiltersMobileItemIcon' @@ -21,7 +21,7 @@ const TableButtonFiltersMobile = ({ updateRouteFilterKey(filterKey: string): void }) => { const overlayTriggerState = useOverlayTriggerState({}) - const isMobile = useMediaQuery((t) => t.breakpoints.down('tablet')) + const isMobile = useIsMobile() const handleClose = () => { if (isMobile) { diff --git a/packages/ui/src/Tooltip/TooltipButton.tsx b/packages/ui/src/Tooltip/TooltipButton.tsx index a29759aaab..817ddbaf29 100644 --- a/packages/ui/src/Tooltip/TooltipButton.tsx +++ b/packages/ui/src/Tooltip/TooltipButton.tsx @@ -1,13 +1,13 @@ +import { useIsMobile } from 'curve-ui-kit/src/hooks/useBreakpoints' import { MouseEvent, ReactNode, useCallback, useRef, useState } from 'react' import { useTooltipTrigger } from 'react-aria' -import { useTooltipTriggerState } from 'react-stately' import type { TooltipTriggerProps } from 'react-stately' +import { useTooltipTriggerState } from 'react-stately' import styled from 'styled-components' import Icon from 'ui/src/Icon' import Tooltip from 'ui/src/Tooltip/Tooltip' import type { TooltipProps } from 'ui/src/Tooltip/types' import { breakpoints } from 'ui/src/utils' -import useMediaQuery from '@mui/material/useMediaQuery' export type IconStyles = { $svgTop?: string } @@ -34,7 +34,7 @@ function TooltipButton({ iconStyles?: IconStyles }) { const state = useTooltipTriggerState({ delay: 0, ...props }) - const isMobile = useMediaQuery((t) => t.breakpoints.down('tablet')) + const isMobile = useIsMobile() const ref = useRef(null) const { triggerProps, tooltipProps } = useTooltipTrigger(props, state, ref) diff --git a/tests/cypress/e2e/loan/llamalend-markets.cy.ts b/tests/cypress/e2e/loan/llamalend-markets.cy.ts index 99815454a0..029ed474bb 100644 --- a/tests/cypress/e2e/loan/llamalend-markets.cy.ts +++ b/tests/cypress/e2e/loan/llamalend-markets.cy.ts @@ -14,18 +14,15 @@ import { assertInViewport, assertNotInViewport, type Breakpoint, - checkIsDarkMode, hideDomainBanner, LOAD_TIMEOUT, oneDesktopViewport, - oneMobileViewport, oneViewport, RETRY_IN_CI, } from '@/support/ui' import { SMALL_POOL_TVL } from '@ui-kit/features/user-profile/store' describe(`LlamaLend Markets`, () => { - let isDarkMode: boolean let breakpoint: Breakpoint let width: number, height: number let vaultData: LendingVaultResponses @@ -46,7 +43,6 @@ describe(`LlamaLend Markets`, () => { cy.visit('/crvusd/ethereum/beta-markets/', { onBeforeLoad: (window) => { window.localStorage.clear() - isDarkMode = checkIsDarkMode(window) hideDomainBanner(window) }, ...LOAD_TIMEOUT, @@ -55,11 +51,7 @@ describe(`LlamaLend Markets`, () => { }) const firstRow = () => cy.get(`[data-testid^="data-table-row-"]`).eq(0) - const copyFirstAddress = () => cy.get(`[data-testid^="copy-market-address"]:visible`).first() - it('should have sticky headers', () => { - cy.viewport(...oneMobileViewport()) - const breakpoint = 'mobile' cy.get('[data-testid^="data-table-row"]').last().then(assertNotInViewport) cy.get('[data-testid^="data-table-row"]').eq(10).scrollIntoView() cy.get('[data-testid="data-table-head"] th').eq(1).then(assertInViewport) @@ -71,14 +63,11 @@ describe(`LlamaLend Markets`, () => { mobile: [194, 180, 156, 144], // on tablet, we expect always 3 rows to fit all market filter chips tablet: [174], - // on desktop, we expect 2 rows as the chips should always fit. The original design was 128 px. - desktop: [174, 128], + // on desktop, we have an extra row until 1450px + desktop: [182, 128], }[breakpoint] cy.get('[data-testid="table-filters"]').invoke('outerHeight').should('be.oneOf', filterHeight) - - // mobile row is usually 77px but can be higher when the text is long - const rowHeight = { mobile: [77, 88], tablet: [88], desktop: [88] }[breakpoint] - cy.get('[data-testid^="data-table-row"]').eq(10).invoke('outerHeight').should('be.oneOf', rowHeight) + cy.get('[data-testid^="data-table-row"]').eq(10).invoke('outerHeight').should('equal', 64) }) it('should sort', () => { @@ -109,14 +98,7 @@ describe(`LlamaLend Markets`, () => { cy.get(`[data-testid="pool-type-mint"]`).should('not.exist') }) expandFirstRowOnMobile() - - const [green, red] = [isDarkMode ? '#32ce79' : '#167d4a', '#ed242f'] - checkLineGraphColor('borrow', red) - - if (breakpoint != 'mobile') { - showHiddenColumn({ element: 'line-graph-lend', toggle: 'lendChart' }) - } - checkLineGraphColor('lend', green) + checkLineGraphColor('borrow', '#ed242f') // check that scrolling loads more snapshots: cy.get(`@lend-snapshots.all`, LOAD_TIMEOUT).then((calls1) => { @@ -124,6 +106,7 @@ describe(`LlamaLend Markets`, () => { .last() .scrollIntoView({ offset: { top: -height / 2, left: 0 } }) // scroll to the last row, make sure it's still visible if (breakpoint == 'mobile') { + cy.get(`[data-testid="expand-icon"]`).last().scrollIntoView() cy.get(`[data-testid="expand-icon"]`).last().click() } cy.wait('@lend-snapshots') @@ -200,10 +183,11 @@ describe(`LlamaLend Markets`, () => { it('should allow filtering favorites', { scrollBehavior: false }, () => { expandFirstRowOnMobile() if (breakpoint == 'desktop') { - // on desktop, favorite icon is only visible on hover - firstRow().trigger('mouseenter', { waitForAnimations: true, scrollBehavior: false, force: true }) + // on desktop, the favorite icon is not visible until hovered - but cypress doesn't support that so use force + cy.get(`[data-testid="favorite-icon"]`).first().click({ force: true }) + } else { + cy.get(`[data-testid="favorite-icon"]:visible`).first().click() } - cy.get(`[data-testid="favorite-icon"]:visible`).first().click() withFilterChips(() => cy.get(`[data-testid="chip-favorites"]`).click()) cy.get(`[data-testid^="data-table-row"]`).should('have.length', 1) cy.get(`[data-testid="favorite-icon"]:visible`).should('not.exist') @@ -225,21 +209,17 @@ describe(`LlamaLend Markets`, () => { }), )) - it(`should hover and copy the market address`, RETRY_IN_CI, () => { + it(`should copy the market address`, RETRY_IN_CI, () => { if (breakpoint === 'mobile') { expandFirstRowOnMobile() - } else { - const hoverBackground = isDarkMode ? 'rgb(254, 250, 239)' : 'rgb(37, 36, 32)' - cy.get(`[data-testid^="copy-market-address"]`).should('have.css', 'opacity', breakpoint === 'desktop' ? '0' : '1') - firstRow().should('not.have.css', 'background-color', hoverBackground) - cy.scrollTo(0, 0) - firstRow().trigger('mouseenter', { waitForAnimations: true, scrollBehavior: false, force: true }) - firstRow().should('have.css', 'background-color', hoverBackground) - copyFirstAddress().should('have.css', 'opacity', '1') } - // todo: this test fails sometimes in ci because the click doesn't work - copyFirstAddress().click() - copyFirstAddress().click() // click again, in chrome in CI the first click doesn't work (because of tooltip?) + // unfortunately we need to click twice on Chromium, the first one doesn't work (maybe due to the tooltip) + range(2).forEach(() => + breakpoint === 'desktop' + ? // on desktop, the copy button is not visible until hovered - but cypress doesn't support that so use force + cy.get(`[data-testid^="copy-market-address"]`).first().click({ force: true }) + : cy.get(`[data-testid^="copy-market-address"]:visible`).first().click(), + ) cy.get(`[data-testid="copy-confirmation"]`).should('be.visible') }) @@ -264,7 +244,9 @@ describe(`LlamaLend Markets`, () => { withFilterChips(() => { cy.get(`[data-testid="chip-rewards"]`).click() cy.get(`[data-testid^="data-table-row"]`).should('have.length', 1) - cy.get(`[data-testid="rewards-badge"]`).should('be.visible') + }) + cy.get(`[data-testid="rewards-badge"]`).should('be.visible') + withFilterChips(() => { cy.get(`[data-testid="chip-rewards"]`).click() cy.get(`[data-testid^="data-table-row"]`).should('have.length.above', 1) }) @@ -337,12 +319,5 @@ const selectCoin = (symbol: string, type: TokenType) => { cy.get(`[data-testid="multi-select-filter-${columnId}"]`).click() // open the menu again cy.get(`[data-testid="menu-${columnId}"] [value="${symbol}"]`).click() // select the token cy.get('body').click(0, 0) // close popover - cy.get(`[data-testid="data-table-cell-assets"] [data-testid^="token-icon-${symbol}"]`).should('be.visible') -} - -function showHiddenColumn({ element, toggle }: { element: string; toggle: string }) { - cy.get(`[data-testid="${element}"]`).should('not.exist') - cy.get(`[data-testid="btn-visibility-settings"]`).click() - cy.get(`[data-testid="visibility-toggle-${toggle}"]`).click() - cy.get(`[data-testid="${element}"]`).should('be.visible') + cy.get(`[data-testid="data-table-cell-assets"] [data-testid^="token-icon-${symbol}"]`).should('exist') // token might be hidden behind other tokens } diff --git a/tests/cypress/support/helpers/lending-mocks.ts b/tests/cypress/support/helpers/lending-mocks.ts index 432922086c..8bd59f346d 100644 --- a/tests/cypress/support/helpers/lending-mocks.ts +++ b/tests/cypress/support/helpers/lending-mocks.ts @@ -105,6 +105,6 @@ export const mockLendingVaults = (responses: LendingVaultResponses) => }) export const mockLendingSnapshots = () => - cy.intercept('https://prices.curve.finance/v1/lending/markets/*/*/snapshots?agg=none', { + cy.intercept('https://prices.curve.finance/v1/lending/markets/*/*/snapshots?agg=none&fetch_on_chain=false', { fixture: 'lending-snapshots.json', })