@bufinance/web3-signin 0.1.3 → 0.1.5
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 +1 -1
- package/src/react/avatar-circles.tsx +134 -0
- package/src/react/index.ts +5 -0
- package/src/react/wallet-dropdown.tsx +23 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bufinance/web3-signin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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;
|
package/src/react/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface WalletDropdownClassNames {
|
|
|
21
21
|
tabActive?: string;
|
|
22
22
|
accountRow?: string;
|
|
23
23
|
balanceRow?: string;
|
|
24
|
+
signOut?: string;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface WalletDropdownProps {
|
|
@@ -58,6 +59,14 @@ export interface WalletDropdownProps {
|
|
|
58
59
|
/** Shown when authed but no accounts yet (provisioning). */
|
|
59
60
|
emptyState?: ReactNode;
|
|
60
61
|
onSelectAccount?: (account: WalletAccount) => void;
|
|
62
|
+
/**
|
|
63
|
+
* When provided, a "Log out" button renders at the BOTTOM of the open panel
|
|
64
|
+
* (below children). Both apps wire their own session sign-out — desk via
|
|
65
|
+
* Supabase, defi-web via the BUFI session — so the control lives in one place.
|
|
66
|
+
*/
|
|
67
|
+
onSignOut?: () => void;
|
|
68
|
+
/** Label for the sign-out button (default "Log out"). */
|
|
69
|
+
signOutLabel?: string;
|
|
61
70
|
classNames?: WalletDropdownClassNames;
|
|
62
71
|
}
|
|
63
72
|
|
|
@@ -79,6 +88,8 @@ export function WalletDropdown(props: WalletDropdownProps): ReactNode {
|
|
|
79
88
|
renderBalances = true,
|
|
80
89
|
emptyState,
|
|
81
90
|
onSelectAccount,
|
|
91
|
+
onSignOut,
|
|
92
|
+
signOutLabel = "Log out",
|
|
82
93
|
classNames = {},
|
|
83
94
|
} = props;
|
|
84
95
|
|
|
@@ -188,6 +199,18 @@ export function WalletDropdown(props: WalletDropdownProps): ReactNode {
|
|
|
188
199
|
{children}
|
|
189
200
|
</>
|
|
190
201
|
)}
|
|
202
|
+
|
|
203
|
+
{/* Sign out — always the last thing in the panel. */}
|
|
204
|
+
{onSignOut && (
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
className={classNames.signOut}
|
|
208
|
+
onClick={onSignOut}
|
|
209
|
+
data-bufi-signout
|
|
210
|
+
>
|
|
211
|
+
{signOutLabel}
|
|
212
|
+
</button>
|
|
213
|
+
)}
|
|
191
214
|
</div>
|
|
192
215
|
)}
|
|
193
216
|
</div>
|