+
);
@@ -45,6 +76,10 @@ const preview: Preview = {
},
},
parameters: {
+ docs: {
+ theme,
+ },
+ layout: 'fullscreen',
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
@@ -52,15 +87,8 @@ const preview: Preview = {
date: /Date$/,
},
},
- backgrounds: {
- default: 'dark',
- values: [
- { name: 'dark', value: '#000000' },
- { name: 'light', value: '#ffffff' },
- ],
- },
},
- decorators: [withTheme, withI18n],
+ decorators: [withTheme, withI18n, withAppKit],
};
export default preview;
diff --git a/packages/appkit-react/.storybook/public/ton.svg b/packages/appkit-react/.storybook/public/ton.svg
new file mode 100644
index 000000000..2eba092f4
--- /dev/null
+++ b/packages/appkit-react/.storybook/public/ton.svg
@@ -0,0 +1,12 @@
+
diff --git a/packages/appkit-react/.storybook/theme.ts b/packages/appkit-react/.storybook/theme.ts
new file mode 100644
index 000000000..484846ac4
--- /dev/null
+++ b/packages/appkit-react/.storybook/theme.ts
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { create } from 'storybook/theming';
+
+export default create({
+ base: 'dark',
+
+ // Branding
+ brandTitle: 'TON AppKit',
+ brandUrl: 'https://github.com/ton-connect/kit',
+ brandImage: 'ton.svg',
+ brandTarget: '_self',
+
+ // Colors
+ colorPrimary: '#0098EA',
+ colorSecondary: '#0098EA',
+
+ // UI
+ appBg: '#121214',
+ appContentBg: '#1E1E1E',
+ appPreviewBg: '#121214',
+ appBorderColor: '#2C2C2C',
+ appBorderRadius: 8,
+
+ // Typography
+ fontBase:
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
+ fontCode: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
+
+ // Text colors
+ textColor: '#FFFFFF',
+ textInverseColor: '#121214',
+
+ // Toolbar default and active colors
+ barTextColor: '#909DAB',
+ barSelectedColor: '#0098EA',
+ barHoverColor: '#FFFFFF',
+ barBg: '#121214',
+
+ // Form colors
+ inputBg: '#1E1E1E',
+ inputBorder: '#2C2C2C',
+ inputTextColor: '#FFFFFF',
+ inputBorderRadius: 8,
+});
diff --git a/packages/appkit-react/README.md b/packages/appkit-react/README.md
index 756b02e93..fcfc72d4c 100644
--- a/packages/appkit-react/README.md
+++ b/packages/appkit-react/README.md
@@ -254,10 +254,13 @@ const { data: balance } = useStakedBalance({
userAddress: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c',
});
+const metadata = useStakingProviderMetadata();
+
return (
Staking Quote: {quote?.amountOut}
Staked Balance: {balance?.stakedBalance}
+
Receive Token Ticker: {metadata?.receiveToken?.ticker}
);
```
diff --git a/packages/appkit-react/docs/components.md b/packages/appkit-react/docs/components.md
index e431508a9..d63760fbf 100644
--- a/packages/appkit-react/docs/components.md
+++ b/packages/appkit-react/docs/components.md
@@ -105,3 +105,73 @@ A button that triggers the wallet connection flow.
```tsx
return
;
```
+
+## Staking
+
+### `StakingWidget`
+
+A high-level component that provides a complete staking interface. It handles quote fetching, transaction building, and user interactions.
+
+```tsx
+// Default UI
+return
;
+```
+
+#### Custom UI
+
+You can also use a render function to build a completely custom UI while keeping the staking logic.
+
+```tsx
+return (
+
+ {({ amount, setAmount, sendTransaction, quote, isQuoteLoading, canSubmit }) => (
+
+
setAmount(e.target.value)}
+ placeholder="Amount to stake"
+ />
+
+ {isQuoteLoading ?
Fetching quote...
: quote ?
You will receive: {quote.amountOut}
: null}
+
+
+
+ )}
+
+);
+```
+
+## Swap
+
+### `SwapWidget`
+
+A high-level component that provides a complete swap interface. It handles token selection, quote fetching, and transaction building.
+
+```tsx
+return
;
+```
+
+#### Custom UI
+
+You can also use a render function to build a completely custom UI while keeping the swap logic.
+
+```tsx
+return (
+
+ {({ fromAmount, setFromAmount, toAmount, isQuoteLoading, sendSwapTransaction, canSubmit }) => (
+
+
setFromAmount(e.target.value)} placeholder="Sell" />
+
+
{isQuoteLoading ? 'Calculating...' : `Receive: ${toAmount}`}
+
+
+
+ )}
+
+);
+```
diff --git a/packages/appkit-react/docs/hooks.md b/packages/appkit-react/docs/hooks.md
index b01154a4a..20716c2b6 100644
--- a/packages/appkit-react/docs/hooks.md
+++ b/packages/appkit-react/docs/hooks.md
@@ -695,6 +695,86 @@ return (
);
```
+### `useSwapProvider`
+
+Hook to get a specific swap provider. Returns the provider instance directly or `null` if not found.
+
+```tsx
+const provider = useSwapProvider({ id: 'stonfi' });
+return
Result: {provider ? provider.providerId : 'null'}
;
+```
+
+## Onramp
+
+### `useOnrampQuote`
+
+Hook to get an onramp quote for a specific fiat/crypto pair.
+
+```tsx
+const { data: quote, isLoading } = useOnrampQuote({
+ fiatCurrency: 'USD',
+ cryptoCurrency: 'TON',
+ amount: '100',
+});
+
+if (isLoading) return
Loading quote...
;
+return
Quote: {quote?.cryptoAmount} TON
;
+```
+
+### `useOnrampProvider`
+
+Hook to get a specific onramp provider.
+
+```tsx
+const provider = useOnrampProvider({ id: 'moonpay' });
+
+return
Provider: {provider?.providerId}
;
+```
+
+### `useOnrampProviders`
+
+Hook to get all registered onramp providers.
+
+### `useBuildOnrampUrl`
+
+Hook to build an onramp URL for redirecting the user to the provider.
+
+## Staking
+
+### Staking Hooks
+
+These hooks allow you to interact with staking providers directly.
+
+#### `useStakingQuote`
+Get a quote for staking or unstaking.
+
+#### `useStakedBalance`
+Get the user's staked balance.
+
+#### `useStakingProviderMetadata`
+Get static metadata about a specific staking provider.
+
+```tsx
+const { data: quote } = useStakingQuote({
+ amount: '1000000000',
+ direction: 'stake',
+});
+
+const { data: balance } = useStakedBalance({
+ userAddress: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c',
+});
+
+const metadata = useStakingProviderMetadata();
+
+return (
+
+
Staking Quote: {quote?.amountOut}
+
Staked Balance: {balance?.stakedBalance}
+
Receive Token Ticker: {metadata?.receiveToken?.ticker}
+
+);
+```
+
## Transaction
### `useSendTransaction`
@@ -734,6 +814,41 @@ return (
);
```
+### `useSignMessage`
+
+Hook to sign a transaction-shaped request without broadcasting it. Returns a signed internal-message BoC that can be relayed on-chain by a third party (e.g. a gasless relayer). Requires wallet support for the `SignMessage` feature.
+
+```tsx
+const { mutate: signMessage, isPending, error, data } = useSignMessage();
+
+const handleSign = () => {
+ signMessage({
+ validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes
+ messages: [
+ {
+ address: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c',
+ amount: '100000000', // 0.1 TON in nanotons
+ },
+ ],
+ });
+};
+
+return (
+
+
+ {error &&
Error: {error.message}
}
+ {data && (
+
+
Message Signed!
+
Internal BOC: {data.internalBoc}
+
+ )}
+
+);
+```
+
### `useTransferTon`
Hook to simplify transferring TON to another address.
diff --git a/packages/appkit-react/package.json b/packages/appkit-react/package.json
index 949293502..19b087587 100644
--- a/packages/appkit-react/package.json
+++ b/packages/appkit-react/package.json
@@ -45,10 +45,12 @@
"dependencies": {
"@ton/appkit": "workspace:*",
"clsx": "2.1.1",
+ "qrcode.react": "^4.2.0",
"radix-ui": "^1.4.3",
"rosetta": "1.1.0"
},
"devDependencies": {
+ "@privy-io/react-auth": "^3.22.1",
"@storybook/addon-docs": "10.3.5",
"@storybook/react": "10.3.5",
"@storybook/react-vite": "10.3.5",
@@ -66,6 +68,7 @@
"vite-plugin-node-polyfills": "^0.26.0"
},
"peerDependencies": {
+ "@privy-io/react-auth": ">=3.0.0",
"@tanstack/react-query": ">=5.0.0",
"@tonconnect/ui": ">=2.4.1",
"@tonconnect/ui-react": ">=2.4.1",
@@ -73,6 +76,9 @@
"react-dom": ">=18.0.0"
},
"peerDependenciesMeta": {
+ "@privy-io/react-auth": {
+ "optional": true
+ },
"@tonconnect/ui": {
"optional": true
},
diff --git a/packages/appkit-react/src/components/amount-presets/amount-presets.module.css b/packages/appkit-react/src/components/amount-presets/amount-presets.module.css
new file mode 100644
index 000000000..e1e0344d9
--- /dev/null
+++ b/packages/appkit-react/src/components/amount-presets/amount-presets.module.css
@@ -0,0 +1,19 @@
+.container {
+ width: 100%;
+ display: grid;
+ gap: 8px;
+ justify-content: center;
+ grid-template-columns: 1fr 1fr 1fr 1fr;
+}
+
+.preset {
+ width: 100%;
+ padding: 8px 16px;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.preset:hover {
+ background: var(--ta-color-background-secondary);
+ border-color: var(--ta-color-text-secondary);
+}
diff --git a/packages/appkit-react/src/components/amount-presets/amount-presets.stories.tsx b/packages/appkit-react/src/components/amount-presets/amount-presets.stories.tsx
new file mode 100644
index 000000000..599e1e02c
--- /dev/null
+++ b/packages/appkit-react/src/components/amount-presets/amount-presets.stories.tsx
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { fn } from 'storybook/test';
+
+import { AmountPresets } from './amount-presets';
+
+const meta: Meta
= {
+ title: 'Public/Components/AmountPresets',
+ component: AmountPresets,
+ tags: ['autodocs'],
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ presets: [
+ { label: '10%', amount: '10' },
+ { label: '50%', amount: '50' },
+ { label: '75%', amount: '75' },
+ { label: 'MAX', amount: '100' },
+ ],
+ onPresetSelect: fn(),
+ },
+};
+
+export const WithCurrencySymbol: Story = {
+ args: {
+ presets: [
+ { label: '10', amount: '10' },
+ { label: '50', amount: '50' },
+ { label: '100', amount: '100' },
+ { label: '500', amount: '500' },
+ ],
+ currencySymbol: '$',
+ onPresetSelect: fn(),
+ },
+};
diff --git a/packages/appkit-react/src/components/amount-presets/amount-presets.tsx b/packages/appkit-react/src/components/amount-presets/amount-presets.tsx
new file mode 100644
index 000000000..8e51bf860
--- /dev/null
+++ b/packages/appkit-react/src/components/amount-presets/amount-presets.tsx
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import type { ComponentProps, FC } from 'react';
+import clsx from 'clsx';
+
+import { Button } from '../button';
+import styles from './amount-presets.module.css';
+
+export interface AmountPreset {
+ label: string;
+ amount: string;
+ onSelect?: () => void;
+}
+
+export interface AmountPresetsProps extends ComponentProps<'div'> {
+ presets: AmountPreset[];
+ currencySymbol?: string;
+ onPresetSelect: (value: string) => void;
+}
+
+export const AmountPresets: FC = ({
+ presets,
+ currencySymbol,
+ onPresetSelect,
+ className,
+ ...props
+}) => {
+ return (
+
+ {presets.map((preset) => (
+
+ ))}
+
+ );
+};
diff --git a/packages/appkit-react/src/components/amount-presets/index.ts b/packages/appkit-react/src/components/amount-presets/index.ts
new file mode 100644
index 000000000..640926678
--- /dev/null
+++ b/packages/appkit-react/src/components/amount-presets/index.ts
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+export { AmountPresets } from './amount-presets';
+export type { AmountPreset, AmountPresetsProps } from './amount-presets';
diff --git a/packages/appkit-react/src/components/amount-reversed/amount-reversed.module.css b/packages/appkit-react/src/components/amount-reversed/amount-reversed.module.css
new file mode 100644
index 000000000..051d28d81
--- /dev/null
+++ b/packages/appkit-react/src/components/amount-reversed/amount-reversed.module.css
@@ -0,0 +1,34 @@
+.container {
+ composes: bodySemibold from '../../styles/typography.module.css';
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: text;
+ width: 100%;
+ overflow: hidden;
+ color: var(--ta-color-text-secondary);
+ gap: 8px;
+}
+
+.skeleton {
+ width: 70px;
+ height: calc(var(--ta-body-regular-line-height) - 4px);
+ margin: 2px 0;
+}
+
+.changeDirection {
+ width: 14px;
+ height: 14px;
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ outline: none;
+ color: var(--ta-color-text);
+ transition: opacity 0.2s ease-in-out;
+}
+
+.changeDirection:hover {
+ opacity: 0.8;
+}
diff --git a/packages/appkit-react/src/components/amount-reversed/amount-reversed.tsx b/packages/appkit-react/src/components/amount-reversed/amount-reversed.tsx
new file mode 100644
index 000000000..0e4cc35a9
--- /dev/null
+++ b/packages/appkit-react/src/components/amount-reversed/amount-reversed.tsx
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import type { ComponentProps, FC } from 'react';
+import { formatLargeValue } from '@ton/appkit';
+import clsx from 'clsx';
+
+import styles from './amount-reversed.module.css';
+import { Skeleton } from '../skeleton';
+
+export interface AmountReversedProps extends ComponentProps<'div'> {
+ value: string;
+ onChangeDirection?: () => void;
+ ticker?: string;
+ symbol?: string;
+ decimals?: number;
+ errorMessage?: string;
+ isLoading?: boolean;
+}
+
+export const AmountReversed: FC = ({
+ value,
+ onChangeDirection,
+ ticker,
+ symbol,
+ decimals,
+ errorMessage,
+ className,
+ isLoading,
+ ...props
+}) => {
+ if (errorMessage) {
+ return (
+
+ {errorMessage}
+
+ );
+ }
+
+ return (
+
+ {isLoading ? (
+
+ ) : (
+
+ {symbol}
+ {value ? formatLargeValue(value, decimals) : '0'}
+ {ticker ? ` ${ticker}` : ''}
+
+ )}
+
+ {onChangeDirection && (
+
+ )}
+
+ );
+};
diff --git a/packages/appkit-react/src/components/amount-reversed/index.ts b/packages/appkit-react/src/components/amount-reversed/index.ts
new file mode 100644
index 000000000..665f5f879
--- /dev/null
+++ b/packages/appkit-react/src/components/amount-reversed/index.ts
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+export { AmountReversed } from './amount-reversed';
+export type { AmountReversedProps } from './amount-reversed';
diff --git a/packages/appkit-react/src/components/block/block.module.css b/packages/appkit-react/src/components/block/block.module.css
index 5de8b960d..102bbef8c 100644
--- a/packages/appkit-react/src/components/block/block.module.css
+++ b/packages/appkit-react/src/components/block/block.module.css
@@ -4,7 +4,7 @@
box-sizing: border-box;
padding: 16px;
border-radius: var(--ta-border-radius-l);
- background-color: var(--ta-color-block);
+ background-color: var(--ta-color-background-secondary);
}
.row {
diff --git a/packages/appkit-react/src/components/block/block.stories.tsx b/packages/appkit-react/src/components/block/block.stories.tsx
index a14b28716..e4af9983e 100644
--- a/packages/appkit-react/src/components/block/block.stories.tsx
+++ b/packages/appkit-react/src/components/block/block.stories.tsx
@@ -6,7 +6,7 @@
*
*/
-import type { Meta, StoryObj } from '@storybook/react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
import { Block } from './block';
@@ -31,9 +31,15 @@ export const Column: Story = {
direction: 'column',
children: (
<>
- Item 1
- Item 2
- Item 3
+
+ Item 1
+
+
+ Item 2
+
+
+ Item 3
+
>
),
},
@@ -44,9 +50,15 @@ export const Row: Story = {
direction: 'row',
children: (
<>
- Item 1
- Item 2
- Item 3
+
+ Item 1
+
+
+ Item 2
+
+
+ Item 3
+
>
),
},
diff --git a/packages/appkit-react/src/components/button-with-connect/button-with-connect.tsx b/packages/appkit-react/src/components/button-with-connect/button-with-connect.tsx
new file mode 100644
index 000000000..4eab5f451
--- /dev/null
+++ b/packages/appkit-react/src/components/button-with-connect/button-with-connect.tsx
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import type { FC } from 'react';
+
+import type { ButtonProps } from '../button';
+import { Button } from '../button';
+import { useConnectors, useConnect, useSelectedWallet } from '../../features/wallets';
+import { useI18n } from '../../features/settings';
+
+export const ButtonWithConnect: FC = (props) => {
+ const connectors = useConnectors();
+ const { mutate: connect, isPending: isConnecting } = useConnect();
+ const [wallet] = useSelectedWallet();
+ const isWalletConnected = wallet !== null;
+
+ const { t } = useI18n();
+
+ if (!isWalletConnected) {
+ return (
+
+ );
+ }
+
+ return ;
+};
diff --git a/packages/appkit-react/src/components/button-with-connect/index.ts b/packages/appkit-react/src/components/button-with-connect/index.ts
new file mode 100644
index 000000000..da4d46a18
--- /dev/null
+++ b/packages/appkit-react/src/components/button-with-connect/index.ts
@@ -0,0 +1,9 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+export { ButtonWithConnect } from './button-with-connect';
diff --git a/packages/appkit-react/src/components/button/button.module.css b/packages/appkit-react/src/components/button/button.module.css
index 9482cf6e8..7843e326d 100644
--- a/packages/appkit-react/src/components/button/button.module.css
+++ b/packages/appkit-react/src/components/button/button.module.css
@@ -1,38 +1,116 @@
.button {
- composes: bodyMedium from "../../styles/typography.module.css";
appearance: none;
border: none;
outline: none;
cursor: pointer;
box-sizing: border-box;
- padding: 8px 16px;
- border-radius: 20px;
-
- display: flex;
+ display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
-
- background-color: var(--ta-color-primary);
- color: var(--ta-color-primary-foreground);
- transition: transform 0.125s ease-in-out, background-color 0.2s ease;
- will-change: transform;
+ transition: opacity 0.15s ease-in-out;
+ text-decoration: none;
+ width: fit-content;
}
-.button:hover {
- transform: scale(1.02);
+.button:hover:not(:disabled):not(.loading) {
+ opacity: 0.85;
}
-.button:active {
- transform: scale(0.98);
+.button:active:not(:disabled):not(.loading) {
+ opacity: 0.65;
}
.button:disabled {
- opacity: 0.5;
+ opacity: 0.35;
cursor: not-allowed;
}
-.button:disabled:hover,
-.button:disabled:active {
- transform: none;
+.loading {
+ cursor: wait;
+}
+
+/* Sizes */
+.l {
+ composes: bodySemibold from "../../styles/typography.module.css";
+ padding: 12px 16px;
+ border-radius: var(--ta-border-radius-xl);
+}
+
+.m {
+ composes: labelSemibold from "../../styles/typography.module.css";
+ padding: 12px 16px;
+ border-radius: var(--ta-border-radius-l);
+}
+
+.s {
+ composes: labelSemibold from "../../styles/typography.module.css";
+ padding: 9px 12px;
+ border-radius: var(--ta-border-radius-2xl);
+}
+
+.icon {
+ composes: labelSemibold from "../../styles/typography.module.css";
+ aspect-ratio: 1;
+ padding: 6px;
+ border-radius: var(--ta-border-radius-full);
+}
+
+/* Variants */
+.fill {
+ background-color: var(--ta-color-primary);
+ color: var(--ta-color-primary-foreground);
+}
+
+.secondary {
+ background-color: var(--ta-color-background-secondary);
+ color: var(--ta-color-text);
+}
+
+.bezeled {
+ background-color: var(--ta-color-background-bezeled);
+ color: var(--ta-color-primary);
+}
+
+.gray {
+ background-color: var(--ta-color-background-tertiary);
+ color: var(--ta-color-text);
+}
+
+.ghost {
+ background-color: transparent;
+ color: var(--ta-color-text);
+}
+
+.ghost:hover:not(:disabled):not(.loading) {
+ background-color: var(--ta-color-background-tertiary);
+}
+
+.fullWidth {
+ width: 100%;
+}
+
+.innerIcon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.spinner {
+ width: 18px;
+ height: 18px;
+ border: 2.5px solid currentColor;
+ border-top-color: transparent;
+ border-radius: 50%;
+ animation: button-spin 0.6s linear infinite;
+}
+
+@keyframes button-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
}
diff --git a/packages/appkit-react/src/components/button/button.stories.tsx b/packages/appkit-react/src/components/button/button.stories.tsx
index a268465b5..1682ac0df 100644
--- a/packages/appkit-react/src/components/button/button.stories.tsx
+++ b/packages/appkit-react/src/components/button/button.stories.tsx
@@ -6,7 +6,7 @@
*
*/
-import type { Meta, StoryObj } from '@storybook/react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './button';
@@ -15,9 +15,23 @@ const meta: Meta = {
component: Button,
tags: ['autodocs'],
argTypes: {
+ size: {
+ control: 'select',
+ options: ['s', 'm', 'l'],
+ },
+ variant: {
+ control: 'select',
+ options: ['fill', 'bezeled', 'gray'],
+ },
disabled: {
control: 'boolean',
},
+ loading: {
+ control: 'boolean',
+ },
+ fullWidth: {
+ control: 'boolean',
+ },
},
};
@@ -25,34 +39,71 @@ export default meta;
type Story = StoryObj;
-export const Default: Story = {
+export const Fill: Story = {
+ args: {
+ children: 'Action',
+ variant: 'fill',
+ size: 'l',
+ },
+};
+
+export const Bezeled: Story = {
+ args: {
+ children: 'Action',
+ variant: 'bezeled',
+ size: 'l',
+ },
+};
+
+export const Gray: Story = {
+ args: {
+ children: 'Action',
+ variant: 'gray',
+ size: 'l',
+ },
+};
+
+export const Sizes: Story = {
+ render: (args) => (
+
+
+
+
+
+ ),
args: {
- children: 'Click me',
+ variant: 'fill',
},
};
-export const Disabled: Story = {
+export const Variants: Story = {
+ render: (args) => (
+
+
+
+
+
+ ),
args: {
- children: 'Disabled Button',
- disabled: true,
+ size: 'l',
},
};
-export const WithIcon: Story = {
+export const Loading: Story = {
args: {
- children: (
- <>
-
- Add Item
- >
- ),
+ children: 'Loading Button',
+ loading: true,
},
};
diff --git a/packages/appkit-react/src/components/button/button.tsx b/packages/appkit-react/src/components/button/button.tsx
index a9f9dce44..787d77568 100644
--- a/packages/appkit-react/src/components/button/button.tsx
+++ b/packages/appkit-react/src/components/button/button.tsx
@@ -6,11 +6,60 @@
*
*/
-import type { FC, ComponentProps } from 'react';
+import { forwardRef } from 'react';
+import type { ComponentProps, ReactNode } from 'react';
import clsx from 'clsx';
import styles from './button.module.css';
-export const Button: FC> = ({ className, ...props }) => {
- return ;
-};
+export interface ButtonProps extends ComponentProps<'button'> {
+ size?: 's' | 'm' | 'l' | 'icon';
+ variant?: 'fill' | 'secondary' | 'bezeled' | 'gray' | 'ghost';
+ loading?: boolean;
+ fullWidth?: boolean;
+ icon?: ReactNode;
+}
+
+export const Button = forwardRef(
+ (
+ {
+ className,
+ size = 'm',
+ variant = 'fill',
+ loading = false,
+ fullWidth = false,
+ disabled,
+ icon,
+ children,
+ ...props
+ },
+ ref,
+ ) => {
+ return (
+
+ );
+ },
+);
+
+Button.displayName = 'Button';
diff --git a/packages/appkit-react/src/components/button/index.ts b/packages/appkit-react/src/components/button/index.ts
index 4d25fa681..628283402 100644
--- a/packages/appkit-react/src/components/button/index.ts
+++ b/packages/appkit-react/src/components/button/index.ts
@@ -7,3 +7,4 @@
*/
export { Button } from './button';
+export type { ButtonProps } from './button';
diff --git a/packages/appkit-react/src/components/centered-amount-input/centered-amount-input.module.css b/packages/appkit-react/src/components/centered-amount-input/centered-amount-input.module.css
new file mode 100644
index 000000000..4105120d4
--- /dev/null
+++ b/packages/appkit-react/src/components/centered-amount-input/centered-amount-input.module.css
@@ -0,0 +1,69 @@
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ position: relative;
+ cursor: text;
+ width: 100%;
+ overflow: hidden;
+}
+
+.row {
+ display: flex;
+ align-items: baseline;
+ max-width: 100%;
+}
+
+.input {
+ composes: inputXl from "../../styles/typography.module.css";
+ color: var(--ta-color-text);
+ border: none;
+ outline: none;
+ background: none;
+ padding: 0;
+ text-align: right;
+ min-width: 24px;
+ max-width: 100%;
+ box-sizing: content-box;
+}
+
+.input::placeholder {
+ color: var(--ta-color-text-tertiary);
+ opacity: 1;
+}
+
+.ticker {
+ composes: inputXlSymbol from "../../styles/typography.module.css";
+ color: var(--ta-color-text-tertiary);
+ white-space: nowrap;
+ user-select: none;
+ margin-left: 0.2em;
+}
+
+.symbol {
+ composes: inputXl from "../../styles/typography.module.css";
+ color: var(--ta-color-text-tertiary);
+ white-space: nowrap;
+ user-select: none;
+}
+
+.mirror {
+ composes: inputXl from "../../styles/typography.module.css";
+ position: absolute;
+ visibility: hidden;
+ white-space: nowrap;
+ pointer-events: none;
+}
+
+.measureRow {
+ display: flex;
+ align-items: baseline;
+ position: absolute;
+ visibility: hidden;
+ white-space: nowrap;
+ pointer-events: none;
+}
+
+.measureText {
+ composes: inputXl from "../../styles/typography.module.css";
+}
diff --git a/packages/appkit-react/src/components/centered-amount-input/centered-amount-input.stories.tsx b/packages/appkit-react/src/components/centered-amount-input/centered-amount-input.stories.tsx
new file mode 100644
index 000000000..07d11cd0e
--- /dev/null
+++ b/packages/appkit-react/src/components/centered-amount-input/centered-amount-input.stories.tsx
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { useState } from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+import { CenteredAmountInput } from './centered-amount-input';
+
+const meta: Meta = {
+ title: 'Public/Components/CenteredAmountInput',
+ component: CenteredAmountInput,
+ tags: ['autodocs'],
+};
+
+export default meta;
+type Story = StoryObj;
+
+const Template = (props: { symbol?: string; ticker?: string; placeholder?: string; width?: number }) => {
+ const [value, setValue] = useState('');
+ return (
+
+
+
+ );
+};
+
+export const WithSymbol: Story = {
+ render: () => ,
+};
+
+export const WithTicker: Story = {
+ render: () => ,
+};
+
+export const WithSymbolAndTicker: Story = {
+ render: () => ,
+};
+
+export const NarrowContainer: Story = {
+ render: () => ,
+};
diff --git a/packages/appkit-react/src/components/centered-amount-input/centered-amount-input.tsx b/packages/appkit-react/src/components/centered-amount-input/centered-amount-input.tsx
new file mode 100644
index 000000000..c30e497f2
--- /dev/null
+++ b/packages/appkit-react/src/components/centered-amount-input/centered-amount-input.tsx
@@ -0,0 +1,121 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { useCallback, useLayoutEffect, useRef, useState } from 'react';
+import type { FC, ComponentProps } from 'react';
+import clsx from 'clsx';
+
+import styles from './centered-amount-input.module.css';
+
+const MIN_FONT_SCALE = 0.5;
+
+export interface CenteredAmountInputProps extends ComponentProps<'div'> {
+ value: string;
+ onValueChange: (value: string) => void;
+ ticker?: string;
+ symbol?: string;
+ placeholder?: string;
+ disabled?: boolean;
+}
+
+export const CenteredAmountInput: FC = ({
+ value,
+ onValueChange,
+ ticker,
+ symbol,
+ placeholder = '0',
+ disabled,
+ className,
+ ...props
+}) => {
+ const wrapperRef = useRef(null);
+ const measureRowRef = useRef(null);
+ const mirrorRef = useRef(null);
+ const inputRef = useRef(null);
+ const [inputWidth, setInputWidth] = useState(undefined);
+ const [fontScale, setFontScale] = useState(1);
+
+ const adjustSize = useCallback(() => {
+ const wrapper = wrapperRef.current;
+ const measureRow = measureRowRef.current;
+ const mirror = mirrorRef.current;
+
+ if (!wrapper || !measureRow || !mirror) return;
+
+ const contentWidth = measureRow.offsetWidth;
+ const availableWidth = wrapper.clientWidth - 4;
+
+ let scale = 1;
+ if (contentWidth > 0 && contentWidth > availableWidth) {
+ scale = Math.max(MIN_FONT_SCALE, availableWidth / contentWidth);
+ }
+
+ setFontScale(scale);
+ setInputWidth(mirror.offsetWidth * scale + 4);
+ }, []);
+
+ useLayoutEffect(adjustSize, [value, placeholder, symbol, ticker, adjustSize]);
+
+ useLayoutEffect(() => {
+ const wrapper = wrapperRef.current;
+ if (!wrapper) return;
+
+ const observer = new ResizeObserver(adjustSize);
+ observer.observe(wrapper);
+ return () => observer.disconnect();
+ }, [adjustSize]);
+
+ const scaledInputFontSize = fontScale < 1 ? `calc(var(--ta-input-xl-size) * ${fontScale})` : undefined;
+ const scaledTickerFontSize = fontScale < 1 ? `calc(var(--ta-input-xl-symbol-size) * ${fontScale})` : undefined;
+
+ return (
+ inputRef.current?.focus()}
+ {...props}
+ >
+
+ {symbol && {symbol}}
+ {value || placeholder}
+ {ticker && {ticker}}
+
+
+
+ {symbol && (
+
+ {symbol}
+
+ )}
+ onValueChange(e.target.value)}
+ style={{
+ width: inputWidth ? `${inputWidth}px` : undefined,
+ fontSize: scaledInputFontSize,
+ }}
+ />
+ {ticker && (
+
+ {ticker}
+
+ )}
+
+
+
+ {value || placeholder}
+
+
+ );
+};
diff --git a/packages/appkit-react/src/components/centered-amount-input/index.ts b/packages/appkit-react/src/components/centered-amount-input/index.ts
new file mode 100644
index 000000000..4145d5582
--- /dev/null
+++ b/packages/appkit-react/src/components/centered-amount-input/index.ts
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+export { CenteredAmountInput } from './centered-amount-input';
+export type { CenteredAmountInputProps } from './centered-amount-input';
diff --git a/packages/appkit-react/src/components/circle-icon/circle-icon.tsx b/packages/appkit-react/src/components/circle-icon/circle-icon.tsx
deleted file mode 100644
index 5ed387403..000000000
--- a/packages/appkit-react/src/components/circle-icon/circle-icon.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Copyright (c) TonTech.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- */
-
-import type { FC, ComponentProps } from 'react';
-import clsx from 'clsx';
-import { Avatar } from 'radix-ui';
-
-import styles from './circle-icon.module.css';
-
-export interface CircleIconProps extends ComponentProps<'div'> {
- size?: number;
- src?: string;
- alt?: string;
- fallback?: string;
-}
-
-export const CircleIcon: FC = ({ className, size = 30, src, alt, fallback, ...props }) => {
- return (
-
-
-
- {(fallback || alt) && (
-
- {fallback ? fallback : alt?.[0]}
-
- )}
-
- );
-};
diff --git a/packages/appkit-react/src/components/collapsible/collapsible.module.css b/packages/appkit-react/src/components/collapsible/collapsible.module.css
new file mode 100644
index 000000000..d0362c1ef
--- /dev/null
+++ b/packages/appkit-react/src/components/collapsible/collapsible.module.css
@@ -0,0 +1,4 @@
+.collapsible {
+ overflow: hidden;
+ transition: height 0.25s ease;
+}
diff --git a/packages/appkit-react/src/components/collapsible/collapsible.stories.tsx b/packages/appkit-react/src/components/collapsible/collapsible.stories.tsx
new file mode 100644
index 000000000..4e0775a02
--- /dev/null
+++ b/packages/appkit-react/src/components/collapsible/collapsible.stories.tsx
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { useState } from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+import { Collapsible } from './collapsible';
+
+const meta: Meta = {
+ title: 'Public/Components/Collapsible',
+ component: Collapsible,
+ tags: ['autodocs'],
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ render: () => {
+ const [open, setOpen] = useState(false);
+ return (
+
+
+
+
+
This content is collapsible.
+
It animates from zero height to its natural height.
+
+
+
+ );
+ },
+};
diff --git a/packages/appkit-react/src/components/collapsible/collapsible.tsx b/packages/appkit-react/src/components/collapsible/collapsible.tsx
new file mode 100644
index 000000000..bce384715
--- /dev/null
+++ b/packages/appkit-react/src/components/collapsible/collapsible.tsx
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { useRef, useLayoutEffect, useState } from 'react';
+import type { FC, ComponentProps } from 'react';
+
+import styles from './collapsible.module.css';
+
+export interface CollapsibleProps extends ComponentProps<'div'> {
+ open: boolean;
+}
+
+export const Collapsible: FC = ({ open, children, ...props }) => {
+ const contentRef = useRef(null);
+ const [height, setHeight] = useState(open ? undefined : 0);
+
+ useLayoutEffect(() => {
+ const el = contentRef.current;
+ if (!el) return undefined;
+
+ if (open) {
+ setHeight(el.scrollHeight);
+
+ const onEnd = () => setHeight(undefined);
+ el.addEventListener('transitionend', onEnd, { once: true });
+ return () => el.removeEventListener('transitionend', onEnd);
+ }
+
+ setHeight(el.scrollHeight);
+ const id = requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ setHeight(0);
+ });
+ });
+ return () => cancelAnimationFrame(id);
+ }, [open]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/appkit-react/src/components/collapsible/index.ts b/packages/appkit-react/src/components/collapsible/index.ts
new file mode 100644
index 000000000..ad22db5e2
--- /dev/null
+++ b/packages/appkit-react/src/components/collapsible/index.ts
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+export { Collapsible } from './collapsible';
+export type { CollapsibleProps } from './collapsible';
diff --git a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.module.css b/packages/appkit-react/src/components/currency-item/currency-item.module.css
similarity index 65%
rename from packages/appkit-react/src/features/balances/components/currency-item/currency-item.module.css
rename to packages/appkit-react/src/components/currency-item/currency-item.module.css
index d8dc0d179..17e2058bc 100644
--- a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.module.css
+++ b/packages/appkit-react/src/components/currency-item/currency-item.module.css
@@ -1,23 +1,17 @@
-.currencyItem {
+.container {
box-sizing: border-box;
- border-radius: var(--ta-border-radius-l);
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
- padding: 12px;
- background-color: var(--ta-color-block);
- transition: border-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ padding: 0;
+ background-color: transparent;
cursor: pointer;
- border: 1px solid transparent;
+ border: none;
outline: none;
}
-.currencyItem:hover {
- border-color: var(--ta-color-primary);
-}
-
.icon {
width: 40px;
height: 40px;
@@ -45,7 +39,7 @@
}
.name {
- composes: bodyBold from "../../../../styles/typography.module.css";
+ composes: bodySemibold from "../../styles/typography.module.css";
color: var(--ta-color-text);
white-space: nowrap;
@@ -62,19 +56,26 @@
}
.ticker {
- composes: bodyMedium from "../../../../styles/typography.module.css";
+ composes: labelMedium from "../../styles/typography.module.css";
color: var(--ta-color-text-secondary);
margin: 0;
}
-.details {
+.rightSide {
text-align: right;
}
-.balance {
- composes: bodyMedium from "../../../../styles/typography.module.css";
+.mainBalance {
+ composes: bodyMedium from "../../styles/typography.module.css";
color: var(--ta-color-text);
margin: 0;
}
+
+.underBalance {
+ composes: labelMedium from "../../styles/typography.module.css";
+
+ color: var(--ta-color-text-secondary);
+ margin: 0;
+}
diff --git a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.stories.tsx b/packages/appkit-react/src/components/currency-item/currency-item.stories.tsx
similarity index 83%
rename from packages/appkit-react/src/features/balances/components/currency-item/currency-item.stories.tsx
rename to packages/appkit-react/src/components/currency-item/currency-item.stories.tsx
index 2ac1f7c2c..f665bfe1d 100644
--- a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.stories.tsx
+++ b/packages/appkit-react/src/components/currency-item/currency-item.stories.tsx
@@ -6,18 +6,15 @@
*
*/
-import type { Meta, StoryObj } from '@storybook/react';
-import { fn } from '@storybook/test';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { fn } from 'storybook/test';
import { CurrencyItem } from './currency-item';
const meta: Meta = {
- title: 'Public/Features/Balances/CurrencyItem',
+ title: 'Public/Components/CurrencyItem',
component: CurrencyItem,
tags: ['autodocs'],
- parameters: {
- layout: 'centered',
- },
args: {
onClick: fn(),
},
@@ -74,6 +71,17 @@ export const NoBalance: Story = {
},
};
+export const WithUnderBalance: Story = {
+ args: {
+ ticker: 'TON',
+ name: 'Toncoin',
+ balance: '55',
+ underBalance: '$385.00',
+ icon: 'https://ton.org/download/ton_symbol.png',
+ isVerified: true,
+ },
+};
+
export const CurrencyList: Story = {
render: () => (
diff --git a/packages/appkit-react/src/components/currency-item/currency-item.tsx b/packages/appkit-react/src/components/currency-item/currency-item.tsx
new file mode 100644
index 000000000..4c9dad48f
--- /dev/null
+++ b/packages/appkit-react/src/components/currency-item/currency-item.tsx
@@ -0,0 +1,137 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import type { FC, ComponentProps } from 'react';
+import clsx from 'clsx';
+
+import { Logo } from '../logo';
+import type { LogoProps } from '../logo';
+import styles from './currency-item.module.css';
+
+export interface CurrencyItemProps extends ComponentProps<'button'> {
+ ticker?: string;
+ name?: string;
+ balance?: string;
+ underBalance?: string;
+ icon?: string;
+ isVerified?: boolean;
+}
+
+const Container: FC
> = ({ className, children, ...props }) => (
+
+);
+
+const LogoWrapper: FC = ({ className, ...props }) => (
+
+);
+
+const Info: FC> = ({ className, children, ...props }) => (
+
+ {children}
+
+);
+
+const Header: FC> = ({ className, children, ...props }) => (
+
+ {children}
+
+);
+
+const Name: FC> = ({ className, children, ...props }) => (
+
+ {children}
+
+);
+
+const Ticker: FC> = ({ className, children, ...props }) => (
+
+ {children}
+
+);
+
+const VerifiedBadge: FC> = ({ className, ...props }) => (
+
+);
+
+const RightSide: FC> = ({ className, children, ...props }) => (
+
+ {children}
+
+);
+
+const MainBalance: FC> = ({ className, children, ...props }) => (
+
+ {children}
+
+);
+
+const UnderBalance: FC> = ({ className, children, ...props }) => (
+
+ {children}
+
+);
+
+const CurrencyItemRoot: FC = ({
+ ticker,
+ name,
+ balance,
+ underBalance,
+ icon,
+ isVerified,
+ children,
+ ...props
+}) => {
+ if (children) {
+ return {children};
+ }
+
+ return (
+
+ {(icon || ticker) && }
+
+
+
+ {name || ticker}
+ {isVerified && }
+
+
+
+ {ticker} {name && ticker && <>• {name}>}
+
+
+
+ {(balance || underBalance) && (
+
+ {balance && {balance}}
+ {underBalance && {underBalance}}
+
+ )}
+
+ );
+};
+
+export const CurrencyItem = Object.assign(CurrencyItemRoot, {
+ Container,
+ Logo: LogoWrapper,
+ Info,
+ VerifiedBadge,
+ Header,
+ Name,
+ Ticker,
+ RightSide,
+ MainBalance,
+ UnderBalance,
+});
diff --git a/packages/appkit-react/src/features/balances/components/currency-item/index.ts b/packages/appkit-react/src/components/currency-item/index.ts
similarity index 100%
rename from packages/appkit-react/src/features/balances/components/currency-item/index.ts
rename to packages/appkit-react/src/components/currency-item/index.ts
diff --git a/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.module.css b/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.module.css
new file mode 100644
index 000000000..c5c2e6ad9
--- /dev/null
+++ b/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.module.css
@@ -0,0 +1,49 @@
+.searchWrapper {
+ margin-bottom: 16px;
+}
+
+.body {
+ overflow-y: hidden;
+}
+
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ height: 400px;
+ max-height: 100%;
+ overflow-y: auto;
+ flex: 1;
+}
+
+.section {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.sectionHeader {
+ composes: labelSemibold from "../../styles/typography.module.css";
+
+ color: var(--ta-color-text-secondary);
+ margin: 0;
+ padding: 8px 0 4px;
+}
+
+.section:first-child .sectionHeader {
+ padding-top: 0;
+}
+
+.empty {
+ padding: 32px 0;
+}
+
+.emptyText {
+ composes: bodyRegular from "../../styles/typography.module.css";
+ margin: 0;
+ color: var(--ta-color-text-secondary);
+ text-align: center;
+}
diff --git a/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.tsx b/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.tsx
new file mode 100644
index 000000000..c9519583b
--- /dev/null
+++ b/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.tsx
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import type { FC, ComponentProps } from 'react';
+import clsx from 'clsx';
+
+import type { InputContainerProps } from '../input';
+import { Input } from '../input';
+import type { ModalProps } from '../modal';
+import { Modal } from '../modal';
+import { SearchIcon } from '../search-icon';
+import styles from './currency-select-modal.module.css';
+
+export interface CurrencySelectSearchProps extends Omit {
+ searchValue: string;
+ onSearchChange: (value: string) => void;
+ placeholder?: string;
+}
+
+export const CurrencySelectSearch: FC = ({
+ searchValue,
+ onSearchChange,
+ placeholder,
+ className,
+ ...props
+}) => {
+ return (
+
+
+
+
+
+
+ onSearchChange(e.target.value)}
+ autoFocus
+ />
+
+
+ );
+};
+
+export interface CurrencySelectListContainerProps extends ComponentProps<'div'> {
+ isEmpty: boolean;
+}
+
+export const CurrencySelectListContainer: FC = ({
+ isEmpty,
+ children,
+ className,
+ ...props
+}) => {
+ return (
+
+ {isEmpty ? (
+
+
We didn't find any tokens.
+
Try searching by address.
+
+ ) : (
+ children
+ )}
+
+ );
+};
+
+export const CurrencySelectSectionHeader: FC> = ({ className, children, ...props }) => (
+
+ {children}
+
+);
+
+export const CurrencySelectSection: FC> = ({ className, children, ...props }) => (
+
+ {children}
+
+);
+
+export const CurrencySelectModal: FC = ({ className, ...props }) => {
+ return ;
+};
+
+export const CurrencySelect = {
+ Modal: CurrencySelectModal,
+ Search: CurrencySelectSearch,
+ ListContainer: CurrencySelectListContainer,
+ SectionHeader: CurrencySelectSectionHeader,
+ Section: CurrencySelectSection,
+};
diff --git a/packages/appkit-react/src/components/currency-select-modal/index.ts b/packages/appkit-react/src/components/currency-select-modal/index.ts
new file mode 100644
index 000000000..255042d50
--- /dev/null
+++ b/packages/appkit-react/src/components/currency-select-modal/index.ts
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+export { CurrencySelect } from './currency-select-modal';
+export type { CurrencySelectSearchProps, CurrencySelectListContainerProps } from './currency-select-modal';
diff --git a/packages/appkit-react/src/components/dialog/dialog.tsx b/packages/appkit-react/src/components/dialog/dialog.tsx
new file mode 100644
index 000000000..dbf4fefdd
--- /dev/null
+++ b/packages/appkit-react/src/components/dialog/dialog.tsx
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) TonTech.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { forwardRef, useCallback, useEffect, useId } from 'react';
+import { createPortal } from 'react-dom';
+import type { ComponentPropsWithoutRef, ComponentRef, FC, ReactNode } from 'react';
+
+import { DialogContext, useDialogContext } from './use-dialog-context';
+
+interface DialogRootProps {
+ children: ReactNode;
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+}
+
+const DialogRoot: FC = ({ children, open = false, onOpenChange }) => {
+ const titleId = useId();
+ const handleOpenChange = useCallback((value: boolean) => onOpenChange?.(value), [onOpenChange]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+interface DialogPortalProps {
+ children: ReactNode;
+ container?: Element | null;
+}
+
+const DialogPortal: FC = ({ children, container }) => {
+ const { open } = useDialogContext();
+ if (!open || typeof document === 'undefined') return null;
+ return createPortal(children, container ?? document.body);
+};
+
+const DialogOverlay = forwardRef, ComponentPropsWithoutRef<'div'>>((props, ref) => {
+ useEffect(() => {
+ document.body.style.overflow = 'hidden';
+ return () => {
+ document.body.style.overflow = '';
+ };
+ }, []);
+
+ return ;
+});
+
+DialogOverlay.displayName = 'DialogOverlay';
+
+const DialogContent = forwardRef, ComponentPropsWithoutRef<'div'>>((props, ref) => {
+ const { onOpenChange, titleId } = useDialogContext();
+
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') onOpenChange(false);
+ };
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [onOpenChange]);
+
+ return ;
+});
+
+DialogContent.displayName = 'DialogContent';
+
+const DialogTitle = forwardRef, ComponentPropsWithoutRef<'h2'>>((props, ref) => {
+ const { titleId } = useDialogContext();
+ return ;
+});
+
+DialogTitle.displayName = 'DialogTitle';
+
+const DialogClose = forwardRef, ComponentPropsWithoutRef<'button'>>(
+ ({ onClick, ...props }, ref) => {
+ const { onOpenChange } = useDialogContext();
+ return (
+