- This transaction will emit a cross-chain "HelloEvent" event on ZetaChain
- testnet's Universal Hello contract.
-
-
-
{
- if (e.target.value.length <= MAX_STRING_LENGTH) {
- setStringValue(e.target.value);
- }
- }}
+
+
+
+
Say Hello from
+
-
- Evm Call 🚀
-
-
- {stringValue.length} / {MAX_STRING_LENGTH} characters
-
+
+ Make a cross-chain call with a message from{' '}
+ {supportedChain?.name || 'a supported network'} to a universal
+ contract on ZetaChain that emits a{' '}
+ HelloEvent .
+
- )}
+
+
+
);
}
diff --git a/examples/hello/frontend/src/DisconnectedContent.css b/examples/hello/frontend/src/DisconnectedContent.css
new file mode 100644
index 00000000..4c827b98
--- /dev/null
+++ b/examples/hello/frontend/src/DisconnectedContent.css
@@ -0,0 +1,102 @@
+.hero-content {
+ align-items: flex-start;
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ justify-content: center;
+}
+
+.hero-content-container {
+ display: flex;
+ flex-direction: column-reverse;
+ gap: 32px;
+ min-height: 65vh;
+}
+
+.hero-content-header {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.hero-content-header-title {
+ font-size: 48px;
+ font-style: normal;
+ font-weight: 700;
+ letter-spacing: -1.6px;
+ line-height: 100%;
+ text-align: left;
+}
+
+.hero-content-header-logo {
+ align-items: center;
+ display: flex;
+ gap: 8px;
+}
+
+.hero-content-header-logo-text {
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 130%;
+}
+
+[data-theme='light'] .hero-content-header-logo-text,
+.hero-content-description {
+ color: var(--text-secondary-light);
+}
+
+[data-theme='dark'] .hero-content-header-logo-text,
+.hero-content-description {
+ color: var(--text-secondary-dark);
+}
+
+.hero-content-description {
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%;
+ text-align: left;
+}
+
+.hero-content-animation {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+@media (min-width: 768px) {
+ .hero-content {
+ gap: 24px;
+ }
+
+ .hero-content-container {
+ flex-direction: row;
+ }
+
+ .hero-content-header {
+ gap: 16px;
+ }
+
+ .hero-content-header-title {
+ font-size: 56px;
+ }
+
+ .hero-content-description {
+ font-size: 18px;
+ }
+
+ .hero-content-animation {
+ min-width: 380px;
+ }
+}
+
+@media (min-width: 1168px) {
+ .hero-content-header-title {
+ font-size: 80px;
+ }
+
+ .hero-content-animation {
+ min-width: 568px;
+ }
+}
diff --git a/examples/hello/frontend/src/DisconnectedContent.tsx b/examples/hello/frontend/src/DisconnectedContent.tsx
index 4f6af34b..35552301 100644
--- a/examples/hello/frontend/src/DisconnectedContent.tsx
+++ b/examples/hello/frontend/src/DisconnectedContent.tsx
@@ -1,45 +1,33 @@
-import { useState } from 'react';
+import './DisconnectedContent.css';
-import { WalletSelectionModal } from './components/WalletSelectionModal';
-import { useWallet } from './hooks/useWallet';
-import type { EIP6963ProviderDetail } from './types/wallet';
+import { ConnectWallet } from './components/ConnectWallet';
+import { IconAnimation } from './components/icons/IconAnimation';
+import { IconZetaChainLogo } from './components/icons/IconZetaChainLogo';
+import { Footer } from './Footer';
export function DisconnectedContent() {
- const [isModalOpen, setIsModalOpen] = useState(false);
- const { providers, connectWallet, error, connecting } = useWallet();
-
- const handleConnectClick = () => {
- if (providers.length > 0) {
- setIsModalOpen(true);
- } else {
- alert('No wallet providers found. Please install a wallet extension.');
- }
- };
-
- const handleSelectProvider = (provider: EIP6963ProviderDetail) => {
- connectWallet(provider);
- setIsModalOpen(false);
- };
-
return (
-
Say "Hello" cross-chain!
-
- Connect your EVM wallet and trigger the Universal Hello contract already
- live on ZetaChain testnet from any of our supported EVM chains.
-
-
- {error &&
Error: {error}
}
-
- {connecting ? 'Connecting...' : 'Connect Wallet'}
-
+
+
+
+
Call a Universal App
+
+ from
+
+
+
+
+ Connect your EVM wallet and trigger the Universal Hello contract on
+ ZetaChain testnet from any currently supported EVM chain.
+
+
+
+
+
+
-
setIsModalOpen(false)}
- providers={providers}
- onConnect={handleSelectProvider}
- />
+
);
}
diff --git a/examples/hello/frontend/src/Footer.css b/examples/hello/frontend/src/Footer.css
new file mode 100644
index 00000000..a3676e3f
--- /dev/null
+++ b/examples/hello/frontend/src/Footer.css
@@ -0,0 +1,88 @@
+.footer-container {
+ align-items: flex-start;
+ align-self: stretch;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.footer-card {
+ align-items: center;
+ border-radius: 8px;
+ display: flex;
+ gap: 16px;
+ padding: 24px 16px;
+ width: 100%;
+
+ svg:first-child {
+ border-radius: 6px;
+ height: 40px;
+ width: 40px;
+ flex-shrink: 0;
+ }
+}
+
+.footer-card-text {
+ align-items: flex-start;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ flex: 1;
+}
+
+.footer-card-text-title {
+ font-size: 20px;
+ font-style: normal;
+ font-weight: 500;
+ letter-spacing: -0.48px;
+ line-height: 110%; /* 26.4px */
+ white-space: nowrap;
+}
+
+.footer-card-text-subtitle {
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 130%; /* 20.8px */
+}
+
+[data-theme='light'] .footer-card {
+ border: 1px solid #e5e8ec;
+}
+
+[data-theme='dark'] .footer-card {
+ border: 1px solid #283442;
+}
+
+[data-theme='light'] .footer-card-text-subtitle {
+ color: var(--text-secondary-light);
+}
+
+[data-theme='dark'] .footer-card-text-subtitle {
+ color: var(--text-secondary-dark);
+}
+
+@media (min-width: 1068px) {
+ .footer-container {
+ flex-direction: row;
+ gap: 32px;
+ }
+
+ .footer-card {
+ flex: 1;
+ padding: 32px 24px;
+
+ svg:first-child {
+ height: 48px;
+ width: 48px;
+ }
+ }
+
+ .footer-card-text-title {
+ font-size: 24px;
+ }
+
+ .footer-card-text-subtitle {
+ font-size: 16px;
+ }
+}
diff --git a/examples/hello/frontend/src/Footer.tsx b/examples/hello/frontend/src/Footer.tsx
new file mode 100644
index 00000000..28b7e38b
--- /dev/null
+++ b/examples/hello/frontend/src/Footer.tsx
@@ -0,0 +1,54 @@
+import './Footer.css';
+
+import {
+ IconArrowRotated,
+ IconDiscuss,
+ IconDocs,
+ IconTutorials,
+} from './components/icons';
+
+export function Footer() {
+ return (
+
+ );
+}
diff --git a/examples/hello/frontend/src/MessageFlowCard.css b/examples/hello/frontend/src/MessageFlowCard.css
new file mode 100644
index 00000000..90b9d478
--- /dev/null
+++ b/examples/hello/frontend/src/MessageFlowCard.css
@@ -0,0 +1,214 @@
+.message-flow-container {
+ align-items: flex-start;
+ backdrop-filter: blur(12px);
+ border-radius: 40px;
+ display: flex;
+ flex-direction: column;
+ height: fit-content;
+ padding: 32px;
+ width: 100%;
+}
+
+.approve-container {
+ align-items: center;
+ backdrop-filter: blur(12px);
+ border-radius: 40px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 48px 32px;
+ width: 100%;
+}
+
+.approve-content {
+ margin: 16px 0;
+}
+
+.approve-title {
+ font-size: 20px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 130%; /* 31.2px */
+}
+
+.approve-description {
+ font-variant-numeric: lining-nums tabular-nums;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 130%; /* 20.8px */
+}
+
+.message-flow-title {
+ align-items: center;
+ display: flex;
+ gap: 6px;
+ margin-bottom: 24px;
+}
+
+.message-flow-title-text {
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 100%; /* 16px */
+}
+
+.message-input-container {
+ width: 100%;
+}
+
+.message-input {
+ background: transparent;
+ border: none;
+ font-family: 'Inter', sans-serif;
+ font-size: 24px;
+ font-style: normal;
+ font-variant-numeric: lining-nums tabular-nums;
+ font-weight: 400;
+ line-height: 110%;
+ outline: none;
+ resize: none;
+ text-align: left;
+ width: 100%;
+ min-height: 32px;
+ max-height: 160px;
+ box-sizing: border-box;
+}
+
+.message-input-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+}
+
+.message-input-length-container {
+ align-items: flex-start;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.message-input-length-container-inner {
+ align-items: center;
+ display: flex;
+ gap: 4px;
+}
+
+.message-input-length-max {
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 100%; /* 14px */
+}
+
+.message-input-length-characters {
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 100%; /* 12px */
+}
+
+.message-input-length {
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 100%; /* 24px */
+}
+
+.message-separator {
+ height: 1px;
+ margin: 24px 0;
+ width: 100%;
+}
+
+.message-unsupported-network {
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 100%; /* 14px */
+ text-align: right;
+ margin-bottom: 12px;
+ width: 100%;
+}
+
+[data-theme='light'] .message-input {
+ color: var(--primary-dark);
+}
+
+[data-theme='dark'] .message-input {
+ color: var(--primary-light);
+}
+
+[data-theme='light'] .message-input-length-max,
+[data-theme='dark'] .message-unsupported-network,
+[data-theme='dark'] .approve-description,
+[data-theme='dark'] .message-input-length-characters {
+ color: #a9acb0;
+}
+
+[data-theme='dark'] .message-input-length-max,
+[data-theme='light'] .message-unsupported-network,
+[data-theme='light'] .approve-description,
+[data-theme='light'] .message-input-length-characters {
+ color: #696e75;
+}
+
+[data-theme='light'] .message-separator {
+ background: #e5e8ec;
+}
+
+[data-theme='dark'] .message-separator {
+ background: #283442;
+}
+
+[data-theme='light'] .message-flow-container,
+[data-theme='light'] .approve-container {
+ background: #ffffffcc;
+ border: 1px solid #e5e8ec;
+}
+
+[data-theme='dark'] .message-flow-container,
+[data-theme='dark'] .approve-container {
+ background: #171f29cc;
+ border: 1px solid #283442;
+}
+
+@media (min-width: 768px) {
+ .approve-title {
+ font-size: 24px;
+ }
+
+ .approve-description {
+ font-size: 16px;
+ }
+
+ .message-input-length-container {
+ align-items: center;
+ flex-direction: row;
+ gap: 16px;
+ }
+
+ .message-input-length {
+ font-size: 24px;
+ }
+
+ .message-input-length-max {
+ font-size: 14px;
+ }
+
+ .message-input-length-characters {
+ font-size: 14px;
+ }
+
+ .message-separator {
+ margin: 32px 0;
+ }
+}
+
+@media (min-width: 1168px) {
+ .message-flow-container,
+ .approve-container {
+ flex: 1;
+ width: 568px;
+ }
+}
diff --git a/examples/hello/frontend/src/MessageFlowCard.tsx b/examples/hello/frontend/src/MessageFlowCard.tsx
new file mode 100644
index 00000000..c161e5a7
--- /dev/null
+++ b/examples/hello/frontend/src/MessageFlowCard.tsx
@@ -0,0 +1,178 @@
+import './MessageFlowCard.css';
+
+import { evmCall } from '@zetachain/toolkit/chains/evm';
+import { ethers, ZeroAddress } from 'ethers';
+import { useEffect, useRef, useState } from 'react';
+
+import { Button } from './components/Button';
+import { IconApprove, IconEnvelope, IconSendTitle } from './components/icons';
+import { ConfirmedContent } from './ConfirmedContent';
+import type { SupportedChain } from './constants/chains';
+import type { EIP6963ProviderDetail } from './types/wallet';
+import { formatNumberWithLocale } from './utils/formatNumber';
+
+interface MessageFlowCardProps {
+ selectedProvider: EIP6963ProviderDetail;
+ supportedChain: SupportedChain | undefined;
+}
+
+export function MessageFlowCard({
+ selectedProvider,
+ supportedChain,
+}: MessageFlowCardProps) {
+ const MAX_STRING_LENGTH = 2000;
+ const [isUserSigningTx, setIsUserSigningTx] = useState(false);
+ const [isTxReceiptLoading, setIsTxReceiptLoading] = useState(false);
+ const [stringValue, setStringValue] = useState('');
+ const [connectedChainTxHash, setConnectedChainTxHash] = useState('');
+ const textareaRef = useRef
(null);
+
+ const getStringByteLength = (string: string) => {
+ return new TextEncoder().encode(string).length;
+ };
+
+ const handleEvmCall = async () => {
+ try {
+ const ethersProvider = new ethers.BrowserProvider(
+ selectedProvider.provider
+ );
+ const signer =
+ (await ethersProvider.getSigner()) as ethers.AbstractSigner;
+
+ const helloUniversalContractAddress =
+ '0x61a184EB30D29eD0395d1ADF38CC7d2F966c4A82';
+
+ const evmCallParams = {
+ receiver: helloUniversalContractAddress,
+ types: ['string'],
+ values: [stringValue],
+ revertOptions: {
+ callOnRevert: false,
+ revertAddress: ZeroAddress,
+ revertMessage: '',
+ abortAddress: ZeroAddress,
+ onRevertGasLimit: 1000000,
+ },
+ };
+
+ const evmCallOptions = {
+ signer,
+ txOptions: {
+ gasLimit: 1000000,
+ },
+ };
+
+ setIsUserSigningTx(true);
+
+ const result = await evmCall(evmCallParams, evmCallOptions);
+
+ setIsTxReceiptLoading(true);
+
+ await result.wait();
+
+ setConnectedChainTxHash(result.hash);
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setIsUserSigningTx(false);
+ setIsTxReceiptLoading(false);
+ }
+ };
+
+ // Auto-resize textarea based on content
+ useEffect(() => {
+ const textarea = textareaRef.current;
+ if (textarea) {
+ // Reset height to auto to get the correct scrollHeight
+ textarea.style.height = 'auto';
+ // Set height to scrollHeight to fit content
+ textarea.style.height = `${textarea.scrollHeight}px`;
+ }
+ }, [stringValue]);
+
+ if (connectedChainTxHash || isTxReceiptLoading) {
+ return (
+ {
+ setConnectedChainTxHash('');
+ setStringValue('');
+ }}
+ />
+ );
+ }
+
+ if (isUserSigningTx) {
+ return (
+
+
+
+
Approve from Wallet
+
+ Awaiting approval via your wallet
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Message to Send
+
+
+
+
+ {!supportedChain && (
+
+ Select a network to send a message
+
+ )}
+
+
+
+
+ {formatNumberWithLocale(getStringByteLength(stringValue))}{' '}
+
+
+ / {formatNumberWithLocale(MAX_STRING_LENGTH)}
+
+
+
Characters
+
+
+ MAX_STRING_LENGTH
+ }
+ icon={ }
+ >
+ Send Message
+
+
+
+
+ );
+}
diff --git a/examples/hello/frontend/src/UnsupportedNetworkContent.tsx b/examples/hello/frontend/src/UnsupportedNetworkContent.tsx
deleted file mode 100644
index 71be70a3..00000000
--- a/examples/hello/frontend/src/UnsupportedNetworkContent.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useState } from 'react';
-
-import { ChainSelectionModal } from './components/ChainSelectionModal';
-import type { SupportedChain } from './constants/chains';
-import { useSwitchChain } from './hooks/useSwitchChain';
-
-interface UnsupportedNetworkContentProps {
- decimalChainId: number;
-}
-
-export function UnsupportedNetworkContent({
- decimalChainId,
-}: UnsupportedNetworkContentProps) {
- const [isChainSelectionModalOpen, setIsChainSelectionModalOpen] =
- useState(false);
-
- const { switchChain } = useSwitchChain();
-
- const handleSwitchChain = (chain: SupportedChain) => {
- switchChain(chain.chainId);
- setIsChainSelectionModalOpen(false);
- };
-
- return (
-
-
Unsupported Network
-
- You are connected to an unsupported network with Chain Id{' '}
- {decimalChainId}.
-
-
Please switch to a supported network to continue.
-
{
- setIsChainSelectionModalOpen(true);
- }}
- >
- Switch to a supported network
-
-
setIsChainSelectionModalOpen(false)}
- onSwitchChain={handleSwitchChain}
- />
-
- );
-}
diff --git a/examples/hello/frontend/src/components/Button.css b/examples/hello/frontend/src/components/Button.css
new file mode 100644
index 00000000..336b8db1
--- /dev/null
+++ b/examples/hello/frontend/src/components/Button.css
@@ -0,0 +1,55 @@
+.button {
+ align-items: center;
+ border-radius: 1000px;
+ display: flex;
+ font-size: 16px;
+ font-weight: 500;
+ gap: 8px;
+ justify-content: center;
+ line-height: 100%;
+ padding: 16px;
+ transition: all 0.2s ease;
+
+ &:disabled {
+ cursor: not-allowed;
+ }
+}
+
+.button-content {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.button-icon {
+ align-items: center;
+ display: flex;
+}
+
+[data-theme='light'] .button {
+ background-color: var(--button-light);
+ color: var(--primary-light);
+
+ &:disabled {
+ background-color: #e5e8ec;
+ color: #696e75;
+ }
+}
+
+[data-theme='dark'] .button {
+ background-color: var(--button-dark);
+ color: var(--primary-dark);
+
+ &:disabled {
+ background-color: #283442;
+ color: #a9acb0;
+ }
+}
+
+.button-variant-default {
+ padding: 16px;
+}
+
+.button-variant-thin {
+ padding: 12px 24px;
+}
diff --git a/examples/hello/frontend/src/components/Button.tsx b/examples/hello/frontend/src/components/Button.tsx
new file mode 100644
index 00000000..1824f2a1
--- /dev/null
+++ b/examples/hello/frontend/src/components/Button.tsx
@@ -0,0 +1,40 @@
+import './Button.css';
+
+import clsx from 'clsx';
+import type { ButtonHTMLAttributes, ReactNode } from 'react';
+
+interface ButtonProps extends ButtonHTMLAttributes {
+ children?: ReactNode;
+ className?: string;
+ icon?: ReactNode;
+ variant?: 'thin' | 'default';
+}
+
+export const Button = ({
+ children,
+ icon,
+ type = 'button',
+ className,
+ variant = 'default',
+ ...props
+}: ButtonProps) => {
+ return (
+
+
+ {icon && {icon} }
+ {children}
+
+
+ );
+};
diff --git a/examples/hello/frontend/src/components/ConnectWallet.css b/examples/hello/frontend/src/components/ConnectWallet.css
new file mode 100644
index 00000000..63083ac4
--- /dev/null
+++ b/examples/hello/frontend/src/components/ConnectWallet.css
@@ -0,0 +1,3 @@
+.header-connect-wallet-button {
+ min-width: 200px;
+}
diff --git a/examples/hello/frontend/src/components/ConnectWallet.tsx b/examples/hello/frontend/src/components/ConnectWallet.tsx
new file mode 100644
index 00000000..9cdf0239
--- /dev/null
+++ b/examples/hello/frontend/src/components/ConnectWallet.tsx
@@ -0,0 +1,45 @@
+import './ConnectWallet.css';
+
+import { useState } from 'react';
+
+import { useWallet } from '../hooks/useWallet';
+import type { EIP6963ProviderDetail } from '../types/wallet';
+import { Button } from './Button';
+import { IconWallet } from './icons';
+import { WalletSelectionModal } from './WalletSelectionModal';
+
+export const ConnectWallet = () => {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const { providers, connectWallet, connecting } = useWallet();
+
+ const handleConnectClick = () => {
+ if (providers.length > 0) {
+ setIsModalOpen(true);
+ } else {
+ alert('No wallet providers found. Please install a wallet extension.');
+ }
+ };
+
+ const handleSelectProvider = (provider: EIP6963ProviderDetail) => {
+ connectWallet(provider);
+ setIsModalOpen(false);
+ };
+ return (
+ <>
+ }
+ className="header-connect-wallet-button"
+ onClick={handleConnectClick}
+ disabled={connecting}
+ >
+ {connecting ? 'Connecting...' : 'Connect Wallet'}
+
+ setIsModalOpen(false)}
+ providers={providers}
+ onConnect={handleSelectProvider}
+ />
+ >
+ );
+};
diff --git a/examples/hello/frontend/src/components/Dropdown.css b/examples/hello/frontend/src/components/Dropdown.css
new file mode 100644
index 00000000..7d23a6a1
--- /dev/null
+++ b/examples/hello/frontend/src/components/Dropdown.css
@@ -0,0 +1,283 @@
+.dropdown {
+ display: inline-block;
+ position: relative;
+ width: calc(312px + 8px + 16px);
+}
+
+.dropdown-trigger {
+ align-items: center;
+ background: none;
+ border-radius: 1000px;
+ border: 1px solid;
+ cursor: pointer;
+ display: flex;
+ font-family: 'Inter', sans-serif;
+ font-size: 16px;
+ font-weight: 500;
+ justify-content: space-between;
+ padding: 8px 16px 8px 8px;
+ width: 100%;
+}
+
+.dropdown-trigger:disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+
+.dropdown-trigger.hidden {
+ visibility: hidden;
+}
+
+.dropdown-trigger.collapsed {
+ border-color: rgba(255, 255, 255, 0.2);
+}
+
+.dropdown-trigger.expanded {
+ border-color: rgba(255, 255, 255, 0.4);
+}
+
+.dropdown-option-divider {
+ align-items: center;
+ align-self: stretch;
+ display: flex;
+ flex-direction: column;
+ height: 2px;
+ justify-content: center;
+}
+
+.dropdown-trigger-content {
+ align-items: center;
+ display: flex;
+ gap: 8px;
+ width: 100%;
+}
+
+.dropdown-trigger-icon {
+ align-items: center;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ min-height: 40px;
+ min-width: 40px;
+ overflow: hidden;
+}
+
+.dropdown-trigger-icon img {
+ height: 100%;
+ object-fit: cover;
+ width: 100%;
+}
+
+.dropdown-trigger-text {
+ flex: 1;
+ text-align: left;
+ font-weight: 400;
+ font-size: 28px;
+ white-space: nowrap;
+ line-height: 100%; /* 40px */
+}
+
+.dropdown-arrow {
+ transition: transform 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.dropdown-arrow.rotated {
+ transform: rotate(180deg);
+}
+
+.dropdown-arrow-icon {
+ width: 24px;
+ height: 24px;
+}
+
+.dropdown-menu {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ border-radius: 32px;
+ padding: 8px 0;
+ width: 100%;
+ overflow-y: auto;
+ z-index: 1000;
+ box-sizing: border-box;
+}
+
+.dropdown-option {
+ align-items: center;
+ background: none;
+ border-radius: 8px;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ gap: 12px;
+ margin-bottom: 4px;
+ padding: 8px 12px 8px 8px;
+ transition: all 0.2s ease;
+ width: 100%;
+}
+
+.dropdown-option:last-child {
+ margin-bottom: 0;
+}
+
+.dropdown-option:disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+.dropdown-option-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.dropdown-trigger-icon-placeholder {
+ border-width: 6px;
+ border-style: solid;
+}
+
+.dropdown-option-text {
+ flex: 1;
+ font-size: 28px;
+ font-weight: 400;
+ line-height: 100%; /* 40px */
+ text-align: left;
+ white-space: nowrap;
+}
+
+.dropdown-checkmark {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+}
+
+/* Light theme styles */
+[data-theme='light'] .dropdown-trigger {
+ background-color: var(--primary-light);
+ color: var(--primary-dark);
+ border: 2.5px solid #e5e8ec;
+}
+
+[data-theme='light'] .dropdown-trigger:hover:not(:disabled) {
+ border-color: rgba(0, 0, 0, 0.25);
+}
+
+[data-theme='light'] .dropdown-trigger.expanded {
+ border-color: rgba(0, 0, 0, 0.3);
+}
+
+[data-theme='light'] .dropdown-menu {
+ background-color: var(--primary-light);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+[data-theme='light'] .dropdown-option {
+ color: var(--primary-dark);
+}
+
+[data-theme='light'] .dropdown-checkmark {
+ color: var(--primary-200);
+}
+
+[data-theme='light'] .dropdown-option-divider {
+ background: linear-gradient(90deg, #ffffff 0%, #e5e8ec 50%, #ffffff 100%);
+}
+
+[data-theme='light'] .dropdown-trigger-icon-placeholder {
+ border-color: #e5e8ec;
+}
+
+/* Dark theme styles */
+[data-theme='dark'] .dropdown-trigger {
+ background-color: var(--primary-dark);
+ border: 2.5px solid #171f29;
+ color: var(--primary-light);
+}
+
+[data-theme='dark'] .dropdown-trigger.expanded {
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+[data-theme='dark'] .dropdown-menu {
+ background-color: #26303d;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+}
+
+[data-theme='dark'] .dropdown-option {
+ color: var(--primary-light);
+}
+
+[data-theme='dark'] .dropdown-checkmark {
+ color: var(--button-dark);
+}
+
+[data-theme='dark'] .dropdown-option-divider {
+ background: linear-gradient(90deg, #26303d 0%, #171f29 50%, #26303d 100%);
+}
+
+[data-theme='dark'] .dropdown-trigger-icon-placeholder {
+ border-color: #283442;
+}
+
+/* Responsive adjustments */
+@media (min-width: 768px) {
+ .dropdown {
+ width: calc(444px + 20px + 16px);
+ }
+
+ .dropdown-trigger {
+ padding: 12px 20px 12px 16px;
+ font-size: 18px;
+ }
+
+ .dropdown-trigger-content {
+ gap: 12px;
+ }
+
+ .dropdown-trigger-text {
+ font-size: 40px;
+ }
+
+ .dropdown-menu {
+ border-radius: 48px;
+ }
+
+ .dropdown-option {
+ padding: 12px 24px 12px 16px;
+ font-size: 18px;
+ }
+
+ .dropdown-trigger-icon,
+ .dropdown-option-icon {
+ min-width: 48px;
+ min-height: 48px;
+ }
+
+ .dropdown-trigger-icon-placeholder {
+ border-width: 8px;
+ }
+
+ .dropdown-arrow-icon {
+ width: 30px;
+ height: 30px;
+ }
+
+ .dropdown-checkmark {
+ width: 40px;
+ height: 40px;
+ }
+
+ .dropdown-option-text {
+ font-size: 36px;
+ }
+}
diff --git a/examples/hello/frontend/src/components/Dropdown.tsx b/examples/hello/frontend/src/components/Dropdown.tsx
new file mode 100644
index 00000000..1ddd2311
--- /dev/null
+++ b/examples/hello/frontend/src/components/Dropdown.tsx
@@ -0,0 +1,214 @@
+import './Dropdown.css';
+
+import { type ReactNode, useEffect, useRef, useState } from 'react';
+
+export interface DropdownOption {
+ id: string | number;
+ label: string;
+ value: T;
+ icon?: ReactNode;
+ disabled?: boolean;
+ colorHex?: string;
+}
+
+interface DropdownProps {
+ options: DropdownOption[];
+ selectedOption?: DropdownOption;
+ onSelect?: (option: DropdownOption) => void;
+ placeholder?: string;
+ disabled?: boolean;
+ className?: string;
+ triggerClassName?: string;
+ dropdownClassName?: string;
+ optionClassName?: string;
+ renderTrigger?: (
+ selectedOption: DropdownOption | undefined,
+ placeholder: string,
+ isOpen: boolean
+ ) => ReactNode;
+ renderOption?: (option: DropdownOption, isSelected: boolean) => ReactNode;
+}
+
+export const Dropdown = ({
+ options,
+ selectedOption,
+ onSelect,
+ placeholder = 'Select an option',
+ disabled = false,
+ className = '',
+ triggerClassName = '',
+ dropdownClassName = '',
+ optionClassName = '',
+ renderTrigger,
+ renderOption,
+}: DropdownProps) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const dropdownRef = useRef(null);
+
+ const handleToggle = () => {
+ if (!disabled) {
+ setIsOpen(!isOpen);
+ }
+ };
+
+ const handleSelect = (option: DropdownOption) => {
+ if (!option.disabled) {
+ onSelect?.(option);
+ setIsOpen(false);
+ }
+ };
+
+ // Close dropdown when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ dropdownRef.current &&
+ !dropdownRef.current.contains(event.target as Node)
+ ) {
+ setIsOpen(false);
+ }
+ };
+
+ if (isOpen) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [isOpen]);
+
+ // Close dropdown on escape key
+ useEffect(() => {
+ const handleEscape = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setIsOpen(false);
+ }
+ };
+
+ if (isOpen) {
+ document.addEventListener('keydown', handleEscape);
+ }
+
+ return () => {
+ document.removeEventListener('keydown', handleEscape);
+ };
+ }, [isOpen]);
+
+ const defaultTriggerContent = (
+ <>
+
+ {selectedOption?.colorHex ? (
+
+ ) : (
+
+ )}
+
+ {selectedOption ? selectedOption.label : placeholder}
+
+
+
+ >
+ );
+
+ const defaultOptionContent = (
+ option: DropdownOption,
+ isSelected: boolean
+ ) => (
+ <>
+ {option.colorHex && (
+
+ )}
+ {option.label}
+ {isSelected && (
+
+ )}
+ >
+ );
+
+ return (
+
+ {/* Always keep trigger in DOM to maintain layout space */}
+
+ {renderTrigger
+ ? renderTrigger(selectedOption, placeholder, isOpen)
+ : defaultTriggerContent}
+
+
+ {/* Show menu when open (overlays trigger) */}
+ {isOpen && (
+
+ {options.map((option, index) => {
+ const isSelected = selectedOption?.id === option.id;
+ const isFirst = index === 0;
+ return (
+
+ {!isFirst &&
}
+
handleSelect(option)}
+ type="button"
+ disabled={option.disabled}
+ role="option"
+ aria-selected={isSelected}
+ >
+ {renderOption
+ ? renderOption(option, isSelected)
+ : defaultOptionContent(option, isSelected)}
+
+
+ );
+ })}
+
+ )}
+
+ );
+};
diff --git a/examples/hello/frontend/src/components/Header.css b/examples/hello/frontend/src/components/Header.css
index bdbaa96b..a97b8b2d 100644
--- a/examples/hello/frontend/src/components/Header.css
+++ b/examples/hello/frontend/src/components/Header.css
@@ -1,67 +1,19 @@
.header-container {
align-items: center;
display: flex;
- justify-content: space-between;
+ justify-content: flex-end;
left: 0;
padding: 1rem;
position: fixed;
top: 0;
- width: calc(100vw);
+ width: 100%;
z-index: 100;
}
-.header-logo {
- image-rendering: pixelated;
- width: 100px;
-}
-
-.header-connected-container {
- align-items: center;
- display: flex;
-}
-
-.header-connected-container > div {
- align-items: center;
- display: flex;
-
- span {
- color: var(--primary-color);
- user-select: none;
- }
-}
-
-.header-provider-icon,
-.header-chain-icon {
- width: 1rem;
-}
-
-.header-provider-container {
+.header-controls {
align-items: center;
- background-color: var(--primary-light);
display: flex;
- gap: 0.5rem;
- padding: 0.5rem;
-}
-
-.header-chain-icon-container {
- align-items: center;
- background-color: var(--primary-color);
- border: 2px solid var(--primary-light);
- display: flex;
- justify-content: center;
- padding: calc(0.5rem - 2.2px);
-}
-
-.header-disconnect-button {
- background-color: var(--danger-color);
- color: var(--primary-light);
- font-size: 1rem;
- padding: 0.5rem 1rem;
- transition: opacity 0.2s;
-
- &:hover {
- opacity: 0.8;
- }
+ gap: 1rem;
}
@media (min-width: 768px) {
@@ -69,20 +21,7 @@
padding: 2rem;
}
- .header-logo {
- width: 160px;
- }
-
- .header-disconnect-button {
- font-size: 1.25rem;
- padding: 1rem 1.5rem;
- }
-
- .header-provider-container {
- padding: 1rem;
- }
-
- .header-chain-icon-container {
- padding: calc(1rem - 1px);
+ .header-controls {
+ gap: 8px;
}
}
diff --git a/examples/hello/frontend/src/components/Header.tsx b/examples/hello/frontend/src/components/Header.tsx
index 35bfc147..fcb443c2 100644
--- a/examples/hello/frontend/src/components/Header.tsx
+++ b/examples/hello/frontend/src/components/Header.tsx
@@ -1,51 +1,25 @@
import './Header.css';
-import { SUPPORTED_CHAINS } from '../constants/chains';
import { useWallet } from '../hooks/useWallet';
-import { truncateAddress } from '../utils/truncate';
-import { IconZetaChainLogo } from './IconZetaChainLogo';
+import { ConnectWallet } from './ConnectWallet';
+import { ThemeToggle } from './ThemeToggle';
+import { WalletControls } from './WalletControls';
export const Header = () => {
- const { account, disconnectWallet, decimalChainId, selectedProvider } =
- useWallet();
-
- const supportedChain = SUPPORTED_CHAINS.find(
- (chain) => chain.chainId === decimalChainId
- );
+ const { account } = useWallet();
return (
-
- {!!account && (
-
-
-
-
-
-
-
-
{truncateAddress(account)}
-
+
+ {!account ? (
+
+
-
- X
-
-
- )}
+ ) : (
+
+ )}
+
+
);
};
diff --git a/examples/hello/frontend/src/components/IconZetaChainLogo.tsx b/examples/hello/frontend/src/components/IconZetaChainLogo.tsx
deleted file mode 100644
index 6a634348..00000000
--- a/examples/hello/frontend/src/components/IconZetaChainLogo.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import type { SVGProps } from 'react';
-
-const IconZetaChainLogo = ({
- color,
- size,
- className,
- ...otherProps
-}: SVGProps
& {
- color?: string;
- size?: number;
- className?: string;
-}) => {
- const width = size || 478;
- const height = width * 0.1;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export { IconZetaChainLogo };
diff --git a/examples/hello/frontend/src/components/Modal.css b/examples/hello/frontend/src/components/Modal.css
index 31d9158c..e5cf2497 100644
--- a/examples/hello/frontend/src/components/Modal.css
+++ b/examples/hello/frontend/src/components/Modal.css
@@ -12,14 +12,16 @@
}
.modal-content {
- background-color: #2c2c2c;
padding: 2rem;
width: 90%;
max-width: 400px;
position: relative;
+ border-radius: 16px;
}
.modal-title {
+ font-size: 20px;
+ font-weight: 600;
margin-top: 0;
margin-bottom: 1.5rem;
}
@@ -33,20 +35,18 @@
.item-button {
align-items: center;
- background-color: #3a3a3a;
- border: 2px solid var(--primary-light);
- color: white;
cursor: pointer;
display: flex;
font-size: 1rem;
gap: 1rem;
padding: 0.75rem 1rem;
+ border-radius: 8px;
text-align: left;
- transition: opacity 0.2s;
+ transition: all 0.2s ease-in;
}
.item-button:hover {
- opacity: 0.8;
+ filter: brightness(0.8);
}
.item-icon {
@@ -55,14 +55,50 @@
}
.close-button {
- background-color: var(--primary-light);
- color: var(--primary-color);
- border: none;
- padding: 0.5rem 1rem;
+ align-items: center;
+ border-radius: 50%;
cursor: pointer;
+ display: flex;
+ height: 28px;
+ justify-content: center;
+ padding: 0.5rem;
+ position: absolute;
+ right: 16px;
+ top: 16px;
transition: opacity 0.2s;
+ width: 28px;
}
.close-button:hover {
opacity: 0.8;
}
+
+[data-theme='light'] .modal-content,
+[data-theme='light'] .item-button {
+ background-color: var(--primary-light);
+ color: var(--primary-dark);
+ border: 1px solid #f0f0f0;
+}
+
+[data-theme='dark'] .modal-content,
+[data-theme='dark'] .item-button {
+ background-color: #313131;
+ border: 1px solid #3f3f3f;
+ color: var(--primary-light);
+}
+
+[data-theme='dark'] .close-button {
+ background-color: #3f3f3f;
+ border: 1px solid #3f3f3f;
+}
+
+[data-theme='light'] .close-button {
+ background-color: #e2e2e2;
+ border: 1px solid #d6d6d6;
+}
+
+@media (min-width: 768px) {
+ .modal-content {
+ border-radius: 16px;
+ }
+}
diff --git a/examples/hello/frontend/src/components/Modal.tsx b/examples/hello/frontend/src/components/Modal.tsx
index 3a1354db..59b91aad 100644
--- a/examples/hello/frontend/src/components/Modal.tsx
+++ b/examples/hello/frontend/src/components/Modal.tsx
@@ -2,6 +2,8 @@ import './Modal.css';
import { type ReactNode, useEffect } from 'react';
+import { IconCloseModal } from './icons/IconCloseModal';
+
interface ModalProps {
isOpen: boolean;
onClose: () => void;
@@ -43,7 +45,7 @@ export const Modal = ({ isOpen, onClose, title, children }: ModalProps) => {
onClick={onClose}
aria-label="Close modal"
>
- Close
+
diff --git a/examples/hello/frontend/src/components/NetworkSelector.tsx b/examples/hello/frontend/src/components/NetworkSelector.tsx
new file mode 100644
index 00000000..9fedd2c2
--- /dev/null
+++ b/examples/hello/frontend/src/components/NetworkSelector.tsx
@@ -0,0 +1,59 @@
+import { useMemo } from 'react';
+
+import { SUPPORTED_CHAINS, type SupportedChain } from '../constants/chains';
+import { Dropdown, type DropdownOption } from './Dropdown';
+
+interface NetworkSelectorProps {
+ selectedChain?: SupportedChain;
+ onNetworkSelect?: (chain: SupportedChain) => void;
+ placeholder?: string;
+ disabled?: boolean;
+ className?: string;
+}
+
+export const NetworkSelector = ({
+ selectedChain,
+ onNetworkSelect,
+ placeholder = 'Select Network',
+ disabled = false,
+ className = '',
+}: NetworkSelectorProps) => {
+ // Convert chains to dropdown options
+ const options: DropdownOption
[] = useMemo(
+ () =>
+ SUPPORTED_CHAINS.map((chain) => ({
+ id: chain.chainId,
+ label: chain.name,
+ value: chain,
+ icon: ,
+ colorHex: chain.colorHex,
+ })),
+ []
+ );
+
+ // Find the selected option based on the selected chain
+ const selectedOption = useMemo(
+ () =>
+ selectedChain
+ ? options.find(
+ (option) => option.value.chainId === selectedChain.chainId
+ )
+ : undefined,
+ [selectedChain, options]
+ );
+
+ const handleSelect = (option: DropdownOption) => {
+ onNetworkSelect?.(option.value);
+ };
+
+ return (
+
+ );
+};
diff --git a/examples/hello/frontend/src/components/ThemeToggle.css b/examples/hello/frontend/src/components/ThemeToggle.css
new file mode 100644
index 00000000..677b7b7c
--- /dev/null
+++ b/examples/hello/frontend/src/components/ThemeToggle.css
@@ -0,0 +1,9 @@
+.theme-toggle {
+ align-items: center;
+ background-color: transparent;
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ padding: 8px;
+ transition: all 0.2s ease-in-out;
+}
diff --git a/examples/hello/frontend/src/components/ThemeToggle.tsx b/examples/hello/frontend/src/components/ThemeToggle.tsx
new file mode 100644
index 00000000..05b67bdc
--- /dev/null
+++ b/examples/hello/frontend/src/components/ThemeToggle.tsx
@@ -0,0 +1,27 @@
+import './ThemeToggle.css';
+
+import React from 'react';
+
+import { useTheme } from '../hooks/useTheme';
+import { IconThemeMoon, IconThemeSun } from './icons';
+
+export const ThemeToggle: React.FC = () => {
+ const { theme, toggleTheme } = useTheme();
+
+ return (
+
+ {theme === 'light' ? (
+ // Moon icon for dark mode
+
+ ) : (
+ // Sun icon for light mode
+
+ )}
+
+ );
+};
diff --git a/examples/hello/frontend/src/components/WalletControls.css b/examples/hello/frontend/src/components/WalletControls.css
new file mode 100644
index 00000000..08b64624
--- /dev/null
+++ b/examples/hello/frontend/src/components/WalletControls.css
@@ -0,0 +1,56 @@
+.wallet-controls-container {
+ align-items: center;
+ display: flex;
+ gap: 8px;
+ padding: 8px;
+ border-radius: 24px;
+ backdrop-filter: blur(12px);
+}
+
+.wallet-controls-icon {
+ border-radius: 100%;
+ height: 32px;
+ width: 32px;
+}
+
+.wallet-controls-address {
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 130%; /* 15.6px */
+ user-select: none;
+}
+
+.wallet-controls-disconnect-button {
+ align-items: center;
+ border-radius: 100%;
+ display: flex;
+ height: 32px;
+ justify-content: center;
+ width: 32px;
+}
+
+[data-theme='light'] .wallet-controls-container {
+ background: #ffffffcc;
+ border: 1px solid #e5e8ec;
+}
+
+[data-theme='dark'] .wallet-controls-container {
+ background: #171f29cc;
+ border: 1px solid #283442;
+}
+
+[data-theme='light'] .wallet-controls-icon {
+ background-color: #00a87d;
+}
+
+[data-theme='dark'] .wallet-controls-icon {
+ background-color: #b0ff61;
+}
+
+[data-theme='light'] .wallet-controls-disconnect-button {
+ background-color: #f9f9fb;
+}
+
+[data-theme='dark'] .wallet-controls-disconnect-button {
+ background-color: #26303d;
+}
diff --git a/examples/hello/frontend/src/components/WalletControls.tsx b/examples/hello/frontend/src/components/WalletControls.tsx
new file mode 100644
index 00000000..0a4bc8b7
--- /dev/null
+++ b/examples/hello/frontend/src/components/WalletControls.tsx
@@ -0,0 +1,31 @@
+import './WalletControls.css';
+
+import { useWallet } from '../hooks/useWallet';
+import { truncateAddress } from '../utils/truncate';
+import { IconDisconnect } from './icons';
+
+export const WalletControls = () => {
+ const { account, disconnectWallet } = useWallet();
+
+ if (!account) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {truncateAddress(account)}
+
+
+
+
+
+
+ );
+};
diff --git a/examples/hello/frontend/src/components/WalletSelectionModal.tsx b/examples/hello/frontend/src/components/WalletSelectionModal.tsx
index bb0c7fe5..6e80e3b1 100644
--- a/examples/hello/frontend/src/components/WalletSelectionModal.tsx
+++ b/examples/hello/frontend/src/components/WalletSelectionModal.tsx
@@ -15,7 +15,7 @@ export const WalletSelectionModal = ({
onConnect,
}: WalletSelectionModalProps) => {
return (
-
+
{providers.map((provider) => (
& {
+ size?: number;
+ className?: string;
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export { IconAnimation };
diff --git a/examples/hello/frontend/src/components/icons/IconApprove.tsx b/examples/hello/frontend/src/components/icons/IconApprove.tsx
new file mode 100644
index 00000000..27005644
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconApprove.tsx
@@ -0,0 +1,94 @@
+import type { SVGProps } from 'react';
+
+import { useTheme } from '../../hooks/useTheme';
+
+const IconApprove = ({
+ size = 88,
+ ...otherProps
+}: SVGProps & {
+ size?: number;
+ className?: string;
+}) => {
+ const { theme } = useTheme();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export { IconApprove };
diff --git a/examples/hello/frontend/src/components/icons/IconArrowRotated.tsx b/examples/hello/frontend/src/components/icons/IconArrowRotated.tsx
new file mode 100644
index 00000000..b12b2a40
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconArrowRotated.tsx
@@ -0,0 +1,33 @@
+import type { SVGProps } from 'react';
+
+import { useTheme } from '../../hooks/useTheme';
+
+const IconArrowRotated = ({
+ size = 34,
+ ...otherProps
+}: SVGProps & {
+ size?: number;
+ className?: string;
+}) => {
+ const { theme } = useTheme();
+
+ return (
+
+
+
+ );
+};
+
+export { IconArrowRotated };
diff --git a/examples/hello/frontend/src/components/icons/IconCloseModal.tsx b/examples/hello/frontend/src/components/icons/IconCloseModal.tsx
new file mode 100644
index 00000000..6abbf500
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconCloseModal.tsx
@@ -0,0 +1,28 @@
+import type { SVGProps } from 'react';
+
+const IconCloseModal = ({
+ size = 10,
+ ...otherProps
+}: SVGProps & {
+ size?: number;
+ className?: string;
+}) => {
+ return (
+
+ Close
+
+
+ );
+};
+
+export { IconCloseModal };
diff --git a/examples/hello/frontend/src/components/icons/IconDisconnect.tsx b/examples/hello/frontend/src/components/icons/IconDisconnect.tsx
new file mode 100644
index 00000000..c7ec9bab
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconDisconnect.tsx
@@ -0,0 +1,33 @@
+import type { SVGProps } from 'react';
+
+import { useTheme } from '../../hooks/useTheme';
+
+const IconDisconnect = ({
+ size = 16,
+ ...otherProps
+}: SVGProps & {
+ size?: number;
+ className?: string;
+}) => {
+ const { theme } = useTheme();
+
+ return (
+
+
+
+ );
+};
+
+export { IconDisconnect };
diff --git a/examples/hello/frontend/src/components/icons/IconDiscuss.tsx b/examples/hello/frontend/src/components/icons/IconDiscuss.tsx
new file mode 100644
index 00000000..574f92d4
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconDiscuss.tsx
@@ -0,0 +1,28 @@
+import type { SVGProps } from 'react';
+
+const IconDiscuss = ({
+ size = 24,
+ ...otherProps
+}: SVGProps & {
+ size?: number;
+ className?: string;
+}) => {
+ return (
+
+
+
+
+ );
+};
+
+export { IconDiscuss };
diff --git a/examples/hello/frontend/src/components/icons/IconDocs.tsx b/examples/hello/frontend/src/components/icons/IconDocs.tsx
new file mode 100644
index 00000000..9d24c43e
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconDocs.tsx
@@ -0,0 +1,53 @@
+import type { SVGProps } from 'react';
+
+const IconDocs = ({
+ size = 24,
+ ...otherProps
+}: SVGProps & {
+ size?: number;
+ className?: string;
+}) => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export { IconDocs };
diff --git a/examples/hello/frontend/src/components/icons/IconEnvelope.tsx b/examples/hello/frontend/src/components/icons/IconEnvelope.tsx
new file mode 100644
index 00000000..c30bd512
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconEnvelope.tsx
@@ -0,0 +1,29 @@
+import type { SVGProps } from 'react';
+
+const IconEnvelope = ({
+ size = 24,
+ ...otherProps
+}: SVGProps & {
+ size?: number;
+ className?: string;
+}) => {
+ return (
+
+
+
+ );
+};
+
+export { IconEnvelope };
diff --git a/examples/hello/frontend/src/components/IconExternalLink.tsx b/examples/hello/frontend/src/components/icons/IconExternalLink.tsx
similarity index 100%
rename from examples/hello/frontend/src/components/IconExternalLink.tsx
rename to examples/hello/frontend/src/components/icons/IconExternalLink.tsx
diff --git a/examples/hello/frontend/src/components/icons/IconReceived.tsx b/examples/hello/frontend/src/components/icons/IconReceived.tsx
new file mode 100644
index 00000000..61dcc8ec
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconReceived.tsx
@@ -0,0 +1,35 @@
+import type { SVGProps } from 'react';
+
+const IconReceived = ({
+ ...otherProps
+}: SVGProps & {
+ className?: string;
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export { IconReceived };
diff --git a/examples/hello/frontend/src/components/icons/IconSendTitle.tsx b/examples/hello/frontend/src/components/icons/IconSendTitle.tsx
new file mode 100644
index 00000000..d0c45c68
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconSendTitle.tsx
@@ -0,0 +1,44 @@
+import type { SVGProps } from 'react';
+
+const IconSendTitle = ({
+ ...otherProps
+}: SVGProps & {
+ className?: string;
+}) => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+export { IconSendTitle };
diff --git a/examples/hello/frontend/src/components/icons/IconSpinner.tsx b/examples/hello/frontend/src/components/icons/IconSpinner.tsx
new file mode 100644
index 00000000..6ef3041d
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconSpinner.tsx
@@ -0,0 +1,73 @@
+import type { SVGProps } from 'react';
+
+const IconSpinner = ({
+ size = 16,
+ ...otherProps
+}: SVGProps & {
+ size?: number;
+ className?: string;
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export { IconSpinner };
diff --git a/examples/hello/frontend/src/components/icons/IconThemeMoon.tsx b/examples/hello/frontend/src/components/icons/IconThemeMoon.tsx
new file mode 100644
index 00000000..a261d1a3
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconThemeMoon.tsx
@@ -0,0 +1,33 @@
+import type { SVGProps } from 'react';
+
+const IconThemeMoon = ({
+ color = '#000000',
+ size = 24,
+ className,
+ ...otherProps
+}: SVGProps & {
+ color?: string;
+ size?: number;
+ className?: string;
+}) => {
+ return (
+
+
+
+ );
+};
+
+export { IconThemeMoon };
diff --git a/examples/hello/frontend/src/components/icons/IconThemeSun.tsx b/examples/hello/frontend/src/components/icons/IconThemeSun.tsx
new file mode 100644
index 00000000..a19c7d05
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconThemeSun.tsx
@@ -0,0 +1,33 @@
+import type { SVGProps } from 'react';
+
+const IconThemeSun = ({
+ color = '#ffffff',
+ size = 24,
+ className,
+ ...otherProps
+}: SVGProps & {
+ color?: string;
+ size?: number;
+ className?: string;
+}) => {
+ return (
+
+
+
+ );
+};
+
+export { IconThemeSun };
diff --git a/examples/hello/frontend/src/components/icons/IconTutorials.tsx b/examples/hello/frontend/src/components/icons/IconTutorials.tsx
new file mode 100644
index 00000000..aa0db77a
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconTutorials.tsx
@@ -0,0 +1,30 @@
+import type { SVGProps } from 'react';
+
+const IconTutorials = ({
+ size = 24,
+ ...otherProps
+}: SVGProps & {
+ size?: number;
+ className?: string;
+}) => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+export { IconTutorials };
diff --git a/examples/hello/frontend/src/components/icons/IconWallet.tsx b/examples/hello/frontend/src/components/icons/IconWallet.tsx
new file mode 100644
index 00000000..1379e5a4
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconWallet.tsx
@@ -0,0 +1,48 @@
+import clsx from 'clsx';
+import type { SVGProps } from 'react';
+
+import { useTheme } from '../../hooks/useTheme';
+
+const IconWallet = ({
+ size = 24,
+ className,
+ ...otherProps
+}: SVGProps & {
+ size?: number;
+ className?: string;
+}) => {
+ const { theme } = useTheme();
+
+ return (
+
+
+
+
+ );
+};
+
+export { IconWallet };
diff --git a/examples/hello/frontend/src/components/icons/IconZetaChainLogo.tsx b/examples/hello/frontend/src/components/icons/IconZetaChainLogo.tsx
new file mode 100644
index 00000000..76fc167f
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/IconZetaChainLogo.tsx
@@ -0,0 +1,36 @@
+import type { SVGProps } from 'react';
+
+import { useTheme } from '../../hooks/useTheme';
+
+const IconZetaChainLogo = ({
+ size,
+ className,
+ ...otherProps
+}: SVGProps & {
+ size?: number;
+ className?: string;
+}) => {
+ const { theme } = useTheme();
+
+ const width = size || 130;
+ const height = width * 0.25;
+
+ return (
+
+
+
+ );
+};
+
+export { IconZetaChainLogo };
diff --git a/examples/hello/frontend/src/components/icons/index.ts b/examples/hello/frontend/src/components/icons/index.ts
new file mode 100644
index 00000000..3c1846c1
--- /dev/null
+++ b/examples/hello/frontend/src/components/icons/index.ts
@@ -0,0 +1,16 @@
+export * from './IconAnimation';
+export * from './IconApprove';
+export * from './IconArrowRotated';
+export * from './IconDisconnect';
+export * from './IconDiscuss';
+export * from './IconDocs';
+export * from './IconEnvelope';
+export * from './IconExternalLink';
+export * from './IconReceived';
+export * from './IconSendTitle';
+export * from './IconSpinner';
+export * from './IconThemeMoon';
+export * from './IconThemeSun';
+export * from './IconTutorials';
+export * from './IconWallet';
+export * from './IconZetaChainLogo';
diff --git a/examples/hello/frontend/src/constants/chains.ts b/examples/hello/frontend/src/constants/chains.ts
index b85939c5..fdd6d1d4 100644
--- a/examples/hello/frontend/src/constants/chains.ts
+++ b/examples/hello/frontend/src/constants/chains.ts
@@ -3,44 +3,51 @@ export interface SupportedChain {
name: string;
chainId: number;
icon: string;
+ colorHex: string;
}
export const SUPPORTED_CHAINS: SupportedChain[] = [
{
- explorerUrl: 'https://sepolia.etherscan.io/tx/',
- name: 'Ethereum Sepolia',
- chainId: 11155111,
- icon: '/logos/ethereum-logo.svg',
+ explorerUrl: 'https://sepolia.arbiscan.io/tx/',
+ name: 'Arbitrum Sepolia',
+ chainId: 421614,
+ icon: '/logos/arbitrum-logo.svg',
+ colorHex: '#28446A',
},
{
- explorerUrl: 'https://testnet.bscscan.com/tx/',
- name: 'BSC Testnet',
- chainId: 97,
- icon: '/logos/bsc-logo.svg',
+ explorerUrl: 'https://testnet.snowtrace.io/tx/',
+ name: 'Avalanche Fuji',
+ chainId: 43113,
+ icon: '/logos/avalanche-logo.svg',
+ colorHex: '#FF394A',
},
{
explorerUrl: 'https://sepolia.basescan.org/tx/',
name: 'Base Sepolia',
chainId: 84532,
icon: '/logos/base-logo.svg',
+ colorHex: '#0052FF',
},
{
- explorerUrl: 'https://sepolia.arbiscan.io/tx/',
- name: 'Arbitrum Sepolia',
- chainId: 421614,
- icon: '/logos/arbitrum-logo.svg',
+ explorerUrl: 'https://testnet.bscscan.com/tx/',
+ name: 'BSC Testnet',
+ chainId: 97,
+ icon: '/logos/bsc-logo.svg',
+ colorHex: '#E1A411',
},
{
- explorerUrl: 'https://testnet.snowtrace.io/tx/',
- name: 'Avalanche Fuji',
- chainId: 43113,
- icon: '/logos/avalanche-logo.svg',
+ explorerUrl: 'https://sepolia.etherscan.io/tx/',
+ name: 'Ethereum Sepolia',
+ chainId: 11155111,
+ icon: '/logos/ethereum-logo.svg',
+ colorHex: '#3457D5',
},
{
explorerUrl: 'https://amoy.polygonscan.com/tx/',
name: 'Polygon Amoy',
chainId: 80002,
icon: '/logos/polygon-logo.svg',
+ colorHex: '#692BD7',
},
];
diff --git a/examples/hello/frontend/src/context/ThemeContext.ts b/examples/hello/frontend/src/context/ThemeContext.ts
new file mode 100644
index 00000000..076d86a7
--- /dev/null
+++ b/examples/hello/frontend/src/context/ThemeContext.ts
@@ -0,0 +1,12 @@
+import { createContext } from 'react';
+
+export type Theme = 'light' | 'dark';
+
+export interface ThemeContextType {
+ theme: Theme;
+ toggleTheme: () => void;
+}
+
+export const ThemeContext = createContext(
+ undefined
+);
diff --git a/examples/hello/frontend/src/context/ThemeProvider.tsx b/examples/hello/frontend/src/context/ThemeProvider.tsx
new file mode 100644
index 00000000..bfe6f054
--- /dev/null
+++ b/examples/hello/frontend/src/context/ThemeProvider.tsx
@@ -0,0 +1,40 @@
+import React, { useEffect, useState } from 'react';
+
+import type { Theme } from './ThemeContext.js';
+import { ThemeContext } from './ThemeContext.js';
+
+interface ThemeProviderProps {
+ children: React.ReactNode;
+}
+
+export const ThemeProvider: React.FC = ({ children }) => {
+ const [theme, setTheme] = useState(() => {
+ // Check localStorage first, then system preference, default to dark
+ const savedTheme = localStorage.getItem('theme') as Theme;
+ if (savedTheme) return savedTheme;
+
+ if (
+ window.matchMedia &&
+ window.matchMedia('(prefers-color-scheme: light)').matches
+ ) {
+ return 'light';
+ }
+ return 'dark';
+ });
+
+ useEffect(() => {
+ // Apply theme to document root
+ document.documentElement.setAttribute('data-theme', theme);
+ localStorage.setItem('theme', theme);
+ }, [theme]);
+
+ const toggleTheme = () => {
+ setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/examples/hello/frontend/src/context/WalletContext.ts b/examples/hello/frontend/src/context/WalletContext.ts
index 3d8d4bf7..8d8f18ee 100644
--- a/examples/hello/frontend/src/context/WalletContext.ts
+++ b/examples/hello/frontend/src/context/WalletContext.ts
@@ -1,4 +1,5 @@
import { createContext } from 'react';
+
import type { EIP6963ProviderDetail } from '../types/wallet';
interface WalletContextType {
@@ -31,4 +32,4 @@ export const WalletContext = createContext({
connectWallet: async () => ({ success: false }),
disconnectWallet: () => {},
account: null,
-});
\ No newline at end of file
+});
diff --git a/examples/hello/frontend/src/fonts.css b/examples/hello/frontend/src/fonts.css
new file mode 100644
index 00000000..08f870ec
--- /dev/null
+++ b/examples/hello/frontend/src/fonts.css
@@ -0,0 +1,82 @@
+/* Inter Font Family */
+
+@font-face {
+ font-family: 'Inter';
+ font-weight: 100;
+ font-style: normal;
+ font-display: swap;
+ font-variant-numeric: tabular-nums;
+ src: url('/fonts/inter/inter-thin.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-weight: 200;
+ font-style: normal;
+ font-display: swap;
+ font-variant-numeric: tabular-nums;
+ src: url('/fonts/inter/inter-extra-light.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-weight: 300;
+ font-style: normal;
+ font-display: swap;
+ font-variant-numeric: tabular-nums;
+ src: url('/fonts/inter/inter-light.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+ font-variant-numeric: tabular-nums;
+ src: url('/fonts/inter/inter-regular.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-weight: 500;
+ font-style: normal;
+ font-display: swap;
+ font-variant-numeric: tabular-nums;
+ src: url('/fonts/inter/inter-medium.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-weight: 600;
+ font-style: normal;
+ font-display: swap;
+ font-variant-numeric: tabular-nums;
+ src: url('/fonts/inter/inter-semi-bold.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+ font-variant-numeric: tabular-nums;
+ src: url('/fonts/inter/inter-bold.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-weight: 800;
+ font-style: normal;
+ font-display: swap;
+ font-variant-numeric: tabular-nums;
+ src: url('/fonts/inter/inter-extra-bold.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-weight: 900;
+ font-style: normal;
+ font-display: swap;
+ font-variant-numeric: tabular-nums;
+ src: url('/fonts/inter/inter-black.ttf') format('truetype');
+}
diff --git a/examples/hello/frontend/src/hooks/useTheme.ts b/examples/hello/frontend/src/hooks/useTheme.ts
new file mode 100644
index 00000000..f92a86ac
--- /dev/null
+++ b/examples/hello/frontend/src/hooks/useTheme.ts
@@ -0,0 +1,11 @@
+import { useContext } from 'react';
+
+import { ThemeContext } from '../context/ThemeContext';
+
+export const useTheme = () => {
+ const context = useContext(ThemeContext);
+ if (context === undefined) {
+ throw new Error('useTheme must be used within a ThemeProvider');
+ }
+ return context;
+};
diff --git a/examples/hello/frontend/src/index.css b/examples/hello/frontend/src/index.css
index 2b29082b..3b38bd30 100644
--- a/examples/hello/frontend/src/index.css
+++ b/examples/hello/frontend/src/index.css
@@ -2,24 +2,52 @@
--primary-color: #004937;
--primary-200: #00a87d;
--primary-color-rgb: 0, 73, 55;
- --primary-light: #f9f9fb;
+ --primary-dark: #000000;
+ --primary-light: #ffffff;
+ --text-secondary-light: #696e75;
+ --text-secondary-dark: #a9acb0;
+ --button-light: #00a87d;
+ --button-dark: #b0ff61;
--danger-color: #bc3c3c;
- font-family: 'Handjet', sans-serif;
+ font-family: 'Inter', sans-serif;
font-size: 16px;
- background-color: var(--primary-color);
- color-scheme: light dark;
- color: var(--primary-light);
-
- width: 100vw;
-
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+/* Dark theme (default) */
+:root[data-theme='dark'] {
+ --bg-color: var(--primary-dark);
+ --text-color: var(--primary-light);
+ --button-bg: var(--primary-light);
+ --button-text: var(--primary-color);
+ --input-bg: var(--primary-color);
+ --input-border: var(--primary-light);
+ --input-text: var(--primary-light);
+ --header-bg: transparent;
+}
+
+/* Light theme */
+:root[data-theme='light'] {
+ --bg-color: var(--primary-light);
+ --text-color: var(--primary-dark);
+ --button-bg: var(--primary-color);
+ --button-text: var(--primary-light);
+ --input-bg: var(--primary-light);
+ --input-border: var(--primary-color);
+ --input-text: var(--primary-color);
+ --header-bg: rgba(249, 249, 251, 0.95);
+}
+
+:root {
+ background-color: var(--bg-color);
+ color: var(--text-color);
+}
+
* {
box-sizing: border-box;
border: 0;
@@ -32,57 +60,38 @@
#root {
align-items: center;
display: flex;
- height: 100vh;
justify-content: center;
- width: 100vw;
-}
-
-.handjet {
- font-family: 'Handjet', sans-serif;
}
-.silkscreen {
- font-family: 'Silkscreen', sans-serif;
+.Inter {
+ font-family: 'Inter', sans-serif;
}
h1,
h2,
h3 {
- font-family: 'Silkscreen', sans-serif;
+ font-family: 'Inter', sans-serif;
font-size: 2rem;
font-style: normal;
font-weight: 400;
+ color: var(--text-color);
}
input {
- background-color: var(--primary-color);
- border: 4px solid var(--primary-light);
- color: var(--primary-light);
- font-family: 'Handjet', sans-serif;
+ font-family: 'Inter', sans-serif;
text-align: left;
&::placeholder {
- color: var(--primary-light);
opacity: 0.5;
}
}
-.subheading {
- font-family: 'Silkscreen', sans-serif;
- font-size: 1.5rem;
- font-style: normal;
- font-weight: 400;
-}
-
button {
- background-color: var(--primary-light);
- color: var(--primary-color);
cursor: pointer;
- font-family: 'Silkscreen', sans-serif;
+ font-family: 'Inter', sans-serif;
font-size: 1rem;
font-style: normal;
font-weight: 400;
- padding: 1rem 2rem;
transition: opacity 0.2s ease-in-out;
&:hover {
@@ -92,26 +101,22 @@ button {
p,
a {
- font-family: 'Handjet', sans-serif;
+ font-family: 'Inter', sans-serif;
font-size: 1.5rem;
+ color: var(--text-color);
}
a {
- color: var(--primary-200);
+ text-decoration: none;
}
.main-container {
align-items: center;
display: flex;
flex-direction: column;
- font-size: 2rem;
- font-style: normal;
- font-weight: 400;
- gap: 2.5rem;
+ gap: 104px;
justify-content: center;
- padding: 0 2rem;
- max-width: 100vw;
- min-height: calc(100vh - 80px);
+ margin: 88px 24px 64px;
}
.sm-only {
@@ -122,12 +127,21 @@ a {
display: none;
}
+.text-primary-light {
+ color: var(--primary-light);
+}
+
+.text-primary-dark {
+ color: var(--primary-dark);
+}
+
@media (min-width: 768px) {
h1,
h2,
h3 {
font-size: 3rem;
line-height: 1.2;
+ font-weight: 700;
}
p,
@@ -140,11 +154,7 @@ a {
}
.main-container {
- max-width: 80vw;
- }
-
- .subheading {
- font-size: 1.75rem;
+ margin: 80px 80px 64px;
}
.lg-only {
@@ -155,3 +165,10 @@ a {
display: none;
}
}
+
+@media (min-width: 1168px) {
+ .main-container {
+ gap: 0;
+ margin: 80px 172px 64px;
+ }
+}
diff --git a/examples/hello/frontend/src/main.tsx b/examples/hello/frontend/src/main.tsx
index 27fcc60d..32fe9912 100644
--- a/examples/hello/frontend/src/main.tsx
+++ b/examples/hello/frontend/src/main.tsx
@@ -1,4 +1,5 @@
import './index.css';
+import './fonts.css';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
diff --git a/examples/hello/frontend/src/utils/formatNumber.ts b/examples/hello/frontend/src/utils/formatNumber.ts
new file mode 100644
index 00000000..792c13e7
--- /dev/null
+++ b/examples/hello/frontend/src/utils/formatNumber.ts
@@ -0,0 +1,21 @@
+/**
+ * Formats a number with comma separators
+ * @param num - The number to format
+ * @returns The formatted number string
+ */
+export const formatNumber = (num: number): string => {
+ return num.toLocaleString('en-US');
+};
+
+/**
+ * Formats a number with custom locale
+ * @param num - The number to format
+ * @param locale - The locale to use (default: 'en-US')
+ * @returns The formatted number string
+ */
+export const formatNumberWithLocale = (
+ num: number,
+ locale: string = 'en-US'
+): string => {
+ return num.toLocaleString(locale);
+};
diff --git a/examples/hello/frontend/src/utils/truncate.ts b/examples/hello/frontend/src/utils/truncate.ts
index d650ed39..719fc332 100644
--- a/examples/hello/frontend/src/utils/truncate.ts
+++ b/examples/hello/frontend/src/utils/truncate.ts
@@ -1,3 +1,3 @@
export const truncateAddress = (address: string) => {
- return `${address.slice(0, 6)}...${address.slice(-4)}`;
+ return `${address.slice(0, 8)}...${address.slice(-4)}`;
};
diff --git a/examples/hello/frontend/yarn.lock b/examples/hello/frontend/yarn.lock
index d492f3d8..80557ebf 100644
--- a/examples/hello/frontend/yarn.lock
+++ b/examples/hello/frontend/yarn.lock
@@ -2141,18 +2141,6 @@
dependencies:
tslib "^2.8.0"
-"@tanstack/query-core@5.83.0":
- version "5.83.0"
- resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz"
- integrity sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==
-
-"@tanstack/react-query@^5.83.0":
- version "5.83.0"
- resolved "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz"
- integrity sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==
- dependencies:
- "@tanstack/query-core" "5.83.0"
-
"@ton/core@^0.60.1":
version "0.60.1"
resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.60.1.tgz#cc9a62fb308d7597b1217dc8e44c7e2dcc0aceaa"
@@ -3315,6 +3303,11 @@ clone@^1.0.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
+clsx@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
+ integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
+
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"