@bufinance/web3-signin 0.1.4 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bufinance/web3-signin",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Headless cross-app Web3 wallet sign-in for BUFI: EIP-6963 wallet discovery + Supabase signInWithWeb3 (EIP-4361) + Turnstile captcha. Shared by desk-v1 and defi-web-app. Source of truth lives here; both apps consume it.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,134 @@
1
+ import { type CSSProperties, useState } from "react";
2
+
3
+ /**
4
+ * AssetCircles — overlapping token/asset icon circles that arc up and reveal a
5
+ * ticker tooltip on hover, with a "+N" overflow chip. Ported from desk-v1's
6
+ * `WalletTokenIcons` (the "which assets does this wallet hold" affordance) into
7
+ * the shared package so BOTH apps render assets the same way. This is the
8
+ * asset/token stack (icon + ticker tooltip) — NOT a people/profile stack.
9
+ *
10
+ * Dependency-light: inline styles only (no Tailwind purge risk, no framer/radix),
11
+ * so it drops into any consumer unchanged. The host supplies `assets`.
12
+ */
13
+ export interface AssetCircleItem {
14
+ /** Token icon URL. */
15
+ iconUrl: string;
16
+ /** Ticker shown in the tooltip + used as alt text (e.g. "USDC"). */
17
+ label: string;
18
+ }
19
+
20
+ export interface AssetCirclesProps {
21
+ assets: AssetCircleItem[];
22
+ /** Max circles before collapsing the rest into a "+N" chip (default 4). */
23
+ max?: number;
24
+ /** Circle diameter in px (default 18, tuned for a header pill). */
25
+ size?: number;
26
+ className?: string;
27
+ }
28
+
29
+ export function AssetCircles({
30
+ assets,
31
+ max = 4,
32
+ size = 18,
33
+ className,
34
+ }: AssetCirclesProps) {
35
+ const [hover, setHover] = useState<number | null>(null);
36
+ if (assets.length === 0) return null;
37
+
38
+ const shown = assets.slice(0, max);
39
+ const overflow = assets.length - shown.length;
40
+ const overlap = Math.round(size / 3);
41
+ const lift = Math.max(5, Math.round(size * 0.4));
42
+
43
+ return (
44
+ <div
45
+ className={className}
46
+ style={{ display: "inline-flex", alignItems: "center" }}
47
+ data-bufi-asset-circles
48
+ >
49
+ {shown.map((a, i) => {
50
+ const h = hover === i;
51
+ return (
52
+ <span
53
+ key={`${a.label}:${i}`}
54
+ onMouseEnter={() => setHover(i)}
55
+ onMouseLeave={() => setHover((p) => (p === i ? null : p))}
56
+ style={{
57
+ position: "relative",
58
+ marginLeft: i === 0 ? 0 : -overlap,
59
+ zIndex: h ? 50 : shown.length - i,
60
+ transform: h ? `translateY(-${lift}px) scale(1.2)` : "none",
61
+ transition: "transform 160ms cubic-bezier(0.23, 1, 0.32, 1)",
62
+ display: "inline-flex",
63
+ }}
64
+ >
65
+ {/* Ticker tooltip — sits just above the circle. */}
66
+ {h && <span style={tooltipStyle}>{a.label}</span>}
67
+ <span
68
+ style={{
69
+ width: size,
70
+ height: size,
71
+ borderRadius: "9999px",
72
+ overflow: "hidden",
73
+ display: "inline-flex",
74
+ alignItems: "center",
75
+ justifyContent: "center",
76
+ background: "#fff",
77
+ boxShadow: "0 0 0 2px #fff",
78
+ }}
79
+ >
80
+ {/* eslint-disable-next-line @next/next/no-img-element */}
81
+ <img
82
+ src={a.iconUrl}
83
+ alt={a.label}
84
+ width={size}
85
+ height={size}
86
+ style={{ width: size, height: size, objectFit: "cover", display: "block" }}
87
+ />
88
+ </span>
89
+ </span>
90
+ );
91
+ })}
92
+
93
+ {overflow > 0 && (
94
+ <span
95
+ style={{
96
+ width: size,
97
+ height: size,
98
+ marginLeft: -overlap,
99
+ borderRadius: "9999px",
100
+ display: "grid",
101
+ placeItems: "center",
102
+ background: "#F4F4F4",
103
+ boxShadow: "0 0 0 2px #fff",
104
+ fontSize: Math.max(9, Math.round(size * 0.5)),
105
+ fontWeight: 600,
106
+ color: "#6a55cf",
107
+ }}
108
+ >
109
+ +{overflow}
110
+ </span>
111
+ )}
112
+ </div>
113
+ );
114
+ }
115
+
116
+ const tooltipStyle: CSSProperties = {
117
+ position: "absolute",
118
+ bottom: "100%",
119
+ left: "50%",
120
+ transform: "translateX(-50%)",
121
+ marginBottom: 6,
122
+ whiteSpace: "nowrap",
123
+ padding: "2px 6px",
124
+ borderRadius: 6,
125
+ fontSize: 9,
126
+ fontWeight: 700,
127
+ background: "#6a55cf",
128
+ color: "#fff",
129
+ pointerEvents: "none",
130
+ zIndex: 60,
131
+ boxShadow: "0 2px 6px -2px rgba(0,0,0,0.3)",
132
+ };
133
+
134
+ export default AssetCircles;
@@ -11,3 +11,16 @@ export {
11
11
  type WalletDropdownProps,
12
12
  type WalletDropdownClassNames,
13
13
  } from "./wallet-dropdown";
14
+ export {
15
+ AssetCircles,
16
+ type AssetCircleItem,
17
+ type AssetCirclesProps,
18
+ } from "./avatar-circles";
19
+ export {
20
+ WalletBalancePanel,
21
+ type WalletBalancePanelProps,
22
+ type WalletBalancePanelChain,
23
+ type WalletBalancePanelToken,
24
+ type WalletBalancePanelTab,
25
+ type WalletBalancePanelClassNames,
26
+ } from "./wallet-balance-panel";
@@ -0,0 +1,232 @@
1
+ import { type ReactNode } from "react";
2
+
3
+ /**
4
+ * WalletBalancePanel — the shared, presentational body of the wallet dropdown:
5
+ * header (title + network tabs + optional scope tabs + total + ghost) and a
6
+ * per-chain token list. Ported from defi-web's inline dropdown body so desk +
7
+ * defi-web render the SAME body (Phase 1 of the shared <BufiWallet>).
8
+ *
9
+ * Presentational + injected: the host supplies the chain/token data, the live
10
+ * total + ghost as ReactNodes (so it can keep animated counters), and theming
11
+ * via `classNames` (each app's tokens differ). No data-fetching, no app deps.
12
+ */
13
+ export interface WalletBalancePanelToken {
14
+ asset: string;
15
+ /** Already display-formatted amount, e.g. "12.45". */
16
+ amount: string;
17
+ iconUrl?: string;
18
+ }
19
+
20
+ export interface WalletBalancePanelChain {
21
+ chainId: number;
22
+ label: string;
23
+ iconUrl?: string;
24
+ /** Net-filtered, host-decided which tokens to show (e.g. nonzero only). */
25
+ tokens: WalletBalancePanelToken[];
26
+ }
27
+
28
+ export interface WalletBalancePanelTab {
29
+ key: string;
30
+ label: string;
31
+ }
32
+
33
+ export interface WalletBalancePanelClassNames {
34
+ header?: string;
35
+ headerRow?: string;
36
+ titleWrap?: string;
37
+ titleIcon?: string;
38
+ title?: string;
39
+ netRow?: string;
40
+ netTab?: string;
41
+ netTabActive?: string;
42
+ scopeRow?: string;
43
+ scopeTab?: string;
44
+ scopeTabActive?: string;
45
+ totalRow?: string;
46
+ totalLabel?: string;
47
+ totalValue?: string;
48
+ ghostRow?: string;
49
+ body?: string;
50
+ empty?: string;
51
+ chainRow?: string;
52
+ chainIcon?: string;
53
+ chainLabel?: string;
54
+ tokenList?: string;
55
+ tokenRow?: string;
56
+ tokenIcon?: string;
57
+ tokenZero?: string;
58
+ }
59
+
60
+ export interface WalletBalancePanelProps {
61
+ title: string;
62
+ /** "Personal" / "Treasury" — labels the total row. */
63
+ scopeLabel?: string;
64
+ /** Live total node (host renders e.g. "≈ $<AnimatedNumber/>"), or "—". */
65
+ totalText: ReactNode;
66
+ /** False → render the empty state instead of the chain list. */
67
+ hasAddress?: boolean;
68
+ emptyMessage?: ReactNode;
69
+ /** Small wallet icon before the title. */
70
+ titleIcon?: ReactNode;
71
+ /** Extra node beside the title (e.g. <AssetCircles>). */
72
+ titleExtra?: ReactNode;
73
+ /** Network tabs (mainnet/testnet); omit to hide. */
74
+ networks?: WalletBalancePanelTab[];
75
+ activeNetwork?: string;
76
+ onNetworkChange?: (key: string) => void;
77
+ /** Scope tabs (team: Treasury/Operations); omit to hide. */
78
+ scopes?: WalletBalancePanelTab[];
79
+ activeScope?: string;
80
+ onScopeChange?: (key: string) => void;
81
+ /** Ghost (private) balance row node; omit to hide. */
82
+ ghost?: ReactNode;
83
+ /** Per-chain balances, already net-filtered by the host. */
84
+ chains: WalletBalancePanelChain[];
85
+ /** Per-token cell renderer override (host can pass its own currency cell). */
86
+ renderToken?: (token: WalletBalancePanelToken, chain: WalletBalancePanelChain) => ReactNode;
87
+ /** Shown when a chain has no nonzero tokens (default "$0.00"). */
88
+ zeroText?: ReactNode;
89
+ classNames?: WalletBalancePanelClassNames;
90
+ }
91
+
92
+ function Tabs({
93
+ tabs,
94
+ active,
95
+ onChange,
96
+ rowClass,
97
+ tabClass,
98
+ activeClass,
99
+ }: {
100
+ tabs: WalletBalancePanelTab[];
101
+ active?: string;
102
+ onChange?: (k: string) => void;
103
+ rowClass?: string;
104
+ tabClass?: string;
105
+ activeClass?: string;
106
+ }) {
107
+ return (
108
+ <div className={rowClass} role="tablist">
109
+ {tabs.map((t) => {
110
+ const on = t.key === active;
111
+ return (
112
+ <button
113
+ key={t.key}
114
+ type="button"
115
+ role="tab"
116
+ aria-selected={on}
117
+ onClick={() => onChange?.(t.key)}
118
+ className={`${tabClass ?? ""} ${on ? activeClass ?? "" : ""}`.trim()}
119
+ >
120
+ {t.label}
121
+ </button>
122
+ );
123
+ })}
124
+ </div>
125
+ );
126
+ }
127
+
128
+ export function WalletBalancePanel({
129
+ title,
130
+ scopeLabel,
131
+ totalText,
132
+ hasAddress = true,
133
+ emptyMessage,
134
+ titleIcon,
135
+ titleExtra,
136
+ networks,
137
+ activeNetwork,
138
+ onNetworkChange,
139
+ scopes,
140
+ activeScope,
141
+ onScopeChange,
142
+ ghost,
143
+ chains,
144
+ renderToken,
145
+ zeroText = "$0.00",
146
+ classNames = {},
147
+ }: WalletBalancePanelProps): ReactNode {
148
+ return (
149
+ <div data-bufi-balance-panel>
150
+ <div className={classNames.header}>
151
+ <div className={classNames.headerRow}>
152
+ <div className={classNames.titleWrap}>
153
+ {titleIcon ? <span className={classNames.titleIcon}>{titleIcon}</span> : null}
154
+ <span className={classNames.title}>{title}</span>
155
+ {titleExtra}
156
+ </div>
157
+ {networks && networks.length > 0 ? (
158
+ <Tabs
159
+ tabs={networks}
160
+ active={activeNetwork}
161
+ onChange={onNetworkChange}
162
+ rowClass={classNames.netRow}
163
+ tabClass={classNames.netTab}
164
+ activeClass={classNames.netTabActive}
165
+ />
166
+ ) : null}
167
+ </div>
168
+
169
+ {scopes && scopes.length > 1 ? (
170
+ <Tabs
171
+ tabs={scopes}
172
+ active={activeScope}
173
+ onChange={onScopeChange}
174
+ rowClass={classNames.scopeRow}
175
+ tabClass={classNames.scopeTab}
176
+ activeClass={classNames.scopeTabActive}
177
+ />
178
+ ) : null}
179
+
180
+ <div className={classNames.totalRow}>
181
+ <span className={classNames.totalLabel}>{scopeLabel ? `${scopeLabel} balance` : "Balance"}</span>
182
+ <span className={classNames.totalValue}>{totalText}</span>
183
+ </div>
184
+
185
+ {ghost ? <div className={classNames.ghostRow}>{ghost}</div> : null}
186
+ </div>
187
+
188
+ <div className={classNames.body}>
189
+ {!hasAddress ? (
190
+ <div className={classNames.empty}>{emptyMessage}</div>
191
+ ) : (
192
+ chains.map((c) => (
193
+ <div key={c.chainId} className={classNames.chainRow}>
194
+ <span className={classNames.chainIcon}>
195
+ {c.iconUrl ? (
196
+ // eslint-disable-next-line @next/next/no-img-element
197
+ <img src={c.iconUrl} alt="" width={16} height={16} />
198
+ ) : (
199
+ c.label.slice(0, 2)
200
+ )}
201
+ </span>
202
+ <div style={{ minWidth: 0, flex: 1 }}>
203
+ <div className={classNames.chainLabel}>{c.label}</div>
204
+ <div className={classNames.tokenList}>
205
+ {c.tokens.length > 0 ? (
206
+ c.tokens.map((t) =>
207
+ renderToken ? (
208
+ <span key={t.asset}>{renderToken(t, c)}</span>
209
+ ) : (
210
+ <span key={t.asset} className={classNames.tokenRow}>
211
+ {t.iconUrl ? (
212
+ // eslint-disable-next-line @next/next/no-img-element
213
+ <img src={t.iconUrl} alt="" width={14} height={14} className={classNames.tokenIcon} />
214
+ ) : null}
215
+ {t.amount} {t.asset}
216
+ </span>
217
+ ),
218
+ )
219
+ ) : (
220
+ <span className={classNames.tokenZero}>{zeroText}</span>
221
+ )}
222
+ </div>
223
+ </div>
224
+ </div>
225
+ ))
226
+ )}
227
+ </div>
228
+ </div>
229
+ );
230
+ }
231
+
232
+ export default WalletBalancePanel;