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
+---
+
+
+
+ {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}
-
-
- )
- }
+const TokenCard = memo(function TokenCard({ id, logo, link, onClick, totalNetworks }: TokenCardProps) {
+ const logoElement = (
+
+ )
- 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 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
+
+
+
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 = (
+
+ )
+ 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 = (
+
+ )
+ 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%;