feat: Add comprehensive wallet UI with tabs for send/receive/transactions

Transform WalletViewer from connection manager to full-featured wallet:

**Tab-based Interface:**
- Overview: Balance display with quick action buttons
- Transactions: Payment history with incoming/outgoing indicators
- Send: Pay Lightning invoices (BOLT11) with optional amounts
- Receive: Generate Lightning invoices with amount and description
- Manage: Wallet connection management (previous functionality)

**Features:**
- Real-time balance updates for active wallet
- Transaction list with type, amount, description, and timestamps
- Send payments with invoice validation and success feedback
- Generate invoices with copy-to-clipboard functionality
- Empty states for no wallet/transactions
- Loading and error states for all operations
- Responsive design with proper spacing and typography

**UX Improvements:**
- Tabbed navigation with icons
- Large balance display on overview
- Color-coded transaction types (green=incoming, red=outgoing)
- Copy invoice button with toast notification
- "Generate new invoice" reset flow
- Disabled state for buttons during operations

This provides a complete Lightning wallet experience within Grimoire,
ready for integration with zaps and other payment features.
This commit is contained in:
Claude
2026-01-14 18:36:54 +00:00
parent 62ce34d9c5
commit 378696371b

View File

@@ -9,6 +9,13 @@ import {
Info,
AlertCircle,
Zap,
Send,
Download,
Settings,
ArrowUpRight,
ArrowDownLeft,
Copy,
Clock,
} from "lucide-react";
import walletManager from "@/services/wallet";
import type {
@@ -20,6 +27,7 @@ import { use$ } from "applesauce-react/hooks";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Textarea } from "./ui/textarea";
import {
Dialog,
DialogContent,
@@ -29,6 +37,7 @@ import {
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
export interface WalletViewerProps {
action: "view" | "connect";
@@ -37,14 +46,14 @@ export interface WalletViewerProps {
}
/**
* WalletViewer - Manage NIP-47 Nostr Wallet Connect connections
* WalletViewer - Full-featured Lightning wallet UI
*
* Features:
* - View all wallet connections
* - Add new NWC connections
* - View wallet balances
* - Set active wallet
* - Remove connections
* - Overview: Balance and quick actions
* - Transactions: Payment history
* - Send: Pay Lightning invoices
* - Receive: Generate Lightning invoices
* - Manage: Wallet connections
*/
function WalletViewer({ action, connectionURI, name }: WalletViewerProps) {
const [connections, setConnections] = useState<WalletConnectionInfo[]>([]);
@@ -53,6 +62,7 @@ function WalletViewer({ action, connectionURI, name }: WalletViewerProps) {
const [newConnectionURI, setNewConnectionURI] = useState(connectionURI || "");
const [newConnectionName, setNewConnectionName] = useState(name || "");
const [isAddingConnection, setIsAddingConnection] = useState(false);
const [activeTab, setActiveTab] = useState("overview");
// Load connections on mount and when action changes
useEffect(() => {
@@ -119,19 +129,21 @@ function WalletViewer({ action, connectionURI, name }: WalletViewerProps) {
}
};
return (
<div className="h-full w-full flex flex-col bg-background text-foreground">
{/* Header */}
<div className="border-b border-border px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<Wallet className="size-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Wallet Manager</h1>
</div>
// Show empty state if no connections
if (connections.length === 0) {
return (
<div className="h-full w-full flex flex-col bg-background text-foreground items-center justify-center p-8">
<Wallet className="size-16 mx-auto mb-4 opacity-50 text-muted-foreground" />
<h2 className="text-xl font-semibold mb-2">No Wallet Connected</h2>
<p className="text-muted-foreground text-sm mb-6 text-center max-w-md">
Connect a NIP-47 Nostr Wallet Connect wallet to send and receive
Lightning payments
</p>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
<Button>
<Plus className="size-4 mr-2" />
Add Connection
Add Wallet Connection
</Button>
</DialogTrigger>
<DialogContent>
@@ -182,26 +194,623 @@ function WalletViewer({ action, connectionURI, name }: WalletViewerProps) {
</DialogContent>
</Dialog>
</div>
);
}
{/* Wallet List */}
<div className="flex-1 overflow-y-auto">
{connections.length === 0 && (
<div className="text-center text-muted-foreground font-mono text-sm p-8">
<Wallet className="size-12 mx-auto mb-4 opacity-50" />
<p className="mb-2">No wallet connections</p>
<p className="text-xs">
Add a NIP-47 wallet connection to get started
</p>
return (
<div className="h-full w-full flex flex-col bg-background text-foreground">
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="h-full flex flex-col"
>
<div className="border-b border-border">
<TabsList className="w-full justify-start rounded-none border-b-0 bg-transparent p-0">
<TabsTrigger
value="overview"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary"
>
<Wallet className="size-4 mr-2" />
Overview
</TabsTrigger>
<TabsTrigger
value="transactions"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary"
>
<Clock className="size-4 mr-2" />
Transactions
</TabsTrigger>
<TabsTrigger
value="send"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary"
>
<Send className="size-4 mr-2" />
Send
</TabsTrigger>
<TabsTrigger
value="receive"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary"
>
<Download className="size-4 mr-2" />
Receive
</TabsTrigger>
<TabsTrigger
value="manage"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary"
>
<Settings className="size-4 mr-2" />
Manage
</TabsTrigger>
</TabsList>
</div>
<div className="flex-1 overflow-y-auto">
<TabsContent value="overview" className="m-0 h-full">
<OverviewTab activeWalletId={activeWalletId} />
</TabsContent>
<TabsContent value="transactions" className="m-0 h-full">
<TransactionsTab activeWalletId={activeWalletId} />
</TabsContent>
<TabsContent value="send" className="m-0 h-full">
<SendTab activeWalletId={activeWalletId} />
</TabsContent>
<TabsContent value="receive" className="m-0 h-full">
<ReceiveTab activeWalletId={activeWalletId} />
</TabsContent>
<TabsContent value="manage" className="m-0 h-full">
<ManageTab
connections={connections}
activeWalletId={activeWalletId}
onSetActive={handleSetActiveWallet}
onRemove={handleRemoveConnection}
onAddConnection={handleAddConnection}
isAddDialogOpen={isAddDialogOpen}
setIsAddDialogOpen={setIsAddDialogOpen}
newConnectionURI={newConnectionURI}
setNewConnectionURI={setNewConnectionURI}
newConnectionName={newConnectionName}
setNewConnectionName={setNewConnectionName}
isAddingConnection={isAddingConnection}
/>
</TabsContent>
</div>
</Tabs>
</div>
);
}
// Overview Tab - Balance and quick actions
function OverviewTab({ activeWalletId }: { activeWalletId?: string }) {
const [balance, setBalance] = useState<WalletBalance | null>(null);
const [info, setInfo] = useState<WalletInfo | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (activeWalletId) {
loadWalletData();
}
}, [activeWalletId]);
const loadWalletData = async () => {
setIsLoading(true);
setError(null);
try {
const [balanceData, infoData] = await Promise.all([
walletManager.getBalance(),
walletManager.getInfo(),
]);
setBalance(balanceData);
setInfo(infoData);
} catch (err) {
console.error("Failed to load wallet data:", err);
setError(
err instanceof Error ? err.message : "Failed to load wallet data",
);
} finally {
setIsLoading(false);
}
};
const formatBalance = (millisats: number) => {
const sats = Math.floor(millisats / 1000);
return sats.toLocaleString();
};
if (!activeWalletId) {
return (
<div className="flex items-center justify-center h-full text-muted-foreground">
No active wallet selected
</div>
);
}
return (
<div className="p-6 space-y-6">
{/* Balance Card */}
<div className="border rounded-lg p-6 bg-muted/30">
<div className="text-sm text-muted-foreground mb-2">Total Balance</div>
{isLoading ? (
<div className="flex items-center gap-2">
<Loader2 className="size-5 animate-spin text-muted-foreground" />
<span>Loading...</span>
</div>
) : error ? (
<div className="flex items-center gap-2 text-red-500">
<AlertCircle className="size-5" />
<span>{error}</span>
</div>
) : balance ? (
<div className="flex items-baseline gap-2">
<span className="text-4xl font-bold font-mono">
{formatBalance(balance.balance)}
</span>
<span className="text-xl text-muted-foreground">sats</span>
</div>
) : null}
{info && (
<div className="mt-4 text-xs text-muted-foreground space-y-1">
{info.alias && <div>Wallet: {info.alias}</div>}
{info.network && <div>Network: {info.network}</div>}
</div>
)}
</div>
{/* Quick Actions */}
<div className="grid grid-cols-2 gap-4">
<Button variant="outline" size="lg" className="h-auto py-4" disabled>
<Send className="size-5 mr-2" />
<div className="text-left">
<div className="font-semibold">Send</div>
<div className="text-xs text-muted-foreground">Pay invoice</div>
</div>
</Button>
<Button variant="outline" size="lg" className="h-auto py-4" disabled>
<Download className="size-5 mr-2" />
<div className="text-left">
<div className="font-semibold">Receive</div>
<div className="text-xs text-muted-foreground">Create invoice</div>
</div>
</Button>
</div>
</div>
);
}
// Transactions Tab - Payment history
function TransactionsTab({ activeWalletId }: { activeWalletId?: string }) {
const [transactions, setTransactions] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (activeWalletId) {
loadTransactions();
}
}, [activeWalletId]);
const loadTransactions = async () => {
setIsLoading(true);
setError(null);
try {
const txs = await walletManager.listTransactions();
setTransactions(txs);
} catch (err) {
console.error("Failed to load transactions:", err);
setError(
err instanceof Error ? err.message : "Failed to load transactions",
);
} finally {
setIsLoading(false);
}
};
const formatAmount = (millisats: number) => {
const sats = Math.floor(millisats / 1000);
return sats.toLocaleString();
};
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString();
};
if (!activeWalletId) {
return (
<div className="flex items-center justify-center h-full text-muted-foreground">
No active wallet selected
</div>
);
}
if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
<Loader2 className="size-8 animate-spin text-muted-foreground" />
</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-full gap-4">
<AlertCircle className="size-12 text-red-500" />
<p className="text-red-500">{error}</p>
<Button onClick={loadTransactions}>Retry</Button>
</div>
);
}
if (transactions.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<Clock className="size-12 mb-4 opacity-50" />
<p>No transactions yet</p>
</div>
);
}
return (
<div className="divide-y divide-border">
{transactions.map((tx, index) => (
<div
key={index}
className="px-4 py-3 hover:bg-muted/30 transition-colors"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
{tx.type === "incoming" ? (
<ArrowDownLeft className="size-4 text-green-500" />
) : (
<ArrowUpRight className="size-4 text-red-500" />
)}
<span className="font-semibold text-sm">
{tx.type === "incoming" ? "Received" : "Sent"}
</span>
</div>
{tx.description && (
<p className="text-sm text-muted-foreground truncate">
{tx.description}
</p>
)}
<p className="text-xs text-muted-foreground mt-1">
{formatDate(tx.settled_at || tx.created_at)}
</p>
</div>
<div className="text-right">
<div
className={`font-mono font-semibold ${tx.type === "incoming" ? "text-green-500" : "text-red-500"}`}
>
{tx.type === "incoming" ? "+" : "-"}
{formatAmount(tx.amount)} sats
</div>
</div>
</div>
</div>
))}
</div>
);
}
// Send Tab - Pay Lightning invoice
function SendTab({ activeWalletId }: { activeWalletId?: string }) {
const [invoice, setInvoice] = useState("");
const [amount, setAmount] = useState("");
const [isSending, setIsSending] = useState(false);
const handleSend = async () => {
if (!invoice) {
toast.error("Please enter a Lightning invoice");
return;
}
setIsSending(true);
try {
const result = await walletManager.payInvoice(invoice);
toast.success("Payment sent successfully!");
setInvoice("");
setAmount("");
console.log("Payment preimage:", result.preimage);
} catch (error) {
console.error("Failed to send payment:", error);
toast.error(
`Failed to send payment: ${error instanceof Error ? error.message : "Unknown error"}`,
);
} finally {
setIsSending(false);
}
};
if (!activeWalletId) {
return (
<div className="flex items-center justify-center h-full text-muted-foreground">
No active wallet selected
</div>
);
}
return (
<div className="p-6 max-w-2xl mx-auto space-y-6">
<div className="space-y-2">
<label className="text-sm font-medium">Lightning Invoice</label>
<Textarea
placeholder="lnbc..."
value={invoice}
onChange={(e) => setInvoice(e.target.value)}
disabled={isSending}
className="font-mono text-sm"
rows={4}
/>
<p className="text-xs text-muted-foreground">
Paste a Lightning invoice (BOLT11) to send payment
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">
Amount (optional, if not specified in invoice)
</label>
<Input
type="number"
placeholder="1000"
value={amount}
onChange={(e) => setAmount(e.target.value)}
disabled={isSending}
className="font-mono"
/>
<p className="text-xs text-muted-foreground">
Amount in satoshis (only for zero-amount invoices)
</p>
</div>
<Button
onClick={handleSend}
disabled={isSending || !invoice}
className="w-full"
size="lg"
>
{isSending ? (
<>
<Loader2 className="size-4 mr-2 animate-spin" />
Sending...
</>
) : (
<>
<Send className="size-4 mr-2" />
Send Payment
</>
)}
</Button>
</div>
);
}
// Receive Tab - Generate Lightning invoice
function ReceiveTab({ activeWalletId }: { activeWalletId?: string }) {
const [amount, setAmount] = useState("");
const [description, setDescription] = useState("");
const [invoice, setInvoice] = useState("");
const [isGenerating, setIsGenerating] = useState(false);
const handleGenerate = async () => {
const amountNum = parseInt(amount);
if (!amountNum || amountNum <= 0) {
toast.error("Please enter a valid amount");
return;
}
setIsGenerating(true);
try {
const result = await walletManager.makeInvoice(amountNum, description);
setInvoice(result.invoice);
toast.success("Invoice generated!");
} catch (error) {
console.error("Failed to generate invoice:", error);
toast.error(
`Failed to generate invoice: ${error instanceof Error ? error.message : "Unknown error"}`,
);
} finally {
setIsGenerating(false);
}
};
const handleCopy = () => {
navigator.clipboard.writeText(invoice);
toast.success("Invoice copied to clipboard!");
};
const handleReset = () => {
setInvoice("");
setAmount("");
setDescription("");
};
if (!activeWalletId) {
return (
<div className="flex items-center justify-center h-full text-muted-foreground">
No active wallet selected
</div>
);
}
if (invoice) {
return (
<div className="p-6 max-w-2xl mx-auto space-y-6">
<div className="border rounded-lg p-6 bg-muted/30 space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-semibold">Invoice Generated</h3>
<Zap className="size-5 text-yellow-500" />
</div>
<div className="p-4 bg-background rounded border font-mono text-sm break-all">
{invoice}
</div>
<Button onClick={handleCopy} className="w-full" variant="outline">
<Copy className="size-4 mr-2" />
Copy Invoice
</Button>
</div>
<Button onClick={handleReset} variant="outline" className="w-full">
Generate New Invoice
</Button>
</div>
);
}
return (
<div className="p-6 max-w-2xl mx-auto space-y-6">
<div className="space-y-2">
<label className="text-sm font-medium">Amount (sats) *</label>
<Input
type="number"
placeholder="1000"
value={amount}
onChange={(e) => setAmount(e.target.value)}
disabled={isGenerating}
className="font-mono"
/>
<p className="text-xs text-muted-foreground">
Amount to receive in satoshis
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Description (optional)</label>
<Input
placeholder="Payment for..."
value={description}
onChange={(e) => setDescription(e.target.value)}
disabled={isGenerating}
/>
<p className="text-xs text-muted-foreground">
Optional note for the payer
</p>
</div>
<Button
onClick={handleGenerate}
disabled={isGenerating || !amount}
className="w-full"
size="lg"
>
{isGenerating ? (
<>
<Loader2 className="size-4 mr-2 animate-spin" />
Generating...
</>
) : (
<>
<Download className="size-4 mr-2" />
Generate Invoice
</>
)}
</Button>
</div>
);
}
// Manage Tab - Wallet connections
interface ManageTabProps {
connections: WalletConnectionInfo[];
activeWalletId?: string;
onSetActive: (id: string) => void;
onRemove: (id: string) => void;
onAddConnection: (uri: string, name?: string) => void;
isAddDialogOpen: boolean;
setIsAddDialogOpen: (open: boolean) => void;
newConnectionURI: string;
setNewConnectionURI: (uri: string) => void;
newConnectionName: string;
setNewConnectionName: (name: string) => void;
isAddingConnection: boolean;
}
function ManageTab({
connections,
activeWalletId,
onSetActive,
onRemove,
onAddConnection,
isAddDialogOpen,
setIsAddDialogOpen,
newConnectionURI,
setNewConnectionURI,
newConnectionName,
setNewConnectionName,
isAddingConnection,
}: ManageTabProps) {
return (
<div>
<div className="p-4 border-b border-border flex items-center justify-between">
<h3 className="font-semibold">Wallet Connections</h3>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
<Plus className="size-4 mr-2" />
Add Connection
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Wallet Connection</DialogTitle>
<DialogDescription>
Connect a NIP-47 Nostr Wallet Connect wallet. Get a connection
URI from your wallet provider (e.g., Alby, Mutiny, etc.)
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">
Connection URI (required)
</label>
<Input
placeholder="nostr+walletconnect://..."
value={newConnectionURI}
onChange={(e) => setNewConnectionURI(e.target.value)}
disabled={isAddingConnection}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">
Wallet Name (optional)
</label>
<Input
placeholder="My Wallet"
value={newConnectionName}
onChange={(e) => setNewConnectionName(e.target.value)}
disabled={isAddingConnection}
/>
</div>
</div>
<DialogFooter>
<Button
onClick={() =>
onAddConnection(newConnectionURI, newConnectionName)
}
disabled={isAddingConnection || !newConnectionURI}
>
{isAddingConnection && (
<Loader2 className="size-4 mr-2 animate-spin" />
)}
Connect
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="divide-y divide-border">
{connections.map((connection) => (
<WalletCard
key={connection.id}
connection={connection}
isActive={activeWalletId === connection.id}
onSetActive={handleSetActiveWallet}
onRemove={handleRemoveConnection}
onSetActive={onSetActive}
onRemove={onRemove}
/>
))}
</div>
@@ -257,98 +866,92 @@ function WalletCard({
};
return (
<div className="border-b border-border">
<div className="px-4 py-3 flex flex-col gap-3">
{/* Main Row */}
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-sm truncate">
{connection.name}
</h3>
{isActive && (
<Tooltip>
<TooltipTrigger asChild>
<div className="cursor-help">
<Check className="size-4 text-green-500" />
</div>
</TooltipTrigger>
<TooltipContent>
<p>Active Wallet</p>
</TooltipContent>
</Tooltip>
)}
</div>
{/* Relays */}
<div className="text-xs text-muted-foreground font-mono truncate mb-2">
{connection.relays.join(", ")}
</div>
{/* Balance */}
<div className="flex items-center gap-2">
{isLoadingBalance ? (
<div className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="size-4 animate-spin" />
<span className="text-sm">Loading balance...</span>
</div>
) : error ? (
<div className="flex items-center gap-2 text-red-500">
<AlertCircle className="size-4" />
<span className="text-sm">{error}</span>
</div>
) : balance ? (
<div className="flex items-center gap-2">
<Zap className="size-4 text-yellow-500" />
<span className="font-mono font-semibold">
{formatBalance(balance.balance)} sats
</span>
</div>
) : null}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2 flex-shrink-0">
{info && (
<div className="px-4 py-3 hover:bg-muted/30 transition-colors">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-sm truncate">
{connection.name}
</h3>
{isActive && (
<Tooltip>
<TooltipTrigger asChild>
<button className="text-muted-foreground hover:text-foreground transition-colors">
<Info className="size-4" />
</button>
<div className="cursor-help">
<Check className="size-4 text-green-500" />
</div>
</TooltipTrigger>
<TooltipContent>
<div className="text-xs space-y-1">
{info.alias && <div>Alias: {info.alias}</div>}
{info.network && <div>Network: {info.network}</div>}
<div>Methods: {info.methods.join(", ")}</div>
</div>
<p>Active Wallet</p>
</TooltipContent>
</Tooltip>
)}
{!isActive && (
<Button
size="sm"
variant="ghost"
onClick={() => onSetActive(connection.id)}
>
Set Active
</Button>
)}
</div>
<div className="text-xs text-muted-foreground font-mono truncate mb-2">
{connection.relays.join(", ")}
</div>
<div className="flex items-center gap-2">
{isLoadingBalance ? (
<div className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="size-4 animate-spin" />
<span className="text-sm">Loading balance...</span>
</div>
) : error ? (
<div className="flex items-center gap-2 text-red-500">
<AlertCircle className="size-4" />
<span className="text-sm">{error}</span>
</div>
) : balance ? (
<div className="flex items-center gap-2">
<Zap className="size-4 text-yellow-500" />
<span className="font-mono font-semibold">
{formatBalance(balance.balance)} sats
</span>
</div>
) : null}
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{info && (
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onRemove(connection.id)}
className="text-muted-foreground hover:text-red-500 transition-colors"
>
<Trash2 className="size-4" />
<button className="text-muted-foreground hover:text-foreground transition-colors">
<Info className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Remove Connection</p>
<div className="text-xs space-y-1">
{info.alias && <div>Alias: {info.alias}</div>}
{info.network && <div>Network: {info.network}</div>}
<div>Methods: {info.methods.join(", ")}</div>
</div>
</TooltipContent>
</Tooltip>
</div>
)}
{!isActive && (
<Button
size="sm"
variant="ghost"
onClick={() => onSetActive(connection.id)}
>
Set Active
</Button>
)}
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onRemove(connection.id)}
className="text-muted-foreground hover:text-red-500 transition-colors"
>
<Trash2 className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Remove Connection</p>
</TooltipContent>
</Tooltip>
</div>
</div>
</div>