Skip to content

Commit 48c475e

Browse files
chore: clean up dropddown menu, better playground ux
1 parent cac0e42 commit 48c475e

File tree

3 files changed

+147
-180
lines changed

3 files changed

+147
-180
lines changed

examples/next-js/app/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@/components/playground';
1313
import { Blocks, Code2, Anchor, Zap } from 'lucide-react';
1414
import { Button } from '@/components/ui/button';
15+
import { ConnectButton } from '@/components/connector/connect-button';
1516

1617
export default function Home() {
1718
const npmCommand = 'npm i @solana/connector';
@@ -100,8 +101,8 @@ export default function Home() {
100101
{/* Tabbed Navigation */}
101102
<Tabs defaultValue="components" className="w-full">
102103
<div className="sticky top-16 z-40 bg-bg1/95 backdrop-blur-sm border-b border-sand-200">
103-
<div className="max-w-7xl mx-auto">
104-
<TabsList className="h-14 w-full justify-start gap-0 bg-transparent p-0 rounded-none">
104+
<div className="max-w-7xl mx-auto flex items-center justify-between px-4 lg:px-6">
105+
<TabsList className="h-14 justify-start gap-0 bg-transparent p-0 rounded-none">
105106
<TabsTrigger
106107
value="components"
107108
className="h-14 px-4 rounded-none border-b-2 border-transparent data-[state=active]:border-sand-1500 data-[state=active]:bg-transparent data-[state=active]:shadow-none gap-2"
@@ -131,6 +132,7 @@ export default function Home() {
131132
<span className="hidden sm:inline">Transactions</span>
132133
</TabsTrigger>
133134
</TabsList>
135+
<ConnectButton />
134136
</div>
135137
</div>
136138

examples/next-js/components/connector/wallet-dropdown-content.tsx

Lines changed: 143 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,11 @@ import {
1111
TransactionHistoryElement,
1212
DisconnectElement,
1313
} from '@solana/connector/react';
14-
import {
15-
DropdownMenu,
16-
DropdownMenuContent,
17-
DropdownMenuItem,
18-
DropdownMenuTrigger,
19-
} from '@/components/ui/dropdown-menu';
2014
import {
2115
Wallet,
2216
Copy,
2317
Globe,
18+
ChevronLeft,
2419
Check,
2520
RefreshCw,
2621
Coins,
@@ -31,13 +26,16 @@ import {
3126
LogOut,
3227
} from 'lucide-react';
3328
import { useState } from 'react';
29+
import { motion } from 'motion/react';
3430

3531
interface WalletDropdownContentProps {
3632
selectedAccount: string;
3733
walletIcon?: string;
3834
walletName: string;
3935
}
4036

37+
type DropdownView = 'wallet' | 'network';
38+
4139
const clusterColors: Record<string, string> = {
4240
'solana:mainnet': 'bg-green-500',
4341
'solana:devnet': 'bg-blue-500',
@@ -46,6 +44,7 @@ const clusterColors: Record<string, string> = {
4644
};
4745

4846
export function WalletDropdownContent({ selectedAccount, walletIcon, walletName }: WalletDropdownContentProps) {
47+
const [view, setView] = useState<DropdownView>('wallet');
4948
const [copied, setCopied] = useState(false);
5049

5150
const shortAddress = `${selectedAccount.slice(0, 4)}...${selectedAccount.slice(-4)}`;
@@ -56,72 +55,63 @@ export function WalletDropdownContent({ selectedAccount, walletIcon, walletName
5655
setTimeout(() => setCopied(false), 2000);
5756
}
5857

59-
return (
60-
<div className="w-[360px] p-4 space-y-4">
61-
{/* Header with Avatar and Address */}
62-
<div className="flex items-center justify-between">
63-
<div className="flex items-center gap-3">
64-
<Avatar className="h-12 w-12">
65-
{walletIcon && <AvatarImage src={walletIcon} alt={walletName} />}
66-
<AvatarFallback>
67-
<Wallet className="h-6 w-6" />
68-
</AvatarFallback>
69-
</Avatar>
70-
<div>
71-
<div className="font-semibold text-lg">{shortAddress}</div>
72-
<div className="text-xs text-muted-foreground">{walletName}</div>
58+
// Wallet View
59+
if (view === 'wallet') {
60+
return (
61+
<motion.div
62+
key="wallet"
63+
initial={{ opacity: 0 }}
64+
animate={{ opacity: 1 }}
65+
exit={{ opacity: 0 }}
66+
transition={{ duration: 0.15 }}
67+
className="w-[360px] p-4 space-y-4"
68+
>
69+
{/* Header with Avatar and Address */}
70+
<div className="flex items-center justify-between">
71+
<div className="flex items-center gap-3">
72+
<Avatar className="h-12 w-12">
73+
{walletIcon && <AvatarImage src={walletIcon} alt={walletName} />}
74+
<AvatarFallback>
75+
<Wallet className="h-6 w-6" />
76+
</AvatarFallback>
77+
</Avatar>
78+
<div>
79+
<div className="font-semibold text-lg">{shortAddress}</div>
80+
<div className="text-xs text-muted-foreground">{walletName}</div>
81+
</div>
7382
</div>
74-
</div>
75-
<div className="flex items-center gap-2">
76-
<Button
77-
type="button"
78-
onClick={handleCopy}
79-
variant="outline"
80-
size="icon"
81-
className="rounded-full"
82-
title={copied ? 'Copied!' : 'Copy address'}
83-
>
84-
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
85-
</Button>
83+
<div className="flex items-center gap-2">
84+
<Button
85+
type="button"
86+
onClick={handleCopy}
87+
variant="outline"
88+
size="icon"
89+
className="rounded-full"
90+
title={copied ? 'Copied!' : 'Copy address'}
91+
>
92+
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
93+
</Button>
8694

87-
{/* Network Selector Globe Button */}
88-
<ClusterElement
89-
render={({ cluster, clusters, setCluster }) => (
90-
<DropdownMenu>
91-
<DropdownMenuTrigger asChild>
92-
<Button
93-
type="button"
94-
variant="outline"
95-
size="icon"
96-
className="rounded-full relative"
97-
title={`Network: ${cluster?.label || 'Unknown'}`}
98-
>
99-
<Globe className="h-4 w-4" />
100-
<span
101-
className={`absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded-full border-2 border-background ${clusterColors[cluster?.id || ''] || 'bg-emerald-500'}`}
102-
/>
103-
</Button>
104-
</DropdownMenuTrigger>
105-
<DropdownMenuContent align="end" className="w-auto rounded-[16px]">
106-
{clusters.map(c => (
107-
<DropdownMenuItem
108-
key={c.id}
109-
onClick={() => setCluster(c.id)}
110-
className="flex items-center gap-2 cursor-pointer rounded-[13px]"
111-
>
112-
<span
113-
className={`h-2 w-2 rounded-full ${clusterColors[c.id] || 'bg-purple-500'}`}
114-
/>
115-
<span className="flex-1">{c.label}</span>
116-
{cluster?.id === c.id && <Check className="h-4 w-4" />}
117-
</DropdownMenuItem>
118-
))}
119-
</DropdownMenuContent>
120-
</DropdownMenu>
121-
)}
122-
/>
95+
{/* Network Selector Globe Button */}
96+
<ClusterElement
97+
render={({ cluster }) => (
98+
<Button
99+
type="button"
100+
variant="outline"
101+
size="icon"
102+
className="rounded-full relative"
103+
onClick={() => setView('network')}
104+
title={`Network: ${cluster?.label || 'Unknown'}`}
105+
>
106+
<Globe className="h-4 w-4" />
107+
<span
108+
className={`absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded-full border-2 border-background ${clusterColors[cluster?.id || ''] || 'bg-emerald-500'}`}
109+
/>
110+
</Button>
111+
)}
112+
/>
113+
</div>
123114
</div>
124-
</div>
125115

126116
{/* Full Width Balance */}
127117
<BalanceElement
@@ -306,20 +296,90 @@ export function WalletDropdownContent({ selectedAccount, walletIcon, walletName
306296
</AccordionItem>
307297
</Accordion>
308298

309-
{/* Disconnect Button */}
310-
<DisconnectElement
311-
render={({ disconnect, disconnecting }) => (
312-
<Button
313-
variant="destructive"
314-
className="w-full h-11 text-base rounded-[12px]"
315-
onClick={disconnect}
316-
disabled={disconnecting}
317-
>
318-
<LogOut className="h-4 w-4 mr-2" />
319-
{disconnecting ? 'Disconnecting...' : 'Disconnect'}
320-
</Button>
321-
)}
299+
{/* Disconnect Button */}
300+
<DisconnectElement
301+
render={({ disconnect, disconnecting }) => (
302+
<Button
303+
variant="destructive"
304+
className="w-full h-11 text-base rounded-[12px]"
305+
onClick={disconnect}
306+
disabled={disconnecting}
307+
>
308+
<LogOut className="h-4 w-4 mr-2" />
309+
{disconnecting ? 'Disconnecting...' : 'Disconnect'}
310+
</Button>
311+
)}
312+
/>
313+
</motion.div>
314+
);
315+
}
316+
317+
// Network Settings View
318+
return (
319+
<motion.div
320+
key="network"
321+
initial={{ opacity: 0 }}
322+
animate={{ opacity: 1 }}
323+
exit={{ opacity: 0 }}
324+
transition={{ duration: 0.15 }}
325+
className="w-[360px] p-4 space-y-4"
326+
>
327+
{/* Header */}
328+
<div className="flex items-center gap-3">
329+
<button
330+
type="button"
331+
onClick={() => setView('wallet')}
332+
className="rounded-full border border-border p-2 hover:bg-accent transition-colors"
333+
>
334+
<ChevronLeft className="h-4 w-4" />
335+
</button>
336+
<span className="font-semibold text-lg">Network Settings</span>
337+
</div>
338+
339+
{/* Network Options */}
340+
<ClusterElement
341+
render={({ cluster, clusters, setCluster }) => {
342+
const currentClusterId = (cluster as { id?: string })?.id || 'solana:mainnet';
343+
return (
344+
<div className="rounded-[12px] border bg-muted/50 overflow-hidden">
345+
{clusters.map((network, index) => {
346+
const isSelected = currentClusterId === network.id;
347+
return (
348+
<div
349+
key={network.id}
350+
role="button"
351+
tabIndex={0}
352+
onClick={() => setCluster(network.id)}
353+
onKeyDown={e => {
354+
if (e.key === 'Enter' || e.key === ' ') {
355+
e.preventDefault();
356+
setCluster(network.id);
357+
}
358+
}}
359+
className={`w-full flex items-center justify-between p-4 hover:bg-accent/50 transition-colors cursor-pointer ${
360+
index !== clusters.length - 1 ? 'border-b border-border' : ''
361+
}`}
362+
>
363+
<div className="flex items-center gap-2">
364+
<span
365+
className={`h-2 w-2 rounded-full ${clusterColors[network.id] || 'bg-purple-500'}`}
366+
/>
367+
<span className="font-medium">{network.label}</span>
368+
</div>
369+
<div
370+
className={`h-5 w-5 rounded-full border-2 flex items-center justify-center transition-colors ${
371+
isSelected ? 'bg-primary border-primary' : 'border-muted-foreground/30'
372+
}`}
373+
>
374+
{isSelected && <Check className="h-3 w-3 text-primary-foreground" />}
375+
</div>
376+
</div>
377+
);
378+
})}
379+
</div>
380+
);
381+
}}
322382
/>
323-
</div>
383+
</motion.div>
324384
);
325385
}

0 commit comments

Comments
 (0)