@octanejs/rainbowkit 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/package.json +59 -0
- package/src/connect-button.tsrx +117 -0
- package/src/hooks.ts +47 -0
- package/src/index.ts +10 -0
- package/src/internal.ts +158 -0
- package/src/provider.tsrx +406 -0
- package/src/styles.css +92 -0
- package/src/theme/index.ts +68 -0
- package/src/wallet-button.tsrx +57 -0
- package/status.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominic Gannaway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @octanejs/rainbowkit
|
|
2
|
+
|
|
3
|
+
An Octane-native wallet connection UI preserving RainbowKit's defining provider,
|
|
4
|
+
connect-control, modal-hook, and theme contracts.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pnpm add @octanejs/rainbowkit @octanejs/wagmi @octanejs/tanstack-query viem
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```tsrx
|
|
11
|
+
import { QueryClient, QueryClientProvider } from '@octanejs/tanstack-query';
|
|
12
|
+
import { WagmiProvider, createConfig, http } from '@octanejs/wagmi';
|
|
13
|
+
import { injected } from '@octanejs/wagmi/connectors';
|
|
14
|
+
import { mainnet } from '@octanejs/wagmi/chains';
|
|
15
|
+
import {
|
|
16
|
+
ConnectButton,
|
|
17
|
+
RainbowKitProvider,
|
|
18
|
+
lightTheme,
|
|
19
|
+
} from '@octanejs/rainbowkit';
|
|
20
|
+
import '@octanejs/rainbowkit/styles.css';
|
|
21
|
+
|
|
22
|
+
const config = createConfig({
|
|
23
|
+
chains: [mainnet],
|
|
24
|
+
connectors: [injected()],
|
|
25
|
+
transports: { [mainnet.id]: http() },
|
|
26
|
+
});
|
|
27
|
+
const queryClient = new QueryClient();
|
|
28
|
+
|
|
29
|
+
function App() @{
|
|
30
|
+
<WagmiProvider config={config}>
|
|
31
|
+
<QueryClientProvider client={queryClient}>
|
|
32
|
+
<RainbowKitProvider theme={lightTheme()}>
|
|
33
|
+
<ConnectButton />
|
|
34
|
+
</RainbowKitProvider>
|
|
35
|
+
</QueryClientProvider>
|
|
36
|
+
</WagmiProvider>
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Important Wagmi divergence
|
|
41
|
+
|
|
42
|
+
This package is a compatibility adapter for RainbowKit 2.2.11's public UI
|
|
43
|
+
contracts, not a repackaging of its React implementation. Upstream RainbowKit
|
|
44
|
+
2.2.11 declares `wagmi ^2.9.0`; this package intentionally targets
|
|
45
|
+
`@octanejs/wagmi` v3. It does not satisfy upstream RainbowKit's peer range and
|
|
46
|
+
must not be presented as drop-in dependency parity.
|
|
47
|
+
|
|
48
|
+
The adapter derives the familiar disconnected, connecting, connected, wrong
|
|
49
|
+
network, and modal-open states directly from Wagmi v3. The supported cohort is
|
|
50
|
+
`RainbowKitProvider`, `ConnectButton`, `ConnectButton.Custom`, `WalletButton`,
|
|
51
|
+
`useConnectModal`, `useAccountModal`, `useChainModal`, and the familiar light,
|
|
52
|
+
dark, and midnight theme factories. `rainbowTheme` is an explicitly documented
|
|
53
|
+
Octane-only purple/rounded preset.
|
|
54
|
+
|
|
55
|
+
## Wallet catalogue and accessibility
|
|
56
|
+
|
|
57
|
+
Wallet choices come from the connectors configured on the enclosing
|
|
58
|
+
`WagmiProvider`. Applications configure injected/EIP-6963 and WalletConnect
|
|
59
|
+
connectors themselves, including a WalletConnect project id. Pass `wallets` to
|
|
60
|
+
`RainbowKitProvider` to order named entries or explain a configured wallet that
|
|
61
|
+
is unavailable:
|
|
62
|
+
|
|
63
|
+
```tsrx
|
|
64
|
+
<RainbowKitProvider
|
|
65
|
+
wallets={[
|
|
66
|
+
{ id: 'injected', name: 'Browser wallet' },
|
|
67
|
+
{
|
|
68
|
+
id: 'walletConnect',
|
|
69
|
+
name: 'WalletConnect',
|
|
70
|
+
unavailableReason: 'WalletConnect projectId is required.',
|
|
71
|
+
},
|
|
72
|
+
]}
|
|
73
|
+
>
|
|
74
|
+
<ConnectButton />
|
|
75
|
+
</RainbowKitProvider>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Entries match the canonical connector `uid` when `connectorUid` is supplied,
|
|
79
|
+
with explicit id/name matching as the convenience fallback. They are
|
|
80
|
+
deduplicated by that canonical identity while preserving configured/discovery
|
|
81
|
+
order.
|
|
82
|
+
|
|
83
|
+
The native dialog has an accessible name and description, initial focus, tab
|
|
84
|
+
containment, Escape and outside dismissal, opener focus restoration, live
|
|
85
|
+
status messages, and document-scoped scroll/background containment. The
|
|
86
|
+
included stylesheet provides the baseline responsive, touch-target, and
|
|
87
|
+
reduced-motion presentation; applications remain responsible for validating
|
|
88
|
+
their final composed CSS.
|
|
89
|
+
|
|
90
|
+
## Deliberately unsupported
|
|
91
|
+
|
|
92
|
+
The first cohort omits RainbowKit's wallet factory catalogue, authentication
|
|
93
|
+
adapter, recent transactions, ENS/avatar resolution, cool mode, locale
|
|
94
|
+
translations, pixel-identical themes, account avatars/balances, and React-only
|
|
95
|
+
internals. Unsupported upstream props are not accepted by the public types.
|
|
96
|
+
Supply vendor connector SDKs directly through Wagmi.
|
|
97
|
+
|
|
98
|
+
SSR emits the deterministic disconnected control. Live connector state remains
|
|
99
|
+
authoritative after hydration; persisted state never authorizes wallet actions.
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/rainbowkit",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"octane": {
|
|
10
|
+
"hookSlots": {
|
|
11
|
+
"manual": [
|
|
12
|
+
"src"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"description": "Octane-native RainbowKit connection UI contracts over @octanejs/wagmi v3.",
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
23
|
+
"directory": "packages/rainbowkit"
|
|
24
|
+
},
|
|
25
|
+
"main": "src/index.ts",
|
|
26
|
+
"module": "src/index.ts",
|
|
27
|
+
"types": "src/index.ts",
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"README.md",
|
|
31
|
+
"status.json"
|
|
32
|
+
],
|
|
33
|
+
"exports": {
|
|
34
|
+
".": "./src/index.ts",
|
|
35
|
+
"./styles.css": "./src/styles.css"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@wagmi/core": "3.6.4",
|
|
39
|
+
"@octanejs/wagmi": "0.0.1"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@octanejs/tanstack-query": "0.1.20",
|
|
43
|
+
"octane": "0.1.21"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
47
|
+
"@tsrx/react": "^0.2.56",
|
|
48
|
+
"@wagmi/connectors": "8.0.25",
|
|
49
|
+
"react": "^19.2.7",
|
|
50
|
+
"react-dom": "^19.2.7",
|
|
51
|
+
"viem": "2.55.10",
|
|
52
|
+
"vitest": "^4.1.10",
|
|
53
|
+
"octane": "0.1.21",
|
|
54
|
+
"@octanejs/tanstack-query": "0.1.20"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"test": "vitest run --root ../.. --project rainbowkit --project rainbowkit-ssr"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useChains, useConnect, useConnection, useDisconnect } from '@octanejs/wagmi';
|
|
2
|
+
import { isChildrenBlock, useContext, type OctaneNode } from 'octane';
|
|
3
|
+
import { useAccountModal, useChainModal, useConnectModal } from './hooks';
|
|
4
|
+
import { RainbowKitContext } from './internal';
|
|
5
|
+
|
|
6
|
+
export type ConnectButtonRendererProps = {
|
|
7
|
+
account?: { address: string; displayName: string };
|
|
8
|
+
accountModalOpen: boolean;
|
|
9
|
+
authenticationStatus?: undefined;
|
|
10
|
+
chain?: { id: number; name?: string; unsupported: boolean };
|
|
11
|
+
chainModalOpen: boolean;
|
|
12
|
+
connectModalOpen: boolean;
|
|
13
|
+
connectionStatus: 'connected' | 'connecting' | 'disconnected';
|
|
14
|
+
mounted: boolean;
|
|
15
|
+
openAccountModal?: (event?: Event) => void;
|
|
16
|
+
openChainModal?: (event?: Event) => void;
|
|
17
|
+
openConnectModal?: (event?: Event) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ConnectButtonProps = {
|
|
21
|
+
chainStatus?: 'name' | 'none';
|
|
22
|
+
label?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function formatAddress(address: string): string {
|
|
26
|
+
return `${address.slice(0, 6)}…${address.slice(-4)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function useConnectButtonState(): ConnectButtonRendererProps {
|
|
30
|
+
const connection = useConnection();
|
|
31
|
+
const connect = useConnect();
|
|
32
|
+
const rainbowKit = useContext(RainbowKitContext);
|
|
33
|
+
const chains = useChains();
|
|
34
|
+
const accountModal = useAccountModal();
|
|
35
|
+
const chainModal = useChainModal();
|
|
36
|
+
const connectModal = useConnectModal();
|
|
37
|
+
const chain = chains.find((candidate) => candidate.id === connection.chainId);
|
|
38
|
+
const connected = connection.status === 'connected' && Boolean(connection.address);
|
|
39
|
+
const mounted = rainbowKit?.mounted ?? false;
|
|
40
|
+
return {
|
|
41
|
+
account: mounted && connected
|
|
42
|
+
? {
|
|
43
|
+
address: connection.address!,
|
|
44
|
+
displayName: formatAddress(connection.address!),
|
|
45
|
+
}
|
|
46
|
+
: undefined,
|
|
47
|
+
accountModalOpen: accountModal.accountModalOpen,
|
|
48
|
+
authenticationStatus: undefined,
|
|
49
|
+
chain: mounted && connected
|
|
50
|
+
? { id: connection.chainId!, name: chain?.name, unsupported: chain === undefined }
|
|
51
|
+
: undefined,
|
|
52
|
+
chainModalOpen: chainModal.chainModalOpen,
|
|
53
|
+
connectModalOpen: connectModal.connectModalOpen,
|
|
54
|
+
connectionStatus: !mounted
|
|
55
|
+
? 'disconnected'
|
|
56
|
+
: connected
|
|
57
|
+
? 'connected'
|
|
58
|
+
: connect.isPending || rainbowKit?.connecting
|
|
59
|
+
? 'connecting'
|
|
60
|
+
: 'disconnected',
|
|
61
|
+
mounted,
|
|
62
|
+
openAccountModal: mounted ? accountModal.openAccountModal : undefined,
|
|
63
|
+
openChainModal: mounted ? chainModal.openChainModal : undefined,
|
|
64
|
+
openConnectModal: mounted ? connectModal.openConnectModal : undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function ConnectButton(props: ConnectButtonProps = {}) @{
|
|
69
|
+
const state = useConnectButtonState();
|
|
70
|
+
const disconnect = useDisconnect();
|
|
71
|
+
@if (!state.account) {
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
className="rk-connect-button"
|
|
75
|
+
disabled={!state.mounted || state.connectionStatus === 'connecting'}
|
|
76
|
+
onClick={state.openConnectModal}
|
|
77
|
+
>
|
|
78
|
+
{props.label ?? 'Connect Wallet'}
|
|
79
|
+
</button>
|
|
80
|
+
} @else {
|
|
81
|
+
<div>
|
|
82
|
+
@if (state.chain?.unsupported) {
|
|
83
|
+
<button type="button" className="rk-connect-button" onClick={state.openChainModal}>
|
|
84
|
+
Wrong network
|
|
85
|
+
</button>
|
|
86
|
+
} @else {
|
|
87
|
+
@if (props.chainStatus !== 'none') {
|
|
88
|
+
<button type="button" className="rk-connect-button" onClick={state.openChainModal}>
|
|
89
|
+
{state.chain?.name ?? `Chain ${state.chain?.id}`}
|
|
90
|
+
</button>
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
className="rk-connect-button"
|
|
96
|
+
onClick={state.openAccountModal}
|
|
97
|
+
>{state.account.displayName}</button>
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
className="rk-connect-button"
|
|
101
|
+
disabled={disconnect.isPending}
|
|
102
|
+
onClick={() => disconnect.mutate(undefined)}
|
|
103
|
+
>
|
|
104
|
+
Disconnect
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
ConnectButton.Custom = function ConnectButtonCustom(props: {
|
|
111
|
+
children: OctaneNode | ((state: ConnectButtonRendererProps) => OctaneNode);
|
|
112
|
+
}) {
|
|
113
|
+
const state = useConnectButtonState();
|
|
114
|
+
return typeof props.children === 'function' && !isChildrenBlock(props.children)
|
|
115
|
+
? props.children(state)
|
|
116
|
+
: props.children;
|
|
117
|
+
};
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useContext } from 'octane';
|
|
2
|
+
import { RainbowKitContext } from './internal';
|
|
3
|
+
|
|
4
|
+
export function useConnectModal(): {
|
|
5
|
+
connectModalOpen: boolean;
|
|
6
|
+
openConnectModal?: (event?: Event) => void;
|
|
7
|
+
} {
|
|
8
|
+
const context = useContext(RainbowKitContext);
|
|
9
|
+
return {
|
|
10
|
+
connectModalOpen: context?.activeModal?.kind === 'connect',
|
|
11
|
+
openConnectModal:
|
|
12
|
+
context?.mounted && !context.connected
|
|
13
|
+
? (event) =>
|
|
14
|
+
context.openModal('connect', event?.currentTarget as HTMLElement | null | undefined)
|
|
15
|
+
: undefined,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useAccountModal(): {
|
|
20
|
+
accountModalOpen: boolean;
|
|
21
|
+
openAccountModal?: (event?: Event) => void;
|
|
22
|
+
} {
|
|
23
|
+
const context = useContext(RainbowKitContext);
|
|
24
|
+
return {
|
|
25
|
+
accountModalOpen: context?.activeModal?.kind === 'account',
|
|
26
|
+
openAccountModal:
|
|
27
|
+
context?.mounted && context.connected
|
|
28
|
+
? (event) =>
|
|
29
|
+
context.openModal('account', event?.currentTarget as HTMLElement | null | undefined)
|
|
30
|
+
: undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useChainModal(): {
|
|
35
|
+
chainModalOpen: boolean;
|
|
36
|
+
openChainModal?: (event?: Event) => void;
|
|
37
|
+
} {
|
|
38
|
+
const context = useContext(RainbowKitContext);
|
|
39
|
+
return {
|
|
40
|
+
chainModalOpen: context?.activeModal?.kind === 'chain',
|
|
41
|
+
openChainModal:
|
|
42
|
+
context?.mounted && context.connected
|
|
43
|
+
? (event) =>
|
|
44
|
+
context.openModal('chain', event?.currentTarget as HTMLElement | null | undefined)
|
|
45
|
+
: undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { RainbowKitProvider, type RainbowKitProviderProps } from './provider.tsrx';
|
|
2
|
+
export {
|
|
3
|
+
ConnectButton,
|
|
4
|
+
type ConnectButtonProps,
|
|
5
|
+
type ConnectButtonRendererProps,
|
|
6
|
+
} from './connect-button.tsrx';
|
|
7
|
+
export { WalletButton, type WalletButtonProps } from './wallet-button.tsrx';
|
|
8
|
+
export { useAccountModal, useChainModal, useConnectModal } from './hooks';
|
|
9
|
+
export { darkTheme, lightTheme, midnightTheme, rainbowTheme, type Theme } from './theme';
|
|
10
|
+
export type { WalletDescriptor } from './internal';
|
package/src/internal.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createContext } from 'octane';
|
|
2
|
+
import type { Connector } from '@wagmi/core';
|
|
3
|
+
|
|
4
|
+
export type ModalKind = 'account' | 'chain' | 'connect';
|
|
5
|
+
export type ModalToken = string;
|
|
6
|
+
|
|
7
|
+
export type WalletDescriptor = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
connectorUid?: string;
|
|
11
|
+
unavailableReason?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type WalletEntry = WalletDescriptor & {
|
|
15
|
+
connector?: Connector;
|
|
16
|
+
identity: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type RainbowKitContextValue = {
|
|
20
|
+
activeModal: { kind: ModalKind; token: ModalToken } | null;
|
|
21
|
+
beginConnecting: () => symbol;
|
|
22
|
+
closeModal: (token?: ModalToken) => void;
|
|
23
|
+
connected: boolean;
|
|
24
|
+
connecting: boolean;
|
|
25
|
+
endConnecting: (token: symbol) => void;
|
|
26
|
+
mounted: boolean;
|
|
27
|
+
openModal: (kind: ModalKind, opener?: HTMLElement | null) => void;
|
|
28
|
+
wallets: readonly WalletEntry[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const RainbowKitContext = createContext<RainbowKitContextValue | null>(null);
|
|
32
|
+
|
|
33
|
+
type DocumentModalState = {
|
|
34
|
+
stack: ModalToken[];
|
|
35
|
+
elements: Map<ModalToken, HTMLElement>;
|
|
36
|
+
originalIsolation: Map<HTMLElement, { ariaHidden: string | null; inert: boolean }>;
|
|
37
|
+
bodyLockCount: number;
|
|
38
|
+
originalBodyOverflow: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const documentStates = new WeakMap<Document, DocumentModalState>();
|
|
42
|
+
const tokenDocuments = new Map<ModalToken, Document>();
|
|
43
|
+
|
|
44
|
+
function stateFor(document: Document): DocumentModalState {
|
|
45
|
+
let state = documentStates.get(document);
|
|
46
|
+
if (!state) {
|
|
47
|
+
state = {
|
|
48
|
+
stack: [],
|
|
49
|
+
elements: new Map(),
|
|
50
|
+
originalIsolation: new Map(),
|
|
51
|
+
bodyLockCount: 0,
|
|
52
|
+
originalBodyOverflow: '',
|
|
53
|
+
};
|
|
54
|
+
documentStates.set(document, state);
|
|
55
|
+
}
|
|
56
|
+
return state;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rememberIsolation(state: DocumentModalState, element: HTMLElement): void {
|
|
60
|
+
if (!state.originalIsolation.has(element)) {
|
|
61
|
+
state.originalIsolation.set(element, {
|
|
62
|
+
ariaHidden: element.getAttribute('aria-hidden'),
|
|
63
|
+
inert: element.inert,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function restoreIsolation(state: DocumentModalState): void {
|
|
69
|
+
for (const [element, original] of state.originalIsolation) {
|
|
70
|
+
element.inert = original.inert;
|
|
71
|
+
if (original.ariaHidden === null) element.removeAttribute('aria-hidden');
|
|
72
|
+
else element.setAttribute('aria-hidden', original.ariaHidden);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function syncModalIsolation(document: Document): void {
|
|
77
|
+
const state = stateFor(document);
|
|
78
|
+
restoreIsolation(state);
|
|
79
|
+
const topToken = state.stack.at(-1);
|
|
80
|
+
const dialog = topToken ? state.elements.get(topToken) : undefined;
|
|
81
|
+
if (!dialog) {
|
|
82
|
+
if (state.stack.length === 0) state.originalIsolation.clear();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let child: HTMLElement = dialog;
|
|
87
|
+
let parent = child.parentElement;
|
|
88
|
+
while (parent) {
|
|
89
|
+
rememberIsolation(state, child);
|
|
90
|
+
child.inert = false;
|
|
91
|
+
child.removeAttribute('aria-hidden');
|
|
92
|
+
for (const sibling of Array.from(parent.children)) {
|
|
93
|
+
if (sibling === child) continue;
|
|
94
|
+
const element = sibling as HTMLElement;
|
|
95
|
+
rememberIsolation(state, element);
|
|
96
|
+
element.inert = true;
|
|
97
|
+
element.setAttribute('aria-hidden', 'true');
|
|
98
|
+
}
|
|
99
|
+
if (parent === document.body) break;
|
|
100
|
+
child = parent;
|
|
101
|
+
parent = parent.parentElement;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function connectorIdentity(connector: Connector): string {
|
|
106
|
+
return connector.uid || `${connector.id}:${connector.name}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function pushModal(token: ModalToken, document: Document): void {
|
|
110
|
+
removeModal(token);
|
|
111
|
+
tokenDocuments.set(token, document);
|
|
112
|
+
stateFor(document).stack.push(token);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function removeModal(token: ModalToken): void {
|
|
116
|
+
const document = tokenDocuments.get(token);
|
|
117
|
+
if (!document) return;
|
|
118
|
+
const state = stateFor(document);
|
|
119
|
+
const index = state.stack.indexOf(token);
|
|
120
|
+
if (index >= 0) state.stack.splice(index, 1);
|
|
121
|
+
state.elements.delete(token);
|
|
122
|
+
tokenDocuments.delete(token);
|
|
123
|
+
syncModalIsolation(document);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isTopModal(token: ModalToken): boolean {
|
|
127
|
+
const document = tokenDocuments.get(token);
|
|
128
|
+
return document ? stateFor(document).stack.at(-1) === token : false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function registerModal(token: ModalToken, element: HTMLElement): void {
|
|
132
|
+
const document = element.ownerDocument;
|
|
133
|
+
tokenDocuments.set(token, document);
|
|
134
|
+
stateFor(document).elements.set(token, element);
|
|
135
|
+
syncModalIsolation(document);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function focusTopModal(document: Document): boolean {
|
|
139
|
+
const state = stateFor(document);
|
|
140
|
+
const token = state.stack.at(-1);
|
|
141
|
+
const element = token ? state.elements.get(token) : undefined;
|
|
142
|
+
if (element && !element.contains(document.activeElement)) element.focus();
|
|
143
|
+
return Boolean(element);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function lockBody(document: Document): void {
|
|
147
|
+
const state = stateFor(document);
|
|
148
|
+
if (state.bodyLockCount++ === 0) {
|
|
149
|
+
state.originalBodyOverflow = document.body.style.overflow;
|
|
150
|
+
document.body.style.overflow = 'hidden';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function unlockBody(document: Document): void {
|
|
155
|
+
const state = stateFor(document);
|
|
156
|
+
if (state.bodyLockCount === 0) return;
|
|
157
|
+
if (--state.bodyLockCount === 0) document.body.style.overflow = state.originalBodyOverflow;
|
|
158
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'octane';
|
|
2
|
+
import { useConnect, useConnection, useDisconnect, useSwitchChain } from '@octanejs/wagmi';
|
|
3
|
+
import type { OctaneNode } from 'octane';
|
|
4
|
+
import type { Connector } from '@wagmi/core';
|
|
5
|
+
import {
|
|
6
|
+
RainbowKitContext,
|
|
7
|
+
connectorIdentity,
|
|
8
|
+
focusTopModal,
|
|
9
|
+
isTopModal,
|
|
10
|
+
lockBody,
|
|
11
|
+
pushModal,
|
|
12
|
+
registerModal,
|
|
13
|
+
removeModal,
|
|
14
|
+
unlockBody,
|
|
15
|
+
type ModalKind,
|
|
16
|
+
type ModalToken,
|
|
17
|
+
type WalletDescriptor,
|
|
18
|
+
type WalletEntry,
|
|
19
|
+
} from './internal';
|
|
20
|
+
import { lightTheme, type Theme } from './theme';
|
|
21
|
+
import './styles.css';
|
|
22
|
+
|
|
23
|
+
export type RainbowKitProviderProps = {
|
|
24
|
+
children?: OctaneNode;
|
|
25
|
+
theme?: Theme;
|
|
26
|
+
wallets?: readonly WalletDescriptor[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function themeStyle(theme: Theme): Record<string, string> {
|
|
30
|
+
return {
|
|
31
|
+
'--rk-accent': theme.colors.accentColor,
|
|
32
|
+
'--rk-accent-foreground': theme.colors.accentColorForeground,
|
|
33
|
+
'--rk-connect-background': theme.colors.connectButtonBackground,
|
|
34
|
+
'--rk-connect-text': theme.colors.connectButtonText,
|
|
35
|
+
'--rk-modal-background': theme.colors.modalBackground,
|
|
36
|
+
'--rk-modal-text': theme.colors.modalText,
|
|
37
|
+
'--rk-font-body': theme.fonts.body,
|
|
38
|
+
'--rk-action-radius': theme.radii.actionButton,
|
|
39
|
+
'--rk-connect-radius': theme.radii.connectButton,
|
|
40
|
+
'--rk-modal-radius': theme.radii.modal,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function walletEntries(
|
|
45
|
+
connectors: readonly Connector[],
|
|
46
|
+
descriptors: readonly WalletDescriptor[],
|
|
47
|
+
): readonly WalletEntry[] {
|
|
48
|
+
const byIdentity = new Map(connectors.map(
|
|
49
|
+
(connector) => [connectorIdentity(connector), connector],
|
|
50
|
+
));
|
|
51
|
+
const consumed = new Set<string>();
|
|
52
|
+
const entries: WalletEntry[] = [];
|
|
53
|
+
for (const descriptor of descriptors) {
|
|
54
|
+
const fallback = connectors.find(
|
|
55
|
+
(connector) => connector.id === descriptor.id || connector.name === descriptor.name,
|
|
56
|
+
);
|
|
57
|
+
const connector = descriptor.connectorUid
|
|
58
|
+
? byIdentity.get(descriptor.connectorUid) ?? fallback
|
|
59
|
+
: fallback;
|
|
60
|
+
const identity =
|
|
61
|
+
connector ? connectorIdentity(connector) : descriptor.connectorUid || descriptor.id;
|
|
62
|
+
if (consumed.has(identity)) continue;
|
|
63
|
+
consumed.add(identity);
|
|
64
|
+
entries.push({ ...descriptor, connector, identity });
|
|
65
|
+
}
|
|
66
|
+
for (const connector of connectors) {
|
|
67
|
+
const identity = connectorIdentity(connector);
|
|
68
|
+
if (consumed.has(identity)) continue;
|
|
69
|
+
consumed.add(identity);
|
|
70
|
+
entries.push({ id: connector.id, name: connector.name, connector, identity });
|
|
71
|
+
}
|
|
72
|
+
return entries;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function errorMessage(error: unknown): string {
|
|
76
|
+
return error instanceof Error ? error.message : 'The wallet request failed.';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function Modal(props: {
|
|
80
|
+
kind: ModalKind;
|
|
81
|
+
token: ModalToken;
|
|
82
|
+
wallets: readonly WalletEntry[];
|
|
83
|
+
close: (token: ModalToken) => void;
|
|
84
|
+
titleId: string;
|
|
85
|
+
descriptionId: string;
|
|
86
|
+
beginConnecting: () => symbol;
|
|
87
|
+
endConnecting: (token: symbol) => void;
|
|
88
|
+
opener: HTMLElement | null;
|
|
89
|
+
}) @{
|
|
90
|
+
const connection = useConnection();
|
|
91
|
+
const connect = useConnect();
|
|
92
|
+
const disconnect = useDisconnect();
|
|
93
|
+
const switchChain = useSwitchChain();
|
|
94
|
+
const [pendingWalletIdentity, setPendingWalletIdentity] = useState<string | null>(null);
|
|
95
|
+
const dialogRef = useRef<HTMLElement | null>(null);
|
|
96
|
+
const focusScope = props.opener?.closest<HTMLElement>('[data-rk]');
|
|
97
|
+
const restoreFocus = () => {
|
|
98
|
+
const ownerDocument = props.opener?.ownerDocument ?? dialogRef.current?.ownerDocument;
|
|
99
|
+
if (!ownerDocument) return;
|
|
100
|
+
queueMicrotask(() => {
|
|
101
|
+
if (focusTopModal(ownerDocument)) return;
|
|
102
|
+
const fallback = focusScope?.querySelector<HTMLElement>(
|
|
103
|
+
'.rk-connect-button:not([disabled]), .rk-wallet-button:not([disabled])',
|
|
104
|
+
);
|
|
105
|
+
((props.opener?.isConnected ? props.opener : fallback))?.focus();
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
const title =
|
|
109
|
+
props.kind === 'connect'
|
|
110
|
+
? 'Connect a wallet'
|
|
111
|
+
: props.kind === 'chain'
|
|
112
|
+
? 'Switch network'
|
|
113
|
+
: 'Account';
|
|
114
|
+
const description =
|
|
115
|
+
props.kind === 'connect'
|
|
116
|
+
? 'Choose an available wallet.'
|
|
117
|
+
: props.kind === 'chain'
|
|
118
|
+
? 'Choose a network for the connected wallet.'
|
|
119
|
+
: 'Manage the connected wallet.';
|
|
120
|
+
|
|
121
|
+
useLayoutEffect(() => {
|
|
122
|
+
const dialog = dialogRef.current;
|
|
123
|
+
if (!dialog) return;
|
|
124
|
+
const ownerDocument = dialog.ownerDocument;
|
|
125
|
+
pushModal(props.token, ownerDocument);
|
|
126
|
+
lockBody(ownerDocument);
|
|
127
|
+
registerModal(props.token, dialog);
|
|
128
|
+
const focusable = () => Array.from(
|
|
129
|
+
dialog.querySelectorAll<HTMLElement>(
|
|
130
|
+
'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
focusable()[0]?.focus();
|
|
134
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
135
|
+
if (!isTopModal(props.token)) return;
|
|
136
|
+
if (event.key === 'Escape') {
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
props.close(props.token);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (event.key !== 'Tab') return;
|
|
142
|
+
const items = focusable();
|
|
143
|
+
if (items.length === 0) {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
dialog.focus();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const first = items[0]!;
|
|
149
|
+
const last = items.at(-1)!;
|
|
150
|
+
if (
|
|
151
|
+
!dialog.contains(ownerDocument.activeElement) ||
|
|
152
|
+
!items.includes(ownerDocument.activeElement as HTMLElement)
|
|
153
|
+
) {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
((event.shiftKey ? last : first)).focus();
|
|
156
|
+
} else if (event.shiftKey && ownerDocument.activeElement === first) {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
last.focus();
|
|
159
|
+
} else if (!event.shiftKey && ownerDocument.activeElement === last) {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
first.focus();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
ownerDocument.addEventListener('keydown', onKeyDown);
|
|
165
|
+
return () => {
|
|
166
|
+
ownerDocument.removeEventListener('keydown', onKeyDown);
|
|
167
|
+
removeModal(props.token);
|
|
168
|
+
unlockBody(ownerDocument);
|
|
169
|
+
restoreFocus();
|
|
170
|
+
};
|
|
171
|
+
}, [props.token]);
|
|
172
|
+
|
|
173
|
+
const connectWallet = (entry: WalletEntry) => {
|
|
174
|
+
if (!entry.connector) return;
|
|
175
|
+
const pendingToken = props.beginConnecting();
|
|
176
|
+
setPendingWalletIdentity(entry.identity);
|
|
177
|
+
void connect.connectAsync({ connector: entry.connector }).then(() => {
|
|
178
|
+
props.close(props.token);
|
|
179
|
+
restoreFocus();
|
|
180
|
+
}).catch(() => {}).finally(() => {
|
|
181
|
+
setPendingWalletIdentity(null);
|
|
182
|
+
props.endConnecting(pendingToken);
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
<div
|
|
187
|
+
className="rk-overlay"
|
|
188
|
+
data-rk-overlay=""
|
|
189
|
+
onClick={(event) => {
|
|
190
|
+
if (event.target === event.currentTarget && isTopModal(props.token)) props.close(props.token);
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
<section
|
|
194
|
+
className="rk-dialog"
|
|
195
|
+
ref={dialogRef}
|
|
196
|
+
role="dialog"
|
|
197
|
+
aria-modal="true"
|
|
198
|
+
aria-labelledby={props.titleId}
|
|
199
|
+
aria-describedby={props.descriptionId}
|
|
200
|
+
tabIndex={-1}
|
|
201
|
+
>
|
|
202
|
+
<header className="rk-dialog-header">
|
|
203
|
+
<h2 id={props.titleId}>{title}</h2>
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
className="rk-close"
|
|
207
|
+
aria-label="Close"
|
|
208
|
+
onClick={() => props.close(props.token)}
|
|
209
|
+
>
|
|
210
|
+
×
|
|
211
|
+
</button>
|
|
212
|
+
</header>
|
|
213
|
+
<p id={props.descriptionId}>{description}</p>
|
|
214
|
+
<div role="status" aria-live="polite">
|
|
215
|
+
{connect.isPending
|
|
216
|
+
? 'Waiting for wallet approval'
|
|
217
|
+
: switchChain.isPending
|
|
218
|
+
? 'Switching network'
|
|
219
|
+
: disconnect.isPending
|
|
220
|
+
? 'Disconnecting wallet'
|
|
221
|
+
: ''}
|
|
222
|
+
</div>
|
|
223
|
+
@if (props.kind === 'connect') {
|
|
224
|
+
<>
|
|
225
|
+
@if (props.wallets.length === 0) {
|
|
226
|
+
<p>No compatible wallets are available.</p>
|
|
227
|
+
} @else {
|
|
228
|
+
<ul className="rk-list">
|
|
229
|
+
@for (const wallet of props.wallets; key wallet.identity) {
|
|
230
|
+
<li>
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
className="rk-action"
|
|
234
|
+
disabled={connect.isPending || !wallet.connector}
|
|
235
|
+
onClick={() => connectWallet(wallet)}
|
|
236
|
+
>
|
|
237
|
+
{connect.isPending && pendingWalletIdentity === wallet.identity
|
|
238
|
+
? `Waiting for ${wallet.name}`
|
|
239
|
+
: wallet.name}
|
|
240
|
+
</button>
|
|
241
|
+
@if (!wallet.connector) {
|
|
242
|
+
<p>{wallet.unavailableReason ?? 'Wallet is unavailable in this browser.'}</p>
|
|
243
|
+
}
|
|
244
|
+
</li>
|
|
245
|
+
}
|
|
246
|
+
</ul>
|
|
247
|
+
}
|
|
248
|
+
@if (connect.error) {
|
|
249
|
+
<p role="alert">{errorMessage(connect.error)}</p>
|
|
250
|
+
}
|
|
251
|
+
</>
|
|
252
|
+
} @else {
|
|
253
|
+
@if (props.kind === 'chain') {
|
|
254
|
+
<>
|
|
255
|
+
<ul className="rk-list">
|
|
256
|
+
@for (const chain of switchChain.chains; key chain.id) {
|
|
257
|
+
<li>
|
|
258
|
+
<button
|
|
259
|
+
type="button"
|
|
260
|
+
className="rk-action"
|
|
261
|
+
disabled={switchChain.isPending || connection.chainId === chain.id}
|
|
262
|
+
onClick={() => switchChain.mutate({ chainId: chain.id }, {
|
|
263
|
+
onSuccess: () => props.close(props.token),
|
|
264
|
+
})}
|
|
265
|
+
>{chain.name}</button>
|
|
266
|
+
</li>
|
|
267
|
+
}
|
|
268
|
+
</ul>
|
|
269
|
+
@if (switchChain.error) {
|
|
270
|
+
<p role="alert">{errorMessage(switchChain.error)}</p>
|
|
271
|
+
}
|
|
272
|
+
</>
|
|
273
|
+
} @else {
|
|
274
|
+
<>
|
|
275
|
+
<p>{connection.address ?? 'No connected account'}</p>
|
|
276
|
+
<button
|
|
277
|
+
type="button"
|
|
278
|
+
className="rk-action"
|
|
279
|
+
disabled={disconnect.isPending || connection.status !== 'connected'}
|
|
280
|
+
onClick={() => disconnect.mutate(undefined, {
|
|
281
|
+
onSuccess: () => props.close(props.token),
|
|
282
|
+
})}
|
|
283
|
+
>
|
|
284
|
+
Disconnect
|
|
285
|
+
</button>
|
|
286
|
+
@if (disconnect.error) {
|
|
287
|
+
<p role="alert">{errorMessage(disconnect.error)}</p>
|
|
288
|
+
}
|
|
289
|
+
</>
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
</section>
|
|
293
|
+
</div>
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function RainbowKitProvider(props: RainbowKitProviderProps) @{
|
|
297
|
+
const [activeModal, setActiveModal] = useState<
|
|
298
|
+
| {
|
|
299
|
+
kind: ModalKind;
|
|
300
|
+
token: ModalToken;
|
|
301
|
+
chainId: number | undefined;
|
|
302
|
+
opener: HTMLElement | null;
|
|
303
|
+
ownerDocument: Document | null;
|
|
304
|
+
}
|
|
305
|
+
| null
|
|
306
|
+
>(null);
|
|
307
|
+
const connection = useConnection();
|
|
308
|
+
const [mounted, setMounted] = useState(false);
|
|
309
|
+
const connectingTokens = useRef(new Set<symbol>());
|
|
310
|
+
const modalGeneration = useRef(0);
|
|
311
|
+
const [connectingCount, setConnectingCount] = useState(0);
|
|
312
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
313
|
+
useEffect(() => setMounted(true), []);
|
|
314
|
+
const beginConnecting = useCallback(() => {
|
|
315
|
+
const token = Symbol('rainbowkit-connect');
|
|
316
|
+
connectingTokens.current.add(token);
|
|
317
|
+
setConnectingCount(connectingTokens.current.size);
|
|
318
|
+
return token;
|
|
319
|
+
}, []);
|
|
320
|
+
const endConnecting = useCallback((token: symbol) => {
|
|
321
|
+
if (connectingTokens.current.delete(token)) {
|
|
322
|
+
setConnectingCount(connectingTokens.current.size);
|
|
323
|
+
}
|
|
324
|
+
}, []);
|
|
325
|
+
const connectors = useConnect().connectors;
|
|
326
|
+
const wallets = useMemo(() => walletEntries(connectors, props.wallets ?? []), [
|
|
327
|
+
connectors,
|
|
328
|
+
props.wallets,
|
|
329
|
+
]);
|
|
330
|
+
const selectedTheme = props.theme ?? lightTheme();
|
|
331
|
+
const providerId = useId();
|
|
332
|
+
const closeModal = useCallback((token?: ModalToken) => {
|
|
333
|
+
setActiveModal((current) => {
|
|
334
|
+
if (!current || token && current.token !== token) return current;
|
|
335
|
+
return null;
|
|
336
|
+
});
|
|
337
|
+
}, []);
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
if (!activeModal) return;
|
|
340
|
+
const modalNoLongerApplies =
|
|
341
|
+
activeModal.kind === 'connect'
|
|
342
|
+
? connection.status === 'connected'
|
|
343
|
+
: connection.status !== 'connected' ||
|
|
344
|
+
activeModal.kind === 'chain' && activeModal.chainId !== connection.chainId;
|
|
345
|
+
if (modalNoLongerApplies) closeModal(activeModal.token);
|
|
346
|
+
}, [activeModal, closeModal, connection.chainId, connection.status]);
|
|
347
|
+
const openModal = useCallback((kind: ModalKind, nextOpener?: HTMLElement | null) => {
|
|
348
|
+
const token = `${providerId}:${++modalGeneration.current}`;
|
|
349
|
+
const fallbackDocument = rootRef.current?.ownerDocument;
|
|
350
|
+
const ownerDocument =
|
|
351
|
+
nextOpener?.ownerDocument ?? fallbackDocument ??
|
|
352
|
+
(typeof document === 'undefined' ? null : document);
|
|
353
|
+
setActiveModal({
|
|
354
|
+
kind,
|
|
355
|
+
token,
|
|
356
|
+
chainId: connection.chainId,
|
|
357
|
+
opener: nextOpener ?? ownerDocument?.activeElement as HTMLElement | null | undefined ?? null,
|
|
358
|
+
ownerDocument,
|
|
359
|
+
});
|
|
360
|
+
}, [connection.chainId, providerId]);
|
|
361
|
+
const context = useMemo(
|
|
362
|
+
() => ({
|
|
363
|
+
activeModal,
|
|
364
|
+
beginConnecting,
|
|
365
|
+
closeModal,
|
|
366
|
+
connected: connection.status === 'connected',
|
|
367
|
+
connecting: connectingCount > 0 || connection.status === 'connecting' ||
|
|
368
|
+
connection.status === 'reconnecting',
|
|
369
|
+
endConnecting,
|
|
370
|
+
mounted,
|
|
371
|
+
openModal,
|
|
372
|
+
wallets,
|
|
373
|
+
}),
|
|
374
|
+
[
|
|
375
|
+
activeModal,
|
|
376
|
+
beginConnecting,
|
|
377
|
+
closeModal,
|
|
378
|
+
connectingCount,
|
|
379
|
+
connection.status,
|
|
380
|
+
endConnecting,
|
|
381
|
+
mounted,
|
|
382
|
+
openModal,
|
|
383
|
+
wallets,
|
|
384
|
+
],
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
<div data-rk="" ref={rootRef} style={themeStyle(selectedTheme)}>
|
|
388
|
+
<RainbowKitContext.Provider value={context}>
|
|
389
|
+
{props.children}
|
|
390
|
+
@if (activeModal) {
|
|
391
|
+
<Modal
|
|
392
|
+
key={activeModal.token}
|
|
393
|
+
kind={activeModal.kind}
|
|
394
|
+
token={activeModal.token}
|
|
395
|
+
wallets={wallets}
|
|
396
|
+
close={closeModal}
|
|
397
|
+
titleId={`${providerId}-title`}
|
|
398
|
+
descriptionId={`${providerId}-description`}
|
|
399
|
+
beginConnecting={beginConnecting}
|
|
400
|
+
endConnecting={endConnecting}
|
|
401
|
+
opener={activeModal.opener}
|
|
402
|
+
/>
|
|
403
|
+
}
|
|
404
|
+
</RainbowKitContext.Provider>
|
|
405
|
+
</div>
|
|
406
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
[data-rk] {
|
|
2
|
+
font-family: var(--rk-font-body);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.rk-connect-button,
|
|
6
|
+
.rk-wallet-button,
|
|
7
|
+
.rk-action {
|
|
8
|
+
min-height: 44px;
|
|
9
|
+
border: 0;
|
|
10
|
+
padding: 0 16px;
|
|
11
|
+
font: inherit;
|
|
12
|
+
cursor: pointer;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.rk-connect-button,
|
|
16
|
+
.rk-wallet-button {
|
|
17
|
+
border-radius: var(--rk-connect-radius);
|
|
18
|
+
background: var(--rk-connect-background);
|
|
19
|
+
color: var(--rk-connect-text);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.rk-action {
|
|
23
|
+
border-radius: var(--rk-action-radius);
|
|
24
|
+
background: var(--rk-accent);
|
|
25
|
+
color: var(--rk-accent-foreground);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.rk-overlay {
|
|
29
|
+
position: fixed;
|
|
30
|
+
inset: 0;
|
|
31
|
+
z-index: 2147483647;
|
|
32
|
+
display: grid;
|
|
33
|
+
place-items: center;
|
|
34
|
+
background: rgb(0 0 0 / 55%);
|
|
35
|
+
padding: 16px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.rk-dialog {
|
|
39
|
+
width: min(420px, 100%);
|
|
40
|
+
max-height: min(640px, calc(100vh - 32px));
|
|
41
|
+
overflow: auto;
|
|
42
|
+
border-radius: var(--rk-modal-radius);
|
|
43
|
+
background: var(--rk-modal-background);
|
|
44
|
+
color: var(--rk-modal-text);
|
|
45
|
+
padding: 20px;
|
|
46
|
+
box-shadow: 0 16px 64px rgb(0 0 0 / 30%);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.rk-dialog-header {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: space-between;
|
|
53
|
+
gap: 16px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.rk-close {
|
|
57
|
+
min-width: 44px;
|
|
58
|
+
min-height: 44px;
|
|
59
|
+
border: 0;
|
|
60
|
+
border-radius: 999px;
|
|
61
|
+
background: transparent;
|
|
62
|
+
color: inherit;
|
|
63
|
+
font-size: 20px;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.rk-list {
|
|
68
|
+
display: grid;
|
|
69
|
+
grid-template-columns: 1fr;
|
|
70
|
+
gap: 8px;
|
|
71
|
+
padding: 0;
|
|
72
|
+
list-style: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.rk-list button {
|
|
76
|
+
width: 100%;
|
|
77
|
+
min-height: 44px;
|
|
78
|
+
text-align: left;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
82
|
+
.rk-dialog {
|
|
83
|
+
animation: rk-enter 120ms ease-out;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@keyframes rk-enter {
|
|
88
|
+
from {
|
|
89
|
+
opacity: 0;
|
|
90
|
+
transform: translateY(4px);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type Theme = {
|
|
2
|
+
colors: {
|
|
3
|
+
accentColor: string;
|
|
4
|
+
accentColorForeground: string;
|
|
5
|
+
connectButtonBackground: string;
|
|
6
|
+
connectButtonText: string;
|
|
7
|
+
modalBackground: string;
|
|
8
|
+
modalText: string;
|
|
9
|
+
};
|
|
10
|
+
fonts: { body: string };
|
|
11
|
+
radii: { actionButton: string; connectButton: string; modal: string };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ThemeOptions = {
|
|
15
|
+
accentColor?: string;
|
|
16
|
+
accentColorForeground?: string;
|
|
17
|
+
borderRadius?: 'large' | 'medium' | 'none' | 'small';
|
|
18
|
+
fontStack?: 'rounded' | 'system';
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const radii = {
|
|
22
|
+
large: '20px',
|
|
23
|
+
medium: '12px',
|
|
24
|
+
none: '0',
|
|
25
|
+
small: '8px',
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
function createTheme(dark: boolean, options: ThemeOptions = {}): Theme {
|
|
29
|
+
const radius = radii[options.borderRadius ?? 'large'];
|
|
30
|
+
return {
|
|
31
|
+
colors: {
|
|
32
|
+
accentColor: options.accentColor ?? '#0e76fd',
|
|
33
|
+
accentColorForeground: options.accentColorForeground ?? '#fff',
|
|
34
|
+
connectButtonBackground: dark ? '#1a1b1f' : '#fff',
|
|
35
|
+
connectButtonText: dark ? '#fff' : '#1a1b1f',
|
|
36
|
+
modalBackground: dark ? '#1a1b1f' : '#fff',
|
|
37
|
+
modalText: dark ? '#fff' : '#1a1b1f',
|
|
38
|
+
},
|
|
39
|
+
fonts: {
|
|
40
|
+
body:
|
|
41
|
+
options.fontStack === 'rounded'
|
|
42
|
+
? 'ui-rounded, "SF Pro Rounded", system-ui, sans-serif'
|
|
43
|
+
: 'system-ui, sans-serif',
|
|
44
|
+
},
|
|
45
|
+
radii: { actionButton: radius, connectButton: radius, modal: radius },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function lightTheme(options?: ThemeOptions): Theme {
|
|
50
|
+
return createTheme(false, options);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function darkTheme(options?: ThemeOptions): Theme {
|
|
54
|
+
return createTheme(true, options);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function midnightTheme(options?: ThemeOptions): Theme {
|
|
58
|
+
return createTheme(true, {
|
|
59
|
+
accentColor: '#fff',
|
|
60
|
+
accentColorForeground: '#1a1b1f',
|
|
61
|
+
...options,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Octane extension with a purple rounded preset; not an upstream RainbowKit export. */
|
|
66
|
+
export function rainbowTheme(options?: ThemeOptions): Theme {
|
|
67
|
+
return lightTheme({ accentColor: '#7b3fe4', fontStack: 'rounded', ...options });
|
|
68
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useConnect } from '@octanejs/wagmi';
|
|
2
|
+
import { createElement, isChildrenBlock, useContext } from 'octane';
|
|
3
|
+
import type { OctaneNode } from 'octane';
|
|
4
|
+
import { RainbowKitContext } from './internal';
|
|
5
|
+
|
|
6
|
+
export type WalletButtonProps = {
|
|
7
|
+
children?: | OctaneNode
|
|
8
|
+
| ((props: {
|
|
9
|
+
ready: boolean;
|
|
10
|
+
connect: () => void;
|
|
11
|
+
error: unknown | null;
|
|
12
|
+
}) => OctaneNode);
|
|
13
|
+
wallet: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function WalletButton(props: WalletButtonProps) {
|
|
17
|
+
const context = useContext(RainbowKitContext);
|
|
18
|
+
const mutation = useConnect();
|
|
19
|
+
const entry = context?.wallets.find(
|
|
20
|
+
(candidate) =>
|
|
21
|
+
candidate.identity === props.wallet || candidate.id === props.wallet ||
|
|
22
|
+
candidate.name === props.wallet,
|
|
23
|
+
);
|
|
24
|
+
const connector = entry?.connector;
|
|
25
|
+
const ready = Boolean(context?.mounted && connector);
|
|
26
|
+
const connect = () => {
|
|
27
|
+
if (!context?.mounted || !connector) return;
|
|
28
|
+
const token = context.beginConnecting();
|
|
29
|
+
void mutation.connectAsync({ connector }).catch(() => {}).finally(
|
|
30
|
+
() => context.endConnecting(token),
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
if (props.children != null) {
|
|
34
|
+
return typeof props.children === 'function' && !isChildrenBlock(props.children)
|
|
35
|
+
? props.children({ ready, connect, error: mutation.error })
|
|
36
|
+
: props.children;
|
|
37
|
+
}
|
|
38
|
+
return [
|
|
39
|
+
createElement(
|
|
40
|
+
'button',
|
|
41
|
+
{
|
|
42
|
+
type: 'button',
|
|
43
|
+
className: 'rk-wallet-button',
|
|
44
|
+
disabled: !ready || context?.connecting || mutation.isPending,
|
|
45
|
+
onClick: connect,
|
|
46
|
+
},
|
|
47
|
+
entry?.name ?? props.wallet,
|
|
48
|
+
),
|
|
49
|
+
mutation.error
|
|
50
|
+
? createElement(
|
|
51
|
+
'p',
|
|
52
|
+
{ role: 'alert', className: 'rk-wallet-error' },
|
|
53
|
+
mutation.error instanceof Error ? mutation.error.message : 'The wallet request failed.',
|
|
54
|
+
)
|
|
55
|
+
: null,
|
|
56
|
+
];
|
|
57
|
+
}
|
package/status.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"upstream": {
|
|
3
|
+
"package": "@rainbow-me/rainbowkit",
|
|
4
|
+
"version": "2.2.11"
|
|
5
|
+
},
|
|
6
|
+
"surface": "Octane-native RainbowKitProvider, ConnectButton and ConnectButton.Custom, WalletButton, connect/account/chain modal hooks, connector selection, account/chain actions, native accessible dialogs, and light/dark/midnight themes.",
|
|
7
|
+
"divergences": [
|
|
8
|
+
"IMPORTANT: upstream RainbowKit 2.2.11 declares wagmi ^2.9.0. This adapter intentionally consumes @octanejs/wagmi v3 and is not drop-in dependency or peer-range parity.",
|
|
9
|
+
"The React DOM and vanilla-extract implementation is replaced by native Octane TSRX, DOM events, focus/scroll containment, and CSS custom properties.",
|
|
10
|
+
"The wallet list merges optional configured descriptors with the enclosing Wagmi v3 connector list, deduplicated by canonical connector uid with explicit id/name fallback. Unavailable configured entries remain visible with a reason. RainbowKit wallet factories, vendor SDKs, and WalletConnect project configuration remain application-owned.",
|
|
11
|
+
"Authentication, recent transactions, ENS/avatar resolution, localization, cool mode, account avatars/balances, chain icons, and pixel-identical upstream themes are unsupported and their upstream props are not accepted.",
|
|
12
|
+
"rainbowTheme is an explicitly documented Octane-only purple/rounded preset; it is not an upstream RainbowKit export."
|
|
13
|
+
],
|
|
14
|
+
"ssr": "The provider and controls emit deterministic disconnected markup without browser wallet access. Connector discovery and live Wagmi state become authoritative after hydration; no hydrated UI state authorizes wallet actions.",
|
|
15
|
+
"verified": "2026-07-29",
|
|
16
|
+
"provenance": {
|
|
17
|
+
"source": "https://github.com/rainbow-me/rainbowkit",
|
|
18
|
+
"license": "MIT"
|
|
19
|
+
},
|
|
20
|
+
"supportedExports": [
|
|
21
|
+
"RainbowKitProvider",
|
|
22
|
+
"ConnectButton",
|
|
23
|
+
"ConnectButton.Custom",
|
|
24
|
+
"WalletButton",
|
|
25
|
+
"useConnectModal",
|
|
26
|
+
"useAccountModal",
|
|
27
|
+
"useChainModal",
|
|
28
|
+
"lightTheme",
|
|
29
|
+
"darkTheme",
|
|
30
|
+
"midnightTheme",
|
|
31
|
+
"rainbowTheme"
|
|
32
|
+
],
|
|
33
|
+
"unsupported": [
|
|
34
|
+
"wallet factory catalogue",
|
|
35
|
+
"authentication adapter",
|
|
36
|
+
"recent transactions",
|
|
37
|
+
"ENS and avatar resolution",
|
|
38
|
+
"localization",
|
|
39
|
+
"cool mode",
|
|
40
|
+
"pixel-identical themes",
|
|
41
|
+
"account avatars and balances",
|
|
42
|
+
"chain icons",
|
|
43
|
+
"React DOM internals"
|
|
44
|
+
]
|
|
45
|
+
}
|