@livo-build/kit 0.1.0
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/README.md +122 -0
- package/STYLING.md +76 -0
- package/dist/contracts/createLivoContracts.d.ts +24 -0
- package/dist/contracts/createLivoContracts.js +35 -0
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/useContractValue.d.ts +11 -0
- package/dist/contracts/useContractValue.js +23 -0
- package/dist/data/index.d.ts +2 -0
- package/dist/data/index.js +2 -0
- package/dist/data/useApi.d.ts +17 -0
- package/dist/data/useApi.js +42 -0
- package/dist/data/useSubgraph.d.ts +9 -0
- package/dist/data/useSubgraph.js +32 -0
- package/dist/format.d.ts +16 -0
- package/dist/format.js +59 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +22 -0
- package/dist/provider/Web3Provider.d.ts +11 -0
- package/dist/provider/Web3Provider.js +11 -0
- package/dist/provider/index.d.ts +1 -0
- package/dist/provider/index.js +1 -0
- package/dist/toast/index.d.ts +1 -0
- package/dist/toast/index.js +1 -0
- package/dist/toast/toast.css +30 -0
- package/dist/toast/toast.d.ts +23 -0
- package/dist/toast/toast.js +64 -0
- package/dist/tx/TxButton.d.ts +13 -0
- package/dist/tx/TxButton.js +56 -0
- package/dist/tx/index.d.ts +3 -0
- package/dist/tx/index.js +3 -0
- package/dist/tx/revert.d.ts +1 -0
- package/dist/tx/revert.js +24 -0
- package/dist/tx/tx.css +13 -0
- package/dist/tx/useTx.d.ts +21 -0
- package/dist/tx/useTx.js +45 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/ui.css +23 -0
- package/dist/ui/ui.d.ts +40 -0
- package/dist/ui/ui.js +56 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.js +2 -0
- package/dist/wallet/ConnectWallet.d.ts +21 -0
- package/dist/wallet/ConnectWallet.js +34 -0
- package/dist/wallet/WalletModal.d.ts +20 -0
- package/dist/wallet/WalletModal.js +40 -0
- package/dist/wallet/index.d.ts +3 -0
- package/dist/wallet/index.js +3 -0
- package/dist/wallet/useWalletConnectors.d.ts +16 -0
- package/dist/wallet/useWalletConnectors.js +31 -0
- package/dist/wallet/wallet.css +64 -0
- package/dist/web3/Address.d.ts +12 -0
- package/dist/web3/Address.js +25 -0
- package/dist/web3/Balance.d.ts +14 -0
- package/dist/web3/Balance.js +22 -0
- package/dist/web3/NetworkGuard.d.ts +12 -0
- package/dist/web3/NetworkGuard.js +15 -0
- package/dist/web3/TokenAmountInput.d.ts +13 -0
- package/dist/web3/TokenAmountInput.js +19 -0
- package/dist/web3/index.d.ts +4 -0
- package/dist/web3/index.js +4 -0
- package/dist/web3/web3.css +15 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# @livo-build/kit
|
|
2
|
+
|
|
3
|
+
Livo's frontend kit — reusable React + [wagmi](https://wagmi.sh) v3 building blocks
|
|
4
|
+
for web3 apps built on Livo. The frontend counterpart to
|
|
5
|
+
[`@livo-build/runtime`](../runtime) (the backend stdlib). Zero runtime
|
|
6
|
+
dependencies of its own; React, wagmi, viem and @tanstack/react-query are peers.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @livo-build/kit
|
|
12
|
+
# peers (a Livo scaffold already has these):
|
|
13
|
+
npm install wagmi viem @tanstack/react-query
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { LivoWeb3Provider, ConnectWallet } from "@livo-build/kit";
|
|
20
|
+
import { wagmiConfig } from "./wagmi"; // your chains + connectors (stays in your app)
|
|
21
|
+
|
|
22
|
+
export default function App() {
|
|
23
|
+
return (
|
|
24
|
+
<LivoWeb3Provider config={wagmiConfig}>
|
|
25
|
+
<ConnectWallet />
|
|
26
|
+
</LivoWeb3Provider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`ConnectWallet` is a button that opens a responsive wallet-picker **modal**
|
|
32
|
+
(centered on desktop, a bottom sheet on mobile) and turns into an account pill
|
|
33
|
+
(copy / disconnect) once connected.
|
|
34
|
+
|
|
35
|
+
## Not opinionated — style it your way
|
|
36
|
+
|
|
37
|
+
The kit owns behaviour + structure + accessibility; the **look is yours**. Defaults
|
|
38
|
+
are deliberately neutral (they inherit the app font and lean grayscale) so apps don't
|
|
39
|
+
all look the same. Restyle via CSS variables, a per-part `classNames` prop, the class
|
|
40
|
+
hooks / `[data-state]` attributes, or go fully headless on the hooks. **See
|
|
41
|
+
[STYLING.md](./STYLING.md).** Don't ship the neutral default as-is for a real product —
|
|
42
|
+
spend a moment making it match your app.
|
|
43
|
+
|
|
44
|
+
## Customize (three levels)
|
|
45
|
+
|
|
46
|
+
1. **Restyle** — override the CSS variables (`--wm-accent`, `--wm-radius`,
|
|
47
|
+
`--wm-row`, …) on `:root` or any parent. No logic touched.
|
|
48
|
+
2. **Custom trigger** — keep the modal, supply your own button:
|
|
49
|
+
```tsx
|
|
50
|
+
<ConnectWallet>
|
|
51
|
+
{({ open, isConnected, address }) => (
|
|
52
|
+
<button onClick={open}>{isConnected ? address : "Sign in"}</button>
|
|
53
|
+
)}
|
|
54
|
+
</ConnectWallet>
|
|
55
|
+
```
|
|
56
|
+
3. **Fully custom UI** — build your own on the controlled `<WalletModal open onClose/>`
|
|
57
|
+
and the headless `useWalletConnectors()` hook (deduped connectors + connect /
|
|
58
|
+
pending / error wiring).
|
|
59
|
+
|
|
60
|
+
## Transactions
|
|
61
|
+
|
|
62
|
+
The whole write lifecycle (submit → pending → confirming → success/error) in one
|
|
63
|
+
component, with toasts + an explorer link (wrap your app in `ToastProvider`):
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { ToastProvider, TxButton } from "@livo-build/kit";
|
|
67
|
+
import { addresses, abis } from "./livo/contracts"; // generated by Livo
|
|
68
|
+
|
|
69
|
+
<ToastProvider>
|
|
70
|
+
<TxButton address={addresses.Counter} abi={abis.Counter} functionName="increment">
|
|
71
|
+
Increment
|
|
72
|
+
</TxButton>
|
|
73
|
+
</ToastProvider>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Headless version: `const { send, status, hash, error } = useTx({ address, abi, functionName })`.
|
|
77
|
+
`decodeRevert(error)` turns a viem error into a short human message.
|
|
78
|
+
|
|
79
|
+
## Contracts (bound to Livo's generated bindings)
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { createLivoContracts } from "@livo-build/kit";
|
|
83
|
+
import { addresses, abis } from "./livo/contracts"; // generated by sync_contract_bindings
|
|
84
|
+
|
|
85
|
+
export const contracts = createLivoContracts({ addresses, abis });
|
|
86
|
+
|
|
87
|
+
const n = contracts.useRead<bigint>("Counter", "number"); // read (chain-aware)
|
|
88
|
+
<TxButton {...contracts.useContract("Counter")} functionName="increment">+1</TxButton> // write
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Data, web3 primitives, UI
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
const { data } = useApi<{ ok: boolean }>("/health"); // same-origin /api/*
|
|
95
|
+
const { data } = useSubgraph({ url, query: GET_ITEMS }); // your indexer's graphql_url_latest
|
|
96
|
+
|
|
97
|
+
<Address address={addr} explorer /> <Balance address={addr} />
|
|
98
|
+
<TokenAmountInput value={v} onChange={setV} decimals={18} max={bal} symbol="USDC" />
|
|
99
|
+
<NetworkGuard chainId={11155111} name="Sepolia">{/* on-chain UI */}</NetworkGuard>
|
|
100
|
+
|
|
101
|
+
<Dialog open={open} onClose={close} title="Confirm">…</Dialog>
|
|
102
|
+
<Card/> <Skeleton width={120}/> <Spinner/> <EmptyState title="Nothing yet"/> <CopyButton value={addr}/>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Exports
|
|
106
|
+
|
|
107
|
+
- **wallet** — `ConnectWallet`, `WalletModal`, `useWalletConnectors`
|
|
108
|
+
- **provider** — `LivoWeb3Provider`
|
|
109
|
+
- **contracts** — `createLivoContracts`, `useContractValue`, `resolveAddress`
|
|
110
|
+
- **tx** — `TxButton`, `useTx`, `decodeRevert`
|
|
111
|
+
- **toast** — `ToastProvider`, `useToast`
|
|
112
|
+
- **data** — `useApi`, `apiClient`, `useSubgraph`
|
|
113
|
+
- **web3** — `Address`, `Balance`, `NetworkGuard`, `TokenAmountInput`
|
|
114
|
+
- **ui** — `Dialog`, `Card`, `Skeleton`, `Spinner`, `EmptyState`, `CopyButton`
|
|
115
|
+
- **format** — `shortAddress`, `shortHash`, `formatToken`, `parseToken`, `formatUsd`, `timeAgo`, `avatarGradient`
|
|
116
|
+
|
|
117
|
+
Every component is neutral by default — **style it** (see [STYLING.md](./STYLING.md)).
|
|
118
|
+
|
|
119
|
+
## Versioning
|
|
120
|
+
|
|
121
|
+
Published from `packages/kit` via `publish-kit.yml` on merge to `main` (idempotent).
|
|
122
|
+
Livo scaffolds pin the version from `convex/lib/frontendVersion.ts` (`KIT_VERSION`).
|
package/STYLING.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Styling @livo-build/kit
|
|
2
|
+
|
|
3
|
+
The kit's components are **functional, not opinionated**. They own behaviour,
|
|
4
|
+
structure, and accessibility; the LOOK is yours. Defaults are deliberately neutral
|
|
5
|
+
(they `inherit` the app font and lean grayscale) so apps built with the kit don't
|
|
6
|
+
all look alike. Style them any of these ways, from quickest to most control.
|
|
7
|
+
|
|
8
|
+
## 1. Override CSS variables (quickest)
|
|
9
|
+
|
|
10
|
+
Every colour/radius is a variable. Set them on `:root` or any ancestor:
|
|
11
|
+
|
|
12
|
+
```css
|
|
13
|
+
:root {
|
|
14
|
+
--wm-accent: #6d28d9; /* wallet modal: primary */
|
|
15
|
+
--wm-bg: #0b0b10; /* surface */
|
|
16
|
+
--wm-radius: 12px;
|
|
17
|
+
--kit-accent: #6d28d9; /* TxButton + spinners */
|
|
18
|
+
--kit-t-bg: #0b0b10; /* toasts */
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Wallet modal vars: `--wm-accent`, `--wm-accent-fg`, `--wm-bg`, `--wm-fg`, `--wm-muted`,
|
|
23
|
+
`--wm-row`, `--wm-row-hover`, `--wm-border`, `--wm-overlay`, `--wm-radius`, `--wm-row-radius`, `--wm-shadow`.
|
|
24
|
+
Button: `--kit-accent`, `--kit-accent-fg`. Toasts: `--kit-t-bg`, `--kit-t-fg`, `--kit-t-muted`,
|
|
25
|
+
`--kit-t-border`, `--kit-t-success`, `--kit-t-error`, `--kit-t-accent`.
|
|
26
|
+
|
|
27
|
+
## 2. Pass `classNames` (bring your own design / Tailwind)
|
|
28
|
+
|
|
29
|
+
Per-part class injection — your classes are merged onto the kit's, so you can
|
|
30
|
+
replace the look entirely (e.g. Tailwind):
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
<ConnectWallet
|
|
34
|
+
classNames={{
|
|
35
|
+
trigger: "rounded-full bg-violet-600 px-5 py-2 text-white",
|
|
36
|
+
account: "rounded-full border px-3 py-1",
|
|
37
|
+
modal: { sheet: "!rounded-3xl !bg-zinc-900", wallet: "hover:!bg-zinc-800" },
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`WalletModal` parts: `root, overlay, sheet, header, title, close, list, wallet`.
|
|
43
|
+
`TxButton` takes a plain `className`.
|
|
44
|
+
|
|
45
|
+
## 3. Target class hooks + `[data-*]` states (global CSS)
|
|
46
|
+
|
|
47
|
+
Stable hooks on every element. State lives in `data-*` so you can style by state:
|
|
48
|
+
|
|
49
|
+
- Wallet: `.wm-cta` (connect btn), `.wm-account` (`[data-state="connected"]`),
|
|
50
|
+
`.wm-sheet`, `.wm-overlay`, `.wm-wallet` (`[data-state="connecting"|"idle"]`),
|
|
51
|
+
`.wm-menu`. The modal root has `[data-livo-wallet-modal]`.
|
|
52
|
+
- Tx: `.kit-btn` with `[data-state="idle|pending|confirming|success|error"]`.
|
|
53
|
+
- Toast: `.kit-toast` with `[data-variant="info|success|error|loading"]`.
|
|
54
|
+
|
|
55
|
+
```css
|
|
56
|
+
.kit-btn[data-state="confirming"] { background: theme(colors.amber.500); }
|
|
57
|
+
.kit-toast[data-variant="error"] { border-color: theme(colors.red.500); }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 4. Go fully headless (max control)
|
|
61
|
+
|
|
62
|
+
Skip the components, keep the wiring — build any UI on the hooks:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
const { wallets, connect, pendingUid, error } = useWalletConnectors();
|
|
66
|
+
const { send, status, hash, error } = useTx({ address, abi, functionName });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
These carry all the logic (connector discovery, connect flow, tx lifecycle); you
|
|
70
|
+
render 100% of the markup and styles.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
**Rule of thumb for building a real product:** don't ship the neutral default as-is —
|
|
75
|
+
spend a moment styling it to match the app (one of the four ways above). The kit is
|
|
76
|
+
there so you never hand-roll the wallet/tx/toast *logic*, not so every site looks the same.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Abi } from "viem";
|
|
2
|
+
import type { QueryResult } from "../data/useApi";
|
|
3
|
+
export interface LivoBindings {
|
|
4
|
+
addresses: Record<string, Record<string, string>>;
|
|
5
|
+
abis: Record<string, Abi>;
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveAddress(bindings: LivoBindings, name: string, chainId: number): `0x${string}` | undefined;
|
|
8
|
+
export interface ContractRef {
|
|
9
|
+
address: `0x${string}` | undefined;
|
|
10
|
+
abi: Abi;
|
|
11
|
+
}
|
|
12
|
+
export interface LivoContracts {
|
|
13
|
+
/** Resolve { address, abi } for a contract on the connected (or given) chain —
|
|
14
|
+
* spread straight into <TxButton> / useTx for writes. */
|
|
15
|
+
useContract: (name: string, chainId?: number) => ContractRef;
|
|
16
|
+
/** Read a view/pure value, address + abi resolved from the bindings. */
|
|
17
|
+
useRead: <T = unknown>(name: string, functionName: string, args?: readonly unknown[], opts?: {
|
|
18
|
+
chainId?: number;
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
}) => QueryResult<T>;
|
|
21
|
+
/** Raw address lookup (non-hook). */
|
|
22
|
+
addressOf: (name: string, chainId: number) => `0x${string}` | undefined;
|
|
23
|
+
}
|
|
24
|
+
export declare function createLivoContracts(bindings: LivoBindings): LivoContracts;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useChainId } from "wagmi";
|
|
2
|
+
import { useContractValue } from "./useContractValue";
|
|
3
|
+
export function resolveAddress(bindings, name, chainId) {
|
|
4
|
+
const a = (bindings.addresses[String(chainId)] ?? {})[name];
|
|
5
|
+
return a ? a : undefined;
|
|
6
|
+
}
|
|
7
|
+
// Bind Livo's generated contract bindings into ergonomic hooks. Mirrors the
|
|
8
|
+
// runtime's bindContracts(addresses, abis) for keepers/bots — the frontend twin.
|
|
9
|
+
//
|
|
10
|
+
// import { addresses, abis } from "./livo/contracts";
|
|
11
|
+
// export const contracts = createLivoContracts({ addresses, abis });
|
|
12
|
+
// const n = contracts.useRead<bigint>("Counter", "number");
|
|
13
|
+
// <TxButton {...contracts.useContract("Counter")} functionName="increment">+1</TxButton>
|
|
14
|
+
export function createLivoContracts(bindings) {
|
|
15
|
+
return {
|
|
16
|
+
useContract(name, chainId) {
|
|
17
|
+
const current = useChainId();
|
|
18
|
+
const cid = chainId ?? current;
|
|
19
|
+
return { address: resolveAddress(bindings, name, cid), abi: bindings.abis[name] };
|
|
20
|
+
},
|
|
21
|
+
useRead(name, functionName, args, opts) {
|
|
22
|
+
const current = useChainId();
|
|
23
|
+
const cid = opts?.chainId ?? current;
|
|
24
|
+
return useContractValue({
|
|
25
|
+
address: resolveAddress(bindings, name, cid),
|
|
26
|
+
abi: bindings.abis[name],
|
|
27
|
+
functionName,
|
|
28
|
+
args,
|
|
29
|
+
chainId: opts?.chainId,
|
|
30
|
+
enabled: opts?.enabled,
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
addressOf: (name, chainId) => resolveAddress(bindings, name, chainId),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Abi } from "viem";
|
|
2
|
+
import type { QueryResult } from "../data/useApi";
|
|
3
|
+
export interface UseContractValueParams {
|
|
4
|
+
address?: `0x${string}`;
|
|
5
|
+
abi: Abi;
|
|
6
|
+
functionName: string;
|
|
7
|
+
args?: readonly unknown[];
|
|
8
|
+
chainId?: number;
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function useContractValue<T = unknown>(params: UseContractValueParams): QueryResult<T>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useReadContract } from "wagmi";
|
|
2
|
+
// Read a contract view/pure value. A thin, explicit-typed wrapper over wagmi's
|
|
3
|
+
// useReadContract (so the .d.ts stays portable). Usually you'll use the bound
|
|
4
|
+
// `useRead` from createLivoContracts instead, which resolves address + abi for you.
|
|
5
|
+
export function useContractValue(params) {
|
|
6
|
+
const r = useReadContract({
|
|
7
|
+
address: params.address,
|
|
8
|
+
abi: params.abi,
|
|
9
|
+
functionName: params.functionName,
|
|
10
|
+
args: params.args,
|
|
11
|
+
chainId: params.chainId,
|
|
12
|
+
query: { enabled: (params.enabled ?? true) && Boolean(params.address) },
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
data: r.data,
|
|
16
|
+
error: (r.error ?? null),
|
|
17
|
+
isLoading: r.isLoading,
|
|
18
|
+
isError: r.isError,
|
|
19
|
+
refetch: () => {
|
|
20
|
+
void r.refetch();
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface QueryResult<T> {
|
|
2
|
+
data: T | undefined;
|
|
3
|
+
error: Error | null;
|
|
4
|
+
isLoading: boolean;
|
|
5
|
+
isError: boolean;
|
|
6
|
+
refetch: () => void;
|
|
7
|
+
}
|
|
8
|
+
export interface UseApiOptions {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
init?: RequestInit;
|
|
11
|
+
}
|
|
12
|
+
export declare function useApi<T = unknown>(path: string, options?: UseApiOptions): QueryResult<T>;
|
|
13
|
+
/** Imperative same-origin api client (outside React). */
|
|
14
|
+
export declare const apiClient: {
|
|
15
|
+
get: <T = unknown>(path: string) => Promise<T>;
|
|
16
|
+
post: <T = unknown>(path: string, body: unknown) => Promise<T>;
|
|
17
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
// Call a Livo same-origin api/ backend ({slug}.livo.build/api/*). Paths are
|
|
3
|
+
// resolved under /api, so `useApi("/health")` hits /api/health — zero CORS, keys
|
|
4
|
+
// stay server-side. JSON in/out; a non-2xx throws with the body's `error` if present.
|
|
5
|
+
async function apiFetch(path, init) {
|
|
6
|
+
const p = path.startsWith("/api") ? path : "/api" + (path.startsWith("/") ? path : "/" + path);
|
|
7
|
+
const res = await fetch(p, init);
|
|
8
|
+
const text = await res.text();
|
|
9
|
+
let body = null;
|
|
10
|
+
try {
|
|
11
|
+
body = text ? JSON.parse(text) : null;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
body = text;
|
|
15
|
+
}
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
const errField = body && typeof body === "object" ? body.error : undefined;
|
|
18
|
+
throw new Error(errField ? String(errField) : "HTTP " + res.status);
|
|
19
|
+
}
|
|
20
|
+
return body;
|
|
21
|
+
}
|
|
22
|
+
export function useApi(path, options) {
|
|
23
|
+
const q = useQuery({
|
|
24
|
+
queryKey: ["livo-api", path],
|
|
25
|
+
queryFn: () => apiFetch(path, options?.init),
|
|
26
|
+
enabled: options?.enabled ?? true,
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
data: q.data,
|
|
30
|
+
error: q.error ?? null,
|
|
31
|
+
isLoading: q.isLoading,
|
|
32
|
+
isError: q.isError,
|
|
33
|
+
refetch: () => {
|
|
34
|
+
void q.refetch();
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Imperative same-origin api client (outside React). */
|
|
39
|
+
export const apiClient = {
|
|
40
|
+
get: (path) => apiFetch(path),
|
|
41
|
+
post: (path, body) => apiFetch(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }),
|
|
42
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { QueryResult } from "./useApi";
|
|
2
|
+
export interface UseSubgraphArgs {
|
|
3
|
+
/** The subgraph GraphQL endpoint — your indexer's graphql_url_latest. */
|
|
4
|
+
url: string;
|
|
5
|
+
query: string;
|
|
6
|
+
variables?: Record<string, unknown>;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function useSubgraph<T = unknown>(args: UseSubgraphArgs): QueryResult<T>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
// Query a Livo indexer (Goldsky subgraph) from the frontend. Pass the project's
|
|
3
|
+
// `graphql_url_latest` (from get_indexer / a VITE_INDEXER_*_URL build var). GraphQL
|
|
4
|
+
// errors surface as a thrown Error.
|
|
5
|
+
export function useSubgraph(args) {
|
|
6
|
+
const { url, query, variables, enabled } = args;
|
|
7
|
+
const q = useQuery({
|
|
8
|
+
queryKey: ["livo-subgraph", url, query, variables],
|
|
9
|
+
enabled: (enabled ?? true) && Boolean(url),
|
|
10
|
+
queryFn: async () => {
|
|
11
|
+
const res = await fetch(url, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: { "content-type": "application/json" },
|
|
14
|
+
body: JSON.stringify({ query, variables }),
|
|
15
|
+
});
|
|
16
|
+
const json = (await res.json());
|
|
17
|
+
if (json.errors && json.errors.length) {
|
|
18
|
+
throw new Error(json.errors[0]?.message ?? "Subgraph query failed");
|
|
19
|
+
}
|
|
20
|
+
return json.data;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
data: q.data,
|
|
25
|
+
error: q.error ?? null,
|
|
26
|
+
isLoading: q.isLoading,
|
|
27
|
+
isError: q.isError,
|
|
28
|
+
refetch: () => {
|
|
29
|
+
void q.refetch();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Shorten an address to `0x1234…abcd`. */
|
|
2
|
+
export declare function shortAddress(a?: string, lead?: number, tail?: number): string;
|
|
3
|
+
/** Shorten a tx hash / hex to `0x1234…abcd` (alias of shortAddress with hash defaults). */
|
|
4
|
+
export declare function shortHash(h?: string): string;
|
|
5
|
+
/** Deterministic gradient (a stable identicon) derived from an address — no deps. */
|
|
6
|
+
export declare function avatarGradient(addr: string): string;
|
|
7
|
+
/** Format a base-unit BigInt token amount to a human string. `formatToken(1500000n, 6)` → "1.5". */
|
|
8
|
+
export declare function formatToken(amount: bigint, decimals?: number, maxFractionDigits?: number): string;
|
|
9
|
+
/** Parse a human token string into a base-unit BigInt. `parseToken("1.5", 6)` → 1500000n. */
|
|
10
|
+
export declare function parseToken(value: string, decimals?: number): bigint;
|
|
11
|
+
/** Format a number as USD. `formatUsd(1234.5)` → "$1,234.50". */
|
|
12
|
+
export declare function formatUsd(n: number, opts?: {
|
|
13
|
+
compact?: boolean;
|
|
14
|
+
}): string;
|
|
15
|
+
/** Short relative time from a unix-seconds (or ms) timestamp. `timeAgo(t)` → "3m ago". */
|
|
16
|
+
export declare function timeAgo(ts: number, nowMs?: number): string;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Small formatting helpers used across the kit (and handy in app code). Web3
|
|
2
|
+
// number formatting is error-prone (decimals, BigInt) — these get it right.
|
|
3
|
+
/** Shorten an address to `0x1234…abcd`. */
|
|
4
|
+
export function shortAddress(a, lead = 6, tail = 4) {
|
|
5
|
+
return a ? a.slice(0, lead) + "…" + a.slice(-tail) : "";
|
|
6
|
+
}
|
|
7
|
+
/** Shorten a tx hash / hex to `0x1234…abcd` (alias of shortAddress with hash defaults). */
|
|
8
|
+
export function shortHash(h) {
|
|
9
|
+
return shortAddress(h, 8, 6);
|
|
10
|
+
}
|
|
11
|
+
/** Deterministic gradient (a stable identicon) derived from an address — no deps. */
|
|
12
|
+
export function avatarGradient(addr) {
|
|
13
|
+
const a = parseInt(addr.slice(2, 8) || "0", 16) % 360;
|
|
14
|
+
const b = (parseInt(addr.slice(8, 14) || "0", 16) + 40) % 360;
|
|
15
|
+
return "linear-gradient(135deg, hsl(" + a + " 78% 60%), hsl(" + b + " 78% 52%))";
|
|
16
|
+
}
|
|
17
|
+
/** Format a base-unit BigInt token amount to a human string. `formatToken(1500000n, 6)` → "1.5". */
|
|
18
|
+
export function formatToken(amount, decimals = 18, maxFractionDigits = 6) {
|
|
19
|
+
const neg = amount < 0n;
|
|
20
|
+
const v = neg ? -amount : amount;
|
|
21
|
+
const base = 10n ** BigInt(decimals);
|
|
22
|
+
const whole = v / base;
|
|
23
|
+
let frac = (v % base).toString().padStart(decimals, "0").slice(0, maxFractionDigits).replace(/0+$/, "");
|
|
24
|
+
const sign = neg ? "-" : "";
|
|
25
|
+
return sign + whole.toString() + (frac ? "." + frac : "");
|
|
26
|
+
}
|
|
27
|
+
/** Parse a human token string into a base-unit BigInt. `parseToken("1.5", 6)` → 1500000n. */
|
|
28
|
+
export function parseToken(value, decimals = 18) {
|
|
29
|
+
const s = value.trim();
|
|
30
|
+
if (!s || !/^\d*\.?\d*$/.test(s))
|
|
31
|
+
throw new Error(`Invalid amount: "${value}"`);
|
|
32
|
+
const [whole = "0", frac = ""] = s.split(".");
|
|
33
|
+
const fracPadded = frac.slice(0, decimals).padEnd(decimals, "0");
|
|
34
|
+
return BigInt(whole || "0") * 10n ** BigInt(decimals) + BigInt(fracPadded || "0");
|
|
35
|
+
}
|
|
36
|
+
/** Format a number as USD. `formatUsd(1234.5)` → "$1,234.50". */
|
|
37
|
+
export function formatUsd(n, opts = {}) {
|
|
38
|
+
return new Intl.NumberFormat("en-US", {
|
|
39
|
+
style: "currency",
|
|
40
|
+
currency: "USD",
|
|
41
|
+
notation: opts.compact ? "compact" : "standard",
|
|
42
|
+
maximumFractionDigits: opts.compact ? 2 : undefined,
|
|
43
|
+
}).format(n);
|
|
44
|
+
}
|
|
45
|
+
/** Short relative time from a unix-seconds (or ms) timestamp. `timeAgo(t)` → "3m ago". */
|
|
46
|
+
export function timeAgo(ts, nowMs = Date.now()) {
|
|
47
|
+
const tsMs = ts > 1e12 ? ts : ts * 1000;
|
|
48
|
+
const s = Math.max(0, Math.round((nowMs - tsMs) / 1000));
|
|
49
|
+
if (s < 60)
|
|
50
|
+
return s + "s ago";
|
|
51
|
+
const m = Math.round(s / 60);
|
|
52
|
+
if (m < 60)
|
|
53
|
+
return m + "m ago";
|
|
54
|
+
const h = Math.round(m / 60);
|
|
55
|
+
if (h < 24)
|
|
56
|
+
return h + "h ago";
|
|
57
|
+
const d = Math.round(h / 24);
|
|
58
|
+
return d + "d ago";
|
|
59
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @livo-build/kit — Livo's frontend kit. Functional, NEUTRAL React + wagmi v3
|
|
2
|
+
// building blocks (you style them — see STYLING.md). Importing a component pulls in
|
|
3
|
+
// its CSS transitively (or import "@livo-build/kit/styles.css").
|
|
4
|
+
//
|
|
5
|
+
// wallet/ — ConnectWallet, WalletModal, useWalletConnectors
|
|
6
|
+
// provider/ — LivoWeb3Provider (wagmi + react-query)
|
|
7
|
+
// contracts/ — useContractValue, createLivoContracts (bound to Livo's generated bindings)
|
|
8
|
+
// tx/ — useTx, TxButton, decodeRevert
|
|
9
|
+
// toast/ — ToastProvider, useToast
|
|
10
|
+
// data/ — useApi (same-origin /api/*), useSubgraph (indexer)
|
|
11
|
+
// web3/ — Address, Balance, NetworkGuard, TokenAmountInput
|
|
12
|
+
// ui/ — Dialog, Card, Skeleton, Spinner, EmptyState, CopyButton
|
|
13
|
+
// format — shortAddress, formatToken, parseToken, formatUsd, timeAgo, …
|
|
14
|
+
export * from "./wallet";
|
|
15
|
+
export * from "./provider";
|
|
16
|
+
export * from "./contracts";
|
|
17
|
+
export * from "./tx";
|
|
18
|
+
export * from "./toast";
|
|
19
|
+
export * from "./data";
|
|
20
|
+
export * from "./web3";
|
|
21
|
+
export * from "./ui";
|
|
22
|
+
export * from "./format";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type Config } from "wagmi";
|
|
3
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
4
|
+
export interface LivoWeb3ProviderProps {
|
|
5
|
+
/** Your project's wagmi config (chains + connectors + transports). */
|
|
6
|
+
config: Config;
|
|
7
|
+
/** Bring your own QueryClient, or omit and one is created. */
|
|
8
|
+
queryClient?: QueryClient;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
export declare function LivoWeb3Provider({ config, queryClient, children }: LivoWeb3ProviderProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { WagmiProvider } from "wagmi";
|
|
4
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
5
|
+
// Wraps an app in the wagmi + react-query providers the kit's hooks need. The
|
|
6
|
+
// wagmi `config` (which chains / connectors) stays in YOUR project so it's easy to
|
|
7
|
+
// edit — pass it in here.
|
|
8
|
+
export function LivoWeb3Provider({ config, queryClient, children }) {
|
|
9
|
+
const [client] = useState(() => queryClient ?? new QueryClient());
|
|
10
|
+
return (_jsx(WagmiProvider, { config: config, children: _jsx(QueryClientProvider, { client: client, children: children }) }));
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LivoWeb3Provider, type LivoWeb3ProviderProps } from "./Web3Provider";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LivoWeb3Provider } from "./Web3Provider";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ToastProvider, useToast, type ToastApi, type ToastOptions, type ToastVariant, } from "./toast";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ToastProvider, useToast, } from "./toast";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* Neutral by default — override these CSS variables or target the class hooks /
|
|
2
|
+
[data-variant] attributes to restyle. Inherits the app font. */
|
|
3
|
+
.kit-toaster{
|
|
4
|
+
--kit-t-bg:#fff; --kit-t-fg:#111827; --kit-t-muted:#6b7280; --kit-t-border:#e5e7eb;
|
|
5
|
+
--kit-t-success:#16a34a; --kit-t-error:#dc2626; --kit-t-accent:currentColor;
|
|
6
|
+
position:fixed; z-index:2147483600; right:16px; bottom:16px; display:flex; flex-direction:column; gap:10px;
|
|
7
|
+
font-family:inherit; max-width:calc(100vw - 32px); width:360px; pointer-events:none;
|
|
8
|
+
}
|
|
9
|
+
@media (prefers-color-scheme:dark){.kit-toaster:not(.kit-light){
|
|
10
|
+
--kit-t-bg:#1f2127; --kit-t-fg:#f5f5f7; --kit-t-muted:#9b9ba4; --kit-t-border:#2c2e36;
|
|
11
|
+
}}
|
|
12
|
+
@media (max-width:480px){.kit-toaster{left:16px;right:16px;width:auto}}
|
|
13
|
+
.kit-toast{
|
|
14
|
+
pointer-events:auto; display:flex; align-items:flex-start; gap:10px; padding:12px 14px;
|
|
15
|
+
background:var(--kit-t-bg); color:var(--kit-t-fg); border:1px solid var(--kit-t-border);
|
|
16
|
+
border-radius:12px; box-shadow:0 10px 30px rgba(0,0,0,.14); animation:kit-t-in .2s cubic-bezier(.2,.9,.25,1);
|
|
17
|
+
}
|
|
18
|
+
.kit-toast-icon{flex:none;width:18px;height:18px;margin-top:1px;display:inline-flex;align-items:center;justify-content:center}
|
|
19
|
+
.kit-toast-body{flex:1;min-width:0}
|
|
20
|
+
.kit-toast-title{font-size:14px;font-weight:600;margin:0;line-height:1.35;overflow-wrap:anywhere}
|
|
21
|
+
.kit-toast-desc{font-size:13px;color:var(--kit-t-muted);margin:2px 0 0;overflow-wrap:anywhere}
|
|
22
|
+
.kit-toast-desc a{color:inherit;text-decoration:underline}
|
|
23
|
+
.kit-toast-x{flex:none;border:0;background:transparent;color:var(--kit-t-muted);cursor:pointer;padding:0;width:18px;height:18px;line-height:1}
|
|
24
|
+
.kit-toast-x:hover{color:var(--kit-t-fg)}
|
|
25
|
+
.kit-spin{width:16px;height:16px;border:2px solid transparent;border-top-color:var(--kit-t-accent);border-radius:50%;opacity:.6;animation:kit-t-spin .7s linear infinite}
|
|
26
|
+
.kit-ok{color:var(--kit-t-success)}
|
|
27
|
+
.kit-err{color:var(--kit-t-error)}
|
|
28
|
+
@keyframes kit-t-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
|
29
|
+
@keyframes kit-t-spin{to{transform:rotate(360deg)}}
|
|
30
|
+
@media (prefers-reduced-motion:reduce){.kit-toast{animation:none}}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import "./toast.css";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export type ToastVariant = "info" | "success" | "error" | "loading";
|
|
4
|
+
export interface ToastOptions {
|
|
5
|
+
title: ReactNode;
|
|
6
|
+
description?: ReactNode;
|
|
7
|
+
variant?: ToastVariant;
|
|
8
|
+
/** Auto-dismiss after N ms. 0 / undefined for loading = sticky. */
|
|
9
|
+
duration?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ToastApi {
|
|
12
|
+
show: (opts: ToastOptions) => number;
|
|
13
|
+
update: (id: number, opts: Partial<ToastOptions>) => void;
|
|
14
|
+
dismiss: (id: number) => void;
|
|
15
|
+
success: (title: ReactNode, description?: ReactNode) => number;
|
|
16
|
+
error: (title: ReactNode, description?: ReactNode) => number;
|
|
17
|
+
}
|
|
18
|
+
/** Access the toast API. Returns a no-op (never throws) when no <ToastProvider> is
|
|
19
|
+
* mounted, so components like <TxButton> work with or without it. */
|
|
20
|
+
export declare function useToast(): ToastApi;
|
|
21
|
+
export declare function ToastProvider({ children }: {
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}): import("react").JSX.Element;
|