@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
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "./toast.css";
3
+ import { createContext, useCallback, useContext, useRef, useState } from "react";
4
+ const ToastContext = createContext(null);
5
+ const NOOP = {
6
+ show: () => 0,
7
+ update: () => { },
8
+ dismiss: () => { },
9
+ success: () => 0,
10
+ error: () => 0,
11
+ };
12
+ /** Access the toast API. Returns a no-op (never throws) when no <ToastProvider> is
13
+ * mounted, so components like <TxButton> work with or without it. */
14
+ export function useToast() {
15
+ return useContext(ToastContext) ?? NOOP;
16
+ }
17
+ function Icon({ variant }) {
18
+ if (variant === "loading")
19
+ return _jsx("span", { className: "kit-spin" });
20
+ if (variant === "success")
21
+ return (_jsx("svg", { className: "kit-ok", width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", children: _jsx("path", { d: "M5 10l3 3 7-7", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
22
+ if (variant === "error")
23
+ return (_jsxs("svg", { className: "kit-err", width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", children: [_jsx("path", { d: "M10 6v5M10 14h.01", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("circle", { cx: "10", cy: "10", r: "8", stroke: "currentColor", strokeWidth: "1.6" })] }));
24
+ return (_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", children: [_jsx("circle", { cx: "10", cy: "10", r: "8", stroke: "currentColor", strokeWidth: "1.6" }), _jsx("path", { d: "M10 9v5M10 6h.01", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }));
25
+ }
26
+ export function ToastProvider({ children }) {
27
+ const [toasts, setToasts] = useState([]);
28
+ const nextId = useRef(1);
29
+ const timers = useRef(new Map());
30
+ const dismiss = useCallback((id) => {
31
+ setToasts((t) => t.filter((x) => x.id !== id));
32
+ const tm = timers.current.get(id);
33
+ if (tm) {
34
+ clearTimeout(tm);
35
+ timers.current.delete(id);
36
+ }
37
+ }, []);
38
+ const schedule = useCallback((id, variant, duration) => {
39
+ const prev = timers.current.get(id);
40
+ if (prev)
41
+ clearTimeout(prev);
42
+ const ms = duration ?? (variant === "loading" ? 0 : 5000);
43
+ if (ms > 0)
44
+ timers.current.set(id, setTimeout(() => dismiss(id), ms));
45
+ }, [dismiss]);
46
+ const show = useCallback((opts) => {
47
+ const id = nextId.current++;
48
+ setToasts((t) => [...t, { id, variant: "info", ...opts }]);
49
+ schedule(id, opts.variant, opts.duration);
50
+ return id;
51
+ }, [schedule]);
52
+ const update = useCallback((id, opts) => {
53
+ setToasts((t) => t.map((x) => (x.id === id ? { ...x, ...opts } : x)));
54
+ schedule(id, opts.variant, opts.duration);
55
+ }, [schedule]);
56
+ const api = {
57
+ show,
58
+ update,
59
+ dismiss,
60
+ success: (title, description) => show({ title, description, variant: "success" }),
61
+ error: (title, description) => show({ title, description, variant: "error" }),
62
+ };
63
+ return (_jsxs(ToastContext.Provider, { value: api, children: [children, _jsx("div", { className: "kit-toaster", role: "region", "aria-label": "Notifications", children: toasts.map((t) => (_jsxs("div", { className: "kit-toast", role: "status", "data-variant": t.variant ?? "info", children: [_jsx("span", { className: "kit-toast-icon", children: _jsx(Icon, { variant: t.variant ?? "info" }) }), _jsxs("div", { className: "kit-toast-body", children: [_jsx("p", { className: "kit-toast-title", children: t.title }), t.description ? _jsx("p", { className: "kit-toast-desc", children: t.description }) : null] }), _jsx("button", { className: "kit-toast-x", "aria-label": "Dismiss", onClick: () => dismiss(t.id), children: _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", children: _jsx("path", { d: "M3.5 3.5l7 7M10.5 3.5l-7 7", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }) }) })] }, t.id))) })] }));
64
+ }
@@ -0,0 +1,13 @@
1
+ import "./tx.css";
2
+ import { type ReactNode } from "react";
3
+ import { type UseTxOptions } from "./useTx";
4
+ export interface TxButtonProps extends Omit<UseTxOptions, "onSuccess" | "onError"> {
5
+ children: ReactNode;
6
+ className?: string;
7
+ pendingLabel?: ReactNode;
8
+ /** Show automatic toasts for the tx lifecycle (default true; needs <ToastProvider>). */
9
+ toast?: boolean;
10
+ onSuccess?: (hash: `0x${string}`) => void;
11
+ onError?: (error: Error) => void;
12
+ }
13
+ export declare function TxButton({ children, className, pendingLabel, toast, onSuccess, onError, ...txOpts }: TxButtonProps): import("react").JSX.Element;
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import "./tx.css";
3
+ import { useEffect, useRef } from "react";
4
+ import { useConnection } from "wagmi";
5
+ import { useTx } from "./useTx";
6
+ import { decodeRevert } from "./revert";
7
+ import { useToast } from "../toast";
8
+ import { shortHash } from "../format";
9
+ // A button that runs a contract write and reflects the whole lifecycle: disabled +
10
+ // "Confirm in wallet…" / "Confirming…" while busy, and (with a <ToastProvider>) a
11
+ // loading → success/error toast with an explorer link. The #1 boilerplate, gone.
12
+ export function TxButton({ children, className, pendingLabel, toast = true, onSuccess, onError, ...txOpts }) {
13
+ const t = useToast();
14
+ const { chain } = useConnection();
15
+ const toastId = useRef(null);
16
+ const explorer = chain?.blockExplorers?.default?.url;
17
+ const tx = useTx({
18
+ ...txOpts,
19
+ onSuccess: (hash) => {
20
+ if (toast) {
21
+ if (toastId.current != null)
22
+ t.dismiss(toastId.current);
23
+ toastId.current = null;
24
+ const desc = explorer ? (_jsx("a", { href: explorer + "/tx/" + hash, target: "_blank", rel: "noreferrer", children: "View on explorer" })) : (shortHash(hash));
25
+ t.success("Transaction confirmed", desc);
26
+ }
27
+ onSuccess?.(hash);
28
+ },
29
+ onError: (err) => {
30
+ if (toast) {
31
+ if (toastId.current != null)
32
+ t.dismiss(toastId.current);
33
+ toastId.current = null;
34
+ t.error("Transaction failed", decodeRevert(err));
35
+ }
36
+ onError?.(err);
37
+ },
38
+ });
39
+ useEffect(() => {
40
+ if (!toast)
41
+ return;
42
+ const busy = tx.status === "pending" || tx.status === "confirming";
43
+ if (busy && toastId.current == null) {
44
+ toastId.current = t.show({
45
+ variant: "loading",
46
+ title: tx.status === "pending" ? "Confirm in your wallet" : "Transaction pending",
47
+ description: "Waiting for confirmation…",
48
+ });
49
+ }
50
+ }, [tx.status, toast, t]);
51
+ const busy = tx.status === "pending" || tx.status === "confirming";
52
+ return (_jsx("button", { className: "kit-btn " + (className || ""), "data-state": tx.status, disabled: busy, onClick: () => {
53
+ toastId.current = null;
54
+ tx.send();
55
+ }, children: busy ? (pendingLabel ?? (tx.status === "pending" ? "Confirm in wallet…" : "Confirming…")) : children }));
56
+ }
@@ -0,0 +1,3 @@
1
+ export { useTx, type UseTxOptions, type UseTxResult, type TxStatus } from "./useTx";
2
+ export { TxButton, type TxButtonProps } from "./TxButton";
3
+ export { decodeRevert } from "./revert";
@@ -0,0 +1,3 @@
1
+ export { useTx } from "./useTx";
2
+ export { TxButton } from "./TxButton";
3
+ export { decodeRevert } from "./revert";
@@ -0,0 +1 @@
1
+ export declare function decodeRevert(error: unknown): string;
@@ -0,0 +1,24 @@
1
+ // Turn a viem/wagmi error into a short, human message (revert reason where
2
+ // available) instead of a wall of hex. viem errors carry `shortMessage` + a cause
3
+ // chain; we surface the most specific message we can find.
4
+ export function decodeRevert(error) {
5
+ if (!error)
6
+ return "Transaction failed";
7
+ const top = error;
8
+ if (top.shortMessage)
9
+ return top.shortMessage;
10
+ let cause = top.cause;
11
+ for (let i = 0; i < 6 && cause; i++) {
12
+ const c = cause;
13
+ if (c.shortMessage)
14
+ return c.shortMessage;
15
+ if (c.reason)
16
+ return c.reason;
17
+ cause = c.cause;
18
+ }
19
+ if (top.details)
20
+ return top.details;
21
+ if (top.message)
22
+ return top.message.split("\n")[0];
23
+ return "Transaction failed";
24
+ }
package/dist/tx/tx.css ADDED
@@ -0,0 +1,13 @@
1
+ /* Neutral default button. Override --kit-accent (or pass your own className /
2
+ target [data-state]) to restyle. Inherits the app font. */
3
+ .kit-btn{
4
+ display:inline-flex;align-items:center;justify-content:center;gap:8px;
5
+ padding:10px 16px;border:0;border-radius:11px;
6
+ font:inherit;font-weight:600;font-size:14px;cursor:pointer;
7
+ transition:opacity .12s,transform .08s;
8
+ background:var(--kit-accent,#111827);color:var(--kit-accent-fg,#fff);
9
+ }
10
+ .kit-btn:disabled{opacity:.6;cursor:default}
11
+ .kit-btn:hover:not(:disabled){opacity:.9}
12
+ .kit-btn:active:not(:disabled){transform:translateY(1px)}
13
+ @media (prefers-color-scheme:dark){.kit-btn{background:var(--kit-accent,#f5f5f7);color:var(--kit-accent-fg,#111827)}}
@@ -0,0 +1,21 @@
1
+ import type { Abi } from "viem";
2
+ export type TxStatus = "idle" | "pending" | "confirming" | "success" | "error";
3
+ export interface UseTxOptions {
4
+ address: `0x${string}`;
5
+ abi: Abi;
6
+ functionName: string;
7
+ args?: readonly unknown[];
8
+ /** Native value (wei) to send with the call. */
9
+ value?: bigint;
10
+ onSuccess?: (hash: `0x${string}`) => void;
11
+ onError?: (error: Error) => void;
12
+ }
13
+ export interface UseTxResult {
14
+ /** Submit the transaction. Optionally override the args for this send. */
15
+ send: (overrideArgs?: readonly unknown[]) => void;
16
+ status: TxStatus;
17
+ hash?: `0x${string}`;
18
+ error: Error | null;
19
+ reset: () => void;
20
+ }
21
+ export declare function useTx(opts: UseTxOptions): UseTxResult;
@@ -0,0 +1,45 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
3
+ // One hook for the whole write lifecycle: submit → pending (wallet) → confirming
4
+ // (mined) → success | error. Explicit return type keeps the .d.ts portable.
5
+ export function useTx(opts) {
6
+ const { writeContract, data: hash, isPending, error: writeError, reset: resetWrite } = useWriteContract();
7
+ const { isLoading: confirming, isSuccess, error: receiptError } = useWaitForTransactionReceipt({ hash });
8
+ const firedFor = useRef(null);
9
+ const error = (writeError ?? receiptError ?? null);
10
+ const status = error
11
+ ? "error"
12
+ : isSuccess
13
+ ? "success"
14
+ : confirming
15
+ ? "confirming"
16
+ : isPending
17
+ ? "pending"
18
+ : "idle";
19
+ useEffect(() => {
20
+ if (isSuccess && hash && firedFor.current !== hash) {
21
+ firedFor.current = hash;
22
+ opts.onSuccess?.(hash);
23
+ }
24
+ }, [isSuccess, hash, opts]);
25
+ useEffect(() => {
26
+ if (error)
27
+ opts.onError?.(error);
28
+ }, [error, opts]);
29
+ const send = useCallback((overrideArgs) => {
30
+ const variables = {
31
+ address: opts.address,
32
+ abi: opts.abi,
33
+ functionName: opts.functionName,
34
+ args: overrideArgs ?? opts.args ?? [],
35
+ };
36
+ if (opts.value !== undefined)
37
+ variables.value = opts.value;
38
+ writeContract(variables);
39
+ }, [opts, writeContract]);
40
+ const reset = useCallback(() => {
41
+ firedFor.current = null;
42
+ resetWrite();
43
+ }, [resetWrite]);
44
+ return { send, status, hash, error, reset };
45
+ }
@@ -0,0 +1 @@
1
+ export { Dialog, Card, Skeleton, Spinner, EmptyState, CopyButton, type DialogProps, type SkeletonProps, type EmptyStateProps, type CopyButtonProps, } from "./ui";
@@ -0,0 +1 @@
1
+ export { Dialog, Card, Skeleton, Spinner, EmptyState, CopyButton, } from "./ui";
package/dist/ui/ui.css ADDED
@@ -0,0 +1,23 @@
1
+ /* Neutral primitives — override the variables / class hooks to make them yours. */
2
+ .kit-dialog-root{position:fixed;inset:0;z-index:2147483200;display:flex;align-items:center;justify-content:center;font:inherit}
3
+ .kit-dialog-overlay{position:absolute;inset:0;background:rgba(17,24,39,.5);animation:kit-fade .16s ease}
4
+ .kit-dialog{position:relative;width:420px;max-width:calc(100vw - 28px);max-height:calc(100vh - 48px);overflow:auto;background:var(--kit-bg,#fff);color:var(--kit-fg,#111827);border-radius:16px;box-shadow:0 20px 60px rgba(0,0,0,.25);padding:20px;animation:kit-pop .2s cubic-bezier(.2,.9,.25,1);outline:none}
5
+ .kit-dialog-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
6
+ .kit-dialog-title{margin:0;font-size:17px;font-weight:600}
7
+ .kit-dialog-x{width:30px;height:30px;border:0;border-radius:50%;background:var(--kit-row,#f3f4f6);color:var(--kit-muted,#6b7280);cursor:pointer;display:inline-flex;align-items:center;justify-content:center}
8
+ .kit-dialog-x:hover{color:var(--kit-fg,#111827)}
9
+ .kit-card{background:var(--kit-bg,#fff);color:var(--kit-fg,#111827);border:1px solid var(--kit-border,#e5e7eb);border-radius:14px;padding:16px;font:inherit}
10
+ .kit-skeleton{display:block;background:linear-gradient(90deg,var(--kit-row,#f3f4f6) 25%,var(--kit-row-hover,#e9eaee) 37%,var(--kit-row,#f3f4f6) 63%);background-size:400% 100%;border-radius:8px;animation:kit-shimmer 1.4s ease infinite}
11
+ .kit-empty{display:flex;flex-direction:column;align-items:center;text-align:center;gap:8px;padding:32px 16px;color:var(--kit-muted,#6b7280);font:inherit}
12
+ .kit-empty-title{color:var(--kit-fg,#111827);font-size:15px;font-weight:600;margin:0}
13
+ .kit-empty-desc{font-size:14px;margin:0;max-width:34ch}
14
+ .kit-spinner{display:inline-block;box-sizing:border-box;border:2px solid var(--kit-border,#e5e7eb);border-top-color:currentColor;border-radius:50%;animation:kit-spin .7s linear infinite}
15
+ .kit-copy{display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;color:inherit;font:inherit;cursor:pointer;padding:0}
16
+ @keyframes kit-fade{from{opacity:0}to{opacity:1}}
17
+ @keyframes kit-pop{from{opacity:0;transform:translateY(6px) scale(.98)}to{opacity:1;transform:none}}
18
+ @keyframes kit-spin{to{transform:rotate(360deg)}}
19
+ @keyframes kit-shimmer{from{background-position:100% 0}to{background-position:0 0}}
20
+ @media (prefers-color-scheme:dark){.kit-dialog,.kit-card{--kit-bg:#1b1b1f;--kit-fg:#f5f5f7;--kit-border:#2c2c31;--kit-row:#27272c;--kit-row-hover:#313137}}
21
+ @media (max-width:480px){.kit-dialog-root{align-items:flex-end}.kit-dialog{width:100%;max-width:100%;border-radius:16px 16px 0 0;animation:kit-up .22s cubic-bezier(.2,.9,.25,1)}}
22
+ @keyframes kit-up{from{transform:translateY(100%)}to{transform:none}}
23
+ @media (prefers-reduced-motion:reduce){.kit-dialog,.kit-dialog-overlay,.kit-skeleton,.kit-spinner{animation:none}}
@@ -0,0 +1,40 @@
1
+ import "./ui.css";
2
+ import { type ReactNode, type CSSProperties, type HTMLAttributes } from "react";
3
+ export interface DialogProps {
4
+ open: boolean;
5
+ onClose: () => void;
6
+ title?: ReactNode;
7
+ children: ReactNode;
8
+ className?: string;
9
+ }
10
+ export declare function Dialog({ open, onClose, title, children, className }: DialogProps): import("react").JSX.Element | null;
11
+ export declare function Card({ children, className, ...rest }: {
12
+ children: ReactNode;
13
+ } & HTMLAttributes<HTMLDivElement>): import("react").JSX.Element;
14
+ export interface SkeletonProps {
15
+ width?: number | string;
16
+ height?: number | string;
17
+ radius?: number | string;
18
+ className?: string;
19
+ style?: CSSProperties;
20
+ }
21
+ export declare function Skeleton({ width, height, radius, className, style }: SkeletonProps): import("react").JSX.Element;
22
+ export declare function Spinner({ size, className }: {
23
+ size?: number;
24
+ className?: string;
25
+ }): import("react").JSX.Element;
26
+ export interface EmptyStateProps {
27
+ title: ReactNode;
28
+ description?: ReactNode;
29
+ icon?: ReactNode;
30
+ action?: ReactNode;
31
+ className?: string;
32
+ }
33
+ export declare function EmptyState({ title, description, icon, action, className }: EmptyStateProps): import("react").JSX.Element;
34
+ export interface CopyButtonProps {
35
+ value: string;
36
+ children?: ReactNode;
37
+ copiedLabel?: ReactNode;
38
+ className?: string;
39
+ }
40
+ export declare function CopyButton({ value, children, copiedLabel, className }: CopyButtonProps): import("react").JSX.Element;
package/dist/ui/ui.js ADDED
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "./ui.css";
3
+ import { useEffect, useRef, useState, } from "react";
4
+ import { cx } from "../util";
5
+ export function Dialog({ open, onClose, title, children, className }) {
6
+ const ref = useRef(null);
7
+ useEffect(() => {
8
+ if (!open)
9
+ return;
10
+ const onKey = (e) => {
11
+ if (e.key === "Escape")
12
+ onClose();
13
+ };
14
+ window.addEventListener("keydown", onKey);
15
+ const prev = document.body.style.overflow;
16
+ document.body.style.overflow = "hidden";
17
+ return () => {
18
+ window.removeEventListener("keydown", onKey);
19
+ document.body.style.overflow = prev;
20
+ };
21
+ }, [open, onClose]);
22
+ useEffect(() => {
23
+ if (open)
24
+ ref.current?.focus();
25
+ }, [open]);
26
+ if (!open)
27
+ return null;
28
+ return (_jsxs("div", { className: "kit-dialog-root", children: [_jsx("div", { className: "kit-dialog-overlay", onClick: onClose }), _jsxs("div", { className: cx("kit-dialog", className), role: "dialog", "aria-modal": "true", ref: ref, tabIndex: -1, children: [title !== undefined ? (_jsxs("div", { className: "kit-dialog-head", children: [_jsx("h2", { className: "kit-dialog-title", children: title }), _jsx("button", { className: "kit-dialog-x", "aria-label": "Close", onClick: onClose, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: _jsx("path", { d: "M4 4l8 8M12 4l-8 8", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round" }) }) })] })) : null, children] })] }));
29
+ }
30
+ // ---- Card ----------------------------------------------------------------------
31
+ export function Card({ children, className, ...rest }) {
32
+ return (_jsx("div", { className: cx("kit-card", className), ...rest, children: children }));
33
+ }
34
+ export function Skeleton({ width = "100%", height = 16, radius, className, style }) {
35
+ return _jsx("span", { className: cx("kit-skeleton", className), style: { width, height, borderRadius: radius, ...style }, "aria-hidden": "true" });
36
+ }
37
+ // ---- Spinner -------------------------------------------------------------------
38
+ export function Spinner({ size = 18, className }) {
39
+ return _jsx("span", { className: cx("kit-spinner", className), style: { width: size, height: size }, role: "status", "aria-label": "Loading" });
40
+ }
41
+ export function EmptyState({ title, description, icon, action, className }) {
42
+ return (_jsxs("div", { className: cx("kit-empty", className), children: [icon, _jsx("p", { className: "kit-empty-title", children: title }), description ? _jsx("p", { className: "kit-empty-desc", children: description }) : null, action] }));
43
+ }
44
+ export function CopyButton({ value, children, copiedLabel = "Copied", className }) {
45
+ const [copied, setCopied] = useState(false);
46
+ return (_jsx("button", { type: "button", className: cx("kit-copy", className), "data-state": copied ? "copied" : "idle", onClick: () => {
47
+ try {
48
+ navigator.clipboard.writeText(value);
49
+ setCopied(true);
50
+ setTimeout(() => setCopied(false), 1200);
51
+ }
52
+ catch {
53
+ /* clipboard unavailable */
54
+ }
55
+ }, children: copied ? copiedLabel : (children ?? "Copy") }));
56
+ }
package/dist/util.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Join class names, dropping falsy values. */
2
+ export declare const cx: (...c: (string | undefined | false | null)[]) => string;
package/dist/util.js ADDED
@@ -0,0 +1,2 @@
1
+ /** Join class names, dropping falsy values. */
2
+ export const cx = (...c) => c.filter(Boolean).join(" ");
@@ -0,0 +1,21 @@
1
+ import { type ReactNode } from "react";
2
+ import { type WalletModalClassNames } from "./WalletModal";
3
+ export interface ConnectWalletProps {
4
+ label?: string;
5
+ /** Applied to the default trigger/account button (shorthand for classNames.trigger). */
6
+ className?: string;
7
+ title?: string;
8
+ classNames?: {
9
+ trigger?: string;
10
+ account?: string;
11
+ menu?: string;
12
+ modal?: WalletModalClassNames;
13
+ };
14
+ /** Supply your OWN trigger element; the modal still works. Receives helpers. */
15
+ children?: (api: {
16
+ open: () => void;
17
+ isConnected: boolean;
18
+ address?: string;
19
+ }) => ReactNode;
20
+ }
21
+ export declare function ConnectWallet({ label, className, title, classNames, children }?: ConnectWalletProps): import("react").JSX.Element;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { useConnection, useDisconnect } from "wagmi";
4
+ import { shortAddress, avatarGradient } from "../format";
5
+ import { WalletModal } from "./WalletModal";
6
+ const cx = (...c) => c.filter(Boolean).join(" ");
7
+ // Default connect control: a (neutral) button that opens <WalletModal/>, and an
8
+ // account pill (copy / disconnect) once connected. Style it however you like —
9
+ // `className` / `classNames`, the CSS variables, or pass `children` for a fully
10
+ // custom trigger. Works out of the box, looks like whatever you make it.
11
+ export function ConnectWallet({ label, className, title, classNames, children } = {}) {
12
+ const { address, isConnected } = useConnection();
13
+ const { disconnect } = useDisconnect();
14
+ const [open, setOpen] = useState(false);
15
+ const [menu, setMenu] = useState(false);
16
+ if (children) {
17
+ return (_jsxs(_Fragment, { children: [children({ open: () => setOpen(true), isConnected, address }), _jsx(WalletModal, { open: open, onClose: () => setOpen(false), title: title, classNames: classNames?.modal })] }));
18
+ }
19
+ if (isConnected && address) {
20
+ return (_jsxs("div", { className: "wm-account-wrap", children: [_jsxs("button", { className: cx("wm-account", className, classNames?.account), "data-state": "connected", onClick: () => setMenu((m) => !m), children: [_jsx("span", { className: "wm-avatar", style: { backgroundImage: avatarGradient(address) } }), _jsx("span", { className: "wm-addr", children: shortAddress(address) })] }), menu ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "wm-menu-backdrop", onClick: () => setMenu(false) }), _jsxs("div", { className: cx("wm-menu", classNames?.menu), children: [_jsx("button", { onClick: () => {
21
+ try {
22
+ navigator.clipboard.writeText(address);
23
+ }
24
+ catch {
25
+ /* clipboard unavailable */
26
+ }
27
+ setMenu(false);
28
+ }, children: "Copy address" }), _jsx("button", { onClick: () => {
29
+ disconnect();
30
+ setMenu(false);
31
+ }, children: "Disconnect" })] })] })) : null] }));
32
+ }
33
+ return (_jsxs(_Fragment, { children: [_jsx("button", { className: cx("wm-cta", className, classNames?.trigger), "data-state": "disconnected", onClick: () => setOpen(true), children: label || "Connect Wallet" }), _jsx(WalletModal, { open: open, onClose: () => setOpen(false), title: title, classNames: classNames?.modal })] }));
34
+ }
@@ -0,0 +1,20 @@
1
+ import "./wallet.css";
2
+ export interface WalletModalClassNames {
3
+ root?: string;
4
+ overlay?: string;
5
+ sheet?: string;
6
+ header?: string;
7
+ title?: string;
8
+ close?: string;
9
+ list?: string;
10
+ wallet?: string;
11
+ }
12
+ export interface WalletModalProps {
13
+ open: boolean;
14
+ onClose: () => void;
15
+ title?: string;
16
+ onConnected?: () => void;
17
+ /** Bring your own design — extra classes merged onto each part. */
18
+ classNames?: WalletModalClassNames;
19
+ }
20
+ export declare function WalletModal({ open, onClose, title, onConnected, classNames }: WalletModalProps): import("react").JSX.Element | null;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "./wallet.css";
3
+ import { useEffect, useRef } from "react";
4
+ import { useWalletConnectors } from "./useWalletConnectors";
5
+ const cx = (...c) => c.filter(Boolean).join(" ");
6
+ // The connect MODAL. Owns structure + behaviour (focus, Esc, scroll-lock,
7
+ // responsive: centered on desktop, bottom sheet on mobile); the LOOK is yours —
8
+ // override the wallet.css variables, pass `classNames`, or target the class hooks
9
+ // / [data-*] attributes. Headless escape hatch: build your own on useWalletConnectors.
10
+ export function WalletModal({ open, onClose, title, onConnected, classNames }) {
11
+ const { wallets, connect, pendingUid, error, reset } = useWalletConnectors();
12
+ const ref = useRef(null);
13
+ useEffect(() => {
14
+ if (!open)
15
+ return;
16
+ const onKey = (e) => {
17
+ if (e.key === "Escape")
18
+ onClose();
19
+ };
20
+ window.addEventListener("keydown", onKey);
21
+ const prev = document.body.style.overflow;
22
+ document.body.style.overflow = "hidden";
23
+ return () => {
24
+ window.removeEventListener("keydown", onKey);
25
+ document.body.style.overflow = prev;
26
+ };
27
+ }, [open, onClose]);
28
+ useEffect(() => {
29
+ if (open) {
30
+ reset();
31
+ ref.current?.focus();
32
+ }
33
+ }, [open, reset]);
34
+ if (!open)
35
+ return null;
36
+ return (_jsxs("div", { className: cx("wm-root", classNames?.root), role: "presentation", "data-livo-wallet-modal": "", children: [_jsx("div", { className: cx("wm-overlay", classNames?.overlay), onClick: onClose }), _jsxs("div", { className: cx("wm-sheet", classNames?.sheet), role: "dialog", "aria-modal": "true", "aria-label": title || "Connect a wallet", ref: ref, tabIndex: -1, children: [_jsx("div", { className: "wm-handle" }), _jsxs("div", { className: cx("wm-head", classNames?.header), children: [_jsx("h2", { className: cx("wm-title", classNames?.title), children: title || "Connect Wallet" }), _jsx("button", { className: cx("wm-close", classNames?.close), "aria-label": "Close", onClick: onClose, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: _jsx("path", { d: "M4 4l8 8M12 4l-8 8", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round" }) }) })] }), error ? (_jsxs("div", { className: "wm-error", "data-state": "error", children: [_jsx("span", { children: error.message.split("\n")[0] }), _jsx("button", { onClick: () => reset(), children: "Try again" })] })) : null, _jsxs("div", { className: cx("wm-list", classNames?.list), children: [wallets.map((c) => (_jsxs("button", { className: cx("wm-wallet", classNames?.wallet), "data-state": pendingUid === c.uid ? "connecting" : "idle", disabled: pendingUid != null, onClick: () => connect(c, () => {
37
+ onConnected?.();
38
+ onClose();
39
+ }), children: [_jsx("span", { className: "wm-wname", children: c.name }), _jsxs("span", { className: "wm-wicon", children: [c.icon ? (_jsx("img", { src: c.icon, alt: "", width: 28, height: 28 })) : (_jsx("span", { className: "wm-wfallback", children: (c.name || "?").charAt(0) })), pendingUid === c.uid ? _jsx("span", { className: "wm-spin" }) : null] })] }, c.uid))), wallets.length === 0 ? (_jsx("p", { className: "wm-empty", children: "No wallet detected. Install MetaMask, Rabby, or another browser wallet and reload." })) : null] }), _jsx("p", { className: "wm-foot", children: "Your wallet stays in your control \u2014 we never see your keys." })] })] }));
40
+ }
@@ -0,0 +1,3 @@
1
+ export { ConnectWallet, type ConnectWalletProps } from "./ConnectWallet";
2
+ export { WalletModal, type WalletModalProps, type WalletModalClassNames } from "./WalletModal";
3
+ export { useWalletConnectors, type WalletConnectors } from "./useWalletConnectors";
@@ -0,0 +1,3 @@
1
+ export { ConnectWallet } from "./ConnectWallet";
2
+ export { WalletModal } from "./WalletModal";
3
+ export { useWalletConnectors } from "./useWalletConnectors";
@@ -0,0 +1,16 @@
1
+ import { type Connector } from "wagmi";
2
+ export interface WalletConnectors {
3
+ /** Available wallet connectors, de-duped by name (EIP-6963 can surface dups). */
4
+ wallets: readonly Connector[];
5
+ /** Connect to a specific wallet; `onSuccess` fires once connected. */
6
+ connect: (connector: Connector, onSuccess?: () => void) => void;
7
+ /** A connect attempt is in flight. */
8
+ isPending: boolean;
9
+ /** uid of the connector currently connecting (for a per-wallet spinner), else null. */
10
+ pendingUid: string | null;
11
+ /** The last connect error, if any. */
12
+ error: Error | null;
13
+ /** Clear the error / pending state. */
14
+ reset: () => void;
15
+ }
16
+ export declare function useWalletConnectors(): WalletConnectors;
@@ -0,0 +1,31 @@
1
+ import { useMemo } from "react";
2
+ import { useConnect, useConnectors } from "wagmi";
3
+ // The headless core of the wallet modal — a clean, self-owned surface over wagmi's
4
+ // useConnect/useConnectors. Build a fully custom wallet UI on this while keeping the
5
+ // wiring. The explicit return type also keeps the package's .d.ts portable (no leak
6
+ // of wagmi's deep internal types).
7
+ export function useWalletConnectors() {
8
+ const connectors = useConnectors();
9
+ const { connect, isPending, variables, error, reset } = useConnect();
10
+ const wallets = useMemo(() => {
11
+ const seen = new Set();
12
+ return connectors.filter((c) => {
13
+ const k = (c.name || "").toLowerCase();
14
+ if (!c.name || seen.has(k))
15
+ return false;
16
+ seen.add(k);
17
+ return true;
18
+ });
19
+ }, [connectors]);
20
+ const pendingUid = isPending
21
+ ? (variables?.connector?.uid ?? null)
22
+ : null;
23
+ return {
24
+ wallets,
25
+ connect: (connector, onSuccess) => connect({ connector }, onSuccess ? { onSuccess } : undefined),
26
+ isPending,
27
+ pendingUid,
28
+ error: error ?? null,
29
+ reset,
30
+ };
31
+ }