@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.
Files changed (64) hide show
  1. package/README.md +122 -0
  2. package/STYLING.md +76 -0
  3. package/dist/contracts/createLivoContracts.d.ts +24 -0
  4. package/dist/contracts/createLivoContracts.js +35 -0
  5. package/dist/contracts/index.d.ts +2 -0
  6. package/dist/contracts/index.js +2 -0
  7. package/dist/contracts/useContractValue.d.ts +11 -0
  8. package/dist/contracts/useContractValue.js +23 -0
  9. package/dist/data/index.d.ts +2 -0
  10. package/dist/data/index.js +2 -0
  11. package/dist/data/useApi.d.ts +17 -0
  12. package/dist/data/useApi.js +42 -0
  13. package/dist/data/useSubgraph.d.ts +9 -0
  14. package/dist/data/useSubgraph.js +32 -0
  15. package/dist/format.d.ts +16 -0
  16. package/dist/format.js +59 -0
  17. package/dist/index.d.ts +9 -0
  18. package/dist/index.js +22 -0
  19. package/dist/provider/Web3Provider.d.ts +11 -0
  20. package/dist/provider/Web3Provider.js +11 -0
  21. package/dist/provider/index.d.ts +1 -0
  22. package/dist/provider/index.js +1 -0
  23. package/dist/toast/index.d.ts +1 -0
  24. package/dist/toast/index.js +1 -0
  25. package/dist/toast/toast.css +30 -0
  26. package/dist/toast/toast.d.ts +23 -0
  27. package/dist/toast/toast.js +64 -0
  28. package/dist/tx/TxButton.d.ts +13 -0
  29. package/dist/tx/TxButton.js +56 -0
  30. package/dist/tx/index.d.ts +3 -0
  31. package/dist/tx/index.js +3 -0
  32. package/dist/tx/revert.d.ts +1 -0
  33. package/dist/tx/revert.js +24 -0
  34. package/dist/tx/tx.css +13 -0
  35. package/dist/tx/useTx.d.ts +21 -0
  36. package/dist/tx/useTx.js +45 -0
  37. package/dist/ui/index.d.ts +1 -0
  38. package/dist/ui/index.js +1 -0
  39. package/dist/ui/ui.css +23 -0
  40. package/dist/ui/ui.d.ts +40 -0
  41. package/dist/ui/ui.js +56 -0
  42. package/dist/util.d.ts +2 -0
  43. package/dist/util.js +2 -0
  44. package/dist/wallet/ConnectWallet.d.ts +21 -0
  45. package/dist/wallet/ConnectWallet.js +34 -0
  46. package/dist/wallet/WalletModal.d.ts +20 -0
  47. package/dist/wallet/WalletModal.js +40 -0
  48. package/dist/wallet/index.d.ts +3 -0
  49. package/dist/wallet/index.js +3 -0
  50. package/dist/wallet/useWalletConnectors.d.ts +16 -0
  51. package/dist/wallet/useWalletConnectors.js +31 -0
  52. package/dist/wallet/wallet.css +64 -0
  53. package/dist/web3/Address.d.ts +12 -0
  54. package/dist/web3/Address.js +25 -0
  55. package/dist/web3/Balance.d.ts +14 -0
  56. package/dist/web3/Balance.js +22 -0
  57. package/dist/web3/NetworkGuard.d.ts +12 -0
  58. package/dist/web3/NetworkGuard.js +15 -0
  59. package/dist/web3/TokenAmountInput.d.ts +13 -0
  60. package/dist/web3/TokenAmountInput.js +19 -0
  61. package/dist/web3/index.d.ts +4 -0
  62. package/dist/web3/index.js +4 -0
  63. package/dist/web3/web3.css +15 -0
  64. 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,2 @@
1
+ export { useContractValue, type UseContractValueParams } from "./useContractValue";
2
+ export { createLivoContracts, resolveAddress, type LivoBindings, type LivoContracts, type ContractRef, } from "./createLivoContracts";
@@ -0,0 +1,2 @@
1
+ export { useContractValue } from "./useContractValue";
2
+ export { createLivoContracts, resolveAddress, } from "./createLivoContracts";
@@ -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,2 @@
1
+ export { useApi, apiClient, type QueryResult, type UseApiOptions } from "./useApi";
2
+ export { useSubgraph, type UseSubgraphArgs } from "./useSubgraph";
@@ -0,0 +1,2 @@
1
+ export { useApi, apiClient } from "./useApi";
2
+ export { useSubgraph } from "./useSubgraph";
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./wallet";
2
+ export * from "./provider";
3
+ export * from "./contracts";
4
+ export * from "./tx";
5
+ export * from "./toast";
6
+ export * from "./data";
7
+ export * from "./web3";
8
+ export * from "./ui";
9
+ export * from "./format";
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;