diff --git a/public/assets/icons/add.svg b/public/assets/icons/add.svg new file mode 100644 index 00000000000..6a3c331c298 --- /dev/null +++ b/public/assets/icons/add.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/CCIP/AddButton/AddButton.astro b/src/components/CCIP/AddButton/AddButton.astro new file mode 100644 index 00000000000..f2ade415216 --- /dev/null +++ b/src/components/CCIP/AddButton/AddButton.astro @@ -0,0 +1,42 @@ +--- +export interface Props { + href: string + text: string +} + +const { href, text } = Astro.props +--- + + + Add + {text} + + + diff --git a/src/components/CCIP/Cards/Card.css b/src/components/CCIP/Cards/Card.css new file mode 100644 index 00000000000..bb77dae5555 --- /dev/null +++ b/src/components/CCIP/Cards/Card.css @@ -0,0 +1,40 @@ +.card__container { + display: flex; + padding: var(--space-6x); + gap: var(--space-3x); + width: 100%; + background: var(--white); + border: 1px solid var(--gray-200); + border-radius: var(--space-1x); + /* Optimize rendering performance */ + contain: layout style paint; + will-change: background-color; +} + +.card__container:hover { + background-color: var(--gray-50); +} + +.card__container img, +.card__container object, +.card__container object img { + width: var(--space-10x); + height: var(--space-10x); + margin-top: auto; + margin-bottom: auto; +} + +.card__container h3 { + font-size: var(--space-4x); + font-weight: var(--font-weight-medium); + line-height: var(--space-6x); + color: var(--gray-950); + margin-bottom: var(--space-1x); +} + +.card__container p { + margin-bottom: 0; + font-size: var(--space-3x); + line-height: var(--space-5x); + color: var(--gray-500); +} diff --git a/src/components/CCIP/Cards/Card.tsx b/src/components/CCIP/Cards/Card.tsx new file mode 100644 index 00000000000..669bd9155ff --- /dev/null +++ b/src/components/CCIP/Cards/Card.tsx @@ -0,0 +1,43 @@ +import { memo, type ReactNode } from "react" +import "./Card.css" + +interface CardProps { + logo: ReactNode + title: string + subtitle?: string + link?: string + onClick?: () => void + ariaLabel?: string +} + +const Card = memo(function Card({ logo, title, subtitle, link, onClick, ariaLabel }: CardProps) { + const content = ( + <> + {logo} +
+

{title}

+ {subtitle &&

{subtitle}

} +
+ + ) + + if (link) { + return ( + +
{content}
+
+ ) + } + + if (onClick) { + return ( + + ) + } + + return
{content}
+}) + +export default Card diff --git a/src/components/CCIP/Cards/NetworkCard.tsx b/src/components/CCIP/Cards/NetworkCard.tsx index 839f4389d4d..5d5ee0f4e99 100644 --- a/src/components/CCIP/Cards/NetworkCard.tsx +++ b/src/components/CCIP/Cards/NetworkCard.tsx @@ -1,5 +1,5 @@ import { memo } from "react" -import "./NetworkCard.css" +import Card from "./Card.tsx" interface NetworkCardProps { name: string @@ -9,17 +9,9 @@ interface NetworkCardProps { } const NetworkCard = memo(function NetworkCard({ name, totalLanes, totalTokens, logo }: NetworkCardProps) { - return ( -
- -
-

{name}

-

- {totalLanes} {totalLanes > 1 ? "lanes" : "lane"} | {totalTokens} {totalTokens > 1 ? "tokens" : "token"} -

-
-
- ) + const subtitle = `${totalLanes} ${totalLanes === 1 ? "lane" : "lanes"} | ${totalTokens} ${totalTokens === 1 ? "token" : "tokens"}` + + return } title={name} subtitle={subtitle} /> }) export default NetworkCard diff --git a/src/components/CCIP/Cards/TokenCard.css b/src/components/CCIP/Cards/TokenCard.css index c2d092aa132..d8e2b7d6e2d 100644 --- a/src/components/CCIP/Cards/TokenCard.css +++ b/src/components/CCIP/Cards/TokenCard.css @@ -1,19 +1,11 @@ .token-card__container { display: flex; - width: 100%; - height: 110px; - min-width: 110px; - margin: 0 auto; - flex-direction: column; - align-items: center; - text-align: center; - padding: var(--space-4x); + padding: var(--space-6x); gap: var(--space-3x); - background: #ffffff; + width: 100%; + background: var(--white); border: 1px solid var(--gray-200); border-radius: var(--space-1x); - justify-content: center; - cursor: pointer; /* Optimize rendering performance */ contain: layout style paint; will-change: background-color; @@ -27,14 +19,24 @@ .token-card__container object img { width: var(--space-10x); height: var(--space-10x); + margin-top: auto; + margin-bottom: auto; border-radius: 50%; } .token-card__container h3 { font-size: var(--space-4x); - font-weight: 500; + font-weight: var(--font-weight-medium); + line-height: var(--space-6x); color: var(--gray-950); + margin-bottom: var(--space-1x); +} + +.token-card__container p { margin-bottom: 0; + font-size: var(--space-3x); + line-height: var(--space-5x); + color: var(--gray-500); } .truncate { diff --git a/src/components/CCIP/Cards/TokenCard.tsx b/src/components/CCIP/Cards/TokenCard.tsx index 653908ec260..7810da7f4f7 100644 --- a/src/components/CCIP/Cards/TokenCard.tsx +++ b/src/components/CCIP/Cards/TokenCard.tsx @@ -1,5 +1,6 @@ import { memo } from "react" import { fallbackTokenIconUrl } from "~/features/utils/index.ts" +import Card from "./Card.tsx" import "./TokenCard.css" interface TokenCardProps { @@ -7,42 +8,28 @@ interface TokenCardProps { logo?: string link?: string onClick?: () => void + totalNetworks?: number } -const TokenCard = memo(function TokenCard({ id, logo, link, onClick }: TokenCardProps) { - if (link) { - return ( - -
- {/* We cannot use the normal Image/onError syntax as a fallback as the element is server rendered - and the onerror does not seem to work correctly. Using Picture will also not work. */} - - {`${id} - -

{id}

-
-
- ) - } +const TokenCard = memo(function TokenCard({ id, logo, link, onClick, totalNetworks }: TokenCardProps) { + const logoElement = ( + + {`${id} + + ) - if (onClick) { - return ( - - ) - } + const subtitle = + totalNetworks !== undefined ? `${totalNetworks} ${totalNetworks === 1 ? "network" : "networks"}` : undefined return ( -
- - - -

{id}

-
+ ) }) diff --git a/src/components/CCIP/Chain/Chain.astro b/src/components/CCIP/Chain/Chain.astro index fd726a209a2..6a424001c00 100644 --- a/src/components/CCIP/Chain/Chain.astro +++ b/src/components/CCIP/Chain/Chain.astro @@ -17,6 +17,7 @@ import ChainTokenGrid from "./ChainTokenGrid" import { generateChainStructuredData } from "~/utils/ccipStructuredData" import StructuredData from "~/components/StructuredData.astro" import { DOCS_BASE_URL } from "~/utils/structuredData" +import AddButton from "~/components/CCIP/AddButton/AddButton.astro" interface Props { environment: Environment @@ -128,14 +129,7 @@ const chainStructuredData = generateChainStructuredData(

Tokens ({allTokens.length})

{ network.chainType !== "solana" && network.chainType !== "aptos" && ( - - Add - Add my token - + ) } diff --git a/src/components/CCIP/Landing/Grid.css b/src/components/CCIP/Landing/Grid.css new file mode 100644 index 00000000000..14c77deb4b0 --- /dev/null +++ b/src/components/CCIP/Landing/Grid.css @@ -0,0 +1,12 @@ +.grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-2x); +} + +@media (min-width: 992px) { + .grid { + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: var(--space-4x); + } +} diff --git a/src/components/CCIP/Landing/Grid.tsx b/src/components/CCIP/Landing/Grid.tsx new file mode 100644 index 00000000000..cdc929b6b64 --- /dev/null +++ b/src/components/CCIP/Landing/Grid.tsx @@ -0,0 +1,27 @@ +import { useState, type ReactNode } from "react" +import SeeMore from "../SeeMore/SeeMore.tsx" +import "./Grid.css" + +interface GridProps { + items: any[] + renderItem: (item: any, index: number) => ReactNode + initialDisplayCount: number + seeMoreLabel: string + className?: string + seeMoreLink?: string +} + +function Grid({ items, renderItem, initialDisplayCount, seeMoreLabel, className = "grid", seeMoreLink }: GridProps) { + const [seeMore, setSeeMore] = useState(items.length <= initialDisplayCount) + + return ( + <> +
+ {items.slice(0, seeMore ? items.length : initialDisplayCount).map((item, index) => renderItem(item, index))} +
+ {!seeMore && setSeeMore(!seeMore)} label={seeMoreLabel} href={seeMoreLink} />} + + ) +} + +export default Grid diff --git a/src/components/CCIP/Landing/NetworkGrid.css b/src/components/CCIP/Landing/NetworkGrid.css index acf154ba8dc..191be522985 100644 --- a/src/components/CCIP/Landing/NetworkGrid.css +++ b/src/components/CCIP/Landing/NetworkGrid.css @@ -6,7 +6,7 @@ @media (min-width: 992px) { .networks__grid { - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr 1fr; gap: var(--space-6x); } } diff --git a/src/components/CCIP/Landing/NetworkGrid.tsx b/src/components/CCIP/Landing/NetworkGrid.tsx index fc4ff7b26d9..13728a0260c 100644 --- a/src/components/CCIP/Landing/NetworkGrid.tsx +++ b/src/components/CCIP/Landing/NetworkGrid.tsx @@ -1,7 +1,5 @@ -import { useState } from "react" -import NetworkCard from "../Cards/NetworkCard.tsx" -import SeeMore from "../SeeMore/SeeMore.tsx" -import "./NetworkGrid.css" +import Card from "../Cards/Card.tsx" +import Grid from "./Grid.tsx" interface NetworkGridProps { networks: { @@ -14,27 +12,27 @@ interface NetworkGridProps { environment: string } -const BEFORE_SEE_MORE = 2 * 7 // Number of networks to show before the "See more" button, 2 rows x 7 items +const BEFORE_SEE_MORE = 2 * 4 // Number of networks to show before the "See more" button, 2 rows x 4 items function NetworkGrid({ networks, environment }: NetworkGridProps) { - const [seeMore, setSeeMore] = useState(networks.length <= BEFORE_SEE_MORE) return ( - <> -
- {networks.slice(0, seeMore ? networks.length : BEFORE_SEE_MORE).map((chain) => ( - - - - ))} -
- {!seeMore && setSeeMore(!seeMore)} />} - + { + const subtitle = `${chain.totalLanes} ${chain.totalLanes === 1 ? "lane" : "lanes"} | ${chain.totalTokens} ${chain.totalTokens === 1 ? "token" : "tokens"}` + return ( + } + title={chain.name} + subtitle={subtitle} + link={`/ccip/directory/${environment}/chain/${chain.chain}`} + /> + ) + }} + /> ) } diff --git a/src/components/CCIP/Landing/ccip-landing.astro b/src/components/CCIP/Landing/ccip-landing.astro index e1135ffcb4b..4e101df4cbe 100644 --- a/src/components/CCIP/Landing/ccip-landing.astro +++ b/src/components/CCIP/Landing/ccip-landing.astro @@ -6,6 +6,7 @@ import { Environment, getAllNetworks, getAllSupportedTokens, + getAllUniqueVerifiers, getChainsOfToken, getSearchLanes, Version, @@ -13,9 +14,11 @@ import { import { getTokenIconUrl } from "~/features/utils" import LazyNetworkGrid from "./LazyNetworkGrid" import LazyTokenGrid from "../TokenGrid/LazyTokenGrid" +import LazyVerifierGrid from "../VerifierGrid/LazyVerifierGrid" import StructuredData from "~/components/StructuredData.astro" import { generateDirectoryStructuredData } from "~/utils/ccipStructuredData" import { DOCS_BASE_URL } from "~/utils/structuredData" +import AddButton from "~/components/CCIP/AddButton/AddButton.astro" export type Props = { environment: Environment @@ -43,6 +46,10 @@ const allTokens = tokens.map((token) => { totalNetworks: getChainsOfToken({ token, filter: environment }).length, } }) +const allVerifiers = getAllUniqueVerifiers({ + environment, + version: Version.V1_2_0, +}) const searchLanes = getSearchLanes({ environment }) // Generate directory-level structured data (DataCatalog/Dataset) @@ -76,10 +83,16 @@ const directoryStructuredData = generateDirectoryStructuredData(environment, net

Tokens ({allTokens.length})

- Add my token +
+
+
+

Verifiers ({allVerifiers.length})

+
+ +
@@ -125,11 +138,9 @@ const directoryStructuredData = generateDirectoryStructuredData(environment, net @media (min-width: 992px) { .layout { --doc-padding: var(--space-10x); - display: grid; padding-top: var(--doc-padding); padding-bottom: var(--doc-padding); - grid-template-columns: 1fr 1fr; - gap: var(--space-24x); + gap: var(--space-10x); } } diff --git a/src/components/CCIP/SeeMore/SeeMore.css b/src/components/CCIP/SeeMore/SeeMore.css index 8c4c9209c51..102d96cf4b1 100644 --- a/src/components/CCIP/SeeMore/SeeMore.css +++ b/src/components/CCIP/SeeMore/SeeMore.css @@ -7,6 +7,6 @@ } .seeMore__container { display: flex; - justify-content: center; + justify-content: flex-start; align-items: center; } diff --git a/src/components/CCIP/SeeMore/SeeMore.tsx b/src/components/CCIP/SeeMore/SeeMore.tsx index 3e0b7c17278..cab963897c7 100644 --- a/src/components/CCIP/SeeMore/SeeMore.tsx +++ b/src/components/CCIP/SeeMore/SeeMore.tsx @@ -1,14 +1,22 @@ import "./SeeMore.css" interface SeeMoreProps { onClick?: () => void + label?: string + href?: string } -function SeeMore({ onClick }: SeeMoreProps) { +function SeeMore({ onClick, label = "See more", href }: SeeMoreProps) { return (
- + {href ? ( + + {label} + + ) : ( + + )}
) } diff --git a/src/components/CCIP/TokenGrid/TokenGrid.tsx b/src/components/CCIP/TokenGrid/TokenGrid.tsx index 9c303e264a5..7938ef0406a 100644 --- a/src/components/CCIP/TokenGrid/TokenGrid.tsx +++ b/src/components/CCIP/TokenGrid/TokenGrid.tsx @@ -1,35 +1,47 @@ -import { useState } from "react" -import SeeMore from "../SeeMore/SeeMore.tsx" -import "./TokenGrid.css" -import TokenCard from "../Cards/TokenCard.tsx" +import { fallbackTokenIconUrl } from "~/features/utils/index.ts" +import Card from "../Cards/Card.tsx" +import Grid from "../Landing/Grid.tsx" interface TokenGridProps { tokens: { id: string logo: string + totalNetworks?: number }[] environment: string } -const BEFORE_SEE_MORE = 6 * 4 // Number of networks to show before the "See more" button, 6 rows x 4 items +const BEFORE_SEE_MORE = 2 * 4 // Number of tokens to show before the "See more" button, 2 rows x 4 items -function NetworkGrid({ tokens, environment }: TokenGridProps) { - const [seeMore, setSeeMore] = useState(tokens.length <= BEFORE_SEE_MORE) +function TokenGrid({ tokens, environment }: TokenGridProps) { return ( - <> -
- {tokens.slice(0, seeMore ? tokens.length : BEFORE_SEE_MORE).map((token) => ( - { + const subtitle = + token.totalNetworks !== undefined + ? `${token.totalNetworks} ${token.totalNetworks === 1 ? "network" : "networks"}` + : undefined + const logoElement = ( + + {`${token.id} + + ) + return ( + - ))} -
- {!seeMore && setSeeMore(!seeMore)} />} - + ) + }} + /> ) } -export default NetworkGrid +export default TokenGrid diff --git a/src/components/CCIP/VerifierGrid/LazyVerifierGrid.tsx b/src/components/CCIP/VerifierGrid/LazyVerifierGrid.tsx new file mode 100644 index 00000000000..7dd31d36e19 --- /dev/null +++ b/src/components/CCIP/VerifierGrid/LazyVerifierGrid.tsx @@ -0,0 +1,46 @@ +import { lazy, Suspense } from "react" +import type { Environment } from "~/config/data/ccip/types.ts" + +const VerifierGrid = lazy(() => import("./VerifierGrid.tsx")) + +interface LazyVerifierGridProps { + verifiers: Array<{ + id: string + name: string + logo: string + totalNetworks: number + }> + environment: Environment +} + +export default function LazyVerifierGrid({ verifiers, environment }: LazyVerifierGridProps) { + return ( + + {Array.from({ length: 8 }, (_, i) => ( +
+ ))} +
+ } + > + +
+ ) +} diff --git a/src/components/CCIP/VerifierGrid/VerifierGrid.tsx b/src/components/CCIP/VerifierGrid/VerifierGrid.tsx new file mode 100644 index 00000000000..5191cb5925d --- /dev/null +++ b/src/components/CCIP/VerifierGrid/VerifierGrid.tsx @@ -0,0 +1,46 @@ +import { fallbackVerifierIconUrl } from "~/features/utils/index.ts" +import Card from "../Cards/Card.tsx" +import Grid from "../Landing/Grid.tsx" + +interface VerifierGridProps { + verifiers: { + id: string + name: string + logo: string + totalNetworks: number + }[] + environment: string +} + +const BEFORE_SEE_MORE = 2 * 4 // Number of verifiers to show before the "See more" button, 2 rows x 4 items + +function VerifierGrid({ verifiers, environment }: VerifierGridProps) { + return ( + { + const subtitle = `${verifier.totalNetworks} ${verifier.totalNetworks === 1 ? "network" : "networks"}` + const logoElement = ( + + {`${verifier.name} + + ) + return ( + + ) + }} + /> + ) +} + +export default VerifierGrid diff --git a/src/styles/theme.css b/src/styles/theme.css index 2f394ceb5d0..a922dafcf53 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -44,6 +44,10 @@ --color-gray-90: var(--color-base-gray), 90%; --color-gray-95: var(--color-base-gray), 95%; + /* Tertiary color aliases (matches design system) */ + --tertiary-border: #d1d6de; + --tertiary-foreground: #0e1119; + --color-blue: var(--color-base-blue), 61%; --color-blue-dark: var(--color-base-blue-dark), 39%; --color-green: var(--color-base-green), 42%;