mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
Inline style objects became gui props (Box for a div, View or Text rendering the element for the rest, since Box styles only a div), raw colours and sizes became the design's tokens, the brand swatches read @hanzo/design's own values, and the project and creation hues moved to palette.json, which is data. The newsletter's subscribe button is the standard filled Action. connect-steps.tsx, which nothing imports, is deleted with the brand constants nobody read. Pages compared before and after at 1440x900 and 390x844: blockchain, brand, directory, projects, settings, unsubscribe identical; founder 0.03% of pixels; buying specialist's title snaps from 28 to the ramp's 26px and the newsletter's button is now filled. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
625 lines
26 KiB
TypeScript
625 lines
26 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* /blockchain/directory — every chain we serve, and what answers on it.
|
|
*
|
|
* The type and the controls are `@hanzo/ui/marketing`; the inks, the
|
|
* tile and a card's name come from `components/blockchain/surface`; the rest
|
|
* is gui props. A chain's head and a resource link are grounds that answer a
|
|
* pointer, stated as the hover itself.
|
|
*
|
|
* It is NOT a caller of `BlockchainProductLayout` and never could be: measured
|
|
* against its two neighbours it scores 42% and 41% on class vocabulary, against
|
|
* the 83% the last fold on this site was done at, and the content says the same
|
|
* thing louder — this page is a filtered table with an expanding row, and that
|
|
* template is a hero, a card deck and a code sample.
|
|
*
|
|
* The filter buttons are the roles' Action at its two weights, which carries
|
|
* the 44px coarse-pointer floor; the search field is @hanzo/composer's `Field`
|
|
* in a bordered row, so the mark and the input are siblings.
|
|
*/
|
|
|
|
import React, { useState, type ReactNode } from "react";
|
|
import { motion } from "@/components/motion";
|
|
import {
|
|
Globe,
|
|
Search,
|
|
Server,
|
|
Wallet,
|
|
Database,
|
|
Radio,
|
|
Zap,
|
|
Check,
|
|
X,
|
|
ExternalLink,
|
|
ChevronDown,
|
|
Layers,
|
|
ArrowLeftRight,
|
|
} from "lucide-react";
|
|
import { phrase } from "@hanzo/i18n";
|
|
import { useI18n } from "@hanzo/i18n/react";
|
|
import { Box, Text, View, XStack, YStack } from '@hanzo/ui'
|
|
import { Field } from '@hanzo/composer'
|
|
import { RING } from '@/components/shell/pill'
|
|
import { Card, CardTitle, DIM, GROUND, INK, Tile, ink, white } from '@/components/blockchain/surface'
|
|
import { Action, Chip, Display, Lede } from '@hanzo/ui/marketing'
|
|
|
|
type ChainCategory = "l1" | "l2" | "rollup" | "lux" | "cosmos" | "other";
|
|
|
|
interface Chain {
|
|
name: string;
|
|
id: string;
|
|
category: ChainCategory;
|
|
chainId?: number | string;
|
|
rpcEndpoint: string;
|
|
wsEndpoint?: string;
|
|
explorer?: string;
|
|
github?: string;
|
|
faucet?: string;
|
|
docs?: string;
|
|
testnet?: boolean;
|
|
support: {
|
|
rpc: boolean;
|
|
archive: boolean;
|
|
websockets: boolean;
|
|
indexer: boolean;
|
|
tokens: boolean;
|
|
nft: boolean;
|
|
webhooks: boolean;
|
|
smartWallets: boolean;
|
|
dex: boolean;
|
|
prices: boolean;
|
|
};
|
|
}
|
|
|
|
const CHAINS: Chain[] = [
|
|
// L1s
|
|
{
|
|
name: "Ethereum",
|
|
id: "ethereum",
|
|
category: "l1",
|
|
chainId: 1,
|
|
rpcEndpoint: "https://eth.hanzo.ai/v1/{API_KEY}",
|
|
wsEndpoint: "wss://eth.hanzo.ai/ws/v1/{API_KEY}",
|
|
explorer: "https://etherscan.io",
|
|
github: "https://github.com/ethereum",
|
|
docs: "https://ethereum.org/developers",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "Solana",
|
|
id: "solana",
|
|
category: "l1",
|
|
rpcEndpoint: "https://solana.hanzo.ai/v1/{API_KEY}",
|
|
wsEndpoint: "wss://solana.hanzo.ai/ws/v1/{API_KEY}",
|
|
explorer: "https://solscan.io",
|
|
github: "https://github.com/solana-labs",
|
|
docs: "https://docs.solana.com",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: false, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "Bitcoin",
|
|
id: "bitcoin",
|
|
category: "l1",
|
|
rpcEndpoint: "https://btc.hanzo.ai/v1/{API_KEY}",
|
|
explorer: "https://mempool.space",
|
|
github: "https://github.com/bitcoin/bitcoin",
|
|
docs: "https://developer.bitcoin.org",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: false, nft: false, webhooks: true, smartWallets: false, dex: false, prices: true },
|
|
},
|
|
{
|
|
name: "Avalanche C-Chain",
|
|
id: "avalanche",
|
|
category: "l1",
|
|
chainId: 43114,
|
|
rpcEndpoint: "https://avax.hanzo.ai/v1/{API_KEY}",
|
|
wsEndpoint: "wss://avax.hanzo.ai/ws/v1/{API_KEY}",
|
|
explorer: "https://snowtrace.io",
|
|
github: "https://github.com/ava-labs",
|
|
docs: "https://docs.avax.network",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "BNB Chain",
|
|
id: "bnb",
|
|
category: "l1",
|
|
chainId: 56,
|
|
rpcEndpoint: "https://bnb.hanzo.ai/v1/{API_KEY}",
|
|
wsEndpoint: "wss://bnb.hanzo.ai/ws/v1/{API_KEY}",
|
|
explorer: "https://bscscan.com",
|
|
github: "https://github.com/bnb-chain",
|
|
docs: "https://docs.bnbchain.org",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: true, prices: true },
|
|
},
|
|
// L2s & Rollups
|
|
{
|
|
name: "Arbitrum One",
|
|
id: "arbitrum",
|
|
category: "l2",
|
|
chainId: 42161,
|
|
rpcEndpoint: "https://arb.hanzo.ai/v1/{API_KEY}",
|
|
wsEndpoint: "wss://arb.hanzo.ai/ws/v1/{API_KEY}",
|
|
explorer: "https://arbiscan.io",
|
|
github: "https://github.com/OffchainLabs",
|
|
docs: "https://docs.arbitrum.io",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "Optimism",
|
|
id: "optimism",
|
|
category: "l2",
|
|
chainId: 10,
|
|
rpcEndpoint: "https://opt.hanzo.ai/v1/{API_KEY}",
|
|
wsEndpoint: "wss://opt.hanzo.ai/ws/v1/{API_KEY}",
|
|
explorer: "https://optimistic.etherscan.io",
|
|
github: "https://github.com/ethereum-optimism",
|
|
docs: "https://docs.optimism.io",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "Base",
|
|
id: "base",
|
|
category: "l2",
|
|
chainId: 8453,
|
|
rpcEndpoint: "https://base.hanzo.ai/v1/{API_KEY}",
|
|
wsEndpoint: "wss://base.hanzo.ai/ws/v1/{API_KEY}",
|
|
explorer: "https://basescan.org",
|
|
github: "https://github.com/base-org",
|
|
docs: "https://docs.base.org",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "Polygon PoS",
|
|
id: "polygon",
|
|
category: "l2",
|
|
chainId: 137,
|
|
rpcEndpoint: "https://polygon.hanzo.ai/v1/{API_KEY}",
|
|
wsEndpoint: "wss://polygon.hanzo.ai/ws/v1/{API_KEY}",
|
|
explorer: "https://polygonscan.com",
|
|
github: "https://github.com/maticnetwork",
|
|
docs: "https://docs.polygon.technology",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "zkSync Era",
|
|
id: "zksync",
|
|
category: "rollup",
|
|
chainId: 324,
|
|
rpcEndpoint: "https://zksync.hanzo.ai/v1/{API_KEY}",
|
|
wsEndpoint: "wss://zksync.hanzo.ai/ws/v1/{API_KEY}",
|
|
explorer: "https://explorer.zksync.io",
|
|
github: "https://github.com/matter-labs",
|
|
docs: "https://docs.zksync.io",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "Scroll",
|
|
id: "scroll",
|
|
category: "rollup",
|
|
chainId: 534352,
|
|
rpcEndpoint: "https://scroll.hanzo.ai/v1/{API_KEY}",
|
|
explorer: "https://scrollscan.com",
|
|
github: "https://github.com/scroll-tech",
|
|
docs: "https://docs.scroll.io",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "Linea",
|
|
id: "linea",
|
|
category: "rollup",
|
|
chainId: 59144,
|
|
rpcEndpoint: "https://linea.hanzo.ai/v1/{API_KEY}",
|
|
explorer: "https://lineascan.build",
|
|
github: "https://github.com/Consensys/linea",
|
|
docs: "https://docs.linea.build",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: true, prices: true },
|
|
},
|
|
// Lux Ecosystem
|
|
{
|
|
name: "Lux P-Chain",
|
|
id: "lux-pchain",
|
|
category: "lux",
|
|
rpcEndpoint: "https://lux-p.hanzo.ai/v1/{API_KEY}",
|
|
explorer: "https://explorer.lux.network/p-chain",
|
|
github: "https://github.com/luxfi/node",
|
|
docs: "https://docs.lux.network",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: false, nft: false, webhooks: true, smartWallets: false, dex: false, prices: false },
|
|
},
|
|
{
|
|
name: "Lux C-Chain",
|
|
id: "lux-cchain",
|
|
category: "lux",
|
|
chainId: 96369,
|
|
rpcEndpoint: "https://lux-c.hanzo.ai/v1/{API_KEY}",
|
|
wsEndpoint: "wss://lux-c.hanzo.ai/ws/v1/{API_KEY}",
|
|
explorer: "https://explorer.lux.network",
|
|
github: "https://github.com/luxfi/node",
|
|
docs: "https://docs.lux.network",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "Lux D-Chain (DEX)",
|
|
id: "lux-dchain",
|
|
category: "lux",
|
|
rpcEndpoint: "https://lux-d.hanzo.ai/v1/{API_KEY}",
|
|
explorer: "https://explorer.lux.network/d-chain",
|
|
github: "https://github.com/luxfi/node",
|
|
docs: "https://docs.lux.network",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: false, webhooks: true, smartWallets: false, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "Lux A-Chain",
|
|
id: "lux-achain",
|
|
category: "lux",
|
|
rpcEndpoint: "https://lux-a.hanzo.ai/v1/{API_KEY}",
|
|
explorer: "https://explorer.lux.network/a-chain",
|
|
github: "https://github.com/luxfi/node",
|
|
docs: "https://docs.lux.network",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: false, dex: false, prices: false },
|
|
},
|
|
{
|
|
name: "Lux Z-Chain",
|
|
id: "lux-zchain",
|
|
category: "lux",
|
|
rpcEndpoint: "https://lux-z.hanzo.ai/v1/{API_KEY}",
|
|
explorer: "https://explorer.lux.network/z-chain",
|
|
github: "https://github.com/luxfi/node",
|
|
docs: "https://docs.lux.network",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: false, nft: false, webhooks: true, smartWallets: false, dex: false, prices: false },
|
|
},
|
|
{
|
|
name: "Hanzo Network",
|
|
id: "hanzo-network",
|
|
category: "lux",
|
|
chainId: 1088,
|
|
rpcEndpoint: "https://rpc.hanzo.network/v1/{API_KEY}",
|
|
wsEndpoint: "wss://ws.hanzo.network/v1/{API_KEY}",
|
|
explorer: "https://explorer.hanzo.network",
|
|
github: "https://github.com/hanzoai",
|
|
docs: "https://docs.hanzo.ai/docs/network",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "Zoo Network",
|
|
id: "zoo-network",
|
|
category: "lux",
|
|
chainId: 2001,
|
|
rpcEndpoint: "https://rpc.zoo.network/v1/{API_KEY}",
|
|
wsEndpoint: "wss://ws.zoo.network/v1/{API_KEY}",
|
|
explorer: "https://explorer.zoo.network",
|
|
github: "https://github.com/zoo-labs",
|
|
docs: "https://docs.zoo.network",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "Pars Network",
|
|
id: "pars-network",
|
|
category: "lux",
|
|
rpcEndpoint: "https://rpc.pars.network/v1/{API_KEY}",
|
|
explorer: "https://explorer.pars.network",
|
|
docs: "https://docs.pars.network",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: true, dex: false, prices: true },
|
|
},
|
|
// Other Networks
|
|
{
|
|
name: "Cosmos Hub",
|
|
id: "cosmos",
|
|
category: "cosmos",
|
|
rpcEndpoint: "https://cosmos.hanzo.ai/v1/{API_KEY}",
|
|
explorer: "https://mintscan.io/cosmos",
|
|
github: "https://github.com/cosmos/gaia",
|
|
docs: "https://docs.cosmos.network",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: false, webhooks: true, smartWallets: false, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "Aptos",
|
|
id: "aptos",
|
|
category: "other",
|
|
rpcEndpoint: "https://aptos.hanzo.ai/v1/{API_KEY}",
|
|
explorer: "https://explorer.aptoslabs.com",
|
|
github: "https://github.com/aptos-labs",
|
|
docs: "https://aptos.dev",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: false, dex: true, prices: true },
|
|
},
|
|
{
|
|
name: "Sui",
|
|
id: "sui",
|
|
category: "other",
|
|
rpcEndpoint: "https://sui.hanzo.ai/v1/{API_KEY}",
|
|
explorer: "https://suiexplorer.com",
|
|
github: "https://github.com/MystenLabs/sui",
|
|
docs: "https://docs.sui.io",
|
|
support: { rpc: true, archive: true, websockets: true, indexer: true, tokens: true, nft: true, webhooks: true, smartWallets: false, dex: true, prices: true },
|
|
},
|
|
];
|
|
|
|
const CATEGORIES: { id: ChainCategory; name: string; icon: any }[] = [
|
|
{ id: "l1", name: phrase("L1s"), icon: Globe },
|
|
{ id: "l2", name: phrase("L2s"), icon: Layers },
|
|
{ id: "rollup", name: phrase("Rollups"), icon: Layers },
|
|
{ id: "lux", name: phrase("Lux Ecosystem"), icon: Zap },
|
|
{ id: "cosmos", name: "Cosmos", icon: Globe },
|
|
{ id: "other", name: phrase("Other"), icon: Server },
|
|
];
|
|
|
|
const PRODUCTS = [
|
|
{ key: "rpc", name: "RPC", icon: Server },
|
|
{ key: "archive", name: phrase("Archive"), icon: Database },
|
|
{ key: "websockets", name: "WebSockets", icon: Radio },
|
|
{ key: "indexer", name: phrase("Indexer"), icon: Search },
|
|
{ key: "tokens", name: phrase("Tokens"), icon: Database },
|
|
{ key: "nft", name: "NFT", icon: Layers },
|
|
{ key: "webhooks", name: phrase("Webhooks"), icon: Radio },
|
|
{ key: "smartWallets", name: phrase("Wallets"), icon: Wallet },
|
|
{ key: "dex", name: "DEX", icon: ArrowLeftRight },
|
|
{ key: "prices", name: phrase("Prices"), icon: Database },
|
|
];
|
|
|
|
/** A support mark: lit where the product answers on this chain, dark where it does not. */
|
|
const markInk = (on: boolean) => ({
|
|
backgroundColor: on ? white(10) : 'var(--neutral-800)',
|
|
color: on ? ink(70) : 'color-mix(in oklab, var(--muted-foreground) 60%, transparent)',
|
|
})
|
|
|
|
/** A label over a half of the expanded panel. */
|
|
function Label({ children }: { children: ReactNode }) {
|
|
return (
|
|
<Text render="h4" display="block" mb={12} fontSize="$2" lineHeight="1.1607rem" fontWeight="500" color={DIM}>
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
/** One endpoint, set in mono: its name dim, its address in full ink. */
|
|
function Endpoint({ name, children }: { name: string; children: ReactNode }) {
|
|
return (
|
|
<Text display="block" p={12} rounded="var(--radius-lg)" bg="var(--background)" fontFamily="$mono" fontSize="$1" lineHeight="0.9167rem" color={INK}>
|
|
<Text color={DIM}>{name}:</Text> {children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
/** A small line of meta under a chain's name, and the legend's entries. */
|
|
const META = { fontSize: '$1', lineHeight: '0.9167rem', color: DIM } as const
|
|
|
|
const ChainDirectory = () => {
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [selectedCategory, setSelectedCategory] = useState<ChainCategory | "all">("all");
|
|
const [expandedChain, setExpandedChain] = useState<string | null>(null);
|
|
const { t } = useI18n();
|
|
|
|
const filteredChains = CHAINS.filter((chain) => {
|
|
const matchesSearch = chain.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
chain.id.toLowerCase().includes(searchQuery.toLowerCase());
|
|
const matchesCategory = selectedCategory === "all" || chain.category === selectedCategory;
|
|
return matchesSearch && matchesCategory;
|
|
});
|
|
|
|
return (
|
|
<Box minH="100vh" $platform-web={{ color: 'var(--foreground)' }}>
|
|
|
|
{/* Hero */}
|
|
<Box render="section" position="relative" overflow="hidden" pt={96} pb={48} px={16} $md={{ px: 32 }}>
|
|
<Box position="relative" z="var(--z-raised)" width="100%" maxW={1152} mx="auto">
|
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}>
|
|
<Box $platform-web={{ textAlign: 'center' }}>
|
|
{/* The gap under the badge rides this WRAPPER: the pill is
|
|
inline-flex, and an inline-level box's bottom margin does not
|
|
grow the line box it sits in. */}
|
|
<Box mb={24}>
|
|
<Chip px={16} py={8} borderColor={white(30)} bg={white(10)}>
|
|
<Globe width={16} height={16} color={INK} />
|
|
<Text fontSize="$2" lineHeight="1.1607rem" fontWeight="500" color={INK}>
|
|
{t('Chain Resource Directory')}
|
|
</Text>
|
|
</Chip>
|
|
</Box>
|
|
|
|
<Display mb={16}>{t('{n} Chains. One API.', { n: '100+' })}</Display>
|
|
|
|
<Lede mb={32} width="100%" maxW={672} mx="auto">
|
|
{t('RPC endpoints, chain IDs, WebSockets, explorers, and Hanzo product support for every network.')}
|
|
</Lede>
|
|
</Box>
|
|
</motion.div>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Search & Filters */}
|
|
<Box render="section" pb={32} px={16} $md={{ px: 32 }}>
|
|
<Box width="100%" maxW={1152} mx="auto">
|
|
<YStack gap={16} mb={32} $md={{ flexDirection: 'row' }}>
|
|
{/* The mark and the field are siblings in one bordered row, so
|
|
nothing has to be positioned over anything. */}
|
|
<XStack
|
|
items="center"
|
|
gap={8}
|
|
grow={1}
|
|
shrink={1}
|
|
flexBasis="0%"
|
|
px={12}
|
|
py={12}
|
|
borderWidth={1}
|
|
borderColor="var(--border)"
|
|
rounded="var(--radius-lg)"
|
|
bg="var(--secondary)"
|
|
{...RING}
|
|
>
|
|
<Search width={20} height={20} color={DIM} />
|
|
<Field placeholder={t('Search chains...')} value={searchQuery} onChangeText={setSearchQuery} />
|
|
</XStack>
|
|
|
|
<XStack flexWrap="wrap" gap={8}>
|
|
{[{ id: 'all' as const, name: phrase('All') }, ...CATEGORIES].map((cat) => (
|
|
<Action
|
|
key={cat.id}
|
|
render="button" type="button" onClick={() => setSelectedCategory(cat.id)}
|
|
fill={selectedCategory === cat.id}
|
|
px={16}
|
|
rounded="var(--radius-lg)"
|
|
>
|
|
{t(cat.name)}
|
|
</Action>
|
|
))}
|
|
</XStack>
|
|
</YStack>
|
|
|
|
{/* Product Support Legend */}
|
|
<XStack flexWrap="wrap" gap={16} mb={32} p={16} rounded="var(--radius-lg)" borderWidth={1} borderColor="var(--border)" $platform-web={{ backgroundColor: GROUND }}>
|
|
<Text fontSize="$2" lineHeight="1.1607rem" color={DIM}>
|
|
{t('Hanzo Products:')}
|
|
</Text>
|
|
{PRODUCTS.map((product) => {
|
|
const Icon = product.icon;
|
|
return (
|
|
<XStack key={product.key} items="center" gap={6}>
|
|
<Icon width={14} height={14} color={DIM} />
|
|
<Text {...META}>{t(product.name)}</Text>
|
|
</XStack>
|
|
);
|
|
})}
|
|
</XStack>
|
|
|
|
{/* Chains List */}
|
|
<YStack gap={12}>
|
|
{filteredChains.map((chain, index) => {
|
|
const open = expandedChain === chain.id;
|
|
return (
|
|
<motion.div key={chain.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3, delay: index * 0.02 }}>
|
|
<Card overflow="hidden">
|
|
{/* Chain Header: a disclosure whose ground answers a pointer, with no geometry of its own. */}
|
|
<XStack
|
|
render={<button type="button" onClick={() => setExpandedChain(open ? null : chain.id)} />}
|
|
items="center"
|
|
justify="space-between"
|
|
gap={16}
|
|
width="100%"
|
|
p={16}
|
|
borderWidth={0}
|
|
bg="transparent"
|
|
cursor="pointer"
|
|
hoverStyle={{ bg: 'var(--white-06)' }}
|
|
$platform-web={{ textAlign: 'start', color: 'inherit', font: 'inherit' }}
|
|
>
|
|
<XStack items="center" gap={16}>
|
|
<Tile size={40}>
|
|
<Globe width={20} height={20} color={INK} />
|
|
</Tile>
|
|
<Box $platform-web={{ textAlign: 'start' }}>
|
|
<CardTitle mb={0}>{chain.name}</CardTitle>
|
|
<XStack items="center" gap={12}>
|
|
{chain.chainId && <Text {...META}>{t('Chain ID: {id}', { id: chain.chainId })}</Text>}
|
|
<Text {...META}>{chain.category === "lux" ? t("Lux Ecosystem") : chain.category.toUpperCase()}</Text>
|
|
</XStack>
|
|
</Box>
|
|
</XStack>
|
|
|
|
<XStack items="center" gap={16}>
|
|
{/* Product Support Icons — the wide read of the same
|
|
table the expanded panel gives a phone. */}
|
|
<XStack display="none" $md={{ display: 'flex' }} items="center" gap={4}>
|
|
{PRODUCTS.map((product) => {
|
|
const supported = chain.support[product.key as keyof typeof chain.support];
|
|
return (
|
|
<XStack key={product.key} render={<span title={t(product.name)} />} width={24} height={24} items="center" justify="center" rounded="var(--radius)" $platform-web={markInk(supported)}>
|
|
{supported ? <Check width={14} height={14} /> : <X width={14} height={14} />}
|
|
</XStack>
|
|
);
|
|
})}
|
|
</XStack>
|
|
<XStack shrink={0} rotate={open ? '180deg' : '0deg'} $platform-web={{ transition: 'transform var(--duration-fast) var(--ease-in-out)' }}>
|
|
<ChevronDown width={20} height={20} color={DIM} />
|
|
</XStack>
|
|
</XStack>
|
|
</XStack>
|
|
|
|
{/* Expanded Details */}
|
|
{open && (
|
|
<Box px={16} pb={16} borderTopWidth={1} borderColor="var(--border)">
|
|
<YStack gap={24} pt={16} $md={{ flexDirection: 'row' }}>
|
|
{/* Endpoints */}
|
|
<Box grow={1} shrink={1} flexBasis="0%" minW={0}>
|
|
<Label>{t('Endpoints')}</Label>
|
|
<YStack gap={8}>
|
|
<Endpoint name="RPC">{chain.rpcEndpoint}</Endpoint>
|
|
{chain.wsEndpoint && <Endpoint name="WS">{chain.wsEndpoint}</Endpoint>}
|
|
</YStack>
|
|
</Box>
|
|
|
|
{/* Resources */}
|
|
<Box grow={1} shrink={1} flexBasis="0%" minW={0}>
|
|
<Label>{t('Resources')}</Label>
|
|
<XStack flexWrap="wrap" gap={8}>
|
|
{([
|
|
[phrase('Explorer'), chain.explorer],
|
|
['GitHub', chain.github],
|
|
[phrase('Docs'), chain.docs],
|
|
[phrase('Faucet'), chain.faucet],
|
|
] as const).map(([label, href]) =>
|
|
href ? (
|
|
// A link out to somebody else's page for this chain: its ground answers a pointer.
|
|
<Text
|
|
key={label}
|
|
render={<a href={href} target="_blank" rel="noopener noreferrer" />}
|
|
display="inline-flex"
|
|
items="center"
|
|
gap={6}
|
|
px={12}
|
|
py={6}
|
|
rounded="var(--radius-lg)"
|
|
bg="var(--neutral-800)"
|
|
hoverStyle={{ bg: 'var(--neutral-700)' }}
|
|
fontSize="$1"
|
|
lineHeight="0.9167rem"
|
|
textDecorationLine="none"
|
|
$platform-web={{ color: ink(80) }}
|
|
>
|
|
{t(label)} <ExternalLink width={12} height={12} />
|
|
</Text>
|
|
) : null
|
|
)}
|
|
</XStack>
|
|
</Box>
|
|
</YStack>
|
|
|
|
{/* Product Support Grid — the narrow read, where the
|
|
row above has no room for ten marks. */}
|
|
<Box mt={16} $md={{ display: 'none' }}>
|
|
<Label>{t('Product Support')}</Label>
|
|
<View display="grid" gridTemplateColumns="repeat(5, minmax(0, 1fr))" gap={8}>
|
|
{PRODUCTS.map((product) => {
|
|
const supported = chain.support[product.key as keyof typeof chain.support];
|
|
const Icon = product.icon;
|
|
return (
|
|
<YStack key={product.key} items="center" gap={4} p={8} rounded="var(--radius)" $platform-web={{ ...markInk(supported), textAlign: 'center' }}>
|
|
<Icon width={16} height={16} />
|
|
<Text color="inherit" $platform-web={{ fontSize: 'var(--text-floor)', lineHeight: '1.4' }}>
|
|
{t(product.name)}
|
|
</Text>
|
|
</YStack>
|
|
);
|
|
})}
|
|
</View>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</Card>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</YStack>
|
|
|
|
{filteredChains.length === 0 && (
|
|
<Text display="block" py={48} text="center" color={DIM}>
|
|
{t('No chains found matching your search.')}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default ChainDirectory;
|