@livo-build/kit 0.1.0 → 0.1.6

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 (134) hide show
  1. package/README.md +44 -4
  2. package/dist/account/Avatar.d.ts +9 -0
  3. package/dist/account/Avatar.js +15 -0
  4. package/dist/account/Identity.d.ts +9 -0
  5. package/dist/account/Identity.js +11 -0
  6. package/dist/account/account.css +3 -0
  7. package/dist/account/gates.d.ts +18 -0
  8. package/dist/account/gates.js +21 -0
  9. package/dist/account/index.d.ts +3 -0
  10. package/dist/account/index.js +3 -0
  11. package/dist/auth/SignInWithEthereum.d.ts +8 -0
  12. package/dist/auth/SignInWithEthereum.js +18 -0
  13. package/dist/auth/index.d.ts +2 -0
  14. package/dist/auth/index.js +2 -0
  15. package/dist/auth/useSiwe.d.ts +18 -0
  16. package/dist/auth/useSiwe.js +72 -0
  17. package/dist/contracts/index.d.ts +1 -0
  18. package/dist/contracts/index.js +1 -0
  19. package/dist/contracts/useContractEvent.d.ts +10 -0
  20. package/dist/contracts/useContractEvent.js +13 -0
  21. package/dist/data/Async.d.ts +17 -0
  22. package/dist/data/Async.js +22 -0
  23. package/dist/data/DataTable.d.ts +26 -0
  24. package/dist/data/DataTable.js +15 -0
  25. package/dist/data/data.css +22 -0
  26. package/dist/data/index.d.ts +5 -0
  27. package/dist/data/index.js +5 -0
  28. package/dist/data/useCollection.d.ts +42 -0
  29. package/dist/data/useCollection.js +67 -0
  30. package/dist/data/useCollection.test.d.ts +1 -0
  31. package/dist/data/useCollection.test.js +27 -0
  32. package/dist/data/useEventSource.d.ts +15 -0
  33. package/dist/data/useEventSource.js +37 -0
  34. package/dist/data/useWebSocket.d.ts +17 -0
  35. package/dist/data/useWebSocket.js +61 -0
  36. package/dist/dataviz/Stat.d.ts +18 -0
  37. package/dist/dataviz/Stat.js +14 -0
  38. package/dist/dataviz/dataviz.css +15 -0
  39. package/dist/dataviz/index.d.ts +2 -0
  40. package/dist/dataviz/index.js +2 -0
  41. package/dist/dataviz/sparkline.d.ts +15 -0
  42. package/dist/dataviz/sparkline.js +31 -0
  43. package/dist/dataviz/sparkline.test.d.ts +1 -0
  44. package/dist/dataviz/sparkline.test.js +14 -0
  45. package/dist/format.d.ts +3 -0
  46. package/dist/format.js +9 -0
  47. package/dist/hooks/index.d.ts +20 -0
  48. package/dist/hooks/index.js +97 -0
  49. package/dist/hyperliquid/PriceTicker.d.ts +11 -0
  50. package/dist/hyperliquid/PriceTicker.js +24 -0
  51. package/dist/hyperliquid/client.d.ts +9 -0
  52. package/dist/hyperliquid/client.js +17 -0
  53. package/dist/hyperliquid/hl.css +10 -0
  54. package/dist/hyperliquid/index.d.ts +3 -0
  55. package/dist/hyperliquid/index.js +3 -0
  56. package/dist/hyperliquid/useHyperliquid.d.ts +76 -0
  57. package/dist/hyperliquid/useHyperliquid.js +52 -0
  58. package/dist/index.d.ts +10 -0
  59. package/dist/index.js +26 -5
  60. package/dist/nft/MintButton.d.ts +20 -0
  61. package/dist/nft/MintButton.js +9 -0
  62. package/dist/nft/NFTCard.d.ts +15 -0
  63. package/dist/nft/NFTCard.js +14 -0
  64. package/dist/nft/NFTMedia.d.ts +11 -0
  65. package/dist/nft/NFTMedia.js +16 -0
  66. package/dist/nft/index.d.ts +5 -0
  67. package/dist/nft/index.js +5 -0
  68. package/dist/nft/nft.css +16 -0
  69. package/dist/nft/resolveUri.d.ts +1 -0
  70. package/dist/nft/resolveUri.js +15 -0
  71. package/dist/nft/resolveUri.test.d.ts +1 -0
  72. package/dist/nft/resolveUri.test.js +13 -0
  73. package/dist/nft/useNFT.d.ts +34 -0
  74. package/dist/nft/useNFT.js +65 -0
  75. package/dist/provider/LivoApp.d.ts +13 -0
  76. package/dist/provider/LivoApp.js +16 -0
  77. package/dist/provider/index.d.ts +1 -0
  78. package/dist/provider/index.js +1 -0
  79. package/dist/telegram/LinkTelegramButton.d.ts +12 -0
  80. package/dist/telegram/LinkTelegramButton.js +19 -0
  81. package/dist/telegram/index.d.ts +5 -0
  82. package/dist/telegram/index.js +5 -0
  83. package/dist/telegram/miniapp.d.ts +15 -0
  84. package/dist/telegram/miniapp.js +63 -0
  85. package/dist/telegram/telegram.css +6 -0
  86. package/dist/telegram/useTelegramLink.d.ts +20 -0
  87. package/dist/telegram/useTelegramLink.js +114 -0
  88. package/dist/telegram/useTelegramMiniApp.d.ts +17 -0
  89. package/dist/telegram/useTelegramMiniApp.js +24 -0
  90. package/dist/telegram/useTelegramTheme.d.ts +2 -0
  91. package/dist/telegram/useTelegramTheme.js +20 -0
  92. package/dist/theme/ThemeProvider.d.ts +17 -0
  93. package/dist/theme/ThemeProvider.js +23 -0
  94. package/dist/theme/index.d.ts +1 -0
  95. package/dist/theme/index.js +1 -0
  96. package/dist/token/ApproveButton.d.ts +12 -0
  97. package/dist/token/ApproveButton.js +9 -0
  98. package/dist/token/index.d.ts +5 -0
  99. package/dist/token/index.js +5 -0
  100. package/dist/token/useAllowance.d.ts +13 -0
  101. package/dist/token/useAllowance.js +22 -0
  102. package/dist/token/useApprove.d.ts +10 -0
  103. package/dist/token/useApprove.js +14 -0
  104. package/dist/token/useToken.d.ts +7 -0
  105. package/dist/token/useToken.js +15 -0
  106. package/dist/token/useTokenGate.d.ts +17 -0
  107. package/dist/token/useTokenGate.js +24 -0
  108. package/dist/tx/TxProgress.d.ts +13 -0
  109. package/dist/tx/TxProgress.js +22 -0
  110. package/dist/tx/index.d.ts +1 -0
  111. package/dist/tx/index.js +1 -0
  112. package/dist/tx/tx.css +9 -0
  113. package/dist/ui/index.d.ts +1 -1
  114. package/dist/ui/index.js +1 -1
  115. package/dist/ui/ui.css +18 -0
  116. package/dist/ui/ui.d.ts +19 -1
  117. package/dist/ui/ui.js +31 -0
  118. package/dist/user/UserMenu.d.ts +10 -0
  119. package/dist/user/UserMenu.js +18 -0
  120. package/dist/user/index.d.ts +2 -0
  121. package/dist/user/index.js +2 -0
  122. package/dist/user/useUser.d.ts +32 -0
  123. package/dist/user/useUser.js +36 -0
  124. package/dist/user/user.css +15 -0
  125. package/dist/web3/AddressInput.d.ts +10 -0
  126. package/dist/web3/AddressInput.js +20 -0
  127. package/dist/web3/ChainSwitcher.d.ts +5 -0
  128. package/dist/web3/ChainSwitcher.js +11 -0
  129. package/dist/web3/TokenAmount.d.ts +16 -0
  130. package/dist/web3/TokenAmount.js +17 -0
  131. package/dist/web3/index.d.ts +3 -0
  132. package/dist/web3/index.js +3 -0
  133. package/dist/web3/web3.css +12 -1
  134. package/package.json +1 -1
package/README.md CHANGED
@@ -102,16 +102,56 @@ const { data } = useSubgraph({ url, query: GET_ITEMS }); // your indexer's grap
102
102
  <Card/> <Skeleton width={120}/> <Spinner/> <EmptyState title="Nothing yet"/> <CopyButton value={addr}/>
103
103
  ```
104
104
 
105
+ ## Theme it from one object
106
+
107
+ ```tsx
108
+ <KitThemeProvider theme={{ accent: "#6d28d9", radius: 16 }}>
109
+ {/* every kit component beneath inherits the brand */}
110
+ </KitThemeProvider>
111
+ ```
112
+
113
+ ## Accounts, tokens, approvals
114
+
115
+ ```tsx
116
+ <RequireConnection>{/* shown only when connected; else a connect prompt */}</RequireConnection>
117
+ <Connected><Identity address={addr} /></Connected> <Avatar address={addr} />
118
+
119
+ const { symbol, decimals } = useToken(token);
120
+ const { enough } = useAllowance({ token, owner, spender });
121
+ <ApproveButton token={token} spender={spender} amount={amt} /> // approve → toast → done
122
+ ```
123
+
124
+ ## Link a wallet to Telegram
125
+
126
+ For apps that have both a Telegram bot and a web instance — verify the same person on both.
127
+
128
+ ```tsx
129
+ <LinkTelegramButton /> // Mini App: uses verified initData; web: bot deep-link
130
+ const { status, telegram, link } = useTelegramLink(); // headless
131
+ const mini = useTelegramMiniApp(); // { available, initData, user } when open inside Telegram
132
+ ```
133
+
134
+ The hook talks to your `api/` (same-origin), which verifies both proofs with
135
+ `@livo-build/runtime` (`verifyTelegramInitData` + a signed-nonce check) and writes
136
+ the binding to D1 (`TelegramLinks`). Endpoints it expects under `endpoint` (default
137
+ `/api/telegram`): `GET /nonce`, `GET /status`, `POST /link` (Mini App), `POST /start`
138
+ (web deep-link), `POST /unlink`. (Livo's `telegram-app` blueprint scaffolds these.)
139
+
105
140
  ## Exports
106
141
 
107
142
  - **wallet** — `ConnectWallet`, `WalletModal`, `useWalletConnectors`
143
+ - **telegram** — `LinkTelegramButton`, `useTelegramLink`, `useTelegramMiniApp`
108
144
  - **provider** — `LivoWeb3Provider`
109
- - **contracts** — `createLivoContracts`, `useContractValue`, `resolveAddress`
145
+ - **contracts** — `createLivoContracts`, `useContractValue`, `useContractEvent`, `resolveAddress`
110
146
  - **tx** — `TxButton`, `useTx`, `decodeRevert`
111
147
  - **toast** — `ToastProvider`, `useToast`
112
- - **data** — `useApi`, `apiClient`, `useSubgraph`
113
- - **web3** — `Address`, `Balance`, `NetworkGuard`, `TokenAmountInput`
114
- - **ui** — `Dialog`, `Card`, `Skeleton`, `Spinner`, `EmptyState`, `CopyButton`
148
+ - **data** — `useApi`, `apiClient`, `useSubgraph`, `Async`
149
+ - **token** — `useToken`, `useAllowance`, `useApprove`, `ApproveButton`
150
+ - **account** — `Connected`, `Disconnected`, `RequireConnection`, `useIsConnected`, `Avatar`, `Identity`
151
+ - **web3** — `Address`, `Balance`, `NetworkGuard`, `TokenAmountInput`, `ChainSwitcher`, `AddressInput`
152
+ - **ui** — `Dialog`, `Card`, `Skeleton`, `Spinner`, `EmptyState`, `CopyButton`, `Button`, `Badge`
153
+ - **theme** — `KitThemeProvider`
154
+ - **hooks** — `useCopyToClipboard`, `useLocalStorage`, `useDebounce`, `useInterval`
115
155
  - **format** — `shortAddress`, `shortHash`, `formatToken`, `parseToken`, `formatUsd`, `timeAgo`, `avatarGradient`
116
156
 
117
157
  Every component is neutral by default — **style it** (see [STYLING.md](./STYLING.md)).
@@ -0,0 +1,9 @@
1
+ import "./account.css";
2
+ export interface AvatarProps {
3
+ address: `0x${string}`;
4
+ size?: number;
5
+ /** Resolve + use the ENS avatar when available. Default true. */
6
+ ens?: boolean;
7
+ className?: string;
8
+ }
9
+ export declare function Avatar({ address, size, ens, className }: AvatarProps): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import "./account.css";
3
+ import { useEnsName, useEnsAvatar } from "wagmi";
4
+ import { avatarGradient } from "../format";
5
+ import { cx } from "../util";
6
+ // An account avatar: the ENS avatar when set, otherwise a deterministic gradient
7
+ // identicon derived from the address (no image dependency). Neutral.
8
+ export function Avatar({ address, size = 32, ens = true, className }) {
9
+ const { data: name } = useEnsName({ address, query: { enabled: ens } });
10
+ const { data: url } = useEnsAvatar({ name: name ?? undefined, query: { enabled: ens && Boolean(name) } });
11
+ if (url) {
12
+ return _jsx("img", { className: cx("kit-avatar", className), src: url, width: size, height: size, alt: "", style: { borderRadius: "50%" } });
13
+ }
14
+ return (_jsx("span", { className: cx("kit-avatar", className), style: { width: size, height: size, borderRadius: "50%", background: avatarGradient(address), display: "inline-block" }, "aria-hidden": "true" }));
15
+ }
@@ -0,0 +1,9 @@
1
+ import "./account.css";
2
+ export interface IdentityProps {
3
+ address: `0x${string}`;
4
+ size?: number;
5
+ /** Resolve ENS name/avatar. Default true. */
6
+ ens?: boolean;
7
+ className?: string;
8
+ }
9
+ export declare function Identity({ address, size, ens, className }: IdentityProps): import("react").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "./account.css";
3
+ import { useEnsName } from "wagmi";
4
+ import { Avatar } from "./Avatar";
5
+ import { shortAddress } from "../format";
6
+ import { cx } from "../util";
7
+ // Avatar + name (ENS or shortened address) in a row. Neutral; style .kit-identity.
8
+ export function Identity({ address, size = 28, ens = true, className }) {
9
+ const { data: name } = useEnsName({ address, query: { enabled: ens } });
10
+ return (_jsxs("span", { className: cx("kit-identity", className), children: [_jsx(Avatar, { address: address, size: size, ens: ens }), _jsx("span", { className: "kit-identity-name", children: name || shortAddress(address) })] }));
11
+ }
@@ -0,0 +1,3 @@
1
+ .kit-avatar{flex:none;object-fit:cover}
2
+ .kit-identity{display:inline-flex;align-items:center;gap:8px;font:inherit;font-weight:500}
3
+ .kit-identity-name{font-variant-numeric:tabular-nums}
@@ -0,0 +1,18 @@
1
+ import { type ReactNode } from "react";
2
+ /** Whether a wallet is currently connected. */
3
+ export declare function useIsConnected(): boolean;
4
+ /** Render children only when a wallet is connected (else `fallback`). */
5
+ export declare function Connected({ children, fallback }: {
6
+ children: ReactNode;
7
+ fallback?: ReactNode;
8
+ }): import("react").JSX.Element;
9
+ /** Render children only when NO wallet is connected. */
10
+ export declare function Disconnected({ children }: {
11
+ children: ReactNode;
12
+ }): import("react").JSX.Element | null;
13
+ /** Gate UI behind a connection: shows the children when connected, otherwise a
14
+ * connect prompt (the kit's <ConnectWallet/> by default — pass `connect` to override). */
15
+ export declare function RequireConnection({ children, connect }: {
16
+ children: ReactNode;
17
+ connect?: ReactNode;
18
+ }): import("react").JSX.Element;
@@ -0,0 +1,21 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import {} from "react";
3
+ import { useConnection } from "wagmi";
4
+ import { ConnectWallet } from "../wallet";
5
+ /** Whether a wallet is currently connected. */
6
+ export function useIsConnected() {
7
+ return useConnection().isConnected;
8
+ }
9
+ /** Render children only when a wallet is connected (else `fallback`). */
10
+ export function Connected({ children, fallback }) {
11
+ return useIsConnected() ? _jsx(_Fragment, { children: children }) : _jsx(_Fragment, { children: fallback ?? null });
12
+ }
13
+ /** Render children only when NO wallet is connected. */
14
+ export function Disconnected({ children }) {
15
+ return useIsConnected() ? null : _jsx(_Fragment, { children: children });
16
+ }
17
+ /** Gate UI behind a connection: shows the children when connected, otherwise a
18
+ * connect prompt (the kit's <ConnectWallet/> by default — pass `connect` to override). */
19
+ export function RequireConnection({ children, connect }) {
20
+ return useIsConnected() ? _jsx(_Fragment, { children: children }) : _jsx(_Fragment, { children: connect ?? _jsx(ConnectWallet, {}) });
21
+ }
@@ -0,0 +1,3 @@
1
+ export { useIsConnected, Connected, Disconnected, RequireConnection } from "./gates";
2
+ export { Avatar, type AvatarProps } from "./Avatar";
3
+ export { Identity, type IdentityProps } from "./Identity";
@@ -0,0 +1,3 @@
1
+ export { useIsConnected, Connected, Disconnected, RequireConnection } from "./gates";
2
+ export { Avatar } from "./Avatar";
3
+ export { Identity } from "./Identity";
@@ -0,0 +1,8 @@
1
+ import "../wallet/wallet.css";
2
+ import { type ReactNode } from "react";
3
+ import { useSiwe, type UseSiweOptions } from "./useSiwe";
4
+ export interface SignInWithEthereumProps extends UseSiweOptions {
5
+ className?: string;
6
+ children?: (api: ReturnType<typeof useSiwe>) => ReactNode;
7
+ }
8
+ export declare function SignInWithEthereum({ className, children, ...opts }: SignInWithEthereumProps): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "../wallet/wallet.css";
3
+ import {} from "react";
4
+ import { useSiwe } from "./useSiwe";
5
+ import { shortAddress } from "../format";
6
+ import { cx } from "../util";
7
+ // A button that runs the Sign-In With Ethereum flow and reflects session state.
8
+ // Pass `children` for a custom render. Reuses the wallet-connect button styling.
9
+ export function SignInWithEthereum({ className, children, ...opts }) {
10
+ const siwe = useSiwe(opts);
11
+ if (children)
12
+ return _jsx(_Fragment, { children: children(siwe) });
13
+ if (siwe.status === "signed-in") {
14
+ return (_jsxs("button", { className: cx("wm-account", className), onClick: () => siwe.signOut(), children: [shortAddress(siwe.user?.address), " \u00B7 Sign out"] }));
15
+ }
16
+ const busy = siwe.status === "signing" || siwe.status === "loading";
17
+ return (_jsx("button", { className: cx("wm-cta", className), disabled: busy, onClick: () => siwe.signIn(), children: siwe.status === "signing" ? "Check your wallet…" : "Sign in with Ethereum" }));
18
+ }
@@ -0,0 +1,2 @@
1
+ export { useSiwe, type SiweState, type SiweStatus, type SiweUser, type UseSiweOptions } from "./useSiwe";
2
+ export { SignInWithEthereum, type SignInWithEthereumProps } from "./SignInWithEthereum";
@@ -0,0 +1,2 @@
1
+ export { useSiwe } from "./useSiwe";
2
+ export { SignInWithEthereum } from "./SignInWithEthereum";
@@ -0,0 +1,18 @@
1
+ export type SiweStatus = "loading" | "signed-out" | "signing" | "signed-in" | "error";
2
+ export interface SiweUser {
3
+ address: string;
4
+ [key: string]: unknown;
5
+ }
6
+ export interface SiweState {
7
+ status: SiweStatus;
8
+ user?: SiweUser;
9
+ signIn: () => Promise<void>;
10
+ signOut: () => Promise<void>;
11
+ refresh: () => void;
12
+ error?: Error;
13
+ }
14
+ export interface UseSiweOptions {
15
+ /** Base path of the SIWE auth API (default "/api/auth"). */
16
+ endpoint?: string;
17
+ }
18
+ export declare function useSiwe(options?: UseSiweOptions): SiweState;
@@ -0,0 +1,72 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { useConnection, useSignMessage } from "wagmi";
3
+ // Sign-In With Ethereum against a same-origin backend. Expects: GET {endpoint}/nonce
4
+ // → { nonce, message }, POST {endpoint}/verify { message, signature } → { user },
5
+ // GET {endpoint}/me → { user } | 401, POST {endpoint}/logout. The server builds the
6
+ // SIWE message (with createSiweMessage from @livo-build/runtime) and the session.
7
+ export function useSiwe(options = {}) {
8
+ const base = options.endpoint ?? "/api/auth";
9
+ const { address } = useConnection();
10
+ const { signMessageAsync } = useSignMessage();
11
+ const [status, setStatus] = useState("loading");
12
+ const [user, setUser] = useState();
13
+ const [error, setError] = useState();
14
+ const refresh = useCallback(async () => {
15
+ try {
16
+ const r = await fetch(`${base}/me`, { credentials: "include" });
17
+ if (r.ok) {
18
+ const j = (await r.json());
19
+ if (j.user) {
20
+ setUser(j.user);
21
+ setStatus("signed-in");
22
+ return;
23
+ }
24
+ }
25
+ setStatus("signed-out");
26
+ }
27
+ catch (e) {
28
+ setError(e);
29
+ setStatus("error");
30
+ }
31
+ }, [base]);
32
+ useEffect(() => {
33
+ void refresh();
34
+ }, [refresh]);
35
+ const signIn = useCallback(async () => {
36
+ if (!address)
37
+ return;
38
+ setStatus("signing");
39
+ setError(undefined);
40
+ try {
41
+ const nr = await fetch(`${base}/nonce?address=${address}`, { credentials: "include" });
42
+ const { message } = (await nr.json());
43
+ const signature = await signMessageAsync({ message });
44
+ const vr = await fetch(`${base}/verify`, {
45
+ method: "POST",
46
+ credentials: "include",
47
+ headers: { "content-type": "application/json" },
48
+ body: JSON.stringify({ message, signature }),
49
+ });
50
+ const j = (await vr.json());
51
+ if (!vr.ok || !j.user)
52
+ throw new Error(j.error ?? "Sign-in failed");
53
+ setUser(j.user);
54
+ setStatus("signed-in");
55
+ }
56
+ catch (e) {
57
+ setError(e);
58
+ setStatus("error");
59
+ }
60
+ }, [address, base, signMessageAsync]);
61
+ const signOut = useCallback(async () => {
62
+ try {
63
+ await fetch(`${base}/logout`, { method: "POST", credentials: "include" });
64
+ }
65
+ catch {
66
+ /* ignore */
67
+ }
68
+ setUser(undefined);
69
+ setStatus("signed-out");
70
+ }, [base]);
71
+ return { status, user, signIn, signOut, refresh: () => void refresh(), error };
72
+ }
@@ -1,2 +1,3 @@
1
1
  export { useContractValue, type UseContractValueParams } from "./useContractValue";
2
+ export { useContractEvent, type UseContractEventParams } from "./useContractEvent";
2
3
  export { createLivoContracts, resolveAddress, type LivoBindings, type LivoContracts, type ContractRef, } from "./createLivoContracts";
@@ -1,2 +1,3 @@
1
1
  export { useContractValue } from "./useContractValue";
2
+ export { useContractEvent } from "./useContractEvent";
2
3
  export { createLivoContracts, resolveAddress, } from "./createLivoContracts";
@@ -0,0 +1,10 @@
1
+ import type { Abi } from "viem";
2
+ export interface UseContractEventParams {
3
+ address?: `0x${string}`;
4
+ abi: Abi;
5
+ eventName: string;
6
+ onLogs: (logs: readonly unknown[]) => void;
7
+ chainId?: number;
8
+ enabled?: boolean;
9
+ }
10
+ export declare function useContractEvent({ address, abi, eventName, onLogs, chainId, enabled }: UseContractEventParams): void;
@@ -0,0 +1,13 @@
1
+ import { useWatchContractEvent } from "wagmi";
2
+ // Subscribe to a contract event (live). A thin wrapper over useWatchContractEvent —
3
+ // pair with createLivoContracts' useContract(name) to get { address, abi }.
4
+ export function useContractEvent({ address, abi, eventName, onLogs, chainId, enabled }) {
5
+ useWatchContractEvent({
6
+ address,
7
+ abi,
8
+ eventName,
9
+ chainId,
10
+ enabled: (enabled ?? true) && Boolean(address),
11
+ onLogs: (logs) => onLogs(logs),
12
+ });
13
+ }
@@ -0,0 +1,17 @@
1
+ import { type ReactNode } from "react";
2
+ export interface AsyncQuery<T> {
3
+ data: T | undefined;
4
+ error: Error | null;
5
+ isLoading: boolean;
6
+ }
7
+ export interface AsyncProps<T> {
8
+ query: AsyncQuery<T>;
9
+ /** Render the loaded data. */
10
+ children: (data: T) => ReactNode;
11
+ loading?: ReactNode;
12
+ error?: (error: Error) => ReactNode;
13
+ /** Shown when data is "empty" (default: [] or null/undefined). */
14
+ empty?: ReactNode;
15
+ isEmpty?: (data: T) => boolean;
16
+ }
17
+ export declare function Async<T>({ query, children, loading, error, empty, isEmpty }: AsyncProps<T>): import("react").JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import {} from "react";
3
+ import { Spinner } from "../ui";
4
+ // Render the loading / error / empty / data states of ANY kit data hook in one
5
+ // place — `<Async query={useApi("/items")}>{(items) => …}</Async>`. Neutral
6
+ // defaults (spinner, plain error); pass your own for each state.
7
+ export function Async({ query, children, loading, error, empty, isEmpty }) {
8
+ if (query.error) {
9
+ return _jsx(_Fragment, { children: error ? error(query.error) : _jsx("span", { className: "kit-async-error", children: query.error.message }) });
10
+ }
11
+ if (query.isLoading || query.data === undefined) {
12
+ return _jsx(_Fragment, { children: loading ?? _jsx(Spinner, {}) });
13
+ }
14
+ const emptyVal = isEmpty
15
+ ? isEmpty(query.data)
16
+ : Array.isArray(query.data)
17
+ ? query.data.length === 0
18
+ : query.data === null;
19
+ if (emptyVal && empty !== undefined)
20
+ return _jsx(_Fragment, { children: empty });
21
+ return _jsx(_Fragment, { children: children(query.data) });
22
+ }
@@ -0,0 +1,26 @@
1
+ import "./data.css";
2
+ import { type ReactNode } from "react";
3
+ export interface Column<T> {
4
+ /** Field key (also the default sort key) or a synthetic id. */
5
+ key: keyof T | string;
6
+ header: ReactNode;
7
+ /** Custom cell render; defaults to String(item[key]). */
8
+ render?: (item: T, index: number) => ReactNode;
9
+ /** Allow clicking the header to sort by this column's key. Default false. */
10
+ sortable?: boolean;
11
+ align?: "left" | "right" | "center";
12
+ className?: string;
13
+ }
14
+ export interface DataTableProps<T> {
15
+ data: T[];
16
+ columns: Column<T>[];
17
+ pageSize?: number;
18
+ searchable?: boolean;
19
+ searchKeys?: (keyof T)[];
20
+ searchPlaceholder?: string;
21
+ empty?: ReactNode;
22
+ rowKey?: (item: T, index: number) => string | number;
23
+ onRowClick?: (item: T) => void;
24
+ className?: string;
25
+ }
26
+ export declare function DataTable<T>({ data, columns, pageSize, searchable, searchKeys, searchPlaceholder, empty, rowKey, onRowClick, className, }: DataTableProps<T>): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "./data.css";
3
+ import {} from "react";
4
+ import { useCollection } from "./useCollection";
5
+ import { cx } from "../util";
6
+ // A neutral, sortable, searchable, paginated table over an array (data wrangling
7
+ // done for you via useCollection). Style via .kit-table* — see STYLING.md.
8
+ export function DataTable({ data, columns, pageSize, searchable, searchKeys, searchPlaceholder = "Search…", empty, rowKey, onRowClick, className, }) {
9
+ const cfg = { pageSize, searchKeys };
10
+ const c = useCollection(data, cfg);
11
+ return (_jsxs("div", { className: cx("kit-table-wrap", className), children: [searchable && (_jsx("input", { className: "kit-table-search", value: c.query, placeholder: searchPlaceholder, onChange: (e) => c.setQuery(e.target.value) })), _jsxs("table", { className: "kit-table", children: [_jsx("thead", { children: _jsx("tr", { children: columns.map((col) => {
12
+ const active = c.sort?.key === col.key;
13
+ return (_jsxs("th", { "data-align": col.align, "data-sortable": col.sortable || undefined, "data-active": active || undefined, className: col.className, onClick: col.sortable ? () => c.toggleSort(col.key) : undefined, children: [col.header, active ? _jsx("span", { className: "kit-table-caret", children: c.sort?.dir === "asc" ? " ▲" : " ▼" }) : null] }, String(col.key)));
14
+ }) }) }), _jsx("tbody", { children: c.isEmpty ? (_jsx("tr", { className: "kit-table-empty", children: _jsx("td", { colSpan: columns.length, children: empty ?? "No results" }) })) : (c.items.map((item, i) => (_jsx("tr", { "data-clickable": onRowClick ? true : undefined, onClick: onRowClick ? () => onRowClick(item) : undefined, children: columns.map((col) => (_jsx("td", { "data-align": col.align, className: col.className, children: col.render ? col.render(item, i) : String(item[col.key] ?? "") }, String(col.key)))) }, rowKey ? rowKey(item, i) : i)))) })] }), pageSize && c.pageCount > 1 && (_jsxs("div", { className: "kit-table-pager", children: [_jsx("button", { className: "kit-table-page-btn", disabled: c.page === 0, onClick: c.prevPage, children: "\u2039 Prev" }), _jsxs("span", { className: "kit-table-page-info", children: [c.page + 1, " / ", c.pageCount] }), _jsx("button", { className: "kit-table-page-btn", disabled: c.page >= c.pageCount - 1, onClick: c.nextPage, children: "Next \u203A" })] }))] }));
15
+ }
@@ -0,0 +1,22 @@
1
+ /* DataTable — neutral, restyle via .kit-table* or --kit-* vars. */
2
+ .kit-table-wrap{display:flex;flex-direction:column;gap:10px;font:inherit}
3
+ .kit-table-search{align-self:flex-start;min-width:220px;border:1px solid var(--kit-border,#e5e7eb);border-radius:10px;padding:8px 12px;font:inherit;font-size:14px;background:transparent;color:inherit}
4
+ .kit-table-search:focus{outline:0;border-color:var(--kit-muted,#6b7280)}
5
+ .kit-table{width:100%;border-collapse:collapse;font-size:14px}
6
+ .kit-table th,.kit-table td{padding:10px 12px;text-align:left;border-bottom:1px solid var(--kit-border,#eceef1)}
7
+ .kit-table td[data-align="right"],.kit-table th[data-align="right"]{text-align:right;font-variant-numeric:tabular-nums}
8
+ .kit-table td[data-align="center"],.kit-table th[data-align="center"]{text-align:center}
9
+ .kit-table th{font-weight:600;color:var(--kit-muted,#6b7280);user-select:none;white-space:nowrap}
10
+ .kit-table th[data-sortable]{cursor:pointer}
11
+ .kit-table th[data-active]{color:var(--kit-fg,#111827)}
12
+ .kit-table-caret{font-size:10px}
13
+ .kit-table tbody tr[data-clickable]{cursor:pointer}
14
+ .kit-table tbody tr[data-clickable]:hover{background:var(--kit-row-hover,#f6f7f9)}
15
+ .kit-table-empty td{padding:28px 12px;text-align:center;color:var(--kit-muted,#6b7280)}
16
+ .kit-table-pager{display:flex;align-items:center;gap:12px}
17
+ .kit-table-page-btn{border:1px solid var(--kit-border,#e5e7eb);background:transparent;color:inherit;border-radius:9px;padding:6px 12px;font:inherit;font-size:13px;cursor:pointer}
18
+ .kit-table-page-btn:disabled{opacity:.45;cursor:default}
19
+ .kit-table-page-info{color:var(--kit-muted,#6b7280);font-size:13px;font-variant-numeric:tabular-nums}
20
+ @media (prefers-color-scheme:dark){
21
+ .kit-table-wrap{--kit-border:#2c2e36;--kit-fg:#f5f5f7;--kit-row-hover:#1d1f25}
22
+ }
@@ -1,2 +1,7 @@
1
1
  export { useApi, apiClient, type QueryResult, type UseApiOptions } from "./useApi";
2
2
  export { useSubgraph, type UseSubgraphArgs } from "./useSubgraph";
3
+ export { Async, type AsyncProps, type AsyncQuery } from "./Async";
4
+ export { useCollection, processCollection, type Collection, type CollectionConfig, type SortState, type SortDir, } from "./useCollection";
5
+ export { DataTable, type DataTableProps, type Column } from "./DataTable";
6
+ export { useEventSource, type EventSourceState, type UseEventSourceOptions, type StreamStatus } from "./useEventSource";
7
+ export { useWebSocket, type WebSocketState, type UseWebSocketOptions } from "./useWebSocket";
@@ -1,2 +1,7 @@
1
1
  export { useApi, apiClient } from "./useApi";
2
2
  export { useSubgraph } from "./useSubgraph";
3
+ export { Async } from "./Async";
4
+ export { useCollection, processCollection, } from "./useCollection";
5
+ export { DataTable } from "./DataTable";
6
+ export { useEventSource } from "./useEventSource";
7
+ export { useWebSocket } from "./useWebSocket";
@@ -0,0 +1,42 @@
1
+ export type SortDir = "asc" | "desc";
2
+ export interface SortState<T> {
3
+ key: keyof T;
4
+ dir: SortDir;
5
+ }
6
+ export interface CollectionConfig<T> {
7
+ /** Keys to full-text search (case-insensitive). Default: all string/number fields. */
8
+ searchKeys?: (keyof T)[];
9
+ /** Custom search predicate (overrides searchKeys). */
10
+ search?: (item: T, query: string) => boolean;
11
+ /** Persistent filter applied before search/sort. */
12
+ filter?: (item: T) => boolean;
13
+ /** Rows per page. Omit/0 to disable paging (one page). */
14
+ pageSize?: number;
15
+ initialSort?: SortState<T>;
16
+ initialQuery?: string;
17
+ }
18
+ export declare function processCollection<T>(items: T[], state: {
19
+ query?: string;
20
+ sort?: SortState<T> | null;
21
+ } & Pick<CollectionConfig<T>, "searchKeys" | "search" | "filter">): T[];
22
+ export interface Collection<T> {
23
+ /** The current page of processed rows. */
24
+ items: T[];
25
+ /** All processed (filtered+searched+sorted) rows, across pages. */
26
+ all: T[];
27
+ query: string;
28
+ setQuery: (q: string) => void;
29
+ sort: SortState<T> | null;
30
+ /** Set/toggle the sort key (same key flips asc↔desc). */
31
+ toggleSort: (key: keyof T) => void;
32
+ setSort: (sort: SortState<T> | null) => void;
33
+ page: number;
34
+ setPage: (p: number) => void;
35
+ nextPage: () => void;
36
+ prevPage: () => void;
37
+ pageCount: number;
38
+ /** Count after filter+search (before paging). */
39
+ total: number;
40
+ isEmpty: boolean;
41
+ }
42
+ export declare function useCollection<T>(items: T[], config?: CollectionConfig<T>): Collection<T>;
@@ -0,0 +1,67 @@
1
+ import { useMemo, useState } from "react";
2
+ function compare(a, b) {
3
+ if (a == null)
4
+ return b == null ? 0 : -1;
5
+ if (b == null)
6
+ return 1;
7
+ if (typeof a === "number" && typeof b === "number")
8
+ return a - b;
9
+ if (typeof a === "bigint" && typeof b === "bigint")
10
+ return a < b ? -1 : a > b ? 1 : 0;
11
+ return String(a).localeCompare(String(b), undefined, { numeric: true });
12
+ }
13
+ // Pure: filter → search → sort. Exported for testing and non-React use.
14
+ export function processCollection(items, state) {
15
+ let out = state.filter ? items.filter(state.filter) : items.slice();
16
+ const q = (state.query ?? "").trim().toLowerCase();
17
+ if (q) {
18
+ out = out.filter((item) => {
19
+ if (state.search)
20
+ return state.search(item, q);
21
+ const keys = state.searchKeys ?? Object.keys(item);
22
+ return keys.some((k) => {
23
+ const v = item[k];
24
+ return (typeof v === "string" || typeof v === "number") && String(v).toLowerCase().includes(q);
25
+ });
26
+ });
27
+ }
28
+ if (state.sort) {
29
+ const { key, dir } = state.sort;
30
+ const f = dir === "asc" ? 1 : -1;
31
+ out.sort((a, b) => compare(a[key], b[key]) * f);
32
+ }
33
+ return out;
34
+ }
35
+ // Headless client-side data wrangling: search + sort + paginate any array, with
36
+ // React state. Feed `items` (e.g. from useApi/useSubgraph) and render `items`.
37
+ export function useCollection(items, config = {}) {
38
+ const [query, setQueryRaw] = useState(config.initialQuery ?? "");
39
+ const [sort, setSort] = useState(config.initialSort ?? null);
40
+ const [page, setPage] = useState(0);
41
+ const setQuery = (q) => {
42
+ setQueryRaw(q);
43
+ setPage(0);
44
+ };
45
+ const toggleSort = (key) => setSort((s) => (s && s.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir: "asc" }));
46
+ const all = useMemo(() => processCollection(items, { query, sort, searchKeys: config.searchKeys, search: config.search, filter: config.filter }), [items, query, sort, config.searchKeys, config.search, config.filter]);
47
+ const size = config.pageSize && config.pageSize > 0 ? config.pageSize : all.length || 1;
48
+ const pageCount = Math.max(1, Math.ceil(all.length / size));
49
+ const clamped = Math.min(page, pageCount - 1);
50
+ const pageItems = config.pageSize ? all.slice(clamped * size, clamped * size + size) : all;
51
+ return {
52
+ items: pageItems,
53
+ all,
54
+ query,
55
+ setQuery,
56
+ sort,
57
+ toggleSort,
58
+ setSort,
59
+ page: clamped,
60
+ setPage,
61
+ nextPage: () => setPage((p) => Math.min(p + 1, pageCount - 1)),
62
+ prevPage: () => setPage((p) => Math.max(p - 1, 0)),
63
+ pageCount,
64
+ total: all.length,
65
+ isEmpty: all.length === 0,
66
+ };
67
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { processCollection } from "./useCollection";
3
+ const rows = [
4
+ { name: "Alice", age: 30 },
5
+ { name: "bob", age: 25 },
6
+ { name: "Carol", age: 40 },
7
+ ];
8
+ describe("processCollection", () => {
9
+ it("filters, then case-insensitive searches, then sorts (numeric-aware)", () => {
10
+ const sorted = processCollection(rows, { sort: { key: "age", dir: "asc" } });
11
+ expect(sorted.map((r) => r.age)).toEqual([25, 30, 40]);
12
+ const desc = processCollection(rows, { sort: { key: "name", dir: "desc" } });
13
+ expect(desc.map((r) => r.name)).toEqual(["Carol", "bob", "Alice"]);
14
+ const searched = processCollection(rows, { query: "car", searchKeys: ["name"] });
15
+ expect(searched).toEqual([{ name: "Carol", age: 40 }]);
16
+ const filtered = processCollection(rows, { filter: (r) => r.age >= 30, query: "" });
17
+ expect(filtered.map((r) => r.name).sort()).toEqual(["Alice", "Carol"]);
18
+ });
19
+ it("does not mutate the input array", () => {
20
+ const copy = rows.slice();
21
+ processCollection(rows, { sort: { key: "age", dir: "desc" } });
22
+ expect(rows).toEqual(copy);
23
+ });
24
+ it("searches all string/number fields when no searchKeys given", () => {
25
+ expect(processCollection(rows, { query: "40" })).toEqual([{ name: "Carol", age: 40 }]);
26
+ });
27
+ });
@@ -0,0 +1,15 @@
1
+ export type StreamStatus = "connecting" | "open" | "error" | "closed";
2
+ export interface EventSourceState<T> {
3
+ /** Last message, JSON-parsed when possible (else the raw string). */
4
+ data: T | undefined;
5
+ status: StreamStatus;
6
+ error: Error | null;
7
+ }
8
+ export interface UseEventSourceOptions {
9
+ enabled?: boolean;
10
+ /** Send cookies with the request (same-origin auth). */
11
+ withCredentials?: boolean;
12
+ /** Listen for a named event instead of the default `message`. */
13
+ event?: string;
14
+ }
15
+ export declare function useEventSource<T = unknown>(url?: string, opts?: UseEventSourceOptions): EventSourceState<T>;